mirror of
https://github.com/frappe/books.git
synced 2026-08-24 10:04:45 -05:00
Merge pull request #1330 from osama1998H/feature/pos-quick-qty-q-hold
update quantity in POS page using shortcut
This commit is contained in:
@@ -182,7 +182,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "posPrintTemplate",
|
||||
"label": "Pos Print Template",
|
||||
"label": "POS Print Template",
|
||||
"fieldtype": "Link",
|
||||
"target": "PrintTemplate",
|
||||
"section": "Print Templates"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user