feat: add discount functionality to sales and purchase transactions

- Introduced discount_type and discount fields in Bills and SalesReceipts controllers.
- Updated database migrations to include discount and discount_type in estimates and credit notes tables.
- Enhanced SaleReceipt and SaleEstimate models to support discount attributes.
- Implemented formatting for discount amounts in transformers and PDF templates.
- Updated email templates to display discount information.

This commit enhances the handling of discounts across various transaction types, improving the overall functionality and user experience.
This commit is contained in:
Ahmed Bouhuolia
2024-11-30 14:46:43 +02:00
parent 17b3bbe1d8
commit dd1392cdc8
21 changed files with 196 additions and 35 deletions

View File

@@ -4,6 +4,7 @@ import { check, param, query } from 'express-validator';
import {
AbilitySubject,
BillAction,
DiscountType,
IBillDTO,
IBillEditDTO,
} from '@/interfaces';
@@ -144,8 +145,15 @@ export default class BillsController extends BaseController {
.isNumeric()
.toInt(),
// Attachments
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// # Discount
check('discount_type')
.default(DiscountType.Amount)
.isIn([DiscountType.Amount, DiscountType.Percentage]),
check('discount').optional().isDecimal().toFloat(),
];
}

View File

@@ -11,7 +11,7 @@ import {
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { AbilitySubject, DiscountType, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@@ -178,6 +178,12 @@ export default class SalesReceiptsController extends BaseController {
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// # Discount
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isIn([DiscountType.Percentage, DiscountType.Amount]),
];
}

View File

@@ -4,7 +4,7 @@
*/
exports.up = function(knex) {
return knex.schema.alterTable('sales_estimates', (table) => {
table.decimal('discount', 10, 2).nullable().after('credited_amount');
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');

View File

@@ -4,7 +4,7 @@
*/
exports.up = function(knex) {
return knex.schema.alterTable('credit_notes', (table) => {
table.decimal('discount', 10, 2).nullable().after('credited_amount');
table.decimal('discount', 10, 2).nullable().after('exchange_rate');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});

View File

@@ -29,6 +29,12 @@ export interface ISaleReceipt {
localAmount?: number;
entries?: IItemEntry[];
subtotal?: number;
subtotalLocal?: number;
total?: number;
totalLocal?: number;
discountAmount: number;
discountPercentage?: number | null;

View File

@@ -43,8 +43,18 @@ export default class CreditNote extends mixin(TenantModel, [
'isPublished',
'isOpen',
'isClosed',
'creditsRemaining',
'creditsUsed',
'subtotal',
'subtotalLocal',
'discountAmount',
'discountPercentage',
'total',
'totalLocal',
];
}

View File

@@ -11,6 +11,10 @@ export default class PaymentReceive extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
amount!: number;
paymentAmount!: number;
exchangeRate!: number;
/**
* Table name.
*/
@@ -40,6 +44,10 @@ export default class PaymentReceive extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Payment receive total.
* @returns {number}
*/
get total() {
return this.paymentAmount;
}

View File

@@ -43,6 +43,8 @@ export default class SaleEstimate extends mixin(TenantModel, [
static get virtualAttributes() {
return [
'localAmount',
'discountAmount',
'discountPercentage',
'isDelivered',
'isExpired',
'isConvertedToInvoice',

View File

@@ -72,6 +72,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
'taxAmountWithheldLocal',
'discountAmount',
'discountPercentage',
'total',
'totalLocal',

View File

@@ -40,7 +40,21 @@ export default class SaleReceipt extends mixin(TenantModel, [
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount', 'isClosed', 'isDraft'];
return [
'localAmount',
'subtotal',
'subtotalLocal',
'total',
'totalLocal',
'discountAmount',
'discountPercentage',
'isClosed',
'isDraft',
];
}
/**

View File

@@ -21,6 +21,8 @@ export class CreditNoteTransformer extends Transformer {
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'totalFormatted',
'totalLocalFormatted',
'entries',
'attachments',
];
@@ -86,7 +88,6 @@ export class CreditNoteTransformer extends Transformer {
return formatNumber(credit.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param credit
@@ -120,6 +121,28 @@ export class CreditNoteTransformer extends Transformer {
});
};
/**
* Retrieves the formatted total.
* @param credit
* @returns {string}
*/
protected totalFormatted = (credit): string => {
return formatNumber(credit.total, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the formatted total in local currency.
* @param credit
* @returns {string}
*/
protected totalLocalFormatted = (credit): string => {
return formatNumber(credit.totalLocal, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ICreditNote} credit

View File

@@ -254,18 +254,27 @@ export interface EstimatePdfBrandingAttributes {
companyAddress: string;
billedToLabel: string;
// # Total
total: string;
totalLabel: string;
showTotal: boolean;
// # Discount
discount: string;
showDiscount: boolean;
discountLabel: string;
// # Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// # Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// # Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;

View File

@@ -20,6 +20,10 @@ export const transformEstimateToPdfTemplate = (
customerNote: estimate.note,
termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer),
discount: estimate.discountAmountFormatted,
discountLabel: estimate.discountPercentageFormatted
? `Discount [${estimate.discountPercentageFormatted}]`
: 'Discount',
};
};

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformInvoiceToPdfTemplate } from './utils';
@@ -9,7 +8,6 @@ import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
@Service()
export class SaleInvoicePdf {

View File

@@ -28,6 +28,10 @@ export const transformInvoiceToPdfTemplate = (
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
dueAmount: invoice.dueAmountFormatted,
discount: invoice.discountAmountFormatted,
discountLabel: invoice.discountPercentageFormatted
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount',
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,

View File

@@ -13,9 +13,12 @@ export class SaleReceiptTransformer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
'formattedSubtotal',
'discountAmountFormatted',
'discountPercentageFormatted',
'subtotalFormatted',
'subtotalLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'adjustmentFormatted',
'formattedAmount',
'formattedReceiptDate',
@@ -58,8 +61,37 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected formattedSubtotal = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, { money: false });
protected subtotalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotal, { money: false });
};
/**
* Retrieves the estimate formatted subtotal in local currency.
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected subtotalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotalLocal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the receipt formatted total.
* @param receipt
* @returns {string}
*/
protected totalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.total, { money: false });
};
/**
* Retrieves the receipt formatted total in local currency.
* @param receipt
* @returns {string}
*/
protected totalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.totalLocal, { money: false });
};
/**
@@ -67,7 +99,7 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: ISaleReceipt): string => {
protected amountFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});

View File

@@ -1,5 +1,4 @@
import { Inject, Service } from 'typedi';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { GetSaleReceipt } from './GetSaleReceipt';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -18,9 +17,6 @@ export class SaleReceiptsPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleReceiptService: GetSaleReceipt;

View File

@@ -8,8 +8,8 @@ export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
total: saleReceipt.totalFormatted,
subtotal: saleReceipt.subtotalFormatted,
lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
@@ -19,6 +19,10 @@ export const transformReceiptToBrandingTemplateAttributes = (
})),
receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate,
discount: saleReceipt.discountAmountFormatted,
discountLabel: saleReceipt.discountPercentageFormatted
? `Discount [${saleReceipt.discountPercentageFormatted}]`
: 'Discount',
customerAddress: contactAddressTextFormat(saleReceipt.customer),
};
};

View File

@@ -21,10 +21,6 @@ export interface ReceiptEmailTemplateProps {
// # Colors
primaryColor?: string;
// # Invoice total
total: string;
totalLabel?: string;
// # Receipt #
receiptNumber?: string;
receiptNumberLabel?: string;
@@ -32,6 +28,14 @@ export interface ReceiptEmailTemplateProps {
// # Items
items: Array<{ label: string; quantity: string; rate: string }>;
// # Invoice total
total: string;
totalLabel?: string;
// # Discount
discount?: string;
discountLabel?: string;
// # Subtotal
subtotal?: string;
subtotalLabel?: string;
@@ -117,7 +121,7 @@ export const ReceiptEmailTemplate: React.FC<
<Text style={dueAmountLineItemAmountStyle}>{subtotal}</Text>
</Column>
</Row>
<Row style={totalLineRowStyle}>
<Column width={'50%'}>
<Text style={totalLineItemLabelStyle}>{totalLabel}</Text>

View File

@@ -48,6 +48,12 @@ export interface EstimatePaperTemplateProps extends PaperTemplateProps {
showTotal?: boolean;
totalLabel?: string;
// # Discount
discount?: string;
showDiscount?: boolean;
discountLabel?: string;
// # Subtotal
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
@@ -101,6 +107,11 @@ export function EstimatePaperTemplate({
totalLabel = 'Total',
showTotal = true,
// # Discount
discount = '0.00',
discountLabel = 'Discount',
showDiscount = true,
// # Subtotal
subtotal = '1000/00',
subtotalLabel = 'Subtotal',
@@ -202,8 +213,8 @@ export function EstimatePaperTemplate({
<Text>{data.item}</Text>
<Text
fontSize={'12px'}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
>
{data.description}
</Text>
@@ -223,6 +234,12 @@ export function EstimatePaperTemplate({
amount={subtotal}
/>
)}
{showDiscount && discount && (
<PaperTemplate.TotalLine
label={discountLabel}
amount={discount}
/>
)}
{showTotal && (
<PaperTemplate.TotalLine label={totalLabel} amount={total} />
)}

View File

@@ -2,10 +2,7 @@ import { Box } from '../lib/layout/Box';
import { Text } from '../lib/text/Text';
import { Stack } from '../lib/layout/Stack';
import { Group } from '../lib/layout/Group';
import {
PaperTemplate,
PaperTemplateProps,
} from './PaperTemplate';
import { PaperTemplate, PaperTemplateProps } from './PaperTemplate';
import {
DefaultPdfTemplateTerms,
DefaultPdfTemplateItemDescription,
@@ -32,16 +29,21 @@ export interface ReceiptPaperTemplateProps extends PaperTemplateProps {
billedToLabel?: string;
// # Subtotal
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
// # Discount
discount?: string;
showDiscount?: boolean;
discountLabel?: string;
// Total
total?: string;
showTotal?: boolean;
totalLabel?: string;
// Subtotal
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
// Customer Note
showCustomerNote?: boolean;
customerNote?: string;
@@ -99,10 +101,17 @@ export function ReceiptPaperTemplate({
billedToLabel = 'Billed To',
// # Total
total = '$1000.00',
totalLabel = 'Total',
showTotal = true,
// # Discount
discount = '',
discountLabel = 'Discount',
showDiscount = true,
// # Subtotal
subtotal = '1000/00',
subtotalLabel = 'Subtotal',
showSubtotal = true,
@@ -192,8 +201,8 @@ export function ReceiptPaperTemplate({
<Text>{data.item}</Text>
<Text
fontSize={'12px'}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
>
{data.description}
</Text>
@@ -213,6 +222,12 @@ export function ReceiptPaperTemplate({
amount={subtotal}
/>
)}
{showDiscount && discount && (
<PaperTemplate.TotalLine
label={discountLabel}
amount={discount}
/>
)}
{showTotal && (
<PaperTemplate.TotalLine label={totalLabel} amount={total} />
)}