refactor: inventory cost to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-11 22:12:08 +02:00
parent 40b7daa2e3
commit 67ae7ad037
44 changed files with 1436 additions and 1763 deletions

View File

@@ -1,214 +0,0 @@
// import { pick } from 'lodash';
// import { Inject, Injectable } from '@nestjs/common';
// import { EventEmitter2 } from '@nestjs/event-emitter';
// import { Knex } from 'knex';
// import {
// IInventoryLotCost,
// IInventoryTransaction,
// TInventoryTransactionDirection,
// IItemEntry,
// IItemEntryTransactionType,
// IInventoryTransactionsCreatedPayload,
// IInventoryTransactionsDeletedPayload,
// IInventoryItemCostScheduledPayload,
// } from '@/interfaces';
// import { InventoryAverageCostMethod } from './InventoryAverageCost';
// import { InventoryCostLotTracker } from './InventoryCostLotTracker';
// import { ItemsEntriesService } from '../Items/ItemsEntries.service';
// import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
// import { Item } from '../Items/models/Item';
// import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
// import { SettingsStore } from '../Settings/SettingsStore';
// import { events } from '@/common/events/events';
// import { InventoryTransaction } from './models/InventoryTransaction';
// import InventoryCostMethod from './InventoryCostMethod';
// @Injectable()
// export class InventoryService {
// constructor(
// private readonly eventEmitter: EventEmitter2,
// private readonly uow: UnitOfWork,
// @Inject(InventoryTransaction.name)
// private readonly inventoryTransactionModel: typeof InventoryTransaction,
// @Inject(InventoryCostLotTracker.name)
// private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker,
// @Inject(SETTINGS_PROVIDER)
// private readonly settings: SettingsStore,
// ) {}
// /**
// * Transforms the items entries to inventory transactions.
// */
// transformItemEntriesToInventory(transaction: {
// transactionId: number;
// transactionType: IItemEntryTransactionType;
// transactionNumber?: string;
// exchangeRate?: number;
// warehouseId: number | null;
// date: Date | string;
// direction: TInventoryTransactionDirection;
// entries: IItemEntry[];
// createdAt: Date;
// }): IInventoryTransaction[] {
// const exchangeRate = transaction.exchangeRate || 1;
// return transaction.entries.map((entry: IItemEntry) => ({
// ...pick(entry, ['itemId', 'quantity']),
// rate: entry.rate * exchangeRate,
// transactionType: transaction.transactionType,
// transactionId: transaction.transactionId,
// direction: transaction.direction,
// date: transaction.date,
// entryId: entry.id,
// createdAt: transaction.createdAt,
// costAccountId: entry.costAccountId,
// warehouseId: entry.warehouseId || transaction.warehouseId,
// meta: {
// transactionNumber: transaction.transactionNumber,
// description: entry.description,
// },
// }));
// }
// async computeItemCost(fromDate: Date, itemId: number) {
// return this.uow.withTransaction((trx: Knex.Transaction) => {
// return this.computeInventoryItemCost(fromDate, itemId);
// });
// }
// /**
// * 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 {Date} fromDate - From date.
// * @param {number} itemId - Item id.
// */
// async computeInventoryItemCost(
// fromDate: Date,
// itemId: number,
// trx?: Knex.Transaction,
// ) {
// // Fetches the item with associated item category.
// const item = await Item.query().findById(itemId);
// // Cannot continue if the given item was not inventory item.
// if (item.type !== 'inventory') {
// throw new Error('You could not compute item cost has no inventory type.');
// }
// let costMethodComputer: InventoryCostMethod;
// // Switch between methods based on the item cost method.
// switch ('AVG') {
// case 'FIFO':
// case 'LIFO':
// costMethodComputer = new InventoryCostLotTracker(
// tenantId,
// fromDate,
// itemId,
// );
// break;
// case 'AVG':
// costMethodComputer = new InventoryAverageCostMethod(
// fromDate,
// itemId,
// trx,
// );
// break;
// }
// return costMethodComputer.computeItemCost();
// }
// /**
// * Schedule item cost compute job.
// * @param {number} tenantId
// * @param {number} itemId
// * @param {Date} startingDate
// */
// async scheduleComputeItemCost(
// tenantId: number,
// itemId: number,
// startingDate: Date | string,
// ) {
// const agenda = Container.get('agenda');
// const commonJobsQuery = {
// name: 'compute-item-cost',
// lastRunAt: { $exists: false },
// 'data.tenantId': tenantId,
// 'data.itemId': itemId,
// };
// // Cancel any `compute-item-cost` in the queue has upper starting date
// // with the same given item.
// await agenda.cancel({
// ...commonJobsQuery,
// 'data.startingDate': { $lte: startingDate },
// });
// // Retrieve any `compute-item-cost` in the queue has lower starting date
// // with the same given item.
// const dependsJobs = await agenda.jobs({
// ...commonJobsQuery,
// 'data.startingDate': { $gte: startingDate },
// });
// // If the depends jobs cleared.
// if (dependsJobs.length === 0) {
// await agenda.schedule(
// config.scheduleComputeItemCost,
// 'compute-item-cost',
// {
// startingDate,
// itemId,
// tenantId,
// },
// );
// // Triggers `onComputeItemCostJobScheduled` event.
// await this.eventPublisher.emitAsync(
// events.inventory.onComputeItemCostJobScheduled,
// {
// startingDate,
// itemId,
// tenantId,
// } as IInventoryItemCostScheduledPayload,
// );
// } else {
// // Re-schedule the jobs that have higher date from current moment.
// await Promise.all(
// dependsJobs.map((job) =>
// job.schedule(config.scheduleComputeItemCost).save(),
// ),
// );
// }
// }
// /**
// * Mark item cost computing is running.
// * @param {boolean} isRunning -
// */
// async markItemsCostComputeRunning(isRunning: boolean = true) {
// this.settings.set({
// key: 'cost_compute_running',
// group: 'inventory',
// value: isRunning,
// });
// await this.settings.save();
// }
// /**
// * Checks if the items cost compute is running.
// * @returns {boolean}
// */
// isItemsCostComputeRunning() {
// return (
// this.settings.get({
// key: 'cost_compute_running',
// group: 'inventory',
// }) ?? false
// );
// }
// }

