From 8b64671537c591dd0638d79670a5730d3bfe619b Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Fri, 19 Dec 2025 18:28:11 +0530 Subject: [PATCH 01/12] fix: add loyalty point expiry by checking date --- jobs/checkLoyaltyProgramExpiry.ts | 96 +++++++++++++ main/initSheduler.ts | 9 ++ models/baseModels/Invoice/Invoice.ts | 103 ++++++++++++- .../LoyaltyProgram/LoyaltyProgram.ts | 31 +++- .../baseModels/SalesInvoice/SalesInvoice.ts | 9 ++ .../tests/testLoyaltyProgram.spec.ts | 21 ++- models/helpers.ts | 135 ++++++++++++++++-- schemas/app/LoyaltyProgram.json | 22 ++- src/components/StatusPill.vue | 11 ++ src/pages/POS/LoyaltyProgramModal.vue | 24 ++-- 10 files changed, 430 insertions(+), 31 deletions(-) create mode 100644 jobs/checkLoyaltyProgramExpiry.ts diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts new file mode 100644 index 00000000..b1ba026f --- /dev/null +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -0,0 +1,96 @@ +import { parentPort } from 'worker_threads'; +import { DatabaseManager } from '../backend/database/manager'; +import { ModelNameEnum } from '../models/types'; + +if (parentPort) { + parentPort.postMessage({ type: 'check-loyalty-program-expiry' }); +} + +export async function checkLoyaltyProgramExpiry() { + const dm = new DatabaseManager(); + + try { + const currentDate = new Date(); + + const loyaltyPrograms = (await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['name', 'toDate', 'status', 'isEnabled'], + filters: { + status: ['!=', 'Expired'], + isEnabled: true, + }, + })) as Array<{ + name: string; + toDate: string; + status: string; + isEnabled: boolean; + }>; + + let expiredCount = 0; + let processedCount = 0; + + if (loyaltyPrograms) { + for (const program of loyaltyPrograms) { + processedCount++; + + if (program.toDate && new Date(program.toDate) <= currentDate) { + await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) + .where({ name: program.name }) + .update({ + status: 'Expired', + isEnabled: false, + }); + + expiredCount++; + } + } + } + + const result = { + timestamp: currentDate.toISOString(), + processedPrograms: processedCount, + expiredPrograms: expiredCount, + message: `Loyalty program expiry check completed. ${expiredCount} programs expired out of ${processedCount} processed.`, + }; + + if (parentPort) { + parentPort.postMessage({ + type: 'loyalty-program-expiry-complete', + data: result, + }); + } + + return result; + } catch (error) { + const errorResult = { + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : 'Unknown error', + message: 'Loyalty program expiry check failed', + }; + + if (parentPort) { + parentPort.postMessage({ + type: 'loyalty-program-expiry-error', + data: errorResult, + }); + } + + throw error; + } finally { + await dm.call('close'); + } +} + +checkLoyaltyProgramExpiry().catch((error) => { + const errorResult = { + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : 'Unknown error', + message: 'Loyalty program expiry check failed', + }; + + if (parentPort) { + parentPort.postMessage({ + type: 'loyalty-program-expiry-error', + data: errorResult, + }); + } +}); diff --git a/main/initSheduler.ts b/main/initSheduler.ts index 9a4a8551..ea80c2a0 100644 --- a/main/initSheduler.ts +++ b/main/initSheduler.ts @@ -24,6 +24,15 @@ export async function initScheduler(interval: string) { }, }, }, + { + name: 'checkLoyaltyProgramExpiry', + interval: '0 1 * * *', + worker: { + workerData: { + useTsNode: true, + }, + }, + }, ], worker: { argv: ['--require', 'ts-node/register'], diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 2fa9b2d5..1fcf0e1d 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -28,6 +28,8 @@ import { getReturnLoyaltyPoints, getItemQtyMap, getItemVisibility, + validateLoyaltyProgram, + getLoyaltyProgramTier, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -212,13 +214,49 @@ export abstract class Invoice extends Transactional { this.party )) as Party; - if ((this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0)) { + if (this.redeemLoyaltyPoints && (this.loyaltyPoints as number) > 0) { + const currentPoints = partyDoc?.loyaltyPoints || 0; + + let pointsToBeEarned = 0; + if (!this.isReturn && this.loyaltyProgram) { + const loyaltyProgramDoc = (await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + )) as LoyaltyProgram; + + const tier = getLoyaltyProgramTier( + loyaltyProgramDoc, + this?.grandTotal as Money + ); + + if (tier) { + const collectionFactor = tier.collectionFactor as number; + pointsToBeEarned = + Math.round(this?.grandTotal?.float || 0) * collectionFactor; + } + } + + const totalAvailablePoints = currentPoints + pointsToBeEarned; + if ((this.loyaltyPoints as number) > totalAvailablePoints) { + throw new ValidationError( + t`${ + this.party as string + } only has ${currentPoints} points (${pointsToBeEarned} will be earned from this transaction)` + ); + } + } else if ( + (this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0) + ) { throw new ValidationError( t`${this.party as string} only has ${ partyDoc.loyaltyPoints as number } points` ); } + + if (this.loyaltyProgram) { + await validateLoyaltyProgram(this, this.loyaltyProgram); + } } async afterSubmit() { @@ -275,6 +313,10 @@ export abstract class Invoice extends Transactional { if (this.schemaName === ModelNameEnum.SalesInvoice) { this.updateUsedCountOfCoupons(); } + + if (this.loyaltyProgram) { + await this.updateUsedCountOfLoyaltyProgram(); + } } async afterCancel() { @@ -284,6 +326,10 @@ export abstract class Invoice extends Transactional { await this._updateIsItemsReturned(); await this._removeLoyaltyPointEntry(); this.reduceUsedCountOfCoupons(); + + if (this.loyaltyProgram) { + await this.reduceUsedCountOfLoyaltyProgram(); + } } async _removeLoyaltyPointEntry() { @@ -838,6 +884,36 @@ export abstract class Invoice extends Transactional { }); } + async updateUsedCountOfLoyaltyProgram() { + if (!this.loyaltyProgram) { + return; + } + + const loyaltyProgramDoc = await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + ); + + await loyaltyProgramDoc.setAndSync({ + used: (loyaltyProgramDoc.used as number) + 1, + }); + } + + async reduceUsedCountOfLoyaltyProgram() { + if (!this.loyaltyProgram) { + return; + } + + const loyaltyProgramDoc = await this.fyo.doc.getDoc( + ModelNameEnum.LoyaltyProgram, + this.loyaltyProgram + ); + + await loyaltyProgramDoc.setAndSync({ + used: (loyaltyProgramDoc.used as number) - 1, + }); + } + async updateIsItemsFullyReturned(doc?: Invoice) { if (!doc?.returnAgainst || doc.schemaName !== ModelNameEnum.SalesInvoice) { return; @@ -898,11 +974,32 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram )) as LoyaltyProgram; - const expiryDate = this.date as Date; + // Check if loyalty program is enabled + if (!loyaltyProgramDoc.isEnabled) { + return; + } + + const invoiceDate = this.date as Date; const fromDate = loyaltyProgramDoc.fromDate as Date; const toDate = loyaltyProgramDoc.toDate as Date; - if (fromDate <= expiryDate && toDate >= expiryDate) { + const normalizedInvoiceDate = new Date(invoiceDate); + normalizedInvoiceDate.setHours(0, 0, 0, 0); + + const normalizedFromDate = new Date(fromDate); + normalizedFromDate.setHours(0, 0, 0, 0); + + const normalizedToDate = new Date(toDate); + normalizedToDate.setHours(0, 0, 0, 0); + + if (normalizedToDate.getTime() < normalizedInvoiceDate.getTime()) { + return; + } + + if ( + normalizedInvoiceDate.getTime() >= normalizedFromDate.getTime() && + normalizedInvoiceDate.getTime() <= normalizedToDate.getTime() + ) { const party = (await this.loadAndGetLink('party')) as Party; await createLoyaltyPointEntry(this); diff --git a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts index 89f73399..45d5b3f7 100644 --- a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts +++ b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts @@ -1,11 +1,38 @@ +import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FiltersMap, ListViewSettings } from 'fyo/model/types'; +import { FiltersMap, ListViewSettings, ValidationMap } from 'fyo/model/types'; +import { ValidationError } from 'fyo/utils/errors'; import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems'; import { AccountRootTypeEnum } from '../Account/types'; +import { getLoyaltyProgramStatusColumn } from '../../helpers'; export class LoyaltyProgram extends Doc { collectionRules?: CollectionRulesItems[]; expiryDuration?: number; + maximumUse?: number; + used?: number; + + validations: ValidationMap = { + used: (value: DocValue) => { + const used = value as number; + const maximumUse = this.maximumUse as number; + + if (used < 0) { + throw new ValidationError('Used count cannot be negative'); + } + + if (maximumUse > 0 && used > maximumUse) { + throw new ValidationError('Used count cannot exceed maximum use limit'); + } + }, + maximumUse: (value: DocValue) => { + const maximumUse = value as number; + + if (maximumUse < 0) { + throw new ValidationError('Maximum use cannot be negative'); + } + }, + }; static filters: FiltersMap = { expenseAccount: () => ({ @@ -16,7 +43,7 @@ export class LoyaltyProgram extends Doc { static getListViewSettings(): ListViewSettings { return { - columns: ['name', 'fromDate', 'toDate', 'expiryDuration'], + columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'], }; } } diff --git a/models/baseModels/SalesInvoice/SalesInvoice.ts b/models/baseModels/SalesInvoice/SalesInvoice.ts index a2ae5e7e..7fc42efe 100644 --- a/models/baseModels/SalesInvoice/SalesInvoice.ts +++ b/models/baseModels/SalesInvoice/SalesInvoice.ts @@ -115,6 +115,15 @@ export class SalesInvoice extends Invoice { ModelNameEnum.LoyaltyProgram, this.loyaltyProgram )) as LoyaltyProgram; + const toDate = loyaltyProgramDoc?.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + throw new ValidationError( + t`Loyalty program has expired and cannot be applied` + ); + } if (!this?.grandTotal) { return; diff --git a/models/baseModels/tests/testLoyaltyProgram.spec.ts b/models/baseModels/tests/testLoyaltyProgram.spec.ts index 2be654b8..8a1b9657 100644 --- a/models/baseModels/tests/testLoyaltyProgram.spec.ts +++ b/models/baseModels/tests/testLoyaltyProgram.spec.ts @@ -27,10 +27,17 @@ const partyData = { email: 'john@whoe.com', }; +const today = new Date(); +const fromDate = new Date(today); +fromDate.setDate(today.getDate() - 10); + +const toDate = new Date(today); +toDate.setDate(today.getDate() + 20); + const loyaltyProgramData = { name: 'program', - fromDate: new Date('12/10/2024'), - toDate: new Date('12/30/2024'), + fromDate: fromDate, + toDate: toDate, email: 'sample@gmail.com', mobile: '1234567890', expenseAccount: accountData.name, @@ -118,11 +125,11 @@ async function loyaltyPointEntryDoc(sinvName: string) { } } -async function createSalesInvoice() { +async function createSalesInvoice(invoiceDate?: Date) { const sinvDoc = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, { account: 'Debtors', party: partyData.name, - date: new Date('12/11/2024'), + date: invoiceDate || new Date(), items: [ { item: itemData.name, @@ -189,10 +196,10 @@ test('create Sales Invoice and verify loyalty points are created correctly', asy }); test('create SINV with future date and verify loyalty points are not created', async (t) => { - const futureDate = new Date(new Date().setDate(new Date().getDate() + 20)); + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); - const sinvDoc = await createSalesInvoice(); - sinvDoc.date = futureDate; + const sinvDoc = await createSalesInvoice(futureDate); await sinvDoc.sync(); await sinvDoc.submit(); diff --git a/models/helpers.ts b/models/helpers.ts index 07e13435..3f1e73b8 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -738,6 +738,64 @@ export function getDocStatusListColumn(): ColumnConfig { }; } +export function getLoyaltyProgramStatusColumn(): ColumnConfig { + return { + label: t`Status`, + fieldname: 'status', + fieldtype: 'Select', + render(doc) { + const status = getLoyaltyProgramStatus(doc); + const color = loyaltyProgramStatusColor[status] ?? 'gray'; + const label = getLoyaltyProgramStatusText(status); + + return { + template: `${label}`, + metadata: { + status, + color, + label, + }, + }; + }, + }; +} + +export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { + if (!doc) { + return ''; + } + + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const toDate = doc.toDate as Date; + + if (toDate && toDate <= currentDate) { + return 'Expired'; + } + + return 'Active'; +} + +export const loyaltyProgramStatusColor: Record = { + Active: 'green', + Disabled: 'gray', + Expired: 'red', +}; + +export function getLoyaltyProgramStatusText(status: string): string { + switch (status) { + case 'Active': + return t`Active`; + case 'Disabled': + return t`Disabled`; + case 'Expired': + return t`Expired`; + default: + return ''; + } +} + type ModelsWithItems = Invoice | StockTransfer | StockMovement; export async function addItem(name: string, doc: M) { if (!doc.canEdit) { @@ -883,6 +941,15 @@ export async function createLoyaltyPointEntry(doc: Invoice) { if (!loyaltyProgramDoc.isEnabled) { return; } + + const toDate = loyaltyProgramDoc.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + return; + } + const expiryDate = new Date(Date.now()); expiryDate.setDate( @@ -951,18 +1018,22 @@ export function getLoyaltyProgramTier( let loyaltyProgramTier: CollectionRulesItems | undefined; for (const row of loyaltyProgramData.collectionRules) { - if (isPesa(row.minimumTotalSpent)) { - const minimumSpent = row.minimumTotalSpent; + if (row.minimumTotalSpent !== undefined && row.minimumTotalSpent !== null) { + let minimumSpent: Money; - if (!minimumSpent.lte(grandTotal)) { - continue; + if (isPesa(row.minimumTotalSpent)) { + minimumSpent = row.minimumTotalSpent; + } else { + minimumSpent = new Money(row.minimumTotalSpent as number); } - if ( - !loyaltyProgramTier || - minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money) - ) { - loyaltyProgramTier = row; + if (minimumSpent.lte(grandTotal)) { + if ( + !loyaltyProgramTier || + minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money) + ) { + loyaltyProgramTier = row; + } } } } @@ -1537,6 +1608,52 @@ export async function validateCouponCode( } } +export async function validateLoyaltyProgram( + doc: Invoice, + loyaltyProgramName: string +) { + const loyaltyProgram = await doc.fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['fromDate', 'toDate', 'maximumUse', 'used', 'isEnabled'], + filters: { name: loyaltyProgramName }, + }); + + if (!loyaltyProgram[0]?.isEnabled) { + throw new ValidationError( + 'Loyalty program cannot be applied as it is not enabled' + ); + } + + if ( + (loyaltyProgram[0]?.maximumUse as number) > 0 && + (loyaltyProgram[0]?.used as number) >= + (loyaltyProgram[0]?.maximumUse as number) + ) { + throw new ValidationError( + 'Loyalty program has reached maximum usage limit' + ); + } + + if ( + loyaltyProgram[0].fromDate && + (doc.date as Date) < (loyaltyProgram[0].fromDate as Date) + ) { + throw new ValidationError('Loyalty program is not yet active'); + } + + const toDate = loyaltyProgram[0].toDate as Date; + if (toDate) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const normalizedToDate = new Date(toDate); + normalizedToDate.setHours(0, 0, 0, 0); + + // Only throw error if toDate is clearly in the past + if (normalizedToDate.getTime() < today.getTime()) { + throw new ValidationError('Loyalty program has expired'); + } + } +} + export function removeFreeItems(sinvDoc: SalesInvoice) { if (!sinvDoc || !sinvDoc.items) { return; diff --git a/schemas/app/LoyaltyProgram.json b/schemas/app/LoyaltyProgram.json index 5c5c1c71..4b9df873 100644 --- a/schemas/app/LoyaltyProgram.json +++ b/schemas/app/LoyaltyProgram.json @@ -51,7 +51,7 @@ "label": "Expiry Duration", "fieldtype": "Int", "default": 1, - "required": true + "hidden": true }, { "fieldname": "expenseAccount", @@ -59,6 +59,23 @@ "fieldtype": "Link", "target": "Account", "required": true + }, + { + "fieldname": "maximumUse", + "label": "Maximum Use", + "fieldtype": "Int", + "default": 0, + "required": true, + "section": "Validity and Usage" + }, + { + "fieldname": "used", + "label": "Used", + "fieldtype": "Int", + "default": 0, + "required": true, + "readOnly": true, + "section": "Validity and Usage" } ], "quickEditFields": [ @@ -67,7 +84,8 @@ "toDate", "conversionFactor", "expenseAccount", - "expiryDuration" + "maximumUse", + "used" ], "keywordFields": ["name"] } diff --git a/src/components/StatusPill.vue b/src/components/StatusPill.vue index f2d1743a..b9252a99 100644 --- a/src/components/StatusPill.vue +++ b/src/components/StatusPill.vue @@ -8,6 +8,7 @@ import { Doc } from 'fyo/model/doc'; import { isPesa } from 'fyo/utils'; import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { Party } from 'models/baseModels/Party/Party'; +import { LoyaltyProgram } from 'models/baseModels/LoyaltyProgram/LoyaltyProgram'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { getBgTextColorClass } from 'src/utils/colors'; @@ -77,6 +78,7 @@ export default defineComponent({ ReturnIssued: this.t`Return Issued`, Unpaid: this.t`Unpaid`, PartlyPaid: this.t`Partly Paid`, + Expired: this.t`Expired`, }[this.status]; }, color(): UIColors { @@ -99,6 +101,7 @@ const statusColorMap: Record = { ReturnIssued: 'gray', Unpaid: 'red', PartlyPaid: 'yellow', + Expired: 'red', }; function getStatus(doc: Doc) { @@ -110,6 +113,14 @@ function getStatus(doc: Doc) { return 'NotSaved'; } + if (doc instanceof LoyaltyProgram) { + const currentDate = new Date(); + if (doc.toDate && doc.toDate instanceof Date && doc.toDate <= currentDate) { + return 'Expired'; + } + return 'Saved'; + } + if (doc instanceof Party && doc.outstandingAmount?.isZero() !== true) { return 'Outstanding'; } diff --git a/src/pages/POS/LoyaltyProgramModal.vue b/src/pages/POS/LoyaltyProgramModal.vue index a4e921d9..d99f3cbc 100644 --- a/src/pages/POS/LoyaltyProgramModal.vue +++ b/src/pages/POS/LoyaltyProgramModal.vue @@ -128,6 +128,22 @@ export default defineComponent({ return; } + const loyaltyProgramDoc = await this.fyo.db.getAll( + ModelNameEnum.LoyaltyProgram, + { + fields: ['conversionFactor', 'toDate'], + filters: { name: partyData.loyaltyProgram as string }, + } + ); + + const toDate = loyaltyProgramDoc[0]?.toDate as Date; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (toDate && new Date(toDate).getTime() < today.getTime()) { + throw new Error(t`Loyalty program has expired and cannot be applied`); + } + if (this.loyaltyPoints >= newValue) { this.sinvDoc.loyaltyPoints = newValue; } else { @@ -138,14 +154,6 @@ export default defineComponent({ ); } - const loyaltyProgramDoc = await this.fyo.db.getAll( - ModelNameEnum.LoyaltyProgram, - { - fields: ['conversionFactor'], - filters: { name: partyData.loyaltyProgram as string }, - } - ); - const loyaltyPoint = newValue * ((loyaltyProgramDoc[0]?.conversionFactor as number) || 0); From e061179db7a3b3b213cc5b86d39a1fd3105749cb Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Thu, 22 Jan 2026 14:41:25 +0530 Subject: [PATCH 02/12] fix: removed the unused codes --- jobs/checkLoyaltyProgramExpiry.ts | 48 +------------------------------ 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts index b1ba026f..838aeb44 100644 --- a/jobs/checkLoyaltyProgramExpiry.ts +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -1,11 +1,6 @@ -import { parentPort } from 'worker_threads'; import { DatabaseManager } from '../backend/database/manager'; import { ModelNameEnum } from '../models/types'; -if (parentPort) { - parentPort.postMessage({ type: 'check-loyalty-program-expiry' }); -} - export async function checkLoyaltyProgramExpiry() { const dm = new DatabaseManager(); @@ -25,13 +20,8 @@ export async function checkLoyaltyProgramExpiry() { isEnabled: boolean; }>; - let expiredCount = 0; - let processedCount = 0; - if (loyaltyPrograms) { for (const program of loyaltyPrograms) { - processedCount++; - if (program.toDate && new Date(program.toDate) <= currentDate) { await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) .where({ name: program.name }) @@ -39,41 +29,16 @@ export async function checkLoyaltyProgramExpiry() { status: 'Expired', isEnabled: false, }); - - expiredCount++; } } } const result = { timestamp: currentDate.toISOString(), - processedPrograms: processedCount, - expiredPrograms: expiredCount, - message: `Loyalty program expiry check completed. ${expiredCount} programs expired out of ${processedCount} processed.`, }; - if (parentPort) { - parentPort.postMessage({ - type: 'loyalty-program-expiry-complete', - data: result, - }); - } - return result; } catch (error) { - const errorResult = { - timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : 'Unknown error', - message: 'Loyalty program expiry check failed', - }; - - if (parentPort) { - parentPort.postMessage({ - type: 'loyalty-program-expiry-error', - data: errorResult, - }); - } - throw error; } finally { await dm.call('close'); @@ -81,16 +46,5 @@ export async function checkLoyaltyProgramExpiry() { } checkLoyaltyProgramExpiry().catch((error) => { - const errorResult = { - timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : 'Unknown error', - message: 'Loyalty program expiry check failed', - }; - - if (parentPort) { - parentPort.postMessage({ - type: 'loyalty-program-expiry-error', - data: errorResult, - }); - } + throw error; }); From b28df59d943aa3e19675ba6d1b64da21163c4ea8 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Sat, 24 Jan 2026 14:00:25 +0530 Subject: [PATCH 03/12] fix: formatted code --- schemas/app/LoyaltyProgram.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/schemas/app/LoyaltyProgram.json b/schemas/app/LoyaltyProgram.json index 4b9df873..608f97f9 100644 --- a/schemas/app/LoyaltyProgram.json +++ b/schemas/app/LoyaltyProgram.json @@ -65,7 +65,6 @@ "label": "Maximum Use", "fieldtype": "Int", "default": 0, - "required": true, "section": "Validity and Usage" }, { @@ -73,7 +72,6 @@ "label": "Used", "fieldtype": "Int", "default": 0, - "required": true, "readOnly": true, "section": "Validity and Usage" } From 446585dee7b6c7880e89403da3d4db4df205a276 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Sat, 31 Jan 2026 13:45:03 +0530 Subject: [PATCH 04/12] fix: expire loyalty program on maximum usage --- models/baseModels/Invoice/Invoice.ts | 14 ++++++++++++- models/helpers.ts | 31 ++++++++++++++++++++++++++++ src/pages/POS/POS.vue | 20 ++++++++++++++++-- src/pages/POS/POSQuickActions.vue | 4 +++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 1fcf0e1d..51eed4bb 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -30,6 +30,7 @@ import { getItemVisibility, validateLoyaltyProgram, getLoyaltyProgramTier, + isLoyaltyProgramMaxedOut, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -1086,7 +1087,18 @@ export abstract class Invoice extends Transactional { ModelNameEnum.Party, this.party ); - return partyDoc?.loyaltyProgram as string; + const loyaltyProgramName = partyDoc?.loyaltyProgram as string; + + if (!loyaltyProgramName) { + return ''; + } + + const maxedOut = await isLoyaltyProgramMaxedOut( + this.fyo, + loyaltyProgramName + ); + + return maxedOut ? '' : loyaltyProgramName; }, dependsOn: ['party', 'name'], }, diff --git a/models/helpers.ts b/models/helpers.ts index 3f1e73b8..802877ef 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -1654,6 +1654,37 @@ export async function validateLoyaltyProgram( } } +export async function isLoyaltyProgramMaxedOut( + fyo: Fyo, + loyaltyProgramName: string +): Promise { + if (!loyaltyProgramName) { + return false; + } + + const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['maximumUse', 'used', 'isEnabled'], + filters: { name: loyaltyProgramName }, + }); + + if (!loyaltyProgram[0]) { + return false; + } + + if (!loyaltyProgram[0]?.isEnabled) { + return true; + } + + const maximumUse = loyaltyProgram[0]?.maximumUse as number; + const used = loyaltyProgram[0]?.used as number; + + if (!maximumUse) { + return false; + } + + return used >= maximumUse; +} + export function removeFreeItems(sinvDoc: SalesInvoice) { if (!sinvDoc || !sinvDoc.items) { return; diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index cdd6b032..2bb2c11d 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -166,6 +166,7 @@ import { removeFreeItems, getItemRateFromPriceList, getItemVisibility, + isLoyaltyProgramMaxedOut, } from 'models/helpers'; import { POSItem, @@ -495,7 +496,21 @@ export default defineComponent({ filters: { name: value }, }); - this.loyaltyProgram = party[0]?.loyaltyProgram as string; + const loyaltyProgramName = party[0]?.loyaltyProgram as string; + + if (loyaltyProgramName) { + const isMaxedOut = await isLoyaltyProgramMaxedOut( + this.fyo, + loyaltyProgramName + ); + if (isMaxedOut) { + this.loyaltyProgram = ''; + this.loyaltyPoints = 0; + return; + } + } + + this.loyaltyProgram = loyaltyProgramName; this.loyaltyPoints = party[0]?.loyaltyPoints as number; }, @@ -644,7 +659,8 @@ export default defineComponent({ this.fyo.singles.AccountingSettings?.enablePriceList && this.loyaltyPoints && this.sinvDoc.party && - this.sinvDoc.items?.length + this.sinvDoc.items?.length && + this.loyaltyProgram ) { this.toggleModal('LoyaltyProgram', true); } diff --git a/src/pages/POS/POSQuickActions.vue b/src/pages/POS/POSQuickActions.vue index 51f118b7..54572f48 100644 --- a/src/pages/POS/POSQuickActions.vue +++ b/src/pages/POS/POSQuickActions.vue @@ -76,7 +76,9 @@
Date: Thu, 29 Jan 2026 13:06:22 +0530 Subject: [PATCH 05/12] fix: added erp item visibility --- fyo/model/doc.ts | 35 +++++++++++++++++-- .../ERPNextSyncSettings.ts | 3 ++ models/baseModels/InvoiceItem/InvoiceItem.ts | 26 ++++++++++++-- models/baseModels/Item/Item.ts | 2 ++ models/helpers.ts | 6 ++++ models/inventory/Point of Sale/POSSettings.ts | 5 ++- schemas/app/Item.json | 7 ++++ .../inventory/Point of Sale/POSSettings.json | 22 ++++++++++++ src/components/POS/Classic/ItemsGrid.vue | 5 +++ src/components/POS/Classic/ItemsTable.vue | 28 ++++++++++----- .../POS/Modern/ModernPOSItemsGrid.vue | 5 +++ .../POS/Modern/ModernPOSItemsTable.vue | 30 +++++++++++----- src/components/POS/types.ts | 2 +- src/pages/POS/ClassicPOS.vue | 6 ++++ src/pages/POS/ModernPOS.vue | 6 ++++ src/pages/POS/POS.vue | 10 ++++++ src/utils/erpnextSync.ts | 4 +++ 17 files changed, 178 insertions(+), 24 deletions(-) diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 14965ceb..50f7bb7b 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -920,6 +920,35 @@ export class Doc extends Observable { return this; } + async _hasERPSyncableItems(): Promise { + const isSalesInvoice = this.schemaName === ModelNameEnum.SalesInvoice; + if (!isSalesInvoice) { + return true; + } + + const items = (this.get('items') as Doc[]) ?? []; + + for (const item of items) { + const itemName = item.get('item') as string; + if (!itemName) { + continue; + } + + try { + const itemDoc = await this.fyo.doc.getDoc('Item', itemName); + const isInventoryItem = !!itemDoc.get('trackItem'); + const isFromERP = !!itemDoc.get('datafromErp'); + + if (!isInventoryItem && isFromERP) { + return true; + } + } catch (err) { + continue; + } + } + + return false; + } async sync(): Promise { this._syncing = true; @@ -936,10 +965,12 @@ export class Doc extends Observable { if (this._addDocToSyncQueue && !!this.shouldDocSyncToERPNext) { const isSalesInvoice = this.schemaName === ModelNameEnum.SalesInvoice; + const hasERPSyncableItems = await this._hasERPSyncableItems(); if ( - !(isSalesInvoice && this.isSyncedWithErp) || - (isSalesInvoice && !!this.isReturn) + hasERPSyncableItems && + (!(isSalesInvoice && this.isSyncedWithErp) || + (isSalesInvoice && !!this.isReturn)) ) { if (isSalesInvoice && !this.isReturn) { await this.setAndSync('isSyncedWithErp', true); diff --git a/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts b/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts index b163e153..ee17eebc 100644 --- a/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts +++ b/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts @@ -40,6 +40,9 @@ export class ERPNextSyncSettings extends Doc { batchSyncType: () => { return !this.fyo.singles.InventorySettings?.enableBatches; }, + // syncDataFromServer: () => { + // return !this.deviceID; + // }, }; async change(ch: ChangeArg) { diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 590702d2..14ed7ef0 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -19,8 +19,13 @@ import { Item } from '../Item/Item'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { isPesa } from 'fyo/utils'; import { PricingRule } from '../PricingRule/PricingRule'; -import { getItemRateFromPriceList, getPricingRule } from 'models/helpers'; +import { + getItemRateFromPriceList, + getPricingRule, + getItemVisibility, +} from 'models/helpers'; import { SalesInvoice } from '../SalesInvoice/SalesInvoice'; +import { QueryFilter } from 'utils/db/types'; export abstract class InvoiceItem extends Doc { item?: string; @@ -646,13 +651,28 @@ export abstract class InvoiceItem extends Doc { }; static filters: FiltersMap = { - item: (doc: Doc) => { + item: async (doc: Doc): Promise => { let itemNotFor = 'Sales'; if (doc.isSales) { itemNotFor = 'Purchases'; } - return { for: ['not in', [itemNotFor]] }; + const filters: QueryFilter = { + for: ['not in', [itemNotFor]], + }; + + const enableERPNextSync = + doc.fyo.singles.AccountingSettings?.enableERPNextSync; + + if (enableERPNextSync) { + const itemVisibility = await getItemVisibility(doc.fyo); + + if (itemVisibility === 'ERP Sync Items') { + filters.datafromErp = true; + } + } + + return filters; }, batch: async (doc: Doc) => { const batches = await doc.fyo.db.getAll(ModelNameEnum.Batch, { diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 9971a1ba..5f349386 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -30,6 +30,7 @@ export class Item extends Doc { hsnCode?: number; hasSerialNumber?: boolean; serialNumberSeries?: string; + datafromErp?: boolean; uomConversions: UOMConversionItem[] = []; formulas: FormulaMap = { @@ -237,5 +238,6 @@ export class Item extends Doc { trackItem: () => this.inserted, hasBatch: () => this.inserted, hasSerialNumber: () => this.inserted, + datafromErp: () => true, }; } diff --git a/models/helpers.ts b/models/helpers.ts index 07e13435..37e8744c 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -116,6 +116,12 @@ export async function getItemQtyMap(doc: SalesInvoice): Promise { export async function getItemVisibility(fyo: Fyo): Promise { const posProfileName = fyo.singles.POSSettings?.posProfile as string; + const enableERPNextSync = fyo.singles.AccountingSettings?.enableERPNextSync; + + if (enableERPNextSync) { + // When ERP sync is enabled, use itemVisibilityERP from POSSettings + return fyo.singles.POSSettings?.itemVisibilityERP as ItemVisibility; + } if (posProfileName) { const posProfile = await fyo.doc.getDoc( diff --git a/models/inventory/Point of Sale/POSSettings.ts b/models/inventory/Point of Sale/POSSettings.ts index 5fbcc9e6..2a70b60a 100644 --- a/models/inventory/Point of Sale/POSSettings.ts +++ b/models/inventory/Point of Sale/POSSettings.ts @@ -16,6 +16,7 @@ export class POSSettings extends Doc { itemWeightDigits?: number; defaultAccount?: string; itemVisibility?: string; + itemVisibilityERP?: 'ERP Sync Items'; posUI?: 'Classic' | 'Modern'; canChangeRate?: boolean; canEditDiscount?: boolean; @@ -46,6 +47,8 @@ export class POSSettings extends Doc { !this.fyo.singles.InventorySettings?.enableBarcodes || !this.weightEnabledBarcode, itemVisibility: () => - !this.fyo.singles.AccountingSettings?.enablePointOfSaleWithOutInventory, + !!this.fyo.singles.AccountingSettings?.enableERPNextSync, + itemVisibilityERP: () => + !this.fyo.singles.AccountingSettings?.enableERPNextSync, }; } diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 02a483fa..d97b7677 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -172,6 +172,13 @@ "fieldtype": "Table", "target": "UOMConversionItem", "section": "Inventory" + }, + { + "fieldname": "datafromErp", + "fieldtype": "Check", + "hidden": false, + "default": false, + "section": "Default" } ], "quickEditFields": [ diff --git a/schemas/app/inventory/Point of Sale/POSSettings.json b/schemas/app/inventory/Point of Sale/POSSettings.json index 0d2887bb..657a28eb 100644 --- a/schemas/app/inventory/Point of Sale/POSSettings.json +++ b/schemas/app/inventory/Point of Sale/POSSettings.json @@ -121,6 +121,28 @@ "required": true, "section": "Default" }, + { + "fieldname": "itemVisibilityERP", + "label": "Item Visibility", + "fieldtype": "Select", + "options": [ + { + "value": "ERP Sync Items", + "label": "ERP Sync Items" + }, + { + "value": "Inventory Items", + "label": "Inventory Items" + }, + { + "value": "Non-Inventory Items", + "label": "Non-Inventory Items" + } + ], + "default": "ERP Sync Items", + "required": true, + "section": "Default" + }, { "fieldname": "canChangeRate", "label": "Can Change Rate", diff --git a/src/components/POS/Classic/ItemsGrid.vue b/src/components/POS/Classic/ItemsGrid.vue index 08c289a2..0081f134 100644 --- a/src/components/POS/Classic/ItemsGrid.vue +++ b/src/components/POS/Classic/ItemsGrid.vue @@ -63,6 +63,7 @@

@@ -141,6 +142,7 @@ v-else :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" + :is-erp-sync="isErpSync" @add-item="(item) => emitEvent('addItem', item)" /> @@ -483,6 +485,10 @@ export default defineComponent({ type: Array as PropType, default: () => [], }, + isErpSync: { + type: Boolean, + default: false, + }, profile: { type: Object as PropType, required: false, diff --git a/src/pages/POS/ModernPOS.vue b/src/pages/POS/ModernPOS.vue index 7d278ee8..df46a2fe 100644 --- a/src/pages/POS/ModernPOS.vue +++ b/src/pages/POS/ModernPOS.vue @@ -345,6 +345,7 @@ v-if="tableView" :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" + :is-erp-sync="isErpSync" @add-item="(item:string) => emitEvent('addItem', item)" /> @@ -352,6 +353,7 @@ v-else :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" + :is-erp-sync="isErpSync" @add-item="(item:string) => emitEvent('addItem', item)" /> @@ -489,6 +491,10 @@ export default defineComponent({ type: Array as PropType, default: () => [], }, + isErpSync: { + type: Boolean, + default: false, + }, profile: { type: Object as PropType, required: false, diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index cdd6b032..cc9c1799 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -27,6 +27,7 @@ :selected-item-group="selectedItemGroup" :is-pos-shift-open="isPosShiftOpen" :items="(items as [] as POSItem[])" + :is-erp-sync="isErpSync" :sinv-doc="(sinvDoc as SalesInvoice)" :disable-pay-button="disablePayButton" :open-payment-modal="openPaymentModal" @@ -83,6 +84,7 @@ :selected-item-group="selectedItemGroup" :is-pos-shift-open="isPosShiftOpen" :items="(items as [] as POSItem[])" + :is-erp-sync="isErpSync" :sinv-doc="(sinvDoc as SalesInvoice)" :disable-pay-button="disablePayButton" :open-payment-modal="openPaymentModal" @@ -262,6 +264,7 @@ export default defineComponent({ quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null, selectedItemForBatch: '' as string, pendingBatchItem: null as { item: POSItem; quantity: number } | null, + isErpSyncValue: false, }; }, computed: { @@ -271,6 +274,9 @@ export default defineComponent({ return !!fyo.singles.AccountingSettings?.enableDiscounting; }, isPosShiftOpen: () => !!fyo.singles.POSSettings?.isShiftOpen, + isErpSync() { + return this.isErpSyncValue; + }, disablePayButton(): boolean { if (!this.sinvDoc.items?.length || !this.sinvDoc.party) { return true; @@ -295,6 +301,7 @@ export default defineComponent({ async mounted() { await this.setItems(); await this.loadPOSProfile(); + this.isErpSyncValue = !!fyo.singles.AccountingSettings?.enableERPNextSync; }, async activated() { toggleSidebar(false); @@ -694,8 +701,11 @@ export default defineComponent({ if (itemVisibility === 'Inventory Items') { filters.trackItem = true; + } else if (itemVisibility === 'ERP Sync Items') { + filters.datafromErp = true; } else { filters.trackItem = false; + filters.datafromErp = false; } if (this.selectedItemGroup) { diff --git a/src/utils/erpnextSync.ts b/src/utils/erpnextSync.ts index 6197767f..5cfb7cc5 100644 --- a/src/utils/erpnextSync.ts +++ b/src/utils/erpnextSync.ts @@ -150,6 +150,10 @@ export async function syncDocumentsFromERPNext(fyo: Fyo) { continue; } + if (getDocTypeName(doc) === ModelNameEnum.Item) { + doc.datafromErp = true; + } + try { if ((doc.fbooksDocName as string) || (doc.name as string)) { const isDocExists = await fyo.db.exists( From 37972fa55b2ab1effd71b669a30d2828b113586d Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Thu, 29 Jan 2026 16:50:38 +0530 Subject: [PATCH 06/12] fix:formatted code --- fyo/model/doc.ts | 31 +++++++------------ .../ERPNextSyncSettings.ts | 3 -- models/helpers.ts | 1 - models/inventory/Point of Sale/POSSettings.ts | 1 + schemas/app/Item.json | 2 +- .../inventory/Point of Sale/POSSettings.json | 9 ------ 6 files changed, 13 insertions(+), 34 deletions(-) diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 50f7bb7b..5345e568 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -46,6 +46,7 @@ import { import { validateOptions, validateRequired } from './validationFunction'; import { getShouldDocSyncToERPNext } from 'src/utils/erpnextSync'; import { ModelNameEnum } from 'models/types'; +import { DocItem } from 'models/inventory/types'; export class Doc extends Observable { /* eslint-disable @typescript-eslint/no-floating-promises */ @@ -925,29 +926,19 @@ export class Doc extends Observable { if (!isSalesInvoice) { return true; } - - const items = (this.get('items') as Doc[]) ?? []; - - for (const item of items) { - const itemName = item.get('item') as string; - if (!itemName) { - continue; - } - - try { - const itemDoc = await this.fyo.doc.getDoc('Item', itemName); - const isInventoryItem = !!itemDoc.get('trackItem'); - const isFromERP = !!itemDoc.get('datafromErp'); - - if (!isInventoryItem && isFromERP) { - return true; - } - } catch (err) { + for (const item of this.items as DocItem[]) { + if (!item.item) { continue; } + const isFromERP = await this.fyo.getValue( + ModelNameEnum.Item, + item.item, + 'datafromErp' + ); + if (isFromERP) continue; + else return false; } - - return false; + return true; } async sync(): Promise { diff --git a/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts b/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts index ee17eebc..b163e153 100644 --- a/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts +++ b/models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings.ts @@ -40,9 +40,6 @@ export class ERPNextSyncSettings extends Doc { batchSyncType: () => { return !this.fyo.singles.InventorySettings?.enableBatches; }, - // syncDataFromServer: () => { - // return !this.deviceID; - // }, }; async change(ch: ChangeArg) { diff --git a/models/helpers.ts b/models/helpers.ts index 37e8744c..ac458d13 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -119,7 +119,6 @@ export async function getItemVisibility(fyo: Fyo): Promise { const enableERPNextSync = fyo.singles.AccountingSettings?.enableERPNextSync; if (enableERPNextSync) { - // When ERP sync is enabled, use itemVisibilityERP from POSSettings return fyo.singles.POSSettings?.itemVisibilityERP as ItemVisibility; } diff --git a/models/inventory/Point of Sale/POSSettings.ts b/models/inventory/Point of Sale/POSSettings.ts index 2a70b60a..3060b290 100644 --- a/models/inventory/Point of Sale/POSSettings.ts +++ b/models/inventory/Point of Sale/POSSettings.ts @@ -47,6 +47,7 @@ export class POSSettings extends Doc { !this.fyo.singles.InventorySettings?.enableBarcodes || !this.weightEnabledBarcode, itemVisibility: () => + !this.fyo.singles.AccountingSettings?.enablePointOfSaleWithOutInventory || !!this.fyo.singles.AccountingSettings?.enableERPNextSync, itemVisibilityERP: () => !this.fyo.singles.AccountingSettings?.enableERPNextSync, diff --git a/schemas/app/Item.json b/schemas/app/Item.json index d97b7677..d1dca717 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -176,7 +176,7 @@ { "fieldname": "datafromErp", "fieldtype": "Check", - "hidden": false, + "hidden": true, "default": false, "section": "Default" } diff --git a/schemas/app/inventory/Point of Sale/POSSettings.json b/schemas/app/inventory/Point of Sale/POSSettings.json index 657a28eb..f798a7af 100644 --- a/schemas/app/inventory/Point of Sale/POSSettings.json +++ b/schemas/app/inventory/Point of Sale/POSSettings.json @@ -96,13 +96,6 @@ "default": 0, "section": "Barcode" }, - { - "fieldname": "itemWeightDigits", - "label": "item Weight Digits", - "fieldtype": "Int", - "default": 0, - "section": "Barcode" - }, { "fieldname": "itemVisibility", "label": "Item Visibility", @@ -118,7 +111,6 @@ } ], "default": "Inventory Items", - "required": true, "section": "Default" }, { @@ -140,7 +132,6 @@ } ], "default": "ERP Sync Items", - "required": true, "section": "Default" }, { From 0d9c079e1d75905f6174b3b12847e34e6b18905f Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Fri, 30 Jan 2026 14:31:13 +0530 Subject: [PATCH 07/12] fix: rebase code --- models/baseModels/Item/Item.ts | 1 - schemas/app/Item.json | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 5f349386..1d0542b4 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -238,6 +238,5 @@ export class Item extends Doc { trackItem: () => this.inserted, hasBatch: () => this.inserted, hasSerialNumber: () => this.inserted, - datafromErp: () => true, }; } diff --git a/schemas/app/Item.json b/schemas/app/Item.json index d1dca717..09f1bcca 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -178,7 +178,8 @@ "fieldtype": "Check", "hidden": true, "default": false, - "section": "Default" + "section": "Default", + "readOnly": true } ], "quickEditFields": [ From 5e558badc492968f4b43a4756c00c61755fda007 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Mon, 2 Feb 2026 14:55:38 +0530 Subject: [PATCH 08/12] fix: added filter in item visibility --- models/baseModels/InvoiceItem/InvoiceItem.ts | 10 +++++++++- src/pages/POS/POS.vue | 3 +++ src/utils/erpnextSync.ts | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 14ed7ef0..365facda 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -667,8 +667,16 @@ export abstract class InvoiceItem extends Doc { if (enableERPNextSync) { const itemVisibility = await getItemVisibility(doc.fyo); - if (itemVisibility === 'ERP Sync Items') { + if (itemVisibility === 'Inventory Items') { + filters.trackItem = true; + } else if (itemVisibility === 'ERP Sync Items') { filters.datafromErp = true; + } else if (itemVisibility === 'Non-Inventory Items') { + filters.trackItem = false; + filters.datafromErp = false; + } else { + filters.trackItem = false; + filters.datafromErp = false; } } diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index cc9c1799..d7c9b23a 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -703,6 +703,9 @@ export default defineComponent({ filters.trackItem = true; } else if (itemVisibility === 'ERP Sync Items') { filters.datafromErp = true; + } else if (itemVisibility === 'Non-Inventory Items') { + filters.trackItem = false; + filters.datafromErp = false; } else { filters.trackItem = false; filters.datafromErp = false; diff --git a/src/utils/erpnextSync.ts b/src/utils/erpnextSync.ts index 5cfb7cc5..2672c9da 100644 --- a/src/utils/erpnextSync.ts +++ b/src/utils/erpnextSync.ts @@ -243,6 +243,10 @@ async function createNewDocument( token: string, deviceID: string ) { + if (getDocTypeName(doc) === ModelNameEnum.Item) { + doc.datafromErp = true; + } + const newDoc = fyo.doc.getNewDoc(getDocTypeName(doc), doc); await performPreSync(fyo, doc); await appendDocValues(newDoc as DocValueMap, doc); @@ -451,8 +455,13 @@ async function updateExistingDocument( token: string, deviceID: string ) { + const docType = getDocTypeName(doc); + + if (docType === ModelNameEnum.Item) { + } + const existingDoc = await fyo.doc.getDoc( - getDocTypeName(doc), + docType, (doc.fbooksDocName as string) || (doc.name as string) ); @@ -537,6 +546,10 @@ export async function performInitialFullSync(fyo: Fyo) { if (docsByType[docType] && docsByType[docType].length > 0) { for (const doc of docsByType[docType]) { try { + if (docType === ModelNameEnum.Item) { + doc.datafromErp = true; + } + const isDocExists = await fyo.db.exists( docType, (doc.fbooksDocName as string) || (doc.name as string) From 8e10bf313df965bb2b7bc5bdc3a2061235585d31 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Mon, 2 Feb 2026 17:11:24 +0530 Subject: [PATCH 09/12] fix: update status when loyalty program reaches maximum usage --- jobs/checkLoyaltyProgramExpiry.ts | 28 ++++++++++++------- models/baseModels/Invoice/Invoice.ts | 4 +-- .../LoyaltyProgram/LoyaltyProgram.ts | 10 +++++++ models/helpers.ts | 14 +++++++--- src/components/StatusPill.vue | 23 +++++++++++++-- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts index 838aeb44..e6e6b2ab 100644 --- a/jobs/checkLoyaltyProgramExpiry.ts +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -7,22 +7,30 @@ export async function checkLoyaltyProgramExpiry() { try { const currentDate = new Date(); - const loyaltyPrograms = (await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { - fields: ['name', 'toDate', 'status', 'isEnabled'], + const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'], filters: { - status: ['!=', 'Expired'], + status: ['not in', ['Expired', 'Maxed']], isEnabled: true, }, - })) as Array<{ - name: string; - toDate: string; - status: string; - isEnabled: boolean; - }>; + }); if (loyaltyPrograms) { for (const program of loyaltyPrograms) { - if (program.toDate && new Date(program.toDate) <= currentDate) { + const maximumUse = Number(program.maximumUse) || 0; + const used = Number(program.used) || 0; + + if (maximumUse > 0 && used >= maximumUse) { + await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) + .where({ name: program.name }) + .update({ + status: 'Maxed', + isEnabled: false, + }); + continue; + } + + if (program.toDate && new Date(String(program.toDate)) <= currentDate) { await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) .where({ name: program.name }) .update({ diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 51eed4bb..655c6230 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -886,7 +886,7 @@ export abstract class Invoice extends Transactional { } async updateUsedCountOfLoyaltyProgram() { - if (!this.loyaltyProgram) { + if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { return; } @@ -901,7 +901,7 @@ export abstract class Invoice extends Transactional { } async reduceUsedCountOfLoyaltyProgram() { - if (!this.loyaltyProgram) { + if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { return; } diff --git a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts index 45d5b3f7..d2bb47bd 100644 --- a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts +++ b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts @@ -11,6 +11,7 @@ export class LoyaltyProgram extends Doc { expiryDuration?: number; maximumUse?: number; used?: number; + status?: 'Active' | 'Expired' | 'Maxed' | 'Disabled'; validations: ValidationMap = { used: (value: DocValue) => { @@ -34,6 +35,15 @@ export class LoyaltyProgram extends Doc { }, }; + async afterSubmit() { + const maximumUse = (this.maximumUse as number) || 0; + const used = (this.used as number) || 0; + + if (maximumUse > 0 && used >= maximumUse) { + await this.setAndSync({ status: 'Maxed', isEnabled: false }); + } + } + static filters: FiltersMap = { expenseAccount: () => ({ rootType: AccountRootTypeEnum.Expense, diff --git a/models/helpers.ts b/models/helpers.ts index 802877ef..f0a21bc8 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -765,6 +765,13 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { return ''; } + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; + } + const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); @@ -781,6 +788,7 @@ export const loyaltyProgramStatusColor: Record = { Active: 'green', Disabled: 'gray', Expired: 'red', + Maxed: 'orange', }; export function getLoyaltyProgramStatusText(status: string): string { @@ -791,6 +799,8 @@ export function getLoyaltyProgramStatusText(status: string): string { return t`Disabled`; case 'Expired': return t`Expired`; + case 'Maxed': + return t`Maxed`; default: return ''; } @@ -1658,10 +1668,6 @@ export async function isLoyaltyProgramMaxedOut( fyo: Fyo, loyaltyProgramName: string ): Promise { - if (!loyaltyProgramName) { - return false; - } - const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { fields: ['maximumUse', 'used', 'isEnabled'], filters: { name: loyaltyProgramName }, diff --git a/src/components/StatusPill.vue b/src/components/StatusPill.vue index b9252a99..358d9f4e 100644 --- a/src/components/StatusPill.vue +++ b/src/components/StatusPill.vue @@ -79,6 +79,8 @@ export default defineComponent({ Unpaid: this.t`Unpaid`, PartlyPaid: this.t`Partly Paid`, Expired: this.t`Expired`, + Maxed: this.t`Maxed`, + Active: this.t`Active`, }[this.status]; }, color(): UIColors { @@ -102,6 +104,8 @@ const statusColorMap: Record = { Unpaid: 'red', PartlyPaid: 'yellow', Expired: 'red', + Maxed: 'orange', + Active: 'green', }; function getStatus(doc: Doc) { @@ -115,10 +119,23 @@ function getStatus(doc: Doc) { if (doc instanceof LoyaltyProgram) { const currentDate = new Date(); - if (doc.toDate && doc.toDate instanceof Date && doc.toDate <= currentDate) { - return 'Expired'; + currentDate.setHours(0, 0, 0, 0); + + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; } - return 'Saved'; + + if (doc.toDate && doc.toDate instanceof Date) { + const toDate = new Date(doc.toDate); + toDate.setHours(0, 0, 0, 0); + if (toDate <= currentDate) { + return 'Expired'; + } + } + return 'Active'; } if (doc instanceof Party && doc.outstandingAmount?.isZero() !== true) { From 47ef5c321b3dfece58d901181c16447de65e1155 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Mon, 2 Feb 2026 17:19:41 +0530 Subject: [PATCH 10/12] fix: rebased code --- models/baseModels/InvoiceItem/InvoiceItem.ts | 3 --- src/pages/POS/POS.vue | 3 --- src/utils/erpnextSync.ts | 3 --- 3 files changed, 9 deletions(-) diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 365facda..b089ee41 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -674,9 +674,6 @@ export abstract class InvoiceItem extends Doc { } else if (itemVisibility === 'Non-Inventory Items') { filters.trackItem = false; filters.datafromErp = false; - } else { - filters.trackItem = false; - filters.datafromErp = false; } } diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index d7c9b23a..038d190e 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -706,9 +706,6 @@ export default defineComponent({ } else if (itemVisibility === 'Non-Inventory Items') { filters.trackItem = false; filters.datafromErp = false; - } else { - filters.trackItem = false; - filters.datafromErp = false; } if (this.selectedItemGroup) { diff --git a/src/utils/erpnextSync.ts b/src/utils/erpnextSync.ts index 2672c9da..728c2836 100644 --- a/src/utils/erpnextSync.ts +++ b/src/utils/erpnextSync.ts @@ -457,9 +457,6 @@ async function updateExistingDocument( ) { const docType = getDocTypeName(doc); - if (docType === ModelNameEnum.Item) { - } - const existingDoc = await fyo.doc.getDoc( docType, (doc.fbooksDocName as string) || (doc.name as string) From cb092ede86062434e60f4fde8c266da9102e7a3e Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Wed, 4 Feb 2026 15:07:22 +0530 Subject: [PATCH 11/12] fix: added validation for maxed loyaltyprogram --- jobs/checkLoyaltyProgramExpiry.ts | 15 +--- models/baseModels/Invoice/Invoice.ts | 78 +++++++++++++------ .../LoyaltyProgram/LoyaltyProgram.ts | 21 +---- models/baseModels/Party/Party.ts | 12 +++ models/helpers.ts | 74 ++++++++++-------- schemas/app/SalesInvoice.json | 4 +- src/components/StatusPill.vue | 4 +- src/pages/POS/POS.vue | 8 +- 8 files changed, 119 insertions(+), 97 deletions(-) diff --git a/jobs/checkLoyaltyProgramExpiry.ts b/jobs/checkLoyaltyProgramExpiry.ts index e6e6b2ab..ab5eb70c 100644 --- a/jobs/checkLoyaltyProgramExpiry.ts +++ b/jobs/checkLoyaltyProgramExpiry.ts @@ -10,26 +10,13 @@ export async function checkLoyaltyProgramExpiry() { const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, { fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'], filters: { - status: ['not in', ['Expired', 'Maxed']], + status: ['not in', ['Expired']], isEnabled: true, }, }); if (loyaltyPrograms) { for (const program of loyaltyPrograms) { - const maximumUse = Number(program.maximumUse) || 0; - const used = Number(program.used) || 0; - - if (maximumUse > 0 && used >= maximumUse) { - await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) - .where({ name: program.name }) - .update({ - status: 'Maxed', - isEnabled: false, - }); - continue; - } - if (program.toDate && new Date(String(program.toDate)) <= currentDate) { await dm.db?.knex!(ModelNameEnum.LoyaltyProgram) .where({ name: program.name }) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 655c6230..049470af 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -30,7 +30,7 @@ import { getItemVisibility, validateLoyaltyProgram, getLoyaltyProgramTier, - isLoyaltyProgramMaxedOut, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -886,7 +886,7 @@ export abstract class Invoice extends Transactional { } async updateUsedCountOfLoyaltyProgram() { - if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { + if (!this.loyaltyProgram) { return; } @@ -895,13 +895,27 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram ); - await loyaltyProgramDoc.setAndSync({ - used: (loyaltyProgramDoc.used as number) + 1, - }); + const maximumUse = loyaltyProgramDoc.maximumUse as number; + const used = (loyaltyProgramDoc.used as number) || 0; + + if (this.redeemLoyaltyPoints) { + const newUsedCount = used + 1; + + if (maximumUse > 0 && newUsedCount >= maximumUse) { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + isEnabled: false, + }); + } else { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + }); + } + } } async reduceUsedCountOfLoyaltyProgram() { - if (!this.loyaltyProgram || !this.redeemLoyaltyPoints) { + if (!this.loyaltyProgram) { return; } @@ -910,9 +924,22 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram ); - await loyaltyProgramDoc.setAndSync({ - used: (loyaltyProgramDoc.used as number) - 1, - }); + const maximumUse = loyaltyProgramDoc.maximumUse as number; + const used = (loyaltyProgramDoc.used as number) || 0; + const newUsedCount = used - 1; + + if (this.redeemLoyaltyPoints) { + if (newUsedCount < maximumUse) { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + isEnabled: true, + }); + } else { + await loyaltyProgramDoc.setAndSync({ + used: newUsedCount, + }); + } + } } async updateIsItemsFullyReturned(doc?: Invoice) { @@ -975,11 +1002,6 @@ export abstract class Invoice extends Transactional { this.loyaltyProgram )) as LoyaltyProgram; - // Check if loyalty program is enabled - if (!loyaltyProgramDoc.isEnabled) { - return; - } - const invoiceDate = this.date as Date; const fromDate = loyaltyProgramDoc.fromDate as Date; const toDate = loyaltyProgramDoc.toDate as Date; @@ -1093,12 +1115,7 @@ export abstract class Invoice extends Transactional { return ''; } - const maxedOut = await isLoyaltyProgramMaxedOut( - this.fyo, - loyaltyProgramName - ); - - return maxedOut ? '' : loyaltyProgramName; + return loyaltyProgramName; }, dependsOn: ['party', 'name'], }, @@ -1108,6 +1125,17 @@ export abstract class Invoice extends Transactional { return 0; } + const loyaltyProgramName = this.loyaltyProgram as string; + if (loyaltyProgramName) { + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( + this.fyo, + loyaltyProgramName + ); + if (isExpiredAndMaxed) { + return 0; + } + } + const loyaltyPoints = await this.fyo.getValue( ModelNameEnum.Party, this.party, @@ -1115,7 +1143,7 @@ export abstract class Invoice extends Transactional { ); return loyaltyPoints || 0; }, - dependsOn: ['party'], + dependsOn: ['party', 'loyaltyProgram'], }, currency: { formula: async () => { @@ -1308,7 +1336,13 @@ export abstract class Invoice extends Transactional { loyaltyProgram: () => !this.loyaltyProgram, availableLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn, loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn, - redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn, + redeemLoyaltyPoints: () => { + if (!this.loyaltyProgram || this.isReturn) { + return true; + } + + return (this.availableLoyaltyPoints ?? 0) <= 0; + }, coupons: () => this.isSubmitted && !this.coupons?.length, priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList || diff --git a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts index d2bb47bd..20d8db45 100644 --- a/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts +++ b/models/baseModels/LoyaltyProgram/LoyaltyProgram.ts @@ -1,9 +1,8 @@ import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FiltersMap, ListViewSettings, ValidationMap } from 'fyo/model/types'; +import { ListViewSettings, ValidationMap } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems'; -import { AccountRootTypeEnum } from '../Account/types'; import { getLoyaltyProgramStatusColumn } from '../../helpers'; export class LoyaltyProgram extends Doc { @@ -11,7 +10,7 @@ export class LoyaltyProgram extends Doc { expiryDuration?: number; maximumUse?: number; used?: number; - status?: 'Active' | 'Expired' | 'Maxed' | 'Disabled'; + status?: 'Active' | 'Expired' | 'Disabled' | 'Maxed'; validations: ValidationMap = { used: (value: DocValue) => { @@ -35,22 +34,6 @@ export class LoyaltyProgram extends Doc { }, }; - async afterSubmit() { - const maximumUse = (this.maximumUse as number) || 0; - const used = (this.used as number) || 0; - - if (maximumUse > 0 && used >= maximumUse) { - await this.setAndSync({ status: 'Maxed', isEnabled: false }); - } - } - - static filters: FiltersMap = { - expenseAccount: () => ({ - rootType: AccountRootTypeEnum.Expense, - isGroup: false, - }), - }; - static getListViewSettings(): ListViewSettings { return { columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'], diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index bba7181f..e277bd0d 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -14,6 +14,7 @@ import { import { Money } from 'pesa'; import { PartyRole } from './types'; import { ModelNameEnum } from 'models/types'; +import { isLoyaltyProgramExpiredAndMaxed } from 'models/helpers'; export class Party extends Doc { role?: PartyRole; @@ -66,6 +67,17 @@ export class Party extends Doc { } async _getTotalLoyaltyPoints() { + const loyaltyProgramName = this.loyaltyProgram as string; + if (loyaltyProgramName) { + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( + this.fyo, + loyaltyProgramName + ); + if (isExpiredAndMaxed) { + return 0; + } + } + const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, { fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'], filters: { diff --git a/models/helpers.ts b/models/helpers.ts index f0a21bc8..467940c2 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -765,13 +765,6 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { return ''; } - const maximumUse = doc.maximumUse as number; - const used = doc.used as number; - - if (maximumUse > 0 && used >= maximumUse) { - return 'Maxed'; - } - const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); @@ -781,6 +774,13 @@ export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string { return 'Expired'; } + const maximumUse = doc.maximumUse as number; + const used = doc.used as number; + + if (maximumUse > 0 && used >= maximumUse) { + return 'Maxed'; + } + return 'Active'; } @@ -1664,33 +1664,6 @@ export async function validateLoyaltyProgram( } } -export async function isLoyaltyProgramMaxedOut( - fyo: Fyo, - loyaltyProgramName: string -): Promise { - const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { - fields: ['maximumUse', 'used', 'isEnabled'], - filters: { name: loyaltyProgramName }, - }); - - if (!loyaltyProgram[0]) { - return false; - } - - if (!loyaltyProgram[0]?.isEnabled) { - return true; - } - - const maximumUse = loyaltyProgram[0]?.maximumUse as number; - const used = loyaltyProgram[0]?.used as number; - - if (!maximumUse) { - return false; - } - - return used >= maximumUse; -} - export function removeFreeItems(sinvDoc: SalesInvoice) { if (!sinvDoc || !sinvDoc.items) { return; @@ -1765,3 +1738,36 @@ export function roundFreeItemQty( ): number { return Math[roundingMethod](quantity); } + +export async function isLoyaltyProgramExpiredAndMaxed( + fyo: Fyo, + loyaltyProgramName: string +): Promise { + if (!loyaltyProgramName) { + return false; + } + + const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, { + fields: ['toDate', 'maximumUse', 'used', 'isEnabled'], + filters: { name: loyaltyProgramName }, + }); + + if (!loyaltyProgram.length) { + return false; + } + + const program = loyaltyProgram[0]; + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const toDate = program.toDate as Date; + const isExpired = + toDate && new Date(toDate).getTime() < currentDate.getTime(); + + const maximumUse = (program.maximumUse as number) || 0; + const used = (program.used as number) || 0; + const isMaxed = maximumUse > 0 && used >= maximumUse; + + const result = isExpired || isMaxed; + return result; +} diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index f372a543..fcdb4645 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -82,7 +82,7 @@ "fieldtype": "Link", "target": "LoyaltyProgram", "label": "Loyalty Program", - "section": "References", + "section": "Loyalty Points Redemption", "readOnly": true }, { @@ -90,7 +90,7 @@ "fieldtype": "Int", "label": "Available Loyalty Points", "readOnly": true, - "section": "References" + "section": "Loyalty Points Redemption" }, { "fieldname": "redeemLoyaltyPoints", diff --git a/src/components/StatusPill.vue b/src/components/StatusPill.vue index 358d9f4e..71baed7d 100644 --- a/src/components/StatusPill.vue +++ b/src/components/StatusPill.vue @@ -79,8 +79,8 @@ export default defineComponent({ Unpaid: this.t`Unpaid`, PartlyPaid: this.t`Partly Paid`, Expired: this.t`Expired`, - Maxed: this.t`Maxed`, Active: this.t`Active`, + Maxed: this.t`Maxed`, }[this.status]; }, color(): UIColors { @@ -104,8 +104,8 @@ const statusColorMap: Record = { Unpaid: 'red', PartlyPaid: 'yellow', Expired: 'red', - Maxed: 'orange', Active: 'green', + Maxed: 'orange', }; function getStatus(doc: Doc) { diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index 2bb2c11d..d854be35 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -166,7 +166,7 @@ import { removeFreeItems, getItemRateFromPriceList, getItemVisibility, - isLoyaltyProgramMaxedOut, + isLoyaltyProgramExpiredAndMaxed, } from 'models/helpers'; import { POSItem, @@ -499,12 +499,12 @@ export default defineComponent({ const loyaltyProgramName = party[0]?.loyaltyProgram as string; if (loyaltyProgramName) { - const isMaxedOut = await isLoyaltyProgramMaxedOut( + const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed( this.fyo, loyaltyProgramName ); - if (isMaxedOut) { - this.loyaltyProgram = ''; + if (isExpiredAndMaxed) { + this.loyaltyProgram = loyaltyProgramName; this.loyaltyPoints = 0; return; } From 4b4ae969b90598418ff9da665a7b1b5e1294aad5 Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Wed, 4 Feb 2026 16:07:47 +0530 Subject: [PATCH 12/12] fix: display availableQty according to item visibility --- src/components/POS/Classic/ItemsGrid.vue | 8 ++++---- src/components/POS/Classic/ItemsTable.vue | 12 ++++++------ src/components/POS/Modern/ModernPOSItemsGrid.vue | 8 ++++---- src/components/POS/Modern/ModernPOSItemsTable.vue | 14 +++++++------- src/pages/POS/ClassicPOS.vue | 10 +++++----- src/pages/POS/ModernPOS.vue | 10 +++++----- src/pages/POS/POS.vue | 13 +++++++------ 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/components/POS/Classic/ItemsGrid.vue b/src/components/POS/Classic/ItemsGrid.vue index 0081f134..8847e941 100644 --- a/src/components/POS/Classic/ItemsGrid.vue +++ b/src/components/POS/Classic/ItemsGrid.vue @@ -63,7 +63,7 @@

@@ -81,14 +81,14 @@ export default defineComponent({ props: { items: Array, itemQtyMap: Object, - isErpSync: { - type: Boolean, - default: false, + itemVisibility: { + type: String, + default: 'Inventory Items', }, }, computed: { ratio() { - if (this.isErpSync) { + if (this.itemVisibility === 'ERP Sync Items') { return [1, 1.5, 0.8]; } return [1, 1, 1, 0.7]; @@ -119,7 +119,7 @@ export default defineComponent({ }, ] as Field[]; - if (!this.isErpSync) { + if (this.itemVisibility !== 'ERP Sync Items') { fields.splice(2, 0, { fieldname: 'availableQty', label: 'Qty', diff --git a/src/components/POS/Modern/ModernPOSItemsGrid.vue b/src/components/POS/Modern/ModernPOSItemsGrid.vue index cf30ee13..9df82aad 100644 --- a/src/components/POS/Modern/ModernPOSItemsGrid.vue +++ b/src/components/POS/Modern/ModernPOSItemsGrid.vue @@ -66,7 +66,7 @@

@@ -113,7 +113,7 @@ :key="df.fieldname" size="large" :df="df" - :value="row[df.fieldname]" + :value="(row as POSItem)[df.fieldname as keyof POSItem]" :readOnly="true" /> @@ -137,14 +137,14 @@ export default defineComponent({ props: { items: Array, itemQtyMap: Object, - isErpSync: { - type: Boolean, - default: false, + itemVisibility: { + type: String, + default: 'Inventory Items', }, }, computed: { ratio() { - if (this.isErpSync) { + if (this.itemVisibility === 'ERP Sync Items') { return [1, 1.5, 0.8]; } return [1, 1, 1, 0.7]; @@ -175,7 +175,7 @@ export default defineComponent({ }, ] as Field[]; - if (!this.isErpSync) { + if (this.itemVisibility !== 'ERP Sync Items') { fields.splice(2, 0, { fieldname: 'availableQty', label: t`Qty`, diff --git a/src/pages/POS/ClassicPOS.vue b/src/pages/POS/ClassicPOS.vue index 335f5cd2..23832681 100644 --- a/src/pages/POS/ClassicPOS.vue +++ b/src/pages/POS/ClassicPOS.vue @@ -134,7 +134,7 @@ v-if="tableView" :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" @add-item="(item) => emitEvent('addItem', item)" /> @@ -142,7 +142,7 @@ v-else :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" @add-item="(item) => emitEvent('addItem', item)" /> @@ -485,9 +485,9 @@ export default defineComponent({ type: Array as PropType, default: () => [], }, - isErpSync: { - type: Boolean, - default: false, + itemVisibility: { + type: String, + default: 'Inventory Items', }, profile: { type: Object as PropType, diff --git a/src/pages/POS/ModernPOS.vue b/src/pages/POS/ModernPOS.vue index df46a2fe..71723617 100644 --- a/src/pages/POS/ModernPOS.vue +++ b/src/pages/POS/ModernPOS.vue @@ -345,7 +345,7 @@ v-if="tableView" :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" @add-item="(item:string) => emitEvent('addItem', item)" /> @@ -353,7 +353,7 @@ v-else :items="items" :item-qty-map="itemQuantityMap as ItemQtyMap" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" @add-item="(item:string) => emitEvent('addItem', item)" /> @@ -491,9 +491,9 @@ export default defineComponent({ type: Array as PropType, default: () => [], }, - isErpSync: { - type: Boolean, - default: false, + itemVisibility: { + type: String, + default: 'Inventory Items', }, profile: { type: Object as PropType, diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index 038d190e..154a85dd 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -27,7 +27,7 @@ :selected-item-group="selectedItemGroup" :is-pos-shift-open="isPosShiftOpen" :items="(items as [] as POSItem[])" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" :sinv-doc="(sinvDoc as SalesInvoice)" :disable-pay-button="disablePayButton" :open-payment-modal="openPaymentModal" @@ -84,7 +84,7 @@ :selected-item-group="selectedItemGroup" :is-pos-shift-open="isPosShiftOpen" :items="(items as [] as POSItem[])" - :is-erp-sync="isErpSync" + :item-visibility="itemVisibility" :sinv-doc="(sinvDoc as SalesInvoice)" :disable-pay-button="disablePayButton" :open-payment-modal="openPaymentModal" @@ -169,6 +169,7 @@ import { getItemRateFromPriceList, getItemVisibility, } from 'models/helpers'; +import { ItemVisibility } from 'src/components/POS/types'; import { POSItem, ItemQtyMap, @@ -264,7 +265,7 @@ export default defineComponent({ quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null, selectedItemForBatch: '' as string, pendingBatchItem: null as { item: POSItem; quantity: number } | null, - isErpSyncValue: false, + itemVisibilityValue: 'Inventory Items' as ItemVisibility, }; }, computed: { @@ -274,8 +275,8 @@ export default defineComponent({ return !!fyo.singles.AccountingSettings?.enableDiscounting; }, isPosShiftOpen: () => !!fyo.singles.POSSettings?.isShiftOpen, - isErpSync() { - return this.isErpSyncValue; + itemVisibility() { + return this.itemVisibilityValue; }, disablePayButton(): boolean { if (!this.sinvDoc.items?.length || !this.sinvDoc.party) { @@ -301,7 +302,7 @@ export default defineComponent({ async mounted() { await this.setItems(); await this.loadPOSProfile(); - this.isErpSyncValue = !!fyo.singles.AccountingSettings?.enableERPNextSync; + this.itemVisibilityValue = await getItemVisibility(this.fyo); }, async activated() { toggleSidebar(false);