mirror of
https://github.com/frappe/books.git
synced 2024-12-23 03:19:01 +00:00
incr: update item docs with addItem
- start with barcode widget
This commit is contained in:
parent
cf4f8d368c
commit
7656476b6a
@ -157,6 +157,22 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canEdit() {
|
||||||
|
if (!this.schema.isSubmittable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.submitted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cancelled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
get canSave() {
|
get canSave() {
|
||||||
if (!!this.submitted) {
|
if (!!this.submitted) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -6,13 +6,11 @@ import {
|
|||||||
DefaultMap,
|
DefaultMap,
|
||||||
FiltersMap,
|
FiltersMap,
|
||||||
FormulaMap,
|
FormulaMap,
|
||||||
HiddenMap
|
HiddenMap,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
||||||
import { ValidationError } from 'fyo/utils/errors';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import {
|
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
|
||||||
getExchangeRate, getNumberSeries
|
|
||||||
} from 'models/helpers';
|
|
||||||
import { InventorySettings } from 'models/inventory/InventorySettings';
|
import { InventorySettings } from 'models/inventory/InventorySettings';
|
||||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||||
import { Transactional } from 'models/Transactional/Transactional';
|
import { Transactional } from 'models/Transactional/Transactional';
|
||||||
@ -695,4 +693,8 @@ export abstract class Invoice extends Transactional {
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addItem(name: string) {
|
||||||
|
return await addItem(name, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import {
|
|||||||
numberSeriesDefaultsMap,
|
numberSeriesDefaultsMap,
|
||||||
} from './baseModels/Defaults/Defaults';
|
} from './baseModels/Defaults/Defaults';
|
||||||
import { Invoice } from './baseModels/Invoice/Invoice';
|
import { Invoice } from './baseModels/Invoice/Invoice';
|
||||||
|
import { StockMovement } from './inventory/StockMovement';
|
||||||
|
import { StockTransfer } from './inventory/StockTransfer';
|
||||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
import { InvoiceStatus, ModelNameEnum } from './types';
|
||||||
|
|
||||||
export function getInvoiceActions(
|
export function getInvoiceActions(
|
||||||
@ -321,3 +323,27 @@ export function getDocStatusListColumn(): ColumnConfig {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModelsWithItems = Invoice | StockTransfer | StockMovement;
|
||||||
|
export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||||
|
if (!doc.canEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (doc.items ?? []) as NonNullable<M['items']>[number][];
|
||||||
|
|
||||||
|
let item = items.find((i) => i.item === name);
|
||||||
|
if (item) {
|
||||||
|
const q = item.quantity ?? 0;
|
||||||
|
await item.set('quantity', q + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await doc.append('items');
|
||||||
|
item = doc.items?.at(-1);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await item.set('item', name);
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
FormulaMap,
|
FormulaMap,
|
||||||
ListViewSettings,
|
ListViewSettings,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
import { getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
import { addItem, getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
||||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
@ -92,4 +92,8 @@ export class StockMovement extends Transfer {
|
|||||||
static getActions(fyo: Fyo): Action[] {
|
static getActions(fyo: Fyo): Action[] {
|
||||||
return [getLedgerLinkAction(fyo, true)];
|
return [getLedgerLinkAction(fyo, true)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addItem(name: string) {
|
||||||
|
return await addItem(name, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { Action, DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
|
|||||||
import { ValidationError } from 'fyo/utils/errors';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import { Defaults } from 'models/baseModels/Defaults/Defaults';
|
import { Defaults } from 'models/baseModels/Defaults/Defaults';
|
||||||
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
||||||
import { getLedgerLinkAction, getNumberSeries } from 'models/helpers';
|
import { addItem, getLedgerLinkAction, getNumberSeries } from 'models/helpers';
|
||||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
@ -232,4 +232,8 @@ export abstract class StockTransfer extends Transfer {
|
|||||||
role: doc.isSales ? 'Customer' : 'Supplier',
|
role: doc.isSales ? 'Customer' : 'Supplier',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async addItem(name: string) {
|
||||||
|
return await addItem(name, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
109
src/components/Controls/Barcode.vue
Normal file
109
src/components/Controls/Barcode.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Button :icon="true" @click="openModal = true">{{
|
||||||
|
t`Scan Barcode`
|
||||||
|
}}</Button>
|
||||||
|
<Modal :open-modal="openModal" @closemodal="openModal = false">
|
||||||
|
<FormHeader :form-title="t`Barcode Scanner`" />
|
||||||
|
<hr />
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="w-full flex flex-col justify-center items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="
|
||||||
|
border
|
||||||
|
w-96
|
||||||
|
rounded
|
||||||
|
text-base
|
||||||
|
px-3
|
||||||
|
py-2
|
||||||
|
placeholder-gray-600
|
||||||
|
bg-gray-50
|
||||||
|
focus-within:bg-gray-100
|
||||||
|
"
|
||||||
|
@change="(e) => getItem((e.target as HTMLInputElement)?.value)"
|
||||||
|
:placeholder="t`Enter barcode`"
|
||||||
|
/>
|
||||||
|
<div v-if="error" class="text-sm text-red-600 mt-4 w-96 text-center">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="success"
|
||||||
|
class="text-sm text-green-600 mt-4 w-96 text-center"
|
||||||
|
>
|
||||||
|
{{ success }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import Button from '../Button.vue';
|
||||||
|
import FormHeader from '../FormHeader.vue';
|
||||||
|
import Modal from '../Modal.vue';
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Button, Modal, FormHeader },
|
||||||
|
emits: ['item-selected'],
|
||||||
|
watch: {
|
||||||
|
openModal(value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
},
|
||||||
|
error(value: string) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.success = '';
|
||||||
|
},
|
||||||
|
success(value: string) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.error = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
openModal: false,
|
||||||
|
error: '',
|
||||||
|
success: '',
|
||||||
|
} as {
|
||||||
|
openModal: boolean;
|
||||||
|
error: string;
|
||||||
|
success: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clear() {
|
||||||
|
this.error = '';
|
||||||
|
this.success = '';
|
||||||
|
},
|
||||||
|
async getItem(code: string) {
|
||||||
|
const barcode = code.trim();
|
||||||
|
if (!/\d{12,}/.test(barcode)) {
|
||||||
|
return (this.error = this.t`Invalid barcode ${barcode}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (await this.fyo.db.getAll('Item', {
|
||||||
|
filters: { barcode },
|
||||||
|
fields: ['name'],
|
||||||
|
})) as { name: string }[];
|
||||||
|
|
||||||
|
const name = items?.[0]?.name;
|
||||||
|
if (!name) {
|
||||||
|
return (this.error = this.t`Item with barcode ${barcode} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.success = this.t`Quantity 1 of ${name} added.`;
|
||||||
|
this.$emit('item-selected', name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -21,7 +21,6 @@
|
|||||||
bg-white
|
bg-white
|
||||||
rounded-lg
|
rounded-lg
|
||||||
shadow-2xl
|
shadow-2xl
|
||||||
w-form
|
|
||||||
border
|
border
|
||||||
overflow-hidden
|
overflow-hidden
|
||||||
inner
|
inner
|
||||||
@ -44,10 +43,6 @@ export default defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
setCloseListener: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ['closemodal'],
|
emits: ['closemodal'],
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
:set-close-listener="false"
|
:set-close-listener="false"
|
||||||
>
|
>
|
||||||
<!-- Search Input -->
|
<!-- Search Input -->
|
||||||
<div class="p-1">
|
<div class="p-1 w-form">
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
type="search"
|
type="search"
|
||||||
|
@ -167,7 +167,7 @@
|
|||||||
|
|
||||||
<!-- Base Count Selection when Dev -->
|
<!-- Base Count Selection when Dev -->
|
||||||
<Modal :open-modal="openModal" @closemodal="openModal = false">
|
<Modal :open-modal="openModal" @closemodal="openModal = false">
|
||||||
<div class="p-4 text-gray-900">
|
<div class="p-4 text-gray-900 w-form">
|
||||||
<h2 class="text-xl font-semibold select-none">Set Base Count</h2>
|
<h2 class="text-xl font-semibold select-none">Set Base Count</h2>
|
||||||
<p class="text-base mt-2">
|
<p class="text-base mt-2">
|
||||||
Base Count is a lower bound on the number of entries made when
|
Base Count is a lower bound on the number of entries made when
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
<!-- Page Header (Title, Buttons, etc) -->
|
<!-- Page Header (Title, Buttons, etc) -->
|
||||||
<template #header v-if="doc">
|
<template #header v-if="doc">
|
||||||
<StatusBadge :status="status" />
|
<StatusBadge :status="status" />
|
||||||
|
<Barcode
|
||||||
|
v-if="showBarcode"
|
||||||
|
@item-selected="(name) => doc.addItem(name)"
|
||||||
|
/>
|
||||||
<DropdownWithActions
|
<DropdownWithActions
|
||||||
v-for="group of groupedActions"
|
v-for="group of groupedActions"
|
||||||
:key="group.label"
|
:key="group.label"
|
||||||
@ -145,7 +149,9 @@
|
|||||||
import { computed } from '@vue/reactivity';
|
import { computed } from '@vue/reactivity';
|
||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { getDocStatus } from 'models/helpers';
|
import { getDocStatus } from 'models/helpers';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
import Button from 'src/components/Button.vue';
|
import Button from 'src/components/Button.vue';
|
||||||
|
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
import Table from 'src/components/Controls/Table.vue';
|
import Table from 'src/components/Controls/Table.vue';
|
||||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||||
@ -176,6 +182,7 @@ export default {
|
|||||||
FormContainer,
|
FormContainer,
|
||||||
QuickEditForm,
|
QuickEditForm,
|
||||||
FormHeader,
|
FormHeader,
|
||||||
|
Barcode,
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
@ -204,6 +211,25 @@ export default {
|
|||||||
groupedActions() {
|
groupedActions() {
|
||||||
return getGroupedActionsForDoc(this.doc);
|
return getGroupedActionsForDoc(this.doc);
|
||||||
},
|
},
|
||||||
|
showBarcode() {
|
||||||
|
if (!this.doc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.doc.canEdit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fyo.singles.InventorySettings?.enableBarcodes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
ModelNameEnum.Shipment,
|
||||||
|
ModelNameEnum.PurchaseReceipt,
|
||||||
|
ModelNameEnum.StockMovement,
|
||||||
|
].includes(this.schemaName);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
docsPath.value = docsPathMap[this.schemaName];
|
docsPath.value = docsPathMap[this.schemaName];
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<Barcode
|
||||||
|
v-if="doc.canEdit && fyo.singles.InventorySettings?.enableBarcodes"
|
||||||
|
@item-selected="(name) => doc.addItem(name)"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="!doc.isCancelled && !doc.dirty"
|
v-if="!doc.isCancelled && !doc.dirty"
|
||||||
:icon="true"
|
:icon="true"
|
||||||
@ -298,6 +302,7 @@ import { computed } from '@vue/reactivity';
|
|||||||
import { getDocStatus } from 'models/helpers';
|
import { getDocStatus } from 'models/helpers';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import Button from 'src/components/Button.vue';
|
import Button from 'src/components/Button.vue';
|
||||||
|
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||||
import ExchangeRate from 'src/components/Controls/ExchangeRate.vue';
|
import ExchangeRate from 'src/components/Controls/ExchangeRate.vue';
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
import Table from 'src/components/Controls/Table.vue';
|
import Table from 'src/components/Controls/Table.vue';
|
||||||
@ -332,6 +337,7 @@ export default {
|
|||||||
ExchangeRate,
|
ExchangeRate,
|
||||||
FormHeader,
|
FormHeader,
|
||||||
LinkedEntryWidget,
|
LinkedEntryWidget,
|
||||||
|
Barcode,
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
/>
|
/>
|
||||||
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
|
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
|
||||||
<ExportWizard
|
<ExportWizard
|
||||||
|
class="w-form"
|
||||||
:schema-name="schemaName"
|
:schema-name="schemaName"
|
||||||
:title="pageTitle"
|
:title="pageTitle"
|
||||||
:list-filters="listFilters"
|
:list-filters="listFilters"
|
||||||
|
Loading…
Reference in New Issue
Block a user