Merge branch 'develop' into stripe-integrate

This commit is contained in:
Ahmed Bouhuolia
2024-09-17 19:26:13 +02:00
185 changed files with 9316 additions and 517 deletions

View File

@@ -1,6 +1,7 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import composeAsync from 'async/compose';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
import { SaleEstimateValidators } from './SaleEstimateValidators';
@@ -10,6 +11,7 @@ import { formatDateFields } from '@/utils';
import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export class SaleEstimateDTOTransformer {
@@ -28,6 +30,9 @@ export class SaleEstimateDTOTransformer {
@Inject()
private estimateIncrement: SaleEstimateIncrement;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transform create DTO object ot model object.
* @param {number} tenantId
@@ -81,10 +86,18 @@ export class SaleEstimateDTOTransformer {
deliveredAt: moment().toMySqlDateTime(),
}),
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'SaleEstimate'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
}
/**

View File

@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleEstimate } from './GetSaleEstimate';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants';
@Service()
export class SaleEstimatesPdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,25 +21,59 @@ export class SaleEstimatesPdf {
@Inject()
private getSaleEstimate: GetSaleEstimate;
@Inject()
private estimatePdfTemplate: SaleEstimatePdfTemplate;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
* @param {ISaleInvoice} saleInvoice -
*/
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
const saleEstimate = await this.getSaleEstimate.getEstimate(
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
saleEstimateId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/estimate-regular',
{
saleEstimate,
}
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves the given estimate branding attributes.
* @param {number} tenantId - Tenant id.
* @param {number} estimateId - Estimate id.
* @returns {Promise<EstimatePdfBrandingAttributes>}
*/
async getEstimateBrandingAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePdfBrandingAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleEstimate = await this.getSaleEstimate.getEstimate(
tenantId,
estimateId
);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
saleEstimate.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'SaleEstimate',
default: true,
})
)?.id;
const brandingTemplate =
await this.estimatePdfTemplate.getEstimatePdfTemplate(
tenantId,
templateId
);
return {
...brandingTemplate.attributes,
...transformEstimateToPdfTemplate(saleEstimate),
};
}
}

View File

@@ -173,3 +173,122 @@ export const SaleEstimatesSampleData = [
'Line Description': 'Qui suscipit ducimus qui qui.',
},
];
export const defaultEstimatePdfBrandingAttributes = {
primaryColor: '#000',
secondaryColor: '#000',
showCompanyLogo: true,
companyLogo: '',
companyName: '',
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddress: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
showBilledFromAddress: true,
showBilledToAddress: true,
billedToLabel: 'Billed To',
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
showCustomerNote: true,
customerNote:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
customerNoteLabel: 'Customer Note',
showTermsConditions: true,
termsConditions:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
termsConditionsLabel: 'Terms & Conditions',
lines: [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
showEstimateNumber: true,
estimateNumberLabel: 'Estimate Number',
estimateNumebr: '346D3D40-0001',
estimateDate: 'September 3, 2024',
showEstimateDate: true,
estimateDateLabel: 'Estimate Date',
expirationDateLabel: 'Expiration Date',
showExpirationDate: true,
expirationDate: 'September 3, 2024',
};
interface EstimatePdfBrandingLineItem {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface EstimatePdfBrandingAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
billedToAddress: string[];
billedFromAddress: string[];
showBilledFromAddress: boolean;
showBilledToAddress: boolean;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
lines: EstimatePdfBrandingLineItem[];
showEstimateNumber: boolean;
estimateNumberLabel: string;
estimateNumebr: string;
estimateDate: string;
showEstimateDate: boolean;
estimateDateLabel: string;
expirationDateLabel: string;
showExpirationDate: boolean;
expirationDate: string;
}

View File

@@ -0,0 +1,22 @@
import { EstimatePdfBrandingAttributes } from './constants';
export const transformEstimateToPdfTemplate = (
estimate
): Partial<EstimatePdfBrandingAttributes> => {
return {
expirationDate: estimate.formattedExpirationDate,
estimateNumebr: estimate.estimateNumber,
estimateDate: estimate.formattedEstimateDate,
lines: estimate.entries.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
total: estimate.formattedSubtotal,
subtotal: estimate.formattedSubtotal,
customerNote: estimate.customerNote,
termsConditions: estimate.termsConditions,
};
};

View File

@@ -19,6 +19,7 @@ import { formatDateFields } from 'utils';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { ItemEntry } from '@/models';
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export class CommandSaleInvoiceDTOTransformer {
@@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer {
@Inject()
private taxDTOTransformer: ItemEntriesTaxTransactions;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
@@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer {
userId: authorizedUser.id,
} as ISaleInvoice;
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'SaleInvoice'
)
)(initialDTO);
return R.compose(
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
}
/**

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { mergePdfTemplateWithDefaultAttributes } from './utils';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { defaultEstimatePdfBrandingAttributes } from '../Estimates/constants';
@Service()
export class SaleEstimatePdfTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
/**
* Retrieves the estimate pdf template.
* @param {number} tenantId
* @param {number} invoiceTemplateId
* @returns
*/
async getEstimatePdfTemplate(tenantId: number, estimateTemplateId: number) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
estimateTemplateId
);
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultEstimatePdfBrandingAttributes
);
return {
...template,
attributes,
};
}
}

