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

@@ -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

@@ -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,23 @@
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
export const transformCreditNoteToPdfTemplate = (
creditNote: ICreditNote
): Partial<CreditNotePdfTemplateAttributes> => {
return {
creditNoteDate: creditNote.formattedCreditNoteDate,
creditNoteNumebr: creditNote.creditNoteNumber,
total: creditNote.formattedAmount,
subtotal: creditNote.formattedSubtotal,
lines: creditNote.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
customerNote: creditNote.note,
termsConditions: creditNote.termsConditions,
};
};

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

@@ -0,0 +1,63 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class AssignPdfTemplateDefault {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Assigns a default PDF template for a specific tenant.
* @param {number} tenantId - The ID of the tenant for whom the default template is being assigned.
* @param {number} templateId - The ID of the template to be set as the default.
* @returns {Promise<void>} A promise that resolves when the operation is complete.
* @throws {Error} Throws ddan error if the specified template is not found.
*/
public async assignDefaultTemplate(tenantId: number, templateId: number) {
const { PdfTemplate } = this.tenancy.models(tenantId);
const oldPdfTempalte = await PdfTemplate.query()
.findById(templateId)
.throwIfNotFound();
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 });
await PdfTemplate.query(trx)
.findById(templateId)
.patch({ default: true });
// Triggers `onPdfTemplateAssignedDefault` event.
await this.eventPublisher.emitAsync(
events.pdfTemplate.onAssignedDefault,
{
tenantId,
templateId,
}
);
}
);
}
}

View File

@@ -0,0 +1,37 @@
import * as R from 'ramda';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
@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,
});
if (!defaultTemplate || !isEmpty(object[attributeName])) {
return object;
}
return {
...object,
[attributeName]: defaultTemplate.id,
};
};
}

View File

@@ -0,0 +1,51 @@
import { Inject, Service } from 'typedi';
import { ICreateInvoicePdfTemplateDTO } from './types';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreatePdfTemplate {
@Inject()
private tennacy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new pdf template.
* @param {number} tenantId
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO
*/
public createPdfTemplate(
tenantId: number,
templateName: string,
resource: string,
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
) {
const { PdfTemplate } = this.tennacy.models(tenantId);
const attributes = invoiceTemplateDTO;
return this.uow.withTransaction(tenantId, async (trx) => {
// Triggers `onPdfTemplateCreating` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreating, {
tenantId,
});
const pdfTemplate = await PdfTemplate.query(trx).insert({
templateName,
resource,
attributes,
});
// Triggers `onPdfTemplateCreated` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreated, {
tenantId,
});
return pdfTemplate;
});
}
}

View File

@@ -0,0 +1,55 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './types';
@Service()
export class DeletePdfTemplate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes a pdf template.
* @param {number} tenantId
* @param {number} templateId - Pdf template id.
*/
public async deletePdfTemplate(tenantId: number, templateId: number) {
const { PdfTemplate } = this.tenancy.models(tenantId);
const oldPdfTemplate = await PdfTemplate.query()
.findById(templateId)
.throwIfNotFound();
// Cannot delete the predefined pdf templates.
if (oldPdfTemplate.predefined) {
throw new ServiceError(ERRORS.CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE);
}
return this.uow.withTransaction(tenantId, async (trx) => {
// Triggers `onPdfTemplateDeleting` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleting, {
tenantId,
templateId,
oldPdfTemplate,
trx,
});
await PdfTemplate.query(trx).deleteById(templateId);
// Triggers `onPdfTemplateDeleted` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleted, {
tenantId,
templateId,
oldPdfTemplate,
trx,
});
});
}
}

View File