View File

@@ -1,254 +0,0 @@
// import { pick } from 'lodash';
// import { Knex } from 'knex';
// import InventoryCostMethod from './InventoryCostMethod';
// import { InventoryTransaction } from './models/InventoryTransaction';
// export class InventoryAverageCostMethod extends InventoryCostMethod {
// startingDate: Date;
// itemId: number;
// costTransactions: any[];
// trx: Knex.Transaction;
// /**
// * Constructor method.
// * @param {number} tenantId - The given tenant id.
// * @param {Date} startingDate -
// * @param {number} itemId - The given inventory item id.
// */
// constructor(
// tenantId: number,
// startingDate: Date,
// itemId: number,
// trx?: Knex.Transaction,
// ) {
// super(tenantId, startingDate, itemId);
// this.trx = trx;
// this.startingDate = startingDate;
// this.itemId = itemId;
// this.costTransactions = [];
// }
// /**
// * Computes items costs from the given date using average cost method.
// * ----------
// * - Calculate the items average cost in the given date.
// * - Remove the journal entries that associated to the inventory transacions
// * after the given date.
// * - Re-compute the inventory transactions and re-write the journal entries
// * after the given date.
// * ----------
// * @async
// * @param {Date} startingDate
// * @param {number} referenceId
// * @param {string} referenceType
// */
// public async computeItemCost() {
// const { InventoryTransaction } = this.tenantModels;
// const { averageCost, openingQuantity, openingCost } =
// await this.getOpeningAverageCost(this.startingDate, this.itemId);
// const afterInvTransactions =
// await InventoryTransaction.query()
// .modify('filterDateRange', this.startingDate)
// .orderBy('date', 'ASC')
// .orderByRaw("FIELD(direction, 'IN', 'OUT')")
// .orderBy('createdAt', 'ASC')
// .where('item_id', this.itemId)
// .withGraphFetched('item');
// // Tracking inventroy transactions and retrieve cost transactions based on
// // average rate cost method.
// const costTransactions = this.trackingCostTransactions(
// afterInvTransactions,
// openingQuantity,
// openingCost,
// );
// // Revert the inveout out lots transactions
// await this.revertTheInventoryOutLotTrans();
// // Store inventory lots cost transactions.
// await this.storeInventoryLotsCost(costTransactions);
// }
// /**
// * Get items Average cost from specific date from inventory transactions.
// * @async
// * @param {Date} closingDate
// * @return {number}
// */
// public async getOpeningAverageCost(closingDate: Date, itemId: number) {
// const { InventoryCostLotTracker } = this.tenantModels;
// const commonBuilder = (builder: any) => {
// if (closingDate) {
// builder.where('date', '<', closingDate);
// }
// builder.where('item_id', itemId);
// builder.sum('rate as rate');
// builder.sum('quantity as quantity');
// builder.sum('cost as cost');
// builder.first();
// };
// // Calculates the total inventory total quantity and rate `IN` transactions.
// const inInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
// .onBuild(commonBuilder)
// .where('direction', 'IN');
// // Calculates the total inventory total quantity and rate `OUT` transactions.
// const outInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
// .onBuild(commonBuilder)
// .where('direction', 'OUT');
// const [inInvSumation, outInvSumation] = await Promise.all([
// inInvSumationOper,
// outInvSumationOper,
// ]);
// return this.computeItemAverageCost(
// inInvSumation?.cost || 0,
// inInvSumation?.quantity || 0,
// outInvSumation?.cost || 0,
// outInvSumation?.quantity || 0,
// );
// }
// /**
// * Computes the item average cost.
// * @static
// * @param {number} quantityIn
// * @param {number} rateIn
// * @param {number} quantityOut
// * @param {number} rateOut
// */
// public computeItemAverageCost(
// totalCostIn: number,
// totalQuantityIn: number,
// totalCostOut: number,
// totalQuantityOut: number,
// ) {
// const openingCost = totalCostIn - totalCostOut;
// const openingQuantity = totalQuantityIn - totalQuantityOut;
// const averageCost = openingQuantity ? openingCost / openingQuantity : 0;
// return { averageCost, openingCost, openingQuantity };
// }
// private getCost(rate: number, quantity: number) {
// return quantity ? rate * quantity : rate;
// }
// /**
// * Records the journal entries from specific item inventory transactions.
// * @param {IInventoryTransaction[]} invTransactions
// * @param {number} openingAverageCost
// * @param {string} referenceType
// * @param {number} referenceId
// * @param {JournalCommand} journalCommands
// */
// public trackingCostTransactions(
// invTransactions: InventoryTransaction[],
// openingQuantity: number = 0,
// openingCost: number = 0,
// ) {
// const costTransactions: any[] = [];
// // Cumulative item quantity and cost. This will decrement after
// // each out transactions depends on its quantity and cost.
// let accQuantity: number = openingQuantity;
// let accCost: number = openingCost;
// invTransactions.forEach((invTransaction: InventoryTransaction) => {
// const commonEntry = {
// invTransId: invTransaction.id,
// ...pick(invTransaction, [
// 'date',
// 'direction',
// 'itemId',
// 'quantity',
// 'rate',
// 'entryId',
// 'transactionId',
// 'transactionType',
// 'createdAt',
// 'costAccountId',
// 'branchId',
// 'warehouseId',
// ]),
// inventoryTransactionId: invTransaction.id,
// };
// switch (invTransaction.direction) {
// case 'IN':
// const inCost = this.getCost(
// invTransaction.rate,
// invTransaction.quantity,
// );
// // Increases the quantity and cost in `IN` inventory transactions.
// accQuantity += invTransaction.quantity;
// accCost += inCost;
// costTransactions.push({
// ...commonEntry,
// cost: inCost,
// });
// break;
// case 'OUT':
// // Average cost = Total cost / Total quantity
// const averageCost = accQuantity ? accCost / accQuantity : 0;
// const quantity =
// accQuantity > 0
// ? Math.min(invTransaction.quantity, accQuantity)
// : invTransaction.quantity;
// // Cost = the transaction quantity * Average cost.
// const cost = this.getCost(averageCost, quantity);
// // Revenue = transaction quanity * rate.
// // const revenue = quantity * invTransaction.rate;
// costTransactions.push({
// ...commonEntry,
// quantity,
// cost,
// });
// accQuantity = Math.max(accQuantity - quantity, 0);
// accCost = Math.max(accCost - cost, 0);
// if (invTransaction.quantity > quantity) {
// const remainingQuantity = Math.max(
// invTransaction.quantity - quantity,
// 0,
// );
// const remainingIncome = remainingQuantity * invTransaction.rate;
// costTransactions.push({
// ...commonEntry,
// quantity: remainingQuantity,
// cost: 0,
// });
// accQuantity = Math.max(accQuantity - remainingQuantity, 0);
// accCost = Math.max(accCost - remainingIncome, 0);
// }
// break;
// }
// });
// return costTransactions;
// }
// /**
// * Reverts the inventory lots `OUT` transactions.
// * @param {Date} openingDate - Opening date.
// * @param {number} itemId - Item id.
// * @returns {Promise<void>}
// */
// async revertTheInventoryOutLotTrans(): Promise<void> {
// const { InventoryCostLotTracker } = this.tenantModels;
// await InventoryCostLotTracker.query(this.trx)
// .modify('filterDateRange', this.startingDate)
// .orderBy('date', 'DESC')
// .where('item_id', this.itemId)
// .delete();
// }
// }

