From 26452d9c05b34e7061ae8e49bac82e0955c0ccb6 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Wed, 23 Dec 2020 13:11:02 +0200 Subject: [PATCH] feat: changing items quantity on hand with sales invoices and bills. --- server/src/loaders/tenantRepositories.ts | 2 + .../src/services/Items/ItemsEntriesService.ts | 126 ++++++++++++++---- server/src/services/Items/ItemsService.ts | 1 + server/src/services/Sales/SalesInvoices.ts | 18 ++- server/src/subscribers/bills.ts | 59 ++++++-- server/src/subscribers/saleInvoices.ts | 43 ++++++ 6 files changed, 202 insertions(+), 47 deletions(-) diff --git a/server/src/loaders/tenantRepositories.ts b/server/src/loaders/tenantRepositories.ts index ad9003591..40447ac05 100644 --- a/server/src/loaders/tenantRepositories.ts +++ b/server/src/loaders/tenantRepositories.ts @@ -11,6 +11,7 @@ import SettingRepository from 'repositories/SettingRepository'; import ExpenseEntryRepository from 'repositories/ExpenseEntryRepository'; import BillRepository from 'repositories/BillRepository'; import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository'; +import ItemRepository from 'repositories/ItemRepository'; export default (knex, cache) => { return { @@ -27,5 +28,6 @@ export default (knex, cache) => { settingRepository: new SettingRepository(knex, cache), billRepository: new BillRepository(knex, cache), saleInvoiceRepository: new SaleInvoiceRepository(knex, cache), + itemRepository: new ItemRepository(knex, cache), }; }; \ No newline at end of file diff --git a/server/src/services/Items/ItemsEntriesService.ts b/server/src/services/Items/ItemsEntriesService.ts index 201ab2db3..43565124d 100644 --- a/server/src/services/Items/ItemsEntriesService.ts +++ b/server/src/services/Items/ItemsEntriesService.ts @@ -1,19 +1,15 @@ import { difference, map } from 'lodash'; import { Inject, Service } from 'typedi'; -import { - IItemEntry, - IItemEntryDTO, - IItem, -} from 'interfaces'; +import { IItemEntry, IItemEntryDTO, IItem } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; -import { ItemEntry } from 'models'; +import { entriesAmountDiff } from 'utils'; const ERRORS = { ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND', NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', - NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS' + NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS', }; @Service() @@ -23,15 +19,15 @@ export default class ItemsEntriesService { /** * Retrieve the inventory items entries of the reference id and type. - * @param {number} tenantId - * @param {string} referenceType - * @param {string} referenceId + * @param {number} tenantId + * @param {string} referenceType + * @param {string} referenceId * @return {Promise} */ public async getInventoryEntries( tenantId: number, referenceType: string, - referenceId: number, + referenceId: number ): Promise { const { Item, ItemEntry } = this.tenancy.models(tenantId); @@ -46,7 +42,7 @@ export default class ItemsEntriesService { // Inventory items ids. const inventoryItemsIds = map(inventoryItems, 'id'); - + // Filtering the inventory items entries. const inventoryItemsEntries = itemsEntries.filter( (itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1 @@ -57,13 +53,16 @@ export default class ItemsEntriesService { /** * Validates the entries items ids. * @async - * @param {number} tenantId - + * @param {number} tenantId - * @param {IItemEntryDTO} itemEntries - */ - public async validateItemsIdsExistance(tenantId: number, itemEntries: IItemEntryDTO[]) { + public async validateItemsIdsExistance( + tenantId: number, + itemEntries: IItemEntryDTO[] + ) { const { Item } = this.tenancy.models(tenantId); const itemsIds = itemEntries.map((e) => e.itemId); - + const foundItems = await Item.query().whereIn('id', itemsIds); const foundItemsIds = foundItems.map((item: IItem) => item.id); @@ -74,13 +73,18 @@ export default class ItemsEntriesService { } } - /** + /** * Validates the entries ids existance on the storage. - * @param {number} tenantId - - * @param {number} billId - + * @param {number} tenantId - + * @param {number} billId - * @param {IItemEntry[]} billEntries - */ - public async validateEntriesIdsExistance(tenantId: number, referenceId: number, referenceType: string, billEntries: IItemEntryDTO[]) { + public async validateEntriesIdsExistance( + tenantId: number, + referenceId: number, + referenceType: string, + billEntries: IItemEntryDTO[] + ) { const { ItemEntry } = this.tenancy.models(tenantId); const entriesIds = billEntries .filter((e: IItemEntry) => e.id) @@ -94,18 +98,20 @@ export default class ItemsEntriesService { const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); if (notFoundEntriesIds.length > 0) { - throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND) + throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND); } } - /** - * Validate the entries items that not purchase-able. + * Validate the entries items that not purchase-able. */ - public async validateNonPurchasableEntriesItems(tenantId: number, itemEntries: IItemEntryDTO[]) { + public async validateNonPurchasableEntriesItems( + tenantId: number, + itemEntries: IItemEntryDTO[] + ) { const { Item } = this.tenancy.models(tenantId); const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId); - + const purchasbleItems = await Item.query() .where('purchasable', true) .whereIn('id', itemsIds); @@ -119,12 +125,15 @@ export default class ItemsEntriesService { } /** - * Validate the entries items that not sell-able. + * Validate the entries items that not sell-able. */ - public async validateNonSellableEntriesItems(tenantId: number, itemEntries: IItemEntryDTO[]) { + public async validateNonSellableEntriesItems( + tenantId: number, + itemEntries: IItemEntryDTO[] + ) { const { Item } = this.tenancy.models(tenantId); const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId); - + const sellableItems = await Item.query() .where('sellable', true) .whereIn('id', itemsIds); @@ -136,4 +145,65 @@ export default class ItemsEntriesService { throw new ServiceError(ERRORS.NOT_SELL_ABLE_ITEMS); } } -} \ No newline at end of file + + /** + * Changes items quantity from the given items entries the new and old onces. + * @param {number} tenantId + * @param {IItemEntry} entries - Items entries. + * @param {IItemEntry} oldEntries - Old items entries. + */ + public async changeItemsQuantity( + tenantId: number, + entries: IItemEntry[], + oldEntries?: IItemEntry[] + ): Promise { + const { itemRepository } = this.tenancy.repositories(tenantId); + const opers = []; + + const diffEntries = entriesAmountDiff( + entries, + oldEntries, + 'quantity', + 'itemId' + ); + diffEntries.forEach((entry: IItemEntry) => { + const changeQuantityOper = itemRepository.changeNumber( + { id: entry.itemId, type: 'inventory' }, + 'quantityOnHand', + entry.quantity + ); + opers.push(changeQuantityOper); + }); + await Promise.all(opers); + } + + /** + * Increment items quantity from the given items entries. + * @param {number} tenantId - Tenant id. + * @param {IItemEntry} entries - Items entries. + */ + public async incrementItemsEntries( + tenantId: number, + entries: IItemEntry[] + ): Promise { + return this.changeItemsQuantity(tenantId, entries); + } + + /** + * Decrement items quantity from the given items entries. + * @param {number} tenantId - Tenant id. + * @param {IItemEntry} entries - Items entries. + */ + public async decrementItemsQuantity( + tenantId: number, + entries: IItemEntry[] + ): Promise { + return this.changeItemsQuantity( + tenantId, + entries.map((entry) => ({ + ...entry, + quantity: entry.quantity * -1, + })) + ); + } +} diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 8b32fac9c..ddd39a7f4 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -251,6 +251,7 @@ export default class ItemsService implements IItemsService { const storedItem = await Item.query().insertAndFetch({ ...itemDTO, active: defaultTo(itemDTO.active, 1), + quantityOnHand: itemDTO.type === 'inventory' ? 0 : null, }); this.logger.info('[items] item inserted successfully.', { tenantId, diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 99e72744b..bb7c59576 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,17 +1,14 @@ import { Service, Inject } from 'typedi'; -import { omit, sumBy, pick, map } from 'lodash'; +import { omit, sumBy, map } from 'lodash'; import moment from 'moment'; -import uniqid from 'uniqid'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; import { ISaleInvoice, - IItemEntry, ISaleInvoiceCreateDTO, ISaleInvoiceEditDTO, - IInventoryTransaction, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta, @@ -109,7 +106,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost { */ async getInvoiceOrThrowError(tenantId: number, saleInvoiceId: number) { const { SaleInvoice } = this.tenancy.models(tenantId); - const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId); + const saleInvoice = await SaleInvoice.query().findById(saleInvoiceId) + .withGraphFetched('entries'); if (!saleInvoice) { throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND); @@ -355,7 +353,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { /** * Records the inventory transactions of the given sale invoice in case * the invoice has inventory entries only. - * + * * @param {number} tenantId - Tenant id. * @param {SaleInvoice} saleInvoice - Sale invoice DTO. * @param {number} saleInvoiceId - Sale invoice id. @@ -411,9 +409,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { /** * Records the journal entries of the given sale invoice just * in case the invoice has no inventory items entries. - * - * @param {number} tenantId - - * @param {number} saleInvoiceId + * + * @param {number} tenantId - + * @param {number} saleInvoiceId * @param {boolean} override * @return {Promise} */ @@ -458,7 +456,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { // Triggers 'onInventoryTransactionsDeleted' event. this.eventDispatcher.dispatch( events.saleInvoice.onInventoryTransactionsDeleted, - { tenantId, saleInvoiceId }, + { tenantId, saleInvoiceId } ); } diff --git a/server/src/subscribers/bills.ts b/server/src/subscribers/bills.ts index eddf69d7b..22bbd63e5 100644 --- a/server/src/subscribers/bills.ts +++ b/server/src/subscribers/bills.ts @@ -4,6 +4,7 @@ import events from 'subscribers/events'; import TenancyService from 'services/Tenancy/TenancyService'; import BillsService from 'services/Purchases/Bills'; import JournalPosterService from 'services/Sales/JournalPosterService'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; @EventSubscriber() export default class BillSubscriber { @@ -11,6 +12,7 @@ export default class BillSubscriber { billsService: BillsService; logger: any; journalPosterService: JournalPosterService; + itemsEntriesService: ItemsEntriesService; /** * Constructor method. @@ -20,6 +22,7 @@ export default class BillSubscriber { this.billsService = Container.get(BillsService); this.logger = Container.get('logger'); this.journalPosterService = Container.get(JournalPosterService); + this.itemsEntriesService = Container.get(ItemsEntriesService); } /** @@ -118,7 +121,7 @@ export default class BillSubscriber { this.billsService.recordInventoryTransactions( tenantId, bill.id, - bill.billDate, + bill.billDate ); } @@ -127,12 +130,14 @@ export default class BillSubscriber { */ @On(events.bill.onEdited) async handleOverwritingInventoryTransactions({ tenantId, bill }) { - this.logger.info('[bill] overwriting the inventory transactions.', { tenantId }); + this.logger.info('[bill] overwriting the inventory transactions.', { + tenantId, + }); this.billsService.recordInventoryTransactions( tenantId, bill.id, bill.billDate, - true, + true ); } @@ -141,11 +146,11 @@ export default class BillSubscriber { */ @On(events.bill.onDeleted) async handleRevertInventoryTransactions({ tenantId, billId }) { - this.logger.info('[bill] reverting the bill inventory transactions', { tenantId, billId }); - this.billsService.revertInventoryTransactions( + this.logger.info('[bill] reverting the bill inventory transactions', { tenantId, billId, - ); + }); + this.billsService.revertInventoryTransactions(tenantId, billId); } /** @@ -155,11 +160,47 @@ export default class BillSubscriber { @On(events.bill.onInventoryTransactionsCreated) public async handleComputeItemsCosts({ tenantId, billId }) { this.logger.info('[bill] trying to compute the bill items cost.', { - tenantId, billId, - }); - await this.billsService.scheduleComputeBillItemsCost( tenantId, billId, + }); + await this.billsService.scheduleComputeBillItemsCost(tenantId, billId); + } + + /** + * Increments the sale invoice items once the invoice created. + */ + @On(events.bill.onCreated) + public async handleDecrementSaleInvoiceItemsQuantity({ tenantId, bill }) { + await this.itemsEntriesService.incrementItemsEntries( + tenantId, + bill.entries + ); + } + + /** + * Decrements the sale invoice items once the invoice deleted. + */ + @On(events.bill.onDeleted) + public async handleIncrementSaleInvoiceItemsQuantity({ tenantId, oldBill }) { + await this.itemsEntriesService.decrementItemsQuantity( + tenantId, + oldBill.entries + ); + } + + /** + * Handle increment/decrement the different items quantity once the sale invoice be edited. + */ + @On(events.bill.onEdited) + public async handleChangeSaleInvoiceItemsQuantityOnEdit({ + tenantId, + bill, + oldBill, + }) { + await this.itemsEntriesService.changeItemsQuantity( + tenantId, + bill.entries, + oldBill.entries ); } } diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index 9f13d1f83..9bc51da96 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -5,6 +5,7 @@ import TenancyService from 'services/Tenancy/TenancyService'; import SettingsService from 'services/Settings/SettingsService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleInvoicesService from 'services/Sales/SalesInvoices'; +import ItemsEntriesService from 'services/Items/ItemsEntriesService'; @EventSubscriber() export default class SaleInvoiceSubscriber { @@ -13,6 +14,7 @@ export default class SaleInvoiceSubscriber { settingsService: SettingsService; saleEstimatesService: SaleEstimateService; saleInvoicesService: SaleInvoicesService; + itemsEntriesService: ItemsEntriesService; constructor() { this.logger = Container.get('logger'); @@ -20,6 +22,7 @@ export default class SaleInvoiceSubscriber { this.settingsService = Container.get(SettingsService); this.saleEstimatesService = Container.get(SaleEstimateService); this.saleInvoicesService = Container.get(SaleInvoicesService); + this.itemsEntriesService = Container.get(ItemsEntriesService); } /** @@ -188,4 +191,44 @@ export default class SaleInvoiceSubscriber { saleInvoiceId, ); } + + /** + * Increments the sale invoice items once the invoice created. + */ + @On(events.saleInvoice.onCreated) + public async handleDecrementSaleInvoiceItemsQuantity({ tenantId, saleInvoice }) { + await this.itemsEntriesService.decrementItemsQuantity( + tenantId, + saleInvoice.entries, + ); + } + + /** + * Decrements the sale invoice items once the invoice deleted. + */ + @On(events.saleInvoice.onDeleted) + public async handleIncrementSaleInvoiceItemsQuantity({ tenantId, oldSaleInvoice }) { + await this.itemsEntriesService.incrementItemsEntries( + tenantId, + oldSaleInvoice.entries, + ); + } + + /** + * Handle increment/decrement the different items quantity once the sale invoice be edited. + */ + @On(events.saleInvoice.onEdited) + public async handleChangeSaleInvoiceItemsQuantityOnEdit({ tenantId, saleInvoice, oldSaleInvoice }) { + await this.itemsEntriesService.changeItemsQuantity( + tenantId, + saleInvoice.entries.map((entry) => ({ + ...entry, + quantity: entry.quantity * -1, + })), + oldSaleInvoice.entries.map((entry) => ({ + ...entry, + quantity: entry.quantity * -1, + })), + ); + } }