Compare commits

...

7 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
415673656c feat: add api key table 2025-07-01 23:15:55 +02:00
Ahmed Bouhuolia
1906d9f3f5 Merge pull request #766 from bigcapitalhq/discount-line-level
fix: Line-level discount
2024-12-12 15:39:19 +02:00
Ahmed Bouhuolia
d640dc1f40 feat: add totalExcludingTax property and update GL entry calculations 2024-12-12 12:49:52 +02:00
Ahmed Bouhuolia
8cd1b36a02 feat: item-level discount 2024-12-11 15:05:50 +02:00
Ahmed Bouhuolia
5a8d9cc7e8 feat: wip line-level discount 2024-12-11 12:37:15 +02:00
Ahmed Bouhuolia
6323e2ffec fix: line-level discount 2024-12-11 11:44:10 +02:00
Ahmed Bouhuolia
7af2e7ccbc chore: update CHANGELOG 2024-12-09 12:23:46 +02:00
39 changed files with 354 additions and 40 deletions

View File

@@ -2,6 +2,15 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
# [0.22.0]
* feat: estimate, receipt, credit note mail preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/757
* feat: Add discount to transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/758
* fix: update financial forms to use new formatted amount utilities and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/760
* fix: total lines style by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/761
* fix: discount & adjustment sale transactions bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/762
* fix: discount transactions GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/763
# [0.21.2] # [0.21.2]
* hotbug: upload attachments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/755 * hotbug: upload attachments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/755

View File

@@ -127,6 +127,11 @@ export default class BillsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.landed_cost') check('entries.*.landed_cost')
.optional({ nullable: true }) .optional({ nullable: true })

View File

@@ -176,6 +176,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id') check('entries.*.warehouse_id')
.optional({ nullable: true }) .optional({ nullable: true })
@@ -225,6 +229,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id') check('entries.*.warehouse_id')
.optional({ nullable: true }) .optional({ nullable: true })

View File

@@ -239,6 +239,10 @@ export default class PaymentReceivesController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id') check('entries.*.warehouse_id')
.optional({ nullable: true }) .optional({ nullable: true })

View File

@@ -187,6 +187,11 @@ export default class SalesEstimatesController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.warehouse_id') check('entries.*.warehouse_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()

View File

@@ -243,6 +243,10 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toFloat(), .toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.tax_code') check('entries.*.tax_code')
.optional({ nullable: true }) .optional({ nullable: true })

View File

@@ -164,6 +164,11 @@ export default class SalesReceiptsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toInt(), .toInt(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id') check('entries.*.warehouse_id')
.optional({ nullable: true }) .optional({ nullable: true })

View File

@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
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<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('items_entries', (table) => {
table.dropColumn('discount_type');
});
};

View File