View File

@@ -1,13 +1,13 @@
import { Module } from '@nestjs/common';
import { InventoryCostGLStorage } from './InventoryCostGLStorage.service';
import { InventoryCostGLStorage } from './commands/InventoryCostGLStorage.service';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { InventoryCostLotTracker } from './models/InventoryCostLotTracker';
import { InventoryTransaction } from './models/InventoryTransaction';
import { InventoryCostGLBeforeWriteSubscriber } from './subscribers/InventoryCostGLBeforeWriteSubscriber';
import { InventoryItemsQuantitySyncService } from './InventoryItemsQuantitySync.service';
import { InventoryCostMethod } from './InventoryCostMethod';
import { InventoryTransactionsService } from './InventoryTransactions.service';
import { InventoryItemsQuantitySyncService } from './commands/InventoryItemsQuantitySync.service';
import { InventoryTransactionsService } from './commands/InventoryTransactions.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { InventoryComputeCostService } from './commands/InventoryComputeCost.service';
const models = [
RegisterTenancyModel(InventoryCostLotTracker),
@@ -20,8 +20,8 @@ const models = [
InventoryCostGLBeforeWriteSubscriber,
InventoryCostGLStorage,
InventoryItemsQuantitySyncService,
InventoryCostMethod,
InventoryTransactionsService,
InventoryComputeCostService,
],
exports: [...models, InventoryTransactionsService],
})

View File

@@ -1,12 +1,19 @@
import { Injectable } from '@nestjs/common';
// import { InventoryItemCostService } from './InventoryCosts.service';
import { IInventoryItemCostMeta } from './types/InventoryCost.types';
import { InventoryItemCostService } from './commands/InventoryCosts.service';
@Injectable()
export class InventoryCostApplication {
constructor(
// private readonly inventoryCost: InventoryItemCostService,
) {}
constructor(private readonly inventoryCost: InventoryItemCostService) {}
/**
* Computes the item cost.
* @param {Date} fromDate - From date.
* @param {number} itemId - Item id.
* @returns {Promise<Map<number, IInventoryItemCostMeta>>}
*/
computeItemCost(fromDate: Date, itemId: number) {
return this.inventoryCost.getItemsInventoryValuation([itemId], fromDate);
}
/**
* Retrieves the items inventory valuation list.
@@ -14,14 +21,14 @@ export class InventoryCostApplication {
* @param {Date} date
* @returns {Promise<IInventoryItemCostMeta[]>}
*/
public getItemsInventoryValuationList = async (
async getItemsInventoryValuation(
itemsId: number[],
date: Date
): Promise<any> => {
// const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
// itemsId,
// date
// );
// return [...itemsMap.values()];
};
date: Date,
): Promise<any> {
const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
itemsId,
date,
);
return [...itemsMap.values()];
}
}

