feat: rendering pdf templates on the server-side

This commit is contained in:
Ahmed Bouhuolia
2024-09-17 13:53:57 +02:00
parent 4f59b27d70
commit 2c790427fa
44 changed files with 1833 additions and 363 deletions

View File

@@ -59,7 +59,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
creditNoteDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
const creditNoteModel = await this.transformCreateEditDTOToModel(
tenantId,
creditNoteDTO,
customer.currencyCode

View File

@@ -0,0 +1,30 @@
import { Inject } from "typedi";
import { GetPdfTemplate } from "../PdfTemplate/GetPdfTemplate";
import { defaultCreditNoteBrandingAttributes } from "./constants";
import { mergePdfTemplateWithDefaultAttributes } from "../Sales/Invoices/utils";
export class CreditNoteBrandingTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
/**
* Retrieves the credit note branding template.
* @param {number} tenantId
* @param {number} templateId
* @returns {}
*/
public async getCreditNoteBrandingTemplate(tenantId: number, templateId: number) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
templateId
);
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultCreditNoteBrandingAttributes
);
return {
...template,
attributes,
};
}
}

View File

@@ -2,6 +2,7 @@ import { Service, Inject } from 'typedi';
import moment from 'moment';
import { omit } from 'lodash';
import * as R from 'ramda';
import composeAsync from 'async/compose';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
@@ -16,6 +17,7 @@ import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersServ
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export default class BaseCreditNotes {
@@ -34,17 +36,20 @@ export default class BaseCreditNotes {
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transformes the credit/edit DTO to model.
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
* @param {string} customerCurrencyCode -
*/
protected transformCreateEditDTOToModel = (
protected transformCreateEditDTOToModel = async (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
customerCurrencyCode: string,
oldCreditNote?: ICreditNote
): ICreditNote => {
): Promise<ICreditNote> => {
// Retrieve the total amount of the given items entries.
const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries
@@ -83,10 +88,18 @@ export default class BaseCreditNotes {
refundedAmount: 0,
invoicesAmount: 0,
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'CreditNote'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
};
/**

View File

@@ -63,7 +63,7 @@ export default class EditCreditNote extends BaseCreditNotes {
creditNoteEditDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
const creditNoteModel = await this.transformCreateEditDTOToModel(
tenantId,
creditNoteEditDTO,
customer.currencyCode,

View File

@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
import GetCreditNote from './GetCreditNote';
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { transformCreditNoteToPdfTemplate } from './utils';
@Service()
export default class GetCreditNotePdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,25 +21,62 @@ export default class GetCreditNotePdf {
@Inject()
private getCreditNoteService: GetCreditNote;
@Inject()
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
/**
* Retrieve sale invoice pdf content.
* Retrieves sale invoice pdf content.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
*/
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
tenantId,
creditNoteId
);
console.log(brandingAttributes, 'brandingAttributes');
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/credit-note-standard',
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves credit note branding attributes.
* @param {number} tenantId - The ID of the tenant.
* @param {number} creditNoteId - The ID of the credit note.
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
*/
public async getCreditNoteBrandingAttributes(
tenantId: number,
creditNoteId: number
): Promise<CreditNotePdfTemplateAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const creditNote = await this.getCreditNoteService.getCreditNote(
tenantId,
creditNoteId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/credit-note-standard',
{
creditNote,
}
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
// Retrieve the invoice template id of not found get the default template id.
const templateId =
creditNote.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'CreditNote',
default: true,
})
)?.id;
// Retrieves the credit note branding template.
const brandingTemplate =
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
tenantId,
templateId
);
return {
...brandingTemplate.attributes,
...transformCreditNoteToPdfTemplate(creditNote),
};
}
}

View File

@@ -9,7 +9,7 @@ export const ERRORS = {
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES'
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
};
export const DEFAULT_VIEW_COLUMNS = [];
@@ -66,3 +66,72 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const defaultCreditNoteBrandingAttributes = {
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',
],
showBilledToAddress: true,
showBilledFromAddress: true,
billedToLabel: 'Billed To',
// Total
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
// Subtotal
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
// Customer note
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',
// Terms & conditions
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',
},
],
// Credit note number.
showCreditNoteNumber: true,
creditNoteNumberLabel: 'Credit Note Number',
creditNoteNumebr: '346D3D40-0001',
// Credit note date.
creditNoteDate: 'September 3, 2024',
showCreditNoteDate: true,
creditNoteDateLabel: 'Credit Note Date',
};

View File

@@ -0,0 +1,9 @@
import { CreditNotePdfTemplateAttributes } from "@/interfaces";
import CreditNote from "@/models/CreditNote";
export const transformCreditNoteToPdfTemplate = (creditNote: CreditNote): Partial<CreditNotePdfTemplateAttributes> {
return {
};
}

View File

@@ -1,6 +1,7 @@
import * as R from 'ramda';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
@Service()
export class BrandingTemplateDTOTransformer {
@@ -9,31 +10,28 @@ export class BrandingTemplateDTOTransformer {
/**
* Associates the default branding template id.
* @param {number} tenantId
* @param {string} resource
* @param {Record<string, any>} object
* @param {string} attributeName
* @returns
* @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';
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,
const defaultTemplate = await PdfTemplate.query().findOne({
resource,
default: true,
});
if (!defaultTemplate || !isEmpty(object[attributeName])) {
return object;
}
return {
...object,
[attributeName]: defaultTemplate.id,
};
};
},
}

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,58 @@ 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);
}
/**
*
* @param {number} tenantId
* @param {number} estimateId
*/
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,7 @@
import { EstimatePdfBrandingAttributes } from './constants';
export const transformEstimateToPdfTemplate = (
estimate
): Partial<EstimatePdfBrandingAttributes> => {
return {};
};

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 { 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
): Promise<PdfTemplate> {
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,12 @@
import {
IPaymentReceived,
PaymentReceivedPdfTemplateAttributes,
} from '@/interfaces';
export const transformPaymentReceivedToPdfTemplate = (
payment: IPaymentReceived
): Partial<PaymentReceivedPdfTemplateAttributes> => {
return {
// ...payment
};
};

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,15 @@ 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';
@Service()
export class SaleReceiptsPdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,26 +20,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
);
console.log(brandingAttributes, 'attributes');
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 },
});
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves receipt branding attributes.
* @param {number} tenantId
* @param {number] receiptId
* @returns
*/
public async getReceiptBrandingAttributes(
tenantId: number,
receiptId: number
) {
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,6 @@
export const transformReceiptToBrandingTemplateAttributes = () => {
return {};
}