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.
This commit is contained in:
Ahmed Bouhuolia
2024-12-08 18:11:03 +02:00
parent 0a5115fc20
commit 994c441bb8
11 changed files with 351 additions and 30 deletions

View File

@@ -56,8 +56,11 @@ export default class Bill extends mixin(TenantModel, [
'amountLocal', 'amountLocal',
'discountAmount', 'discountAmount',
'discountAmountLocal',
'discountPercentage', 'discountPercentage',
'adjustmentLocal',
'subtotal', 'subtotal',
'subtotalLocal', 'subtotalLocal',
'subtotalExludingTax', 'subtotalExludingTax',
@@ -119,6 +122,15 @@ export default class Bill extends mixin(TenantModel, [
: this.subtotal * (this.discount / 100); : 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. * Discount percentage.
* @returns {number | null} * @returns {number | null}
@@ -127,6 +139,14 @@ export default class Bill extends mixin(TenantModel, [
return this.discountType === DiscountType.Percentage ? this.discount : null; 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) * Invoice total. (Tax included)
* @returns {number} * @returns {number}

View File

@@ -51,10 +51,13 @@ export default class CreditNote extends mixin(TenantModel, [
'subtotalLocal', 'subtotalLocal',
'discountAmount', 'discountAmount',
'discountAmountLocal',
'discountPercentage', 'discountPercentage',
'total', 'total',
'totalLocal', 'totalLocal',
'adjustmentLocal',
]; ];
} }
@@ -92,14 +95,28 @@ export default class CreditNote extends mixin(TenantModel, [
: this.subtotal * (this.discount / 100); : this.subtotal * (this.discount / 100);
} }
/**
* Discount amount in local currency.
* @returns {number}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/** /**
* Discount percentage. * Discount percentage.
* @returns {number | null} * @returns {number | null}
*/ */
get discountPercentage(): number | null { get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage return this.discountType === DiscountType.Percentage ? this.discount : null;
? this.discount }
: null;
/**
* Adjustment amount in local currency.
* @returns {number}
*/
get adjustmentLocal() {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
} }
/** /**

View File

@@ -73,12 +73,14 @@ export default class SaleInvoice extends mixin(TenantModel, [
'taxAmountWithheldLocal', 'taxAmountWithheldLocal',
'discountAmount', 'discountAmount',
'discountAmountLocal',
'discountPercentage', 'discountPercentage',
'total', 'total',
'totalLocal', 'totalLocal',
'writtenoffAmountLocal', 'writtenoffAmountLocal',
'adjustmentLocal',
]; ];
} }
@@ -143,6 +145,14 @@ export default class SaleInvoice extends mixin(TenantModel, [
: this.subtotal * (this.discount / 100); : this.subtotal * (this.discount / 100);
} }
/**
* Local discount amount.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/** /**
* Discount percentage. * Discount percentage.
* @returns {number | null} * @returns {number | null}
@@ -151,6 +161,14 @@ export default class SaleInvoice extends mixin(TenantModel, [
return this.discountType === DiscountType.Percentage ? this.discount : null; 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) * Invoice total. (Tax included)
* @returns {number} * @returns {number}

View File

@@ -53,6 +53,7 @@ export default class SaleReceipt extends mixin(TenantModel, [
'adjustmentLocal', 'adjustmentLocal',
'discountAmount', 'discountAmount',
'discountAmountLocal',
'discountPercentage', 'discountPercentage',
'paid', 'paid',
@@ -97,14 +98,20 @@ export default class SaleReceipt extends mixin(TenantModel, [
: this.subtotal * (this.discount / 100); : 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. * Discount percentage.
* @returns {number | null} * @returns {number | null}
*/ */
get discountPercentage(): number | null { get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage return this.discountType === DiscountType.Percentage ? this.discount : null;
? this.discount
: null;
} }
/** /**

View File

@@ -60,6 +60,14 @@ export default class VendorCredit extends mixin(TenantModel, [
: this.subtotal * (this.discount / 100); : 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. * Discount percentage.
* @returns {number | null} * @returns {number | null}
@@ -68,6 +76,14 @@ export default class VendorCredit extends mixin(TenantModel, [
return this.discountType === DiscountType.Percentage ? this.discount : null; 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. * Vendor credit total.
* @returns {number} * @returns {number}
@@ -180,8 +196,11 @@ export default class VendorCredit extends mixin(TenantModel, [
'localAmount', 'localAmount',
'discountAmount', 'discountAmount',
'discountAmountLocal',
'discountPercentage', 'discountPercentage',
'adjustmentLocal',
'total', 'total',
'totalLocal', 'totalLocal',
]; ];

View File

@@ -12,6 +12,7 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import { SaleReceipt } from '@/models';
@Service() @Service()
export default class CreditNoteGLEntries { export default class CreditNoteGLEntries {
@@ -29,11 +30,15 @@ export default class CreditNoteGLEntries {
*/ */
private getCreditNoteGLedger = ( private getCreditNoteGLedger = (
creditNote: ICreditNote, creditNote: ICreditNote,
receivableAccount: number receivableAccount: number,
discountAccount: number,
adjustmentAccount: number
): Ledger => { ): Ledger => {
const ledgerEntries = this.getCreditNoteGLEntries( const ledgerEntries = this.getCreditNoteGLEntries(
creditNote, creditNote,
receivableAccount receivableAccount,
discountAccount,
adjustmentAccount
); );
return new Ledger(ledgerEntries); return new Ledger(ledgerEntries);
}; };
@@ -49,9 +54,16 @@ export default class CreditNoteGLEntries {
tenantId: number, tenantId: number,
creditNote: ICreditNote, creditNote: ICreditNote,
payableAccount: number, payableAccount: number,
discountAccount: number,
adjustmentAccount: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
const ledger = this.getCreditNoteGLedger(creditNote, payableAccount); const ledger = this.getCreditNoteGLedger(
creditNote,
payableAccount,
discountAccount,
adjustmentAccount
);
await this.ledgerStorage.commit(tenantId, ledger, trx); await this.ledgerStorage.commit(tenantId, ledger, trx);
}; };
@@ -98,11 +110,18 @@ export default class CreditNoteGLEntries {
const ARAccount = await accountRepository.findOrCreateAccountReceivable( const ARAccount = await accountRepository.findOrCreateAccountReceivable(
creditNoteWithItems.currencyCode creditNoteWithItems.currencyCode
); );
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{}
);
const adjustmentAccount =
await accountRepository.findOrCreateOtherChargesAccount({});
// Saves the credit note GL entries. // Saves the credit note GL entries.
await this.saveCreditNoteGLEntries( await this.saveCreditNoteGLEntries(
tenantId, tenantId,
creditNoteWithItems, creditNoteWithItems,
ARAccount.id, ARAccount.id,
discountAccount.id,
adjustmentAccount.id,
trx trx
); );
}; };
@@ -169,7 +188,7 @@ export default class CreditNoteGLEntries {
return { return {
...commonEntry, ...commonEntry,
credit: creditNote.localAmount, credit: creditNote.totalLocal,
accountId: ARAccountId, accountId: ARAccountId,
contactId: creditNote.customerId, contactId: creditNote.customerId,
index: 1, 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. * Retrieve the credit note GL entries.
* @param {ICreditNote} creditNote - Credit note. * @param {ICreditNote} creditNote - Credit note.
@@ -214,13 +277,21 @@ export default class CreditNoteGLEntries {
*/ */
public getCreditNoteGLEntries = ( public getCreditNoteGLEntries = (
creditNote: ICreditNote, creditNote: ICreditNote,
ARAccountId: number ARAccountId: number,
discountAccountId: number,
adjustmentAccountId: number
): ILedgerEntry[] => { ): ILedgerEntry[] => {
const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId); const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId);
const getItemEntry = this.getCreditNoteItemEntry(creditNote); const getItemEntry = this.getCreditNoteItemEntry(creditNote);
const itemsEntries = creditNote.entries.map(getItemEntry); 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];
}; };
} }