View File

@@ -1,302 +0,0 @@
// 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 class InventoryCostLotTracker extends InventoryCostMethod {
// startingDate: Date;
// itemId: number;
// costMethod: TCostMethod;
// itemsById: Map<number, any>;
// 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<any> {
// 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);
// }
// }

View File

@@ -1,149 +0,0 @@
// import { keyBy, get } from 'lodash';
// import { Knex } from 'knex';
// import * as R from 'ramda';
// import { IInventoryItemCostMeta } from './types/InventoryCost.types';
// import { Inject, Injectable } from '@nestjs/common';
// import { InventoryTransaction } from './models/InventoryTransaction';
// import { InventoryCostLotTracker } from './models/InventoryCostLotTracker';
// import { Item } from '../Items/models/Item';
// @Injectable()
// export class InventoryItemCostService {
// constructor(
// @Inject(InventoryTransaction.name)
// private readonly inventoryTransactionModel: typeof InventoryTransaction,
// @Inject(InventoryCostLotTracker.name)
// private readonly inventoryCostLotTrackerModel: typeof InventoryCostLotTracker,
// @Inject(Item.name)
// private readonly itemModel: typeof Item,
// ) {}
// /**
// * Common query of items inventory valuation.
// * @param {number[]} itemsIds -
// * @param {Date} date -
// * @param {Knex.QueryBuilder} builder -
// */
// private itemsInventoryValuationCommonQuery = R.curry(
// (itemsIds: number[], date: Date, builder: Knex.QueryBuilder) => {
// if (date) {
// builder.where('date', '<', date);
// }
// builder.whereIn('item_id', itemsIds);
// builder.sum('rate as rate');
// builder.sum('quantity as quantity');
// builder.sum('cost as cost');
// builder.groupBy('item_id');
// builder.select(['item_id']);
// }
// );
// /**
// *
// * @param {} INValuationMap -
// * @param {} OUTValuationMap -
// * @param {number} itemId
// */
// private getItemInventoryMeta = R.curry(
// (
// INValuationMap,
// OUTValuationMap,
// itemId: number
// ): IInventoryItemCostMeta => {
// const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
// const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
// const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0);
// const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0);
// const valuation = INCost - OUTCost;
// const quantity = INQuantity - OUTQuantity;
// const average = quantity ? valuation / quantity : 0;
// return { itemId, valuation, quantity, average };
// }
// );
// /**
// *
// * @param {number} tenantId
// * @param {number} itemsId
// * @param {Date} date
// * @returns
// */
// private getItemsInventoryINAndOutAggregated = (
// itemsId: number[],
// date: Date
// ): Promise<any> => {
// const commonBuilder = this.itemsInventoryValuationCommonQuery(
// itemsId,
// date
// );
// const INValuationOper = this.inventoryCostLotTrackerModel.query()
// .onBuild(commonBuilder)
// .where('direction', 'IN');
// const OUTValuationOper = this.inventoryCostLotTrackerModel.query()
// .onBuild(commonBuilder)
// .where('direction', 'OUT');
// return Promise.all([OUTValuationOper, INValuationOper]);
// };
// /**
// *
// * @param {number} tenantId -
// * @param {number[]} itemsIds -
// * @param {Date} date -
// */
// private getItemsInventoryInOutMap = async (
// itemsId: number[],
// date: Date
// ) => {
// const [OUTValuation, INValuation] =
// await this.getItemsInventoryINAndOutAggregated(itemsId, date);
// const OUTValuationMap = keyBy(OUTValuation, 'itemId');
// const INValuationMap = keyBy(INValuation, 'itemId');
// return [OUTValuationMap, INValuationMap];
// };
// /**
// *
// * @param {number} tenantId
// * @param {number} itemId
// * @param {Date} date
// * @returns {Promise<Map<number, IInventoryItemCostMeta>>}
// */
// public getItemsInventoryValuation = async (
// itemsId: number[],
// date: Date
// ): Promise<Map<number, IInventoryItemCostMeta>> => {
// // Retrieves the inventory items.
// const items = await this.itemModel.query()
// .whereIn('id', itemsId)
// .where('type', 'inventory');
// // Retrieves the inventory items ids.
// const inventoryItemsIds: number[] = items.map((item) => item.id);
// // Retreives the items inventory IN/OUT map.
// const [OUTValuationMap, INValuationMap] =
// await this.getItemsInventoryInOutMap(itemsId, date);
// const getItemValuation = this.getItemInventoryMeta(
// INValuationMap,
// OUTValuationMap
// );
// const itemsValuations = inventoryItemsIds.map(getItemValuation);
// const itemsValuationsMap = new Map(
// itemsValuations.map((i) => [i.itemId, i])
// );
// return itemsValuationsMap;
// };
// }

View File

