diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 94bfb814d..5752fe39c 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -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(), ]; } diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index 4ba94fffe..368dd2c8e 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -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]), ]; } diff --git a/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js b/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js index e8ab28a7a..748483f26 100644 --- a/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js +++ b/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js @@ -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'); diff --git a/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js b/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js index f59624313..fcca5bfa4 100644 --- a/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js +++ b/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js @@ -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'); }); diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts index c8942f570..ec0d2a726 100644 --- a/packages/server/src/interfaces/SaleReceipt.ts +++ b/packages/server/src/interfaces/SaleReceipt.ts @@ -29,6 +29,12 @@ export interface ISaleReceipt { localAmount?: number; entries?: IItemEntry[]; + subtotal?: number; + subtotalLocal?: number; + + total?: number; + totalLocal?: number; + discountAmount: number; discountPercentage?: number | null; diff --git a/packages/server/src/models/CreditNote.ts b/packages/server/src/models/CreditNote.ts index b7a874ab7..b2cee8572 100644 --- a/packages/server/src/models/CreditNote.ts +++ b/packages/server/src/models/CreditNote.ts @@ -43,8 +43,18 @@ export default class CreditNote extends mixin(TenantModel, [ 'isPublished', 'isOpen', 'isClosed', + 'creditsRemaining', 'creditsUsed', + + 'subtotal', + 'subtotalLocal', + + 'discountAmount', + 'discountPercentage', + + 'total', + 'totalLocal', ]; } diff --git a/packages/server/src/models/PaymentReceive.ts b/packages/server/src/models/PaymentReceive.ts index 47044f757..cd7df23a6 100644 --- a/packages/server/src/models/PaymentReceive.ts +++ b/packages/server/src/models/PaymentReceive.ts @@ -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; } diff --git a/packages/server/src/models/SaleEstimate.ts b/packages/server/src/models/SaleEstimate.ts index 9d6b4698b..3fa8a7905 100644 --- a/packages/server/src/models/SaleEstimate.ts +++ b/packages/server/src/models/SaleEstimate.ts @@ -43,6 +43,8 @@ export default class SaleEstimate extends mixin(TenantModel, [ static get virtualAttributes() { return [ 'localAmount', + 'discountAmount', + 'discountPercentage', 'isDelivered', 'isExpired', 'isConvertedToInvoice', diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 6e9c48cdc..7d56b59e2 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -72,6 +72,7 @@ export default class SaleInvoice extends mixin(TenantModel, [ 'taxAmountWithheldLocal', 'discountAmount', + 'discountPercentage', 'total', 'totalLocal', diff --git a/packages/server/src/models/SaleReceipt.ts b/packages/server/src/models/SaleReceipt.ts index cb109e244..8bef046ed 100644 --- a/packages/server/src/models/SaleReceipt.ts +++ b/packages/server/src/models/SaleReceipt.ts @@ -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', + ]; } /** diff --git a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts index 709ab957b..c136c463e 100644 --- a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts +++ b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts @@ -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 diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts index fd8738a4d..7580c260c 100644 --- a/packages/server/src/services/Sales/Estimates/constants.ts +++ b/packages/server/src/services/Sales/Estimates/constants.ts @@ -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; diff --git a/packages/server/src/services/Sales/Estimates/utils.ts b/packages/server/src/services/Sales/Estimates/utils.ts index efd301af3..c77029ba1 100644 --- a/packages/server/src/services/Sales/Estimates/utils.ts +++ b/packages/server/src/services/Sales/Estimates/utils.ts @@ -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', }; }; diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts index a664ead35..54ae28319 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts @@ -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 { diff --git a/packages/server/src/services/Sales/Invoices/utils.ts b/packages/server/src/services/Sales/Invoices/utils.ts index 0db99bf55..2fcfce6f2 100644 --- a/packages/server/src/services/Sales/Invoices/utils.ts +++ b/packages/server/src/services/Sales/Invoices/utils.ts @@ -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, diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts index 37b84d5ee..4a92bc873 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptTransformer.ts @@ -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, }); diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index 3c4e42060..f8b6f57ea 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -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; diff --git a/packages/server/src/services/Sales/Receipts/utils.ts b/packages/server/src/services/Sales/Receipts/utils.ts index b075aa637..2586c0b33 100644 --- a/packages/server/src/services/Sales/Receipts/utils.ts +++ b/packages/server/src/services/Sales/Receipts/utils.ts @@ -8,8 +8,8 @@ export const transformReceiptToBrandingTemplateAttributes = ( saleReceipt: ISaleReceipt ): Partial => { 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), }; }; diff --git a/shared/email-components/src/lib/ReceiptPaymentEmail.tsx b/shared/email-components/src/lib/ReceiptPaymentEmail.tsx index 129d52cbe..bb92d1d0b 100644 --- a/shared/email-components/src/lib/ReceiptPaymentEmail.tsx +++ b/shared/email-components/src/lib/ReceiptPaymentEmail.tsx @@ -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< {subtotal} - + {totalLabel} diff --git a/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx b/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx index 65c49561f..837ab30aa 100644 --- a/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx +++ b/shared/pdf-templates/src/components/EstimatePaperTemplate.tsx @@ -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({ {data.item} {data.description} @@ -223,6 +234,12 @@ export function EstimatePaperTemplate({ amount={subtotal} /> )} + {showDiscount && discount && ( + + )} {showTotal && ( )} diff --git a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx index a84b482a5..3144c15de 100644 --- a/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx +++ b/shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx @@ -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({ {data.item} {data.description} @@ -213,6 +222,12 @@ export function ReceiptPaperTemplate({ amount={subtotal} /> )} + {showDiscount && discount && ( + + )} {showTotal && ( )}