View File

@@ -18,11 +18,18 @@ export class CreditNoteTransformer extends Transformer {
'formattedAmount', 'formattedAmount',
'formattedCreditsUsed', 'formattedCreditsUsed',
'formattedSubtotal', 'formattedSubtotal',
'discountAmountFormatted', 'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted', 'discountPercentageFormatted',
'adjustmentFormatted', 'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted', 'totalFormatted',
'totalLocalFormatted', 'totalLocalFormatted',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -100,15 +107,25 @@ 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. * Retrieves formatted discount percentage.
* @param credit * @param credit
* @returns {string} * @returns {string}
*/ */
protected discountPercentageFormatted = (credit): string => { protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
? `${credit.discountPercentage}%`
: '';
}; };
/** /**
@@ -123,6 +140,18 @@ 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. * Retrieves the formatted total.
* @param credit * @param credit

View File

@@ -20,13 +20,21 @@ export class PurchaseInvoiceTransformer extends Transformer {
'formattedBalance', 'formattedBalance',
'formattedDueAmount', 'formattedDueAmount',
'formattedExchangeRate', 'formattedExchangeRate',
'subtotalFormatted', 'subtotalFormatted',
'subtotalLocalFormatted', 'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted', 'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted', 'taxAmountWithheldLocalFormatted',
'discountAmountFormatted', 'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted', 'discountPercentageFormatted',
'adjustmentFormatted', 'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted', 'totalFormatted',
'totalLocalFormatted', 'totalLocalFormatted',
'taxes', '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. * Retrieves the formatted discount percentage.
* @param {IBill} bill * @param {IBill} bill
* @returns {string} * @returns {string}
*/ */
protected discountPercentageFormatted = (bill): string => { protected discountPercentageFormatted = (bill): string => {
return bill.discountPercentage return bill.discountPercentage ? `${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. * Retrieves the total formatted.
* @param {IBill} bill * @param {IBill} bill

View File

@@ -17,9 +17,14 @@ export class VendorCreditTransformer extends Transformer {
'formattedCreatedAt', 'formattedCreatedAt',
'formattedCreditsRemaining', 'formattedCreditsRemaining',
'formattedInvoicedAmount', 'formattedInvoicedAmount',
'discountAmountFormatted', 'discountAmountFormatted',
'discountPercentageFormatted', 'discountPercentageFormatted',
'discountAmountLocalFormatted',
'adjustmentFormatted', 'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted', 'totalFormatted',
'entries', 'entries',
'attachments', '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. * Retrieves the formatted discount percentage.
* @param {IVendorCredit} credit * @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. * Retrieves the formatted invoiced amount.
* @param credit * @param credit

View File

@@ -31,13 +31,27 @@ export class SaleReceiptGLEntries {
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
const { SaleReceipt } = this.tenancy.models(tenantId); const { SaleReceipt } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const saleReceipt = await SaleReceipt.query(trx) const saleReceipt = await SaleReceipt.query(trx)
.findById(saleReceiptId) .findById(saleReceiptId)
.withGraphFetched('entries.item'); .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. // 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. // Commits the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, incomeLedger, trx); await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
@@ -87,8 +101,16 @@ export class SaleReceiptGLEntries {
* @param {ISaleReceipt} saleReceipt * @param {ISaleReceipt} saleReceipt
* @returns {Ledger} * @returns {Ledger}
*/ */
private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => { private getIncomeEntriesLedger = (
const entries = this.getIncomeGLEntries(saleReceipt); saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): Ledger => {
const entries = this.getIncomeGLEntries(
saleReceipt,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries); return new Ledger(entries);
}; };
@@ -161,24 +183,77 @@ export class SaleReceiptGLEntries {
return { return {
...commonEntry, ...commonEntry,
debit: saleReceipt.localAmount, debit: saleReceipt.totalLocal,
accountId: saleReceipt.depositAccountId, accountId: saleReceipt.depositAccountId,
index: 1, index: 1,
accountNormal: AccountNormal.DEBIT, 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. * Retrieves the income GL entries.
* @param {ISaleReceipt} saleReceipt - * @param {ISaleReceipt} saleReceipt -
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */
private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => { private getIncomeGLEntries = (
saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt); const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
const creditEntries = saleReceipt.entries.map(getItemEntry); const creditEntries = saleReceipt.entries.map(getItemEntry);
const depositEntry = this.getReceiptDepositEntry(saleReceipt); 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];
}; };
} }

View File

@@ -15,11 +15,17 @@ export class SaleReceiptTransformer extends Transformer {
return [ return [
'discountAmountFormatted', 'discountAmountFormatted',
'discountPercentageFormatted', 'discountPercentageFormatted',
'discountAmountLocalFormatted',
'subtotalFormatted', 'subtotalFormatted',
'subtotalLocalFormatted', 'subtotalLocalFormatted',
'totalFormatted', 'totalFormatted',
'totalLocalFormatted', 'totalLocalFormatted',
'adjustmentFormatted', 'adjustmentFormatted',
'adjustmentLocalFormatted',
'formattedAmount', 'formattedAmount',
'formattedReceiptDate', 'formattedReceiptDate',
'formattedClosedAtDate', 'formattedClosedAtDate',