@@ -0,0 +1,97 @@
import { pick } from 'lodash';
import { Knex } from 'knex';
import { InventoryTransaction } from '../models/InventoryTransaction';
import { InventoryItemOpeningAvgCostService } from './InventoryItemOpeningAvgCost.service';
import { Inject, Injectable } from '@nestjs/common';
import { InventoryAverageCostMethod } from './InventoryAverageCostMethod';
import { StoreInventoryLotsCostService } from './StoreInventortyLotsCost.service';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
@Injectable()
export class InventoryAverageCostMethodService {
constructor(
private readonly itemOpeningAvgCostService: InventoryItemOpeningAvgCostService,
private readonly storeInventoryLotsCostService: StoreInventoryLotsCostService,
@Inject(InventoryTransaction.name)
private readonly inventoryTransactionModel: TenantModelProxy<
typeof InventoryTransaction
>,
) {}
/**
* Retrieves the inventory cost lots.
* @param {Date} startingDate
* @param {number} itemId
* @param {number} openingQuantity
* @param {number} openingCost
* @returns {Promise<any[]>}
*/
async getInventoryCostLots(
startingDate: Date,
itemId: number,
openingQuantity: number,
openingCost: number,
) {
const afterInvTransactions = await this.inventoryTransactionModel()
.query()
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
.orderBy('createdAt', 'ASC')
.where('item_id', itemId)
.withGraphFetched('item');
const avgCostTracker = new InventoryAverageCostMethod();
// Tracking inventroy transactions and retrieve cost transactions based on
// average rate cost method.
return avgCostTracker.trackingCostTransactions(
afterInvTransactions,
openingQuantity,
openingCost,
);
}
/**
* Computes items costs from the given date using average cost method.
* ----------
* - Calculate the items average cost in the given date.
* - Remove the journal entries that associated to the inventory transacions
* after the given date.
* - Re-compute the inventory transactions and re-write the journal entries
* after the given date.
* ----------
* @param {Date} startingDate
* @param {number} itemId
* @param {Knex.Transaction} trx
*/
public async computeItemCost(
startingDate: Date,
itemId: number,
trx?: Knex.Transaction,
) {
const { openingQuantity, openingCost } =
await this.itemOpeningAvgCostService.getOpeningAverageCost(
startingDate,
itemId,
);
// Retrieves the new calculated inventory cost lots.
const inventoryCostLots = await this.getInventoryCostLots(
startingDate,
itemId,
openingQuantity,
openingCost,
);
// Revert the inveout out lots transactions
await this.storeInventoryLotsCostService.revertInventoryCostLotTransactions(
startingDate,
itemId,
trx,
);
// Store inventory lots cost transactions.
await this.storeInventoryLotsCostService.storeInventoryLotsCost(
inventoryCostLots,
trx,
);
}
}

View File

@@ -0,0 +1,120 @@
import { pick } from 'lodash';
import { Knex } from 'knex';
import { InventoryTransaction } from '../models/InventoryTransaction';
export class InventoryAverageCostMethod {
/**
* Constructor method.
* @param {number} tenantId - The given tenant id.
* @param {Date} startingDate -
* @param {number} itemId - The given inventory item id.
*/
constructor() {}
/**
* Computes the cost of the given rate and quantity.
* @param {number} rate - The given rate.
* @param {number} quantity - The given quantity.
* @returns {number}
*/
private getCost(rate: number, quantity: number) {
return quantity ? rate * quantity : rate;
}
/**
* Records the journal entries from specific item inventory transactions.
* @param {IInventoryTransaction[]} invTransactions
* @param {number} openingAverageCost
* @param {string} referenceType
* @param {number} referenceId
* @param {JournalCommand} journalCommands
*/
public trackingCostTransactions(
invTransactions: InventoryTransaction[],
openingQuantity: number = 0,
openingCost: number = 0,
) {
const costTransactions: any[] = [];
// Cumulative item quantity and cost. This will decrement after
// each out transactions depends on its quantity and cost.
let accQuantity: number = openingQuantity;
let accCost: number = openingCost;
invTransactions.forEach((invTransaction: InventoryTransaction) => {
const commonEntry = {
invTransId: invTransaction.id,
...pick(invTransaction, [
'date',
'direction',
'itemId',
'quantity',
'rate',
'entryId',
'transactionId',
'transactionType',
'createdAt',
'costAccountId',
'branchId',
'warehouseId',
]),
inventoryTransactionId: invTransaction.id,
};
switch (invTransaction.direction) {
case 'IN':
const inCost = this.getCost(
invTransaction.rate,
invTransaction.quantity,
);
// Increases the quantity and cost in `IN` inventory transactions.
accQuantity += invTransaction.quantity;
accCost += inCost;
costTransactions.push({
...commonEntry,
cost: inCost,
});
break;
case 'OUT':
// Average cost = Total cost / Total quantity
const averageCost = accQuantity ? accCost / accQuantity : 0;
const quantity =
accQuantity > 0
? Math.min(invTransaction.quantity, accQuantity)
: invTransaction.quantity;
// Cost = the transaction quantity * Average cost.
const cost = this.getCost(averageCost, quantity);
// Revenue = transaction quanity * rate.
// const revenue = quantity * invTransaction.rate;
costTransactions.push({
...commonEntry,
quantity,
cost,
});
accQuantity = Math.max(accQuantity - quantity, 0);
accCost = Math.max(accCost - cost, 0);
if (invTransaction.quantity > quantity) {
const remainingQuantity = Math.max(
invTransaction.quantity - quantity,
0,
);
const remainingIncome = remainingQuantity * invTransaction.rate;
costTransactions.push({
...commonEntry,
quantity: remainingQuantity,
cost: 0,
});
accQuantity = Math.max(accQuantity - remainingQuantity, 0);
accCost = Math.max(accCost - remainingIncome, 0);
}
break;
}
});
return costTransactions;
}
}

