Merge pull request #1330 from osama1998H/feature/pos-quick-qty-q-hold

update quantity in POS page using shortcut
This commit is contained in:
Able k Saju
2025-09-10 16:13:44 +05:30
committed by GitHub
15 changed files with 925 additions and 682 deletions
+1 -1
View File
@@ -182,7 +182,7 @@
},
{
"fieldname": "posPrintTemplate",
"label": "Pos Print Template",
"label": "POS Print Template",
"fieldtype": "Link",
"target": "PrintTemplate",
"section": "Print Templates"
+1 -1
View File
@@ -30,7 +30,7 @@
},
{
"fieldname": "posPrintTemplate",
"label": "Pos Print Template",
"label": "POS Print Template",
"fieldtype": "Link",
"target": "PrintTemplate"
},
@@ -5,7 +5,7 @@
@click="isExapanded = !isExapanded"
/>
<div class="relative">
<div class="relative" @click="emitSelectedRow">
<Link
class="pt-2"
:df="{
@@ -27,7 +27,7 @@
</p>
</div>
<div class="flex items-center">
<div class="flex items-center" @click="emitSelectedRow">
<Int
:df="{
fieldname: 'quantity',
@@ -82,7 +82,7 @@
:df="{
fieldtype: 'Currency',
fieldname: 'amount',
label: 'Amount',
label: t`Amount`,
}"
size="small"
:border="false"
@@ -101,7 +101,7 @@
<div></div>
<template v-if="isExapanded">
<div class="px-4 pt-6 col-span-1">
<div class="px-4 pt-6 col-span-1" @click="emitSelectedRow">
<Int
v-if="isUOMConversionEnabled"
:df="{
@@ -287,7 +287,7 @@ export default defineComponent({
props: {
row: { type: SalesInvoiceItem, required: true },
},
emits: ['runSinvFormulas', 'applyPricingRule'],
emits: ['runSinvFormulas', 'applyPricingRule', 'selectedRow'],
setup() {
return {
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
@@ -344,6 +344,9 @@ export default defineComponent({
},
methods: {
emitSelectedRow() {
this.$emit('selectedRow', this.row);
},
adjustQuantity(change: number) {
let currentQuantity = this.row.quantity ?? 1;
let newQuantity = currentQuantity + change;
@@ -55,6 +55,7 @@
:row="(row as SalesInvoiceItem)"
@run-sinv-formulas="runSinvFormulas"
@apply-pricing-rule="$emit('applyPricingRule')"
@selected-row="selectedItemRow"
/>
</Row>
</div>
@@ -94,7 +95,7 @@ export default defineComponent({
isExapanded: false,
};
},
emits: ['applyPricingRule'],
emits: ['applyPricingRule', 'selectedRow'],
computed: {
ratio() {
return [0.1, 0.9, 0.8, 0.8, 0.8, 0.8, 0.2];
@@ -158,6 +159,9 @@ export default defineComponent({
async runSinvFormulas() {
await this.sinvDoc.runFormulas();
},
selectedItemRow(row: SalesInvoiceItem) {
this.$emit('selectedRow', row);
},
isNumeric,
},
});
@@ -125,6 +125,7 @@
import FormControl from 'src/components/Controls/FormControl.vue';
import Row from 'src/components/Row.vue';
import { isNumeric } from 'src/utils';
import { t } from 'fyo';
import { defineComponent } from 'vue';
import { Field } from 'schemas/types';
import { POSItem } from '../types';
@@ -146,27 +147,27 @@ export default defineComponent({
{
fieldname: 'name',
fieldtype: 'Data',
label: 'Item',
label: t`Item`,
placeholder: 'Item',
readOnly: true,
},
{
fieldname: 'rate',
label: 'Rate',
label: t`Rate`,
placeholder: 'Rate',
fieldtype: 'Currency',
readOnly: true,
},
{
fieldname: 'availableQty',
label: 'Qty',
label: t`Qty`,
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
},
{
fieldname: 'unit',
label: 'Unit',
label: t`Unit`,
placeholder: 'Unit',
fieldtype: 'Data',
target: 'UOM',
@@ -12,7 +12,7 @@
:df="{
fieldname: 'item',
fieldtype: 'Data',
label: 'item',
label: t`Item`,
}"
:class="row.isFreeItem ? 'mt-2' : ''"
size="small"
@@ -33,7 +33,7 @@
:df="{
fieldname: 'quantity',
fieldtype: 'Int',
label: 'Quantity',
label: t`Quantity`,
}"
size="small"
:border="false"
@@ -45,7 +45,7 @@
:df="{
fieldtype: 'Currency',
fieldname: 'rate',
label: 'rate',
label: t`Rate`,
}"
size="small"
:border="false"
@@ -57,7 +57,7 @@
:df="{
fieldtype: 'Currency',
fieldname: 'amount',
label: 'Amount',
label: t`Amount`,
}"
size="small"
:border="false"
@@ -82,7 +82,7 @@
:df="{
fieldname: 'quantity',
fieldtype: 'Float',
label: 'Quantity',
label: t`Quantity`,
}"
@click="handleOpenKeyboard(row, 'quantity')"
size="medium"
@@ -117,7 +117,7 @@
:df="{
fieldtype: 'Int',
fieldname: 'transferQuantity',
label: 'Transfer Quantity',
label: t`Transfer Quantity`,
}"
@click="!isReadOnly && handleOpenKeyboard(row, 'transferQuantity')"
size="medium"
@@ -132,7 +132,7 @@
:df="{
fieldtype: 'Currency',
fieldname: 'rate',
label: 'Rate',
label: t`Rate`,
}"
@click="!isReadOnly && handleOpenKeyboard(row, 'rate')"
size="medium"
@@ -166,7 +166,7 @@
:df="{
fieldtype: 'Float',
fieldname: 'itemDiscountPercent',
label: 'Discount Percent',
label: t`Discount Percent`,
}"
@click="handleOpenKeyboard(row, 'itemDiscountPercent')"
size="medium"
@@ -58,6 +58,7 @@ import Row from 'src/components/Row.vue';
import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
import ModernPOSSelectedItemRow from './ModernPOSSelectedItemRow.vue';
import { isNumeric } from 'src/utils';
import { t } from 'fyo';
import { inject, defineComponent } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
@@ -98,14 +99,14 @@ export default defineComponent({
{
fieldname: 'item',
fieldtype: 'Link',
label: 'Item',
label: t`Item`,
placeholder: 'Item',
required: true,
schemaName: 'Item',
},
{
fieldname: 'quantity',
label: 'Quantity',
label: t`Quantity`,
placeholder: 'Quantity',
fieldtype: 'Int',
required: true,
@@ -113,7 +114,7 @@ export default defineComponent({
},
{
fieldname: 'rate',
label: 'Rate',
label: t`Rate`,
placeholder: 'Rate',
fieldtype: 'Currency',
required: true,
@@ -121,7 +122,7 @@ export default defineComponent({
},
{
fieldname: 'amount',
label: 'Amount',
label: t`Amount`,
placeholder: 'Amount',
fieldtype: 'Currency',
required: true,
+4
View File
@@ -216,6 +216,10 @@ export default defineComponent({
shortcut: [ShortcutKey.shift, 'P'],
description: t`Set Price List`,
},
{
shortcut: ['Q', '0-9'],
description: t`Hold Q and type digits to set selected item quantity`,
},
{
shortcut: [ShortcutKey.pmod, ShortcutKey.shift, 'H'],
description: t`Open Saved or Submitted Invoice List.`,
+2
View File
@@ -180,6 +180,7 @@
<SelectedItemTable
@apply-pricing-rule="emitEvent('applyPricingRule')"
@selected-row="(row) => $emit('selectedRow', row)"
/>
</div>
@@ -503,6 +504,7 @@ export default defineComponent({
'setTransferClearanceDate',
'saveAndContinue',
'handlePaymentAction',
'selectedRow',
],
data() {
return {
+2 -2
View File
@@ -1,11 +1,11 @@
<template>
<Modal :open-modal="openModal" class="w-3/6 p-4">
<h1 class="text-xl font-semibold text-center dark:text-gray-100 pb-4">
Close POS Shift
{{ t`Close POS Shift` }}
</h1>
<h2 class="mt-4 mb-2 text-lg font-medium dark:text-gray-100">
Closing Cash
{{ t`Closing Cash` }}
</h2>
<Table
v-if="isValuesSeeded"
+3
View File
@@ -499,6 +499,7 @@ export default defineComponent({
'selectedReturnInvoice',
'setTransferClearanceDate',
'saveAndContinue',
'selectedRow',
],
data() {
return {
@@ -524,6 +525,8 @@ export default defineComponent({
selectedRow(row: SalesInvoiceItem, field: string) {
this.selectedItemRow = row;
this.selectedItemField = field;
// Bubble up to POS to allow keyboard shortcuts to target this row
this.$emit('selectedRow', row);
},
getItem,
},
+5 -3
View File
@@ -1,13 +1,13 @@
<template>
<Modal class="w-3/6 p-4">
<h1 class="text-xl font-semibold text-center dark:text-gray-100 pb-4">
Open POS Shift
{{ t`Open POS Shift` }}
</h1>
<div class="grid grid-cols-12 gap-6">
<div class="col-span-6">
<h2 class="text-lg font-medium dark:text-gray-100">
Cash In Denominations
{{ t`Cash In Denominations` }}
</h2>
<Table
@@ -22,7 +22,9 @@
</div>
<div class="col-span-6">
<h2 class="text-lg font-medium dark:text-gray-100">Opening Amount</h2>
<h2 class="text-lg font-medium dark:text-gray-100">
{{ t`Opening Amount` }}
</h2>
<Table
v-if="isValuesSeeded"
+179
View File
@@ -63,6 +63,7 @@
@set-transfer-clearance-date="setTransferClearanceDate"
@save-and-continue="handleSaveAndContinue"
@handle-payment-action="handlePaymentAction"
@selected-row="setQuickQtySelectedRow"
/>
<ModernPOS
v-else
@@ -113,6 +114,7 @@
@selected-return-invoice="selectedReturnInvoice"
@save-and-continue="handleSaveAndContinue"
@set-transfer-clearance-date="setTransferClearanceDate"
@selected-row="setQuickQtySelectedRow"
/>
</div>
</template>
@@ -240,6 +242,13 @@ export default defineComponent({
itemQtyMap: {} as ItemQtyMap,
coupons: {} as AppliedCouponCodes,
itemSerialNumbers: {} as ItemSerialNumbers,
// Quick Quantity via holding 'Q'
quickQtyActive: false,
quickQtyBuffer: '' as string,
quickQtyRow: null as SalesInvoiceItem | null,
quickQtyKeyDownHandler: null as ((e: KeyboardEvent) => void) | null,
quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null,
};
},
computed: {
@@ -281,6 +290,7 @@ export default defineComponent({
this.setSinvDoc();
this.setDefaultCustomer();
this.setShortcuts();
this.addQuickQtyListeners();
await this.setItemQtyMap();
await this.setItems();
@@ -288,8 +298,177 @@ export default defineComponent({
deactivated() {
this.shortcuts?.delete(COMPONENT_NAME);
toggleSidebar(true);
this.removeQuickQtyListeners();
},
methods: {
setQuickQtySelectedRow(row: SalesInvoiceItem) {
this.quickQtyRow = row;
},
addQuickQtyListeners() {
this.quickQtyKeyDownHandler = (e: KeyboardEvent) =>
this.onQuickQtyKeyDown(e);
this.quickQtyKeyUpHandler = (e: KeyboardEvent) => this.onQuickQtyKeyUp(e);
window.addEventListener(
'keydown',
this.quickQtyKeyDownHandler as EventListener
);
window.addEventListener(
'keyup',
this.quickQtyKeyUpHandler as EventListener
);
},
removeQuickQtyListeners() {
if (this.quickQtyKeyDownHandler) {
window.removeEventListener(
'keydown',
this.quickQtyKeyDownHandler as EventListener
);
this.quickQtyKeyDownHandler = null;
}
if (this.quickQtyKeyUpHandler) {
window.removeEventListener(
'keyup',
this.quickQtyKeyUpHandler as EventListener
);
this.quickQtyKeyUpHandler = null;
}
},
hasAnyOpenModal(): boolean {
return (
this.openAlertModal ||
this.openPaymentModal ||
this.openKeyboardModal ||
this.openPriceListModal ||
this.openItemEnquiryModal ||
this.openCouponCodeModal ||
this.openShiftCloseModal ||
this.openSavedInvoiceModal ||
this.openLoyaltyProgramModal ||
this.openAppliedCouponsModal ||
this.openReturnSalesInvoiceModal
);
},
onQuickQtyKeyDown(e: KeyboardEvent) {
// Ignore if focus is in an input/contentEditable without modifiers
const notMods = !(e.altKey || e.metaKey || e.ctrlKey);
const target = e.target as HTMLElement | null;
if (
target &&
notMods &&
((target instanceof HTMLInputElement && target.type !== 'button') ||
target instanceof HTMLTextAreaElement ||
target.isContentEditable)
) {
return;
}
// Only active on POS page with no modal open
if (this.hasAnyOpenModal()) {
return;
}
if (e.code === 'KeyQ' && !this.quickQtyActive) {
this.quickQtyActive = true;
this.quickQtyBuffer = '';
return;
}
if (!this.quickQtyActive) {
return;
}
// While holding Q, collect digits; support both main digits and numpad
if (/^Digit[0-9]$/.test(e.code)) {
this.quickQtyBuffer += e.code.replace('Digit', '');
e.preventDefault();
return;
}
if (/^Numpad[0-9]$/.test(e.code)) {
this.quickQtyBuffer += e.code.replace('Numpad', '');
e.preventDefault();
return;
}
if (e.code === 'Backspace') {
this.quickQtyBuffer = this.quickQtyBuffer.slice(0, -1);
e.preventDefault();
return;
}
},
async onQuickQtyKeyUp(e: KeyboardEvent) {
if (e.code !== 'KeyQ' || !this.quickQtyActive) {
return;
}
this.quickQtyActive = false;
const buffer = this.quickQtyBuffer;
this.quickQtyBuffer = '';
if (!buffer || !buffer.length) {
return;
}
const qty = Number(buffer);
if (!Number.isFinite(qty)) {
return;
}
// Determine target row: prefer explicitly selected row; else fallback to last non-free item
let row: SalesInvoiceItem | null = this.quickQtyRow;
if (!row || !(this.sinvDoc.items || []).includes(row)) {
const items = (this.sinvDoc.items || []).filter((r) => !r.isFreeItem);
row = items.length
? (items[items.length - 1] as SalesInvoiceItem)
: null;
}
if (!row) {
return;
}
// Validate and recalculate similar to keyboard modal quantity change
const prevQty = row.quantity ?? 1;
if (!row.isReturn && qty <= 0) {
showToast({
type: 'error',
message: t`Quantity must be greater than zero.`,
duration: 'short',
});
return;
}
try {
await row.set('quantity', qty);
const existingItems =
(this.sinvDoc.items || []).filter(
(invoiceItem: InvoiceItem) =>
invoiceItem.item === row!.item && !invoiceItem.isFreeItem
) || [];
await validateQty(
this.sinvDoc as SalesInvoice,
row,
existingItems as unknown as InvoiceItem[]
);
} catch (error) {
await row.set('quantity', prevQty);
showToast({
type: 'error',
message: t`${error as string}`,
duration: 'short',
});
return;
}
if (!row.isFreeItem) {
await this.applyPricingRule();
await this.sinvDoc.runFormulas();
}
},
async setCustomer(value: string) {
if (!value) {
this.sinvDoc.party = '';
+3 -3
View File
@@ -27,7 +27,7 @@
dark:bg-gray-800 dark:text-white
"
>
{{ tableView ? 'Grid View' : 'List View' }}
{{ tableView ? t`Grid View` : t`List View` }}
</span>
</div>
@@ -69,7 +69,7 @@
dark:bg-gray-800 dark:text-white
"
>
Sales Invoice List
{{ t`Sales Invoice List` }}
</span>
</div>
@@ -122,7 +122,7 @@
dark:bg-gray-800 dark:text-white
"
>
Loyalty Program
{{ t`Loyalty Program` }}
</span>
</div>
+694 -650
View File
File diff suppressed because it is too large Load Diff