View File

@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
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';
import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
@Service()
export class SaleInvoicePdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,6 +21,9 @@ export class SaleInvoicePdf {
@Inject()
private getInvoiceService: GetSaleInvoice;
@Inject()
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id.
@@ -24,19 +34,54 @@ export class SaleInvoicePdf {
tenantId: number,
invoiceId: number
): Promise<Buffer> {
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
const brandingAttributes = await this.getInvoiceBrandingAttributes(
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-regular',
{
saleInvoice,
}
'modules/invoice-standard',
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
// Converts the given html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves the branding attributes of the given sale invoice.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<InvoicePdfTemplateAttributes>}
*/
async getInvoiceBrandingAttributes(
tenantId: number,
invoiceId: number
): Promise<InvoicePdfTemplateAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const invoice = await this.getInvoiceService.getSaleInvoice(
tenantId,
invoiceId
);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
invoice.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'SaleInvoice',
default: true,
})
)?.id;
// Getting the branding template attributes.
const brandingTemplate =
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
tenantId,
templateId
);
// Merge the branding template attributes with the invoice.
return {
...brandingTemplate.attributes,
...transformInvoiceToPdfTemplate(invoice),
};
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { mergePdfTemplateWithDefaultAttributes } from './utils';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { defaultInvoicePdfTemplateAttributes } from './constants';
@Service()
export class SaleInvoicePdfTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
/**
* Retrieves the invoice pdf template.
* @param {number} tenantId
* @param {number} invoiceTemplateId
* @returns
*/
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
invoiceTemplateId
);
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultInvoicePdfTemplateAttributes
);
return {
...template,
attributes,
};
}
}

View File

@@ -158,3 +158,88 @@ export const SaleInvoicesSampleData = [
Description: 'Description',
},
];
export const defaultInvoicePdfTemplateAttributes = {
primaryColor: 'red',
secondaryColor: 'red',
companyName: 'Bigcapital Technology, Inc.',
showCompanyLogo: true,
companyLogo: '',
dueDateLabel: 'Date due',
showDueDate: true,
dateIssueLabel: 'Date of issue',
showDateIssue: true,
// dateIssue,
invoiceNumberLabel: 'Invoice number',
showInvoiceNumber: true,
// Address
showBillingToAddress: true,
showBilledFromAddress: true,
billedToLabel: 'Billed To',
// Entries
lineItemLabel: 'Item',
lineDescriptionLabel: 'Description',
lineRateLabel: 'Rate',
lineTotalLabel: 'Total',
totalLabel: 'Total',
subtotalLabel: 'Subtotal',
discountLabel: 'Discount',
paymentMadeLabel: 'Payment Made',
balanceDueLabel: 'Balance Due',
// Totals
showTotal: true,
showSubtotal: true,
showDiscount: true,
showTaxes: true,
showPaymentMade: true,
showDueAmount: true,
showBalanceDue: true,
discount: '0.00',
// Footer paragraphs.
termsConditionsLabel: 'Terms & Conditions',
showTermsConditions: true,
lines: [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
taxes: [
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
],
statementLabel: 'Statement',
showStatement: true,
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddres: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
}

View File

@@ -0,0 +1,46 @@
import { pickBy } from 'lodash';
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
export const mergePdfTemplateWithDefaultAttributes = (
brandingTemplate?: Record<string, any>,
defaultAttributes: Record<string, any> = {}
) => {
const brandingAttributes = pickBy(
brandingTemplate,
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
);
return {
...defaultAttributes,
...brandingAttributes,
};
};
export const transformInvoiceToPdfTemplate = (
invoice: ISaleInvoice
): Partial<InvoicePdfTemplateAttributes> => {
return {
dueDate: invoice.dueDateFormatted,
dateIssue: invoice.invoiceDateFormatted,
invoiceNumber: invoice.invoiceNo,
total: invoice.totalFormatted,
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
balanceDue: invoice.balanceAmountFormatted,
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,
lines: invoice.entries.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
taxes: invoice.taxes.map((tax) => ({
label: tax.name,
amount: tax.taxRateAmountFormatted,
})),
};
};

View File

@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetPaymentReceived } from './GetPaymentReceived';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
@Service()
export default class GetPaymentReceivedPdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,6 +21,9 @@ export default class GetPaymentReceivedPdf {
@Inject()
private getPaymentService: GetPaymentReceived;
@Inject()
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -24,19 +34,52 @@ export default class GetPaymentReceivedPdf {
tenantId: number,
paymentReceiveId: number
): Promise<Buffer> {
const paymentReceive = await this.getPaymentService.getPaymentReceive(
const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId,
paymentReceiveId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',
{
paymentReceive,
}
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
// Converts the given html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves the given payment received branding attributes.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<PaymentReceivedPdfTemplateAttributes>}
*/
async getPaymentBrandingAttributes(
tenantId: number,
paymentReceivedId: number
): Promise<PaymentReceivedPdfTemplateAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const paymentReceived = await this.getPaymentService.getPaymentReceive(
tenantId,
paymentReceivedId
);
const templateId =
paymentReceived?.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'PaymentReceive',
default: true,
})
)?.id;
const brandingTemplate =
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
tenantId,
templateId
);
return {
...brandingTemplate.attributes,
...transformPaymentReceivedToPdfTemplate(paymentReceived),
};
}
}