View File

@@ -0,0 +1,159 @@
import { pick } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { Item } from '../../Items/models/Item';
import { SETTINGS_PROVIDER } from '../../Settings/Settings.types';
import { SettingsStore } from '../../Settings/SettingsStore';
import { InventoryTransaction } from '../models/InventoryTransaction';
import { IItemEntryTransactionType } from '../../TransactionItemEntry/ItemEntry.types';
import { ModelObject } from 'objection';
import { ItemEntry } from '../../TransactionItemEntry/models/ItemEntry';
import { TInventoryTransactionDirection } from '../types/InventoryCost.types';
import { InventoryAverageCostMethodService } from './InventoryAverageCostMethod.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class InventoryComputeCostService {
constructor(
private readonly uow: UnitOfWork,
private readonly inventoryAverageCostMethod: InventoryAverageCostMethodService,
@Inject(Item.name)
private readonly itemModel: TenantModelProxy<typeof Item>,
@Inject(SETTINGS_PROVIDER)
private readonly settingsStore: () => SettingsStore,
) {}
/**
* Compute item cost.
* @param {Date} fromDate - From date.
* @param {number} itemId - Item id.
* @returns {Promise<void>}
*/
async computeItemCost(fromDate: Date, itemId: number) {
return this.uow.withTransaction((trx: Knex.Transaction) => {
return this.computeInventoryItemCost(fromDate, itemId, trx);
});
}
/**
* 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 {Date} fromDate - From date.
* @param {number} itemId - Item id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
async computeInventoryItemCost(
fromDate: Date,
itemId: number,
trx?: Knex.Transaction,
) {
// Fetches the item with associated item category.
const item = await this.itemModel().query().findById(itemId);
// Cannot continue if the given item was not inventory item.
if (item.type !== 'inventory') {
throw new Error('You could not compute item cost has no inventory type.');
}
return this.inventoryAverageCostMethod.computeItemCost(
fromDate,
itemId,
trx,
);
}
/**
* Schedule item cost compute job.
* @param {number} tenantId
* @param {number} itemId
* @param {Date} startingDate
*/
async scheduleComputeItemCost(
tenantId: number,
itemId: number,
startingDate: Date | string,
) {
// const agenda = Container.get('agenda');
// const commonJobsQuery = {
// name: 'compute-item-cost',
// lastRunAt: { $exists: false },
// 'data.tenantId': tenantId,
// 'data.itemId': itemId,
// };
// // Cancel any `compute-item-cost` in the queue has upper starting date
// // with the same given item.
// await agenda.cancel({
// ...commonJobsQuery,
// 'data.startingDate': { $lte: startingDate },
// });
// // Retrieve any `compute-item-cost` in the queue has lower starting date
// // with the same given item.
// const dependsJobs = await agenda.jobs({
// ...commonJobsQuery,
// 'data.startingDate': { $gte: startingDate },
// });
// // If the depends jobs cleared.
// if (dependsJobs.length === 0) {
// await agenda.schedule(
// this.config.get('inventory.scheduleComputeItemCost'),
// 'compute-item-cost',
// {
// startingDate,
// itemId,
// tenantId,
// },
// );
// // Triggers `onComputeItemCostJobScheduled` event.
// await this.eventEmitter.emitAsync(
// events.inventory.onComputeItemCostJobScheduled,
// {
// startingDate,
// itemId,
// tenantId,
// } as IInventoryItemCostScheduledPayload,
// );
// } else {
// // Re-schedule the jobs that have higher date from current moment.
// await Promise.all(
// dependsJobs.map((job) =>
// job
// .schedule(this.config.get('inventory.scheduleComputeItemCost'))
// .save(),
// ),
// );
// }
}
/**
* Mark item cost computing is running.
* @param {boolean} isRunning -
*/
async markItemsCostComputeRunning(isRunning: boolean = true) {
const settings = await this.settingsStore();
settings.set({
key: 'cost_compute_running',
group: 'inventory',
value: isRunning,
});
await settings.save();
}
/**
* Checks if the items cost compute is running.
* @returns {boolean}
*/
async isItemsCostComputeRunning() {
const settings = await this.settingsStore();
return (
settings.get({
key: 'cost_compute_running',
group: 'inventory',
}) ?? false
);
}
}

View File

