mirror of
https://github.com/frappe/books.git
synced 2025-01-23 15:18:24 +00:00
Merge pull request #1072 from AbleKSaju/feat-weight-enabled-barcode
feat: weight enabled barcode
This commit is contained in:
commit
3838c9d1b6
@ -1,5 +1,5 @@
|
|||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { FiltersMap } from 'fyo/model/types';
|
import { FiltersMap, HiddenMap } from 'fyo/model/types';
|
||||||
import {
|
import {
|
||||||
AccountRootTypeEnum,
|
AccountRootTypeEnum,
|
||||||
AccountTypeEnum,
|
AccountTypeEnum,
|
||||||
@ -10,6 +10,11 @@ export class POSSettings extends Doc {
|
|||||||
inventory?: string;
|
inventory?: string;
|
||||||
cashAccount?: string;
|
cashAccount?: string;
|
||||||
writeOffAccount?: string;
|
writeOffAccount?: string;
|
||||||
|
weightEnabledBarcode?: boolean;
|
||||||
|
checkDigits?: number;
|
||||||
|
itemCodeDigits?: number;
|
||||||
|
itemWeightDigits?: number;
|
||||||
|
|
||||||
posUI?: 'Classic' | 'Modern';
|
posUI?: 'Classic' | 'Modern';
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
@ -19,4 +24,12 @@ export class POSSettings extends Doc {
|
|||||||
isGroup: false,
|
isGroup: false,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hidden: HiddenMap = {
|
||||||
|
weightEnabledBarcode: () =>
|
||||||
|
!this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||||
|
checkDigits: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||||
|
itemCodeDigits: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||||
|
itemWeightDigits: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,34 @@
|
|||||||
"default": "Classic",
|
"default": "Classic",
|
||||||
"required": true,
|
"required": true,
|
||||||
"section": "Default"
|
"section": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "weightEnabledBarcode",
|
||||||
|
"label": "Weigth Enabled Barcode",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": false,
|
||||||
|
"section": "Barcode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "checkDigits",
|
||||||
|
"label": "Check Digits",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"default": 0,
|
||||||
|
"section": "Barcode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "itemCodeDigits",
|
||||||
|
"label": "Item Code Digits",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"default": 0,
|
||||||
|
"section": "Barcode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "itemWeightDigits",
|
||||||
|
"label": "item Weight Digits",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"default": 0,
|
||||||
|
"section": "Barcode"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
border
|
border
|
||||||
rounded
|
rounded
|
||||||
bg-gray-50
|
bg-gray-50
|
||||||
dark:border-gray-800 dark:bg-gray-890 dark:focus-within:bg-gray-900
|
dark:text-gray-200
|
||||||
|
dark:border-gray-800
|
||||||
|
dark:bg-gray-890
|
||||||
|
dark:focus-within:bg-gray-900
|
||||||
focus-within:bg-gray-100
|
focus-within:bg-gray-100
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -84,6 +87,7 @@ export default defineComponent({
|
|||||||
})) as { name: string }[];
|
})) as { name: string }[];
|
||||||
|
|
||||||
const name = items?.[0]?.name;
|
const name = items?.[0]?.name;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return this.error(this.t`Item with barcode ${barcode} not found.`);
|
return this.error(this.t`Item with barcode ${barcode} not found.`);
|
||||||
}
|
}
|
||||||
|
203
src/components/Controls/WeightEnabledBarcode.vue
Normal file
203
src/components/Controls/WeightEnabledBarcode.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
px-2
|
||||||
|
w-36
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
border
|
||||||
|
rounded
|
||||||
|
bg-gray-50
|
||||||
|
dark:text-gray-200
|
||||||
|
dark:border-gray-800
|
||||||
|
dark:bg-gray-890
|
||||||
|
dark:focus-within:bg-gray-900
|
||||||
|
focus-within:bg-gray-100
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="scanner"
|
||||||
|
type="text"
|
||||||
|
class="text-base placeholder-gray-600 w-full bg-transparent outline-none"
|
||||||
|
:placeholder="t`Enter weight barcode`"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<feather-icon
|
||||||
|
name="maximize"
|
||||||
|
class="w-3 h-3 text-gray-600 dark:text-gray-400 cursor-text"
|
||||||
|
@click="() => ($refs.scanner as HTMLInputElement).focus()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { showToast } from 'src/utils/interactive';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'WeightEnabledBarcode',
|
||||||
|
emits: ['item-selected'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timerId: null,
|
||||||
|
barcode: '',
|
||||||
|
cooldown: '',
|
||||||
|
} as {
|
||||||
|
timerId: null | ReturnType<typeof setTimeout>;
|
||||||
|
barcode: string;
|
||||||
|
cooldown: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('keydown', this.scanListener);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
document.removeEventListener('keydown', this.scanListener);
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
document.addEventListener('keydown', this.scanListener);
|
||||||
|
},
|
||||||
|
deactivated() {
|
||||||
|
document.removeEventListener('keydown', this.scanListener);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleChange(e: Event) {
|
||||||
|
const elem = e.target as HTMLInputElement;
|
||||||
|
this.selectItem(elem.value);
|
||||||
|
elem.value = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async selectItem(code: string) {
|
||||||
|
const barcode = code.trim();
|
||||||
|
if (this.cooldown === barcode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cooldown = barcode;
|
||||||
|
setTimeout(() => (this.cooldown = ''), 100);
|
||||||
|
|
||||||
|
const isWeightEnabled =
|
||||||
|
this.fyo.singles.POSSettings?.weightEnabledBarcode;
|
||||||
|
const checkDigits = this.fyo.singles.POSSettings?.checkDigits as number;
|
||||||
|
const itemCodeDigits = this.fyo.singles.POSSettings
|
||||||
|
?.itemCodeDigits as number;
|
||||||
|
const itemWeightDigits = this.fyo.singles.POSSettings
|
||||||
|
?.itemWeightDigits as number;
|
||||||
|
|
||||||
|
if (code.length !== checkDigits + itemCodeDigits + itemWeightDigits) {
|
||||||
|
return this.error(this.t`Barcode ${barcode} has an invalid length.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: Record<string, string> = isWeightEnabled
|
||||||
|
? {
|
||||||
|
barcode: barcode.slice(checkDigits, checkDigits + itemCodeDigits),
|
||||||
|
}
|
||||||
|
: { barcode };
|
||||||
|
const fields = isWeightEnabled
|
||||||
|
? ['name', 'unit', 'trackItem']
|
||||||
|
: ['name', 'trackItem'];
|
||||||
|
|
||||||
|
const items =
|
||||||
|
(await this.fyo.db.getAll('Item', { filters, fields })) || [];
|
||||||
|
const { name, unit, trackItem } = items[0] || {};
|
||||||
|
|
||||||
|
if (!trackItem) {
|
||||||
|
return this.error(
|
||||||
|
this.t`Item ${name as string} is not an Inventory Item.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return this.error(this.t`Item with barcode ${barcode} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = isWeightEnabled
|
||||||
|
? this.parseBarcode(
|
||||||
|
barcode,
|
||||||
|
unit as string,
|
||||||
|
checkDigits + itemCodeDigits
|
||||||
|
)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
this.success(this.t`${name as string} quantity ${quantity} added.`);
|
||||||
|
this.$emit('item-selected', name, quantity);
|
||||||
|
},
|
||||||
|
|
||||||
|
parseBarcode(barcode: string, unitType: string, sliceDigit: number) {
|
||||||
|
const weightRaw = parseInt(barcode.slice(sliceDigit));
|
||||||
|
|
||||||
|
let itemQuantity = 0;
|
||||||
|
|
||||||
|
switch (unitType) {
|
||||||
|
case 'Kg':
|
||||||
|
itemQuantity = Math.floor(weightRaw / 1000);
|
||||||
|
break;
|
||||||
|
case 'Gram':
|
||||||
|
itemQuantity = weightRaw;
|
||||||
|
break;
|
||||||
|
case 'Unit':
|
||||||
|
case 'Meter':
|
||||||
|
case 'Hour':
|
||||||
|
case 'Day':
|
||||||
|
itemQuantity = weightRaw;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown unit type!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemQuantity;
|
||||||
|
},
|
||||||
|
async scanListener({ key, code }: KeyboardEvent) {
|
||||||
|
/**
|
||||||
|
* Based under the assumption that
|
||||||
|
* - Barcode scanners trigger keydown events
|
||||||
|
* - Keydown events are triggered quicker than human can
|
||||||
|
* i.e. at max 20ms between events
|
||||||
|
* - Keydown events are triggered for barcode digits
|
||||||
|
* - The sequence of digits might be punctuated by a return
|
||||||
|
*/
|
||||||
|
|
||||||
|
const keyCode = Number(key);
|
||||||
|
const isEnter = code === 'Enter';
|
||||||
|
if (Number.isNaN(keyCode) && !isEnter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnter) {
|
||||||
|
return await this.setItemFromBarcode();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearInterval();
|
||||||
|
|
||||||
|
this.barcode += key;
|
||||||
|
this.timerId = setTimeout(async () => {
|
||||||
|
await this.setItemFromBarcode();
|
||||||
|
this.barcode = '';
|
||||||
|
}, 20);
|
||||||
|
},
|
||||||
|
async setItemFromBarcode() {
|
||||||
|
if (this.barcode.length < 12) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.selectItem(this.barcode);
|
||||||
|
|
||||||
|
this.barcode = '';
|
||||||
|
this.clearInterval();
|
||||||
|
},
|
||||||
|
clearInterval() {
|
||||||
|
if (this.timerId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
},
|
||||||
|
error(message: string) {
|
||||||
|
showToast({ type: 'error', message });
|
||||||
|
},
|
||||||
|
success(message: string) {
|
||||||
|
showToast({ type: 'success', message });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -98,7 +98,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Barcode
|
<Barcode
|
||||||
v-if="fyo.singles.InventorySettings?.enableBarcodes"
|
v-if="
|
||||||
|
fyo.singles.InventorySettings?.enableBarcodes &&
|
||||||
|
!fyo.singles.POSSettings?.weightEnabledBarcode
|
||||||
|
"
|
||||||
class="w-1/3"
|
class="w-1/3"
|
||||||
@item-selected="
|
@item-selected="
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
@ -106,6 +109,16 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WeightEnabledBarcode
|
||||||
|
v-if="fyo.singles.POSSettings?.weightEnabledBarcode"
|
||||||
|
class="w-1/3"
|
||||||
|
@item-selected="
|
||||||
|
async (name: string,qty:number) => {
|
||||||
|
emitEvent('addItem', await getItem(name) as Item,qty as number);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ItemsTable
|
<ItemsTable
|
||||||
@ -313,6 +326,7 @@ import ItemsTable from 'src/components/POS/Classic/ItemsTable.vue';
|
|||||||
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
|
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
|
||||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||||
import SelectedItemTable from 'src/components/POS/Classic/SelectedItemTable.vue';
|
import SelectedItemTable from 'src/components/POS/Classic/SelectedItemTable.vue';
|
||||||
|
import WeightEnabledBarcode from 'src/components/Controls/WeightEnabledBarcode.vue';
|
||||||
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
||||||
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
||||||
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||||
@ -336,6 +350,7 @@ export default defineComponent({
|
|||||||
SavedInvoiceModal,
|
SavedInvoiceModal,
|
||||||
ClosePOSShiftModal,
|
ClosePOSShiftModal,
|
||||||
LoyaltyProgramModal,
|
LoyaltyProgramModal,
|
||||||
|
WeightEnabledBarcode,
|
||||||
FloatingLabelFloatInput,
|
FloatingLabelFloatInput,
|
||||||
FloatingLabelCurrencyInput,
|
FloatingLabelCurrencyInput,
|
||||||
},
|
},
|
||||||
@ -414,7 +429,7 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
emitEvent(
|
emitEvent(
|
||||||
eventName: PosEmits,
|
eventName: PosEmits,
|
||||||
...args: (string | boolean | Item | Money)[]
|
...args: (string | boolean | Item | number | Money)[]
|
||||||
) {
|
) {
|
||||||
this.$emit(eventName, ...args);
|
this.$emit(eventName, ...args);
|
||||||
},
|
},
|
||||||
|
@ -255,7 +255,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Barcode
|
<Barcode
|
||||||
v-if="fyo.singles.InventorySettings?.enableBarcodes"
|
v-if="
|
||||||
|
fyo.singles.InventorySettings?.enableBarcodes &&
|
||||||
|
!fyo.singles.POSSettings?.weightEnabledBarcode
|
||||||
|
"
|
||||||
class="w-1/3"
|
class="w-1/3"
|
||||||
@item-selected="
|
@item-selected="
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
@ -263,6 +266,16 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WeightEnabledBarcode
|
||||||
|
v-if="fyo.singles.POSSettings?.weightEnabledBarcode"
|
||||||
|
class="w-1/3"
|
||||||
|
@item-selected="
|
||||||
|
async (name: string,qty:number) => {
|
||||||
|
emitEvent('addItem', await getItem(name) as Item,qty as number);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModernPOSItemsTable
|
<ModernPOSItemsTable
|
||||||
@ -321,6 +334,7 @@ import { POSItem, PosEmits, ItemQtyMap } from 'src/components/POS/types';
|
|||||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||||
import ModernPOSItemsGrid from 'src/components/POS/Modern/ModernPOSItemsGrid.vue';
|
import ModernPOSItemsGrid from 'src/components/POS/Modern/ModernPOSItemsGrid.vue';
|
||||||
import ModernPOSItemsTable from 'src/components/POS/Modern/ModernPOSItemsTable.vue';
|
import ModernPOSItemsTable from 'src/components/POS/Modern/ModernPOSItemsTable.vue';
|
||||||
|
import WeightEnabledBarcode from 'src/components/Controls/WeightEnabledBarcode.vue';
|
||||||
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
||||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||||
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
||||||
@ -346,6 +360,7 @@ export default defineComponent({
|
|||||||
ClosePOSShiftModal,
|
ClosePOSShiftModal,
|
||||||
LoyaltyProgramModal,
|
LoyaltyProgramModal,
|
||||||
ModernPOSItemsTable,
|
ModernPOSItemsTable,
|
||||||
|
WeightEnabledBarcode,
|
||||||
FloatingLabelFloatInput,
|
FloatingLabelFloatInput,
|
||||||
FloatingLabelCurrencyInput,
|
FloatingLabelCurrencyInput,
|
||||||
ModernPOSSelectedItemTable,
|
ModernPOSSelectedItemTable,
|
||||||
@ -430,7 +445,7 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
emitEvent(
|
emitEvent(
|
||||||
eventName: PosEmits,
|
eventName: PosEmits,
|
||||||
...args: (string | boolean | Item | Money)[]
|
...args: (string | boolean | Item | number | Money)[]
|
||||||
) {
|
) {
|
||||||
this.$emit(eventName, ...args);
|
this.$emit(eventName, ...args);
|
||||||
},
|
},
|
||||||
|
@ -477,7 +477,7 @@ export default defineComponent({
|
|||||||
this.transferRefNo = ref;
|
this.transferRefNo = ref;
|
||||||
},
|
},
|
||||||
|
|
||||||
async addItem(item: POSItem | Item | undefined) {
|
async addItem(item: POSItem | Item | undefined, quantity?: number) {
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@ -500,7 +500,9 @@ export default defineComponent({
|
|||||||
] ?? 0;
|
] ?? 0;
|
||||||
|
|
||||||
if (itemQty < qtyInBatch) {
|
if (itemQty < qtyInBatch) {
|
||||||
invItem.quantity = (invItem.quantity as number) + 1;
|
invItem.quantity = quantity
|
||||||
|
? (invItem.quantity as number) + quantity
|
||||||
|
: (invItem.quantity as number) + 1;
|
||||||
invItem.rate = item.rate as Money;
|
invItem.rate = item.rate as Money;
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
@ -528,7 +530,9 @@ export default defineComponent({
|
|||||||
existingItems[0].rate = item.rate as Money;
|
existingItems[0].rate = item.rate as Money;
|
||||||
}
|
}
|
||||||
|
|
||||||
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
|
existingItems[0].quantity = quantity
|
||||||
|
? (existingItems[0].quantity as number) + quantity
|
||||||
|
: (existingItems[0].quantity as number) + 1;
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
@ -550,6 +554,7 @@ export default defineComponent({
|
|||||||
await this.sinvDoc.append('items', {
|
await this.sinvDoc.append('items', {
|
||||||
rate: item.rate as Money,
|
rate: item.rate as Money,
|
||||||
item: item.name,
|
item: item.name,
|
||||||
|
quantity: quantity ? quantity : 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.sinvDoc.priceList) {
|
if (this.sinvDoc.priceList) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user