diff --git a/server/src/api/controllers/AccountTypes.ts b/server/src/api/controllers/AccountTypes.ts index 9e832162d..3c5bf3d35 100644 --- a/server/src/api/controllers/AccountTypes.ts +++ b/server/src/api/controllers/AccountTypes.ts @@ -1,4 +1,4 @@ -import { Service, Inject } from 'typedi'; + import { Service, Inject } from 'typedi'; import { Request, Response, Router, NextFunction } from 'express'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BaseController from 'api/controllers/BaseController'; diff --git a/server/src/api/controllers/Currencies.ts b/server/src/api/controllers/Currencies.ts index e7fc42e24..1c8417936 100644 --- a/server/src/api/controllers/Currencies.ts +++ b/server/src/api/controllers/Currencies.ts @@ -17,33 +17,32 @@ export default class CurrenciesController extends BaseController { router() { const router = Router(); - router.get('/', [ - ...this.listSchema, - ], + router.get( + '/', + [...this.listSchema], this.validationResult, asyncMiddleware(this.all.bind(this)) ); - router.post('/', [ - ...this.currencyDTOSchemaValidation, - ], + router.post( + '/', + [...this.currencyDTOSchemaValidation], this.validationResult, asyncMiddleware(this.newCurrency.bind(this)), - this.handlerServiceError, + this.handlerServiceError ); - router.post('/:id', [ - ...this.currencyIdParamSchema, - ...this.currencyEditDTOSchemaValidation - ], + router.post( + '/:id', + [...this.currencyIdParamSchema, ...this.currencyEditDTOSchemaValidation], this.validationResult, asyncMiddleware(this.editCurrency.bind(this)), - this.handlerServiceError, + this.handlerServiceError ); - router.delete('/:currency_code', [ - ...this.currencyParamSchema, - ], + router.delete( + '/:currency_code', + [...this.currencyParamSchema], this.validationResult, asyncMiddleware(this.deleteCurrency.bind(this)), - this.handlerServiceError, + this.handlerServiceError ); return router; } @@ -56,21 +55,15 @@ export default class CurrenciesController extends BaseController { } get currencyEditDTOSchemaValidation(): ValidationChain[] { - return [ - check('currency_name').exists().trim().escape(), - ]; + return [check('currency_name').exists().trim().escape()]; } get currencyIdParamSchema(): ValidationChain[] { - return [ - param('id').exists().isNumeric().toInt(), - ]; + return [param('id').exists().isNumeric().toInt()]; } - + get currencyParamSchema(): ValidationChain[] { - return [ - param('currency_code').exists().trim().escape(), - ]; + return [param('currency_code').exists().trim().escape()]; } get listSchema(): ValidationChain[] { @@ -82,16 +75,16 @@ export default class CurrenciesController extends BaseController { /** * Retrieve all registered currency details. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async all(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; try { const currencies = await this.currenciesService.listCurrencies(tenantId); - return res.status(200).send({ currencies: [ ...currencies, ] }); + return res.status(200).send({ currencies: [...currencies] }); } catch (error) { next(error); } @@ -99,9 +92,9 @@ export default class CurrenciesController extends BaseController { /** * Creates a new currency on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async newCurrency(req: Request, res: Response, next: Function) { const { tenantId } = req; @@ -121,13 +114,13 @@ export default class CurrenciesController extends BaseController { /** * Edits details of the given currency. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async deleteCurrency(req: Request, res: Response, next: Function) { const { tenantId } = req; - const { currency_code: currencyCode } = req.params; + const { currency_code: currencyCode } = req.params; try { await this.currenciesService.deleteCurrency(tenantId, currencyCode); @@ -142,9 +135,9 @@ export default class CurrenciesController extends BaseController { /** * Deletes the currency. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async editCurrency(req: Request, res: Response, next: Function) { const { tenantId } = req; @@ -152,7 +145,11 @@ export default class CurrenciesController extends BaseController { const { body: editCurrencyDTO } = req; try { - const currency = await this.currenciesService.editCurrency(tenantId, currencyId, editCurrencyDTO); + const currency = await this.currenciesService.editCurrency( + tenantId, + currencyId, + editCurrencyDTO + ); return res.status(200).send({ currency_code: currency.currencyCode, message: 'The currency has been edited successfully.', @@ -164,24 +161,29 @@ export default class CurrenciesController extends BaseController { /** * Handles currencies service error. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) { + handlerServiceError( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'currency_not_found') { return res.boom.badRequest(null, { - errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100, }], + errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100 }], }); } if (error.errorType === 'currency_code_exists') { return res.boom.badRequest(null, { - errors: [{ type: 'CURRENCY_CODE_EXISTS', code: 200, }], + errors: [{ type: 'CURRENCY_CODE_EXISTS', code: 200 }], }); } } next(error); } -}; +} diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index b0150bdcb..271607148 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -444,6 +444,11 @@ export default class PaymentReceivesController extends BaseController { ], }); } + if (error.errorType === 'PAYMENT_RECEIVE_NO_IS_REQUIRED') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', code: 1100 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index b6d03b654..164f5c945 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -101,7 +101,7 @@ export default class SalesEstimatesController extends BaseController { check('estimate_date').exists().isISO8601(), check('expiration_date').optional().isISO8601(), check('reference').optional(), - check('estimate_number').exists().trim().escape(), + check('estimate_number').optional().trim().escape(), check('delivered').default(false).isBoolean().toBoolean(), check('entries').exists().isArray({ min: 1 }), @@ -401,6 +401,11 @@ export default class SalesEstimatesController extends BaseController { errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 1300 }], }); } + if (error.errorType === 'SALE_ESTIMATE_NO_IS_REQUIRED') { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE_ESTIMATE_NO_IS_REQUIRED', code: 1400 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 06fd7d010..81c09eadd 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -432,6 +432,13 @@ export default class SaleInvoicesController extends BaseController { ], }); } + if (error.errorType === 'SALE_INVOICE_NO_IS_REQUIRED') { + return res.boom.badRequest(null, { + errors: [ + { type: 'SALE_INVOICE_NO_IS_REQUIRED', code: 1500 }, + ], + }); + } } next(error); } diff --git a/server/src/interfaces/SaleEstimate.ts b/server/src/interfaces/SaleEstimate.ts index 231c7ba59..8cc086e43 100644 --- a/server/src/interfaces/SaleEstimate.ts +++ b/server/src/interfaces/SaleEstimate.ts @@ -6,6 +6,7 @@ export interface ISaleEstimate { amount: number, customerId: number, estimateDate: Date, + estimateNumber: string, reference: string, note: string, termsConditions: string, @@ -19,8 +20,8 @@ export interface ISaleEstimate { export interface ISaleEstimateDTO { customerId: number, estimateDate?: Date, - reference: string, - estimateNumber: string, + reference?: string, + estimateNumber?: string, entries: IItemEntry[], note: string, termsConditions: string, diff --git a/server/src/interfaces/SaleReceipt.ts b/server/src/interfaces/SaleReceipt.ts index fde3f1a90..687441291 100644 --- a/server/src/interfaces/SaleReceipt.ts +++ b/server/src/interfaces/SaleReceipt.ts @@ -22,7 +22,8 @@ export interface ISaleReceiptDTO { depositAccountId: number; receiptDate: Date; sendToEmail: string; - referenceNo: string; + referenceNo?: string; + receiptNumber?: string, receiptMessage: string; statement: string; closed: boolean; diff --git a/server/src/lib/Metable/MetableStore.ts b/server/src/lib/Metable/MetableStore.ts index ccf81a14d..e2809b054 100644 --- a/server/src/lib/Metable/MetableStore.ts +++ b/server/src/lib/Metable/MetableStore.ts @@ -1,13 +1,9 @@ import { Model } from 'objection'; import { omit, isEmpty } from 'lodash'; -import { - IMetadata, - IMetaQuery, - IMetableStore, -} from 'interfaces'; +import { IMetadata, IMetaQuery, IMetableStore } from 'interfaces'; import { itemsStartWith } from 'utils'; -export default class MetableStore implements IMetableStore{ +export default class MetableStore implements IMetableStore { metadata: IMetadata[]; model: Model; extraColumns: string[]; @@ -31,18 +27,19 @@ export default class MetableStore implements IMetableStore{ /** * Find the given metadata key. - * @param {string|IMetaQuery} query - - * @returns {IMetadata} - Metadata object. + * @param {string|IMetaQuery} query - + * @returns {IMetadata} - Metadata object. */ - find(query: string|IMetaQuery): IMetadata { + find(query: string | IMetaQuery): IMetadata { const { key, value, ...extraColumns } = this.parseQuery(query); return this.metadata.find((meta: IMetadata) => { const isSameKey = meta.key === key; - const sameExtraColumns = this.extraColumns - .some((extraColumn: string) => extraColumns[extraColumn] === meta[extraColumn]); + const sameExtraColumns = this.extraColumns.some( + (extraColumn: string) => extraColumns[extraColumn] === meta[extraColumn] + ); - const isSameExtraColumns = (sameExtraColumns || isEmpty(extraColumns)); + const isSameExtraColumns = sameExtraColumns || isEmpty(extraColumns); return isSameKey && isSameExtraColumns; }); @@ -55,10 +52,9 @@ export default class MetableStore implements IMetableStore{ all(): IMetadata[] { return this.metadata .filter((meta: IMetadata) => !meta._markAsDeleted) - .map((meta: IMetadata) => omit( - meta, - itemsStartWith(Object.keys(meta), '_') - )); + .map((meta: IMetadata) => + omit(meta, itemsStartWith(Object.keys(meta), '_')) + ); } /** @@ -66,16 +62,20 @@ export default class MetableStore implements IMetableStore{ * @param {String} key - * @param {Mixied} defaultValue - */ - get(query: string|IMetaQuery, defaultValue: any): any|false { + get(query: string | IMetaQuery, defaultValue: any): any | false { const metadata = this.find(query); - return metadata ? metadata.value : defaultValue || false; + return metadata + ? metadata.value + : typeof defaultValue !== 'undefined' + ? defaultValue + : false; } /** * Markes the metadata to should be deleted. * @param {String} key - */ - remove(query: string|IMetaQuery): void { + remove(query: string | IMetaQuery): void { const metadata: IMetadata = this.find(query); if (metadata) { @@ -99,7 +99,7 @@ export default class MetableStore implements IMetableStore{ * @param {String} key - * @param {String} value - */ - set(query: IMetaQuery|IMetadata[]|string, metaValue?: any): void { + set(query: IMetaQuery | IMetadata[] | string, metaValue?: any): void { if (Array.isArray(query)) { const metadata = query; @@ -127,10 +127,10 @@ export default class MetableStore implements IMetableStore{ /** * Parses query query. - * @param query - * @param value + * @param query + * @param value */ - parseQuery(query: string|IMetaQuery): IMetaQuery { + parseQuery(query: string | IMetaQuery): IMetaQuery { return typeof query !== 'object' ? { key: query } : { ...query }; } @@ -141,9 +141,9 @@ export default class MetableStore implements IMetableStore{ * @return {string|number|boolean} - */ static formatMetaValue( - value: string|boolean|number, + value: string | boolean | number, valueType: string - ) : string|number|boolean { + ): string | number | boolean { let parsedValue; switch (valueType) { @@ -168,7 +168,9 @@ export default class MetableStore implements IMetableStore{ * @param {Array} collection - */ mapMetadataToCollection(metadata: IMetadata[], parseType: string = 'parse') { - return metadata.map((model) => this.mapMetadataToCollection(model, parseType)); + return metadata.map((model) => + this.mapMetadataToCollection(model, parseType) + ); } /** @@ -177,14 +179,16 @@ export default class MetableStore implements IMetableStore{ */ from(meta: []) { if (Array.isArray(meta)) { - meta.forEach((m) => { this.from(m); }); + meta.forEach((m) => { + this.from(m); + }); return; } this.metadata.push(meta); } /** - * + * * @returns {array} */ toArray(): IMetadata[] { @@ -193,7 +197,7 @@ export default class MetableStore implements IMetableStore{ /** * Static method to load metadata to the collection. - * @param {Array} meta + * @param {Array} meta */ static from(meta) { const collection = new MetableCollection(); diff --git a/server/src/services/Sales/AutoIncrementOrdersService.ts b/server/src/services/Sales/AutoIncrementOrdersService.ts index 373b41563..5ae86f8a9 100644 --- a/server/src/services/Sales/AutoIncrementOrdersService.ts +++ b/server/src/services/Sales/AutoIncrementOrdersService.ts @@ -1,6 +1,6 @@ import { Service, Inject } from 'typedi'; import TenancyService from 'services/Tenancy/TenancyService'; -import { transactionIncrement } from 'utils'; +import { transactionIncrement, parseBoolean } from 'utils'; /** * Auto increment orders service. @@ -15,38 +15,18 @@ export default class AutoIncrementOrdersService { * @param {number} tenantId * @param {string} settingsGroup * @param {Function} getMaxTransactionNo - * @return {Promise<[string, string]>} + * @return {Promise} */ - async getNextTransactionNumber( - tenantId: number, - settingsGroup: string, - getOrderTransaction: (prefix: string, number: string) => Promise, - getMaxTransactionNumber: (prefix: string, number: string) => Promise - ): Promise<[string, string]> { + getNextTransactionNumber(tenantId: number, settingsGroup: 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' }); + const autoIncrement = settings.get({ group, key: 'auto_increment' }, false); + 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]; + return parseBoolean(autoIncrement, false) ? `${settingPrefix}${settingNo}` : ''; } /** @@ -55,16 +35,17 @@ export default class AutoIncrementOrdersService { * @param {string} orderGroup - Order group. * @param {string} orderNumber -Order number. */ - async incrementSettingsNextNumber( - tenantId, - orderGroup: string, - orderNumber: string - ) { + async incrementSettingsNextNumber(tenantId: number, group: string) { const settings = this.tenancy.settings(tenantId); + const settingNo = settings.get({ group, key: 'next_number' }); + const autoIncrement = settings.get({ group, key: 'auto_increment' }); + + // Can't continue if the auto-increment of the service was disabled. + if (!autoIncrement) return; settings.set( - { group: orderGroup, key: 'next_number' }, - transactionIncrement(orderNumber) + { group, key: 'next_number' }, + transactionIncrement(settingNo) ); await settings.save(); } diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 6755d4a60..29ee4f8ae 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -31,6 +31,7 @@ import CustomersService from 'services/Contacts/CustomersService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import JournalCommands from 'services/Accounting/JournalCommands'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; +import AutoIncrementOrdersService from './AutoIncrementOrdersService'; const ERRORS = { PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', @@ -41,6 +42,7 @@ const ERRORS = { INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND', ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS', INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET', + PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED' }; /** * Payment receive service. @@ -66,6 +68,9 @@ export default class PaymentReceiveService { @Inject() dynamicListService: DynamicListingService; + @Inject() + autoIncrementOrdersService: AutoIncrementOrdersService; + @Inject('logger') logger: any; @@ -144,7 +149,8 @@ export default class PaymentReceiveService { /** * Validates the invoices IDs existance. * @param {number} tenantId - - * @param {} paymentReceiveEntries - + * @param {number} customerId - + * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries - */ async validateInvoicesIDsExistance( tenantId: number, @@ -225,6 +231,61 @@ export default class PaymentReceiveService { } } + /** + * Retrieve the next unique payment receive number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + getNextPaymentReceiveNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'payment_receives' + ); + } + + /** + * Increment the payment receive next number. + * @param {number} tenantId + */ + incrementNextPaymentReceiveNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'payment_receives' + ); + } + + /** + * Validate the payment receive number require. + * @param {IPaymentReceive} paymentReceiveObj + */ + validatePaymentReceiveNoRequire(paymentReceiveObj: IPaymentReceive) { + if (!paymentReceiveObj.paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_IS_REQUIRED); + } + } + + /** + * Retrieve estimate number to object model. + * @param {number} tenantId + * @param {IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO. + * @param {IPaymentReceive} oldPaymentReceive - Old payment model object. + */ + transformPaymentNumberToModel( + tenantId: number, + paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, + oldPaymentReceive?: IPaymentReceive + ): string { + // Retreive the next invoice number. + const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId); + + if (paymentReceiveDTO.paymentReceiveNo) { + return paymentReceiveDTO.paymentReceiveNo; + } + return oldPaymentReceive + ? oldPaymentReceive.paymentReceiveNo + : autoNextNumber; + } + /** * Validate the payment receive entries IDs existance. * @param {number} tenantId @@ -246,7 +307,6 @@ export default class PaymentReceiveService { 'payment_receive_id', paymentReceiveId ); - const storedEntriesIds = storedEntries.map((entry: any) => entry.id); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); @@ -255,6 +315,36 @@ export default class PaymentReceiveService { } } + /** + * Transformes the create payment receive DTO to model object. + * @param {number} tenantId + * @param {IPaymentReceiveCreateDTO} paymentReceiveDTO + */ + transformPaymentReceiveDTOToModel( + tenantId: number, + paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, + oldPaymentReceive?: IPaymentReceive + ): IPaymentReceive { + const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + + // Retrieve the next payment receive number. + const paymentReceiveNo = this.transformPaymentNumberToModel( + tenantId, + paymentReceiveDTO, + oldPaymentReceive + ); + return { + amount: paymentAmount, + ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ + 'paymentDate', + ]), + ...(paymentReceiveNo ? { paymentReceiveNo } : {}), + entries: paymentReceiveDTO.entries.map((entry) => ({ + ...omit(entry, ['id']), + })), + }; + } + /** * Creates a new payment receive and store it to the storage * with associated invoices payment and journal transactions. @@ -268,34 +358,36 @@ export default class PaymentReceiveService { authorizedUser: ISystemUser ) { const { PaymentReceive } = this.tenancy.models(tenantId); - const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + + // Transformes the payment receive DTO to model. + const paymentReceiveObj = this.transformPaymentReceiveDTOToModel( + tenantId, + paymentReceiveDTO + ); + // Validate payment receive is required. + this.validatePaymentReceiveNoRequire(paymentReceiveObj); // Validate payment receive number uniquiness. - if (paymentReceiveDTO.paymentReceiveNo) { - await this.validatePaymentReceiveNoExistance( - tenantId, - paymentReceiveDTO.paymentReceiveNo - ); - } + await this.validatePaymentReceiveNoExistance( + tenantId, + paymentReceiveObj.paymentReceiveNo + ); // Validate customer existance. await this.customersService.getCustomerByIdOrThrowError( tenantId, paymentReceiveDTO.customerId ); - // Validate the deposit account existance and type. await this.getDepositAccountOrThrowError( tenantId, paymentReceiveDTO.depositAccountId ); - // Validate payment receive invoices IDs existance. await this.validateInvoicesIDsExistance( tenantId, paymentReceiveDTO.customerId, paymentReceiveDTO.entries ); - // Validate invoice payment amount. await this.validateInvoicesPaymentsAmount( tenantId, @@ -304,15 +396,9 @@ export default class PaymentReceiveService { this.logger.info('[payment_receive] inserting to the storage.'); const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({ - amount: paymentAmount, - ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ - 'paymentDate', - ]), - entries: paymentReceiveDTO.entries.map((entry) => ({ - ...omit(entry, ['id']), - })), + ...paymentReceiveObj, }); - + // Triggers `onPaymentReceiveCreated` event. await this.eventDispatcher.dispatch(events.paymentReceive.onCreated, { tenantId, paymentReceive, @@ -349,19 +435,26 @@ export default class PaymentReceiveService { authorizedUser: ISystemUser ) { const { PaymentReceive } = this.tenancy.models(tenantId); - const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); this.logger.info('[payment_receive] trying to edit payment receive.', { tenantId, paymentReceiveId, paymentReceiveDTO, }); - // Validate the payment receive existance. const oldPaymentReceive = await this.getPaymentReceiveOrThrowError( tenantId, paymentReceiveId ); + // Transformes the payment receive DTO to model. + const paymentReceiveObj = this.transformPaymentReceiveDTOToModel( + tenantId, + paymentReceiveDTO, + oldPaymentReceive + ); + // Validate payment receive number existance. + this.validatePaymentReceiveNoRequire(paymentReceiveObj); + // Validate payment receive number uniquiness. if (paymentReceiveDTO.paymentReceiveNo) { await this.validatePaymentReceiveNoExistance( @@ -375,7 +468,6 @@ export default class PaymentReceiveService { tenantId, paymentReceiveDTO.depositAccountId ); - // Validate the entries ids existance on payment receive type. await this.validateEntriesIdsExistance( tenantId, @@ -397,11 +489,7 @@ export default class PaymentReceiveService { // Update the payment receive transaction. const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({ id: paymentReceiveId, - amount: paymentAmount, - ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ - 'paymentDate', - ]), - entries: paymentReceiveDTO.entries, + ...paymentReceiveObj, }); await this.eventDispatcher.dispatch(events.paymentReceive.onEdited, { diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index 561edf2cd..b23edef53 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -19,6 +19,7 @@ import events from 'subscribers/events'; 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', @@ -30,6 +31,7 @@ const ERRORS = { 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' }; /** @@ -56,6 +58,9 @@ export default class SaleEstimateService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; + @Inject() + autoIncrementOrdersService: AutoIncrementOrdersService; + /** * Retrieve sale estimate or throw service error. * @param {number} tenantId @@ -100,7 +105,7 @@ export default class SaleEstimateService { /** * Validates the given sale estimate not already converted to invoice. - * @param {ISaleEstimate} saleEstimate - + * @param {ISaleEstimate} saleEstimate - */ validateEstimateNotConverted(saleEstimate: ISaleEstimate) { if (saleEstimate.isConvertedToInvoice) { @@ -108,6 +113,49 @@ export default class SaleEstimateService { } } + /** + * Retrieve the next unique estimate number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + getNextEstimateNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_estimates' + ); + } + + /** + * Increment the estimate next number. + * @param {number} tenantId - + */ + incrementNextEstimateNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_estimates' + ); + } + + /** + * Retrieve estimate number to object model. + * @param {number} tenantId + * @param {ISaleEstimateDTO} saleEstimateDTO + * @param {ISaleEstimate} oldSaleEstimate + */ + transformEstimateNumberToModel( + tenantId: number, + saleEstimateDTO: ISaleEstimateDTO, + oldSaleEstimate?: ISaleEstimate + ): string { + // Retreive the next invoice number. + const autoNextNumber = this.getNextEstimateNumber(tenantId); + + if (saleEstimateDTO.estimateNumber) { + return saleEstimateDTO.estimateNumber; + } + return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber; + } + /** * Transform DTO object ot model object. * @param {number} tenantId @@ -123,17 +171,24 @@ export default class SaleEstimateService { const { ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); + // Retreive the next estimate number. + const estimateNumber = this.transformEstimateNumberToModel( + tenantId, + estimateDTO, + oldSaleEstimate + ); + return { amount, ...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [ 'estimateDate', 'expirationDate', ]), + ...(estimateNumber ? { estimateNumber } : {}), entries: estimateDTO.entries.map((entry) => ({ reference_type: 'SaleEstimate', ...omit(entry, ['total', 'amount', 'id']), })), - // Avoid rewrite the deliver date in edit mode when already published. ...(estimateDTO.delivered && !oldSaleEstimate?.deliveredAt && { @@ -141,6 +196,16 @@ export default class SaleEstimateService { }), }; } + + /** + * Validate the sale estimate number require. + * @param {ISaleEstimate} saleInvoiceObj + */ + validateEstimateNoRequire(saleInvoiceObj: ISaleEstimate) { + if (!saleInvoiceObj.estimateNumber) { + throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED); + } + } /** * Creates a new estimate with associated entries. @@ -160,11 +225,14 @@ export default class SaleEstimateService { // Transform DTO object ot model object. const estimateObj = this.transformDTOToModel(tenantId, estimateDTO); + // Validate the sale estimate number require. + this.validateEstimateNoRequire(estimateObj); + // Validate estimate number uniquiness on the storage. - if (estimateDTO.estimateNumber) { + if (estimateObj.estimateNumber) { await this.validateEstimateNumberExistance( tenantId, - estimateDTO.estimateNumber + estimateObj.estimateNumber ); } // Retrieve the given customer or throw not found service error. @@ -221,6 +289,9 @@ export default class SaleEstimateService { estimateDTO, oldSaleEstimate ); + // Validate the sale estimate number require. + this.validateEstimateNoRequire(estimateObj); + // Validate estimate number uniquiness on the storage. if (estimateDTO.estimateNumber) { await this.validateEstimateNumberExistance( diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index b7e249a67..3e68b2e17 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -162,28 +162,44 @@ export default class SaleInvoicesService { * @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); - }; - + getNextInvoiceNumber(tenantId: number): string { return this.autoIncrementOrdersService.getNextTransactionNumber( tenantId, - 'sales_invoices', - getTransactionNumber, - getMaxInvoicesNo + 'sales_invoices' ); } + /** + * Increment the invoice next number. + * @param {number} tenantId - + */ + incrementNextInvoiceNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_invoices' + ); + } + + /** + * Retrieve invoice number to object model. + * @param tenantId + * @param saleInvoiceDTO + * @param oldSaleInvoice + */ + transformInvoiceNumberToModel( + tenantId: number, + saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, + oldSaleInvoice?: ISaleInvoice + ): string { + // Retreive the next invoice number. + const autoNextNumber = this.getNextInvoiceNumber(tenantId); + + if (saleInvoiceDTO.invoiceNo) { + return saleInvoiceDTO.invoiceNo; + } + return oldSaleInvoice ? oldSaleInvoice.invoiceNo : autoNextNumber; + } + /** * Transform DTO object to model object. * @param {number} tenantId - Tenant id. @@ -192,33 +208,37 @@ export default class SaleInvoicesService { transformDTOToModel( tenantId: number, saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, - oldSaleInvoice?: ISaleInvoice, - autoNextNumber?: [string, string] // prefix, number + oldSaleInvoice?: ISaleInvoice ): ISaleInvoice { const { ItemEntry } = this.tenancy.models(tenantId); + const balance = sumBy(saleInvoiceDTO.entries, (e) => ItemEntry.calcAmount(e) ); + const invoiceNo = this.transformInvoiceNumberToModel( + tenantId, + saleInvoiceDTO, + oldSaleInvoice + ); return { ...formatDateFields( omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), ['invoiceDate', 'dueDate'] ), // Avoid rewrite the deliver date in edit mode when already published. + balance, ...(saleInvoiceDTO.delivered && !oldSaleInvoice?.deliveredAt && { deliveredAt: moment().toMySqlDateTime(), }), - balance, - paymentAmount: 0, - ...(saleInvoiceDTO.invoiceNo || autoNextNumber + // Avoid add payment amount in edit mode. + ...(!oldSaleInvoice ? { - invoiceNo: saleInvoiceDTO.invoiceNo - ? saleInvoiceDTO.invoiceNo - : join(autoNextNumber, ''), + paymentAmount: 0, } : {}), + ...(invoiceNo ? { invoiceNo } : {}), entries: saleInvoiceDTO.entries.map((entry) => ({ referenceType: 'SaleInvoice', ...omit(entry, ['amount', 'id']), @@ -226,6 +246,16 @@ export default class SaleInvoicesService { }; } + /** + * Validate the invoice number require. + * @param {ISaleInvoice} saleInvoiceObj + */ + validateInvoiceNoRequire(saleInvoiceObj: ISaleInvoice) { + if (!saleInvoiceObj.invoiceNo) { + throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); + } + } + /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. @@ -241,28 +271,25 @@ export default class SaleInvoicesService { ): Promise { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); - // 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 + null ); + this.validateInvoiceNoRequire(saleInvoiceObj); + // Validate customer existance. await this.customersService.getCustomerByIdOrThrowError( tenantId, saleInvoiceDTO.customerId ); + // Validate sale invoice number uniquiness. - if (saleInvoiceDTO.invoiceNo) { + if (saleInvoiceObj.invoiceNo) { await this.validateInvoiceNumberUnique( tenantId, - saleInvoiceDTO.invoiceNo + saleInvoiceObj.invoiceNo ); } // Validate the from estimate id exists on the storage. @@ -296,7 +323,6 @@ export default class SaleInvoicesService { saleInvoiceDTO, saleInvoiceId: saleInvoice.id, authorizedUser, - autoNextNumber, }); this.logger.info('[sale_invoice] successfully inserted.', { tenantId, @@ -317,11 +343,12 @@ export default class SaleInvoicesService { public async editSaleInvoice( tenantId: number, saleInvoiceId: number, - saleInvoiceDTO: any, + saleInvoiceDTO: ISaleInvoiceEditDTO, authorizedUser: ISystemUser ): Promise { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); + // Retrieve the sale invoice or throw not found service error. const oldSaleInvoice = await this.getInvoiceOrThrowError( tenantId, saleInvoiceId diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index d412f3a8e..4d6e1ddbd 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -19,6 +19,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import { ItemEntry } from 'models'; import InventoryService from 'services/Inventory/Inventory'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; +import AutoIncrementOrdersService from './AutoIncrementOrdersService'; const ERRORS = { SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND', @@ -26,6 +27,7 @@ const ERRORS = { 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' }; @Service() @@ -51,6 +53,9 @@ export default class SalesReceiptService { @Inject('logger') logger: any; + @Inject() + autoIncrementOrdersService: AutoIncrementOrdersService; + /** * Validate whether sale receipt exists on the storage. * @param {number} tenantId - @@ -130,6 +135,59 @@ export default class SalesReceiptService { } } + /** + * Validate the sale receipt number require. + * @param {ISaleReceipt} saleReceipt + */ + validateReceiptNoRequire(saleReceipt: ISaleReceipt) { + if (!saleReceipt.receiptNumber) { + throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED); + } + } + + /** + * Retrieve the next unique receipt number. + * @param {number} tenantId - Tenant id. + * @return {string} + */ + getNextReceiptNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'sales_receipts' + ); + } + + /** + * Increment the receipt next number. + * @param {number} tenantId - + */ + incrementNextReceiptNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'sales_receipts' + ); + } + + /** + * Retrieve estimate number to object model. + * @param {number} tenantId + * @param {ISaleReceiptDTO} saleReceiptDTO - Sale receipt DTO. + * @param {ISaleReceipt} oldSaleReceipt - Old receipt model object. + */ + transformReceiptNumberToModel( + tenantId: number, + saleReceiptDTO: ISaleReceiptDTO, + oldSaleReceipt?: ISaleReceipt + ): string { + // Retreive the next invoice number. + const autoNextNumber = this.getNextReceiptNumber(tenantId); + + if (saleReceiptDTO.receiptNumber) { + return saleReceiptDTO.receiptNumber; + } + return oldSaleReceipt ? oldSaleReceipt.receiptNumber : autoNextNumber; + } + /** * Transform DTO object to model object. * @param {ISaleReceiptDTO} saleReceiptDTO - @@ -137,18 +195,26 @@ export default class SalesReceiptService { * @returns {ISaleReceipt} */ transformObjectDTOToModel( + tenantId: number, saleReceiptDTO: ISaleReceiptDTO, oldSaleReceipt?: ISaleReceipt ): ISaleReceipt { const amount = sumBy(saleReceiptDTO.entries, (e) => ItemEntry.calcAmount(e) ); + // Retreive the receipt number. + const receiptNumber = this.transformReceiptNumberToModel( + tenantId, + saleReceiptDTO, + oldSaleReceipt + ); return { amount, ...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [ 'receiptDate', ]), + ...(receiptNumber ? { receiptNumber } : {}), // Avoid rewrite the deliver date in edit mode when already published. ...(saleReceiptDTO.closed && !oldSaleReceipt?.closedAt && { @@ -174,7 +240,12 @@ export default class SalesReceiptService { const { SaleReceipt } = this.tenancy.models(tenantId); // Transform sale receipt DTO to model. - const saleReceiptObj = this.transformObjectDTOToModel(saleReceiptDTO); + const saleReceiptObj = this.transformObjectDTOToModel( + tenantId, + saleReceiptDTO + ); + // Validate receipt number is required. + this.validateReceiptNoRequire(saleReceiptObj); // Validate receipt deposit account existance and type. await this.validateReceiptDepositAccountExistance( @@ -238,9 +309,13 @@ export default class SalesReceiptService { ); // Transform sale receipt DTO to model. const saleReceiptObj = this.transformObjectDTOToModel( + tenantId, saleReceiptDTO, oldSaleReceipt ); + // Validate receipt number is required. + this.validateReceiptNoRequire(saleReceiptObj); + // Validate receipt deposit account existance and type. await this.validateReceiptDepositAccountExistance( tenantId, @@ -309,7 +384,7 @@ export default class SalesReceiptService { await this.eventDispatcher.dispatch(events.saleReceipt.onDeleted, { tenantId, saleReceiptId, - oldSaleReceipt + oldSaleReceipt, }); } @@ -463,7 +538,7 @@ export default class SalesReceiptService { 'SaleReceipt', saleReceipt.receiptDate, 'OUT', - override, + override ); } diff --git a/server/src/services/Sales/constants.ts b/server/src/services/Sales/constants.ts index 27d0a3737..a17e6e85a 100644 --- a/server/src/services/Sales/constants.ts +++ b/server/src/services/Sales/constants.ts @@ -9,4 +9,5 @@ export const ERRORS = { 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT', INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES', + SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED' }; diff --git a/server/src/subscribers/SaleInvoices/index.ts b/server/src/subscribers/SaleInvoices/index.ts index 7fb0c7bb6..2106a47bc 100644 --- a/server/src/subscribers/SaleInvoices/index.ts +++ b/server/src/subscribers/SaleInvoices/index.ts @@ -50,17 +50,9 @@ export default class SaleInvoiceSubscriber { @On(events.saleInvoice.onCreated) public async handleInvoiceNextNumberIncrement({ tenantId, - saleInvoiceId, - saleInvoice, - saleInvoiceDTO, - autoNextNumber, }) { - if (saleInvoiceDTO.invoiceNo || !autoNextNumber) return; - - await this.saleInvoicesService.autoIncrementOrdersService.incrementSettingsNextNumber( + await this.saleInvoicesService.incrementNextInvoiceNumber( tenantId, - 'sales_invoices', - autoNextNumber[1] ); } } diff --git a/server/src/subscribers/paymentReceives.ts b/server/src/subscribers/paymentReceives.ts index e975dbd39..60186060d 100644 --- a/server/src/subscribers/paymentReceives.ts +++ b/server/src/subscribers/paymentReceives.ts @@ -130,9 +130,6 @@ export default class PaymentReceivesSubscriber { tenantId, paymentReceiveId, }) { - await this.settingsService.incrementNextNumber(tenantId, { - key: 'next_number', - group: 'payment_receives', - }); + await this.paymentReceivesService.incrementNextPaymentReceiveNumber(tenantId); } } diff --git a/server/src/utils/index.js b/server/src/utils/index.ts similarity index 91% rename from server/src/utils/index.js rename to server/src/utils/index.ts index 3d90c9814..59eae7d17 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.ts @@ -289,6 +289,30 @@ const transformToMap = (objects, key) => { const transactionIncrement = (s) => s.replace(/([0-8]|\d?9+)?$/, (e) => ++e); +const booleanValuesRepresentingTrue: string[] = [ + 'true', + '1', +]; +const booleanValuesRepresentingFalse: string[] = [ + 'false', + '0', +]; + +const normalizeValue = (value: any): string => value.toString().trim().toLowerCase(); + +const booleanValues: string[] = [ + ...booleanValuesRepresentingTrue, + ...booleanValuesRepresentingFalse, +].map((value) => normalizeValue(value)); + +export const parseBoolean = (value: any, defaultValue: T): T | boolean => { + const normalizedValue = normalizeValue(value); + if (booleanValues.indexOf(normalizedValue) === -1) { + return defaultValue; + } + return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1; +}; + export { hashPassword, origin,