feat: item-level discount

This commit is contained in:
Ahmed Bouhuolia
2024-12-11 15:05:50 +02:00
parent 5a8d9cc7e8
commit 8cd1b36a02
23 changed files with 176 additions and 7 deletions

View File

@@ -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 })

View File

@@ -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 })

View File

@@ -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 })

View File

@@ -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()

View File

@@ -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 })

View File

@@ -13,6 +13,7 @@ export interface IItemEntry {
itemId: number;
description: string;
discountType?: string;
discount: number;
quantity: number;
rate: number;

View File

@@ -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}]`

View File

@@ -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,
});
};
}

View File

@@ -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),
};
};

View File

@@ -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<ReceiptPaperTemplateProps> => {
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),
};

View File

@@ -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,

View File

@@ -20,6 +20,10 @@ export default function BillDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
styleName={TableStyle.Constrant}
/>
);

View File

@@ -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',

View File

@@ -22,6 +22,10 @@ export default function CreditNoteDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
className={'table-constrant'}
/>
);

View File

@@ -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',

View File

@@ -23,6 +23,10 @@ export default function EstimateDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
styleName={TableStyle.Constrant}
/>
);

View File

@@ -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',

View File

@@ -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']
}
/>
);
}

View File

@@ -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',

View File

@@ -24,6 +24,10 @@ export default function ReceiptDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
styleName={TableStyle.Constrant}
/>
);

View File

@@ -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',

View File

@@ -23,6 +23,10 @@ export default function VendorCreditDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
styleName={TableStyle.Constrant}
/>
);

View File

@@ -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',