View File

@@ -0,0 +1,35 @@
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { Inject, Service } from 'typedi';
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
import { defaultPaymentReceivedPdfTemplateAttributes } from './constants';
import { PdfTemplate } from '@/models/PdfTemplate';
@Service()
export class PaymentReceivedBrandingTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
/**
* Retrieves the payment received pdf template.
* @param {number} tenantId
* @param {number} paymentTemplateId
* @returns
*/
public async getPaymentReceivedPdfTemplate(
tenantId: number,
paymentTemplateId: number
) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
paymentTemplateId
);
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultPaymentReceivedPdfTemplateAttributes
);
return {
...template,
attributes,
};
}
}

View File

@@ -1,6 +1,7 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import composeAsync from 'async/compose';
import {
ICustomer,
IPaymentReceived,
@@ -12,6 +13,7 @@ import { PaymentReceivedIncrement } from './PaymentReceivedIncrement';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export class PaymentReceiveDTOTransformer {
@@ -24,6 +26,9 @@ export class PaymentReceiveDTOTransformer {
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transformes the create payment receive DTO to model object.
* @param {number} tenantId
@@ -68,8 +73,16 @@ export class PaymentReceiveDTOTransformer {
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries,
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'SaleInvoice'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<IPaymentReceived>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
}
}

View File

@@ -45,3 +45,53 @@ export const PaymentsReceiveSampleData = [
'Payment Amount': 850,
},
];
export const defaultPaymentReceivedPdfTemplateAttributes = {
primaryColor: '#000',
secondaryColor: '#000',
showCompanyLogo: true,
companyLogo: '',
companyName: 'Bigcapital Technology, Inc.',
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddress: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
showBilledFromAddress: true,
showBillingToAddress: true,
billedToLabel: 'Billed To',
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
lines: [
{
invoiceNumber: 'INV-00001',
invoiceAmount: '$1000.00',
paidAmount: '$1000.00',
},
],
showPaymentReceivedNumber: true,
paymentReceivedNumberLabel: 'Payment Number',
paymentReceivedNumebr: '346D3D40-0001',
paymentReceivedDate: 'September 3, 2024',
showPaymentReceivedDate: true,
paymentReceivedDateLabel: 'Payment Date',
};

View File

@@ -0,0 +1,21 @@
import {
IPaymentReceived,
PaymentReceivedPdfTemplateAttributes,
} from '@/interfaces';
export const transformPaymentReceivedToPdfTemplate = (
payment: IPaymentReceived
): Partial<PaymentReceivedPdfTemplateAttributes> => {
return {
total: payment.formattedAmount,
subtotal: payment.subtotalFormatted,
paymentReceivedNumebr: payment.paymentReceiveNo,
paymentReceivedDate: payment.formattedPaymentDate,
customerName: payment.customer.displayName,
lines: payment.entries.map((entry) => ({
invoiceNumber: entry.invoice.invoiceNo,
invoiceAmount: entry.invoice.totalFormatted,
paidAmount: entry.paymentAmountFormatted,
})),
};
};

View File

@@ -0,0 +1,35 @@
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { Inject, Service } from 'typedi';
import { defaultSaleReceiptBrandingAttributes } from './constants';
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
@Service()
export class SaleReceiptBrandingTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
/**
* Retrieves the sale receipt branding template.
* @param {number} tenantId - The ID of the tenant.
* @param {number} templateId - The ID of the PDF template.
* @returns {Promise<Object>} The sale receipt branding template with merged attributes.
*/
public async getSaleReceiptBrandingTemplate(
tenantId: number,
templateId: number
) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
templateId
);
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultSaleReceiptBrandingAttributes
);
return {
...template,
attributes,
};
}
}

