feat: invoice, estimate and receipt printing.

This commit is contained in:
a.bouhuolia
2021-08-17 10:47:04 +02:00
parent 70939c5741
commit 160b8b6a1b
50 changed files with 3607 additions and 120 deletions

View File

@@ -111,8 +111,6 @@ export default class BaseController {
return response;
}
/**
* Async middleware.
* @param {function} callback
@@ -129,4 +127,14 @@ export default class BaseController {
protected accepts(req) {
return accepts(req);
}
/**
*
* @param {Request} req
* @param {string[]} types
* @returns {string}
*/
protected acceptTypes(req: Request, types: string[]) {
return this.accepts(req).types(types);
}
}

View File

@@ -165,11 +165,12 @@ export default class PaymentReceivesController extends BaseController {
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try {
const storedPaymentReceive = await this.paymentReceiveService.createPaymentReceive(
tenantId,
paymentReceive,
user
);
const storedPaymentReceive =
await this.paymentReceiveService.createPaymentReceive(
tenantId,
paymentReceive,
user
);
return res.status(200).send({
id: storedPaymentReceive.id,
message: 'The payment receive has been created successfully.',
@@ -247,11 +248,13 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const invoices = await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId,
paymentReceiveId
);
return res.status(200).send({ sale_invoices: invoices });
const saleInvoices =
await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId,
paymentReceiveId
);
return res.status(200).send(this.transfromToResponse({ saleInvoices }));
} catch (error) {
next(error);
}
@@ -274,17 +277,11 @@ export default class PaymentReceivesController extends BaseController {
};
try {
const {
paymentReceives,
pagination,
filterMeta,
} = await this.paymentReceiveService.listPaymentReceives(
tenantId,
filter
);
const { paymentReceives, pagination, filterMeta } =
await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
return res.status(200).send({
payment_receives: paymentReceives,
payment_receives: this.transfromToResponse(paymentReceives),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
@@ -334,14 +331,12 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const {
paymentReceive,
entries,
} = await this.PaymentReceivesPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user
);
const { paymentReceive, entries } =
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user
);
return res.status(200).send({
payment_receive: this.transfromToResponse({ ...paymentReceive }),
@@ -442,9 +437,10 @@ export default class PaymentReceivesController extends BaseController {
type: 'INVOICES_NOT_DELIVERED_YET',
code: 200,
data: {
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
),
not_delivered_invoices_ids:
error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
),
},
},
],

View File

@@ -7,7 +7,12 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import SaleEstimatesPdfService from 'services/Sales/Estimates/SaleEstimatesPdf';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SalesEstimatesController extends BaseController {
@Inject()
@@ -16,6 +21,9 @@ export default class SalesEstimatesController extends BaseController {
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleEstimatesPdf: SaleEstimatesPdfService;
/**
* Router constructor.
*/
@@ -135,7 +143,7 @@ export default class SalesEstimatesController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('search_keyword').optional({ nullable: true }).isString().trim()
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
@@ -292,8 +300,25 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({ estimate });
// Response formatter.
res.format({
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf(
tenantId,
estimate
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
// JSON content type.
default: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
});
} catch (error) {
next(error);
}
@@ -318,10 +343,16 @@ export default class SalesEstimatesController extends BaseController {
const { salesEstimates, pagination, filterMeta } =
await this.saleEstimateService.estimatesList(tenantId, filter);
return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates),
pagination,
filter_meta: this.transfromToResponse(filterMeta),
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(
this.transfromToResponse({
salesEstimates,
pagination,
filterMeta,
})
);
},
});
} catch (error) {
next(error);

View File

@@ -8,7 +8,12 @@ import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import { ISaleInvoiceDTO, ISaleInvoiceCreateDTO } from 'interfaces';
import SaleInvoicePdf from 'services/Sales/SaleInvoicePdf';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SaleInvoicesController extends BaseController {
@Inject()
@@ -20,6 +25,9 @@ export default class SaleInvoicesController extends BaseController {
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleInvoicePdf: SaleInvoicePdf;
/**
* Router constructor.
*/
@@ -254,8 +262,8 @@ export default class SaleInvoicesController extends BaseController {
/**
* Retrieve the sale invoice with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req - Request object.
* @param {Response} res - Response object.
*/
async getSaleInvoice(req: Request, res: Response, next: NextFunction) {
const { id: saleInvoiceId } = req.params;
@@ -267,7 +275,25 @@ export default class SaleInvoicesController extends BaseController {
saleInvoiceId,
user
);
return res.status(200).send({ sale_invoice: saleInvoice });
// Response formatter.
res.format({
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoicePdf.saleInvoicePdf(
tenantId,
saleInvoice
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
},
});
} catch (error) {
next(error);
}
@@ -296,7 +322,7 @@ export default class SaleInvoicesController extends BaseController {
await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({
sales_invoices: salesInvoices,
sales_invoices: this.transfromToResponse(salesInvoices),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});

View File

@@ -3,6 +3,7 @@ import { check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleReceiptService from 'services/Sales/SalesReceipts';
import SaleReceiptsPdfService from 'services/Sales/Receipts/SaleReceiptsPdfService';
import BaseController from '../BaseController';
import { ISaleReceiptDTO } from 'interfaces/SaleReceipt';
import { ServiceError } from 'exceptions';
@@ -13,6 +14,9 @@ export default class SalesReceiptsController extends BaseController {
@Inject()
saleReceiptService: SaleReceiptService;
@Inject()
saleReceiptsPdf: SaleReceiptsPdfService;
@Inject()
dynamicListService: DynamicListingService;
@@ -239,17 +243,13 @@ export default class SalesReceiptsController extends BaseController {
};
try {
const {
salesReceipts,
pagination,
filterMeta,
} = await this.saleReceiptService.salesReceiptsList(tenantId, filter);
const { salesReceipts, pagination, filterMeta } =
await this.saleReceiptService.salesReceiptsList(tenantId, filter);
return res.status(200).send({
sale_receipts: salesReceipts,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
const response = this.transfromToResponse({
salesReceipts, pagination, filterMeta
});
return res.status(200).send(response);
} catch (error) {
next(error);
}
@@ -271,9 +271,22 @@ export default class SalesReceiptsController extends BaseController {
saleReceiptId
);
return res.status(200).send({
sale_receipt: saleReceipt,
});
res.format({
'application/pdf': async () => {
const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf(
tenantId,
saleReceipt
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
'application/json': () => {
return res.status(200).send(this.transfromToResponse({ saleReceipt }));
}
})
} catch (error) {
next(error);
}

View File

@@ -42,12 +42,14 @@ import Subscription from 'api/controllers/Subscription';
import Licenses from 'api/controllers/Subscription/Licenses';
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
import Setup from 'api/controllers/Setup';
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
export default () => {
const app = Router();
// - Global routes.
// ---------------------------
app.use(asyncRenderMiddleware);
app.use(i18n.init);
app.use(I18nMiddleware);

View File

@@ -0,0 +1,23 @@
import { Request, Response } from 'express';
const asyncRender = (app) => (path: string, attributes = {}) =>
new Promise((resolve, reject) => {
app.render(path, attributes, (error, data) => {
if (error) { reject(error); }
resolve(data);
});
});
/**
* Injects `asyncRender` method to response object.
* @param {Request} req Express req Object
* @param {Response} res Express res Object
* @param {NextFunction} next Express next Function
*/
const asyncRenderMiddleware = (req: Request, res: Response, next: Function) => {
res.asyncRender = asyncRender(req.app);
next();
};
export default asyncRenderMiddleware;

View File

@@ -162,6 +162,13 @@ export default {
}
},
/**
* Puppeteer remote browserless connection.
*/
puppeteer: {
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
},
protocol: '',
hostname: '',
scheduleComputeItemCost: 'in 5 seconds'

View File

@@ -0,0 +1,64 @@
import moment from "moment";
import { isEmpty, isObject, isUndefined } from 'lodash';
export class Transformer {
/**
*
* @returns
*/
protected includeAttributes = (): string[] => {
return [];
}
/**
*
*/
public transform = (object: any) => {
if (Array.isArray(object)) {
return object.map(this.getTransformation);
} else if (isObject(object)) {
return this.getTransformation(object);
}
return object;
};
/**
*
* @param item
* @returns
*/
protected getTransformation = (item) => {
const attributes = this.getIncludeAttributesTransformed(item);
return {
...!isUndefined(item.toJSON) ? item.toObject() : item,
...attributes
};
};
/**
*
* @param item
* @returns
*/
protected getIncludeAttributesTransformed = (item) => {
const attributes = this.includeAttributes();
return attributes
.filter((attribute) => !isUndefined(this[attribute]))
.reduce((acc, attribute: string) => {
acc[attribute] = this[attribute](item);
return acc;
}, {});
}
/**
*
* @param date
* @returns
*/
protected formatDate(date) {
return date ? moment(date).format('YYYY/MM/DD') : '';
}
}

View File

@@ -10,11 +10,16 @@ import AgendashController from 'api/controllers/Agendash';
import ConvertEmptyStringsToNull from 'api/middleware/ConvertEmptyStringsToNull';
import RateLimiterMiddleware from 'api/middleware/RateLimiterMiddleware'
import config from 'config';
import path from 'path';
export default ({ app }) => {
// Express configuration.
app.set('port', 3000);
// Template engine configuration.
app.set('views', path.join(__dirname, '../resources/views'));
app.set('view engine', 'pug');
// Helmet helps you secure your Express apps by setting various HTTP headers.
app.use(helmet());

View File

@@ -159,5 +159,32 @@
"Liabilities and Equity": "Liabilities and Equity",
"Closing balance": "Closing balance",
"Opening Balance": "Opening balance",
"Total {{accountName}}": "Total {{accountName}}"
"Total {{accountName}}": "Total {{accountName}}",
"invoice.paper.invoice": "Invoice",
"invoice.paper.billed_to": "Billed to",
"invoice.paper.invoice_date": "Invoice date",
"invoice.paper.invoice_number": "Invoice No.",
"invoice.paper.due_date": "Due date",
"invoice.paper.conditions_title": "Conditions & terms",
"invoice.paper.notes_title": "Notes",
"item_entry.paper.item_name": "Item name",
"item_entry.paper.rate": "Rate",
"item_entry.paper.quantity": "Quantity",
"item_entry.paper.total": "Total",
"estimate.paper.estimate": "Estimate",
"estimate.paper.billed_to": "Billed to",
"estimate.paper.estimate_date": "Estimate date",
"estimate.paper.estimate_number": "Estimate number",
"estimate.paper.expiration_date": "Expiration date",
"estimate.paper.conditions_title": "Conditions & terms",
"estimate.paper.notes_title": "Notes",
"estimate.paper.due_amount": "Due amount",
"receipt.paper.receipt": "Receipt",
"receipt.paper.billed_to": "Billed to",
"receipt.paper.receipt_date": "Receipt date",
"receipt.paper.receipt_number": "Receipt number",
"receipt.paper.expiration_date": "Expiration date",
"receipt.paper.conditions_title": "Conditions & terms",
"receipt.paper.notes_title": "Notes",
"receipt.paper.receipt_amount": "Receipt amount"
}

View File

@@ -1,5 +1,5 @@
import { Model, mixin } from 'objection';
import { snakeCase } from 'lodash';
import { snakeCase, transform } from 'lodash';
import { mapKeysDeep } from 'utils';
import PaginationQueryBuilder from 'models/Pagination';
import DateSession from 'models/DateSession';
@@ -47,4 +47,9 @@ export default class ModelBase extends mixin(Model, [DateSession]) {
static relationBindKnex(model) {
return this.knexBinded ? model.bindKnex(this.knexBinded) : model;
}
toObject(opt) {
const parsedJson = super.$formatJson(this, opt);
return parsedJson;
}
}

View File

@@ -0,0 +1,26 @@
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;
}
}

View File

@@ -0,0 +1,35 @@
import { Service } from 'typedi';
import { IBill, IBillPayment } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class BillPaymentTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return ['formattedPaymentDate', 'formattedAmount'];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedPaymentDate = (billPayment: IBillPayment): string => {
return this.formatDate(billPayment.paymentDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (billPayment: IBillPayment): string => {
return formatNumber(billPayment.amount, {
currencyCode: billPayment.currencyCode,
});
};
}

View File

@@ -28,6 +28,7 @@ import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import VendorsService from 'services/Contacts/VendorsService';
import BillPaymentTransformer from './BillPaymentTransformer';
import { ERRORS } from './constants';
/**
@@ -57,6 +58,9 @@ export default class BillPaymentsService implements IBillPaymentsService {
@Inject('logger')
logger: any;
@Inject()
billPaymentTransformer: BillPaymentTransformer;
/**
* Validate whether the bill payment vendor exists on the storage.
* @param {Request} req
@@ -546,7 +550,7 @@ export default class BillPaymentsService implements IBillPaymentsService {
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
return this.billPaymentTransformer.transform(billPayment);
}
/**
@@ -680,7 +684,7 @@ export default class BillPaymentsService implements IBillPaymentsService {
.pagination(filter.page - 1, filter.pageSize);
return {
billPayments: results,
billPayments: this.billPaymentTransformer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};

View File

@@ -35,6 +35,7 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
import VendorsService from 'services/Contacts/VendorsService';
import { ERRORS } from './constants';
import EntriesService from 'services/Entries';
import PurchaseInvoiceTransfromer from './PurchaseInvoices/PurchaseInvoiceTransformer';
/**
* Vendor bills services.
@@ -78,6 +79,9 @@ export default class BillsService
@Inject()
entriesService: EntriesService;
@Inject()
purchaseInvoiceTransformer: PurchaseInvoiceTransfromer;
/**
* Validates whether the vendor is exist.
* @async
@@ -568,7 +572,7 @@ export default class BillsService
.pagination(filter.page - 1, filter.pageSize);
return {
bills: results,
bills: this.purchaseInvoiceTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
@@ -616,7 +620,7 @@ export default class BillsService
if (!bill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return bill;
return this.purchaseInvoiceTransformer.transform(bill);
}
/**

View File

@@ -0,0 +1,66 @@
import { Service } from 'typedi';
import { IBill } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class PurchaseInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedDueAmount',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
return this.formatDate(bill.billDate);
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* @returns {String}
*/
protected formattedDueDate = (bill: IBill): string => {
return this.formatDate(bill.dueDate);
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedAmount = (bill): string => {
return formatNumber(bill.amount, { currencyCode: bill.currencyCode });
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
return formatNumber(bill.paymentAmount, { currencyCode: bill.currencyCode});
};
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
return formatNumber(bill.dueAmount, { currencyCode: bill.currencyCode });
};
}

View File

@@ -0,0 +1,78 @@
import { Service } from 'typedi';
import { ISaleEstimate } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleEstimateTransfromer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedEstimateDate',
'formattedExpirationDate',
'formattedDeliveredAtDate',
'formattedApprovedAtDate',
'formattedRejectedAtDate',
];
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedEstimateDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.estimateDate);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedExpirationDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.expirationDate);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedDeliveredAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.deliveredAt);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedApprovedAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.approvedAt);
};
/**
* Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice
* @returns {String}
*/
protected formattedRejectedAtDate = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.rejectedAt);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleEstimate} estimate
* @returns {string}
*/
protected formattedAmount = (estimate: ISaleEstimate): string => {
return formatNumber(estimate.amount, {
currencyCode: estimate.currencyCode,
});
};
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleEstimatesPdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleEstimatePdf(tenantId: number, saleEstimate) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const organizationEmail = settings.get({
group: 'organization',
key: 'email',
});
const htmlContent = templateRender('modules/estimate-regular', {
saleEstimate,
organizationName,
organizationEmail,
...i18n,
});
console.log(htmlContent, 'XXX');
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,36 @@
import { Service } from 'typedi';
import { IPaymentReceive } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class PaymentReceiveTransfromer extends Transformer {
/**
* Include these attributes to payment receive object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedPaymentDate',
'formattedAmount',
];
};
/**
* Retrieve formatted payment receive date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected formattedPaymentDate = (payment: IPaymentReceive): string => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieve formatted payment amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedAmount = (payment: IPaymentReceive): string => {
return formatNumber(payment.amount, { currencyCode: payment.currencyCode });
};
}

View File

@@ -34,6 +34,7 @@ import JournalCommands from 'services/Accounting/JournalCommands';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
import { ERRORS } from './constants';
import PaymentReceiveTransfromer from './PaymentReceiveTransformer';
/**
* Payment receive service.
@@ -68,6 +69,9 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
paymentReceiveTransformer: PaymentReceiveTransfromer;
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
@@ -584,7 +588,7 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
}
return paymentReceive;
return this.paymentReceiveTransformer.transform(paymentReceive);
}
/**
@@ -661,7 +665,7 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
);
return {
paymentReceives: results,
paymentReceives: this.paymentReceiveTransformer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};

View File

@@ -0,0 +1,44 @@
import { Service } from 'typedi';
import { ISaleReceipt } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleReceiptTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return ['formattedAmount', 'formattedReceiptDate', 'formattedClosedAtDate'];
};
/**
* Retrieve formatted receipt date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedReceiptDate = (receipt: ISaleReceipt): string => {
return this.formatDate(receipt.receiptDate);
};
/**
* Retrieve formatted estimate closed at date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected formattedClosedAtDate = (receipt: ISaleReceipt): string => {
return this.formatDate(receipt.closedAt);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});
};
}

View File

@@ -0,0 +1,41 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleReceiptsPdf {
@Inject()
pdfService: PdfService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleReceiptPdf(tenantId: number, saleReceipt) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const organizationEmail = settings.get({
group: 'organization',
key: 'email',
});
const htmlContent = templateRender('modules/receipt-regular', {
saleReceipt,
organizationEmail,
organizationName,
...i18n,
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -1,3 +1,13 @@
export const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
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',
};
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
{

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import PdfService from 'services/PDF/PdfService';
import { templateRender } from 'utils';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class SaleInvoicePdf {
@Inject()
pdfService: PdfService
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
*/
async saleInvoicePdf(tenantId: number, saleInvoice) {
const i18n = this.tenancy.i18n(tenantId);
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({ group: 'organization', key: 'name' });
const organizationEmail = settings.get({ group: 'organization', key: 'email' });
const htmlContent = templateRender('modules/invoice-regular', {
organizationName,
organizationEmail,
saleInvoice,
...i18n
});
const pdfContent = await this.pdfService.pdfDocument(htmlContent);
return pdfContent;
}
}