@@ -13,14 +13,17 @@ export interface IItemEntry {
itemId: number; itemId: number;
description: string; description: string;
discountType?: string;
discount: number; discount: number;
quantity: number; quantity: number;
rate: number; rate: number;
amount: number; amount: number;
total: number; total: number;
amountInclusingTax: number; totalExcludingTax?: number;
amountExludingTax: number;
subtotalInclusingTax: number;
subtotalExcludingTax: number;
discountAmount: number; discountAmount: number;
landedCost: number; landedCost: number;

View File

@@ -1,6 +1,12 @@
import { Model } from 'objection'; import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate'; 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 { export default class ItemEntry extends TenantModel {
public taxRate: number; public taxRate: number;
@@ -8,7 +14,7 @@ export default class ItemEntry extends TenantModel {
public quantity: number; public quantity: number;
public rate: number; public rate: number;
public isInclusiveTax: number; public isInclusiveTax: number;
public discountType: DiscountType;
/** /**
* Table name. * Table name.
* @returns {string} * @returns {string}
@@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel {
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return [ return [
// Amount (qty * rate)
'amount', 'amount',
'taxAmount', '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', 'total',
]; ];
} }
@@ -45,7 +65,15 @@ export default class ItemEntry extends TenantModel {
* @returns {number} * @returns {number}
*/ */
get total() { get total() {
return this.amountInclusingTax; return this.subtotal - this.discountAmount;
}
/**
* Total (excluding tax).
* @returns {number}
*/
get totalExcludingTax() {
return this.subtotalExcludingTax - this.discountAmount;
} }
/** /**
@@ -57,19 +85,27 @@ export default class ItemEntry extends TenantModel {
return this.quantity * this.rate; return this.quantity * this.rate;
} }
/**
* Subtotal amount (tax inclusive).
* @returns {number}
*/
get subtotal() {
return this.subtotalInclusingTax;
}
/** /**
* Item entry amount including tax. * Item entry amount including tax.
* @returns {number} * @returns {number}
*/ */
get amountInclusingTax() { get subtotalInclusingTax() {
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount; return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
} }
/** /**
* Item entry amount excluding tax. * Subtotal amount (tax exclusive).
* @returns {number} * @returns {number}
*/ */
get amountExludingTax() { get subtotalExcludingTax() {
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount; return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
} }
@@ -78,7 +114,9 @@ export default class ItemEntry extends TenantModel {
* @returns {number} * @returns {number}
*/ */
get discountAmount() { get discountAmount() {
return this.amount * (this.discount / 100); return this.discountType === DiscountType.Percentage
? this.amount * (this.discount / 100)
: this.discount;
} }
/** /**

View File

@@ -210,11 +210,11 @@ export default class CreditNoteGLEntries {
index: number index: number
): ILedgerEntry => { ): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote); const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const localAmount = entry.amount * creditNote.exchangeRate; const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate;
return { return {
...commonEntry, ...commonEntry,
debit: localAmount, debit: totalLocal,
accountId: entry.sellAccountId || entry.item.sellAccountId, accountId: entry.sellAccountId || entry.item.sellAccountId,
note: entry.description, note: entry.description,
index: index + 2, index: index + 2,

View File

@@ -139,13 +139,12 @@ export class BillGLEntries {
private getBillItemEntry = R.curry( private getBillItemEntry = R.curry(
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill); const commonJournalMeta = this.getBillCommonEntry(bill);
const totalLocal = bill.exchangeRate * entry.totalExcludingTax;
const localAmount = bill.exchangeRate * entry.amountExludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return { return {
...commonJournalMeta, ...commonJournalMeta,
debit: localAmount + landedCostAmount, debit: totalLocal + landedCostAmount,
accountId: accountId:
['inventory'].indexOf(entry.item.type) !== -1 ['inventory'].indexOf(entry.item.type) !== -1
? entry.item.inventoryAccountId ? entry.item.inventoryAccountId

View File

@@ -77,11 +77,11 @@ export default class VendorCreditGLEntries {
index: number index: number
): ILedgerEntry => { ): ILedgerEntry => {
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit); const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
const localAmount = entry.amount * vendorCredit.exchangeRate; const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate;
return { return {
...commonEntity, ...commonEntity,
credit: localAmount, credit: totalLocal,
index: index + 2, index: index + 2,
itemId: entry.itemId, itemId: entry.itemId,
itemQuantity: entry.quantity, itemQuantity: entry.quantity,

View File

@@ -13,6 +13,7 @@ export const transformEstimateToPdfTemplate = (
description: entry.description, description: entry.description,
rate: entry.rateFormatted, rate: entry.rateFormatted,
quantity: entry.quantityFormatted, quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted, total: entry.totalFormatted,
})), })),
total: estimate.totalFormatted, total: estimate.totalFormatted,
@@ -21,6 +22,7 @@ export const transformEstimateToPdfTemplate = (
customerNote: estimate.note, customerNote: estimate.note,
termsConditions: estimate.termsConditions, termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer), customerAddress: contactAddressTextFormat(estimate.customer),
showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted),
discount: estimate.discountAmountFormatted, discount: estimate.discountAmountFormatted,
discountLabel: estimate.discountPercentageFormatted discountLabel: estimate.discountPercentageFormatted
? `Discount [${estimate.discountPercentageFormatted}]` ? `Discount [${estimate.discountPercentageFormatted}]`

View File

@@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer {
* @returns {number} * @returns {number}
*/ */
private getDueBalanceItemEntries = (entries: ItemEntry[]) => { private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
return sumBy(entries, (e) => e.amount); return sumBy(entries, (e) => e.total);
}; };
} }

View File

