mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
Merge branch 'develop' into stripe-integrate
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
23
packages/server/src/services/CreditNotes/utils.ts
Normal file
23
packages/server/src/services/CreditNotes/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal file
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
29
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal file
29
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal file
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/PdfTemplate/types.ts
Normal file
67
packages/server/src/services/PdfTemplate/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal file
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal file
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal file
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user