From 5a8d9cc7e8ff4e62769c04e26392c0466b6b4d5a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Dec 2024 12:37:15 +0200 Subject: [PATCH] feat: wip line-level discount --- .../api/controllers/Sales/SalesInvoices.ts | 4 ++ ...dd_discount_type_to_items_entries_table.js | 19 ++++++++ packages/server/src/interfaces/ItemEntry.ts | 4 +- packages/server/src/models/ItemEntry.ts | 46 +++++++++++++++---- .../services/Purchases/Bills/BillGLEntries.ts | 2 +- .../CommandSaleInvoiceDTOTransformer.ts | 2 +- .../Sales/Invoices/InvoiceGLEntries.ts | 2 +- .../TaxRates/ItemEntriesTaxTransactions.ts | 2 +- 8 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 packages/server/src/database/migrations/20241211103019_add_discount_type_to_items_entries_table.js diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index da0a40d18..dbe7679df 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -243,6 +243,10 @@ export default class SaleInvoicesController extends BaseController { .optional({ nullable: true }) .isNumeric() .toFloat(), + check('entries.*.discount_type') + .default(DiscountType.Percentage) + .isString() + .isIn([DiscountType.Percentage, DiscountType.Amount]), check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.tax_code') .optional({ nullable: true }) diff --git a/packages/server/src/database/migrations/20241211103019_add_discount_type_to_items_entries_table.js b/packages/server/src/database/migrations/20241211103019_add_discount_type_to_items_entries_table.js new file mode 100644 index 000000000..292bdd8dc --- /dev/null +++ b/packages/server/src/database/migrations/20241211103019_add_discount_type_to_items_entries_table.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.alterTable('items_entries', (table) => { + table.string('discount_type').defaultTo('percentage').after('discount'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.alterTable('items_entries', (table) => { + table.dropColumn('discount_type'); + }); +}; diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index 210eadf40..36c303406 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -19,8 +19,8 @@ export interface IItemEntry { amount: number; total: number; - amountInclusingTax: number; - amountExludingTax: number; + subtotalInclusingTax: number; + subtotalExcludingTax: number; discountAmount: number; landedCost: number; diff --git a/packages/server/src/models/ItemEntry.ts b/packages/server/src/models/ItemEntry.ts index 0a2aebda0..4df047cd3 100644 --- a/packages/server/src/models/ItemEntry.ts +++ b/packages/server/src/models/ItemEntry.ts @@ -1,6 +1,12 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate'; +import { DiscountType } from '@/interfaces'; + +// Subtotal (qty * rate) (tax inclusive) +// Subtotal Tax Exclusive (Subtotal - Tax Amount) +// Discount (Is percentage ? amount * discount : discount) +// Total (Subtotal - Discount) export default class ItemEntry extends TenantModel { public taxRate: number; @@ -8,7 +14,7 @@ export default class ItemEntry extends TenantModel { public quantity: number; public rate: number; public isInclusiveTax: number; - + public discountType: DiscountType; /** * Table name. * @returns {string} @@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel { */ static get virtualAttributes() { return [ + // Amount (qty * rate) 'amount', + 'taxAmount', - 'amountExludingTax', - 'amountInclusingTax', + + // Subtotal (qty * rate) + (tax inclusive) + 'subtotalInclusingTax', + + // Subtotal Tax Exclusive (Subtotal - Tax Amount) + 'subtotalExcludingTax', + + // Subtotal (qty * rate) + (tax inclusive) + 'subtotal', + + // Discount (Is percentage ? amount * discount : discount) + 'discountAmount', + + // Total (Subtotal - Discount) 'total', ]; } @@ -45,7 +65,7 @@ export default class ItemEntry extends TenantModel { * @returns {number} */ get total() { - return this.amountInclusingTax; + return this.subtotal - this.discountAmount; } /** @@ -57,19 +77,27 @@ export default class ItemEntry extends TenantModel { return this.quantity * this.rate; } + /** + * Subtotal amount (tax inclusive). + * @returns {number} + */ + get subtotal() { + return this.subtotalInclusingTax; + } + /** * Item entry amount including tax. * @returns {number} */ - get amountInclusingTax() { + get subtotalInclusingTax() { return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount; } /** - * Item entry amount excluding tax. + * Subtotal amount (tax exclusive). * @returns {number} */ - get amountExludingTax() { + get subtotalExcludingTax() { return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount; } @@ -78,7 +106,9 @@ export default class ItemEntry extends TenantModel { * @returns {number} */ get discountAmount() { - return this.amount * (this.discount / 100); + return this.discountType === DiscountType.Percentage + ? this.amount * (this.discount / 100) + : this.discount; } /** diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts index a735b9833..7382a568d 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts @@ -140,7 +140,7 @@ export class BillGLEntries { (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { const commonJournalMeta = this.getBillCommonEntry(bill); - const localAmount = bill.exchangeRate * entry.amountExludingTax; + const localAmount = bill.exchangeRate * entry.subtotalExcludingTax; const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); return { diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts index e6a080c04..bf428e6b0 100644 --- a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts @@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer { * @returns {number} */ private getDueBalanceItemEntries = (entries: ItemEntry[]) => { - return sumBy(entries, (e) => e.amount); + return sumBy(entries, (e) => e.total); }; } diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index c5ca66946..d141e5571 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts @@ -199,7 +199,7 @@ export class SaleInvoiceGLEntries { index: number ): ILedgerEntry => { const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); - const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate; + const localAmount = entry.total * saleInvoice.exchangeRate; return { ...commonEntry, diff --git a/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts index 5eaa7b980..2f4adb0ce 100644 --- a/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts +++ b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts @@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi'; import { keyBy, sumBy } from 'lodash'; import { ItemEntry } from '@/models'; import HasTenancyService from '../Tenancy/TenancyService'; -import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces'; +import { IItemEntry } from '@/interfaces'; @Service() export class ItemEntriesTaxTransactions {