diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 17742121..9d60928e 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -24,6 +24,7 @@ import { getPricingRulesConflicts, removeLoyaltyPoint, roundFreeItemQty, + getReturnQtyTotal, } from 'models/helpers'; import { StockTransfer } from 'models/inventory/StockTransfer'; import { validateBatch } from 'models/inventory/helpers'; @@ -94,6 +95,7 @@ export abstract class Invoice extends Transactional { isReturned?: boolean; returnAgainst?: string; + isFullyReturned?: boolean; pricingRuleDetail?: PricingRuleDetail[]; @@ -196,7 +198,8 @@ export abstract class Invoice extends Transactional { await this._removeLoyaltyPointEntry(); await this._updateIsItemsReturned(); this.reduceUsedCountOfCoupons(); - + if (this.schemaName === ModelNameEnum.SalesInvoice) + [await this.updateIsItemsFullyReturned(this)]; return; } @@ -456,6 +459,9 @@ export abstract class Invoice extends Transactional { }, (this.netTotal as Money).abs()) .sub(totalDiscount); + if (this.redeemLoyaltyPoints) { + return this.getLPAddedBaseGrandTotal(); + } return grandTotal; } @@ -648,6 +654,8 @@ export abstract class Invoice extends Transactional { let returnDocItems: DocValueMap[] = []; + const totalQtyOfReturnedItems = await getReturnQtyTotal(this); + const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty( this.schemaName, this.name @@ -655,13 +663,16 @@ export abstract class Invoice extends Transactional { for (const item of docItems) { if (!returnBalanceItemsQty) { - returnDocItems = docItems; + returnDocItems = docItems.map((docItem) => ({ + ...docItem, + name: undefined, + quantity: -(totalQtyOfReturnedItems[docItem.item as string] || 0), + })); + for (const row of returnDocItems) { - row.name = undefined; row.itemDiscountedTotal = await this.getItemsDiscountedTotal( row as InvoiceItem ); - (row.quantity as number) *= -1; } break; } @@ -677,6 +688,10 @@ export abstract class Invoice extends Transactional { const returnedItem: ReturnDocItem | undefined = returnBalanceItemsQty[item.item as string]; + if (!returnedItem) { + continue; + } + let quantity = returnedItem.quantity; let serialNumber: string | undefined = returnedItem.serialNumbers?.join('\n'); @@ -703,6 +718,22 @@ export abstract class Invoice extends Transactional { quantity: quantity, }); } + + returnDocItems.forEach((docItems) => { + const itemName = docItems.item as string; + if (itemName in totalQtyOfReturnedItems) { + docItems.quantity = totalQtyOfReturnedItems[itemName]; + } + }); + + returnDocItems = returnDocItems.filter( + (docItems) => (docItems.quantity as number) > 0 + ); + + returnDocItems.forEach((docItems) => { + docItems.quantity = -(docItems.quantity as number); + }); + const returnDocData = { ...docData, name: undefined, @@ -745,6 +776,32 @@ export abstract class Invoice extends Transactional { }); } + async updateIsItemsFullyReturned(doc?: Invoice) { + let sinvDoc; + if (doc?.returnAgainst) { + sinvDoc = await this.fyo.doc.getDoc( + ModelNameEnum.SalesInvoice, + doc.returnAgainst + ); + } + + const totalQtyOfReturnedItems = await getReturnQtyTotal( + (sinvDoc as Invoice) ?? this + ); + const isFullyReturned = Object.values(totalQtyOfReturnedItems).every( + (quantity) => quantity === 0 + ); + if (!isFullyReturned) { + return; + } + const invoiceDoc = await this.fyo.doc.getDoc( + this.schemaName, + this.returnAgainst + ); + await invoiceDoc.setAndSync({ isFullyReturned }); + await invoiceDoc.submit(); + } + async _updateIsItemsReturned() { if (!this.isReturn || !this.returnAgainst || this.isQuote) { return; @@ -818,6 +875,10 @@ export abstract class Invoice extends Transactional { this.loyaltyPoints as number ); + if (this.redeemLoyaltyPoints && (this.loyaltyPoints as number) > 0) { + this.grandTotal?.add(totalLotaltyAmount); + } + return this.grandTotal?.sub(totalLotaltyAmount); } @@ -910,10 +971,6 @@ export abstract class Invoice extends Transactional { } } - if (this.redeemLoyaltyPoints) { - return await this.getLPAddedBaseGrandTotal(); - } - return this.baseGrandTotal; }, dependsOn: ['discountAmount', 'discountPercent'], diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 9eea9d76..bbf71787 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -401,16 +401,7 @@ export abstract class InvoiceItem extends Doc { return getTaxedTotalBeforeDiscounting(totalTaxRate, rate, quantity); }, - dependsOn: [ - 'itemDiscountAmount', - 'itemDiscountPercent', - 'itemDiscountedTotal', - 'setItemDiscountAmount', - 'tax', - 'rate', - 'quantity', - 'item', - ], + dependsOn: ['rate', 'quantity', 'item'], }, stockNotTransferred: { formula: async () => { @@ -735,12 +726,12 @@ function getDiscountedTotalBeforeTaxation( * - if percent: Quantity * Rate (1 - DiscountPercent / 100) */ - const amount = rate.mul(quantity); if (setDiscountAmount) { - return amount.sub(itemDiscountAmount); + return rate.sub(itemDiscountAmount).mul(quantity); + } else if (itemDiscountPercent > 0) { + return rate.mul(quantity).percent(itemDiscountPercent); } - - return amount.mul(1 - itemDiscountPercent / 100); + return rate.mul(quantity); } function getTaxedTotalAfterDiscounting( diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 6993d865..43fcf92b 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -470,11 +470,9 @@ export class Payment extends Transactional { for (const row of this.for ?? []) { if (!this.fyo.singles.AccountingSettings?.enablePartialPayment) { - const amount = !(this.amountPaid as Money).isZero() - ? (this.amountPaid as Money) - : (this.amount as Money); - const initialAmount = this.initialAmount as Money; - if (amount.lt(initialAmount) && !amount.eq(initialAmount)) { + const amount = this.amount as Money; + const totalAmount = this.totalAmount as Money; + if (amount.lt(totalAmount)) { if (this.writeoff?.isZero()) { this.amount = this.initialAmount; row.amountPaid = this.fyo.pesa(0); @@ -705,7 +703,7 @@ export class Payment extends Transactional { }; validations: ValidationMap = { - amount: (value: DocValue) => { + amount: async (value: DocValue) => { if ((value as Money).isNegative()) { throw new ValidationError( this.fyo.t`Payment amount cannot be less than zero.` @@ -716,13 +714,22 @@ export class Payment extends Transactional { return; } - if (!this.initialAmount) { - this.initialAmount = this.amount as Money; + if (!this.totalAmount) { + for (const row of this.for ?? []) { + const referenceDoc = (await this.fyo.doc.getDoc( + row.referenceType as string, + row.referenceName as string + )) as Invoice; + + this.totalAmount = referenceDoc.outstandingAmount?.abs(); + } } - if ((value as Money).gt(this.initialAmount)) { + + if ((value as Money).gt(this.totalAmount as Money)) { + this.amount = this.initialAmount; throw new ValidationError( this.fyo.t`Payment amount cannot exceed ${this.fyo.format( - this.initialAmount, + this.totalAmount, 'Currency' )}.` ); diff --git a/models/helpers.ts b/models/helpers.ts index 48322734..e4f8566a 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -299,7 +299,6 @@ export function getMakeReturnDocAction(fyo: Fyo): Action { label: fyo.t`Return`, group: fyo.t`Create`, condition: (doc: Doc) => - !doc.isReturn && (!!fyo.singles.AccountingSettings?.enableInvoiceReturns || !!fyo.singles.InventorySettings?.enableStockReturns) && doc.isSubmitted && @@ -737,6 +736,50 @@ export async function addItem(name: string, doc: M) { await item.set('item', name); } +export async function getReturnQtyTotal( + doc: Invoice +): Promise> { + const returnDocs = await doc.fyo.db.getAll(doc.schemaName, { + fields: ['*'], + filters: { + returnAgainst: doc.name as string, + }, + }); + + const returnedDocs = await Promise.all( + returnDocs.map((docss) => + doc.fyo.doc.getDoc(doc.schemaName, docss.name as string) + ) + ); + + const quantitySum: { [key: string]: number } = {}; + + if ('items' in doc && Array.isArray(doc.items)) { + doc.items.forEach((docItem) => { + const itemName = docItem.item as string; + if (itemName) { + quantitySum[itemName] = (docItem.quantity as number) || 0; + } + }); + } + + if (!returnedDocs) { + return quantitySum; + } + returnedDocs.forEach((returnedDoc) => { + if (returnedDoc && returnedDoc.items) { + (returnedDoc.items as InvoiceItem[]).forEach((item) => { + const itemName = item.item; + if (itemName && quantitySum.hasOwnProperty(itemName)) { + quantitySum[itemName] = + quantitySum[itemName] - Math.abs(item.quantity as number); + } + }); + } + }); + return quantitySum; +} + export async function createLoyaltyPointEntry(doc: Invoice) { const loyaltyProgramDoc = (await doc.fyo.doc.getDoc( ModelNameEnum.LoyaltyProgram, diff --git a/schemas/app/Invoice.json b/schemas/app/Invoice.json index 74191775..4bff7f5a 100644 --- a/schemas/app/Invoice.json +++ b/schemas/app/Invoice.json @@ -187,6 +187,12 @@ "hidden": true, "default": false }, + { + "fieldname": "isFullyReturned", + "fieldtype": "Check", + "hidden": true, + "default": false + }, { "fieldname": "isSyncedWithErp", "fieldtype": "Check", diff --git a/src/pages/POS/LoyaltyProgramModal.vue b/src/pages/POS/LoyaltyProgramModal.vue index a2934fe1..afcb3568 100644 --- a/src/pages/POS/LoyaltyProgramModal.vue +++ b/src/pages/POS/LoyaltyProgramModal.vue @@ -1,6 +1,6 @@