mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: item-level discount
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface IItemEntry {
|
||||
|
||||
itemId: number;
|
||||
description: string;
|
||||
discountType?: string;
|
||||
discount: number;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
|
||||
@@ -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}]`
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user