View File

@@ -0,0 +1,60 @@
import { Service } from 'typedi';
import { ISaleInvoice } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class SaleInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedInvoiceDate',
'formattedDueDate',
'formattedAmount',
'formattedDueAmount',
];
};
/**
* Retrieve formatted invoice date.
* @param {ISaleInvoice} invoice
* @returns {String}
*/
protected formattedInvoiceDate = (invoice): string => {
return this.formatDate(invoice.invoiceDate);
};
/**
* Retrieve formatted due date.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueDate = (invoice): string => {
return this.formatDate(invoice.dueDate);
};
/**
* Retrieve formatted invoice amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedAmount = (invoice): string => {
return formatNumber(invoice.balance, {
currencyCode: invoice.currencyCode,
});
};
/**
* Retrieve formatted invoice due amount.
* @param {ISaleInvoice} invoice
* @returns {string}
*/
protected formattedDueAmount(invoice) {
return formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode,
});
}
}

View File

@@ -22,20 +22,8 @@ import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService';
import moment from 'moment';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND',
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
};
import { ERRORS } from './constants';
import SaleEstimateTransfromer from './Estimates/SaleEstimateTransformer';
/**
* Sale estimate service.
@@ -64,6 +52,9 @@ export default class SaleEstimateService implements ISalesEstimatesService{
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
saleEstimateTransformer: SaleEstimateTransfromer;
/**
* Retrieve sale estimate or throw service error.
* @param {number} tenantId
@@ -404,13 +395,13 @@ export default class SaleEstimateService implements ISalesEstimatesService{
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimate = await SaleEstimate.query()
.findById(estimateId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer');
if (!estimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
}
return estimate;
return this.saleEstimateTransformer.transform(estimate);
}
/**
@@ -457,7 +448,7 @@ export default class SaleEstimateService implements ISalesEstimatesService{
.pagination(filter.page - 1, filter.pageSize);
return {
salesEstimates: results,
salesEstimates: this.saleEstimateTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -33,6 +33,7 @@ import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import SaleInvoiceTransfromer from './SaleInvoiceTransformer';
import { ERRORS } from './constants';
/**
@@ -74,6 +75,9 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
saleInvoiceTransformer: SaleInvoiceTransfromer;
/**
* Validate whether sale invoice number unqiue on the storage.
*/
@@ -639,13 +643,13 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer');
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
return saleInvoice;
return this.saleInvoiceTransformer.transform(saleInvoice);
}
/**
@@ -698,7 +702,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
.pagination(filter.page - 1, filter.pageSize);
return {
salesInvoices: results,
salesInvoices: this.saleInvoiceTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -29,16 +29,8 @@ import InventoryService from 'services/Inventory/Inventory';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import CustomersService from 'services/Contacts/CustomersService';
const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
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',
};
import { ERRORS } from './Receipts/constants';
import SaleReceiptTransfromer from './Receipts/SaleReceiptTransformer';
@Service('SalesReceipts')
export default class SalesReceiptService implements ISalesReceiptsService {
@@ -69,6 +61,9 @@ export default class SalesReceiptService implements ISalesReceiptsService {
@Inject()
customersService: CustomersService;
@Inject()
saleReceiptTransformer: SaleReceiptTransfromer;
/**
* Validate whether sale receipt exists on the storage.
* @param {number} tenantId -
@@ -397,14 +392,14 @@ export default class SalesReceiptService implements ISalesReceiptsService {
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries')
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('depositAccount');
if (!saleReceipt) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NOT_FOUND);
}
return saleReceipt;
return this.saleReceiptTransformer.transform(saleReceipt);
}
/**
@@ -456,7 +451,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
.pagination(filter.page - 1, filter.pageSize);
return {
salesReceipts: results,
salesReceipts: this.saleReceiptTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};

View File

@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
import _ from 'lodash';
import path from 'path';
import accounting from 'accounting';
import pug from 'pug';
import Currencies from 'js-money/lib/currency';
import definedOptions from 'data/options';
@@ -378,7 +380,13 @@ const mergeObjectsBykey = (object1, object2, key) => {
return _.values(merged);
}
function templateRender(filePath, options) {
const basePath = path.join(__dirname, '../../resources/views');
return pug.renderFile(`${basePath}/${filePath}.pug`, options);
}
export {
templateRender,
accumSum,
increment,
hashPassword,