import { pick, chain } from 'lodash'; import moment from 'moment'; import { IInventoryLotCost, IInventoryTransaction } from "interfaces"; import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod'; type TCostMethod = 'FIFO' | 'LIFO'; export default class InventoryCostLotTracker extends InventoryCostMethod implements IInventoryCostMethod { startingDate: Date; itemId: number; costMethod: TCostMethod; itemsById: Map; inventoryINTrans: any; inventoryByItem: any; costLotsTransactions: IInventoryLotCost[]; inTransactions: any[]; outTransactions: IInventoryTransaction[]; revertJEntriesTransactions: IInventoryTransaction[]; /** * Constructor method. * @param {Date} startingDate - * @param {number} itemId - * @param {string} costMethod - */ constructor( tenantId: number, startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO' ) { super(tenantId, startingDate, itemId); this.startingDate = startingDate; this.itemId = itemId; this.costMethod = costMethod; // Collect cost lots transactions to insert them to the storage in bulk. this.costLotsTransactions= []; // Collect inventory transactions by item id. this.inventoryByItem = {}; // Collection `IN` inventory tranaction by transaction id. this.inventoryINTrans = {}; // Collects `IN` transactions. this.inTransactions = []; // Collects `OUT` transactions. this.outTransactions = []; } /** * Computes items costs from the given date using FIFO or LIFO cost method. * -------- * - Revert the inventory lots after the given date. * - Remove all the journal entries from the inventory transactions * after the given date. * - Re-tracking the inventory lots from inventory transactions. * - Re-write the journal entries from the given inventory transactions. * @async * @return {void} */ public async computeItemCost(): Promise { await this.revertInventoryLots(this.startingDate); await this.fetchInvINTransactions(); await this.fetchInvOUTTransactions(); await this.fetchRevertInvJReferenceIds(); await this.fetchItemsMapped(); this.trackingInventoryINLots(this.inTransactions); this.trackingInventoryOUTLots(this.outTransactions); // Re-tracking the inventory `IN` and `OUT` lots costs. const storedTrackedInvLotsOper = this.storeInventoryLotsCost( this.costLotsTransactions, ); return Promise.all([ storedTrackedInvLotsOper, ]); } /** * Fetched inventory transactions that has date from the starting date and * fetches available IN LOTs transactions that has remaining bigger than zero. * @private */ private async fetchInvINTransactions() { const { InventoryTransaction, InventoryLotCostTracker } = this.tenantModels; const commonBuilder = (builder: any) => { builder.orderBy('date', (this.costMethod === 'LIFO') ? 'DESC': 'ASC'); builder.where('item_id', this.itemId); }; const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction.query() .modify('filterDateRange', this.startingDate) .orderByRaw("FIELD(direction, 'IN', 'OUT')") .onBuild(commonBuilder) .orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC') .withGraphFetched('item'); const availableINLots: IInventoryLotCost[] = await InventoryLotCostTracker.query() .modify('filterDateRange', null, this.startingDate) .orderBy('date', 'ASC') .where('direction', 'IN') .orderBy('lot_number', 'ASC') .onBuild(commonBuilder) .whereNot('remaining', 0); this.inTransactions = [ ...availableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })), ...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })), ]; } /** * Fetches inventory OUT transactions that has date from the starting date. * @private */ private async fetchInvOUTTransactions() { const { InventoryTransaction } = this.tenantModels; const afterOUTTransactions: IInventoryTransaction[] = await InventoryTransaction.query() .modify('filterDateRange', this.startingDate) .orderBy('date', 'ASC') .orderBy('lot_number', 'ASC') .where('item_id', this.itemId) .where('direction', 'OUT') .withGraphFetched('item'); this.outTransactions = [ ...afterOUTTransactions ]; } private async fetchItemsMapped() { const itemsIds = chain(this.inTransactions).map((e) => e.itemId).uniq().value(); const { Item } = this.tenantModels; const storedItems = await Item.query() .where('type', 'inventory') .whereIn('id', itemsIds); this.itemsById = new Map(storedItems.map((item: any) => [item.id, item])); } /** * Fetch the inventory transactions that should revert its journal entries. * @private */ private async fetchRevertInvJReferenceIds() { const { InventoryTransaction } = this.tenantModels; const revertJEntriesTransactions: IInventoryTransaction[] = await InventoryTransaction.query() .select(['transactionId', 'transactionType']) .modify('filterDateRange', this.startingDate) .where('direction', 'OUT') .where('item_id', this.itemId); this.revertJEntriesTransactions = revertJEntriesTransactions; } /** * Revert the inventory lots to the given date by removing the inventory lots * transactions after the given date and increment the remaining that * associate to lot number. * @async * @return {Promise} */ public async revertInventoryLots(startingDate: Date) { const { InventoryLotCostTracker } = this.tenantModels; const asyncOpers: any[] = []; const inventoryLotsTrans = await InventoryLotCostTracker.query() .modify('filterDateRange', this.startingDate) .orderBy('date', 'DESC') .where('item_id', this.itemId) .where('direction', 'OUT'); const deleteInvLotsTrans = InventoryLotCostTracker.query() .modify('filterDateRange', this.startingDate) .where('item_id', this.itemId) .delete(); inventoryLotsTrans.forEach((inventoryLot: IInventoryLotCost) => { if (!inventoryLot.lotNumber) { return; } const incrementOper = InventoryLotCostTracker.query() .where('lot_number', inventoryLot.lotNumber) .where('direction', 'IN') .increment('remaining', inventoryLot.quantity); asyncOpers.push(incrementOper); }); return Promise.all([deleteInvLotsTrans, ...asyncOpers]); } /** * Tracking inventory `IN` lots transactions. * @public * @param {IInventoryTransaction[]} inventoryTransactions - * @return {void} */ public trackingInventoryINLots( inventoryTransactions: IInventoryTransaction[], ) { inventoryTransactions.forEach((transaction: IInventoryTransaction) => { const { itemId, id } = transaction; (this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = [])); const commonLotTransaction: IInventoryLotCost = { ...pick(transaction, [ 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining' ]), }; this.inventoryByItem[itemId].push(id); this.inventoryINTrans[id] = { ...commonLotTransaction, decrement: 0, remaining: commonLotTransaction.remaining || commonLotTransaction.quantity, }; this.costLotsTransactions.push(this.inventoryINTrans[id]); }); } /** * Tracking inventory `OUT` lots transactions. * @public * @param {IInventoryTransaction[]} inventoryTransactions - * @return {void} */ public trackingInventoryOUTLots( inventoryTransactions: IInventoryTransaction[], ) { inventoryTransactions.forEach((transaction: IInventoryTransaction) => { const { itemId, id } = transaction; (this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = [])); const commonLotTransaction: IInventoryLotCost = { ...pick(transaction, [ 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId', 'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining' ]), }; let invRemaining = transaction.quantity; const idsShouldDel: number[] = []; this.inventoryByItem?.[itemId]?.some((_invTransactionId: number) => { const _invINTransaction = this.inventoryINTrans[_invTransactionId]; // Can't continue if the IN transaction remaining equals zero. if (invRemaining <= 0) { return true; } // Can't continue if the IN transaction date is after the current transaction date. if (moment(_invINTransaction.date).isAfter(transaction.date)) { return true; } // Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction. const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0; const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining; const maxDecrement = Math.min(decrement, invRemaining); const cost = maxDecrement * _invINTransaction.rate; _invINTransaction.decrement += maxDecrement; _invINTransaction.remaining = Math.max( _invINTransaction.remaining - maxDecrement, 0, ); invRemaining = Math.max(invRemaining - maxDecrement, 0); this.costLotsTransactions.push({ ...commonLotTransaction, cost, quantity: maxDecrement, lotNumber: _invINTransaction.lotNumber, }); // Pop the 'IN' lots that has zero remaining. if (_invINTransaction.remaining === 0) { idsShouldDel.push(_invTransactionId); } return false; }); if (invRemaining > 0) { this.costLotsTransactions.push({ ...commonLotTransaction, quantity: invRemaining, }); } this.removeInventoryItems(itemId, idsShouldDel); }); } /** * Remove inventory transactions for specific item id. * @private * @param {number} itemId * @param {number[]} idsShouldDel * @return {void} */ private removeInventoryItems(itemId: number, idsShouldDel: number[]) { // Remove the IN transactions that has zero remaining amount. this.inventoryByItem[itemId] = this.inventoryByItem?.[itemId] ?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1); } }