From 8cd1b36a028eab5531793c9ea713c06d4990a7ea Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Dec 2024 15:05:50 +0200 Subject: [PATCH] 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',