feat: hook up branding templates to invoices

This commit is contained in:
Ahmed Bouhuolia
2024-09-16 20:02:02 +02:00
parent 94c08f0b9e
commit 4f59b27d70
23 changed files with 1141 additions and 154 deletions

View File

@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
properties?: PageProperties,
pdfFormat?: PdfFormat
) {
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
const parsedProperties = {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
...properties,
}
return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat);
}
}

View File

@@ -238,7 +238,7 @@ export default class ItemsEntriesService {
* Sets the cost/sell accounts to the invoice entries.
*/
public setItemsEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);

View File

@@ -33,6 +33,14 @@ export class AssignPdfTemplateDefault {
return this.uow.withTransaction(
tenantId,
async (trx?: Knex.Transaction) => {
// Triggers `onPdfTemplateAssigningDefault` event.
await this.eventPublisher.emitAsync(
events.pdfTemplate.onAssigningDefault,
{
tenantId,
templateId,
}
);
await PdfTemplate.query(trx)
.where('resource', oldPdfTempalte.resource)
.patch({ default: false });
@@ -41,6 +49,7 @@ export class AssignPdfTemplateDefault {
.findById(templateId)
.patch({ default: true });
// Triggers `onPdfTemplateAssignedDefault` event.
await this.eventPublisher.emitAsync(
events.pdfTemplate.onAssignedDefault,
{

View File

@@ -0,0 +1,39 @@
import * as R from 'ramda';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@Service()
export class BrandingTemplateDTOTransformer {
@Inject()
private tenancy: HasTenancyService;
/**
* Associates the default branding template id.
* @param {number} tenantId
* @param {string} resource
* @param {Record<string, any>} object
* @param {string} attributeName
* @returns
*/
public assocDefaultBrandingTemplate = (
tenantId: number,
resource: string,
) => async (object: Record<string, any>) => {
const { PdfTemplate } = this.tenancy.models(tenantId);
const attributeName = 'pdfTemplateId';
const defaultTemplate = await PdfTemplate.query().findOne({
resource,
default: true,
});
console.log(defaultTemplate);
if (!defaultTemplate) {
return object;
}
return {
...object,
[attributeName]: defaultTemplate.id,
};
},
}

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

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

View File

@@ -17,7 +17,7 @@ export class TemplateInjectable {
public async render(
tenantId: number,
filename: string,
options: Record<string, string | number | boolean>
options: Record<string, any>
) {
const i18n = this.tenancy.i18n(tenantId);