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

@@ -0,0 +1,178 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import BaseController from '@/api/controllers/BaseController';
import { PdfTemplateApplication } from '@/services/PdfTemplate/PdfTemplateApplication';
@Service()
export class PdfTemplatesController extends BaseController {
@Inject()
public pdfTemplateApplication: PdfTemplateApplication;
/**
* Router constructor method.
*/
public router() {
const router = Router();
router.delete(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.deletePdfTemplate.bind(this)
);
router.post(
'/:template_id',
[
param('template_id').exists().isInt().toInt(),
check('template_name').exists(),
check('attributes').exists(),
],
this.validationResult,
this.editPdfTemplate.bind(this)
);
router.get(
'/',
[query('resource').optional()],
this.validationResult,
this.getPdfTemplates.bind(this)
);
router.get(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.getPdfTemplate.bind(this)
);
router.post(
'/',
[
check('template_name').exists(),
check('resource').exists(),
check('attributes').exists(),
],
this.validationResult,
this.createPdfInvoiceTemplate.bind(this)
);
router.post(
'/:template_id/assign_default',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.assginPdfTemplateAsDefault.bind(this)
);
return router;
}
async createPdfInvoiceTemplate(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { templateName, resource, attributes } = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.createPdfTemplate(
tenantId,
templateName,
resource,
attributes
);
return res.status(201).send({
id: result.id,
message: 'The PDF template has been created successfully.',
});
} catch (error) {
next(error);
}
}
async editPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
const editTemplateDTO = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.editPdfTemplate(
tenantId,
Number(templateId),
editTemplateDTO
);
return res.status(200).send({
id: result.id,
message: 'The PDF template has been updated successfully.',
});
} catch (error) {
next(error);
}
}
async deletePdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
await this.pdfTemplateApplication.deletePdfTemplate(
tenantId,
Number(templateId)
);
return res.status(204).send({
id: templateId,
message: 'The PDF template has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
async getPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
const template = await this.pdfTemplateApplication.getPdfTemplate(
tenantId,
Number(templateId)
);
return res.status(200).send(template);
} catch (error) {
next(error);
}
}
async getPdfTemplates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const templates = await this.pdfTemplateApplication.getPdfTemplates(
tenantId,
query
);
return res.status(200).send(templates);
} catch (error) {
next(error);
}
}
async assginPdfTemplateAsDefault(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
await this.pdfTemplateApplication.assignPdfTemplateAsDefault(
tenantId,
Number(templateId)
);
return res.status(204).send({
id: templateId,
message: 'The given pdf template has been assigned as default template',
});
} catch (error) {
next(error);
}
}
}

View File

