mirror of
https://github.com/frappe/books.git
synced 2024-11-09 15:20:56 +00:00
Merge pull request #709 from akshayitzme/feat-pos-page
feat: point of sale
This commit is contained in:
commit
82a2c5e5b6
@ -8,8 +8,10 @@ import {
|
||||
import { ModelNameEnum } from '../../models/types';
|
||||
import DatabaseCore from './core';
|
||||
import { BespokeFunction } from './types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DocItem, ReturnDocItem } from 'models/inventory/types';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class BespokeQueries {
|
||||
[key: string]: BespokeFunction;
|
||||
@ -390,4 +392,59 @@ export class BespokeQueries {
|
||||
}
|
||||
return returnBalanceItems;
|
||||
}
|
||||
|
||||
static async getPOSTransactedAmount(
|
||||
db: DatabaseCore,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
lastShiftClosingDate?: Date
|
||||
): Promise<Record<string, Money> | undefined> {
|
||||
const sinvNamesQuery = db.knex!(ModelNameEnum.SalesInvoice)
|
||||
.select('name')
|
||||
.where('isPOS', true)
|
||||
.andWhereBetween('date', [
|
||||
DateTime.fromJSDate(fromDate).toSQLDate(),
|
||||
DateTime.fromJSDate(toDate).toSQLDate(),
|
||||
]);
|
||||
|
||||
if (lastShiftClosingDate) {
|
||||
sinvNamesQuery.andWhere(
|
||||
'created',
|
||||
'>',
|
||||
DateTime.fromJSDate(lastShiftClosingDate).toUTC().toString()
|
||||
);
|
||||
}
|
||||
|
||||
const sinvNames = (await sinvNamesQuery).map(
|
||||
(row: { name: string }) => row.name
|
||||
);
|
||||
|
||||
if (!sinvNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentEntryNames: string[] = (
|
||||
await db.knex!(ModelNameEnum.PaymentFor)
|
||||
.select('parent')
|
||||
.whereIn('referenceName', sinvNames)
|
||||
).map((doc: { parent: string }) => doc.parent);
|
||||
|
||||
const groupedAmounts = (await db.knex!(ModelNameEnum.Payment)
|
||||
.select('paymentMethod')
|
||||
.whereIn('name', paymentEntryNames)
|
||||
.groupBy('paymentMethod')
|
||||
.sum({ amount: 'amount' })) as { paymentMethod: string; amount: Money }[];
|
||||
|
||||
const transactedAmounts = {} as { [paymentMethod: string]: Money };
|
||||
|
||||
if (!groupedAmounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of groupedAmounts) {
|
||||
transactedAmounts[row.paymentMethod] = row.amount;
|
||||
}
|
||||
|
||||
return transactedAmounts;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
RawValueMap,
|
||||
} from './types';
|
||||
import { ReturnDocItem } from 'models/inventory/types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
type FieldMap = Record<string, Record<string, Field>>;
|
||||
|
||||
@ -342,6 +343,19 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
)) as Promise<Record<string, ReturnDocItem> | undefined>;
|
||||
}
|
||||
|
||||
async getPOSTransactedAmount(
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
lastShiftClosingDate?: Date
|
||||
): Promise<Record<string, Money> | undefined> {
|
||||
return (await this.#demux.callBespoke(
|
||||
'getPOSTransactedAmount',
|
||||
fromDate,
|
||||
toDate,
|
||||
lastShiftClosingDate
|
||||
)) as Promise<Record<string, Money> | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal methods
|
||||
*/
|
||||
|
@ -10,6 +10,8 @@ import type { Defaults } from 'models/baseModels/Defaults/Defaults';
|
||||
import type { PrintSettings } from 'models/baseModels/PrintSettings/PrintSettings';
|
||||
import type { InventorySettings } from 'models/inventory/InventorySettings';
|
||||
import type { Misc } from 'models/baseModels/Misc';
|
||||
import type { POSSettings } from 'models/inventory/Point of Sale/POSSettings';
|
||||
import type { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
|
||||
/**
|
||||
* The functions below are used for dynamic evaluation
|
||||
@ -54,6 +56,8 @@ export interface SinglesMap {
|
||||
SystemSettings?: SystemSettings;
|
||||
AccountingSettings?: AccountingSettings;
|
||||
InventorySettings?: InventorySettings;
|
||||
POSSettings?: POSSettings;
|
||||
POSShift?: POSShift;
|
||||
PrintSettings?: PrintSettings;
|
||||
Defaults?: Defaults;
|
||||
Misc?: Misc;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { DefaultCashDenominations } from 'models/inventory/Point of Sale/DefaultCashDenominations';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, HiddenMap } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { PartyRoleEnum } from '../Party/types';
|
||||
|
||||
export class Defaults extends Doc {
|
||||
// Auto Payments
|
||||
@ -35,6 +37,10 @@ export class Defaults extends Doc {
|
||||
purchaseReceiptPrintTemplate?: string;
|
||||
stockMovementPrintTemplate?: string;
|
||||
|
||||
// Point of Sale
|
||||
posCashDenominations?: DefaultCashDenominations[];
|
||||
posCustomer?: string;
|
||||
|
||||
static commonFilters = {
|
||||
// Auto Payments
|
||||
salesPaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
|
||||
@ -73,6 +79,7 @@ export class Defaults extends Doc {
|
||||
type: ModelNameEnum.PurchaseReceipt,
|
||||
}),
|
||||
stockMovementPrintTemplate: () => ({ type: ModelNameEnum.StockMovement }),
|
||||
posCustomer: () => ({ role: PartyRoleEnum.Customer }),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = this.commonFilters;
|
||||
@ -82,6 +89,10 @@ export class Defaults extends Doc {
|
||||
return () => !this.fyo.singles.AccountingSettings?.enableInventory;
|
||||
}
|
||||
|
||||
getPointOfSaleHidden() {
|
||||
return () => !this.fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
}
|
||||
|
||||
hidden: HiddenMap = {
|
||||
stockMovementNumberSeries: this.getInventoryHidden(),
|
||||
shipmentNumberSeries: this.getInventoryHidden(),
|
||||
@ -91,6 +102,8 @@ export class Defaults extends Doc {
|
||||
shipmentPrintTemplate: this.getInventoryHidden(),
|
||||
purchaseReceiptPrintTemplate: this.getInventoryHidden(),
|
||||
stockMovementPrintTemplate: this.getInventoryHidden(),
|
||||
posCashDenominations: this.getPointOfSaleHidden(),
|
||||
posCustomer: this.getPointOfSaleHidden(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,12 @@ import { ShipmentItem } from './inventory/ShipmentItem';
|
||||
import { StockLedgerEntry } from './inventory/StockLedgerEntry';
|
||||
import { StockMovement } from './inventory/StockMovement';
|
||||
import { StockMovementItem } from './inventory/StockMovementItem';
|
||||
import { ClosingAmounts } from './inventory/Point of Sale/ClosingAmounts';
|
||||
import { ClosingCash } from './inventory/Point of Sale/ClosingCash';
|
||||
import { OpeningAmounts } from './inventory/Point of Sale/OpeningAmounts';
|
||||
import { OpeningCash } from './inventory/Point of Sale/OpeningCash';
|
||||
import { POSSettings } from './inventory/Point of Sale/POSSettings';
|
||||
import { POSShift } from './inventory/Point of Sale/POSShift';
|
||||
|
||||
export const models = {
|
||||
Account,
|
||||
@ -70,6 +76,13 @@ export const models = {
|
||||
ShipmentItem,
|
||||
PurchaseReceipt,
|
||||
PurchaseReceiptItem,
|
||||
// POS Models
|
||||
ClosingAmounts,
|
||||
ClosingCash,
|
||||
OpeningAmounts,
|
||||
OpeningCash,
|
||||
POSSettings,
|
||||
POSShift,
|
||||
} as ModelMap;
|
||||
|
||||
export async function getRegionalModels(
|
||||
|
@ -12,6 +12,7 @@ export class InventorySettings extends Doc {
|
||||
enableSerialNumber?: boolean;
|
||||
enableUomConversions?: boolean;
|
||||
enableStockReturns?: boolean;
|
||||
enablePointOfSale?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
stockInHand: () => ({
|
||||
@ -44,5 +45,8 @@ export class InventorySettings extends Doc {
|
||||
enableStockReturns: () => {
|
||||
return !!this.enableStockReturns;
|
||||
},
|
||||
enablePointOfSale: () => {
|
||||
return !!this.fyo.singles.POSShift?.isShiftOpen;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
6
models/inventory/Point of Sale/CashDenominations.ts
Normal file
6
models/inventory/Point of Sale/CashDenominations.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export abstract class CashDenominations extends Doc {
|
||||
denomination?: Money;
|
||||
}
|
27
models/inventory/Point of Sale/ClosingAmounts.ts
Normal file
27
models/inventory/Point of Sale/ClosingAmounts.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FormulaMap } from 'fyo/model/types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class ClosingAmounts extends Doc {
|
||||
closingAmount?: Money;
|
||||
differenceAmount?: Money;
|
||||
expectedAmount?: Money;
|
||||
openingAmount?: Money;
|
||||
paymentMethod?: string;
|
||||
|
||||
formulas: FormulaMap = {
|
||||
differenceAmount: {
|
||||
formula: () => {
|
||||
if (!this.closingAmount) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
if (!this.expectedAmount) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
return this.closingAmount.sub(this.expectedAmount);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
5
models/inventory/Point of Sale/ClosingCash.ts
Normal file
5
models/inventory/Point of Sale/ClosingCash.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class ClosingCash extends CashDenominations {
|
||||
count?: number;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class DefaultCashDenominations extends CashDenominations {}
|
11
models/inventory/Point of Sale/OpeningAmounts.ts
Normal file
11
models/inventory/Point of Sale/OpeningAmounts.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class OpeningAmounts extends Doc {
|
||||
amount?: Money;
|
||||
paymentMethod?: 'Cash' | 'Transfer';
|
||||
|
||||
get openingCashAmount() {
|
||||
return this.parentdoc?.openingCashAmount as Money;
|
||||
}
|
||||
}
|
5
models/inventory/Point of Sale/OpeningCash.ts
Normal file
5
models/inventory/Point of Sale/OpeningCash.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class OpeningCash extends CashDenominations {
|
||||
count?: number;
|
||||
}
|
19
models/inventory/Point of Sale/POSSettings.ts
Normal file
19
models/inventory/Point of Sale/POSSettings.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap } from 'fyo/model/types';
|
||||
import {
|
||||
AccountRootTypeEnum,
|
||||
AccountTypeEnum,
|
||||
} from 'models/baseModels/Account/types';
|
||||
|
||||
export class POSSettings extends Doc {
|
||||
inventory?: string;
|
||||
cashAccount?: string;
|
||||
writeOffAccount?: string;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
cashAccount: () => ({
|
||||
rootType: AccountRootTypeEnum.Asset,
|
||||
accountType: AccountTypeEnum.Cash,
|
||||
}),
|
||||
};
|
||||
}
|
61
models/inventory/Point of Sale/POSShift.ts
Normal file
61
models/inventory/Point of Sale/POSShift.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { ClosingAmounts } from './ClosingAmounts';
|
||||
import { ClosingCash } from './ClosingCash';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { OpeningAmounts } from './OpeningAmounts';
|
||||
import { OpeningCash } from './OpeningCash';
|
||||
|
||||
export class POSShift extends Doc {
|
||||
closingAmounts?: ClosingAmounts[];
|
||||
closingCash?: ClosingCash[];
|
||||
closingDate?: Date;
|
||||
isShiftOpen?: boolean;
|
||||
openingAmounts?: OpeningAmounts[];
|
||||
openingCash?: OpeningCash[];
|
||||
openingDate?: Date;
|
||||
|
||||
get openingCashAmount() {
|
||||
if (!this.openingCash) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let openingAmount = this.fyo.pesa(0);
|
||||
|
||||
this.openingCash.map((row: OpeningCash) => {
|
||||
const denomination = row.denomination ?? this.fyo.pesa(0);
|
||||
const count = row.count ?? 0;
|
||||
|
||||
const amount = denomination.mul(count);
|
||||
openingAmount = openingAmount.add(amount);
|
||||
});
|
||||
return openingAmount;
|
||||
}
|
||||
|
||||
get closingCashAmount() {
|
||||
if (!this.closingCash) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let closingAmount = this.fyo.pesa(0);
|
||||
|
||||
this.closingCash.map((row: ClosingCash) => {
|
||||
const denomination = row.denomination ?? this.fyo.pesa(0);
|
||||
const count = row.count ?? 0;
|
||||
|
||||
const amount = denomination.mul(count);
|
||||
closingAmount = closingAmount.add(amount);
|
||||
});
|
||||
return closingAmount;
|
||||
}
|
||||
|
||||
get openingTransferAmount() {
|
||||
if (!this.openingAmounts) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
const transferAmountRow = this.openingAmounts.filter(
|
||||
(row) => row.paymentMethod === 'Transfer'
|
||||
)[0];
|
||||
|
||||
return transferAmountRow.amount ?? this.fyo.pesa(0);
|
||||
}
|
||||
}
|
103
models/inventory/Point of Sale/tests/testPointOfSale.spec.ts
Normal file
103
models/inventory/Point of Sale/tests/testPointOfSale.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { Payment } from 'models/baseModels/Payment/Payment';
|
||||
import { Money } from 'pesa';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
const customer = { name: 'Someone', role: 'Both' };
|
||||
const itemMap = {
|
||||
Pen: {
|
||||
name: 'Pen',
|
||||
rate: 700,
|
||||
},
|
||||
Ink: {
|
||||
name: 'Ink',
|
||||
rate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
test('insert test docs', async (t) => {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemMap.Pen).sync();
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemMap.Ink).sync();
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Party, customer).sync();
|
||||
});
|
||||
|
||||
let sinvDocOne: SalesInvoice | undefined;
|
||||
|
||||
test('check pos transacted amount', async (t) => {
|
||||
const transactedAmountBeforeTxn = await fyo.db.getPOSTransactedAmount(
|
||||
new Date('2023-01-01'),
|
||||
new Date('2023-01-02')
|
||||
);
|
||||
|
||||
t.equals(transactedAmountBeforeTxn, undefined);
|
||||
|
||||
sinvDocOne = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
isPOS: true,
|
||||
date: new Date('2023-01-01'),
|
||||
account: 'Debtors',
|
||||
party: customer.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinvDocOne.append('items', {
|
||||
item: itemMap.Pen.name,
|
||||
rate: itemMap.Pen.rate,
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
await (await sinvDocOne.sync()).submit();
|
||||
const paymentDocOne = sinvDocOne.getPayment() as Payment;
|
||||
|
||||
await paymentDocOne.sync();
|
||||
|
||||
const sinvDocTwo = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
isPOS: true,
|
||||
date: new Date('2023-01-01'),
|
||||
account: 'Debtors',
|
||||
party: customer.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinvDocTwo.append('items', {
|
||||
item: itemMap.Pen.name,
|
||||
rate: itemMap.Pen.rate,
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
await (await sinvDocTwo.sync()).submit();
|
||||
const paymentDocTwo = sinvDocTwo.getPayment() as Payment;
|
||||
|
||||
await paymentDocTwo.setMultiple({
|
||||
paymentMethod: 'Transfer',
|
||||
clearanceDate: new Date('2023-01-01'),
|
||||
referenceId: 'xxxxxxxx',
|
||||
});
|
||||
|
||||
await paymentDocTwo.sync();
|
||||
|
||||
const transactedAmountAfterTxn: Record<string, Money> | undefined =
|
||||
await fyo.db.getPOSTransactedAmount(
|
||||
new Date('2023-01-01'),
|
||||
new Date('2023-01-02')
|
||||
);
|
||||
|
||||
t.true(transactedAmountAfterTxn);
|
||||
|
||||
t.equals(
|
||||
transactedAmountAfterTxn?.Cash,
|
||||
sinvDocOne.grandTotal?.float,
|
||||
'transacted cash amount matches'
|
||||
);
|
||||
|
||||
t.equals(
|
||||
transactedAmountAfterTxn?.Transfer,
|
||||
sinvDocTwo.grandTotal?.float,
|
||||
'transacted transfer amount matches'
|
||||
);
|
||||
});
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
@ -45,7 +45,9 @@ export enum ModelNameEnum {
|
||||
PurchaseReceiptItem = 'PurchaseReceiptItem',
|
||||
Location = 'Location',
|
||||
CustomForm = 'CustomForm',
|
||||
CustomField = 'CustomField'
|
||||
CustomField = 'CustomField',
|
||||
POSSettings = 'POSSettings',
|
||||
POSShift = 'POSShift'
|
||||
}
|
||||
|
||||
export type ModelName = keyof typeof ModelNameEnum;
|
||||
|
@ -164,6 +164,21 @@
|
||||
"fieldtype": "Link",
|
||||
"target": "PrintTemplate",
|
||||
"section": "Print Templates"
|
||||
},
|
||||
{
|
||||
"fieldname": "posCustomer",
|
||||
"label": "POS Customer",
|
||||
"fieldtype": "Link",
|
||||
"target": "Party",
|
||||
"create": true,
|
||||
"section": "Point of Sale"
|
||||
},
|
||||
{
|
||||
"fieldname": "posCashDenominations",
|
||||
"label": "Cash Denominations",
|
||||
"fieldtype": "Table",
|
||||
"target": "DefaultCashDenominations",
|
||||
"section": "Point of Sale"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -61,6 +61,12 @@
|
||||
"target": "SalesInvoice",
|
||||
"label": "Return Against",
|
||||
"section": "References"
|
||||
},
|
||||
{
|
||||
"fieldname": "isPOS",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"keywordFields": ["name", "party"]
|
||||
|
@ -63,6 +63,13 @@
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "enablePointOfSale",
|
||||
"label": "Enable Point of Sale",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
14
schemas/app/inventory/Point of Sale/CashDenominations.json
Normal file
14
schemas/app/inventory/Point of Sale/CashDenominations.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "CashDenominations",
|
||||
"label": "Cash Denominations",
|
||||
"isAbstract": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "denomination",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Denomination",
|
||||
"placeholder": "Denomination",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
42
schemas/app/inventory/Point of Sale/ClosingAmounts.json
Normal file
42
schemas/app/inventory/Point of Sale/ClosingAmounts.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "ClosingAmounts",
|
||||
"label": "Closing Amount",
|
||||
"isChild": true,
|
||||
"extends": "POSShiftAmounts",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "openingAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Opening Amount",
|
||||
"placeholder": "Opening Amount",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "closingAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Closing Amount",
|
||||
"placeholder": "Closing Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "expectedAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Expected Amount",
|
||||
"placeholder": "Expected Amount",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "differenceAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Difference Amount",
|
||||
"placeholder": "Difference Amount",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"tableFields": [
|
||||
"paymentMethod",
|
||||
"openingAmount",
|
||||
"closingAmount",
|
||||
"expectedAmount",
|
||||
"differenceAmount"
|
||||
]
|
||||
}
|
17
schemas/app/inventory/Point of Sale/ClosingCash.json
Normal file
17
schemas/app/inventory/Point of Sale/ClosingCash.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ClosingCash",
|
||||
"label": "Closing Cash In Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "count",
|
||||
"label": "Count",
|
||||
"placeholder": "Count",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tableFields": ["denomination", "count"]
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "DefaultCashDenominations",
|
||||
"label": "Default Cash Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"tableFields": ["denomination"]
|
||||
}
|
15
schemas/app/inventory/Point of Sale/OpeningAmounts.json
Normal file
15
schemas/app/inventory/Point of Sale/OpeningAmounts.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "OpeningAmounts",
|
||||
"label": "Opening Amount",
|
||||
"isChild": true,
|
||||
"extends": "POSShiftAmounts",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"label": "Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Defaults"
|
||||
}
|
||||
],
|
||||
"tableFields": ["paymentMethod", "amount"]
|
||||
}
|
17
schemas/app/inventory/Point of Sale/OpeningCash.json
Normal file
17
schemas/app/inventory/Point of Sale/OpeningCash.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "OpeningCash",
|
||||
"label": "Opening Cash In Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "count",
|
||||
"label": "Count",
|
||||
"placeholder": "Count",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tableFields": ["denomination", "count"]
|
||||
}
|
36
schemas/app/inventory/Point of Sale/POSSettings.json
Normal file
36
schemas/app/inventory/Point of Sale/POSSettings.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "POSSettings",
|
||||
"label": "POS Settings",
|
||||
"isSingle": true,
|
||||
"isChild": false,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "inventory",
|
||||
"label": "Inventory",
|
||||
"fieldtype": "Link",
|
||||
"target": "Location",
|
||||
"create": true,
|
||||
"default": "Stores",
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "cashAccount",
|
||||
"label": "Counter Cash Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"default": "Cash In Hand",
|
||||
"required": true,
|
||||
"create": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "writeOffAccount",
|
||||
"label": "Write Off Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"default": "Write Off",
|
||||
"section": "Default"
|
||||
}
|
||||
]
|
||||
}
|
43
schemas/app/inventory/Point of Sale/POSShift.json
Normal file
43
schemas/app/inventory/Point of Sale/POSShift.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "POSShift",
|
||||
"isSingle": true,
|
||||
"isChild": false,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "isShiftOpen",
|
||||
"label": "Is POS Shift Open",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "openingDate",
|
||||
"label": "Opening Date",
|
||||
"fieldtype": "Datetime"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingDate",
|
||||
"label": "Closing Date",
|
||||
"fieldtype": "Datetime"
|
||||
},
|
||||
{
|
||||
"fieldname": "openingCash",
|
||||
"fieldtype": "Table",
|
||||
"target": "OpeningCash"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingCash",
|
||||
"fieldtype": "Table",
|
||||
"target": "ClosingCash"
|
||||
},
|
||||
{
|
||||
"fieldname": "openingAmounts",
|
||||
"fieldtype": "Table",
|
||||
"target": "OpeningAmounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingAmounts",
|
||||
"fieldtype": "Table",
|
||||
"target": "ClosingAmounts"
|
||||
}
|
||||
]
|
||||
}
|
25
schemas/app/inventory/Point of Sale/POSShiftAmounts.json
Normal file
25
schemas/app/inventory/Point of Sale/POSShiftAmounts.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "POSShiftAmounts",
|
||||
"label": "POS Shift Amount",
|
||||
"isChild": true,
|
||||
"isAbstract": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "paymentMethod",
|
||||
"label": "Payment Method",
|
||||
"placeholder": "Payment Method",
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{
|
||||
"value": "Cash",
|
||||
"label": "Cash"
|
||||
},
|
||||
{
|
||||
"value": "Transfer",
|
||||
"label": "Transfer"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
@ -52,6 +52,15 @@ import base from './meta/base.json';
|
||||
import child from './meta/child.json';
|
||||
import submittable from './meta/submittable.json';
|
||||
import tree from './meta/tree.json';
|
||||
import CashDenominations from './app/inventory/Point of Sale/CashDenominations.json';
|
||||
import ClosingAmounts from './app/inventory/Point of Sale/ClosingAmounts.json';
|
||||
import ClosingCash from './app/inventory/Point of Sale/ClosingCash.json';
|
||||
import DefaultCashDenominations from './app/inventory/Point of Sale/DefaultCashDenominations.json';
|
||||
import OpeningAmounts from './app/inventory/Point of Sale/OpeningAmounts.json';
|
||||
import OpeningCash from './app/inventory/Point of Sale/OpeningCash.json';
|
||||
import POSSettings from './app/inventory/Point of Sale/POSSettings.json';
|
||||
import POSShift from './app/inventory/Point of Sale/POSShift.json';
|
||||
import POSShiftAmounts from './app/inventory/Point of Sale/POSShiftAmounts.json';
|
||||
import { Schema, SchemaStub } from './types';
|
||||
|
||||
export const coreSchemas: Schema[] = [
|
||||
@ -129,4 +138,14 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
||||
|
||||
CustomForm as Schema,
|
||||
CustomField as Schema,
|
||||
|
||||
CashDenominations as Schema,
|
||||
ClosingAmounts as Schema,
|
||||
ClosingCash as Schema,
|
||||
DefaultCashDenominations as Schema,
|
||||
OpeningAmounts as Schema,
|
||||
OpeningCash as Schema,
|
||||
POSSettings as Schema,
|
||||
POSShift as Schema,
|
||||
POSShiftAmounts as Schema,
|
||||
];
|
||||
|
@ -9,6 +9,7 @@ import Inventory from './inventory.vue';
|
||||
import Invoice from './invoice.vue';
|
||||
import Item from './item.vue';
|
||||
import Mail from './mail.vue';
|
||||
import POS from './pos.vue';
|
||||
import OpeningAc from './opening-ac.vue';
|
||||
import Percentage from './percentage.vue';
|
||||
import Property from './property.vue';
|
||||
@ -36,6 +37,7 @@ export default {
|
||||
'invoice': Invoice,
|
||||
'item': Item,
|
||||
'mail': Mail,
|
||||
'pos': POS,
|
||||
'opening-ac': OpeningAc,
|
||||
'percentage': Percentage,
|
||||
'property': Property,
|
||||
|
15
src/components/Icons/18/pos.vue
Normal file
15
src/components/Icons/18/pos.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
:fill="darkColor"
|
||||
d="M21 13V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V13H2V11L3 6H21L22 11V13H21ZM5 13V19H19V13H5ZM6 14H14V17H6V14ZM3 3H21V5H3V3Z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
import Base from '../base.vue';
|
||||
export default {
|
||||
extends: Base,
|
||||
};
|
||||
</script>
|
116
src/components/POS/FloatingLabelCurrencyInput.vue
Normal file
116
src/components/POS/FloatingLabelCurrencyInput.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="inputType"
|
||||
:class="[inputClasses, size === 'large' ? 'text-lg' : 'text-sm']"
|
||||
:value="round(value)"
|
||||
:max="isNumeric(df) ? df.maxvalue : undefined"
|
||||
:min="isNumeric(df) ? df.minvalue : undefined"
|
||||
:readonly="isReadOnly"
|
||||
:tabindex="isReadOnly ? '-1' : '0'"
|
||||
@blur="onBlur"
|
||||
class="
|
||||
block
|
||||
px-2.5
|
||||
pb-2.5
|
||||
pt-4
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
bg-gray-25
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
"
|
||||
/>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-8
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ currency ? fyo.currencySymbols[currency] : undefined }}</label
|
||||
>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-1
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ df.label }}</label
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FloatingLabelInputBase from './FloatingLabelInputBase.vue';
|
||||
import { safeParsePesa } from 'utils/index';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelCurrencyInput',
|
||||
extends: FloatingLabelInputBase,
|
||||
computed: {
|
||||
currency(): string | undefined {
|
||||
if (this.value) {
|
||||
return (this.value as Money).getCurrency();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
round(v: unknown) {
|
||||
if (!isPesa(v)) {
|
||||
v = this.parse(v);
|
||||
}
|
||||
|
||||
if (isPesa(v)) {
|
||||
return v.round();
|
||||
}
|
||||
|
||||
return fyo.pesa(0).round();
|
||||
},
|
||||
parse(value: unknown): Money {
|
||||
return safeParsePesa(value, this.fyo);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
14
src/components/POS/FloatingLabelFloatInput.vue
Normal file
14
src/components/POS/FloatingLabelFloatInput.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FloatingLabelInputBase from './FloatingLabelInputBase.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelFloatInput',
|
||||
extends: FloatingLabelInputBase,
|
||||
computed: {
|
||||
inputType() {
|
||||
return 'number';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
63
src/components/POS/FloatingLabelInputBase.vue
Normal file
63
src/components/POS/FloatingLabelInputBase.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="inputType"
|
||||
:class="[inputClasses, size === 'large' ? 'text-lg' : 'text-sm']"
|
||||
:value="value"
|
||||
:max="isNumeric(df) ? df.maxvalue : undefined"
|
||||
:min="isNumeric(df) ? df.minvalue : undefined"
|
||||
:readonly="isReadOnly"
|
||||
:tabindex="isReadOnly ? '-1' : '0'"
|
||||
@blur="onBlur"
|
||||
class="
|
||||
block
|
||||
px-2.5
|
||||
pb-2.5
|
||||
pt-4
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
bg-gray-25
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
"
|
||||
/>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-1
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ df.label }}</label
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Base from '../Controls/Base.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelInputBase',
|
||||
extends: Base,
|
||||
});
|
||||
</script>
|
166
src/components/POS/ItemsTable.vue
Normal file
166
src/components/POS/ItemsTable.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border flex items-center mt-4 px-2 rounded-t-md text-gray-600 w-full"
|
||||
>
|
||||
<div
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
class="flex items-center px-2 py-2 text-lg"
|
||||
:class="{
|
||||
'ms-auto': isNumeric(df as Field),
|
||||
}"
|
||||
:style="{
|
||||
height: ``,
|
||||
}"
|
||||
>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<div class="overflow-y-auto" style="height: 72.5vh">
|
||||
<Row
|
||||
v-if="items"
|
||||
v-for="row in items"
|
||||
:ratio="ratio"
|
||||
:border="true"
|
||||
class="
|
||||
border-b border-l border-r
|
||||
flex
|
||||
group
|
||||
h-row-mid
|
||||
hover:bg-gray-25
|
||||
items-center
|
||||
justify-center
|
||||
px-2
|
||||
w-full
|
||||
"
|
||||
@click="handleChange(row as POSItem)"
|
||||
>
|
||||
<FormControl
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
size="large"
|
||||
class=""
|
||||
:df="df"
|
||||
:value="row[df.fieldname]"
|
||||
:readOnly="true"
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FormControl from '../Controls/FormControl.vue';
|
||||
import Row from 'src/components/Row.vue';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { ItemQtyMap } from './types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { POSItem } from './types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ItemsTable',
|
||||
components: { FormControl, Row },
|
||||
emits: ['addItem', 'updateValues'],
|
||||
setup() {
|
||||
return {
|
||||
itemQtyMap: inject('itemQtyMap') as ItemQtyMap,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [] as POSItem[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return [1, 1, 1, 0.7];
|
||||
},
|
||||
tableFields() {
|
||||
return [
|
||||
{
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Item',
|
||||
placeholder: 'Item',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
placeholder: 'Rate',
|
||||
fieldtype: 'Currency',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'availableQty',
|
||||
label: 'Available Qty',
|
||||
placeholder: 'Available Qty',
|
||||
fieldtype: 'Float',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'unit',
|
||||
label: 'Unit',
|
||||
placeholder: 'Unit',
|
||||
fieldtype: 'Data',
|
||||
target: 'UOM',
|
||||
readOnly: true,
|
||||
},
|
||||
] as Field[];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
itemQtyMap: {
|
||||
async handler() {
|
||||
this.setItems();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
await this.setItems();
|
||||
},
|
||||
methods: {
|
||||
async setItems() {
|
||||
const items = (await fyo.db.getAll(ModelNameEnum.Item, {
|
||||
fields: [],
|
||||
filters: { trackItem: true },
|
||||
})) as Item[];
|
||||
|
||||
this.items = [] as POSItem[];
|
||||
for (const item of items) {
|
||||
let availableQty = 0;
|
||||
|
||||
if (!!this.itemQtyMap[item.name as string]) {
|
||||
availableQty = this.itemQtyMap[item.name as string].availableQty;
|
||||
}
|
||||
|
||||
if (!item.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.push({
|
||||
availableQty,
|
||||
name: item.name,
|
||||
rate: item.rate as Money,
|
||||
unit: item.unit as string,
|
||||
hasBatch: !!item.hasBatch,
|
||||
hasSerialNumber: !!item.hasSerialNumber,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleChange(value: POSItem) {
|
||||
this.$emit('addItem', value);
|
||||
this.$emit('updateValues');
|
||||
},
|
||||
isNumeric,
|
||||
},
|
||||
});
|
||||
</script>
|
354
src/components/POS/SelectedItemRow.vue
Normal file
354
src/components/POS/SelectedItemRow.vue
Normal file
@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<feather-icon
|
||||
:name="isExapanded ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 inline-flex"
|
||||
@click="isExapanded = !isExapanded"
|
||||
/>
|
||||
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Data',
|
||||
label: 'item',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.item"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Int
|
||||
:df="{
|
||||
fieldname: 'quantity',
|
||||
fieldtype: 'Int',
|
||||
label: 'Quantity',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.quantity"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'unit',
|
||||
fieldtype: 'Data',
|
||||
label: 'Unit',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.unit"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'rate',
|
||||
label: 'rate',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.rate"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'amount',
|
||||
label: 'Amount',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.amount"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<div class="px-4">
|
||||
<feather-icon
|
||||
name="trash"
|
||||
class="w-4 text-xl text-red-500"
|
||||
@click="$emit('removeItem', row.idx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
|
||||
<template v-if="isExapanded">
|
||||
<div class="px-4 pt-6 col-span-1">
|
||||
<Float
|
||||
:df="{
|
||||
fieldname: 'quantity',
|
||||
fieldtype: 'Float',
|
||||
label: 'Quantity',
|
||||
}"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.quantity"
|
||||
@change="(value:number) => (row.quantity = value)"
|
||||
:read-only="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2 flex">
|
||||
<Link
|
||||
v-if="isUOMConversionEnabled"
|
||||
:df="{
|
||||
fieldname: 'transferUnit',
|
||||
fieldtype: 'Link',
|
||||
target: 'UOM',
|
||||
label: t`Transfer Unit`,
|
||||
}"
|
||||
class="flex-1"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.transferUnit"
|
||||
@change="(value:string) => setTransferUnit((row.transferUnit = value))"
|
||||
/>
|
||||
<feather-icon
|
||||
v-if="isUOMConversionEnabled"
|
||||
name="refresh-ccw"
|
||||
class="w-3.5 ml-2 mt-4 text-blue-500"
|
||||
@click="row.transferUnit = row.unit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2">
|
||||
<Int
|
||||
v-if="isUOMConversionEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'transferQuantity',
|
||||
label: 'Transfer Quantity',
|
||||
}"
|
||||
size="medium"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.transferQuantity"
|
||||
@change="(value:number) => setTransferQty((row.transferQuantity = value))"
|
||||
:read-only="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
<div></div>
|
||||
|
||||
<div class="px-4 pt-6 flex">
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
}"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.rate"
|
||||
:read-only="false"
|
||||
@change="(value:Money) => (row.rate = value)"
|
||||
/>
|
||||
<feather-icon
|
||||
name="refresh-ccw"
|
||||
class="w-3.5 ml-2 mt-5 text-blue-500 flex-none"
|
||||
@click="row.rate= (defaultRate as Money)"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-6 pt-6 col-span-2">
|
||||
<Currency
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'discountAmount',
|
||||
label: 'Discount Amount',
|
||||
}"
|
||||
class="col-span-2"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountAmount"
|
||||
:read-only="row.itemDiscountPercent as number > 0"
|
||||
@change="(value:number) => setItemDiscount('amount', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2">
|
||||
<Float
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'itemDiscountPercent',
|
||||
label: 'Discount Percent',
|
||||
}"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountPercent"
|
||||
:read-only="!row.itemDiscountAmount?.isZero()"
|
||||
@change="(value:number) => setItemDiscount('percent', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=""></div>
|
||||
|
||||
<div
|
||||
v-if="row.links?.item && row.links?.item.hasBatch"
|
||||
class="pl-6 px-4 pt-6 col-span-2"
|
||||
>
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'batch',
|
||||
fieldtype: 'Link',
|
||||
target: 'Batch',
|
||||
label: t`Batch`,
|
||||
}"
|
||||
value=""
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:read-only="false"
|
||||
@change="(value:string) => setBatch(value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="row.links?.item && row.links?.item.hasBatch"
|
||||
class="px-2 pt-6 col-span-2"
|
||||
>
|
||||
<Float
|
||||
:df="{
|
||||
fieldname: 'availableQtyInBatch',
|
||||
fieldtype: 'Float',
|
||||
label: t`Qty in Batch`,
|
||||
}"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:value="availableQtyInBatch"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSerialNumber" class="px-2 pt-8 col-span-2">
|
||||
<Text
|
||||
:df="{
|
||||
label: t`Serial Number`,
|
||||
fieldtype: 'Text',
|
||||
fieldname: 'serialNumber',
|
||||
}"
|
||||
:value="row.serialNumber"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="hasSerialNumber"
|
||||
@change="(value:string)=> setSerialNumber(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Currency from '../Controls/Currency.vue';
|
||||
import Data from '../Controls/Data.vue';
|
||||
import Float from '../Controls/Float.vue';
|
||||
import Int from '../Controls/Int.vue';
|
||||
import Link from '../Controls/Link.vue';
|
||||
import Text from '../Controls/Text.vue';
|
||||
import { inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { Money } from 'pesa';
|
||||
import { DiscountType } from './types';
|
||||
import { t } from 'fyo';
|
||||
import { validateSerialNumberCount } from 'src/utils/pos';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemRow',
|
||||
components: { Currency, Data, Float, Int, Link, Text },
|
||||
props: {
|
||||
row: { type: SalesInvoiceItem, required: true },
|
||||
},
|
||||
emits: ['removeItem', 'runSinvFormulas', 'setItemSerialNumbers'],
|
||||
setup() {
|
||||
return {
|
||||
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
|
||||
itemSerialNumbers: inject('itemSerialNumbers') as {
|
||||
[item: string]: string;
|
||||
},
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExapanded: false,
|
||||
batches: [] as string[],
|
||||
availableQtyInBatch: 0,
|
||||
|
||||
defaultRate: this.row.rate as Money,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isUOMConversionEnabled(): boolean {
|
||||
return !!fyo.singles.InventorySettings?.enableUomConversions;
|
||||
},
|
||||
hasSerialNumber(): boolean {
|
||||
return !!(this.row.links?.item && this.row.links?.item.hasSerialNumber);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getAvailableQtyInBatch(): Promise<number> {
|
||||
if (!this.row.batch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (
|
||||
(await fyo.db.getStockQuantity(
|
||||
this.row.item as string,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.row.batch
|
||||
)) ?? 0
|
||||
);
|
||||
},
|
||||
async setBatch(batch: string) {
|
||||
this.row.batch = batch;
|
||||
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
|
||||
},
|
||||
setSerialNumber(serialNumber: string) {
|
||||
if (!serialNumber) {
|
||||
return;
|
||||
}
|
||||
this.itemSerialNumbers[this.row.item as string] = serialNumber;
|
||||
|
||||
validateSerialNumberCount(
|
||||
serialNumber,
|
||||
this.row.quantity ?? 0,
|
||||
this.row.item!
|
||||
);
|
||||
},
|
||||
setItemDiscount(type: DiscountType, value: Money | number) {
|
||||
if (type === 'percent') {
|
||||
this.row.setItemDiscountAmount = false;
|
||||
this.row.itemDiscountPercent = value as number;
|
||||
this.$emit('runSinvFormulas');
|
||||
return;
|
||||
}
|
||||
this.row.setItemDiscountAmount = true;
|
||||
this.row.itemDiscountAmount = value as Money;
|
||||
this.$emit('runSinvFormulas');
|
||||
},
|
||||
setTransferUnit(unit: string) {
|
||||
this.row.setTransferUnit = unit;
|
||||
this.row._applyFormula('transferUnit');
|
||||
},
|
||||
setTransferQty(quantity: number) {
|
||||
this.row.transferQuantity = quantity;
|
||||
this.row._applyFormula('transferQuantity');
|
||||
this.$emit('runSinvFormulas');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
150
src/components/POS/SelectedItemTable.vue
Normal file
150
src/components/POS/SelectedItemTable.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border rounded-t px-2 text-gray-600 w-full flex items-center mt-4"
|
||||
>
|
||||
<div
|
||||
v-if="tableFields"
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
class="items-center text-lg flex px-2 py-2"
|
||||
:class="{
|
||||
'ms-auto': isNumeric(df as Field),
|
||||
}"
|
||||
:style="{
|
||||
height: ``,
|
||||
}"
|
||||
>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<div class="overflow-y-auto" style="height: 50vh">
|
||||
<Row
|
||||
v-for="row in sinvDoc.items"
|
||||
:ratio="ratio"
|
||||
class="
|
||||
border
|
||||
w-full
|
||||
px-2
|
||||
py-2
|
||||
group
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
hover:bg-gray-25
|
||||
"
|
||||
>
|
||||
<SelectedItemRow
|
||||
:row="(row as SalesInvoiceItem)"
|
||||
@remove-item="removeItem"
|
||||
@run-sinv-formulas="runSinvFormulas"
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FormContainer from '../FormContainer.vue';
|
||||
import FormControl from '../Controls/FormControl.vue';
|
||||
import Link from '../Controls/Link.vue';
|
||||
import Row from '../Row.vue';
|
||||
import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
|
||||
import SelectedItemRow from './SelectedItemRow.vue';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { inject } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { Field } from 'schemas/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemTable',
|
||||
components: {
|
||||
FormContainer,
|
||||
FormControl,
|
||||
Link,
|
||||
Row,
|
||||
RowEditForm,
|
||||
SelectedItemRow,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExapanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return [0.1, 1, 0.8, 0.8, 0.8, 0.8, 0.2];
|
||||
},
|
||||
tableFields() {
|
||||
return [
|
||||
{
|
||||
fieldname: 'toggler',
|
||||
fieldtype: 'Link',
|
||||
label: ' ',
|
||||
},
|
||||
{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Link',
|
||||
label: 'Item',
|
||||
placeholder: 'Item',
|
||||
required: true,
|
||||
schemaName: 'Item',
|
||||
},
|
||||
{
|
||||
fieldname: 'quantity',
|
||||
label: 'Quantity',
|
||||
placeholder: 'Quantity',
|
||||
fieldtype: 'Int',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'unit',
|
||||
label: 'Stock Unit',
|
||||
placeholder: 'Unit',
|
||||
fieldtype: 'Link',
|
||||
required: true,
|
||||
schemaName: 'UOM',
|
||||
},
|
||||
{
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
placeholder: 'Rate',
|
||||
fieldtype: 'Currency',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'amount',
|
||||
label: 'Amount',
|
||||
placeholder: 'Amount',
|
||||
fieldtype: 'Currency',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'removeItem',
|
||||
fieldtype: 'Link',
|
||||
label: ' ',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeItem(idx: number) {
|
||||
this.sinvDoc.remove('items', idx);
|
||||
},
|
||||
async runSinvFormulas() {
|
||||
await this.sinvDoc.runFormulas();
|
||||
},
|
||||
isNumeric,
|
||||
},
|
||||
});
|
||||
</script>
|
20
src/components/POS/types.ts
Normal file
20
src/components/POS/types.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Money } from "pesa";
|
||||
|
||||
export type ItemQtyMap = {
|
||||
[item: string]: { availableQty: number;[batch: string]: number };
|
||||
}
|
||||
|
||||
export type ItemSerialNumbers = { [item: string]: string };
|
||||
|
||||
export type DiscountType = "percent" | "amount";
|
||||
|
||||
export type ModalName = 'ShiftOpen' | 'ShiftClose' | 'Payment'
|
||||
|
||||
export interface POSItem {
|
||||
name: string,
|
||||
rate: Money,
|
||||
availableQty: number,
|
||||
unit: string,
|
||||
hasBatch: boolean,
|
||||
hasSerialNumber: boolean,
|
||||
}
|
209
src/pages/POS/ClosePOSShiftModal.vue
Normal file
209
src/pages/POS/ClosePOSShiftModal.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<Modal :open-modal="openModal" class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Close POS Shift</h1>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-medium">Closing Cash</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
:df="getField('closingCash')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.closingCash ?? []"
|
||||
:read-only="false"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<h2 class="mt-6 mb-2 text-lg font-medium">Closing Amounts</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
:df="getField('closingAmounts')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.closingAmounts"
|
||||
:read-only="true"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<Button
|
||||
class="w-full py-5 bg-red-500"
|
||||
@click="$emit('toggleModal', 'ShiftClose', false)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { OpeningAmounts } from 'models/inventory/Point of Sale/OpeningAmounts';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { computed } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { t } from 'fyo';
|
||||
import {
|
||||
validateClosingAmounts,
|
||||
transferPOSCashAndWriteOff,
|
||||
} from 'src/utils/pos';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClosePOSShiftModal',
|
||||
components: { Button, Modal, Table },
|
||||
provide() {
|
||||
return {
|
||||
doc: computed(() => this.posShiftDoc),
|
||||
};
|
||||
},
|
||||
props: {
|
||||
openModal: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['toggleModal'],
|
||||
data() {
|
||||
return {
|
||||
isValuesSeeded: false,
|
||||
|
||||
posShiftDoc: undefined as POSShift | undefined,
|
||||
transactedAmount: {} as Record<string, Money> | undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
openModal: {
|
||||
async handler() {
|
||||
await this.setTransactedAmount();
|
||||
await this.seedClosingAmounts();
|
||||
},
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
this.posShiftDoc = fyo.singles[ModelNameEnum.POSShift];
|
||||
await this.seedValues();
|
||||
await this.setTransactedAmount();
|
||||
},
|
||||
methods: {
|
||||
async setTransactedAmount() {
|
||||
if (!fyo.singles.POSShift?.openingDate) {
|
||||
return;
|
||||
}
|
||||
const fromDate = fyo.singles.POSShift?.openingDate;
|
||||
this.transactedAmount = await fyo.db.getPOSTransactedAmount(
|
||||
fromDate,
|
||||
new Date(),
|
||||
fyo.singles.POSShift.closingDate as Date
|
||||
);
|
||||
},
|
||||
seedClosingCash() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.closingCash = [];
|
||||
|
||||
this.posShiftDoc?.openingCash?.map(async (row) => {
|
||||
await this.posShiftDoc?.append('closingCash', {
|
||||
count: row.count,
|
||||
denomination: row.denomination as Money,
|
||||
});
|
||||
});
|
||||
},
|
||||
async seedClosingAmounts() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.closingAmounts = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
const openingAmounts = this.posShiftDoc
|
||||
.openingAmounts as OpeningAmounts[];
|
||||
|
||||
for (const row of openingAmounts) {
|
||||
if (!row.paymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
let expectedAmount = fyo.pesa(0);
|
||||
|
||||
if (row.paymentMethod === 'Cash') {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.posShiftDoc.openingCashAmount as Money
|
||||
);
|
||||
}
|
||||
|
||||
if (row.paymentMethod === 'Transfer') {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.posShiftDoc.openingTransferAmount as Money
|
||||
);
|
||||
}
|
||||
|
||||
if (this.transactedAmount) {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.transactedAmount[row.paymentMethod]
|
||||
);
|
||||
}
|
||||
|
||||
await this.posShiftDoc.append('closingAmounts', {
|
||||
paymentMethod: row.paymentMethod,
|
||||
openingAmount: row.amount,
|
||||
closingAmount: fyo.pesa(0),
|
||||
expectedAmount: expectedAmount,
|
||||
differenceAmount: fyo.pesa(0),
|
||||
});
|
||||
await this.posShiftDoc.sync();
|
||||
}
|
||||
},
|
||||
async seedValues() {
|
||||
this.isValuesSeeded = false;
|
||||
this.seedClosingCash();
|
||||
await this.seedClosingAmounts();
|
||||
this.isValuesSeeded = true;
|
||||
},
|
||||
getField(fieldname: string) {
|
||||
return fyo.getField(ModelNameEnum.POSShift, fieldname);
|
||||
},
|
||||
async handleChange() {
|
||||
await this.posShiftDoc?.sync();
|
||||
},
|
||||
async handleSubmit() {
|
||||
try {
|
||||
validateClosingAmounts(this.posShiftDoc as POSShift);
|
||||
await this.posShiftDoc?.set('isShiftOpen', false);
|
||||
await this.posShiftDoc?.set('closingDate', new Date());
|
||||
await this.posShiftDoc?.sync();
|
||||
await transferPOSCashAndWriteOff(fyo, this.posShiftDoc as POSShift);
|
||||
|
||||
this.$emit('toggleModal', 'ShiftClose');
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'short',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
223
src/pages/POS/OpenPOSShiftModal.vue
Normal file
223
src/pages/POS/OpenPOSShiftModal.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<Modal class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Open POS Shift</h1>
|
||||
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Cash In Denominations</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="mt-4 text-base"
|
||||
:df="getField('openingCash')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.openingCash"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Opening Amount</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="mt-4 text-base"
|
||||
:df="getField('openingAmounts')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:max-rows-before-overflow="4"
|
||||
:value="posShiftDoc?.openingAmounts"
|
||||
:read-only="true"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<Button class="w-full py-5 bg-red-500" @click="$router.back()">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Back` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
import { AccountTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { computed } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OpenPOSShift',
|
||||
components: { Button, Modal, Table },
|
||||
provide() {
|
||||
return {
|
||||
doc: computed(() => this.posShiftDoc),
|
||||
};
|
||||
},
|
||||
emits: ['toggleModal'],
|
||||
data() {
|
||||
return {
|
||||
posShiftDoc: undefined as POSShift | undefined,
|
||||
|
||||
isValuesSeeded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
getDefaultCashDenominations() {
|
||||
return this.fyo.singles.Defaults?.posCashDenominations;
|
||||
},
|
||||
posCashAccount() {
|
||||
return fyo.singles.POSSettings?.cashAccount;
|
||||
},
|
||||
posOpeningCashAmount(): Money {
|
||||
return this.posShiftDoc?.openingCashAmount as Money;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.isValuesSeeded = false;
|
||||
this.posShiftDoc = fyo.singles[ModelNameEnum.POSShift];
|
||||
|
||||
await this.seedDefaults();
|
||||
this.isValuesSeeded = true;
|
||||
},
|
||||
methods: {
|
||||
async seedDefaultCashDenomiations() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingCash = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
const denominations = this.getDefaultCashDenominations;
|
||||
|
||||
if (!denominations) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of denominations) {
|
||||
await this.posShiftDoc.append('openingCash', {
|
||||
denomination: row.denomination,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
await this.posShiftDoc.sync();
|
||||
}
|
||||
},
|
||||
async seedPaymentMethods() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingAmounts = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
await this.posShiftDoc.set('openingAmounts', [
|
||||
{
|
||||
paymentMethod: 'Cash',
|
||||
amount: fyo.pesa(0),
|
||||
},
|
||||
{
|
||||
paymentMethod: 'Transfer',
|
||||
amount: fyo.pesa(0),
|
||||
},
|
||||
]);
|
||||
await this.posShiftDoc.sync();
|
||||
},
|
||||
async seedDefaults() {
|
||||
if (!!this.posShiftDoc?.isShiftOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.seedDefaultCashDenomiations();
|
||||
await this.seedPaymentMethods();
|
||||
},
|
||||
getField(fieldname: string) {
|
||||
return this.fyo.getField(ModelNameEnum.POSShift, fieldname);
|
||||
},
|
||||
setOpeningCashAmount() {
|
||||
if (!this.posShiftDoc?.openingAmounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingAmounts.map((row) => {
|
||||
if (row.paymentMethod === 'Cash') {
|
||||
row.amount = this.posShiftDoc?.openingCashAmount as Money;
|
||||
}
|
||||
});
|
||||
},
|
||||
async handleChange() {
|
||||
await this.posShiftDoc?.sync();
|
||||
this.setOpeningCashAmount();
|
||||
},
|
||||
async handleSubmit() {
|
||||
try {
|
||||
if (this.posShiftDoc?.openingCashAmount.isNegative()) {
|
||||
throw new ValidationError(
|
||||
t`Opening Cash Amount can not be negative.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.posShiftDoc?.setMultiple({
|
||||
isShiftOpen: true,
|
||||
openingDate: new Date(),
|
||||
});
|
||||
|
||||
await this.posShiftDoc?.sync();
|
||||
|
||||
if (!this.posShiftDoc?.openingCashAmount.isZero()) {
|
||||
const jvDoc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
|
||||
entryType: 'Journal Entry',
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: this.posCashAccount,
|
||||
debit: this.posShiftDoc?.openingCashAmount as Money,
|
||||
credit: this.fyo.pesa(0),
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: this.fyo.pesa(0),
|
||||
credit: this.posShiftDoc?.openingCashAmount as Money,
|
||||
});
|
||||
|
||||
await (await jvDoc.sync()).submit();
|
||||
}
|
||||
|
||||
this.$emit('toggleModal', 'ShiftOpen');
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'short',
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
550
src/pages/POS/POS.vue
Normal file
550
src/pages/POS/POS.vue
Normal file
@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<PageHeader :title="t`Point of Sale`">
|
||||
<slot>
|
||||
<Button class="bg-red-500" @click="toggleModal('ShiftClose')">
|
||||
<span class="font-medium text-white">{{ t`Close POS Shift ` }}</span>
|
||||
</Button>
|
||||
</slot>
|
||||
</PageHeader>
|
||||
|
||||
<OpenPOSShiftModal
|
||||
v-if="!isPosShiftOpen"
|
||||
:open-modal="!isPosShiftOpen"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<ClosePOSShiftModal
|
||||
:open-modal="openShiftCloseModal"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<PaymentModal
|
||||
:open-modal="openPaymentModal"
|
||||
@create-transaction="createTransaction"
|
||||
@toggle-modal="toggleModal"
|
||||
@set-cash-amount="setCashAmount"
|
||||
@set-transfer-amount="setTransferAmount"
|
||||
@set-transfer-ref-no="setTransferRefNo"
|
||||
@set-transfer-clearance-date="setTransferClearanceDate"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="bg-gray-25 gap-2 grid grid-cols-12 p-4"
|
||||
style="height: calc(100vh - var(--h-row-largest))"
|
||||
>
|
||||
<div class="bg-white border col-span-5 rounded-md">
|
||||
<div class="rounded-md p-4 col-span-5">
|
||||
<!-- Item Search -->
|
||||
<Link
|
||||
class="border-r flex-shrink-0 w-full"
|
||||
:df="{
|
||||
label: t`Search an Item`,
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'item',
|
||||
target: 'Item',
|
||||
}"
|
||||
:border="true"
|
||||
:value="itemSearchTerm"
|
||||
@keyup.enter="
|
||||
async () => await addItem(await getItem(itemSearchTerm))
|
||||
"
|
||||
@change="(item: string) =>itemSearchTerm= item"
|
||||
/>
|
||||
<ItemsTable @add-item="addItem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-7">
|
||||
<div class="flex flex-col gap-3" style="height: calc(100vh - 6rem)">
|
||||
<div class="bg-white border grow h-full p-4 rounded-md">
|
||||
<!-- Customer Search -->
|
||||
<Link
|
||||
v-if="sinvDoc.fieldMap"
|
||||
class="flex-shrink-0"
|
||||
:border="true"
|
||||
:value="sinvDoc.party"
|
||||
:df="sinvDoc.fieldMap.party"
|
||||
@change="(value:string) => (sinvDoc.party = value)"
|
||||
/>
|
||||
|
||||
<SelectedItemTable />
|
||||
</div>
|
||||
|
||||
<div class="bg-white border p-4 rounded-md">
|
||||
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
|
||||
<div class="">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<FloatingLabelFloatInput
|
||||
:df="{
|
||||
label: t`Total Quantity`,
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'totalQuantity',
|
||||
minvalue: 0,
|
||||
maxvalue: 1000,
|
||||
}"
|
||||
size="large"
|
||||
:value="totalQuantity"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
|
||||
<FloatingLabelCurrencyInput
|
||||
:df="{
|
||||
label: t`Add'l Discounts`,
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'additionalDiscount',
|
||||
minvalue: 0,
|
||||
}"
|
||||
size="large"
|
||||
:value="additionalDiscounts"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
@change="(amount:Money)=> additionalDiscounts= amount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<FloatingLabelCurrencyInput
|
||||
:df="{
|
||||
label: t`Item Discounts`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'itemDiscounts',
|
||||
}"
|
||||
size="large"
|
||||
:value="itemDiscounts"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
<FloatingLabelCurrencyInput
|
||||
v-if="sinvDoc.fieldMap"
|
||||
:df="sinvDoc.fieldMap.grandTotal"
|
||||
size="large"
|
||||
:value="sinvDoc.grandTotal"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<Button
|
||||
class="w-full bg-red-500 py-6"
|
||||
:disabled="!sinvDoc.items?.length"
|
||||
@click="clearValues"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="mt-4 w-full bg-green-500 py-6"
|
||||
:disabled="disablePayButton"
|
||||
@click="toggleModal('Payment', true)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Pay` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
|
||||
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
||||
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
||||
import ItemsTable from 'src/components/POS/ItemsTable.vue';
|
||||
import Link from 'src/components/Controls/Link.vue';
|
||||
import OpenPOSShiftModal from './OpenPOSShiftModal.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import PaymentModal from './PaymentModal.vue';
|
||||
import SelectedItemTable from 'src/components/POS/SelectedItemTable.vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { routeTo, toggleSidebar } from 'src/utils/ui';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { t } from 'fyo';
|
||||
import {
|
||||
ItemQtyMap,
|
||||
ItemSerialNumbers,
|
||||
POSItem,
|
||||
} from 'src/components/POS/types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { ModalName } from 'src/components/POS/types';
|
||||
import { Money } from 'pesa';
|
||||
import { Payment } from 'models/baseModels/Payment/Payment';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { Shipment } from 'models/inventory/Shipment';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import {
|
||||
getItem,
|
||||
getItemDiscounts,
|
||||
getItemQtyMap,
|
||||
getTotalQuantity,
|
||||
getTotalTaxedAmount,
|
||||
validateIsPosSettingsSet,
|
||||
validateShipment,
|
||||
validateSinv,
|
||||
} from 'src/utils/pos';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'POS',
|
||||
components: {
|
||||
Button,
|
||||
ClosePOSShiftModal,
|
||||
FloatingLabelCurrencyInput,
|
||||
FloatingLabelFloatInput,
|
||||
ItemsTable,
|
||||
Link,
|
||||
OpenPOSShiftModal,
|
||||
PageHeader,
|
||||
PaymentModal,
|
||||
SelectedItemTable,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
cashAmount: computed(() => this.cashAmount),
|
||||
doc: computed(() => this.sinvDoc),
|
||||
isDiscountingEnabled: computed(() => this.isDiscountingEnabled),
|
||||
itemDiscounts: computed(() => this.itemDiscounts),
|
||||
itemQtyMap: computed(() => this.itemQtyMap),
|
||||
itemSerialNumbers: computed(() => this.itemSerialNumbers),
|
||||
sinvDoc: computed(() => this.sinvDoc),
|
||||
totalTaxedAmount: computed(() => this.totalTaxedAmount),
|
||||
transferAmount: computed(() => this.transferAmount),
|
||||
transferClearanceDate: computed(() => this.transferClearanceDate),
|
||||
transferRefNo: computed(() => this.transferRefNo),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isItemsSeeded: false,
|
||||
openPaymentModal: false,
|
||||
openShiftCloseModal: false,
|
||||
openShiftOpenModal: false,
|
||||
|
||||
additionalDiscounts: fyo.pesa(0),
|
||||
cashAmount: fyo.pesa(0),
|
||||
itemDiscounts: fyo.pesa(0),
|
||||
totalTaxedAmount: fyo.pesa(0),
|
||||
transferAmount: fyo.pesa(0),
|
||||
|
||||
totalQuantity: 0,
|
||||
|
||||
defaultCustomer: undefined as string | undefined,
|
||||
itemSearchTerm: '',
|
||||
transferRefNo: undefined as string | undefined,
|
||||
|
||||
transferClearanceDate: undefined as Date | undefined,
|
||||
|
||||
itemQtyMap: {} as ItemQtyMap,
|
||||
itemSerialNumbers: {} as ItemSerialNumbers,
|
||||
paymentDoc: {} as Payment,
|
||||
sinvDoc: {} as SalesInvoice,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultPOSCashAccount: () =>
|
||||
fyo.singles.POSSettings?.cashAccount ?? undefined,
|
||||
isDiscountingEnabled(): boolean {
|
||||
return !!fyo.singles.AccountingSettings?.enableDiscounting;
|
||||
},
|
||||
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
|
||||
isPaymentAmountSet(): boolean {
|
||||
if (this.sinvDoc.grandTotal?.isZero()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.cashAmount.isZero() && this.transferAmount.isZero()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
disablePayButton(): boolean {
|
||||
if (!this.sinvDoc.items?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.sinvDoc.party) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sinvDoc: {
|
||||
handler() {
|
||||
this.updateValues();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
toggleSidebar(false);
|
||||
validateIsPosSettingsSet(fyo);
|
||||
this.setSinvDoc();
|
||||
this.setDefaultCustomer();
|
||||
await this.setItemQtyMap();
|
||||
},
|
||||
deactivated() {
|
||||
toggleSidebar(true);
|
||||
},
|
||||
methods: {
|
||||
setCashAmount(amount: Money) {
|
||||
this.cashAmount = amount;
|
||||
},
|
||||
setDefaultCustomer() {
|
||||
this.defaultCustomer = this.fyo.singles.Defaults?.posCustomer ?? '';
|
||||
this.sinvDoc.party = this.defaultCustomer;
|
||||
},
|
||||
setItemDiscounts() {
|
||||
this.itemDiscounts = getItemDiscounts(
|
||||
this.sinvDoc.items as SalesInvoiceItem[]
|
||||
);
|
||||
},
|
||||
async setItemQtyMap() {
|
||||
this.itemQtyMap = await getItemQtyMap();
|
||||
},
|
||||
setSinvDoc() {
|
||||
this.sinvDoc = this.fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
account: 'Debtors',
|
||||
party: this.sinvDoc.party ?? this.defaultCustomer,
|
||||
isPOS: true,
|
||||
}) as SalesInvoice;
|
||||
},
|
||||
setTotalQuantity() {
|
||||
this.totalQuantity = getTotalQuantity(
|
||||
this.sinvDoc.items as SalesInvoiceItem[]
|
||||
);
|
||||
},
|
||||
setTotalTaxedAmount() {
|
||||
this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice);
|
||||
},
|
||||
setTransferAmount(amount: Money = fyo.pesa(0)) {
|
||||
this.transferAmount = amount;
|
||||
},
|
||||
setTransferClearanceDate(date: Date) {
|
||||
this.transferClearanceDate = date;
|
||||
},
|
||||
setTransferRefNo(ref: string) {
|
||||
this.transferRefNo = ref;
|
||||
},
|
||||
|
||||
async addItem(item: POSItem | Item | undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.sinvDoc.runFormulas();
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.itemQtyMap[item.name as string] ||
|
||||
this.itemQtyMap[item.name as string].availableQty === 0
|
||||
) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`Item ${item.name as string} has Zero Quantity`,
|
||||
duration: 'short',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingItems =
|
||||
this.sinvDoc.items?.filter(
|
||||
(invoiceItem) => invoiceItem.item === item.name
|
||||
) ?? [];
|
||||
|
||||
if (item.hasBatch) {
|
||||
for (const item of existingItems) {
|
||||
const itemQty = item.quantity ?? 0;
|
||||
const qtyInBatch =
|
||||
this.itemQtyMap[item.item as string][item.batch as string] ?? 0;
|
||||
|
||||
if (itemQty < qtyInBatch) {
|
||||
item.quantity = (item.quantity as number) + 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingItems.length) {
|
||||
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
});
|
||||
},
|
||||
async createTransaction(shouldPrint = false) {
|
||||
try {
|
||||
await this.validate();
|
||||
await this.submitSinvDoc(shouldPrint);
|
||||
await this.makePayment();
|
||||
await this.makeStockTransfer();
|
||||
await this.afterTransaction();
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async makePayment() {
|
||||
this.paymentDoc = this.sinvDoc.getPayment() as Payment;
|
||||
const paymentMethod = this.cashAmount.isZero() ? 'Transfer' : 'Cash';
|
||||
await this.paymentDoc.set('paymentMethod', paymentMethod);
|
||||
|
||||
if (paymentMethod === 'Transfer') {
|
||||
await this.paymentDoc.setMultiple({
|
||||
amount: this.transferAmount as Money,
|
||||
referenceId: this.transferRefNo,
|
||||
clearanceDate: this.transferClearanceDate,
|
||||
});
|
||||
}
|
||||
|
||||
if (paymentMethod === 'Cash') {
|
||||
await this.paymentDoc.setMultiple({
|
||||
paymentAccount: this.defaultPOSCashAccount,
|
||||
amount: this.cashAmount as Money,
|
||||
});
|
||||
}
|
||||
|
||||
this.paymentDoc.once('afterSubmit', () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Payment ${this.paymentDoc.name as string} is Saved`,
|
||||
duration: 'short',
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await this.paymentDoc?.sync();
|
||||
await this.paymentDoc?.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async makeStockTransfer() {
|
||||
const shipmentDoc = (await this.sinvDoc.getStockTransfer()) as Shipment;
|
||||
if (!shipmentDoc.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of shipmentDoc.items) {
|
||||
item.location = fyo.singles.POSSettings?.inventory;
|
||||
item.serialNumber =
|
||||
this.itemSerialNumbers[item.item as string] ?? undefined;
|
||||
}
|
||||
|
||||
shipmentDoc.once('afterSubmit', () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Shipment ${shipmentDoc.name as string} is Submitted`,
|
||||
duration: 'short',
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await shipmentDoc.sync();
|
||||
await shipmentDoc.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async submitSinvDoc(shouldPrint: boolean) {
|
||||
this.sinvDoc.once('afterSubmit', async () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Sales Invoice ${this.sinvDoc.name as string} is Submitted`,
|
||||
duration: 'short',
|
||||
});
|
||||
|
||||
if (shouldPrint) {
|
||||
await routeTo(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`/print/${this.sinvDoc.schemaName}/${this.sinvDoc.name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await this.validate();
|
||||
await this.sinvDoc.runFormulas();
|
||||
await this.sinvDoc.sync();
|
||||
await this.sinvDoc.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async afterTransaction() {
|
||||
await this.setItemQtyMap();
|
||||
this.clearValues();
|
||||
this.setSinvDoc();
|
||||
this.toggleModal('Payment', false);
|
||||
},
|
||||
clearValues() {
|
||||
this.setSinvDoc();
|
||||
this.itemSerialNumbers = {};
|
||||
|
||||
this.cashAmount = fyo.pesa(0);
|
||||
this.transferAmount = fyo.pesa(0);
|
||||
},
|
||||
toggleModal(modal: ModalName, value?: boolean) {
|
||||
if (value) {
|
||||
return (this[`open${modal}Modal`] = value);
|
||||
}
|
||||
return (this[`open${modal}Modal`] = !this[`open${modal}Modal`]);
|
||||
},
|
||||
updateValues() {
|
||||
this.setTotalQuantity();
|
||||
this.setItemDiscounts();
|
||||
this.setTotalTaxedAmount();
|
||||
},
|
||||
async validate() {
|
||||
validateSinv(this.sinvDoc as SalesInvoice, this.itemQtyMap);
|
||||
await validateShipment(this.itemSerialNumbers);
|
||||
},
|
||||
|
||||
getItem,
|
||||
},
|
||||
});
|
||||
</script>
|
325
src/pages/POS/PaymentModal.vue
Normal file
325
src/pages/POS/PaymentModal.vue
Normal file
@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<Modal class="w-2/6 ml-auto mr-3.5" :set-close-listener="false">
|
||||
<div v-if="sinvDoc.fieldMap" class="px-4 py-6 grid" style="height: 95vh">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<Currency
|
||||
:df="fyo.fieldMap.PaymentFor.amount"
|
||||
:read-only="!transferAmount.isZero()"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="cashAmount"
|
||||
@change="(amount:Money)=> $emit('setCashAmount', amount)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="w-full py-5 bg-teal-500"
|
||||
@click="setCashOrTransferAmount"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cash` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Currency
|
||||
:df="fyo.fieldMap.PaymentFor.amount"
|
||||
:read-only="!cashAmount.isZero()"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="transferAmount"
|
||||
@change="(value:Money)=> $emit('setTransferAmount', value)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="w-full py-5 bg-teal-500"
|
||||
@click="setCashOrTransferAmount('Transfer')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Transfer` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-2 gap-6">
|
||||
<Data
|
||||
v-show="!transferAmount.isZero()"
|
||||
:df="fyo.fieldMap.Payment.referenceId"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="!transferAmount.isZero()"
|
||||
:value="transferRefNo"
|
||||
@change="(value:string) => $emit('setTransferRefNo', value)"
|
||||
/>
|
||||
|
||||
<Date
|
||||
v-show="!transferAmount.isZero()"
|
||||
:df="fyo.fieldMap.Payment.clearanceDate"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="!transferAmount.isZero()"
|
||||
:value="transferClearanceDate"
|
||||
@change="(value:Date) => $emit('setTransferClearanceDate', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-14 grid grid-cols-2 gap-6">
|
||||
<Currency
|
||||
v-show="showPaidChange"
|
||||
:df="{
|
||||
label: t`Paid Change`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'paidChange',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="paidChange"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
v-show="showBalanceAmount"
|
||||
:df="{
|
||||
label: t`Balance Amount`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'balanceAmount',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="balanceAmount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-14 row-start-4 row-span-2 grid grid-cols-2 gap-x-6 gap-y-11"
|
||||
>
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.netTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.netTotal"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
label: t`Taxes and Charges`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'taxesAndCharges',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="totalTaxedAmount"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.baseGrandTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.baseGrandTotal"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="sinvDoc.fieldMap.discountAmount"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="itemDiscounts"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.grandTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.grandTotal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-red-500"
|
||||
style="padding: 1.35rem"
|
||||
@click="$emit('toggleModal', 'Payment')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<Button
|
||||
class="w-full bg-blue-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="disableSubmitButton"
|
||||
@click="$emit('createTransaction')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button
|
||||
class="w-full bg-green-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="disableSubmitButton"
|
||||
@click="$emit('createTransaction', true)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit & Print` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Currency from 'src/components/Controls/Currency.vue';
|
||||
import Data from 'src/components/Controls/Data.vue';
|
||||
import Date from 'src/components/Controls/Date.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import { Money } from 'pesa';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PaymentModal',
|
||||
components: {
|
||||
Modal,
|
||||
Currency,
|
||||
Button,
|
||||
Data,
|
||||
Date,
|
||||
},
|
||||
emits: [
|
||||
'createTransaction',
|
||||
'setCashAmount',
|
||||
'setTransferAmount',
|
||||
'setTransferClearanceDate',
|
||||
'setTransferRefNo',
|
||||
'toggleModal',
|
||||
],
|
||||
setup() {
|
||||
return {
|
||||
cashAmount: inject('cashAmount') as Money,
|
||||
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
|
||||
itemDiscounts: inject('itemDiscounts') as Money,
|
||||
transferAmount: inject('transferAmount') as Money,
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
transferRefNo: inject('transferRefNo') as string,
|
||||
transferClearanceDate: inject('transferClearanceDate') as Date,
|
||||
totalTaxedAmount: inject('totalTaxedAmount') as Money,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
balanceAmount(): Money {
|
||||
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
|
||||
|
||||
if (this.cashAmount.isZero()) {
|
||||
return grandTotal.sub(this.transferAmount);
|
||||
}
|
||||
|
||||
return grandTotal.sub(this.cashAmount);
|
||||
},
|
||||
paidChange(): Money {
|
||||
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
|
||||
|
||||
if (this.cashAmount.isZero()) {
|
||||
return this.transferAmount.sub(grandTotal);
|
||||
}
|
||||
|
||||
return this.cashAmount.sub(grandTotal);
|
||||
},
|
||||
showBalanceAmount(): boolean {
|
||||
if (
|
||||
this.cashAmount.eq(fyo.pesa(0)) &&
|
||||
this.transferAmount.eq(fyo.pesa(0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cashAmount.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.transferAmount.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
showPaidChange(): boolean {
|
||||
if (
|
||||
this.cashAmount.eq(fyo.pesa(0)) &&
|
||||
this.transferAmount.eq(fyo.pesa(0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cashAmount.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.transferAmount.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
disableSubmitButton(): boolean {
|
||||
if (
|
||||
!this.sinvDoc.grandTotal?.isZero() &&
|
||||
this.transferAmount.isZero() &&
|
||||
this.cashAmount.isZero()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.cashAmount.isZero() &&
|
||||
(!this.transferRefNo || !this.transferClearanceDate)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setCashOrTransferAmount(paymentMethod = 'Cash') {
|
||||
if (paymentMethod === 'Transfer') {
|
||||
this.$emit('setCashAmount', fyo.pesa(0));
|
||||
this.$emit('setTransferAmount', this.sinvDoc?.grandTotal);
|
||||
return;
|
||||
}
|
||||
this.$emit('setTransferAmount', fyo.pesa(0));
|
||||
this.$emit('setCashAmount', this.sinvDoc?.grandTotal);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -115,6 +115,7 @@ export default defineComponent({
|
||||
ModelNameEnum.AccountingSettings,
|
||||
ModelNameEnum.InventorySettings,
|
||||
ModelNameEnum.Defaults,
|
||||
ModelNameEnum.POSSettings,
|
||||
ModelNameEnum.PrintSettings,
|
||||
ModelNameEnum.SystemSettings,
|
||||
].some((s) => this.fyo.singles[s]?.canSave);
|
||||
@ -133,6 +134,7 @@ export default defineComponent({
|
||||
[ModelNameEnum.PrintSettings]: this.t`Print`,
|
||||
[ModelNameEnum.InventorySettings]: this.t`Inventory`,
|
||||
[ModelNameEnum.Defaults]: this.t`Defaults`,
|
||||
[ModelNameEnum.POSSettings]: this.t`POS Settings`,
|
||||
[ModelNameEnum.SystemSettings]: this.t`System`,
|
||||
};
|
||||
},
|
||||
@ -140,16 +142,26 @@ export default defineComponent({
|
||||
const enableInventory =
|
||||
!!this.fyo.singles.AccountingSettings?.enableInventory;
|
||||
|
||||
const enablePOS = !!this.fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
|
||||
return [
|
||||
ModelNameEnum.AccountingSettings,
|
||||
ModelNameEnum.InventorySettings,
|
||||
ModelNameEnum.Defaults,
|
||||
ModelNameEnum.POSSettings,
|
||||
ModelNameEnum.PrintSettings,
|
||||
ModelNameEnum.SystemSettings,
|
||||
]
|
||||
.filter((s) =>
|
||||
s === ModelNameEnum.InventorySettings ? enableInventory : true
|
||||
)
|
||||
.filter((s) => {
|
||||
if (s === ModelNameEnum.InventorySettings && !enableInventory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s === ModelNameEnum.POSSettings && !enablePOS) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((s) => this.fyo.schemaMap[s]!);
|
||||
},
|
||||
activeGroup(): Map<string, Field[]> {
|
||||
|
@ -11,6 +11,7 @@ import Report from 'src/pages/Report.vue';
|
||||
import Settings from 'src/pages/Settings/Settings.vue';
|
||||
import TemplateBuilder from 'src/pages/TemplateBuilder/TemplateBuilder.vue';
|
||||
import CustomizeForm from 'src/pages/CustomizeForm/CustomizeForm.vue';
|
||||
import POS from 'src/pages/POS/POS.vue';
|
||||
import type { HistoryState } from 'vue-router';
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import { historyState } from './utils/refs';
|
||||
@ -124,6 +125,18 @@ const routes: RouteRecordRaw[] = [
|
||||
edit: (route) => route.query,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/pos',
|
||||
name: 'Point of Sale',
|
||||
components: {
|
||||
default: POS,
|
||||
edit: QuickEditForm,
|
||||
},
|
||||
props: {
|
||||
default: true,
|
||||
edit: (route) => route.query,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({ routes, history: createWebHistory() });
|
||||
|
294
src/utils/pos.ts
Normal file
294
src/utils/pos.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { AccountTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { ValuationMethod } from 'models/inventory/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import {
|
||||
getRawStockLedgerEntries,
|
||||
getStockBalanceEntries,
|
||||
getStockLedgerEntries,
|
||||
} from 'reports/inventory/helpers';
|
||||
import { ItemQtyMap, ItemSerialNumbers } from 'src/components/POS/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { showToast } from './interactive';
|
||||
|
||||
export async function getItemQtyMap(): Promise<ItemQtyMap> {
|
||||
const itemQtyMap: ItemQtyMap = {};
|
||||
const valuationMethod =
|
||||
fyo.singles.InventorySettings?.valuationMethod ?? ValuationMethod.FIFO;
|
||||
|
||||
const rawSLEs = await getRawStockLedgerEntries(fyo);
|
||||
const rawData = getStockLedgerEntries(rawSLEs, valuationMethod);
|
||||
const posInventory = fyo.singles.POSSettings?.inventory;
|
||||
|
||||
const stockBalance = getStockBalanceEntries(rawData, {
|
||||
location: posInventory,
|
||||
});
|
||||
|
||||
for (const row of stockBalance) {
|
||||
if (!itemQtyMap[row.item]) {
|
||||
itemQtyMap[row.item] = { availableQty: 0 };
|
||||
}
|
||||
|
||||
if (row.batch) {
|
||||
itemQtyMap[row.item][row.batch] = row.balanceQuantity;
|
||||
}
|
||||
|
||||
itemQtyMap[row.item].availableQty += row.balanceQuantity;
|
||||
}
|
||||
return itemQtyMap;
|
||||
}
|
||||
|
||||
export function getTotalQuantity(items: SalesInvoiceItem[]): number {
|
||||
let totalQuantity = safeParseFloat(0);
|
||||
|
||||
if (!items.length) {
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const quantity = item.quantity ?? 0;
|
||||
totalQuantity = safeParseFloat(totalQuantity + quantity);
|
||||
}
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
export function getItemDiscounts(items: SalesInvoiceItem[]): Money {
|
||||
let itemDiscounts = fyo.pesa(0);
|
||||
|
||||
if (!items.length) {
|
||||
return itemDiscounts;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.itemDiscountAmount?.isZero()) {
|
||||
itemDiscounts = itemDiscounts.add(item.itemDiscountAmount as Money);
|
||||
}
|
||||
|
||||
if (item.amount && (item.itemDiscountPercent as number) > 1) {
|
||||
itemDiscounts = itemDiscounts.add(
|
||||
item.amount.percent(item.itemDiscountPercent as number)
|
||||
);
|
||||
}
|
||||
}
|
||||
return itemDiscounts;
|
||||
}
|
||||
|
||||
export async function getItem(item: string): Promise<Item | undefined> {
|
||||
const itemDoc = (await fyo.doc.getDoc(ModelNameEnum.Item, item)) as Item;
|
||||
if (!itemDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
return itemDoc;
|
||||
}
|
||||
|
||||
export function validateSinv(sinvDoc: SalesInvoice, itemQtyMap: ItemQtyMap) {
|
||||
if (!sinvDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateSinvItems(sinvDoc.items as SalesInvoiceItem[], itemQtyMap);
|
||||
}
|
||||
|
||||
function validateSinvItems(
|
||||
sinvItems: SalesInvoiceItem[],
|
||||
itemQtyMap: ItemQtyMap
|
||||
) {
|
||||
for (const item of sinvItems) {
|
||||
if (!item.quantity || item.quantity < 1) {
|
||||
throw new ValidationError(
|
||||
t`Invalid Quantity for Item ${item.item as string}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!itemQtyMap[item.item as string]) {
|
||||
throw new ValidationError(t`Item ${item.item as string} not in Stock`);
|
||||
}
|
||||
|
||||
if (item.quantity > itemQtyMap[item.item as string].availableQty) {
|
||||
throw new ValidationError(
|
||||
t`Insufficient Quantity. Item ${item.item as string} has only ${
|
||||
itemQtyMap[item.item as string].availableQty
|
||||
} quantities available. you selected ${item.quantity}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateShipment(itemSerialNumbers: ItemSerialNumbers) {
|
||||
if (!itemSerialNumbers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const idx in itemSerialNumbers) {
|
||||
const serialNumbers = itemSerialNumbers[idx].split('\n');
|
||||
|
||||
for (const serialNumber of serialNumbers) {
|
||||
const status = await fyo.getValue(
|
||||
ModelNameEnum.SerialNumber,
|
||||
serialNumber,
|
||||
'status'
|
||||
);
|
||||
|
||||
if (status !== 'Active') {
|
||||
throw new ValidationError(
|
||||
t`Serial Number ${serialNumber} status is not Active.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateIsPosSettingsSet(fyo: Fyo) {
|
||||
try {
|
||||
const inventory = fyo.singles.POSSettings?.inventory;
|
||||
if (!inventory) {
|
||||
throw new ValidationError(
|
||||
t`POS Inventory is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
|
||||
const cashAccount = fyo.singles.POSSettings?.cashAccount;
|
||||
if (!cashAccount) {
|
||||
throw new ValidationError(
|
||||
t`POS Counter Cash Account is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
|
||||
const writeOffAccount = fyo.singles.POSSettings?.writeOffAccount;
|
||||
if (!writeOffAccount) {
|
||||
throw new ValidationError(
|
||||
t`POS Write Off Account is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'long',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTotalTaxedAmount(sinvDoc: SalesInvoice): Money {
|
||||
let totalTaxedAmount = fyo.pesa(0);
|
||||
if (!sinvDoc.items?.length || !sinvDoc.taxes?.length) {
|
||||
return totalTaxedAmount;
|
||||
}
|
||||
|
||||
for (const row of sinvDoc.taxes) {
|
||||
totalTaxedAmount = totalTaxedAmount.add(row.amount as Money);
|
||||
}
|
||||
return totalTaxedAmount;
|
||||
}
|
||||
|
||||
export function validateClosingAmounts(posShiftDoc: POSShift) {
|
||||
try {
|
||||
if (!posShiftDoc) {
|
||||
throw new ValidationError(
|
||||
`POS Shift Document not loaded. Please reload.`
|
||||
);
|
||||
}
|
||||
|
||||
posShiftDoc.closingAmounts?.map((row) => {
|
||||
if (row.closingAmount?.isNegative()) {
|
||||
throw new ValidationError(
|
||||
t`Closing ${row.paymentMethod as string} Amount can not be negative.`
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
export async function transferPOSCashAndWriteOff(
|
||||
fyo: Fyo,
|
||||
posShiftDoc: POSShift
|
||||
) {
|
||||
const expectedCashAmount = posShiftDoc.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.expectedAmount as Money;
|
||||
|
||||
if (expectedCashAmount.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closingCashAmount = posShiftDoc.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.closingAmount as Money;
|
||||
|
||||
const jvDoc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
|
||||
entryType: 'Journal Entry',
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: closingCashAmount,
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.cashAccount,
|
||||
credit: closingCashAmount,
|
||||
});
|
||||
|
||||
const differenceAmount = posShiftDoc?.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.differenceAmount as Money;
|
||||
|
||||
if (differenceAmount.isNegative()) {
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: differenceAmount.abs(),
|
||||
credit: fyo.pesa(0),
|
||||
});
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.writeOffAccount,
|
||||
debit: fyo.pesa(0),
|
||||
credit: differenceAmount.abs(),
|
||||
});
|
||||
}
|
||||
|
||||
if (!differenceAmount.isZero() && differenceAmount.isPositive()) {
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.writeOffAccount,
|
||||
debit: differenceAmount,
|
||||
credit: fyo.pesa(0),
|
||||
});
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: fyo.pesa(0),
|
||||
credit: differenceAmount,
|
||||
});
|
||||
}
|
||||
|
||||
await (await jvDoc.sync()).submit();
|
||||
}
|
||||
|
||||
export function validateSerialNumberCount(
|
||||
serialNumbers: string | undefined,
|
||||
quantity: number,
|
||||
item: string
|
||||
) {
|
||||
let serialNumberCount = 0;
|
||||
|
||||
if (serialNumbers) {
|
||||
serialNumberCount = serialNumbers.split('\n').length;
|
||||
}
|
||||
|
||||
if (quantity !== serialNumberCount) {
|
||||
const errorMessage = t`Need ${quantity} Serial Numbers for Item ${item}. You have provided ${serialNumberCount}`;
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
duration: 'long',
|
||||
});
|
||||
throw new ValidationError(errorMessage);
|
||||
}
|
||||
}
|
@ -101,6 +101,20 @@ function getInventorySidebar(): SidebarRoot[] {
|
||||
];
|
||||
}
|
||||
|
||||
function getPOSSidebar() {
|
||||
const isPOSEnabled = !!fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
if (!isPOSEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
label: t`POS`,
|
||||
name: 'pos',
|
||||
route: '/pos',
|
||||
icon: 'pos',
|
||||
};
|
||||
}
|
||||
|
||||
function getReportSidebar() {
|
||||
return {
|
||||
label: t`Reports`,
|
||||
@ -256,6 +270,7 @@ function getCompleteSidebar(): SidebarConfig {
|
||||
},
|
||||
getReportSidebar(),
|
||||
getInventorySidebar(),
|
||||
getPOSSidebar(),
|
||||
getRegionalSidebar(),
|
||||
{
|
||||
label: t`Setup`,
|
||||
|
Loading…
Reference in New Issue
Block a user