From 6323e2ffec78b43a13a8c7848ba22d9e75d78e15 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Dec 2024 11:44:10 +0200 Subject: [PATCH 1/4] fix: line-level discount --- .../src/components/EstimatePaperTemplate.tsx | 15 +++++++++++- .../src/components/InvoicePaperTemplate.tsx | 23 +++++++++++++++---- .../src/components/PaperTemplate.tsx | 7 ++++-- .../src/components/ReceiptPaperTemplate.tsx | 15 ++++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx b/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx index ac48e7c19..d601e9e56 100644 --- a/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx +++ b/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx @@ -92,6 +92,10 @@ export interface EstimatePaperTemplateProps extends PaperTemplateProps { lineQuantityLabel?: string; lineRateLabel?: string; lineTotalLabel?: string; + + // # Line Discount + lineDiscountLabel?: string; + showLineDiscount?: boolean; } export function EstimatePaperTemplate({ @@ -173,8 +177,11 @@ export function EstimatePaperTemplate({ lineQuantityLabel = 'Qty', lineRateLabel = 'Rate', lineTotalLabel = 'Total', -}: EstimatePaperTemplateProps) { + // # Line Discount + lineDiscountLabel = 'Discount', + showLineDiscount = false, +}: EstimatePaperTemplateProps) { return ( @@ -240,6 +247,12 @@ export function EstimatePaperTemplate({ }, { label: lineQuantityLabel, accessor: 'quantity' }, { label: lineRateLabel, accessor: 'rate', align: 'right' }, + { + label: lineDiscountLabel, + accessor: 'discount', + align: 'right', + visible: showLineDiscount, + }, { label: lineTotalLabel, accessor: 'total', align: 'right' }, ]} data={lines} diff --git a/shared/pdf-templates/src/components/InvoicePaperTemplate.tsx b/shared/pdf-templates/src/components/InvoicePaperTemplate.tsx index 487ae16ef..fd45bbb73 100644 --- a/shared/pdf-templates/src/components/InvoicePaperTemplate.tsx +++ b/shared/pdf-templates/src/components/InvoicePaperTemplate.tsx @@ -17,15 +17,16 @@ import { DefaultPdfTemplateAddressBilledFrom, } from './_constants'; -interface PapaerLine { +interface InvoiceLine { item?: string; description?: string; quantity?: string; rate?: string; total?: string; + discount?: string; } -interface PaperTax { +interface InvoiceTaxLine { label: string; amount: string; } @@ -71,6 +72,10 @@ export interface InvoicePaperTemplateProps extends PaperTemplateProps { lineRateLabel?: string; lineTotalLabel?: string; + // # Line Discount + lineDiscountLabel?: string; + showLineDiscount?: boolean; + // Total showTotal?: boolean; totalLabel?: string; @@ -113,8 +118,8 @@ export interface InvoicePaperTemplateProps extends PaperTemplateProps { showStatement?: boolean; statement?: string; - lines?: Array; - taxes?: Array; + lines?: Array; + taxes?: Array; } export function InvoicePaperTemplate({ @@ -165,6 +170,10 @@ export function InvoicePaperTemplate({ paymentMadeLabel = 'Payment Made', dueAmountLabel = 'Balance Due', + // # Line Discount + lineDiscountLabel = 'Discount', + showLineDiscount = false, + // Totals showTotal = true, total = '$662.75', @@ -277,6 +286,12 @@ export function InvoicePaperTemplate({ align: 'right', }, { label: lineRateLabel, accessor: 'rate', align: 'right' }, + { + label: lineDiscountLabel, + accessor: 'discount', + align: 'right', + visible: showLineDiscount, + }, { label: lineTotalLabel, accessor: 'total', align: 'right' }, ]} data={lines} diff --git a/shared/pdf-templates/src/components/PaperTemplate.tsx b/shared/pdf-templates/src/components/PaperTemplate.tsx index 220e0305a..85f9b134a 100644 --- a/shared/pdf-templates/src/components/PaperTemplate.tsx +++ b/shared/pdf-templates/src/components/PaperTemplate.tsx @@ -90,11 +90,14 @@ interface PaperTemplateTableProps { value?: JSX.Element; align?: 'left' | 'center' | 'right'; thStyle?: React.CSSProperties; + visible?: boolean; }>; data: Array>; } PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => { + const filteredColumns = columns.filter((col) => col.visible !== false); + return ( { > - {columns.map((col, index) => ( + {filteredColumns.map((col, index) => ( {col.label} @@ -151,7 +154,7 @@ PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => { {data.map((_data: any) => ( - {columns.map((column, index) => ( + {filteredColumns.map((column, index) => ( {isFunction(column?.accessor) ? column?.accessor(_data) diff --git a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx index 04ddb24e3..76f3a38fa 100644 --- a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx +++ b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx @@ -71,9 +71,14 @@ export interface ReceiptPaperTemplateProps extends PaperTemplateProps { description: string; rate: string; quantity: string; + discount?: string; total: string; }>; + // # Line Discount + lineDiscountLabel?: string; + showLineDiscount?: boolean; + // Receipt Date. receiptDateLabel?: string; showReceiptDate?: boolean; @@ -165,6 +170,10 @@ export function ReceiptPaperTemplate({ lineQuantityLabel = 'Qty', lineRateLabel = 'Rate', lineTotalLabel = 'Total', + + // # Line Discount + lineDiscountLabel = 'Discount', + showLineDiscount = false, }: ReceiptPaperTemplateProps) { return ( @@ -226,6 +235,12 @@ export function ReceiptPaperTemplate({ }, { label: lineQuantityLabel, accessor: 'quantity' }, { label: lineRateLabel, accessor: 'rate', align: 'right' }, + { + label: lineDiscountLabel, + accessor: 'discount', + align: 'right', + visible: showLineDiscount, + }, { label: lineTotalLabel, accessor: 'total', align: 'right' }, ]} data={lines} From 5a8d9cc7e8ff4e62769c04e26392c0466b6b4d5a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Dec 2024 12:37:15 +0200 Subject: [PATCH 2/4] 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 { From 8cd1b36a028eab5531793c9ea713c06d4990a7ea Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Dec 2024 15:05:50 +0200 Subject: [PATCH 3/4] feat: item-level discount --- .../src/api/controllers/Purchases/Bills.ts | 5 +++ .../api/controllers/Purchases/VendorCredit.ts | 8 ++++ .../src/api/controllers/Sales/CreditNotes.ts | 4 ++ .../api/controllers/Sales/SalesEstimates.ts | 5 +++ .../api/controllers/Sales/SalesReceipts.ts | 5 +++ packages/server/src/interfaces/ItemEntry.ts | 1 + .../src/services/Sales/Estimates/utils.ts | 2 + .../Sales/Invoices/ItemEntryTransformer.ts | 40 ++++++++++++++++++- .../src/services/Sales/Invoices/utils.ts | 3 +- .../src/services/Sales/Receipts/utils.ts | 7 +++- .../src/components/Datatable/DataTable.tsx | 4 ++ .../Drawers/BillDrawer/BillDetailTable.tsx | 4 ++ .../containers/Drawers/BillDrawer/utils.tsx | 12 ++++++ .../CreditNoteDetailTable.tsx | 4 ++ .../Drawers/CreditNoteDetailDrawer/utils.tsx | 13 +++++- .../EstimateDetailTable.tsx | 4 ++ .../Drawers/EstimateDetailDrawer/utils.tsx | 12 ++++++ .../InvoiceDetailTable.tsx | 5 +++ .../Drawers/InvoiceDetailDrawer/utils.tsx | 12 ++++++ .../ReceiptDetailTable.tsx | 4 ++ .../Drawers/ReceiptDetailDrawer/utils.tsx | 12 ++++++ .../VendorCreditDetailTable.tsx | 4 ++ .../VendorCreditDetailDrawer/utils.tsx | 13 +++++- 23 files changed, 176 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 629579863..ec901247e 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -127,6 +127,11 @@ export default class BillsController 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.*.landed_cost') .optional({ nullable: true }) diff --git a/packages/server/src/api/controllers/Purchases/VendorCredit.ts b/packages/server/src/api/controllers/Purchases/VendorCredit.ts index 6f65ca700..dd8bdae7d 100644 --- a/packages/server/src/api/controllers/Purchases/VendorCredit.ts +++ b/packages/server/src/api/controllers/Purchases/VendorCredit.ts @@ -176,6 +176,10 @@ export default class VendorCreditController 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.*.warehouse_id') .optional({ nullable: true }) @@ -225,6 +229,10 @@ export default class VendorCreditController 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.*.warehouse_id') .optional({ nullable: true }) diff --git a/packages/server/src/api/controllers/Sales/CreditNotes.ts b/packages/server/src/api/controllers/Sales/CreditNotes.ts index 55ec28e98..fa1a90b27 100644 --- a/packages/server/src/api/controllers/Sales/CreditNotes.ts +++ b/packages/server/src/api/controllers/Sales/CreditNotes.ts @@ -239,6 +239,10 @@ export default class PaymentReceivesController 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.*.warehouse_id') .optional({ nullable: true }) diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 3af6b9805..1c784a1f0 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -187,6 +187,11 @@ export default class SalesEstimatesController extends BaseController { .optional({ nullable: true }) .isNumeric() .toFloat(), + check('entries.*.discount_type') + .default(DiscountType.Percentage) + .isString() + .isIn([DiscountType.Percentage, DiscountType.Amount]), + check('entries.*.warehouse_id') .optional({ nullable: true }) .isNumeric() diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index a48a10e09..47e03d03f 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -164,6 +164,11 @@ export default class SalesReceiptsController extends BaseController { .optional({ nullable: true }) .isNumeric() .toInt(), + check('entries.*.discount_type') + .default(DiscountType.Percentage) + .isString() + .isIn([DiscountType.Percentage, DiscountType.Amount]), + check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.warehouse_id') .optional({ nullable: true }) diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index 36c303406..e26b8cdba 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -13,6 +13,7 @@ export interface IItemEntry { itemId: number; description: string; + discountType?: string; discount: number; quantity: number; rate: number; diff --git a/packages/server/src/services/Sales/Estimates/utils.ts b/packages/server/src/services/Sales/Estimates/utils.ts index d39fd7f0b..e67a3e235 100644 --- a/packages/server/src/services/Sales/Estimates/utils.ts +++ b/packages/server/src/services/Sales/Estimates/utils.ts @@ -13,6 +13,7 @@ export const transformEstimateToPdfTemplate = ( description: entry.description, rate: entry.rateFormatted, quantity: entry.quantityFormatted, + discount: entry.discountFormatted, total: entry.totalFormatted, })), total: estimate.totalFormatted, @@ -21,6 +22,7 @@ export const transformEstimateToPdfTemplate = ( customerNote: estimate.note, termsConditions: estimate.termsConditions, customerAddress: contactAddressTextFormat(estimate.customer), + showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted), discount: estimate.discountAmountFormatted, discountLabel: estimate.discountPercentageFormatted ? `Discount [${estimate.discountPercentageFormatted}]` diff --git a/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts b/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts index dbaea4862..5fcf33b85 100644 --- a/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts @@ -1,4 +1,4 @@ -import { IItemEntry } from '@/interfaces'; +import { DiscountType, IItemEntry } from '@/interfaces'; import { Transformer } from '@/lib/Transformer/Transformer'; import { formatNumber } from '@/utils'; @@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer { * @returns {Array} */ public includeAttributes = (): string[] => { - return ['quantityFormatted', 'rateFormatted', 'totalFormatted']; + return [ + 'quantityFormatted', + 'rateFormatted', + 'totalFormatted', + 'discountFormatted', + 'discountAmountFormatted', + ]; }; /** @@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer { money: false, }); }; + + /** + * Retrieves the formatted discount of item entry. + * @param {IItemEntry} entry + * @returns {string} + */ + protected discountFormatted = (entry: IItemEntry): string => { + if (!entry.discount) { + return ''; + } + return entry.discountType === DiscountType.Percentage + ? `${entry.discount}%` + : formatNumber(entry.discount, { + currencyCode: this.context.currencyCode, + money: false, + }); + }; + + /** + * Retrieves the formatted discount amount of item entry. + * @param {IItemEntry} entry + * @returns {string} + */ + protected discountAmountFormatted = (entry: IItemEntry): string => { + return formatNumber(entry.discountAmount, { + currencyCode: this.context.currencyCode, + money: false, + excerptZero: true, + }); + }; } diff --git a/packages/server/src/services/Sales/Invoices/utils.ts b/packages/server/src/services/Sales/Invoices/utils.ts index af8ba6145..0de33fc81 100644 --- a/packages/server/src/services/Sales/Invoices/utils.ts +++ b/packages/server/src/services/Sales/Invoices/utils.ts @@ -43,13 +43,14 @@ export const transformInvoiceToPdfTemplate = ( description: entry.description, rate: entry.rateFormatted, quantity: entry.quantityFormatted, + discount: entry.discountFormatted, total: entry.totalFormatted, })), taxes: invoice.taxes.map((tax) => ({ label: tax.name, amount: tax.taxRateAmountFormatted, })), - + showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted), customerAddress: contactAddressTextFormat(invoice.customer), }; }; diff --git a/packages/server/src/services/Sales/Receipts/utils.ts b/packages/server/src/services/Sales/Receipts/utils.ts index 3d3bb8733..f29c0189a 100644 --- a/packages/server/src/services/Sales/Receipts/utils.ts +++ b/packages/server/src/services/Sales/Receipts/utils.ts @@ -1,9 +1,8 @@ -import { ISaleReceipt } from '@/interfaces'; import { contactAddressTextFormat } from '@/utils/address-text-format'; import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates'; export const transformReceiptToBrandingTemplateAttributes = ( - saleReceipt: ISaleReceipt + saleReceipt ): Partial => { return { total: saleReceipt.totalFormatted, @@ -13,6 +12,7 @@ export const transformReceiptToBrandingTemplateAttributes = ( description: entry.description, rate: entry.rateFormatted, quantity: entry.quantityFormatted, + discount: entry.discountFormatted, total: entry.totalFormatted, })), receiptNumber: saleReceipt.receiptNumber, @@ -21,6 +21,9 @@ export const transformReceiptToBrandingTemplateAttributes = ( discountLabel: saleReceipt.discountPercentageFormatted ? `Discount [${saleReceipt.discountPercentageFormatted}]` : 'Discount', + showLineDiscount: saleReceipt.entries.some( + (entry) => entry.discountFormatted + ), adjustment: saleReceipt.adjustmentFormatted, customerAddress: contactAddressTextFormat(saleReceipt.customer), }; diff --git a/packages/webapp/src/components/Datatable/DataTable.tsx b/packages/webapp/src/components/Datatable/DataTable.tsx index 4709f7021..77ce28704 100644 --- a/packages/webapp/src/components/Datatable/DataTable.tsx +++ b/packages/webapp/src/components/Datatable/DataTable.tsx @@ -62,6 +62,9 @@ export function DataTable(props) { initialPageIndex = 0, initialPageSize = 20, + // Hidden columns. + initialHiddenColumns = [], + updateDebounceTime = 200, selectionColumnWidth = 42, @@ -115,6 +118,7 @@ export function DataTable(props) { columnResizing: { columnWidths: initialColumnsWidths || {}, }, + hiddenColumns: initialHiddenColumns, }, manualPagination, pageCount: controlledPageCount, diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTable.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTable.tsx index 3f1e73023..ea5ba5481 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTable.tsx @@ -20,6 +20,10 @@ export default function BillDetailTable() { e.discount_formatted) ? [] : ['discount'] + } styleName={TableStyle.Constrant} /> ); diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx index cc2d2c466..a25924eea 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/utils.tsx @@ -70,6 +70,18 @@ export const useBillReadonlyEntriesTableColumns = () => { disableSortBy: true, textOverview: true, }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + align: 'right', + disableSortBy: true, + textOverview: true, + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + }, { Header: intl.get('amount'), accessor: 'total_formatted', diff --git a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/CreditNoteDetailTable.tsx b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/CreditNoteDetailTable.tsx index 08d200488..ae520cc27 100644 --- a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/CreditNoteDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/CreditNoteDetailTable.tsx @@ -22,6 +22,10 @@ export default function CreditNoteDetailTable() { e.discount_formatted) ? [] : ['discount'] + } className={'table-constrant'} /> ); diff --git a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx index c4a5bcd74..ca8915d2f 100644 --- a/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/CreditNoteDetailDrawer/utils.tsx @@ -16,7 +16,6 @@ import { Icon, FormattedMessage as T, TextOverviewTooltipCell, - FormatNumberCell, Choose, } from '@/components'; import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider'; @@ -68,6 +67,18 @@ export const useCreditNoteReadOnlyEntriesColumns = () => { disableSortBy: true, textOverview: true, }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + align: 'right', + disableSortBy: true, + textOverview: true, + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + }, { Header: intl.get('amount'), accessor: 'total_formatted', diff --git a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailTable.tsx b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailTable.tsx index 26df57b6d..ee9ecada8 100644 --- a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailTable.tsx @@ -23,6 +23,10 @@ export default function EstimateDetailTable() { e.discount_formatted) ? [] : ['discount'] + } styleName={TableStyle.Constrant} /> ); diff --git a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/utils.tsx index eb9def4e6..fbf43d1f6 100644 --- a/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/EstimateDetailDrawer/utils.tsx @@ -55,6 +55,18 @@ export const useEstimateReadonlyEntriesColumns = () => { disableSortBy: true, textOverview: true, }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + align: 'right', + disableSortBy: true, + textOverview: true, + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + }, { Header: intl.get('amount'), accessor: 'total_formatted', diff --git a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailTable.tsx b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailTable.tsx index fc9605148..da566b1e5 100644 --- a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailTable.tsx @@ -1,5 +1,6 @@ // @ts-nocheck import React from 'react'; +import * as R from 'ramda'; import { CommercialDocEntriesTable } from '@/components'; @@ -25,6 +26,10 @@ export default function InvoiceDetailTable() { columns={columns} data={entries} styleName={TableStyle.Constrant} + initialHiddenColumns={ + // If any entry has no discount, hide the discount column. + entries?.some((e) => e.discount_formatted) ? [] : ['discount'] + } /> ); } diff --git a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx index 33396a2c5..e8dc8fed1 100644 --- a/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/InvoiceDetailDrawer/utils.tsx @@ -73,6 +73,18 @@ export const useInvoiceReadonlyEntriesColumns = () => { magicSpacing: 5, }), }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + align: 'right', + disableSortBy: true, + textOverview: true, + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + }, { Header: intl.get('amount'), accessor: 'total_formatted', diff --git a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailTable.tsx b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailTable.tsx index 38d886a45..5740847f0 100644 --- a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailTable.tsx @@ -24,6 +24,10 @@ export default function ReceiptDetailTable() { e.discount_formatted) ? [] : ['discount'] + } styleName={TableStyle.Constrant} /> ); diff --git a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx index 87399cd85..693655f76 100644 --- a/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/ReceiptDetailDrawer/utils.tsx @@ -50,6 +50,18 @@ export const useReceiptReadonlyEntriesTableColumns = () => { disableSortBy: true, textOverview: true, }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + align: 'right', + disableSortBy: true, + textOverview: true, + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + }, { Header: intl.get('amount'), accessor: 'amount', diff --git a/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/VendorCreditDetailTable.tsx b/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/VendorCreditDetailTable.tsx index 74823af9c..58bdcaefa 100644 --- a/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/VendorCreditDetailTable.tsx +++ b/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/VendorCreditDetailTable.tsx @@ -23,6 +23,10 @@ export default function VendorCreditDetailTable() { e.discount_formatted) ? [] : ['discount'] + } styleName={TableStyle.Constrant} /> ); diff --git a/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/utils.tsx b/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/utils.tsx index 63007d86a..9bcbac3e9 100644 --- a/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/utils.tsx +++ b/packages/webapp/src/containers/Drawers/VendorCreditDetailDrawer/utils.tsx @@ -16,7 +16,6 @@ import { Icon, FormattedMessage as T, TextOverviewTooltipCell, - FormatNumberCell, Choose, } from '@/components'; import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider'; @@ -69,6 +68,18 @@ export const useVendorCreditReadonlyEntriesTableColumns = () => { disableSortBy: true, textOverview: true, }, + { + id: 'discount', + Header: 'Discount', + accessor: 'discount_formatted', + width: getColumnWidth(entries, 'discount_formatted', { + minWidth: 60, + magicSpacing: 5, + }), + align: 'right', + disableSortBy: true, + textOverview: true, + }, { Header: intl.get('amount'), accessor: 'total_formatted', From d640dc1f40dc7c4e6a5384eb300fe4884327eb30 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 12 Dec 2024 12:49:52 +0200 Subject: [PATCH 4/4] feat: add totalExcludingTax property and update GL entry calculations --- packages/server/src/interfaces/ItemEntry.ts | 2 ++ packages/server/src/models/ItemEntry.ts | 8 ++++++++ .../src/services/CreditNotes/CreditNoteGLEntries.ts | 4 ++-- .../src/services/Purchases/Bills/BillGLEntries.ts | 5 ++--- .../Purchases/VendorCredits/VendorCreditGLEntries.ts | 4 ++-- .../src/services/Sales/Invoices/InvoiceGLEntries.ts | 2 +- .../services/Sales/Receipts/SaleReceiptGLEntries.ts | 12 ++++++------ .../src/components/ReceiptPaperTemplate.tsx | 1 + 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index e26b8cdba..a2693ab5c 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -20,6 +20,8 @@ export interface IItemEntry { amount: number; total: number; + totalExcludingTax?: number; + subtotalInclusingTax: number; subtotalExcludingTax: number; discountAmount: number; diff --git a/packages/server/src/models/ItemEntry.ts b/packages/server/src/models/ItemEntry.ts index 4df047cd3..3152779ce 100644 --- a/packages/server/src/models/ItemEntry.ts +++ b/packages/server/src/models/ItemEntry.ts @@ -68,6 +68,14 @@ export default class ItemEntry extends TenantModel { return this.subtotal - this.discountAmount; } + /** + * Total (excluding tax). + * @returns {number} + */ + get totalExcludingTax() { + return this.subtotalExcludingTax - this.discountAmount; + } + /** * Item entry amount. * Amount of item entry that may include or exclude tax. diff --git a/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts b/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts index 08734d37a..78bf7884f 100644 --- a/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts +++ b/packages/server/src/services/CreditNotes/CreditNoteGLEntries.ts @@ -210,11 +210,11 @@ export default class CreditNoteGLEntries { index: number ): ILedgerEntry => { const commonEntry = this.getCreditNoteCommonEntry(creditNote); - const localAmount = entry.amount * creditNote.exchangeRate; + const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate; return { ...commonEntry, - debit: localAmount, + debit: totalLocal, accountId: entry.sellAccountId || entry.item.sellAccountId, note: entry.description, index: index + 2, diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts index 7382a568d..a5daf87fe 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts @@ -139,13 +139,12 @@ export class BillGLEntries { private getBillItemEntry = R.curry( (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { const commonJournalMeta = this.getBillCommonEntry(bill); - - const localAmount = bill.exchangeRate * entry.subtotalExcludingTax; + const totalLocal = bill.exchangeRate * entry.totalExcludingTax; const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); return { ...commonJournalMeta, - debit: localAmount + landedCostAmount, + debit: totalLocal + landedCostAmount, accountId: ['inventory'].indexOf(entry.item.type) !== -1 ? entry.item.inventoryAccountId diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditGLEntries.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditGLEntries.ts index 0747a5845..eca22e2e5 100644 --- a/packages/server/src/services/Purchases/VendorCredits/VendorCreditGLEntries.ts +++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditGLEntries.ts @@ -77,11 +77,11 @@ export default class VendorCreditGLEntries { index: number ): ILedgerEntry => { const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit); - const localAmount = entry.amount * vendorCredit.exchangeRate; + const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate; return { ...commonEntity, - credit: localAmount, + credit: totalLocal, index: index + 2, itemId: entry.itemId, itemQuantity: entry.quantity, diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index d141e5571..570da2566 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.total * saleInvoice.exchangeRate; + const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate; return { ...commonEntry, diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts index 8dc7bf803..3cb4d9d0e 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptGLEntries.ts @@ -143,10 +143,10 @@ export class SaleReceiptGLEntries { }; /** - * Retrieve receipt income item GL entry. - * @param {ISaleReceipt} saleReceipt - - * @param {IItemEntry} entry - - * @param {number} index - + * Retrieve receipt income item G/L entry. + * @param {ISaleReceipt} saleReceipt - + * @param {IItemEntry} entry - + * @param {number} index - * @returns {ILedgerEntry} */ private getReceiptIncomeItemEntry = R.curry( @@ -156,11 +156,11 @@ export class SaleReceiptGLEntries { index: number ): ILedgerEntry => { const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); - const itemIncome = entry.amount * saleReceipt.exchangeRate; + const totalLocal = entry.totalExcludingTax * saleReceipt.exchangeRate; return { ...commonEntry, - credit: itemIncome, + credit: totalLocal, accountId: entry.item.sellAccountId, note: entry.description, index: index + 2, diff --git a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx index 76f3a38fa..f263d3952 100644 --- a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx +++ b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx @@ -232,6 +232,7 @@ export function ReceiptPaperTemplate({ ), + thStyle: { width: '60%' }, }, { label: lineQuantityLabel, accessor: 'quantity' }, { label: lineRateLabel, accessor: 'rate', align: 'right' },