@@ -199,7 +199,7 @@ export class SaleInvoiceGLEntries {
index: number index: number
): ILedgerEntry => { ): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate; const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate;
return { return {
...commonEntry, ...commonEntry,

View File

@@ -1,4 +1,4 @@
import { IItemEntry } from '@/interfaces'; import { DiscountType, IItemEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils'; import { formatNumber } from '@/utils';
@@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['quantityFormatted', 'rateFormatted', 'totalFormatted']; return [
'quantityFormatted',
'rateFormatted',
'totalFormatted',
'discountFormatted',
'discountAmountFormatted',
];
}; };
/** /**
@@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer {
money: false, 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, description: entry.description,
rate: entry.rateFormatted, rate: entry.rateFormatted,
quantity: entry.quantityFormatted, quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted, total: entry.totalFormatted,
})), })),
taxes: invoice.taxes.map((tax) => ({ taxes: invoice.taxes.map((tax) => ({
label: tax.name, label: tax.name,
amount: tax.taxRateAmountFormatted, amount: tax.taxRateAmountFormatted,
})), })),
showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted),
customerAddress: contactAddressTextFormat(invoice.customer), customerAddress: contactAddressTextFormat(invoice.customer),
}; };
}; };

View File

@@ -143,10 +143,10 @@ export class SaleReceiptGLEntries {
}; };
/** /**
* Retrieve receipt income item GL entry. * Retrieve receipt income item G/L entry.
* @param {ISaleReceipt} saleReceipt - * @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry - * @param {IItemEntry} entry -
* @param {number} index - * @param {number} index -
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
*/ */
private getReceiptIncomeItemEntry = R.curry( private getReceiptIncomeItemEntry = R.curry(
@@ -156,11 +156,11 @@ export class SaleReceiptGLEntries {
index: number index: number
): ILedgerEntry => { ): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const itemIncome = entry.amount * saleReceipt.exchangeRate; const totalLocal = entry.totalExcludingTax * saleReceipt.exchangeRate;
return { return {
...commonEntry, ...commonEntry,
credit: itemIncome, credit: totalLocal,
accountId: entry.item.sellAccountId, accountId: entry.item.sellAccountId,
note: entry.description, note: entry.description,
index: index + 2, index: index + 2,

View File

@@ -1,9 +1,8 @@
import { ISaleReceipt } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format'; import { contactAddressTextFormat } from '@/utils/address-text-format';
import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates'; import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates';
export const transformReceiptToBrandingTemplateAttributes = ( export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt saleReceipt
): Partial<ReceiptPaperTemplateProps> => { ): Partial<ReceiptPaperTemplateProps> => {
return { return {
total: saleReceipt.totalFormatted, total: saleReceipt.totalFormatted,
@@ -13,6 +12,7 @@ export const transformReceiptToBrandingTemplateAttributes = (
description: entry.description, description: entry.description,
rate: entry.rateFormatted, rate: entry.rateFormatted,
quantity: entry.quantityFormatted, quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted, total: entry.totalFormatted,
})), })),
receiptNumber: saleReceipt.receiptNumber, receiptNumber: saleReceipt.receiptNumber,
@@ -21,6 +21,9 @@ export const transformReceiptToBrandingTemplateAttributes = (
discountLabel: saleReceipt.discountPercentageFormatted discountLabel: saleReceipt.discountPercentageFormatted
? `Discount [${saleReceipt.discountPercentageFormatted}]` ? `Discount [${saleReceipt.discountPercentageFormatted}]`
: 'Discount', : 'Discount',
showLineDiscount: saleReceipt.entries.some(
(entry) => entry.discountFormatted
),
adjustment: saleReceipt.adjustmentFormatted, adjustment: saleReceipt.adjustmentFormatted,
customerAddress: contactAddressTextFormat(saleReceipt.customer), customerAddress: contactAddressTextFormat(saleReceipt.customer),
}; };

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { keyBy, sumBy } from 'lodash'; import { keyBy, sumBy } from 'lodash';
import { ItemEntry } from '@/models'; import { ItemEntry } from '@/models';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces'; import { IItemEntry } from '@/interfaces';
@Service() @Service()
export class ItemEntriesTaxTransactions { export class ItemEntriesTaxTransactions {

View File

@@ -0,0 +1,27 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('api_keys', (table) => {
table.increments('id').primary();
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.integer('user_id').unsigned().index().references('id').inTable('users');
table.string('name');
table.text('key');
table.dateTime('expires_at').nullable();
table.dateTime('revoked_at').nullable();
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTableIfExists('api_keys');
};

View File

@@ -62,6 +62,9 @@ export function DataTable(props) {
initialPageIndex = 0, initialPageIndex = 0,
initialPageSize = 20, initialPageSize = 20,
// Hidden columns.
initialHiddenColumns = [],
updateDebounceTime = 200, updateDebounceTime = 200,
selectionColumnWidth = 42, selectionColumnWidth = 42,
@@ -115,6 +118,7 @@ export function DataTable(props) {
columnResizing: { columnResizing: {
columnWidths: initialColumnsWidths || {}, columnWidths: initialColumnsWidths || {},
}, },
hiddenColumns: initialHiddenColumns,
}, },
manualPagination, manualPagination,
pageCount: controlledPageCount, pageCount: controlledPageCount,

View File

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

View File

@@ -70,6 +70,18 @@ export const useBillReadonlyEntriesTableColumns = () => {
disableSortBy: true, disableSortBy: true,
textOverview: 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'), Header: intl.get('amount'),
accessor: 'total_formatted', accessor: 'total_formatted',

View File

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

View File

@@ -16,7 +16,6 @@ import {
Icon, Icon,
FormattedMessage as T, FormattedMessage as T,
TextOverviewTooltipCell, TextOverviewTooltipCell,
FormatNumberCell,
Choose, Choose,
} from '@/components'; } from '@/components';
import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider'; import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider';
@@ -68,6 +67,18 @@ export const useCreditNoteReadOnlyEntriesColumns = () => {
disableSortBy: true, disableSortBy: true,
textOverview: 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'), Header: intl.get('amount'),
accessor: 'total_formatted', accessor: 'total_formatted',

View File

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

View File

@@ -55,6 +55,18 @@ export const useEstimateReadonlyEntriesColumns = () => {
disableSortBy: true, disableSortBy: true,
textOverview: 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'), Header: intl.get('amount'),
accessor: 'total_formatted', accessor: 'total_formatted',

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import * as R from 'ramda';
import { CommercialDocEntriesTable } from '@/components'; import { CommercialDocEntriesTable } from '@/components';
@@ -25,6 +26,10 @@ export default function InvoiceDetailTable() {
columns={columns} columns={columns}
data={entries} data={entries}
styleName={TableStyle.Constrant} 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, 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'), Header: intl.get('amount'),
accessor: 'total_formatted', accessor: 'total_formatted',

View File

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

View File

@@ -50,6 +50,18 @@ export const useReceiptReadonlyEntriesTableColumns = () => {
disableSortBy: true, disableSortBy: true,
textOverview: 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'), Header: intl.get('amount'),
accessor: 'amount', accessor: 'amount',

View File

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

View File

@@ -16,7 +16,6 @@ import {
Icon, Icon,
FormattedMessage as T, FormattedMessage as T,
TextOverviewTooltipCell, TextOverviewTooltipCell,
FormatNumberCell,
Choose, Choose,
} from '@/components'; } from '@/components';
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider'; import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
@@ -69,6 +68,18 @@ export const useVendorCreditReadonlyEntriesTableColumns = () => {
disableSortBy: true, disableSortBy: true,
textOverview: 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'), Header: intl.get('amount'),
accessor: 'total_formatted', accessor: 'total_formatted',

View File

@@ -92,6 +92,10 @@ export interface EstimatePaperTemplateProps extends PaperTemplateProps {
lineQuantityLabel?: string; lineQuantityLabel?: string;
lineRateLabel?: string; lineRateLabel?: string;
lineTotalLabel?: string; lineTotalLabel?: string;
// # Line Discount
lineDiscountLabel?: string;
showLineDiscount?: boolean;
} }
export function EstimatePaperTemplate({ export function EstimatePaperTemplate({
@@ -173,8 +177,11 @@ export function EstimatePaperTemplate({
lineQuantityLabel = 'Qty', lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate', lineRateLabel = 'Rate',
lineTotalLabel = 'Total', lineTotalLabel = 'Total',
}: EstimatePaperTemplateProps) {
// # Line Discount
lineDiscountLabel = 'Discount',
showLineDiscount = false,
}: EstimatePaperTemplateProps) {
return ( return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}> <PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
<Stack spacing={24}> <Stack spacing={24}>
@@ -240,6 +247,12 @@ export function EstimatePaperTemplate({
}, },
{ label: lineQuantityLabel, accessor: 'quantity' }, { label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' }, { label: lineRateLabel, accessor: 'rate', align: 'right' },
{
label: lineDiscountLabel,
accessor: 'discount',
align: 'right',
visible: showLineDiscount,
},
{ label: lineTotalLabel, accessor: 'total', align: 'right' }, { label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}

View File

@@ -17,15 +17,16 @@ import {
DefaultPdfTemplateAddressBilledFrom, DefaultPdfTemplateAddressBilledFrom,
} from './_constants'; } from './_constants';
interface PapaerLine { interface InvoiceLine {
item?: string; item?: string;
description?: string; description?: string;
quantity?: string; quantity?: string;
rate?: string; rate?: string;
total?: string; total?: string;
discount?: string;
} }
interface PaperTax { interface InvoiceTaxLine {
label: string; label: string;
amount: string; amount: string;
} }
@@ -71,6 +72,10 @@ export interface InvoicePaperTemplateProps extends PaperTemplateProps {
lineRateLabel?: string; lineRateLabel?: string;
lineTotalLabel?: string; lineTotalLabel?: string;
// # Line Discount
lineDiscountLabel?: string;
showLineDiscount?: boolean;
// Total // Total
showTotal?: boolean; showTotal?: boolean;
totalLabel?: string; totalLabel?: string;
@@ -113,8 +118,8 @@ export interface InvoicePaperTemplateProps extends PaperTemplateProps {
showStatement?: boolean; showStatement?: boolean;
statement?: string; statement?: string;
lines?: Array<PapaerLine>; lines?: Array<InvoiceLine>;
taxes?: Array<PaperTax>; taxes?: Array<InvoiceTaxLine>;
} }
export function InvoicePaperTemplate({ export function InvoicePaperTemplate({
@@ -165,6 +170,10 @@ export function InvoicePaperTemplate({
paymentMadeLabel = 'Payment Made', paymentMadeLabel = 'Payment Made',
dueAmountLabel = 'Balance Due', dueAmountLabel = 'Balance Due',
// # Line Discount
lineDiscountLabel = 'Discount',
showLineDiscount = false,
// Totals // Totals
showTotal = true, showTotal = true,
total = '$662.75', total = '$662.75',
@@ -277,6 +286,12 @@ export function InvoicePaperTemplate({
align: 'right', align: 'right',
}, },
{ label: lineRateLabel, accessor: 'rate', align: 'right' }, { label: lineRateLabel, accessor: 'rate', align: 'right' },
{
label: lineDiscountLabel,
accessor: 'discount',
align: 'right',
visible: showLineDiscount,
},
{ label: lineTotalLabel, accessor: 'total', align: 'right' }, { label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}

View File

@@ -90,11 +90,14 @@ interface PaperTemplateTableProps {
value?: JSX.Element; value?: JSX.Element;
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
thStyle?: React.CSSProperties; thStyle?: React.CSSProperties;
visible?: boolean;
}>; }>;
data: Array<Record<string, any>>; data: Array<Record<string, any>>;
} }
PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => { PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
const filteredColumns = columns.filter((col) => col.visible !== false);
return ( return (
<table <table
className={css` className={css`
@@ -140,7 +143,7 @@ PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
> >
<thead> <thead>
<tr> <tr>
{columns.map((col, index) => ( {filteredColumns.map((col, index) => (
<x.th key={index} textAlign={col.align} style={col.thStyle}> <x.th key={index} textAlign={col.align} style={col.thStyle}>
{col.label} {col.label}
</x.th> </x.th>
@@ -151,7 +154,7 @@ PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
<tbody> <tbody>
{data.map((_data: any) => ( {data.map((_data: any) => (
<tr> <tr>
{columns.map((column, index) => ( {filteredColumns.map((column, index) => (
<x.td textAlign={column.align} key={index}> <x.td textAlign={column.align} key={index}>
{isFunction(column?.accessor) {isFunction(column?.accessor)
? column?.accessor(_data) ? column?.accessor(_data)

View File

@@ -71,9 +71,14 @@ export interface ReceiptPaperTemplateProps extends PaperTemplateProps {
description: string; description: string;
rate: string; rate: string;
quantity: string; quantity: string;
discount?: string;
total: string; total: string;
}>; }>;
// # Line Discount
lineDiscountLabel?: string;
showLineDiscount?: boolean;
// Receipt Date. // Receipt Date.
receiptDateLabel?: string; receiptDateLabel?: string;
showReceiptDate?: boolean; showReceiptDate?: boolean;
@@ -165,6 +170,10 @@ export function ReceiptPaperTemplate({
lineQuantityLabel = 'Qty', lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate', lineRateLabel = 'Rate',
lineTotalLabel = 'Total', lineTotalLabel = 'Total',
// # Line Discount
lineDiscountLabel = 'Discount',
showLineDiscount = false,
}: ReceiptPaperTemplateProps) { }: ReceiptPaperTemplateProps) {
return ( return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}> <PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
@@ -223,9 +232,16 @@ export function ReceiptPaperTemplate({
</Text> </Text>
</Stack> </Stack>
), ),
thStyle: { width: '60%' },
}, },
{ label: lineQuantityLabel, accessor: 'quantity' }, { label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' }, { label: lineRateLabel, accessor: 'rate', align: 'right' },
{
label: lineDiscountLabel,
accessor: 'discount',
align: 'right',
visible: showLineDiscount,
},
{ label: lineTotalLabel, accessor: 'total', align: 'right' }, { label: lineTotalLabel, accessor: 'total', align: 'right' },
]} ]}
data={lines} data={lines}