mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-14 20:00:33 +00:00
Merge pull request #280 from bigcapitalhq/optimize-printing
feat: optimize documents printing
This commit is contained in:
@@ -5,6 +5,7 @@ global.__root_dir = path.join(__dirname, '..');
|
||||
global.__resources_dir = path.join(global.__root_dir, 'resources');
|
||||
global.__locales_dir = path.join(global.__resources_dir, 'locales');
|
||||
global.__views_dir = path.join(global.__root_dir, 'views');
|
||||
global.__storage_dir = path.join(global.__root_dir, 'storage');
|
||||
|
||||
moment.prototype.toMySqlDateTime = function () {
|
||||
return this.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('storage', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('key').notNullable();
|
||||
table.string('path').notNullable();
|
||||
table.string('extension').notNullable();
|
||||
table.integer('expire_in');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('storage');
|
||||
};
|
||||
26
packages/server/src/lib/Chromiumly/Chromiumly.ts
Normal file
26
packages/server/src/lib/Chromiumly/Chromiumly.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ChromiumRoute, LibreOfficeRoute, PdfEngineRoute } from './_types';
|
||||
|
||||
export class Chromiumly {
|
||||
public static readonly GOTENBERG_ENDPOINT = process.env.GOTENBERG_URL || '';
|
||||
|
||||
public static readonly CHROMIUM_PATH = 'forms/chromium/convert';
|
||||
public static readonly PDF_ENGINES_PATH = 'forms/pdfengines';
|
||||
public static readonly LIBRE_OFFICE_PATH = 'forms/libreoffice';
|
||||
|
||||
public static readonly GOTENBERG_DOCS_ENDPOINT =
|
||||
process.env.GOTENBERG_DOCS_URL || '';
|
||||
|
||||
public static readonly CHROMIUM_ROUTES = {
|
||||
url: ChromiumRoute.URL,
|
||||
html: ChromiumRoute.HTML,
|
||||
markdown: ChromiumRoute.MARKDOWN,
|
||||
};
|
||||
|
||||
public static readonly PDF_ENGINE_ROUTES = {
|
||||
merge: PdfEngineRoute.MERGE,
|
||||
};
|
||||
|
||||
public static readonly LIBRE_OFFICE_ROUTES = {
|
||||
convert: LibreOfficeRoute.CONVERT,
|
||||
};
|
||||
}
|
||||
66
packages/server/src/lib/Chromiumly/ConvertUtils.ts
Normal file
66
packages/server/src/lib/Chromiumly/ConvertUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import FormData from 'form-data';
|
||||
import { GotenbergUtils } from './GotenbergUtils';
|
||||
import { PageProperties } from './_types';
|
||||
|
||||
export class ConverterUtils {
|
||||
public static injectPageProperties(
|
||||
data: FormData,
|
||||
pageProperties: PageProperties
|
||||
): void {
|
||||
if (pageProperties.size) {
|
||||
GotenbergUtils.assert(
|
||||
pageProperties.size.width >= 1.0 && pageProperties.size.height >= 1.5,
|
||||
'size is smaller than the minimum printing requirements (i.e. 1.0 x 1.5 in)'
|
||||
);
|
||||
|
||||
data.append('paperWidth', pageProperties.size.width);
|
||||
data.append('paperHeight', pageProperties.size.height);
|
||||
}
|
||||
if (pageProperties.margins) {
|
||||
GotenbergUtils.assert(
|
||||
pageProperties.margins.top >= 0 &&
|
||||
pageProperties.margins.bottom >= 0 &&
|
||||
pageProperties.margins.left >= 0 &&
|
||||
pageProperties.margins.left >= 0,
|
||||
'negative margins are not allowed'
|
||||
);
|
||||
data.append('marginTop', pageProperties.margins.top);
|
||||
data.append('marginBottom', pageProperties.margins.bottom);
|
||||
data.append('marginLeft', pageProperties.margins.left);
|
||||
data.append('marginRight', pageProperties.margins.right);
|
||||
}
|
||||
if (pageProperties.preferCssPageSize) {
|
||||
data.append(
|
||||
'preferCssPageSize',
|
||||
String(pageProperties.preferCssPageSize)
|
||||
);
|
||||
}
|
||||
if (pageProperties.printBackground) {
|
||||
data.append('printBackground', String(pageProperties.printBackground));
|
||||
}
|
||||
if (pageProperties.landscape) {
|
||||
data.append('landscape', String(pageProperties.landscape));
|
||||
}
|
||||
if (pageProperties.scale) {
|
||||
GotenbergUtils.assert(
|
||||
pageProperties.scale >= 0.1 && pageProperties.scale <= 2.0,
|
||||
'scale is outside of [0.1 - 2] range'
|
||||
);
|
||||
data.append('scale', pageProperties.scale);
|
||||
}
|
||||
|
||||
if (pageProperties.nativePageRanges) {
|
||||
GotenbergUtils.assert(
|
||||
pageProperties.nativePageRanges.from > 0 &&
|
||||
pageProperties.nativePageRanges.to > 0 &&
|
||||
pageProperties.nativePageRanges.to >=
|
||||
pageProperties.nativePageRanges.from,
|
||||
'page ranges syntax error'
|
||||
);
|
||||
data.append(
|
||||
'nativePageRanges',
|
||||
`${pageProperties.nativePageRanges.from}-${pageProperties.nativePageRanges.to}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/server/src/lib/Chromiumly/Converter.ts
Normal file
10
packages/server/src/lib/Chromiumly/Converter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Chromiumly } from './Chromiumly';
|
||||
import { ChromiumRoute } from './_types';
|
||||
|
||||
export abstract class Converter {
|
||||
readonly endpoint: string;
|
||||
|
||||
constructor(route: ChromiumRoute) {
|
||||
this.endpoint = `${Chromiumly.GOTENBERG_ENDPOINT}/${Chromiumly.CHROMIUM_PATH}/${Chromiumly.CHROMIUM_ROUTES[route]}`;
|
||||
}
|
||||
}
|
||||
24
packages/server/src/lib/Chromiumly/GotenbergUtils.ts
Normal file
24
packages/server/src/lib/Chromiumly/GotenbergUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import FormData from 'form-data';
|
||||
import Axios from 'axios';
|
||||
|
||||
export class GotenbergUtils {
|
||||
public static assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
|
||||
try {
|
||||
const response = await Axios.post(endpoint, data, {
|
||||
headers: {
|
||||
...data.getHeaders(),
|
||||
},
|
||||
responseType: 'arraybuffer', // This ensures you get a Buffer bac
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/server/src/lib/Chromiumly/HTMLConvert.ts
Normal file
38
packages/server/src/lib/Chromiumly/HTMLConvert.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { constants, createReadStream, PathLike, promises } from 'fs';
|
||||
import FormData from 'form-data';
|
||||
import { GotenbergUtils } from './GotenbergUtils';
|
||||
import { IConverter, PageProperties } from './_types';
|
||||
import { PdfFormat, ChromiumRoute } from './_types';
|
||||
import { ConverterUtils } from './ConvertUtils';
|
||||
import { Converter } from './Converter';
|
||||
|
||||
export class HtmlConverter extends Converter implements IConverter {
|
||||
constructor() {
|
||||
super(ChromiumRoute.HTML);
|
||||
}
|
||||
|
||||
async convert({
|
||||
html,
|
||||
properties,
|
||||
pdfFormat,
|
||||
}: {
|
||||
html: PathLike;
|
||||
properties?: PageProperties;
|
||||
pdfFormat?: PdfFormat;
|
||||
}): Promise<Buffer> {
|
||||
try {
|
||||
await promises.access(html, constants.R_OK);
|
||||
const data = new FormData();
|
||||
if (pdfFormat) {
|
||||
data.append('pdfFormat', pdfFormat);
|
||||
}
|
||||
data.append('index.html', createReadStream(html));
|
||||
if (properties) {
|
||||
ConverterUtils.injectPageProperties(data, properties);
|
||||
}
|
||||
return GotenbergUtils.fetch(this.endpoint, data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/server/src/lib/Chromiumly/UrlConvert.ts
Normal file
38
packages/server/src/lib/Chromiumly/UrlConvert.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import FormData from 'form-data';
|
||||
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
|
||||
import { ConverterUtils } from './ConvertUtils';
|
||||
import { Converter } from './Converter';
|
||||
import { GotenbergUtils } from './GotenbergUtils';
|
||||
|
||||
export class UrlConverter extends Converter implements IConverter {
|
||||
constructor() {
|
||||
super(ChromiumRoute.URL);
|
||||
}
|
||||
|
||||
async convert({
|
||||
url,
|
||||
properties,
|
||||
pdfFormat,
|
||||
}: {
|
||||
url: string;
|
||||
properties?: PageProperties;
|
||||
pdfFormat?: PdfFormat;
|
||||
}): Promise<Buffer> {
|
||||
try {
|
||||
const _url = new URL(url);
|
||||
const data = new FormData();
|
||||
|
||||
if (pdfFormat) {
|
||||
data.append('pdfFormat', pdfFormat);
|
||||
}
|
||||
data.append('url', _url.href);
|
||||
|
||||
if (properties) {
|
||||
ConverterUtils.injectPageProperties(data, properties);
|
||||
}
|
||||
return GotenbergUtils.fetch(this.endpoint, data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/server/src/lib/Chromiumly/_types.ts
Normal file
51
packages/server/src/lib/Chromiumly/_types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { PathLike } from 'fs';
|
||||
|
||||
export type PageSize = {
|
||||
width: number; // Paper width, in inches (default 8.5)
|
||||
height: number; //Paper height, in inches (default 11)
|
||||
};
|
||||
|
||||
export type PageMargins = {
|
||||
top: number; // Top margin, in inches (default 0.39)
|
||||
bottom: number; // Bottom margin, in inches (default 0.39)
|
||||
left: number; // Left margin, in inches (default 0.39)
|
||||
right: number; // Right margin, in inches (default 0.39)
|
||||
};
|
||||
|
||||
export type PageProperties = {
|
||||
size?: PageSize;
|
||||
margins?: PageMargins;
|
||||
preferCssPageSize?: boolean; // Define whether to prefer page size as defined by CSS (default false)
|
||||
printBackground?: boolean; // Print the background graphics (default false)
|
||||
landscape?: boolean; // Set the paper orientation to landscape (default false)
|
||||
scale?: number; // The scale of the page rendering (default 1.0)
|
||||
nativePageRanges?: { from: number; to: number }; // Page ranges to print
|
||||
};
|
||||
|
||||
export interface IConverter {
|
||||
convert({
|
||||
...args
|
||||
}: {
|
||||
[x: string]: string | PathLike | PageProperties | PdfFormat;
|
||||
}): Promise<Buffer>;
|
||||
}
|
||||
|
||||
export enum PdfFormat {
|
||||
A_1a = 'PDF/A-1a',
|
||||
A_2b = 'PDF/A-2b',
|
||||
A_3b = 'PDF/A-3b',
|
||||
}
|
||||
|
||||
export enum ChromiumRoute {
|
||||
URL = 'url',
|
||||
HTML = 'html',
|
||||
MARKDOWN = 'markdown',
|
||||
}
|
||||
|
||||
export enum PdfEngineRoute {
|
||||
MERGE = 'merge',
|
||||
}
|
||||
|
||||
export enum LibreOfficeRoute {
|
||||
CONVERT = 'convert',
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { json, Request, Response, NextFunction } from 'express';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import errorHandler from 'errorhandler';
|
||||
@@ -42,6 +43,8 @@ export default ({ app }) => {
|
||||
// Middleware for intercepting and transforming json responses.
|
||||
app.use(JSONResponseTransformer(snakecaseResponseTransformer));
|
||||
|
||||
app.use('/public', express.static(path.join(global.__storage_dir)));
|
||||
|
||||
// Handle multi-media requests.
|
||||
app.use(
|
||||
fileUpload({
|
||||
|
||||
@@ -60,6 +60,7 @@ import Time from 'models/Time';
|
||||
import Task from 'models/Task';
|
||||
import TaxRate from 'models/TaxRate';
|
||||
import TaxRateTransaction from 'models/TaxRateTransaction';
|
||||
import Attachment from 'models/Attachment';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
@@ -123,6 +124,7 @@ export default (knex) => {
|
||||
Task,
|
||||
TaxRate,
|
||||
TaxRateTransaction,
|
||||
Attachment
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
};
|
||||
|
||||
23
packages/server/src/models/Attachment.ts
Normal file
23
packages/server/src/models/Attachment.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { mixin } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
|
||||
export default class Attachment extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'storage';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { PageProperties, PdfFormat } from '@/lib/Chromiumly/_types';
|
||||
import { UrlConverter } from '@/lib/Chromiumly/UrlConvert';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Chromiumly } from '@/lib/Chromiumly/Chromiumly';
|
||||
import { PDF_FILE_EXPIRE_IN, getPdfFilesStorageDir } from './utils';
|
||||
|
||||
@Service()
|
||||
export class ChromiumlyHtmlConvert {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Write HTML content to temporary file.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} content - HTML content.
|
||||
* @returns {Promise<[string, () => Promise<void>]>}
|
||||
*/
|
||||
async writeTempHtmlFile(
|
||||
tenantId: number,
|
||||
content: string
|
||||
): Promise<[string, () => Promise<void>]> {
|
||||
const { Attachment } = this.tenancy.models(tenantId);
|
||||
|
||||
const filename = `document-${Date.now()}.html`;
|
||||
const storageDir = getPdfFilesStorageDir(filename);
|
||||
const filePath = path.join(global.__storage_dir, storageDir);
|
||||
|
||||
await fs.writeFile(filePath, content);
|
||||
await Attachment.query().insert({
|
||||
key: filename,
|
||||
path: storageDir,
|
||||
expire_in: PDF_FILE_EXPIRE_IN, // ms
|
||||
extension: 'html',
|
||||
});
|
||||
const cleanup = async () => {
|
||||
await fs.unlink(filePath);
|
||||
await Attachment.query().where('key', filename).delete();
|
||||
};
|
||||
return [filename, cleanup];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given HTML content to PDF.
|
||||
* @param {string} html
|
||||
* @param {PageProperties} properties
|
||||
* @param {PdfFormat} pdfFormat
|
||||
* @returns {Array<Buffer>}
|
||||
*/
|
||||
async convert(
|
||||
tenantId: number,
|
||||
html: string,
|
||||
properties?: PageProperties,
|
||||
pdfFormat?: PdfFormat
|
||||
): Promise<Buffer> {
|
||||
const [filename, cleanupTempFile] = await this.writeTempHtmlFile(
|
||||
tenantId,
|
||||
html
|
||||
);
|
||||
const fileDir = getPdfFilesStorageDir(filename);
|
||||
const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir);
|
||||
const urlConverter = new UrlConverter();
|
||||
|
||||
const buffer = await urlConverter.convert({
|
||||
url,
|
||||
properties,
|
||||
pdfFormat,
|
||||
});
|
||||
await cleanupTempFile();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PageProperties, PdfFormat } from '@/lib/Chromiumly/_types';
|
||||
import { ChromiumlyHtmlConvert } from './ChromiumlyHtmlConvert';
|
||||
|
||||
@Service()
|
||||
export class ChromiumlyTenancy {
|
||||
@Inject()
|
||||
private htmlConvert: ChromiumlyHtmlConvert;
|
||||
|
||||
/**
|
||||
* Converts the given HTML content to PDF.
|
||||
* @param {string} content
|
||||
* @param {PageProperties} properties
|
||||
* @param {PdfFormat} pdfFormat
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public convertHtmlContent(
|
||||
tenantId: number,
|
||||
content: string,
|
||||
properties?: PageProperties,
|
||||
pdfFormat?: PdfFormat
|
||||
) {
|
||||
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
|
||||
}
|
||||
}
|
||||
8
packages/server/src/services/ChromiumlyTenancy/utils.ts
Normal file
8
packages/server/src/services/ChromiumlyTenancy/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import path from 'path';
|
||||
|
||||
export const PDF_FILE_SUB_DIR = '/pdf';
|
||||
export const PDF_FILE_EXPIRE_IN = 40; // ms
|
||||
|
||||
export const getPdfFilesStorageDir = (filename: string) => {
|
||||
return path.join(PDF_FILE_SUB_DIR, filename);
|
||||
}
|
||||
@@ -1,37 +1,29 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
||||
|
||||
@Service()
|
||||
export default class GetCreditNotePdf {
|
||||
@Inject()
|
||||
pdfService: PdfService;
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
*/
|
||||
async getCreditNotePdf(tenantId: number, creditNote) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/credit-note-standard', {
|
||||
organization,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
creditNote,
|
||||
...i18n,
|
||||
public async getCreditNotePdf(tenantId: number, creditNote) {
|
||||
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 },
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Service } from 'typedi';
|
||||
import puppeteer from 'puppeteer';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export default class PdfService {
|
||||
|
||||
/**
|
||||
* Pdf document.
|
||||
* @param content
|
||||
* @returns
|
||||
*/
|
||||
async pdfDocument(content: string) {
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: config.puppeteer.browserWSEndpoint,
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(content);
|
||||
|
||||
const pdf = await page.pdf({ format: 'a4' });
|
||||
|
||||
await browser.close();
|
||||
|
||||
return pdf;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,29 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private pdfService: PdfService;
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
*/
|
||||
async getSaleEstimatePdf(tenantId: number, saleEstimate) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/estimate-regular', {
|
||||
saleEstimate,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
...i18n,
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/estimate-regular',
|
||||
{
|
||||
saleEstimate,
|
||||
}
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ export class CommandSaleInvoiceValidators {
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice number unqiue on the storage.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} invoiceNumber -
|
||||
* @param {number} notInvoiceId -
|
||||
*/
|
||||
public async validateInvoiceNumberUnique(
|
||||
tenantId: number,
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
pdfService: PdfService;
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
* @param {number} tenantId - Tenant Id.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async saleInvoicePdf(tenantId: number, saleInvoice) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/invoice-regular', {
|
||||
organization,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
saleInvoice,
|
||||
...i18n,
|
||||
async saleInvoicePdf(
|
||||
tenantId: number,
|
||||
saleInvoice: ISaleInvoice
|
||||
): Promise<Buffer> {
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/invoice-regular',
|
||||
{
|
||||
saleInvoice,
|
||||
}
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { IPaymentReceive } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetPaymentReceivePdf {
|
||||
@Inject()
|
||||
private pdfService: PdfService;
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
* @param {number} tenantId -
|
||||
* @param {IPaymentReceive} paymentReceive -
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async getPaymentReceivePdf(tenantId: number, paymentReceive) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/payment-receive-standard', {
|
||||
organization,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
paymentReceive,
|
||||
...i18n,
|
||||
async getPaymentReceivePdf(
|
||||
tenantId: number,
|
||||
paymentReceive: IPaymentReceive
|
||||
): Promise<Buffer> {
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/payment-receive-standard',
|
||||
{
|
||||
paymentReceive,
|
||||
}
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PdfService from '@/services/PDF/PdfService';
|
||||
import { templateRender } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
pdfService: PdfService;
|
||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
*/
|
||||
async saleReceiptPdf(tenantId: number, saleReceipt) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const htmlContent = templateRender('modules/receipt-regular', {
|
||||
saleReceipt,
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
...i18n,
|
||||
public async saleReceiptPdf(tenantId: number, saleReceipt) {
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/receipt-regular',
|
||||
{
|
||||
saleReceipt,
|
||||
}
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
|
||||
|
||||
return pdfContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { templateRender } from '@/utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class TemplateInjectable {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Renders the given filename of the template.
|
||||
* @param {number} tenantId
|
||||
* @param {string} filename
|
||||
* @returns {string}
|
||||
*/
|
||||
public async render(
|
||||
tenantId: number,
|
||||
filename: string,
|
||||
options: Record<string, string | number | boolean>
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
return templateRender(filename, {
|
||||
organizationName: organization.metadata.name,
|
||||
organizationEmail: organization.metadata.email,
|
||||
__: i18n.__,
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ const mergeObjectsBykey = (object1, object2, key) => {
|
||||
};
|
||||
|
||||
function templateRender(filePath, options) {
|
||||
const basePath = path.join(__dirname, '../../resources/views');
|
||||
const basePath = path.join(global.__resources_dir, '/views');
|
||||
return pug.renderFile(`${basePath}/${filePath}.pug`, options);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user