View File

@@ -12,6 +12,7 @@ import { formatDateFields } from '@/utils';
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
import { ItemEntry } from '@/models';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export class SaleReceiptDTOTransformer {
@@ -30,6 +31,9 @@ export class SaleReceiptDTOTransformer {
@Inject()
private receiptIncrement: SaleReceiptIncrement;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transform create DTO object to model object.
* @param {ISaleReceiptDTO} saleReceiptDTO -
@@ -88,9 +92,17 @@ export class SaleReceiptDTOTransformer {
}),
entries,
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'SaleReceipt'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
}
}

View File

@@ -2,9 +2,16 @@ 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';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
@Service()
export class SaleReceiptsPdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,26 +21,64 @@ export class SaleReceiptsPdf {
@Inject()
private getSaleReceiptService: GetSaleReceipt;
@Inject()
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
/**
* Retrieves sale invoice pdf content.
* @param {number} tenantId -
* @param {number} tenantId -
* @param {number} saleInvoiceId -
* @returns {Promise<Buffer>}
*/
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId,
saleReceiptId
);
// Converts the receipt template to html content.
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/receipt-regular',
{
saleReceipt,
}
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
// Renders the html content to pdf document.
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves receipt branding attributes.
* @param {number} tenantId
* @param {number} receiptId
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
*/
public async getReceiptBrandingAttributes(
tenantId: number,
receiptId: number
): Promise<ISaleReceiptBrandingTemplateAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
tenantId,
receiptId
);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
saleReceipt.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'SaleReceipt',
default: true,
})
)?.id;
// Retrieves the receipt branding template.
const brandingTemplate =
await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate(
tenantId,
templateId
);
return {
...brandingTemplate.attributes,
...transformReceiptToBrandingTemplateAttributes(saleReceipt),
};
}
}

View File

@@ -22,7 +22,7 @@ export const ERRORS = {
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
};
export const DEFAULT_VIEW_COLUMNS = [];
@@ -47,22 +47,84 @@ export const DEFAULT_VIEWS = [
},
];
export const SaleReceiptsSampleData = [
{
"Receipt Date": "2023-01-01",
"Customer": "Randall Kohler",
"Deposit Account": "Petty Cash",
"Exchange Rate": "",
"Receipt Number": "REC-00001",
"Reference No.": "REF-0001",
"Statement": "Delectus unde aut soluta et accusamus placeat.",
"Receipt Message": "Vitae asperiores dicta.",
"Closed": "T",
"Item": "Schmitt Group",
"Quantity": 100,
"Rate": 200,
"Line Description": "Distinctio distinctio sit veritatis consequatur iste quod veritatis."
}
]
'Receipt Date': '2023-01-01',
Customer: 'Randall Kohler',
'Deposit Account': 'Petty Cash',
'Exchange Rate': '',
'Receipt Number': 'REC-00001',
'Reference No.': 'REF-0001',
Statement: 'Delectus unde aut soluta et accusamus placeat.',
'Receipt Message': 'Vitae asperiores dicta.',
Closed: 'T',
Item: 'Schmitt Group',
Quantity: 100,
Rate: 200,
'Line Description':
'Distinctio distinctio sit veritatis consequatur iste quod veritatis.',
},
];
export const defaultSaleReceiptBrandingAttributes = {
primaryColor: '',
secondaryColor: '',
showCompanyLogo: true,
companyLogo: '',
companyName: 'Bigcapital Technology, Inc.',
// # Address
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddress: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
showBilledFromAddress: true,
showBilledToAddress: true,
billedToLabel: 'Billed To',
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
showCustomerNote: true,
customerNote:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
customerNoteLabel: 'Customer Note',
showTermsConditions: true,
termsConditions:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
termsConditionsLabel: 'Terms & Conditions',
lines: [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
showReceiptNumber: true,
receiptNumberLabel: 'Receipt Number',
receiptNumebr: '346D3D40-0001',
receiptDate: 'September 3, 2024',
showReceiptDate: true,
receiptDateLabel: 'Receipt Date',
};

View File

@@ -0,0 +1,20 @@
import { ISaleReceipt, ISaleReceiptBrandingTemplateAttributes } from "@/interfaces";
export const transformReceiptToBrandingTemplateAttributes = (saleReceipt: ISaleReceipt): Partial<ISaleReceiptBrandingTemplateAttributes> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate,
};
}