From 994c441bb8d13facc339557bcf8d887b9e9a3afd Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 8 Dec 2024 18:11:03 +0200 Subject: [PATCH] feat: add local discount and adjustment calculations to financial models and transformers - Introduced `discountAmountLocal` and `adjustmentLocal` properties across Bill, CreditNote, SaleInvoice, SaleReceipt, and VendorCredit models to calculate amounts in local currency. - Updated transformers for CreditNote, PurchaseInvoice, and VendorCredit to include formatted representations of local discount and adjustment amounts. - Enhanced GL entry services to handle discount and adjustment entries for SaleReceipt and CreditNote, ensuring accurate ledger entries. - Improved overall consistency in handling financial calculations across various models and services. --- packages/server/src/models/Bill.ts | 20 +++++ packages/server/src/models/CreditNote.ts | 23 ++++- packages/server/src/models/SaleInvoice.ts | 18 ++++ packages/server/src/models/SaleReceipt.ts | 13 ++- packages/server/src/models/VendorCredit.ts | 19 ++++ .../CreditNotes/CreditNoteGLEntries.ts | 83 ++++++++++++++++-- .../CreditNotes/CreditNoteTransformer.ts | 47 ++++++++-- .../Bills/PurchaseInvoiceTransformer.ts | 36 +++++++- .../VendorCredits/VendorCreditTransformer.ts | 29 +++++++ .../Sales/Receipts/SaleReceiptGLEntries.ts | 87 +++++++++++++++++-- .../Sales/Receipts/SaleReceiptTransformer.ts | 6 ++ 11 files changed, 351 insertions(+), 30 deletions(-) diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index c2e6eb3f4..e813ac52e 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -56,8 +56,11 @@ export default class Bill extends mixin(TenantModel, [ 'amountLocal', 'discountAmount', + 'discountAmountLocal', 'discountPercentage', + 'adjustmentLocal', + 'subtotal', 'subtotalLocal', 'subtotalExludingTax', @@ -119,6 +122,15 @@ export default class Bill extends mixin(TenantModel, [ : this.subtotal * (this.discount / 100); } + /** + * Discount amount in local currency. + * @returns {number | null} + */ + get discountAmountLocal() { + return this.discountAmount ? this.discountAmount * this.exchangeRate : null; + } + + /** /** * Discount percentage. * @returns {number | null} @@ -127,6 +139,14 @@ export default class Bill extends mixin(TenantModel, [ return this.discountType === DiscountType.Percentage ? this.discount : null; } + /** + * Adjustment amount in local currency. + * @returns {number | null} + */ + get adjustmentLocal() { + return this.adjustment ? this.adjustment * this.exchangeRate : null; + } + /** * Invoice total. (Tax included) * @returns {number} diff --git a/packages/server/src/models/CreditNote.ts b/packages/server/src/models/CreditNote.ts index cdbbf9052..ab5f6c878 100644 --- a/packages/server/src/models/CreditNote.ts +++ b/packages/server/src/models/CreditNote.ts @@ -51,10 +51,13 @@ export default class CreditNote extends mixin(TenantModel, [ 'subtotalLocal', 'discountAmount', + 'discountAmountLocal', 'discountPercentage', 'total', 'totalLocal', + + 'adjustmentLocal', ]; } @@ -92,14 +95,28 @@ export default class CreditNote extends mixin(TenantModel, [ : this.subtotal * (this.discount / 100); } + /** + * Discount amount in local currency. + * @returns {number} + */ + get discountAmountLocal() { + return this.discountAmount ? this.discountAmount * this.exchangeRate : null; + } + /** * Discount percentage. * @returns {number | null} */ get discountPercentage(): number | null { - return this.discountType === DiscountType.Percentage - ? this.discount - : null; + return this.discountType === DiscountType.Percentage ? this.discount : null; + } + + /** + * Adjustment amount in local currency. + * @returns {number} + */ + get adjustmentLocal() { + return this.adjustment ? this.adjustment * this.exchangeRate : null; } /** diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 098d4a20d..dfcb0a954 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -73,12 +73,14 @@ export default class SaleInvoice extends mixin(TenantModel, [ 'taxAmountWithheldLocal', 'discountAmount', + 'discountAmountLocal', 'discountPercentage', 'total', 'totalLocal', 'writtenoffAmountLocal', + 'adjustmentLocal', ]; } @@ -143,6 +145,14 @@ export default class SaleInvoice extends mixin(TenantModel, [ : this.subtotal * (this.discount / 100); } + /** + * Local discount amount. + * @returns {number | null} + */ + get discountAmountLocal() { + return this.discountAmount ? this.discountAmount * this.exchangeRate : null; + } + /** * Discount percentage. * @returns {number | null} @@ -151,6 +161,14 @@ export default class SaleInvoice extends mixin(TenantModel, [ return this.discountType === DiscountType.Percentage ? this.discount : null; } + /** + * Adjustment amount in local currency. + * @returns {number | null} + */ + get adjustmentLocal(): number | null { + return this.adjustment ? this.adjustment * this.exchangeRate : null; + } + /** * Invoice total. (Tax included) * @returns {number} diff --git a/packages/server/src/models/SaleReceipt.ts b/packages/server/src/models/SaleReceipt.ts index 939f462a1..95abf7430 100644 --- a/packages/server/src/models/SaleReceipt.ts +++ b/packages/server/src/models/SaleReceipt.ts @@ -53,6 +53,7 @@ export default class SaleReceipt extends mixin(TenantModel, [ 'adjustmentLocal', 'discountAmount', + 'discountAmountLocal', 'discountPercentage', 'paid', @@ -97,14 +98,20 @@ export default class SaleReceipt extends mixin(TenantModel, [ : this.subtotal * (this.discount / 100); } + /** + * Discount amount in local currency. + * @returns {number | null} + */ + get discountAmountLocal() { + return this.discountAmount ? this.discountAmount * this.exchangeRate : null; + } + /** * Discount percentage. * @returns {number | null} */ get discountPercentage(): number | null { - return this.discountType === DiscountType.Percentage - ? this.discount - : null; + return this.discountType === DiscountType.Percentage ? this.discount : null; } /** diff --git a/packages/server/src/models/VendorCredit.ts b/packages/server/src/models/VendorCredit.ts index 0c4b3d423..75473060e 100644 --- a/packages/server/src/models/VendorCredit.ts +++ b/packages/server/src/models/VendorCredit.ts @@ -60,6 +60,14 @@ export default class VendorCredit extends mixin(TenantModel, [ : this.subtotal * (this.discount / 100); } + /** + * Discount amount in local currency. + * @returns {number | null} + */ + get discountAmountLocal() { + return this.discountAmount ? this.discountAmount * this.exchangeRate : null; + } + /** * Discount percentage. * @returns {number | null} @@ -68,6 +76,14 @@ export default class VendorCredit extends mixin(TenantModel, [ return this.discountType === DiscountType.Percentage ? this.discount : null; } + /** + * Adjustment amount in local currency. + * @returns {number | null} + */ + get adjustmentLocal() { + return this.adjustment ? this.adjustment * this.exchangeRate : null; + } + /** * Vendor credit total. * @returns {number} @@ -180,8 +196,11 @@ export default class VendorCredit extends mixin(TenantModel, [ 'localAmount', 'discountAmount', + 'discountAmountLocal', 'discountPercentage', + 'adjustmentLocal', + 'total', 'totalLocal', ]; diff --git a/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts b/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts index 697072f3c..08734d37a 100644 --- a/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts +++ b/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts @@ -12,6 +12,7 @@ import { import HasTenancyService from '@/services/Tenancy/TenancyService'; import Ledger from '@/services/Accounting/Ledger'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +import { SaleReceipt } from '@/models'; @Service() export default class CreditNoteGLEntries { @@ -29,11 +30,15 @@ export default class CreditNoteGLEntries { */ private getCreditNoteGLedger = ( creditNote: ICreditNote, - receivableAccount: number + receivableAccount: number, + discountAccount: number, + adjustmentAccount: number ): Ledger => { const ledgerEntries = this.getCreditNoteGLEntries( creditNote, - receivableAccount + receivableAccount, + discountAccount, + adjustmentAccount ); return new Ledger(ledgerEntries); }; @@ -49,9 +54,16 @@ export default class CreditNoteGLEntries { tenantId: number, creditNote: ICreditNote, payableAccount: number, + discountAccount: number, + adjustmentAccount: number, trx?: Knex.Transaction ): Promise => { - const ledger = this.getCreditNoteGLedger(creditNote, payableAccount); + const ledger = this.getCreditNoteGLedger( + creditNote, + payableAccount, + discountAccount, + adjustmentAccount + ); await this.ledgerStorage.commit(tenantId, ledger, trx); }; @@ -98,11 +110,18 @@ export default class CreditNoteGLEntries { const ARAccount = await accountRepository.findOrCreateAccountReceivable( creditNoteWithItems.currencyCode ); + const discountAccount = await accountRepository.findOrCreateDiscountAccount( + {} + ); + const adjustmentAccount = + await accountRepository.findOrCreateOtherChargesAccount({}); // Saves the credit note GL entries. await this.saveCreditNoteGLEntries( tenantId, creditNoteWithItems, ARAccount.id, + discountAccount.id, + adjustmentAccount.id, trx ); }; @@ -169,7 +188,7 @@ export default class CreditNoteGLEntries { return { ...commonEntry, - credit: creditNote.localAmount, + credit: creditNote.totalLocal, accountId: ARAccountId, contactId: creditNote.customerId, index: 1, @@ -206,6 +225,50 @@ export default class CreditNoteGLEntries { } ); + /** + * Retrieves the credit note discount entry. + * @param {ICreditNote} creditNote + * @param {number} discountAccountId + * @returns {ILedgerEntry} + */ + private getDiscountEntry = ( + creditNote: ICreditNote, + discountAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); + + return { + ...commonEntry, + credit: creditNote.discountAmountLocal, + accountId: discountAccountId, + index: 1, + accountNormal: AccountNormal.CREDIT, + }; + }; + + /** + * Retrieves the credit note adjustment entry. + * @param {ICreditNote} creditNote + * @param {number} adjustmentAccountId + * @returns {ILedgerEntry} + */ + private getAdjustmentEntry = ( + creditNote: ICreditNote, + adjustmentAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); + const adjustmentAmount = Math.abs(creditNote.adjustmentLocal); + + return { + ...commonEntry, + credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0, + debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0, + accountId: adjustmentAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + }; + }; + /** * Retrieve the credit note GL entries. * @param {ICreditNote} creditNote - Credit note. @@ -214,13 +277,21 @@ export default class CreditNoteGLEntries { */ public getCreditNoteGLEntries = ( creditNote: ICreditNote, - ARAccountId: number + ARAccountId: number, + discountAccountId: number, + adjustmentAccountId: number ): ILedgerEntry[] => { const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId); const getItemEntry = this.getCreditNoteItemEntry(creditNote); const itemsEntries = creditNote.entries.map(getItemEntry); - return [AREntry, ...itemsEntries]; + const discountEntry = this.getDiscountEntry(creditNote, discountAccountId); + const adjustmentEntry = this.getAdjustmentEntry( + creditNote, + adjustmentAccountId + ); + + return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries]; }; } diff --git a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts index 611d006ad..86e84681d 100644 --- a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts +++ b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts @@ -18,11 +18,18 @@ export class CreditNoteTransformer extends Transformer { 'formattedAmount', 'formattedCreditsUsed', 'formattedSubtotal', + 'discountAmountFormatted', + 'discountAmountLocalFormatted', + 'discountPercentageFormatted', + 'adjustmentFormatted', + 'adjustmentLocalFormatted', + 'totalFormatted', 'totalLocalFormatted', + 'entries', 'attachments', ]; @@ -39,7 +46,7 @@ export class CreditNoteTransformer extends Transformer { /** * Retrieve formatted created at date. - * @param credit + * @param credit * @returns {string} */ protected formattedCreatedAt = (credit): string => { @@ -90,7 +97,7 @@ export class CreditNoteTransformer extends Transformer { /** * Retrieves formatted discount amount. - * @param credit + * @param credit * @returns {string} */ protected discountAmountFormatted = (credit): string => { @@ -100,20 +107,30 @@ export class CreditNoteTransformer extends Transformer { }); }; + /** + * Retrieves the formatted discount amount in local currency. + * @param {ICreditNote} credit + * @returns {string} + */ + protected discountAmountLocalFormatted = (credit): string => { + return formatNumber(credit.discountAmountLocal, { + currencyCode: credit.currencyCode, + excerptZero: true, + }); + }; + /** * Retrieves formatted discount percentage. - * @param credit + * @param credit * @returns {string} */ protected discountPercentageFormatted = (credit): string => { - return credit.discountPercentage - ? `${credit.discountPercentage}%` - : ''; + return credit.discountPercentage ? `${credit.discountPercentage}%` : ''; }; /** * Retrieves formatted adjustment amount. - * @param credit + * @param credit * @returns {string} */ protected adjustmentFormatted = (credit): string => { @@ -123,9 +140,21 @@ export class CreditNoteTransformer extends Transformer { }); }; + /** + * Retrieves the formatted adjustment amount in local currency. + * @param {ICreditNote} credit + * @returns {string} + */ + protected adjustmentLocalFormatted = (credit): string => { + return formatNumber(credit.adjustmentLocal, { + currencyCode: this.context.organization.baseCurrency, + excerptZero: true, + }); + }; + /** * Retrieves the formatted total. - * @param credit + * @param credit * @returns {string} */ protected totalFormatted = (credit): string => { @@ -136,7 +165,7 @@ export class CreditNoteTransformer extends Transformer { /** * Retrieves the formatted total in local currency. - * @param credit + * @param credit * @returns {string} */ protected totalLocalFormatted = (credit): string => { diff --git a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts index a6766a0db..05aa0311d 100644 --- a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts @@ -20,13 +20,21 @@ export class PurchaseInvoiceTransformer extends Transformer { 'formattedBalance', 'formattedDueAmount', 'formattedExchangeRate', + 'subtotalFormatted', 'subtotalLocalFormatted', + 'subtotalExcludingTaxFormatted', 'taxAmountWithheldLocalFormatted', + 'discountAmountFormatted', + 'discountAmountLocalFormatted', + 'discountPercentageFormatted', + 'adjustmentFormatted', + 'adjustmentLocalFormatted', + 'totalFormatted', 'totalLocalFormatted', 'taxes', @@ -175,15 +183,25 @@ export class PurchaseInvoiceTransformer extends Transformer { }); }; + /** + * Retrieves the formatted discount amount in local currency. + * @param {IBill} bill + * @returns {string} + */ + protected discountAmountLocalFormatted = (bill): string => { + return formatNumber(bill.discountAmountLocal, { + currencyCode: this.context.organization.baseCurrency, + excerptZero: true, + }); + }; + /** * Retrieves the formatted discount percentage. * @param {IBill} bill * @returns {string} */ protected discountPercentageFormatted = (bill): string => { - return bill.discountPercentage - ? `${bill.discountPercentage}%` - : ''; + return bill.discountPercentage ? `${bill.discountPercentage}%` : ''; }; /** @@ -198,6 +216,18 @@ export class PurchaseInvoiceTransformer extends Transformer { }); }; + /** + * Retrieves the formatted adjustment amount in local currency. + * @param {IBill} bill + * @returns {string} + */ + protected adjustmentLocalFormatted = (bill): string => { + return formatNumber(bill.adjustmentLocal, { + currencyCode: this.context.organization.baseCurrency, + excerptZero: true, + }); + }; + /** * Retrieves the total formatted. * @param {IBill} bill diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts index 8cc36ba7f..61e4d6736 100644 --- a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts +++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts @@ -17,9 +17,14 @@ export class VendorCreditTransformer extends Transformer { 'formattedCreatedAt', 'formattedCreditsRemaining', 'formattedInvoicedAmount', + 'discountAmountFormatted', 'discountPercentageFormatted', + 'discountAmountLocalFormatted', + 'adjustmentFormatted', + 'adjustmentLocalFormatted', + 'totalFormatted', 'entries', 'attachments', @@ -87,6 +92,18 @@ export class VendorCreditTransformer extends Transformer { }); }; + /** + * Retrieves the formatted discount amount in local currency. + * @param {IVendorCredit} credit + * @returns {string} + */ + protected discountAmountLocalFormatted = (credit): string => { + return formatNumber(credit.discountAmountLocal, { + currencyCode: this.context.organization.baseCurrency, + excerptZero: true, + }); + }; + /** * Retrieves the formatted discount percentage. * @param {IVendorCredit} credit @@ -108,6 +125,18 @@ export class VendorCreditTransformer extends Transformer { }); }; + /** + * Retrieves the formatted adjustment amount in local currency. + * @param {IVendorCredit} credit + * @returns {string} + */ + protected adjustmentLocalFormatted = (credit): string => { + return formatNumber(credit.adjustmentLocal, { + currencyCode: this.context.organization.baseCurrency, + excerptZero: true, + }); + }; + /** * Retrieves the formatted invoiced amount. * @param credit diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts index d354141e9..5f45d6f8d 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts @@ -31,13 +31,27 @@ export class SaleReceiptGLEntries { trx?: Knex.Transaction ): Promise => { const { SaleReceipt } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); const saleReceipt = await SaleReceipt.query(trx) .findById(saleReceiptId) .withGraphFetched('entries.item'); + // Find or create the discount expense account. + const discountAccount = await accountRepository.findOrCreateDiscountAccount( + {}, + trx + ); + // Find or create the other charges account. + const otherChargesAccount = + await accountRepository.findOrCreateOtherChargesAccount({}, trx); + // Retrieve the income entries ledger. - const incomeLedger = this.getIncomeEntriesLedger(saleReceipt); + const incomeLedger = this.getIncomeEntriesLedger( + saleReceipt, + discountAccount.id, + otherChargesAccount.id + ); // Commits the ledger entries to the storage. await this.ledgerStorage.commit(tenantId, incomeLedger, trx); @@ -87,8 +101,16 @@ export class SaleReceiptGLEntries { * @param {ISaleReceipt} saleReceipt * @returns {Ledger} */ - private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => { - const entries = this.getIncomeGLEntries(saleReceipt); + private getIncomeEntriesLedger = ( + saleReceipt: ISaleReceipt, + discountAccountId: number, + otherChargesAccountId: number + ): Ledger => { + const entries = this.getIncomeGLEntries( + saleReceipt, + discountAccountId, + otherChargesAccountId + ); return new Ledger(entries); }; @@ -161,24 +183,77 @@ export class SaleReceiptGLEntries { return { ...commonEntry, - debit: saleReceipt.localAmount, + debit: saleReceipt.totalLocal, accountId: saleReceipt.depositAccountId, index: 1, accountNormal: AccountNormal.DEBIT, }; }; + /** + * Retrieves the discount GL entry. + * @param {ISaleReceipt} saleReceipt + * @param {number} discountAccountId + * @returns {ILedgerEntry} + */ + private getDiscountEntry = ( + saleReceipt: ISaleReceipt, + discountAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); + + return { + ...commonEntry, + debit: saleReceipt.discountAmount, + accountId: discountAccountId, + index: 1, + accountNormal: AccountNormal.CREDIT, + }; + }; + + /** + * Retrieves the adjustment GL entry. + * @param {ISaleReceipt} saleReceipt + * @param {number} adjustmentAccountId + * @returns {ILedgerEntry} + */ + private getAdjustmentEntry = ( + saleReceipt: ISaleReceipt, + adjustmentAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); + const adjustmentAmount = Math.abs(saleReceipt.adjustment); + + return { + ...commonEntry, + debit: saleReceipt.adjustment < 0 ? adjustmentAmount : 0, + credit: saleReceipt.adjustment > 0 ? adjustmentAmount : 0, + accountId: adjustmentAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + }; + }; + /** * Retrieves the income GL entries. * @param {ISaleReceipt} saleReceipt - * @returns {ILedgerEntry[]} */ - private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => { + private getIncomeGLEntries = ( + saleReceipt: ISaleReceipt, + discountAccountId: number, + otherChargesAccountId: number + ): ILedgerEntry[] => { const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt); const creditEntries = saleReceipt.entries.map(getItemEntry); const depositEntry = this.getReceiptDepositEntry(saleReceipt); + const discountEntry = this.getDiscountEntry(saleReceipt, discountAccountId); + const adjustmentEntry = this.getAdjustmentEntry( + saleReceipt, + otherChargesAccountId + ); - return [depositEntry, ...creditEntries]; + return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry]; }; } diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts index 52d59821e..eb833b344 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts @@ -15,11 +15,17 @@ export class SaleReceiptTransformer extends Transformer { return [ 'discountAmountFormatted', 'discountPercentageFormatted', + 'discountAmountLocalFormatted', + 'subtotalFormatted', 'subtotalLocalFormatted', + 'totalFormatted', 'totalLocalFormatted', + 'adjustmentFormatted', + 'adjustmentLocalFormatted', + 'formattedAmount', 'formattedReceiptDate', 'formattedClosedAtDate',