@@ -0,0 +1,58 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IEditPdfTemplateDTO } from './types';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class EditPdfTemplate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Edits an existing pdf template.
* @param {number} tenantId
* @param {number} templateId - Template id.
* @param {IEditPdfTemplateDTO} editTemplateDTO
*/
public async editPdfTemplate(
tenantId: number,
templateId: number,
editTemplateDTO: IEditPdfTemplateDTO
) {
const { PdfTemplate } = this.tenancy.models(tenantId);
const oldPdfTemplate = await PdfTemplate.query()
.findById(templateId)
.throwIfNotFound();
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPdfTemplateEditing` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onEditing, {
tenantId,
templateId,
});
const pdfTemplate = await PdfTemplate.query(trx)
.where('id', templateId)
.update({
templateName: editTemplateDTO.templateName,
attributes: editTemplateDTO.attributes,
});
// Triggers `onPdfTemplatedEdited` event.
await this.eventPublisher.emitAsync(events.pdfTemplate.onEdited, {
tenantId,
templateId,
});
return pdfTemplate;
});
}
}

View File

@@ -0,0 +1,29 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class GetPdfTemplate {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves a pdf template by its ID.
* @param {number} tenantId - The ID of the tenant.
* @param {number} templateId - The ID of the pdf template to retrieve.
* @return {Promise<any>} - The retrieved pdf template.
*/
async getPdfTemplate(
tenantId: number,
templateId: number,
trx?: Knex.Transaction
): Promise<any> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const template = await PdfTemplate.query(trx)
.findById(templateId)
.throwIfNotFound();
return template;
}
}

View File

@@ -0,0 +1,37 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPdfTemplatesTransformer } from './GetPdfTemplatesTransformer';
import { Inject, Service } from 'typedi';
@Service()
export class GetPdfTemplates {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformInjectable: TransformerInjectable;
/**
* Retrieves a list of PDF templates for a specified tenant.
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
* @param {Object} [query] - Optional query parameters to filter the templates.
* @param {string} [query.resource] - The resource type to filter the templates by.
* @returns {Promise<any>} - A promise that resolves to the transformed list of PDF templates.
*/
async getPdfTemplates(tenantId: number, query?: { resource?: string }) {
const { PdfTemplate } = this.tenancy.models(tenantId);
const templates = await PdfTemplate.query().onBuild((q) => {
if (query?.resource) {
q.where('resource', query?.resource);
}
q.orderBy('createdAt', 'ASC');
});
return this.transformInjectable.transform(
tenantId,
templates,
new GetPdfTemplatesTransformer()
);
}
}

View File

@@ -0,0 +1,38 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
export class GetPdfTemplatesTransformer extends Transformer {
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['attributes'];
};
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['createdAtFormatted', 'resourceFormatted'];
};
/**
* Formats the creation date of the PDF template.
* @param {Object} template
* @returns {string} A formatted string representing the creation date of the template.
*/
protected createdAtFormatted = (template) => {
return this.formatDate(template.createdAt);
};
/**
* Formats the creation date of the PDF template.
* @param {Object} template -
* @returns {string} A formatted string representing the creation date of the template.
*/
protected resourceFormatted = (template) => {
return getTransactionTypeLabel(template.resource);
};
}

View File

@@ -0,0 +1,123 @@
import { Inject, Service } from 'typedi';
import { ICreateInvoicePdfTemplateDTO, IEditPdfTemplateDTO } from './types';
import { CreatePdfTemplate } from './CreatePdfTemplate';
import { DeletePdfTemplate } from './DeletePdfTemplate';
import { GetPdfTemplate } from './GetPdfTemplate';
import { GetPdfTemplates } from './GetPdfTemplates';
import { EditPdfTemplate } from './EditPdfTemplate';
import { AssignPdfTemplateDefault } from './AssignPdfTemplateDefault';
@Service()
export class PdfTemplateApplication {
@Inject()
private createPdfTemplateService: CreatePdfTemplate;
@Inject()
private deletePdfTemplateService: DeletePdfTemplate;
@Inject()
private getPdfTemplateService: GetPdfTemplate;
@Inject()
private getPdfTemplatesService: GetPdfTemplates;
@Inject()
private editPdfTemplateService: EditPdfTemplate;
@Inject()
private assignPdfTemplateDefaultService: AssignPdfTemplateDefault;
/**
* Creates a new PDF template.
* @param {number} tenantId -
* @param {string} templateName - The name of the PDF template to create.
* @param {string} resource - The resource type associated with the PDF template.
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - The data transfer object containing the details for the new PDF template.
* @returns {Promise<any>}
*/
public async createPdfTemplate(
tenantId: number,
templateName: string,
resource: string,
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
) {
return this.createPdfTemplateService.createPdfTemplate(
tenantId,
templateName,
resource,
invoiceTemplateDTO
);
}
/**
* Edits an existing PDF template.
* @param {number} tenantId - The ID of the tenant.
* @param {number} templateId - The ID of the PDF template to edit.
* @param {IEditPdfTemplateDTO} editTemplateDTO - The data transfer object containing the updated details for the PDF template.
* @returns {Promise<any>}
*/
public async editPdfTemplate(
tenantId: number,
templateId: number,
editTemplateDTO: IEditPdfTemplateDTO
) {
return this.editPdfTemplateService.editPdfTemplate(
tenantId,
templateId,
editTemplateDTO
);
}
/**
* Deletes a PDF template.
* @param {number} tenantId - The ID of the tenant.
* @param {number} templateId - The ID of the PDF template to delete.
* @returns {Promise<any>}
*/
public async deletePdfTemplate(tenantId: number, templateId: number) {
return this.deletePdfTemplateService.deletePdfTemplate(
tenantId,
templateId
);
}
/**
* Retrieves a PDF template by its ID for a specified tenant.
* @param {number} tenantId -
* @param {number} templateId - The ID of the PDF template to retrieve.
* @returns {Promise<any>}
*/
public async getPdfTemplate(tenantId: number, templateId: number) {
return this.getPdfTemplateService.getPdfTemplate(tenantId, templateId);
}
/**
* Retrieves a list of PDF templates.
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
* @param {Object} query
* @returns {Promise<any>}
*/
public async getPdfTemplates(
tenantId: number,
query?: { resource?: string }
) {
return this.getPdfTemplatesService.getPdfTemplates(tenantId, query);
}
/**
* Assigns a PDF template as the default template.
* @param {number} tenantId
* @param {number} templateId - The ID of the PDF template to assign as default.
* @returns {Promise<any>}
*/
public async assignPdfTemplateAsDefault(
tenantId: number,
templateId: number
) {
return this.assignPdfTemplateDefaultService.assignDefaultTemplate(
tenantId,
templateId
);
}
}

View File

@@ -0,0 +1,67 @@
export enum ERRORS {
CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE = 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE',
}
export interface IEditPdfTemplateDTO {
templateName: string;
attributes: Record<string, any>;
}
export interface ICreateInvoicePdfTemplateDTO {
// Colors
primaryColor?: string;
secondaryColor?: string;
// Company Logo
showCompanyLogo?: boolean;
companyLogo?: string;
// Top details.
showInvoiceNumber?: boolean;
invoiceNumberLabel?: string;
showDateIssue?: boolean;
dateIssueLabel?: string;
showDueDate?: boolean;
dueDateLabel?: string;
// Company name
companyName?: string;
// Addresses
showBilledFromAddress?: boolean;
showBillingToAddress?: boolean;
billedToLabel?: string;
// Entries
itemNameLabel?: string;
itemDescriptionLabel?: string;
itemRateLabel?: string;
itemTotalLabel?: string;
// Totals
showSubtotal?: boolean;
subtotalLabel?: string;
showDiscount?: boolean;
discountLabel?: string;
showTaxes?: boolean;
showTotal?: boolean;
totalLabel?: string;
paymentMadeLabel?: string;
showPaymentMade?: boolean;
dueAmountLabel?: string;
showDueAmount?: boolean;
// Footer paragraphs.
termsConditionsLabel?: string;
showTermsConditions?: boolean;
statementLabel?: string;
showStatement?: boolean;
}

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,
};
}

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);