mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-13 11:20:31 +00:00
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:
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -29,6 +29,12 @@ export interface ISaleReceipt {
|
||||
localAmount?: number;
|
||||
entries?: IItemEntry[];
|
||||
|
||||
subtotal?: number;
|
||||
subtotalLocal?: number;
|
||||
|
||||
total?: number;
|
||||
totalLocal?: number;
|
||||
|
||||
discountAmount: number;
|
||||
discountPercentage?: number | null;
|
||||
|
||||
|
||||
@@ -43,8 +43,18 @@ export default class CreditNote extends mixin(TenantModel, [
|
||||
'isPublished',
|
||||
'isOpen',
|
||||
'isClosed',
|
||||
|
||||
'creditsRemaining',
|
||||
'creditsUsed',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
|
||||
'discountAmount',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'discountAmount',
|
||||
'discountPercentage',
|
||||
'isDelivered',
|
||||
'isExpired',
|
||||
'isConvertedToInvoice',
|
||||
|
||||
@@ -72,6 +72,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
|
||||
'taxAmountWithheldLocal',
|
||||
'discountAmount',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user