@@ -1,8 +1,9 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
import { Ledger } from '../Ledger/Ledger';
import { AccountTransaction } from '../Accounts/models/AccountTransaction.model';
import { LedgerStorageService } from '../../Ledger/LedgerStorage.service';
import { Ledger } from '../../Ledger/Ledger';
import { AccountTransaction } from '../../Accounts/models/AccountTransaction.model';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
@Injectable()
export class InventoryCostGLStorage {
@@ -10,7 +11,9 @@ export class InventoryCostGLStorage {
private readonly ledgerStorage: LedgerStorageService,
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction,
private readonly accountTransactionModel: TenantModelProxy<
typeof AccountTransaction
>,
) {}
/**
@@ -23,7 +26,7 @@ export class InventoryCostGLStorage {
trx?: Knex.Transaction,
): Promise<void> {
// Retrieve transactions from specific date range and costable transactions only.
const transactions = await this.accountTransactionModel
const transactions = await this.accountTransactionModel()
.query()
.where('costable', true)
.modify('filterDateRange', startingDate)

View File

@@ -0,0 +1,135 @@
import { keyBy, get } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import { IInventoryItemCostMeta } from '../types/InventoryCost.types';
import { Inject, Injectable } from '@nestjs/common';
import { InventoryTransaction } from '../models/InventoryTransaction';
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
import { Item } from '../../Items/models/Item';
@Injectable()
export class InventoryItemCostService {
constructor(
@Inject(InventoryTransaction.name)
private readonly inventoryTransactionModel: typeof InventoryTransaction,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTrackerModel: typeof InventoryCostLotTracker,
@Inject(Item.name)
private readonly itemModel: typeof Item,
) {}
/**
*
* @param {} INValuationMap -
* @param {} OUTValuationMap -
* @param {number} itemId
*/
private getItemInventoryMeta = R.curry(
(
INValuationMap,
OUTValuationMap,
itemId: number,
): IInventoryItemCostMeta => {
const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0);
const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0);
const valuation = INCost - OUTCost;
const quantity = INQuantity - OUTQuantity;
const average = quantity ? valuation / quantity : 0;
return { itemId, valuation, quantity, average };
},
);
/**
*
* @param {number} tenantId
* @param {number} itemsId
* @param {Date} date
* @returns
*/
private getItemsInventoryINAndOutAggregated = (
itemsId: number[],
date: Date,
): Promise<any> => {
const commonBuilder = (builder: Knex.QueryBuilder) => {
if (date) {
builder.where('date', '<', date);
}
builder.whereIn('item_id', itemsId);
builder.sum('rate as rate');
builder.sum('quantity as quantity');
builder.sum('cost as cost');
builder.groupBy('item_id');
builder.select(['item_id']);
};
const INValuationOper = this.inventoryCostLotTrackerModel
.query()
.onBuild(commonBuilder)
.where('direction', 'IN');
const OUTValuationOper = this.inventoryCostLotTrackerModel
.query()
.onBuild(commonBuilder)
.where('direction', 'OUT');
return Promise.all([OUTValuationOper, INValuationOper]);
};
/**
*
* @param {number} tenantId -
* @param {number[]} itemsIds -
* @param {Date} date -
*/
private getItemsInventoryInOutMap = async (itemsId: number[], date: Date) => {
const [OUTValuation, INValuation] =
await this.getItemsInventoryINAndOutAggregated(itemsId, date);
const OUTValuationMap = keyBy(OUTValuation, 'itemId');
const INValuationMap = keyBy(INValuation, 'itemId');
return [OUTValuationMap, INValuationMap];
};
/**
*
* @param {number} tenantId
* @param {number} itemId
* @param {Date} date
* @returns {Promise<Map<number, IInventoryItemCostMeta>>}
*/
public getItemsInventoryValuation = async (
itemsId: number[],
date: Date,
): Promise<Map<number, IInventoryItemCostMeta>> => {
// Retrieves the inventory items.
const items = await this.itemModel
.query()
.whereIn('id', itemsId)
.where('type', 'inventory');
// Retrieves the inventory items ids.
const inventoryItemsIds: number[] = items.map((item) => item.id);
// Retreives the items inventory IN/OUT map.
const [OUTValuationMap, INValuationMap] =
await this.getItemsInventoryInOutMap(itemsId, date);
const getItemValuation = this.getItemInventoryMeta(
INValuationMap,
OUTValuationMap,
);
const itemsValuations = inventoryItemsIds.map(getItemValuation);
const itemsValuationsMap = new Map(
itemsValuations.map((i) => [i.itemId, i]),
);
return itemsValuationsMap;
};
}

View File

