This commit is contained in:
Ahmed Bouhuolia
2025-01-15 15:28:39 +02:00
parent 936800600b
commit 7bcd578c11
11 changed files with 894 additions and 1000 deletions

View File

@@ -1,16 +1,16 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Bill } from '../models/Bill'; import { Bill } from '../models/Bill';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { InventoryService } from '@/modules/InventoryCost/Inventory'; import { InventoryTransactionsService } from '@/modules/InventoryCost/InventoryTransactions.service';
@Injectable() @Injectable()
export class BillInventoryTransactions { export class BillInventoryTransactions {
constructor( constructor(
private readonly itemsEntriesService: ItemsEntriesService, private readonly itemsEntriesService: ItemsEntriesService,
private readonly inventoryService: InventoryService, private readonly inventoryTransactionsService: InventoryTransactionsService,
private readonly bill: typeof Bill @Inject(Bill.name)
private readonly bill: typeof Bill,
) {} ) {}
/** /**
@@ -21,19 +21,18 @@ export class BillInventoryTransactions {
public async recordInventoryTransactions( public async recordInventoryTransactions(
billId: number, billId: number,
override?: boolean, override?: boolean,
trx?: Knex.Transaction trx?: Knex.Transaction,
): Promise<void> { ): Promise<void> {
// Retireve bill with assocaited entries and allocated cost entries. // Retireve bill with assocaited entries and allocated cost entries.
const bill = await this.bill.query(trx) const bill = await this.bill
.query(trx)
.findById(billId) .findById(billId)
.withGraphFetched('entries.allocatedCostEntries'); .withGraphFetched('entries.allocatedCostEntries');
// Loads the inventory items entries of the given sale invoice. // Loads the inventory items entries of the given sale invoice.
const inventoryEntries = const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries( await this.itemsEntriesService.filterInventoryEntries(bill.entries);
bill.entries
);
const transaction = { const transaction = {
transactionId: bill.id, transactionId: bill.id,
transactionType: 'Bill', transactionType: 'Bill',
@@ -46,10 +45,10 @@ export class BillInventoryTransactions {
warehouseId: bill.warehouseId, warehouseId: bill.warehouseId,
}; };
await this.inventoryService.recordInventoryTransactionsFromItemsEntries( await this.inventoryTransactionsService.recordInventoryTransactionsFromItemsEntries(
transaction, transaction,
override, override,
trx trx,
); );
} }
@@ -61,13 +60,13 @@ export class BillInventoryTransactions {
*/ */
public async revertInventoryTransactions( public async revertInventoryTransactions(
billId: number, billId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
) { ) {
// Deletes the inventory transactions by the given reference id and type. // Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions( await this.inventoryTransactionsService.deleteInventoryTransactions(
billId, billId,
'Bill', 'Bill',
trx trx,
); );
} }
} }

View File

