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.
# [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]
* 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 })
.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

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

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

@@ -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;
description: string;
discountType?: string;
discount: number;
quantity: number;
rate: number;
amount: number;
total: number;
amountInclusingTax: number;
amountExludingTax: number;
totalExcludingTax?: number;
subtotalInclusingTax: number;
subtotalExcludingTax: number;
discountAmount: number;
landedCost: number;

View File

@@ -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,15 @@ export default class ItemEntry extends TenantModel {
* @returns {number}
*/
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;
}
/**
* 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 +114,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;
}
/**

View File

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

View File

@@ -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.amountExludingTax;
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

View File

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

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

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

View File

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

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

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

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

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

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

View File

@@ -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 (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
<Stack spacing={24}>
@@ -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}

View File

@@ -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<PapaerLine>;
taxes?: Array<PaperTax>;
lines?: Array<InvoiceLine>;
taxes?: Array<InvoiceTaxLine>;
}
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}

View File

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

View File

@@ -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 (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
@@ -223,9 +232,16 @@ export function ReceiptPaperTemplate({
</Text>
</Stack>
),
thStyle: { width: '60%' },
},
{ 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}