@@ -236,6 +236,9 @@ export default class PaymentReceivesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -167,6 +167,9 @@ export default class PaymentReceivesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -168,9 +168,7 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
@@ -186,6 +184,9 @@ export default class SalesEstimatesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -224,9 +224,7 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
@@ -257,6 +255,9 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -148,17 +148,20 @@ export default class SalesReceiptsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('receipt_message').optional().trim(),
check('statement').optional().trim(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -67,6 +67,7 @@ import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoC
import { StripeIntegrationController } from './controllers/StripeIntegration/StripeIntegrationController';
import { ShareLinkController } from './controllers/ShareLink/ShareLinkController';
import { PublicSharableLinkController } from './controllers/ShareLink/PublicSharableLinkController';
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
export default () => {
const app = Router();
@@ -155,6 +156,11 @@ export default () => {
'/stripe_integration',
Container.get(StripeIntegrationController).router()
);
dashboard.use(
'/pdf-templates',
Container.get(PdfTemplatesController).router()
);
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());
dashboard.use('/', Container.get(WarehousesItemController).router());

View File

@@ -1,5 +1,5 @@
export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice d';
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';

View File

@@ -0,0 +1,75 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.createTable('pdf_templates', (table) => {
table.increments('id').primary();
table.text('resource');
table.text('template_name');
table.json('attributes');
table.boolean('predefined').defaultTo(false);
table.boolean('default').defaultTo(false);
table.timestamps();
})
.table('sales_invoices', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('sales_estimates', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('sales_receipts', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('credit_notes', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('payment_receives', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema
.table('payment_receives', (table) => {
table.dropColumn('pdf_template_id');
})
.table('credit_notes', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_receipts', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_estimates', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_invoices', (table) => {
table.dropColumn('pdf_template_id');
})
.dropTableIfExists('pdf_templates');
};

View File

@@ -0,0 +1,44 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex('pdf_templates').insert([
{
resource: 'SaleInvoice',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'SaleEstimate',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'SaleReceipt',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'CreditNote',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'PaymentReceive',
templateName: 'Standard Template',
predefined: true,
default: true,
},
]);
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {};

View File

@@ -62,6 +62,8 @@ export interface ICreditNote {
branchId?: number;
warehouseId: number;
createdAt?: Date;
termsConditions: string;
note: string;
}
export enum CreditNoteAction {
@@ -258,3 +260,49 @@ export type ICreditNoteGLCommonEntry = Pick<
| 'debit'
| 'branchId'
>;
export interface CreditNotePdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
billedToAddress: string[];
billedFromAddress: string[];
showBilledToAddress: boolean;
showBilledFromAddress: 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: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
showCreditNoteNumber: boolean;
creditNoteNumberLabel: string;
creditNoteNumebr: string;
creditNoteDate: string;
showCreditNoteDate: boolean;
creditNoteDateLabel: string;
}

View File

@@ -25,6 +25,7 @@ export interface IPaymentReceived {
updatedAt: Date;
localAmount?: number;
branchId?: number;
pdfTemplateId?: number;
}
export interface IPaymentReceivedCreateDTO {
customerId: number;
@@ -185,3 +186,52 @@ export interface PaymentReceiveMailPresendEvent {
paymentReceiveId: number;
messageOptions: PaymentReceiveMailOptsDTO;
}
export interface PaymentReceivedPdfLineItem {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface PaymentReceivedPdfTax {
label: string;
amount: string;
}
export interface PaymentReceivedPdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
billedToAddress: string[];
billedFromAddress: string[];
showBilledFromAddress: boolean;
showBillingToAddress: boolean;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
lines: Array<{
invoiceNumber: string;
invoiceAmount: string;
paidAmount: string;
}>;
showPaymentReceivedNumber: boolean;
paymentReceivedNumberLabel: string;
paymentReceivedNumebr: string;
paymentReceivedDate: string;
showPaymentReceivedDate: boolean;
paymentReceivedDateLabel: string;
}

View File

@@ -143,3 +143,4 @@ export interface ISaleEstimateMailPresendEvent {
saleEstimateId: number;
messageOptions: SaleEstimateMailOptionsDTO;
}

View File

@@ -45,6 +45,11 @@ export interface ISaleInvoice {
subtotal: number;
subtotalLocal: number;
subtotalExludingTax: number;
termsConditions: string;
invoiceMessage: string;
pdfTemplateId?: number;
}
export interface ISaleInvoiceDTO {
@@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
}
// Invoice Pdf Document
export interface InvoicePdfLine {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface InvoicePdfTax {
label: string;
amount: string;
}
export interface InvoicePdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
companyName: string;
showCompanyLogo: boolean;
companyLogo: string;
dueDate: string;
dueDateLabel: string;
showDueDate: boolean;
dateIssue: string;
dateIssueLabel: string;
showDateIssue: boolean;
invoiceNumberLabel: string;
invoiceNumber: string;
showInvoiceNumber: boolean;
showBillingToAddress: boolean;
showBilledFromAddress: boolean;
billedToLabel: string;
lineItemLabel: string;
lineDescriptionLabel: string;
lineRateLabel: string;
lineTotalLabel: string;
totalLabel: string;
subtotalLabel: string;
discountLabel: string;
paymentMadeLabel: string;
balanceDueLabel: string;
showTotal: boolean;
showSubtotal: boolean;
showDiscount: boolean;
showTaxes: boolean;
showPaymentMade: boolean;
showDueAmount: boolean;
showBalanceDue: boolean;
total: string;
subtotal: string;
discount: string;
paymentMade: string;
balanceDue: string;
termsConditionsLabel: string;
showTermsConditions: boolean;
termsConditions: string;
lines: InvoicePdfLine[];
taxes: InvoicePdfTax[];
statementLabel: string;
showStatement: boolean;
statement: string;
billedToAddress: string[];
billedFromAddres: string[];
}

View File

@@ -155,3 +155,57 @@ export interface ISaleReceiptMailPresend {
saleReceiptId: number;
messageOptions: SaleReceiptMailOptsDTO;
}
export interface ISaleReceiptBrandingTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
// Address
billedToAddress: string[];
billedFromAddress: string[];
showBilledFromAddress: boolean;
showBilledToAddress: boolean;
billedToLabel: string;
// Total
total: string;
totalLabel: string;
showTotal: boolean;
// Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
// Lines
lines: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
// Receipt Number
showReceiptNumber: boolean;
receiptNumberLabel: string;
receiptNumebr: string;
// Receipt Date
receiptDate: string;
showReceiptDate: boolean;
receiptDateLabel: string;
}

View File

@@ -68,6 +68,7 @@ import { BankRule } from '@/models/BankRule';
import { BankRuleCondition } from '@/models/BankRuleCondition';
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
import { PdfTemplate } from '@/models/PdfTemplate';
export default (knex) => {
const models = {
@@ -139,6 +140,7 @@ export default (knex) => {
BankRuleCondition,
RecognizedBankTransaction,
MatchedBankTransaction,
PdfTemplate
};
return mapValues(models, (model) => model.bindKnex(knex));
};

View File

@@ -0,0 +1,45 @@
import TenantModel from 'models/TenantModel';
export class PdfTemplate extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'pdf_templates';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Json schema.
*/
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
templateName: { type: 'string' },
attributes: { type: 'object' }, // JSON field definition
},
};
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {};
}
}

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

View File

@@ -58,7 +58,7 @@ export default {
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
},
/**
@@ -687,4 +687,19 @@ export default {
import: {
onImportCommitted: 'onImportFileCommitted',
},
// Branding templates
pdfTemplate: {
onCreating: 'onPdfTemplateCreating',
onCreated: 'onPdfTemplateCreated',
onEditing: 'onPdfTemplateEditing',
onEdited: 'onPdfTemplatedEdited',
onDeleting: 'onPdfTemplateDeleting',
onDeleted: 'onPdfTemplateDeleted',
onAssignedDefault: 'onPdfTemplateAssignedDefault',
onAssigningDefault: 'onPdfTemplateAssigningDefault',
},
};