@@ -1,13 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InventoryTransactionsService } from '@/modules/InventoryCost/InventoryTransactions.service';
import { InventoryService } from '@/modules/InventoryCost/Inventory';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { CreditNote } from '../models/CreditNote'; import { CreditNote } from '../models/CreditNote';
import { Knex } from 'knex'; import { Knex } from 'knex';
@Injectable() @Injectable()
export class CreditNoteInventoryTransactions { export class CreditNoteInventoryTransactions {
constructor( constructor(
private readonly inventoryService: InventoryService, private readonly inventoryService: InventoryTransactionsService,
private readonly itemsEntriesService: ItemsEntriesService, private readonly itemsEntriesService: ItemsEntriesService,
) {} ) {}

View File

@@ -1,362 +1,214 @@
import { pick } from 'lodash'; // import { pick } from 'lodash';
import { Inject, Injectable } from '@nestjs/common'; // import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; // import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex'; // import { Knex } from 'knex';
import { // import {
IInventoryLotCost, // IInventoryLotCost,
IInventoryTransaction, // IInventoryTransaction,
TInventoryTransactionDirection, // TInventoryTransactionDirection,
IItemEntry, // IItemEntry,
IItemEntryTransactionType, // IItemEntryTransactionType,
IInventoryTransactionsCreatedPayload, // IInventoryTransactionsCreatedPayload,
IInventoryTransactionsDeletedPayload, // IInventoryTransactionsDeletedPayload,
IInventoryItemCostScheduledPayload, // IInventoryItemCostScheduledPayload,
} from '@/interfaces'; // } from '@/interfaces';
import { InventoryAverageCostMethod } from './InventoryAverageCost'; // import { InventoryAverageCostMethod } from './InventoryAverageCost';
import { InventoryCostLotTracker } from './InventoryCostLotTracker'; // import { InventoryCostLotTracker } from './InventoryCostLotTracker';
import { ItemsEntriesService } from '../Items/ItemsEntries.service'; // import { ItemsEntriesService } from '../Items/ItemsEntries.service';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service'; // import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { Item } from '../Items/models/Item'; // import { Item } from '../Items/models/Item';
import { SETTINGS_PROVIDER } from '../Settings/Settings.types'; // import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
import { SettingsStore } from '../Settings/SettingsStore'; // import { SettingsStore } from '../Settings/SettingsStore';
import { events } from '@/common/events/events'; // import { events } from '@/common/events/events';
import { InventoryTransaction } from './models/InventoryTransaction'; // import { InventoryTransaction } from './models/InventoryTransaction';
import InventoryCostMethod from './InventoryCostMethod'; // import InventoryCostMethod from './InventoryCostMethod';
@Injectable() // @Injectable()
export class InventoryService { // export class InventoryService {
constructor( // constructor(
private readonly eventEmitter: EventEmitter2, // private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork, // private readonly uow: UnitOfWork,
@Inject(InventoryTransaction.name) // @Inject(InventoryTransaction.name)
private readonly inventoryTransactionModel: typeof InventoryTransaction, // private readonly inventoryTransactionModel: typeof InventoryTransaction,
@Inject(InventoryCostLotTracker.name) // @Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker, // private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker,
@Inject(SETTINGS_PROVIDER) // @Inject(SETTINGS_PROVIDER)
private readonly settings: SettingsStore, // private readonly settings: SettingsStore,
) {} // ) {}
/** // /**
* Transforms the items entries to inventory transactions. // * Transforms the items entries to inventory transactions.
*/ // */
transformItemEntriesToInventory(transaction: { // transformItemEntriesToInventory(transaction: {
transactionId: number; // transactionId: number;
transactionType: IItemEntryTransactionType; // transactionType: IItemEntryTransactionType;
transactionNumber?: string; // transactionNumber?: string;
exchangeRate?: number; // exchangeRate?: number;
warehouseId: number | null; // warehouseId: number | null;
date: Date | string; // date: Date | string;
direction: TInventoryTransactionDirection; // direction: TInventoryTransactionDirection;
entries: IItemEntry[]; // entries: IItemEntry[];
createdAt: Date; // createdAt: Date;
}): IInventoryTransaction[] { // }): IInventoryTransaction[] {
const exchangeRate = transaction.exchangeRate || 1; // const exchangeRate = transaction.exchangeRate || 1;
return transaction.entries.map((entry: IItemEntry) => ({ // return transaction.entries.map((entry: IItemEntry) => ({
...pick(entry, ['itemId', 'quantity']), // ...pick(entry, ['itemId', 'quantity']),
rate: entry.rate * exchangeRate, // rate: entry.rate * exchangeRate,
transactionType: transaction.transactionType, // transactionType: transaction.transactionType,
transactionId: transaction.transactionId, // transactionId: transaction.transactionId,
direction: transaction.direction, // direction: transaction.direction,
date: transaction.date, // date: transaction.date,
entryId: entry.id, // entryId: entry.id,
createdAt: transaction.createdAt, // createdAt: transaction.createdAt,
costAccountId: entry.costAccountId, // costAccountId: entry.costAccountId,
warehouseId: entry.warehouseId || transaction.warehouseId, // warehouseId: entry.warehouseId || transaction.warehouseId,
meta: { // meta: {
transactionNumber: transaction.transactionNumber, // transactionNumber: transaction.transactionNumber,
description: entry.description, // description: entry.description,
}, // },
})); // }));
} // }
async computeItemCost(fromDate: Date, itemId: number) { // async computeItemCost(fromDate: Date, itemId: number) {
return this.uow.withTransaction((trx: Knex.Transaction) => { // return this.uow.withTransaction((trx: Knex.Transaction) => {
return this.computeInventoryItemCost(fromDate, itemId); // return this.computeInventoryItemCost(fromDate, itemId);
}); // });
} // }
/** // /**
* Computes the given item cost and records the inventory lots transactions // * 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. // * and journal entries based on the cost method FIFO, LIFO or average cost rate.
* @param {Date} fromDate - From date. // * @param {Date} fromDate - From date.
* @param {number} itemId - Item id. // * @param {number} itemId - Item id.
*/ // */
async computeInventoryItemCost( // async computeInventoryItemCost(
fromDate: Date, // fromDate: Date,
itemId: number, // itemId: number,
trx?: Knex.Transaction, // trx?: Knex.Transaction,
) { // ) {
// Fetches the item with associated item category. // // Fetches the item with associated item category.
const item = await Item.query().findById(itemId); // const item = await Item.query().findById(itemId);
// Cannot continue if the given item was not inventory item. // // Cannot continue if the given item was not inventory item.
if (item.type !== 'inventory') { // if (item.type !== 'inventory') {
throw new Error('You could not compute item cost has no inventory type.'); // throw new Error('You could not compute item cost has no inventory type.');
} // }
let costMethodComputer: InventoryCostMethod; // let costMethodComputer: InventoryCostMethod;
// Switch between methods based on the item cost method. // // Switch between methods based on the item cost method.
switch ('AVG') { // switch ('AVG') {
case 'FIFO': // case 'FIFO':
case 'LIFO': // case 'LIFO':
costMethodComputer = new InventoryCostLotTracker( // costMethodComputer = new InventoryCostLotTracker(
tenantId, // tenantId,
fromDate, // fromDate,
itemId, // itemId,
); // );
break; // break;
case 'AVG': // case 'AVG':
costMethodComputer = new InventoryAverageCostMethod( // costMethodComputer = new InventoryAverageCostMethod(
fromDate, // fromDate,
itemId, // itemId,
trx, // trx,
); // );
break; // break;
} // }
return costMethodComputer.computeItemCost(); // return costMethodComputer.computeItemCost();
} // }
/** // /**
* Schedule item cost compute job. // * Schedule item cost compute job.
* @param {number} tenantId // * @param {number} tenantId
* @param {number} itemId // * @param {number} itemId
* @param {Date} startingDate // * @param {Date} startingDate
*/ // */
async scheduleComputeItemCost( // async scheduleComputeItemCost(
tenantId: number, // tenantId: number,
itemId: number, // itemId: number,
startingDate: Date | string, // startingDate: Date | string,
) { // ) {
const agenda = Container.get('agenda'); // const agenda = Container.get('agenda');
const commonJobsQuery = { // const commonJobsQuery = {
name: 'compute-item-cost', // name: 'compute-item-cost',
lastRunAt: { $exists: false }, // lastRunAt: { $exists: false },
'data.tenantId': tenantId, // 'data.tenantId': tenantId,
'data.itemId': itemId, // 'data.itemId': itemId,
}; // };
// Cancel any `compute-item-cost` in the queue has upper starting date // // Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item. // // with the same given item.
await agenda.cancel({ // await agenda.cancel({
...commonJobsQuery, // ...commonJobsQuery,
'data.startingDate': { $lte: startingDate }, // 'data.startingDate': { $lte: startingDate },
}); // });
// Retrieve any `compute-item-cost` in the queue has lower starting date // // Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item. // // with the same given item.
const dependsJobs = await agenda.jobs({ // const dependsJobs = await agenda.jobs({
...commonJobsQuery, // ...commonJobsQuery,
'data.startingDate': { $gte: startingDate }, // 'data.startingDate': { $gte: startingDate },
}); // });
// If the depends jobs cleared. // // If the depends jobs cleared.
if (dependsJobs.length === 0) { // if (dependsJobs.length === 0) {
await agenda.schedule( // await agenda.schedule(
config.scheduleComputeItemCost, // config.scheduleComputeItemCost,
'compute-item-cost', // 'compute-item-cost',
{ // {
startingDate, // startingDate,
itemId, // itemId,
tenantId, // tenantId,
}, // },
); // );
// Triggers `onComputeItemCostJobScheduled` event. // // Triggers `onComputeItemCostJobScheduled` event.
await this.eventPublisher.emitAsync( // await this.eventPublisher.emitAsync(
events.inventory.onComputeItemCostJobScheduled, // events.inventory.onComputeItemCostJobScheduled,
{ // {
startingDate, // startingDate,
itemId, // itemId,
tenantId, // tenantId,
} as IInventoryItemCostScheduledPayload, // } as IInventoryItemCostScheduledPayload,
); // );
} else { // } else {
// Re-schedule the jobs that have higher date from current moment. // // Re-schedule the jobs that have higher date from current moment.
await Promise.all( // await Promise.all(
dependsJobs.map((job) => // dependsJobs.map((job) =>
job.schedule(config.scheduleComputeItemCost).save(), // job.schedule(config.scheduleComputeItemCost).save(),
), // ),
); // );
} // }
} // }
/**
* Records the inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
async recordInventoryTransactions(
transactions: IInventoryTransaction[],
override: boolean = false,
trx?: Knex.Transaction,
): Promise<void> {
const bulkInsertOpers = [];
transactions.forEach((transaction: IInventoryTransaction) => { // /**
const oper = this.recordInventoryTransaction(transaction, override, trx); // * Mark item cost computing is running.
bulkInsertOpers.push(oper); // * @param {boolean} isRunning -
}); // */
const inventoryTransactions = await Promise.all(bulkInsertOpers); // async markItemsCostComputeRunning(isRunning: boolean = true) {
// this.settings.set({
// key: 'cost_compute_running',
// group: 'inventory',
// value: isRunning,
// });
// await this.settings.save();
// }
// Triggers `onInventoryTransactionsCreated` event. // /**
await this.eventEmitter.emitAsync( // * Checks if the items cost compute is running.
events.inventory.onInventoryTransactionsCreated, // * @returns {boolean}
{ // */
inventoryTransactions, // isItemsCostComputeRunning() {
trx, // return (
} as IInventoryTransactionsCreatedPayload, // this.settings.get({
); // key: 'cost_compute_running',
} // group: 'inventory',
// }) ?? false
/** // );
* Writes the inventory transactiosn on the storage from the given // }
* inventory transactions entries. // }
*
* @param {number} tenantId -
* @param {IInventoryTransaction} inventoryEntry -
* @param {boolean} deleteOld -
*/
async recordInventoryTransaction(
inventoryEntry: IInventoryTransaction,
deleteOld: boolean = false,
trx: Knex.Transaction,
): Promise<IInventoryTransaction> {
if (deleteOld) {
await this.deleteInventoryTransactions(
inventoryEntry.transactionId,
inventoryEntry.transactionType,
trx,
);
}
return this.inventoryTransactionModel.query(trx).insertGraph({
...inventoryEntry,
});
}
/**
* Records the inventory transactions from items entries that have (inventory) type.
*
* @param {number} tenantId
* @param {number} transactionId
* @param {string} transactionType
* @param {Date|string} transactionDate
* @param {boolean} override
*/
async recordInventoryTransactionsFromItemsEntries(
transaction: {
transactionId: number;
transactionType: IItemEntryTransactionType;
exchangeRate: number;
date: Date | string;
direction: TInventoryTransactionDirection;
entries: IItemEntry[];
createdAt: Date | string;
warehouseId: number;
},
override: boolean = false,
trx?: Knex.Transaction,
): Promise<void> {
// Can't continue if there is no entries has inventory items in the invoice.
if (transaction.entries.length <= 0) {
return;
}
// Inventory transactions.
const inventoryTranscations =
this.transformItemEntriesToInventory(transaction);
// Records the inventory transactions of the given sale invoice.
await this.recordInventoryTransactions(
inventoryTranscations,
override,
trx,
);
}
/**
* Deletes the given inventory transactions.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType
* @param {number} transactionId
* @return {Promise<{
* oldInventoryTransactions: IInventoryTransaction[]
* }>}
*/
async deleteInventoryTransactions(
transactionId: number,
transactionType: string,
trx?: Knex.Transaction,
): Promise<{ oldInventoryTransactions: InventoryTransaction[] }> {
// Retrieve the inventory transactions of the given sale invoice.
const oldInventoryTransactions = await this.inventoryTransactionModel
.query(trx)
.where({ transactionId, transactionType });
// Deletes the inventory transactions by the given transaction type and id.
await this.inventoryTransactionModel
.query(trx)
.where({ transactionType, transactionId })
.delete();
// Triggers `onInventoryTransactionsDeleted` event.
await this.eventEmitter.emitAsync(
events.inventory.onInventoryTransactionsDeleted,
{
oldInventoryTransactions,
transactionId,
transactionType,
trx,
} as IInventoryTransactionsDeletedPayload,
);
return { oldInventoryTransactions };
}
/**
* Records the inventory cost lot transaction.
* @param {number} tenantId
* @param {IInventoryLotCost} inventoryLotEntry
* @return {Promise<IInventoryLotCost>}
*/
async recordInventoryCostLotTransaction(
tenantId: number,
inventoryLotEntry: IInventoryLotCost,
): Promise<void> {
return this.inventoryCostLotTracker.query().insert({
...inventoryLotEntry,
});
}
/**
* 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 +1,254 @@
import { pick } from 'lodash'; // import { pick } from 'lodash';
import { Knex } from 'knex'; // import { Knex } from 'knex';
import InventoryCostMethod from './InventoryCostMethod'; // import InventoryCostMethod from './InventoryCostMethod';
import { InventoryTransaction } from './models/InventoryTransaction'; // import { InventoryTransaction } from './models/InventoryTransaction';
export class InventoryAverageCostMethod extends InventoryCostMethod { // export class InventoryAverageCostMethod extends InventoryCostMethod {
startingDate: Date; // startingDate: Date;
itemId: number; // itemId: number;
costTransactions: any[]; // costTransactions: any[];
trx: Knex.Transaction; // trx: Knex.Transaction;
/** // /**
* Constructor method. // * Constructor method.
* @param {number} tenantId - The given tenant id. // * @param {number} tenantId - The given tenant id.
* @param {Date} startingDate - // * @param {Date} startingDate -
* @param {number} itemId - The given inventory item id. // * @param {number} itemId - The given inventory item id.
*/ // */
constructor( // constructor(
tenantId: number, // tenantId: number,
startingDate: Date, // startingDate: Date,
itemId: number, // itemId: number,
trx?: Knex.Transaction, // trx?: Knex.Transaction,
) { // ) {
super(tenantId, startingDate, itemId); // super(tenantId, startingDate, itemId);
this.trx = trx; // this.trx = trx;
this.startingDate = startingDate; // this.startingDate = startingDate;
this.itemId = itemId; // this.itemId = itemId;
this.costTransactions = []; // this.costTransactions = [];
} // }
/** // /**
* Computes items costs from the given date using average cost method. // * Computes items costs from the given date using average cost method.
* ---------- // * ----------
* - Calculate the items average cost in the given date. // * - Calculate the items average cost in the given date.
* - Remove the journal entries that associated to the inventory transacions // * - Remove the journal entries that associated to the inventory transacions
* after the given date. // * after the given date.
* - Re-compute the inventory transactions and re-write the journal entries // * - Re-compute the inventory transactions and re-write the journal entries
* after the given date. // * after the given date.
* ---------- // * ----------
* @async // * @async
* @param {Date} startingDate // * @param {Date} startingDate
* @param {number} referenceId // * @param {number} referenceId
* @param {string} referenceType // * @param {string} referenceType
*/ // */
public async computeItemCost() { // public async computeItemCost() {
const { InventoryTransaction } = this.tenantModels; // const { InventoryTransaction } = this.tenantModels;
const { averageCost, openingQuantity, openingCost } = // const { averageCost, openingQuantity, openingCost } =
await this.getOpeningAverageCost(this.startingDate, this.itemId); // await this.getOpeningAverageCost(this.startingDate, this.itemId);
const afterInvTransactions = // const afterInvTransactions =
await InventoryTransaction.query() // await InventoryTransaction.query()
.modify('filterDateRange', this.startingDate) // .modify('filterDateRange', this.startingDate)
.orderBy('date', 'ASC') // .orderBy('date', 'ASC')
.orderByRaw("FIELD(direction, 'IN', 'OUT')") // .orderByRaw("FIELD(direction, 'IN', 'OUT')")
.orderBy('createdAt', 'ASC') // .orderBy('createdAt', 'ASC')
.where('item_id', this.itemId) // .where('item_id', this.itemId)
.withGraphFetched('item'); // .withGraphFetched('item');
// Tracking inventroy transactions and retrieve cost transactions based on // // Tracking inventroy transactions and retrieve cost transactions based on
// average rate cost method. // // average rate cost method.
const costTransactions = this.trackingCostTransactions( // const costTransactions = this.trackingCostTransactions(
afterInvTransactions, // afterInvTransactions,
openingQuantity, // openingQuantity,
openingCost, // openingCost,
); // );
// Revert the inveout out lots transactions // // Revert the inveout out lots transactions
await this.revertTheInventoryOutLotTrans(); // await this.revertTheInventoryOutLotTrans();
// Store inventory lots cost transactions. // // Store inventory lots cost transactions.
await this.storeInventoryLotsCost(costTransactions); // await this.storeInventoryLotsCost(costTransactions);
} // }
/** // /**
* Get items Average cost from specific date from inventory transactions. // * Get items Average cost from specific date from inventory transactions.
* @async // * @async
* @param {Date} closingDate // * @param {Date} closingDate
* @return {number} // * @return {number}
*/ // */
public async getOpeningAverageCost(closingDate: Date, itemId: number) { // public async getOpeningAverageCost(closingDate: Date, itemId: number) {
const { InventoryCostLotTracker } = this.tenantModels; // const { InventoryCostLotTracker } = this.tenantModels;
const commonBuilder = (builder: any) => { // const commonBuilder = (builder: any) => {
if (closingDate) { // if (closingDate) {
builder.where('date', '<', closingDate); // builder.where('date', '<', closingDate);
} // }
builder.where('item_id', itemId); // builder.where('item_id', itemId);
builder.sum('rate as rate'); // builder.sum('rate as rate');
builder.sum('quantity as quantity'); // builder.sum('quantity as quantity');
builder.sum('cost as cost'); // builder.sum('cost as cost');
builder.first(); // builder.first();
}; // };
// Calculates the total inventory total quantity and rate `IN` transactions. // // Calculates the total inventory total quantity and rate `IN` transactions.
const inInvSumationOper: Promise<any> = InventoryCostLotTracker.query() // const inInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
.onBuild(commonBuilder) // .onBuild(commonBuilder)
.where('direction', 'IN'); // .where('direction', 'IN');
// Calculates the total inventory total quantity and rate `OUT` transactions. // // Calculates the total inventory total quantity and rate `OUT` transactions.
const outInvSumationOper: Promise<any> = InventoryCostLotTracker.query() // const outInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
.onBuild(commonBuilder) // .onBuild(commonBuilder)
.where('direction', 'OUT'); // .where('direction', 'OUT');
const [inInvSumation, outInvSumation] = await Promise.all([ // const [inInvSumation, outInvSumation] = await Promise.all([
inInvSumationOper, // inInvSumationOper,
outInvSumationOper, // outInvSumationOper,
]); // ]);
return this.computeItemAverageCost( // return this.computeItemAverageCost(
inInvSumation?.cost || 0, // inInvSumation?.cost || 0,
inInvSumation?.quantity || 0, // inInvSumation?.quantity || 0,
outInvSumation?.cost || 0, // outInvSumation?.cost || 0,
outInvSumation?.quantity || 0, // outInvSumation?.quantity || 0,
); // );
} // }
/** // /**
* Computes the item average cost. // * Computes the item average cost.
* @static // * @static
* @param {number} quantityIn // * @param {number} quantityIn
* @param {number} rateIn // * @param {number} rateIn
* @param {number} quantityOut // * @param {number} quantityOut
* @param {number} rateOut // * @param {number} rateOut
*/ // */
public computeItemAverageCost( // public computeItemAverageCost(
totalCostIn: number, // totalCostIn: number,
totalQuantityIn: number, // totalQuantityIn: number,
totalCostOut: number, // totalCostOut: number,
totalQuantityOut: number, // totalQuantityOut: number,
) { // ) {
const openingCost = totalCostIn - totalCostOut; // const openingCost = totalCostIn - totalCostOut;
const openingQuantity = totalQuantityIn - totalQuantityOut; // const openingQuantity = totalQuantityIn - totalQuantityOut;
const averageCost = openingQuantity ? openingCost / openingQuantity : 0; // const averageCost = openingQuantity ? openingCost / openingQuantity : 0;
return { averageCost, openingCost, openingQuantity }; // return { averageCost, openingCost, openingQuantity };
} // }
private getCost(rate: number, quantity: number) { // private getCost(rate: number, quantity: number) {
return quantity ? rate * quantity : rate; // return quantity ? rate * quantity : rate;
} // }
/** // /**
* Records the journal entries from specific item inventory transactions. // * Records the journal entries from specific item inventory transactions.
* @param {IInventoryTransaction[]} invTransactions // * @param {IInventoryTransaction[]} invTransactions
* @param {number} openingAverageCost // * @param {number} openingAverageCost
* @param {string} referenceType // * @param {string} referenceType
* @param {number} referenceId // * @param {number} referenceId
* @param {JournalCommand} journalCommands // * @param {JournalCommand} journalCommands
*/ // */
public trackingCostTransactions( // public trackingCostTransactions(
invTransactions: InventoryTransaction[], // invTransactions: InventoryTransaction[],
openingQuantity: number = 0, // openingQuantity: number = 0,
openingCost: number = 0, // openingCost: number = 0,
) { // ) {
const costTransactions: any[] = []; // const costTransactions: any[] = [];
// Cumulative item quantity and cost. This will decrement after // // Cumulative item quantity and cost. This will decrement after
// each out transactions depends on its quantity and cost. // // each out transactions depends on its quantity and cost.
let accQuantity: number = openingQuantity; // let accQuantity: number = openingQuantity;
let accCost: number = openingCost; // let accCost: number = openingCost;
invTransactions.forEach((invTransaction: InventoryTransaction) => { // invTransactions.forEach((invTransaction: InventoryTransaction) => {
const commonEntry = { // const commonEntry = {
invTransId: invTransaction.id, // invTransId: invTransaction.id,
...pick(invTransaction, [ // ...pick(invTransaction, [
'date', // 'date',
'direction', // 'direction',
'itemId', // 'itemId',
'quantity', // 'quantity',
'rate', // 'rate',
'entryId', // 'entryId',
'transactionId', // 'transactionId',
'transactionType', // 'transactionType',
'createdAt', // 'createdAt',
'costAccountId', // 'costAccountId',
'branchId', // 'branchId',
'warehouseId', // 'warehouseId',
]), // ]),
inventoryTransactionId: invTransaction.id, // inventoryTransactionId: invTransaction.id,
}; // };
switch (invTransaction.direction) { // switch (invTransaction.direction) {
case 'IN': // case 'IN':
const inCost = this.getCost( // const inCost = this.getCost(
invTransaction.rate, // invTransaction.rate,
invTransaction.quantity, // invTransaction.quantity,
); // );
// Increases the quantity and cost in `IN` inventory transactions. // // Increases the quantity and cost in `IN` inventory transactions.
accQuantity += invTransaction.quantity; // accQuantity += invTransaction.quantity;
accCost += inCost; // accCost += inCost;
costTransactions.push({ // costTransactions.push({
...commonEntry, // ...commonEntry,
cost: inCost, // cost: inCost,
}); // });
break; // break;
case 'OUT': // case 'OUT':
// Average cost = Total cost / Total quantity // // Average cost = Total cost / Total quantity
const averageCost = accQuantity ? accCost / accQuantity : 0; // const averageCost = accQuantity ? accCost / accQuantity : 0;
const quantity = // const quantity =
accQuantity > 0 // accQuantity > 0
? Math.min(invTransaction.quantity, accQuantity) // ? Math.min(invTransaction.quantity, accQuantity)
: invTransaction.quantity; // : invTransaction.quantity;
// Cost = the transaction quantity * Average cost. // // Cost = the transaction quantity * Average cost.
const cost = this.getCost(averageCost, quantity); // const cost = this.getCost(averageCost, quantity);
// Revenue = transaction quanity * rate. // // Revenue = transaction quanity * rate.
// const revenue = quantity * invTransaction.rate; // // const revenue = quantity * invTransaction.rate;
costTransactions.push({ // costTransactions.push({
...commonEntry, // ...commonEntry,
quantity, // quantity,
cost, // cost,
}); // });
accQuantity = Math.max(accQuantity - quantity, 0); // accQuantity = Math.max(accQuantity - quantity, 0);
accCost = Math.max(accCost - cost, 0); // accCost = Math.max(accCost - cost, 0);
if (invTransaction.quantity > quantity) { // if (invTransaction.quantity > quantity) {
const remainingQuantity = Math.max( // const remainingQuantity = Math.max(
invTransaction.quantity - quantity, // invTransaction.quantity - quantity,
0, // 0,
); // );
const remainingIncome = remainingQuantity * invTransaction.rate; // const remainingIncome = remainingQuantity * invTransaction.rate;
costTransactions.push({ // costTransactions.push({
...commonEntry, // ...commonEntry,
quantity: remainingQuantity, // quantity: remainingQuantity,
cost: 0, // cost: 0,
}); // });
accQuantity = Math.max(accQuantity - remainingQuantity, 0); // accQuantity = Math.max(accQuantity - remainingQuantity, 0);
accCost = Math.max(accCost - remainingIncome, 0); // accCost = Math.max(accCost - remainingIncome, 0);
} // }
break; // break;
} // }
}); // });
return costTransactions; // return costTransactions;
} // }
/** // /**
* Reverts the inventory lots `OUT` transactions. // * Reverts the inventory lots `OUT` transactions.
* @param {Date} openingDate - Opening date. // * @param {Date} openingDate - Opening date.
* @param {number} itemId - Item id. // * @param {number} itemId - Item id.
* @returns {Promise<void>} // * @returns {Promise<void>}
*/ // */
async revertTheInventoryOutLotTrans(): Promise<void> { // async revertTheInventoryOutLotTrans(): Promise<void> {
const { InventoryCostLotTracker } = this.tenantModels; // const { InventoryCostLotTracker } = this.tenantModels;
await InventoryCostLotTracker.query(this.trx) // await InventoryCostLotTracker.query(this.trx)
.modify('filterDateRange', this.startingDate) // .modify('filterDateRange', this.startingDate)
.orderBy('date', 'DESC') // .orderBy('date', 'DESC')
.where('item_id', this.itemId) // .where('item_id', this.itemId)
.delete(); // .delete();
} // }
} // }

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InventoryItemCostService } from './InventoryCosts.service'; // import { InventoryItemCostService } from './InventoryCosts.service';
import { IInventoryItemCostMeta } from './types/InventoryCost.types'; import { IInventoryItemCostMeta } from './types/InventoryCost.types';
@Injectable() @Injectable()
export class InventoryCostApplication { export class InventoryCostApplication {
constructor( constructor(
private readonly inventoryCost: InventoryItemCostService, // private readonly inventoryCost: InventoryItemCostService,
) {} ) {}
/** /**
@@ -17,11 +17,11 @@ export class InventoryCostApplication {
public getItemsInventoryValuationList = async ( public getItemsInventoryValuationList = async (
itemsId: number[], itemsId: number[],
date: Date date: Date
): Promise<IInventoryItemCostMeta[]> => { ): Promise<any> => {
const itemsMap = await this.inventoryCost.getItemsInventoryValuation( // const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
itemsId, // itemsId,
date // date
); // );
return [...itemsMap.values()]; // return [...itemsMap.values()];
}; };
} }

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
import { chain } from 'lodash'; import { chain } from 'lodash';
import { pick } from 'lodash';
import { IItemEntryTransactionType } from '../TransactionItemEntry/ItemEntry.types';
import { TInventoryTransactionDirection } from './types/InventoryCost.types';
/** /**
* Grpups by transaction type and id the inventory transactions. * Grpups by transaction type and id the inventory transactions.
@@ -6,10 +9,48 @@ import { chain } from 'lodash';
* @returns * @returns
*/ */
export function groupInventoryTransactionsByTypeId( export function groupInventoryTransactionsByTypeId(
transactions: { transactionType: string; transactionId: number }[] transactions: { transactionType: string; transactionId: number }[],
): { transactionType: string; transactionId: number }[][] { ): { transactionType: string; transactionId: number }[][] {
return chain(transactions) return chain(transactions)
.groupBy((t) => `${t.transactionType}-${t.transactionId}`) .groupBy((t) => `${t.transactionType}-${t.transactionId}`)
.values() .values()
.value(); .value();
} }
/**
* Transforms the items entries to inventory transactions.
*/
export function 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,
},
}));
}