@@ -0,0 +1,80 @@
import { Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
@Injectable()
export class InventoryItemOpeningAvgCostService {
constructor(
private readonly inventoryCostLotTrackerModel: TenantModelProxy<
typeof InventoryCostLotTracker
>,
) {}
/**
* Get items Average cost from specific date from inventory transactions.
* @param {Date} closingDate - Closing date.
* @param {number} itemId - Item id.
* @return {Promise<{
* averageCost: number,
* openingCost: number,
* openingQuantity: number,
* }>}
*/
public async getOpeningAverageCost(closingDate: Date, itemId: number) {
const commonBuilder = (builder: any) => {
if (closingDate) {
builder.where('date', '<', closingDate);
}
builder.where('item_id', itemId);
builder.sum('rate as rate');
builder.sum('quantity as quantity');
builder.sum('cost as cost');
builder.first();
};
// Calculates the total inventory total quantity and rate `IN` transactions.
const inInvSumationOper = this.inventoryCostLotTrackerModel()
.query()
.onBuild(commonBuilder)
.where('direction', 'IN');
// Calculates the total inventory total quantity and rate `OUT` transactions.
const outInvSumationOper = this.inventoryCostLotTrackerModel()
.query()
.onBuild(commonBuilder)
.where('direction', 'OUT');
const [inInvSumation, outInvSumation] = await Promise.all([
inInvSumationOper,
outInvSumationOper,
]);
return this.computeItemAverageCost(
inInvSumation?.cost || 0,
inInvSumation?.quantity || 0,
outInvSumation?.cost || 0,
outInvSumation?.quantity || 0,
);
}
/**
* Computes the item average cost.
* @static
* @param {number} quantityIn
* @param {number} rateIn
* @param {number} quantityOut
* @param {number} rateOut
*/
public computeItemAverageCost(
totalCostIn: number,
totalQuantityIn: number,
totalCostOut: number,
totalQuantityOut: number,
) {
const openingCost = totalCostIn - totalCostOut;
const openingQuantity = totalQuantityIn - totalQuantityOut;
const averageCost = openingQuantity ? openingCost / openingQuantity : 0;
return { averageCost, openingCost, openingQuantity };
}
}

View File

@@ -1,11 +1,11 @@
import { toSafeInteger } from 'lodash';
import { IItemsQuantityChanges } from './types/InventoryCost.types';
import { IItemsQuantityChanges } from '../types/InventoryCost.types';
import { Knex } from 'knex';
import { Inject } from '@nestjs/common';
import { Item } from '../Items/models/Item';
import { Item } from '../../Items/models/Item';
import { Injectable } from '@nestjs/common';
import { InventoryTransaction } from './models/InventoryTransaction';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryTransaction } from '../models/InventoryTransaction';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
/**
* Syncs the inventory transactions with inventory items quantity.

View File

@@ -5,14 +5,14 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IInventoryTransactionsDeletedPayload,
TInventoryTransactionDirection,
} from './types/InventoryCost.types';
import { InventoryCostLotTracker } from './models/InventoryCostLotTracker';
import { InventoryTransaction } from './models/InventoryTransaction';
} from '../types/InventoryCost.types';
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
import { InventoryTransaction } from '../models/InventoryTransaction';
import { events } from '@/common/events/events';
import { IInventoryTransactionsCreatedPayload } from './types/InventoryCost.types';
import { transformItemEntriesToInventory } from './utils';
import { IItemEntryTransactionType } from '../TransactionItemEntry/ItemEntry.types';
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
import { IInventoryTransactionsCreatedPayload } from '../types/InventoryCost.types';
import { transformItemEntriesToInventory } from '../utils';
import { IItemEntryTransactionType } from '../../TransactionItemEntry/ItemEntry.types';
import { ItemEntry } from '../../TransactionItemEntry/models/ItemEntry';
export class InventoryTransactionsService {
constructor(
@@ -82,7 +82,6 @@ export class InventoryTransactionsService {
/**
* Records the inventory transactions from items entries that have (inventory) type.
*
* @param {number} tenantId
* @param {number} transactionId
* @param {string} transactionType

View File

@@ -1,10 +1,11 @@
import { Knex } from 'knex';
import { Inject } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { omit } from 'lodash';
import { InventoryCostLotTracker } from './models/InventoryCostLotTracker';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
export class InventoryCostMethod {
@Injectable()
export class StoreInventoryLotsCostService {
constructor(
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: TenantModelProxy<
@@ -20,7 +21,7 @@ export class InventoryCostMethod {
*/
public storeInventoryLotsCost(
costLotsTransactions: InventoryCostLotTracker[],
trx: Knex.Transaction,
trx?: Knex.Transaction,
): Promise<object> {
const opers: any = [];
@@ -43,4 +44,24 @@ export class InventoryCostMethod {
});
return Promise.all(opers);
}
/**
* Reverts the inventory lots `OUT` transactions.
* @param {Date} startingDate - Starting date.
* @param {number} itemId - Item id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
async revertInventoryCostLotTransactions(
startingDate: Date,
itemId: number,
trx?: Knex.Transaction,
): Promise<void> {
await this.inventoryCostLotTracker()
.query(trx)
.modify('filterDateRange', startingDate)
.orderBy('date', 'DESC')
.where('item_id', itemId)
.delete();
}
}

View File

@@ -0,0 +1,40 @@
import { JOB_REF, Processor } from '@nestjs/bullmq';
import { Inject, Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { ClsService } from 'nestjs-cls';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number;
startingDate: Date;
}
@Processor({
name: 'compute-item-cost',
scope: Scope.REQUEST,
})
export class ComputeItemCostProcessor {
constructor(
private readonly inventoryComputeCostService: InventoryComputeCostService,
private readonly clsService: ClsService,
@Inject(JOB_REF)
private readonly jobRef: Job<ComputeItemCostJobPayload>,
) {}
/**
* Handle compute item cost job.
*/
async handleComputeItemCost() {
const { itemId, startingDate, organizationId, userId } = this.jobRef.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
await this.inventoryComputeCostService.computeItemCost(
startingDate,
itemId,
);
}
}

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IInventoryCostLotsGLEntriesWriteEvent } from '../types/InventoryCost.types';
import { InventoryCostGLStorage } from '../InventoryCostGLStorage.service';
import { InventoryCostGLStorage } from '../commands/InventoryCostGLStorage.service';
@Injectable()
export class InventoryCostGLBeforeWriteSubscriber {
@@ -21,7 +21,7 @@ export class InventoryCostGLBeforeWriteSubscriber {
}: IInventoryCostLotsGLEntriesWriteEvent) {
await this.inventoryCostGLStorage.revertInventoryCostGLEntries(
startingDate,
trx
trx,
);
};
}
}