diff --git a/server/src/api/controllers/ExchangeRates.ts b/server/src/api/controllers/ExchangeRates.ts index dd523ce7b..e1bf38503 100644 --- a/server/src/api/controllers/ExchangeRates.ts +++ b/server/src/api/controllers/ExchangeRates.ts @@ -103,7 +103,7 @@ export default class ExchangeRatesController extends BaseController { const { tenantId } = req; const filter = { page: 1, - pageSize: 100, + pageSize: 12, filterRoles: [], columnSortBy: 'created_at', sortOrder: 'asc', diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index ad8d75c07..9d9684d38 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -48,7 +48,7 @@ export default class BillsController extends BaseController { ); router.post( '/:id', [ - ...this.billValidationSchema, + ...this.billEditValidationSchema, ...this.specificBillValidationSchema, ], this.validationResult, @@ -106,7 +106,6 @@ export default class BillsController extends BaseController { check('entries').isArray({ min: 1 }), - check('entries.*.id').optional().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), @@ -121,7 +120,7 @@ export default class BillsController extends BaseController { */ get billEditValidationSchema() { return [ - check('bill_number').exists().trim().escape(), + check('bill_number').optional().trim().escape(), check('reference_no').optional().trim().escape(), check('bill_date').exists().isISO8601(), check('due_date').optional().isISO8601(), diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index f26635db8..297472803 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -407,6 +407,11 @@ export default class BillsPayments extends BaseController { errors: [{ type: 'BILLS_NOT_FOUND', code: 1000 }], }); } + if (error.errorType === 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY') { + return res.status(400).send({ + errors: [{ type: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', code: 1100 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 271607148..70d521885 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -91,6 +91,7 @@ export default class PaymentReceivesController extends BaseController { */ get paymentReceiveSchema(): ValidationChain[] { return [ + check('customer_id').exists().isNumeric().toInt(), check('payment_date').exists(), check('reference_no').optional(), check('deposit_account_id').exists().isNumeric().toInt(), @@ -132,7 +133,6 @@ export default class PaymentReceivesController extends BaseController { */ get newPaymentReceiveValidation() { return [ - check('customer_id').exists().isNumeric().toInt(), ...this.paymentReceiveSchema, ]; } @@ -449,6 +449,11 @@ export default class PaymentReceivesController extends BaseController { errors: [{ type: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', code: 1100 }], }); } + if (error.errorType === 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', code: 1200 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 81c09eadd..2e104def2 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -301,6 +301,7 @@ export default class SaleInvoicesController extends BaseController { filterMeta, pagination, } = await this.saleInvoiceService.salesInvoicesList(tenantId, filter); + return res.status(200).send({ sales_invoices: salesInvoices, pagination: this.transfromToResponse(pagination), diff --git a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js index 18e8377b4..6bd95c289 100644 --- a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js +++ b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js @@ -4,6 +4,7 @@ exports.up = function(knex) { return knex.schema.createTable('sales_estimates', (table) => { table.increments(); table.decimal('amount', 13, 3); + table.string('currency_code', 3); table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); table.date('estimate_date').index(); table.date('expiration_date').index(); diff --git a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js index f07d52e4c..c1f093312 100644 --- a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js +++ b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js @@ -3,6 +3,7 @@ exports.up = function(knex) { return knex.schema.createTable('sales_receipts', table => { table.increments(); table.decimal('amount', 13, 3); + table.string('currency_code', 3); table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts'); table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); table.date('receipt_date').index(); diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js index 4629c4a3c..010a17c26 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -13,6 +13,7 @@ exports.up = function(knex) { table.decimal('balance', 13, 3); table.decimal('payment_amount', 13, 3); + table.string('currency_code', 3); table.string('inv_lot_number').index(); diff --git a/server/src/database/migrations/20200715194514_create_payment_receives_table.js b/server/src/database/migrations/20200715194514_create_payment_receives_table.js index 7dc739e57..4c24aa1dc 100644 --- a/server/src/database/migrations/20200715194514_create_payment_receives_table.js +++ b/server/src/database/migrations/20200715194514_create_payment_receives_table.js @@ -6,6 +6,7 @@ exports.up = function(knex) { table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); table.date('payment_date').index(); table.decimal('amount', 13, 3).defaultTo(0); + table.string('currency_code', 3); table.string('reference_no').index(); table.integer('deposit_account_id').unsigned().references('id').inTable('accounts'); table.string('payment_receive_no').nullable(); diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js index 8dc2ce1aa..c8c432a32 100644 --- a/server/src/database/migrations/20200719152005_create_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -10,6 +10,7 @@ exports.up = function(knex) { table.string('status').index(); table.text('note'); table.decimal('amount', 13, 3).defaultTo(0); + table.string('currency_code'); table.decimal('payment_amount', 13, 3).defaultTo(0); table.string('inv_lot_number').index(); table.date('opened_at').index(); diff --git a/server/src/database/migrations/20200719153909_create_bills_payments_table.js b/server/src/database/migrations/20200719153909_create_bills_payments_table.js index e61b93e5c..1e618b5dd 100644 --- a/server/src/database/migrations/20200719153909_create_bills_payments_table.js +++ b/server/src/database/migrations/20200719153909_create_bills_payments_table.js @@ -4,6 +4,7 @@ exports.up = function(knex) { table.increments(); table.integer('vendor_id').unsigned().index().references('id').inTable('contacts'); table.decimal('amount', 13, 3).defaultTo(0); + table.string('currency_code'); table.integer('payment_account_id').unsigned().references('id').inTable('accounts'); table.string('payment_number').nullable().index(); table.date('payment_date').index(); diff --git a/server/src/interfaces/PaymentReceive.ts b/server/src/interfaces/PaymentReceive.ts index a545b8a01..40dcb40ac 100644 --- a/server/src/interfaces/PaymentReceive.ts +++ b/server/src/interfaces/PaymentReceive.ts @@ -25,6 +25,7 @@ export interface IPaymentReceiveCreateDTO { }; export interface IPaymentReceiveEditDTO { + customerId: number, paymentDate: Date, amount: number, referenceNo: string, diff --git a/server/src/interfaces/SaleEstimate.ts b/server/src/interfaces/SaleEstimate.ts index 8cc086e43..f3679570d 100644 --- a/server/src/interfaces/SaleEstimate.ts +++ b/server/src/interfaces/SaleEstimate.ts @@ -4,6 +4,7 @@ import { IDynamicListFilterDTO } from 'interfaces/DynamicFilter'; export interface ISaleEstimate { id?: number, amount: number, + currencyCode: string, customerId: number, estimateDate: Date, estimateNumber: string, diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index ff0447109..d3789b011 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -5,6 +5,7 @@ export interface ISaleInvoice { id: number, balance: number, paymentAmount: number, + currencyCode: string, invoiceDate: Date, dueDate: Date, dueAmount: number, diff --git a/server/src/interfaces/SaleReceipt.ts b/server/src/interfaces/SaleReceipt.ts index 687441291..2046eddae 100644 --- a/server/src/interfaces/SaleReceipt.ts +++ b/server/src/interfaces/SaleReceipt.ts @@ -10,6 +10,7 @@ export interface ISaleReceipt { receiptMessage: string; receiptNumber: string; amount: number; + currencyCode: string, statement: string; closedAt: Date | string; entries: any[]; diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 938af5f46..92cf31ac7 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -63,6 +63,11 @@ export default class ContactsService { * @param {IContactNewDTO | IContactEditDTO} contactDTO */ private transformContactObj(contactDTO: IContactNewDTO | IContactEditDTO) { + const baseCurrency = 'USD'; + const currencyCode = typeof contactDTO.currencyCode !== 'undefined' + ? contactDTO.currencyCode + : baseCurrency; + return { ...omit(contactDTO, [ 'billingAddress1', @@ -74,6 +79,7 @@ export default class ContactsService { billing_address_2: contactDTO?.billingAddress2, shipping_address_1: contactDTO?.shippingAddress1, shipping_address_2: contactDTO?.shippingAddress2, + ...(currencyCode ? ({ currencyCode }) : {}), }; } @@ -99,7 +105,6 @@ export default class ContactsService { contactService, ...contactObj, }); - this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact, @@ -123,12 +128,12 @@ export default class ContactsService { const { contactRepository } = this.tenancy.repositories(tenantId); const contactObj = this.transformContactObj(contactDTO); + // Retrieve the given contact by id or throw not found service error. const contact = await this.getContactByIdOrThrowError( tenantId, contactId, contactService ); - this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, @@ -196,7 +201,7 @@ export default class ContactsService { const dynamicList = await this.dynamicListService.dynamicList( tenantId, Contact, - contactsFilter, + contactsFilter ); // Retrieve contacts list by the given query. const contacts = await Contact.query().onBuild((builder) => { diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 052505823..ada57f3d2 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -116,7 +116,7 @@ export default class VendorsService { * @param {number} tenantId * @param {number} customerId */ - private getVendorByIdOrThrowError(tenantId: number, customerId: number) { + public getVendorByIdOrThrowError(tenantId: number, customerId: number) { return this.contactService.getContactByIdOrThrowError( tenantId, customerId, diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index 7d2312f79..5159abd8a 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -54,9 +54,9 @@ export default class InventoryService { /** * Computes the given item cost and records the inventory lots transactions * and journal entries based on the cost method FIFO, LIFO or average cost rate. - * @param {number} tenantId - - * @param {Date} fromDate - - * @param {number} itemId - + * @param {number} tenantId - Tenant id. + * @param {Date} fromDate - From date. + * @param {number} itemId - Item id. */ async computeItemCost(tenantId: number, fromDate: Date, itemId: number) { const { Item } = this.tenancy.models(tenantId); diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index ce7bf769b..989a2427f 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -27,6 +27,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { entriesAmountDiff, formatDateFields } from 'utils'; import { ServiceError } from 'exceptions'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; +import VendorsService from 'services/Contacts/VendorsService'; const ERRORS = { BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', @@ -38,6 +39,7 @@ const ERRORS = { BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND', INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', + PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', }; /** @@ -58,6 +60,9 @@ export default class BillPaymentsService { @Inject() dynamicListService: DynamicListingService; + @Inject() + vendorsService: VendorsService; + @EventDispatcher() eventDispatcher: EventDispatcherInterface; @@ -118,7 +123,6 @@ export default class BillPaymentsService { const paymentAccount = await accountRepository.findOneById( paymentAccountId ); - if (!paymentAccount) { throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); } @@ -263,6 +267,45 @@ export default class BillPaymentsService { } } + /** + * * Validate the payment vendor whether modified. + * @param {string} billPaymentNo + */ + validateVendorNotModified( + billPaymentDTO: IBillPaymentDTO, + oldBillPayment: IBillPayment + ) { + if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) { + throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY); + } + } + + /** + * Transforms create/edit DTO to model. + * @param {number} tenantId + * @param {IBillPaymentDTO} billPaymentDTO - Bill payment. + * @param {IBillPayment} oldBillPayment - Old bill payment. + * @return {Promise} + */ + async transformDTOToModel( + tenantId: number, + billPaymentDTO: IBillPaymentDTO, + oldBillPayment?: IBillPayment + ): Promise { + // Retrieve vendor details by the given vendor id. + const vendor = await this.vendorsService.getVendorByIdOrThrowError( + tenantId, + billPaymentDTO.vendorId + ); + + return { + amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), + currencyCode: vendor.currencyCode, + ...formatDateFields(billPaymentDTO, ['paymentDate']), + entries: billPaymentDTO.entries, + }; + } + /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. @@ -288,11 +331,11 @@ export default class BillPaymentsService { }); const { BillPayment } = this.tenancy.models(tenantId); - const billPaymentObj = { - amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), - ...formatDateFields(billPaymentDTO, ['paymentDate']), - }; - + // Transform create DTO to model object. + const billPaymentObj = await this.transformDTOToModel( + tenantId, + billPaymentDTO + ); // Validate vendor existance on the storage. await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); @@ -301,7 +344,6 @@ export default class BillPaymentsService { tenantId, billPaymentObj.paymentAccountId ); - // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber); @@ -312,15 +354,13 @@ export default class BillPaymentsService { billPaymentObj.entries, billPaymentDTO.vendorId ); - // Validates the bills due payment amount. await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); const billPayment = await BillPayment.query().insertGraphAndFetch({ - ...omit(billPaymentObj, ['entries']), - entries: billPaymentDTO.entries, + ...billPaymentObj, }); - + // Triggers `onBillPaymentCreated` event. await this.eventDispatcher.dispatch(events.billPayment.onCreated, { tenantId, billPayment, @@ -363,11 +403,14 @@ export default class BillPaymentsService { tenantId, billPaymentId ); - - const billPaymentObj = { - amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), - ...formatDateFields(billPaymentDTO, ['paymentDate']), - }; + // Transform bill payment DTO to model object. + const billPaymentObj = await this.transformDTOToModel( + tenantId, + billPaymentDTO, + oldBillPayment + ); + // Validate vendor not modified. + this.validateVendorNotModified(billPaymentDTO, oldBillPayment); // Validate vendor existance on the storage. await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); @@ -377,28 +420,24 @@ export default class BillPaymentsService { tenantId, billPaymentObj.paymentAccountId ); - // Validate the items entries IDs existance on the storage. await this.validateEntriesIdsExistance( tenantId, billPaymentId, billPaymentObj.entries ); - // Validate the bills existance and associated to the given vendor. await this.validateBillsExistance( tenantId, billPaymentObj.entries, billPaymentDTO.vendorId ); - // Validates the bills due payment amount. await this.validateBillsDueAmount( tenantId, billPaymentObj.entries, oldBillPayment.entries ); - // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { await this.validatePaymentNumber( @@ -409,8 +448,7 @@ export default class BillPaymentsService { } const billPayment = await BillPayment.query().upsertGraphAndFetch({ id: billPaymentId, - ...omit(billPaymentObj, ['entries']), - entries: billPaymentDTO.entries, + ...billPaymentObj, }); await this.eventDispatcher.dispatch(events.billPayment.onEdited, { tenantId, diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index bf3ab1137..5d2a716ff 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -27,6 +27,7 @@ import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; +import VendorsService from 'services/Contacts/VendorsService'; import { ERRORS } from './constants'; /** @@ -46,7 +47,7 @@ export default class BillsService extends SalesInvoicesCost { @Inject() tenancy: TenancyService; - + @EventDispatcher() eventDispatcher: EventDispatcherInterface; @@ -62,6 +63,9 @@ export default class BillsService extends SalesInvoicesCost { @Inject() journalPosterService: JournalPosterService; + @Inject() + vendorsService: VendorsService; + /** * Validates whether the vendor is exist. * @async @@ -136,16 +140,25 @@ export default class BillsService extends SalesInvoicesCost { } /** - * Converts bill DTO to model. + * Validate the bill number require. + * @param {string} billNo - + */ + validateBillNoRequire(billNo: string) { + if (!billNo) { + throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); + } + } + + /** + * Converts create bill DTO to model. * @param {number} tenantId * @param {IBillDTO} billDTO * @param {IBill} oldBill - * * @returns {IBill} */ private async billDTOToModel( tenantId: number, - billDTO: IBillDTO | IBillEditDTO, + billDTO: IBillDTO, authorizedUser: ISystemUser, oldBill?: IBill ) { @@ -157,15 +170,25 @@ export default class BillsService extends SalesInvoicesCost { })); const amount = sumBy(entries, 'amount'); + // Bill number from DTO or from auto-increment. + const billNumber = billDTO.billNumber || oldBill?.billNumber; + + // Retrieve vendor details by the given vendor id. + const vendor = await this.vendorsService.getVendorByIdOrThrowError( + tenantId, + billDTO.vendorId + ); return { ...formatDateFields(omit(billDTO, ['open', 'entries']), [ 'billDate', 'dueDate', ]), amount, + currencyCode: vendor.currencyCode, + billNumber, entries: entries.map((entry) => ({ reference_type: 'Bill', - ...omit(entry, ['amount', 'id']), + ...omit(entry, ['amount']), })), // Avoid rewrite the open date in edit mode when already opened. ...(billDTO.open && @@ -205,16 +228,14 @@ export default class BillsService extends SalesInvoicesCost { const billObj = await this.billDTOToModel( tenantId, billDTO, - authorizedUser, - null + authorizedUser ); // Retrieve vendor or throw not found service error. await this.getVendorOrThrowError(tenantId, billDTO.vendorId); // Validate the bill number uniqiness on the storage. - if (billDTO.billNumber) { - await this.validateBillNumberExists(tenantId, billDTO.billNumber); - } + await this.validateBillNumberExists(tenantId, billDTO.billNumber); + // Validate items IDs existance. await this.itemsEntriesService.validateItemsIdsExistance( tenantId, @@ -277,7 +298,6 @@ export default class BillsService extends SalesInvoicesCost { authorizedUser, oldBill ); - // Retrieve vendor details or throw not found service error. await this.getVendorOrThrowError(tenantId, billDTO.vendorId); @@ -292,7 +312,6 @@ export default class BillsService extends SalesInvoicesCost { 'Bill', billDTO.entries ); - // Validate the items ids existance on the storage. await this.itemsEntriesService.validateItemsIdsExistance( tenantId, diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts index e73fbb71b..b251516be 100644 --- a/server/src/services/Purchases/constants.ts +++ b/server/src/services/Purchases/constants.ts @@ -7,4 +7,5 @@ export const ERRORS = { BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', + BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED' }; diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 29ee4f8ae..2a6acafbf 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -42,7 +42,9 @@ 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_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', + PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED', + PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', }; /** * Payment receive service. @@ -264,28 +266,6 @@ export default class PaymentReceiveService { } } - /** - * 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 @@ -315,32 +295,69 @@ export default class PaymentReceiveService { } } + /** + * Validates the payment receive number require. + * @param {string} paymentReceiveNo + */ + validatePaymentNoRequire(paymentReceiveNo: string) { + if (!paymentReceiveNo) { + throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED); + } + } + + /** + * Validate the payment customer whether modified. + * @param {IPaymentReceiveEditDTO} paymentReceiveDTO + * @param {IPaymentReceive} oldPaymentReceive + */ + validateCustomerNotModified( + paymentReceiveDTO: IPaymentReceiveEditDTO, + oldPaymentReceive: IPaymentReceive + ) { + if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) { + throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE); + } + } + /** * Transformes the create payment receive DTO to model object. * @param {number} tenantId - * @param {IPaymentReceiveCreateDTO} paymentReceiveDTO + * @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO. + * @param {IPaymentReceive} oldPaymentReceive - + * @return {IPaymentReceive} */ - transformPaymentReceiveDTOToModel( + async transformPaymentReceiveDTOToModel( tenantId: number, paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, oldPaymentReceive?: IPaymentReceive - ): IPaymentReceive { + ): Promise { const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); - // Retrieve the next payment receive number. - const paymentReceiveNo = this.transformPaymentNumberToModel( + // Retrieve customer details. + const customer = await this.customersService.getCustomerByIdOrThrowError( tenantId, - paymentReceiveDTO, - oldPaymentReceive + paymentReceiveDTO.customerId ); + // Retreive the next invoice number. + const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId); + + // Retrieve the next payment receive number. + const paymentReceiveNo = + paymentReceiveDTO.paymentReceiveNo || + oldPaymentReceive?.paymentReceiveNo || + autoNextNumber; + + this.validatePaymentNoRequire(paymentReceiveNo); + return { amount: paymentAmount, + currencyCode: customer.currencyCode, ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ 'paymentDate', ]), ...(paymentReceiveNo ? { paymentReceiveNo } : {}), entries: paymentReceiveDTO.entries.map((entry) => ({ - ...omit(entry, ['id']), + ...entry, })), }; } @@ -360,13 +377,10 @@ export default class PaymentReceiveService { const { PaymentReceive } = this.tenancy.models(tenantId); // Transformes the payment receive DTO to model. - const paymentReceiveObj = this.transformPaymentReceiveDTOToModel( + const paymentReceiveObj = await this.transformPaymentReceiveDTOToModel( tenantId, paymentReceiveDTO ); - // Validate payment receive is required. - this.validatePaymentReceiveNoRequire(paymentReceiveObj); - // Validate payment receive number uniquiness. await this.validatePaymentReceiveNoExistance( tenantId, @@ -393,7 +407,6 @@ export default class PaymentReceiveService { tenantId, paymentReceiveDTO.entries ); - this.logger.info('[payment_receive] inserting to the storage.'); const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({ ...paymentReceiveObj, @@ -447,13 +460,13 @@ export default class PaymentReceiveService { paymentReceiveId ); // Transformes the payment receive DTO to model. - const paymentReceiveObj = this.transformPaymentReceiveDTOToModel( + const paymentReceiveObj = await this.transformPaymentReceiveDTOToModel( tenantId, paymentReceiveDTO, oldPaymentReceive ); - // Validate payment receive number existance. - this.validatePaymentReceiveNoRequire(paymentReceiveObj); + // Validate customer whether modified. + this.validateCustomerNotModified(paymentReceiveDTO, oldPaymentReceive); // Validate payment receive number uniquiness. if (paymentReceiveDTO.paymentReceiveNo) { @@ -527,7 +540,7 @@ export default class PaymentReceiveService { const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models( tenantId ); - + // Retreive payment receive or throw not found service error. const oldPaymentReceive = await this.getPaymentReceiveOrThrowError( tenantId, paymentReceiveId diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index b23edef53..b4022bdd2 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -31,7 +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' + SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED', }; /** @@ -157,25 +157,34 @@ export default class SaleEstimateService { } /** - * Transform DTO object ot model object. + * Transform create DTO object ot model object. * @param {number} tenantId - * @param {ISaleEstimateDTO} saleEstimateDTO - * @param {ISaleEstimate} oldSaleEstimate + * @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO. * @return {ISaleEstimate} */ - transformDTOToModel( + async transformDTOToModel( tenantId: number, estimateDTO: ISaleEstimateDTO, oldSaleEstimate?: ISaleEstimate - ): ISaleEstimate { + ): Promise { const { ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e)); + // Retreive the next invoice number. + const autoNextNumber = this.getNextEstimateNumber(tenantId); + // Retreive the next estimate number. - const estimateNumber = this.transformEstimateNumberToModel( + const estimateNumber = estimateDTO.estimateNumber || + oldSaleEstimate?.estimateNumber || + autoNextNumber; + + // Validate the sale estimate number require. + this.validateEstimateNoRequire(estimateNumber); + + // Retrieve customer details. + const customer = await this.customersService.getCustomerByIdOrThrowError( tenantId, - estimateDTO, - oldSaleEstimate + estimateDTO.customerId ); return { @@ -184,25 +193,26 @@ export default class SaleEstimateService { 'estimateDate', 'expirationDate', ]), + currencyCode: customer.currencyCode, ...(estimateNumber ? { estimateNumber } : {}), entries: estimateDTO.entries.map((entry) => ({ reference_type: 'SaleEstimate', - ...omit(entry, ['total', 'amount', 'id']), + ...entry, })), // Avoid rewrite the deliver date in edit mode when already published. ...(estimateDTO.delivered && - !oldSaleEstimate?.deliveredAt && { - deliveredAt: moment().toMySqlDateTime(), - }), + !oldSaleEstimate?.deliveredAt && { + deliveredAt: moment().toMySqlDateTime(), + }), }; } - + /** * Validate the sale estimate number require. * @param {ISaleEstimate} saleInvoiceObj */ - validateEstimateNoRequire(saleInvoiceObj: ISaleEstimate) { - if (!saleInvoiceObj.estimateNumber) { + validateEstimateNoRequire(estimateNumber: string) { + if (!estimateNumber) { throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED); } } @@ -223,27 +233,25 @@ export default class SaleEstimateService { this.logger.info('[sale_estimate] inserting sale estimate to the storage.'); // Transform DTO object ot model object. - const estimateObj = this.transformDTOToModel(tenantId, estimateDTO); - - // Validate the sale estimate number require. - this.validateEstimateNoRequire(estimateObj); - + const estimateObj = await this.transformDTOToModel( + tenantId, + estimateDTO + ); // Validate estimate number uniquiness on the storage. - if (estimateObj.estimateNumber) { - await this.validateEstimateNumberExistance( - tenantId, - estimateObj.estimateNumber - ); - } + await this.validateEstimateNumberExistance( + tenantId, + estimateObj.estimateNumber + ); // Retrieve the given customer or throw not found service error. - await this.customersService.getCustomer(tenantId, estimateDTO.customerId); - + await this.customersService.getCustomerByIdOrThrowError( + tenantId, + estimateDTO.customerId + ); // Validate items IDs existance on the storage. await this.itemsEntriesService.validateItemsIdsExistance( tenantId, estimateDTO.entries ); - // Validate non-sellable items. await this.itemsEntriesService.validateNonSellableEntriesItems( tenantId, @@ -284,14 +292,11 @@ export default class SaleEstimateService { estimateId ); // Transform DTO object ot model object. - const estimateObj = this.transformDTOToModel( + const estimateObj = await this.transformDTOToModel( tenantId, estimateDTO, oldSaleEstimate ); - // Validate the sale estimate number require. - this.validateEstimateNoRequire(estimateObj); - // Validate estimate number uniquiness on the storage. if (estimateDTO.estimateNumber) { await this.validateEstimateNumberExistance( @@ -301,13 +306,15 @@ export default class SaleEstimateService { ); } // Retrieve the given customer or throw not found service error. - await this.customersService.getCustomer(tenantId, estimateDTO.customerId); - + await this.customersService.getCustomerByIdOrThrowError( + tenantId, + estimateDTO.customerId + ); // Validate sale estimate entries existance. await this.itemsEntriesService.validateEntriesIdsExistance( tenantId, estimateId, - 'SaleEstiamte', + 'SaleEstimate', estimateDTO.entries ); // Validate items IDs existance on the storage. diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 3e68b2e17..36d6c586d 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -181,46 +181,36 @@ export default class SaleInvoicesService { } /** - * Retrieve invoice number to object model. - * @param tenantId - * @param saleInvoiceDTO - * @param oldSaleInvoice + * Transformes the create DTO to invoice object model. + * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. + * @param {ISaleInvoice} oldSaleInvoice - Old sale invoice. + * @return {ISaleInvoice} */ - transformInvoiceNumberToModel( + private async transformDTOToModel( 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. - * @param {ISaleInvoiceDTO} saleInvoiceDTO - Sale invoice DTO. - */ - transformDTOToModel( - tenantId: number, - saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO, - oldSaleInvoice?: ISaleInvoice - ): ISaleInvoice { + ): Promise { const { ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, (e) => ItemEntry.calcAmount(e) ); - - const invoiceNo = this.transformInvoiceNumberToModel( + // Retrieve customer details. + const customer = await this.customersService.getCustomerByIdOrThrowError( tenantId, - saleInvoiceDTO, - oldSaleInvoice + saleInvoiceDTO.customerId ); + // Retreive the next invoice number. + const autoNextNumber = this.getNextInvoiceNumber(tenantId); + + // Invoice number. + const invoiceNo = + saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber; + + // Validate the invoice is required. + this.validateInvoiceNoRequire(invoiceNo); + return { ...formatDateFields( omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']), @@ -228,20 +218,17 @@ export default class SaleInvoicesService { ), // Avoid rewrite the deliver date in edit mode when already published. balance, + currencyCode: customer.currencyCode, ...(saleInvoiceDTO.delivered && !oldSaleInvoice?.deliveredAt && { deliveredAt: moment().toMySqlDateTime(), }), - // Avoid add payment amount in edit mode. - ...(!oldSaleInvoice - ? { - paymentAmount: 0, - } - : {}), + // Avoid override payment amount in edit mode. + ...(!oldSaleInvoice && { paymentAmount: 0 }), ...(invoiceNo ? { invoiceNo } : {}), entries: saleInvoiceDTO.entries.map((entry) => ({ referenceType: 'SaleInvoice', - ...omit(entry, ['amount', 'id']), + ...entry, })), }; } @@ -250,8 +237,8 @@ export default class SaleInvoicesService { * Validate the invoice number require. * @param {ISaleInvoice} saleInvoiceObj */ - validateInvoiceNoRequire(saleInvoiceObj: ISaleInvoice) { - if (!saleInvoiceObj.invoiceNo) { + validateInvoiceNoRequire(invoiceNo: string) { + if (!invoiceNo) { throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED); } } @@ -272,19 +259,15 @@ export default class SaleInvoicesService { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); // Transform DTO object to model object. - const saleInvoiceObj = this.transformDTOToModel( + const saleInvoiceObj = await this.transformDTOToModel( tenantId, - saleInvoiceDTO, - null + saleInvoiceDTO ); - this.validateInvoiceNoRequire(saleInvoiceObj); - // Validate customer existance. await this.customersService.getCustomerByIdOrThrowError( tenantId, saleInvoiceDTO.customerId ); - // Validate sale invoice number uniquiness. if (saleInvoiceObj.invoiceNo) { await this.validateInvoiceNumberUnique( @@ -354,7 +337,7 @@ export default class SaleInvoicesService { saleInvoiceId ); // Transform DTO object to model object. - const saleInvoiceObj = this.transformDTOToModel( + const saleInvoiceObj = await this.transformDTOToModel( tenantId, saleInvoiceDTO, oldSaleInvoice @@ -396,10 +379,10 @@ export default class SaleInvoicesService { ); this.logger.info('[sale_invoice] trying to update sale invoice.'); - const saleInvoice: ISaleInvoice = await saleInvoiceRepository.update( - { ...omit(saleInvoiceObj, ['paymentAmount']) }, - { id: saleInvoiceId } - ); + const saleInvoice: ISaleInvoice = await saleInvoiceRepository.upsertGraph({ + id: saleInvoiceId, + ...saleInvoiceObj, + }); // Triggers `onSaleInvoiceEdited` event. await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, { tenantId, diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index 4d6e1ddbd..bf5210239 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -20,6 +20,7 @@ import { ItemEntry } from 'models'; 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', @@ -27,7 +28,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' + SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', }; @Service() @@ -56,6 +57,9 @@ export default class SalesReceiptService { @Inject() autoIncrementOrdersService: AutoIncrementOrdersService; + @Inject() + customersService: CustomersService; + /** * Validate whether sale receipt exists on the storage. * @param {number} tenantId - @@ -139,8 +143,8 @@ export default class SalesReceiptService { * Validate the sale receipt number require. * @param {ISaleReceipt} saleReceipt */ - validateReceiptNoRequire(saleReceipt: ISaleReceipt) { - if (!saleReceipt.receiptNumber) { + validateReceiptNoRequire(receiptNumber: string) { + if (!receiptNumber) { throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED); } } @@ -189,40 +193,52 @@ export default class SalesReceiptService { } /** - * Transform DTO object to model object. + * Transform create DTO object to model object. * @param {ISaleReceiptDTO} saleReceiptDTO - * @param {ISaleReceipt} oldSaleReceipt - * @returns {ISaleReceipt} */ - transformObjectDTOToModel( + async transformDTOToModel( tenantId: number, saleReceiptDTO: ISaleReceiptDTO, oldSaleReceipt?: ISaleReceipt - ): ISaleReceipt { + ): Promise { const amount = sumBy(saleReceiptDTO.entries, (e) => ItemEntry.calcAmount(e) ); + // Retreive the next invoice number. + const autoNextNumber = this.getNextReceiptNumber(tenantId); + // Retreive the receipt number. - const receiptNumber = this.transformReceiptNumberToModel( + const receiptNumber = + saleReceiptDTO.receiptNumber || + oldSaleReceipt?.receiptNumber || + autoNextNumber; + + // Validate receipt number require. + this.validateReceiptNoRequire(receiptNumber); + + // Retrieve customer details. + const customer = await this.customersService.getCustomerByIdOrThrowError( tenantId, - saleReceiptDTO, - oldSaleReceipt + saleReceiptDTO.customerId ); return { amount, + currencyCode: customer.currencyCode, ...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [ 'receiptDate', ]), - ...(receiptNumber ? { receiptNumber } : {}), + receiptNumber, // Avoid rewrite the deliver date in edit mode when already published. ...(saleReceiptDTO.closed && - !oldSaleReceipt?.closedAt && { + !oldSaleReceipt.closedAt && { closedAt: moment().toMySqlDateTime(), }), entries: saleReceiptDTO.entries.map((entry) => ({ reference_type: 'SaleReceipt', - ...omit(entry, ['id', 'amount']), + ...entry, })), }; } @@ -240,13 +256,10 @@ export default class SalesReceiptService { const { SaleReceipt } = this.tenancy.models(tenantId); // Transform sale receipt DTO to model. - const saleReceiptObj = this.transformObjectDTOToModel( + const saleReceiptObj = await this.transformDTOToModel( tenantId, saleReceiptDTO ); - // Validate receipt number is required. - this.validateReceiptNoRequire(saleReceiptObj); - // Validate receipt deposit account existance and type. await this.validateReceiptDepositAccountExistance( tenantId, @@ -308,14 +321,11 @@ export default class SalesReceiptService { saleReceiptId ); // Transform sale receipt DTO to model. - const saleReceiptObj = this.transformObjectDTOToModel( + const saleReceiptObj = await this.transformDTOToModel( tenantId, saleReceiptDTO, oldSaleReceipt ); - // Validate receipt number is required. - this.validateReceiptNoRequire(saleReceiptObj); - // Validate receipt deposit account existance and type. await this.validateReceiptDepositAccountExistance( tenantId, diff --git a/server/src/subscribers/SaleReceipt/index.ts b/server/src/subscribers/SaleReceipt/index.ts index b678f8d9d..d8b91b27d 100644 --- a/server/src/subscribers/SaleReceipt/index.ts +++ b/server/src/subscribers/SaleReceipt/index.ts @@ -3,17 +3,20 @@ import { On, EventSubscriber } from 'event-dispatch'; import events from 'subscribers/events'; import TenancyService from 'services/Tenancy/TenancyService'; import SettingsService from 'services/Settings/SettingsService'; +import SalesReceiptService from 'services/Sales/SalesReceipts'; @EventSubscriber() export default class SaleReceiptSubscriber { logger: any; tenancy: TenancyService; settingsService: SettingsService; + saleReceiptsService: SalesReceiptService; constructor() { this.logger = Container.get('logger'); this.tenancy = Container.get(TenancyService); this.settingsService = Container.get(SettingsService); + this.saleReceiptsService = Container.get(SalesReceiptService); } /** @@ -21,9 +24,6 @@ export default class SaleReceiptSubscriber { */ @On(events.saleReceipt.onCreated) public async handleReceiptNextNumberIncrement({ tenantId, saleReceiptId }) { - await this.settingsService.incrementNextNumber(tenantId, { - key: 'next_number', - group: 'sales_receipts', - }); + await this.saleReceiptsService.incrementNextReceiptNumber(tenantId); } }