From 47b40f694080c84524f3e2484963e1350b5ea596 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Wed, 23 Dec 2020 21:31:17 +0200 Subject: [PATCH] feat: add opening quantity, cost and date to items. --- server/src/api/controllers/Items.ts | 12 ++++ .../20190822214306_create_items_table.js | 5 ++ server/src/interfaces/InventoryTransaction.ts | 2 +- server/src/interfaces/Item.ts | 10 ++++ server/src/loaders/events.ts | 3 +- server/src/services/Inventory/Inventory.ts | 42 ++++++++++---- server/src/services/Items/ItemsService.ts | 56 ++++++++++++++++++- server/src/subscribers/events.ts | 2 +- server/src/subscribers/items.ts | 32 +++++++++++ 9 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 server/src/subscribers/items.ts diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 58f828584..3b93e30ff 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -25,6 +25,7 @@ export default class ItemsController extends BaseController { router.post('/', [ ...this.validateItemSchema, + ...this.validateNewItemSchema, ], this.validationResult, asyncMiddleware(this.newItem.bind(this)), @@ -90,6 +91,17 @@ export default class ItemsController extends BaseController { return router; } + /** + * New item validation schema. + */ + get validateNewItemSchema(): ValidationChain[] { + return [ + check('opening_quantity').default(0).isInt({ min: 0 }).toInt(), + check('opening_cost').optional({ nullable: true }).isFloat({ min: 0 }).toFloat(), + check('opening_date').optional({ nullable: true }).isISO8601(), + ]; + } + /** * Validate item schema. */ diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js index 83f03280a..7b5afe204 100644 --- a/server/src/database/migrations/20190822214306_create_items_table.js +++ b/server/src/database/migrations/20190822214306_create_items_table.js @@ -17,6 +17,11 @@ exports.up = function (knex) { table.text('sell_description').nullable(); table.text('purchase_description').nullable(); table.integer('quantity_on_hand'); + + table.integer('opening_quantity'); + table.decimal('opening_cost', 13, 3); + table.date('opening_date'); + table.text('note').nullable(); table.boolean('active'); table.integer('category_id').unsigned().index().references('id').inTable('items_categories'); diff --git a/server/src/interfaces/InventoryTransaction.ts b/server/src/interfaces/InventoryTransaction.ts index fb7d3c5e3..34c4e7a66 100644 --- a/server/src/interfaces/InventoryTransaction.ts +++ b/server/src/interfaces/InventoryTransaction.ts @@ -8,7 +8,7 @@ export interface IInventoryTransaction { quantity: number, rate: number, transactionType: string, - transactionId: string, + transactionId: number, lotNumber: string, entryId: number }; diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index f5cf5726f..7062c6cf1 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -21,6 +21,11 @@ export interface IItem{ purchaseDescription: string, quantityOnHand: number, + + openingQuantity: number, + openingCost: number, + openingDate: Date, + note: string, active: boolean, @@ -52,6 +57,11 @@ export interface IItemDTO { purchaseDescription: string, quantityOnHand: number, + + openingQuantity?: number, + openingCost?: number, + openingDate?: Date, + note: string, active: boolean, diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index 6a59554d6..80ea77f2a 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -13,4 +13,5 @@ import 'subscribers/paymentMades'; import 'subscribers/paymentReceives'; import 'subscribers/saleEstimates'; import 'subscribers/saleReceipts'; -import 'subscribers/inventory'; \ No newline at end of file +import 'subscribers/inventory'; +import 'subscribers/items'; \ No newline at end of file diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index c9cdab37c..739309067 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -127,20 +127,38 @@ export default class InventoryService { inventoryEntries: IInventoryTransaction[], deleteOld: boolean, ): Promise { + inventoryEntries.forEach(async (entry: IInventoryTransaction) => { + await this.recordInventoryTransaction( + tenantId, + entry, + deleteOld, + ); + }); + } + + /** + * + * @param {number} tenantId + * @param {IInventoryTransaction} inventoryEntry + * @param {boolean} deleteOld + */ + async recordInventoryTransaction( + tenantId: number, + inventoryEntry: IInventoryTransaction, + deleteOld: boolean = false, + ) { const { InventoryTransaction, Item } = this.tenancy.models(tenantId); - inventoryEntries.forEach(async (entry: any) => { - if (deleteOld) { - await this.deleteInventoryTransactions( - tenantId, - entry.transactionId, - entry.transactionType, - ); - } - await InventoryTransaction.query().insert({ - ...entry, - lotNumber: entry.lotNumber, - }); + if (deleteOld) { + await this.deleteInventoryTransactions( + tenantId, + inventoryEntry.transactionId, + inventoryEntry.transactionType, + ); + } + await InventoryTransaction.query().insert({ + ...inventoryEntry, + lotNumber: inventoryEntry.lotNumber, }); } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index ddd39a7f4..646ae465a 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,9 +1,15 @@ import { defaultTo, difference } from 'lodash'; import { Service, Inject } from 'typedi'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import events from 'subscribers/events'; import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; +import InventoryService from 'services/Inventory/Inventory'; const ERRORS = { NOT_FOUND: 'NOT_FOUND', @@ -32,6 +38,12 @@ export default class ItemsService implements IItemsService { @Inject('logger') logger: any; + @Inject() + inventoryService: InventoryService; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Retrieve item details or throw not found error. * @param {number} tenantId @@ -248,17 +260,55 @@ export default class ItemsService implements IItemsService { itemDTO.inventoryAccountId ); } - const storedItem = await Item.query().insertAndFetch({ + const item = await Item.query().insertAndFetch({ ...itemDTO, active: defaultTo(itemDTO.active, 1), - quantityOnHand: itemDTO.type === 'inventory' ? 0 : null, + quantityOnHand: + itemDTO.type === 'inventory' + ? defaultTo(itemDTO.openingQuantity, 0) + : null, }); this.logger.info('[items] item inserted successfully.', { tenantId, itemDTO, }); + // Triggers `onItemCreated` event. + await this.eventDispatcher.dispatch(events.item.onCreated, { + tenantId, + item, + itemId: item.id, + }); + return item; + } - return storedItem; + /** + * Records the opening items inventory transaction. + * @param {number} tenantId + * @param itemId + * @param openingQuantity + * @param openingCost + * @param openingDate + */ + public async recordOpeningItemsInventoryTransaction( + tenantId: number, + itemId: number, + openingQuantity: number, + openingCost: number, + openingDate: Date + ): Promise { + // Gets the next inventory lot number. + const lotNumber = this.inventoryService.getNextLotNumber(tenantId); + + await this.inventoryService.recordInventoryTransaction(tenantId, { + date: openingDate, + quantity: openingQuantity, + rate: openingCost, + direction: 'IN', + transactionType: 'OpeningItem', + itemId, + lotNumber, + }); + await this.inventoryService.incrementNextLotNumber(tenantId); } /** diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index d8781d512..28a8622b0 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -168,7 +168,7 @@ export default { /** * Items service. */ - items: { + item: { onCreated: 'onItemCreated', onEdited: 'onItemEdited', onDeleted: 'onItemDeleted', diff --git a/server/src/subscribers/items.ts b/server/src/subscribers/items.ts new file mode 100644 index 000000000..c68374c73 --- /dev/null +++ b/server/src/subscribers/items.ts @@ -0,0 +1,32 @@ +import { Container } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import ItemsService from 'services/Items/ItemsService'; + +@EventSubscriber() +export default class ItemsSubscriber{ + itemsService: ItemsService; + + constructor() { + this.itemsService = Container.get(ItemsService); + }; + + /** + * Handle writing opening item inventory transaction. + */ + @On(events.item.onCreated) + handleWriteOpeningInventoryTransaction({ tenantId, item }) { + // Can't continue if the opeing cost, quantity or opening date was empty. + if (!item.openingCost || !item.openingQuantity || !item.openingDate) { + return; + } + // Records the opeing items inventory transaction once the item created. + this.itemsService.recordOpeningItemsInventoryTransaction( + tenantId, + item.id, + item.openingQuantity, + item.openingCost, + item.openingDate, + ) + } +} \ No newline at end of file