View File

@@ -1,4 +1,4 @@
import { InventoryService } from '@/modules/InventoryCost/Inventory'; import { InventoryTransactionsService } from '@/modules/InventoryCost/InventoryTransactions.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
@@ -8,7 +8,7 @@ import { SaleInvoice } from '../../models/SaleInvoice';
export class InvoiceInventoryTransactions { export class InvoiceInventoryTransactions {
constructor( constructor(
private readonly itemsEntriesService: ItemsEntriesService, private readonly itemsEntriesService: ItemsEntriesService,
private readonly inventoryService: InventoryService, private readonly inventoryService: InventoryTransactionsService,
) {} ) {}
/** /**

View File

@@ -1,13 +1,13 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { VendorCredit } from '../models/VendorCredit'; import { VendorCredit } from '../models/VendorCredit';
import { InventoryService } from '@/modules/InventoryCost/Inventory'; import { InventoryTransactionsService } from '@/modules/InventoryCost/InventoryTransactions.service';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
@Injectable() @Injectable()
export class VendorCreditInventoryTransactions { export class VendorCreditInventoryTransactions {
constructor( constructor(
private readonly inventoryService: InventoryService, private readonly inventoryService: InventoryTransactionsService,
private readonly itemsEntriesService: ItemsEntriesService private readonly itemsEntriesService: ItemsEntriesService
) {} ) {}

View File

@@ -12,9 +12,11 @@ import { VendorValidators } from './commands/VendorValidators';
import { VendorsApplication } from './VendorsApplication.service'; import { VendorsApplication } from './VendorsApplication.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { VendorsController } from './Vendors.controller'; import { VendorsController } from './Vendors.controller';
import { GetVendorsService } from './queries/GetVendors.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
@Module({ @Module({
imports: [TenancyDatabaseModule], imports: [TenancyDatabaseModule, DynamicListModule],
controllers: [VendorsController], controllers: [VendorsController],
providers: [ providers: [
ActivateVendorService, ActivateVendorService,
@@ -23,6 +25,7 @@ import { VendorsController } from './Vendors.controller';
EditVendorService, EditVendorService,
EditOpeningBalanceVendorService, EditOpeningBalanceVendorService,
GetVendorService, GetVendorService,
GetVendorsService,
VendorValidators, VendorValidators,
DeleteVendorService, DeleteVendorService,
VendorsApplication, VendorsApplication,