From 6d58767e9f5e57b77464bf0f7d29f851adada9d1 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 4 Mar 2021 16:31:21 +0200 Subject: [PATCH] feat: auto-increment invoices. --- server/src/models/SaleInvoice.js | 60 ++++++++++------ .../Sales/AutoIncrementOrdersService.ts | 71 +++++++++++++++++++ server/src/services/Sales/SalesInvoices.ts | 59 +++++++++++++-- server/src/subscribers/SaleInvoices/index.ts | 16 +++-- server/src/utils/index.js | 9 ++- 5 files changed, 183 insertions(+), 32 deletions(-) create mode 100644 server/src/services/Sales/AutoIncrementOrdersService.ts diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 8dd272335..71c89aaf4 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -1,9 +1,7 @@ import { Model, raw } from 'objection'; import moment from 'moment'; +import knex from 'knex'; import TenantModel from 'models/TenantModel'; -import { defaultToTransform } from 'utils'; -import { QueryBuilder } from 'knex'; -import { query } from 'winston'; export default class SaleInvoice extends TenantModel { /** @@ -91,7 +89,9 @@ export default class SaleInvoice extends TenantModel { */ get remainingDays() { // Can't continue in case due date not defined. - if (!this.dueDate) { return null; } + if (!this.dueDate) { + return null; + } const date = moment(); const dueDate = moment(this.dueDate); @@ -112,12 +112,14 @@ export default class SaleInvoice extends TenantModel { } /** - * - * @param {*} asDate + * + * @param {*} asDate */ getOverdueDays(asDate = moment().format('YYYY-MM-DD')) { // Can't continue in case due date not defined. - if (!this.dueDate) { return null; } + if (!this.dueDate) { + return null; + } const date = moment(asDate); const dueDate = moment(this.dueDate); @@ -198,7 +200,7 @@ export default class SaleInvoice extends TenantModel { * Filters the sale invoices from the given date. */ fromDate(query, fromDate) { - query.where('invoice_date', '<=', fromDate) + query.where('invoice_date', '<=', fromDate); }, /** * Sort the sale invoices by full-payment invoices. @@ -210,7 +212,25 @@ export default class SaleInvoice extends TenantModel { * Sort the sale invoices by the due amount. */ sortByDueAmount(query, order) { - query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`) + query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`); + }, + + /** + * Retrieve the max invoice + */ + maxInvoiceNo(query, prefix, number) { + query + .select( + raw(`REPLACE(INVOICE_NO, "${prefix}", "") AS INV_NUMBER`) + ) + .havingRaw('CHAR_LENGTH(INV_NUMBER) = ??', [number.length]) + .orderBy('invNumber', 'DESC') + .limit(1) + .first(); + }, + + byPrefixAndNumber(query, prefix, number) { + query.where('invoice_no', `${prefix}${number}`) } }; } @@ -247,7 +267,7 @@ export default class SaleInvoice extends TenantModel { }, filter(query) { query.where('contact_service', 'Customer'); - } + }, }, transactions: { @@ -255,7 +275,7 @@ export default class SaleInvoice extends TenantModel { modelClass: AccountTransaction.default, join: { from: 'sales_invoices.id', - to: 'accounts_transactions.referenceId' + to: 'accounts_transactions.referenceId', }, filter(builder) { builder.where('reference_type', 'SaleInvoice'); @@ -267,7 +287,7 @@ export default class SaleInvoice extends TenantModel { modelClass: InventoryCostLotTracker.default, join: { from: 'sales_invoices.id', - to: 'inventory_cost_lot_tracker.transactionId' + to: 'inventory_cost_lot_tracker.transactionId', }, filter(builder) { builder.where('transaction_type', 'SaleInvoice'); @@ -287,15 +307,15 @@ export default class SaleInvoice extends TenantModel { /** * Change payment amount. - * @param {Integer} invoiceId - * @param {Numeric} amount + * @param {Integer} invoiceId + * @param {Numeric} amount */ static async changePaymentAmount(invoiceId, amount) { const changeMethod = amount > 0 ? 'increment' : 'decrement'; await this.query() .where('id', invoiceId) - [changeMethod]('payment_amount', Math.abs(amount)); + [changeMethod]('payment_amount', Math.abs(amount)); } /** @@ -369,7 +389,7 @@ export default class SaleInvoice extends TenantModel { fieldType: 'number', sortQuery(query, role) { query.modify('sortByDueAmount', role.order); - } + }, }, created_at: { label: 'Created at', @@ -379,7 +399,7 @@ export default class SaleInvoice extends TenantModel { status: { label: 'Status', options: [ - { key: 'draft', label: 'Draft', }, + { key: 'draft', label: 'Draft' }, { key: 'delivered', label: 'Delivered' }, { key: 'unpaid', label: 'Unpaid' }, { key: 'overdue', label: 'Overdue' }, @@ -387,7 +407,7 @@ export default class SaleInvoice extends TenantModel { { key: 'paid', label: 'Paid' }, ], query: (query, role) => { - switch(role.value) { + switch (role.value) { case 'draft': query.modify('draft'); break; @@ -410,8 +430,8 @@ export default class SaleInvoice extends TenantModel { }, sortQuery(query, role) { query.modify('sortByStatus', role.order); - } - } + }, + }, }; } } diff --git a/server/src/services/Sales/AutoIncrementOrdersService.ts b/server/src/services/Sales/AutoIncrementOrdersService.ts new file mode 100644 index 000000000..373b41563 --- /dev/null +++ b/server/src/services/Sales/AutoIncrementOrdersService.ts @@ -0,0 +1,71 @@ +import { Service, Inject } from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService'; +import { transactionIncrement } from 'utils'; + +/** + * Auto increment orders service. + */ +@Service() +export default class AutoIncrementOrdersService { + @Inject() + tenancy: TenancyService; + + /** + * Retrieve the next service transaction number. + * @param {number} tenantId + * @param {string} settingsGroup + * @param {Function} getMaxTransactionNo + * @return {Promise<[string, string]>} + */ + async getNextTransactionNumber( + tenantId: number, + settingsGroup: string, + getOrderTransaction: (prefix: string, number: string) => Promise, + getMaxTransactionNumber: (prefix: string, number: string) => Promise + ): Promise<[string, string]> { + const settings = this.tenancy.settings(tenantId); + const group = settingsGroup; + + // Settings service transaction number and prefix. + const settingNo = settings.get({ group, key: 'next_number' }); + const settingPrefix = settings.get({ group, key: 'number_prefix' }); + + let nextInvoiceNumber = settingNo; + + const orderTransaction = await getOrderTransaction( + settingPrefix, + settingNo + ); + if (orderTransaction) { + // Retrieve the max invoice number in the given prefix. + const maxInvoiceNo = await getMaxTransactionNumber( + settingPrefix, + settingNo + ); + if (maxInvoiceNo) { + nextInvoiceNumber = transactionIncrement(maxInvoiceNo); + } + } + return [settingPrefix, nextInvoiceNumber]; + } + + /** + * Increment setting next number. + * @param {number} tenantId - + * @param {string} orderGroup - Order group. + * @param {string} orderNumber -Order number. + */ + async incrementSettingsNextNumber( + tenantId, + orderGroup: string, + orderNumber: string + ) { + const settings = this.tenancy.settings(tenantId); + + settings.set( + { group: orderGroup, key: 'next_number' }, + transactionIncrement(orderNumber) + ); + await settings.save(); + } +} diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index b647966cd..b7e249a67 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,5 +1,5 @@ import { Service, Inject } from 'typedi'; -import { omit, sumBy } from 'lodash'; +import { omit, sumBy, join } from 'lodash'; import moment from 'moment'; import { EventDispatcher, @@ -21,14 +21,15 @@ import JournalCommands from 'services/Accounting/JournalCommands'; import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; import TenancyService from 'services/Tenancy/TenancyService'; -import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import CustomersService from 'services/Contacts/CustomersService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import JournalPosterService from './JournalPosterService'; +import AutoIncrementOrdersService from './AutoIncrementOrdersService'; import { ERRORS } from './constants'; /** @@ -67,6 +68,9 @@ export default class SaleInvoicesService { @Inject() journalService: JournalPosterService; + @Inject() + autoIncrementOrdersService: AutoIncrementOrdersService; + /** * Validate whether sale invoice number unqiue on the storage. */ @@ -153,6 +157,33 @@ export default class SaleInvoicesService { return saleInvoice; } + /** + * Retrieve the next unique invoice number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + async getNextInvoiceNumber(tenantId: number): Promise<[string, string]> { + const { SaleInvoice } = this.tenancy.models(tenantId); + + // Retrieve the max invoice number in the given prefix. + const getMaxInvoicesNo = (prefix, number) => { + return SaleInvoice.query() + .modify('maxInvoiceNo', prefix, number) + .then((res) => res?.invNumber); + }; + // Retrieve the order transaction number by number. + const getTransactionNumber = (prefix, number) => { + return SaleInvoice.query().modify('byPrefixAndNumber', prefix, number); + }; + + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_invoices', + getTransactionNumber, + getMaxInvoicesNo + ); + } + /** * Transform DTO object to model object. * @param {number} tenantId - Tenant id. @@ -161,7 +192,8 @@ export default class SaleInvoicesService { transformDTOToModel( tenantId: number, saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, - oldSaleInvoice?: ISaleInvoice + oldSaleInvoice?: ISaleInvoice, + autoNextNumber?: [string, string] // prefix, number ): ISaleInvoice { const { ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, (e) => @@ -180,6 +212,13 @@ export default class SaleInvoicesService { }), balance, paymentAmount: 0, + ...(saleInvoiceDTO.invoiceNo || autoNextNumber + ? { + invoiceNo: saleInvoiceDTO.invoiceNo + ? saleInvoiceDTO.invoiceNo + : join(autoNextNumber, ''), + } + : {}), entries: saleInvoiceDTO.entries.map((entry) => ({ referenceType: 'SaleInvoice', ...omit(entry, ['amount', 'id']), @@ -202,9 +241,18 @@ export default class SaleInvoicesService { ): Promise { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); - // Transform DTO object to model object. - const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO); + // The next invoice number automattically or manually. + const autoNextNumber = !saleInvoiceDTO.invoiceNo + ? await this.getNextInvoiceNumber(tenantId) + : null; + // Transform DTO object to model object. + const saleInvoiceObj = this.transformDTOToModel( + tenantId, + saleInvoiceDTO, + null, + autoNextNumber + ); // Validate customer existance. await this.customersService.getCustomerByIdOrThrowError( tenantId, @@ -248,6 +296,7 @@ export default class SaleInvoicesService { saleInvoiceDTO, saleInvoiceId: saleInvoice.id, authorizedUser, + autoNextNumber, }); this.logger.info('[sale_invoice] successfully inserted.', { tenantId, diff --git a/server/src/subscribers/SaleInvoices/index.ts b/server/src/subscribers/SaleInvoices/index.ts index 72e93d270..7fb0c7bb6 100644 --- a/server/src/subscribers/SaleInvoices/index.ts +++ b/server/src/subscribers/SaleInvoices/index.ts @@ -4,6 +4,7 @@ import events from 'subscribers/events'; import TenancyService from 'services/Tenancy/TenancyService'; import SettingsService from 'services/Settings/SettingsService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; +import SaleInvoicesService from 'services/Sales/SalesInvoices'; @EventSubscriber() export default class SaleInvoiceSubscriber { @@ -11,6 +12,7 @@ export default class SaleInvoiceSubscriber { tenancy: TenancyService; settingsService: SettingsService; saleEstimatesService: SaleEstimateService; + saleInvoicesService: SaleInvoicesService; /** * Constructor method. @@ -20,6 +22,7 @@ export default class SaleInvoiceSubscriber { this.tenancy = Container.get(TenancyService); this.settingsService = Container.get(SettingsService); this.saleEstimatesService = Container.get(SaleEstimateService); + this.saleInvoicesService = Container.get(SaleInvoicesService); } /** @@ -49,10 +52,15 @@ export default class SaleInvoiceSubscriber { tenantId, saleInvoiceId, saleInvoice, + saleInvoiceDTO, + autoNextNumber, }) { - await this.settingsService.incrementNextNumber(tenantId, { - key: 'next_number', - group: 'sales_invoices', - }); + if (saleInvoiceDTO.invoiceNo || !autoNextNumber) return; + + await this.saleInvoicesService.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_invoices', + autoNextNumber[1] + ); } } diff --git a/server/src/utils/index.js b/server/src/utils/index.js index 09d648265..3d90c9814 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.js @@ -281,11 +281,13 @@ function defaultToTransform(value, defaultOrTransformedValue, defaultValue) { const transformToMap = (objects, key) => { const map = new Map(); - objects.forEach(object => { + objects.forEach((object) => { map.set(object[key], object); }); return map; -} +}; + +const transactionIncrement = (s) => s.replace(/([0-8]|\d?9+)?$/, (e) => ++e); export { hashPassword, @@ -308,5 +310,6 @@ export { formatNumber, isBlank, defaultToTransform, - transformToMap + transformToMap, + transactionIncrement, };