mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
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 './commands/InventoryItemsQuantitySync.service';
|
||||
import { InventoryTransactionsService } from './commands/InventoryTransactions.service';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { InventoryComputeCostService } from './commands/InventoryComputeCost.service';
|
||||
import { InventoryCostApplication } from './InventoryCostApplication';
|
||||
import { StoreInventoryLotsCostService } from './commands/StoreInventortyLotsCost.service';
|
||||
import { ComputeItemCostProcessor } from './processors/ComputeItemCost.processor';
|
||||
import { WriteInventoryTransactionsGLEntriesProcessor } from './processors/WriteInventoryTransactionsGLEntries.processor';
|
||||
import {
|
||||
ComputeItemCostQueue,
|
||||
WriteInventoryTransactionsGLEntriesQueue,
|
||||
} from './types/InventoryCost.types';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service';
|
||||
import { InventoryItemCostService } from './commands/InventoryCosts.service';
|
||||
import { InventoryItemOpeningAvgCostService } from './commands/InventoryItemOpeningAvgCost.service';
|
||||
import { InventoryCostSubscriber } from './subscribers/InventoryCost.subscriber';
|
||||
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
|
||||
import { ImportModule } from '../Import/Import.module';
|
||||
|
||||
const models = [
|
||||
RegisterTenancyModel(InventoryCostLotTracker),
|
||||
RegisterTenancyModel(InventoryTransaction),
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LedgerModule,
|
||||
...models,
|
||||
BullModule.registerQueue({ name: ComputeItemCostQueue }),
|
||||
BullModule.registerQueue({
|
||||
name: WriteInventoryTransactionsGLEntriesQueue,
|
||||
}),
|
||||
forwardRef(() => SaleInvoicesModule),
|
||||
ImportModule,
|
||||
],
|
||||
providers: [
|
||||
InventoryCostGLBeforeWriteSubscriber,
|
||||
InventoryCostGLStorage,
|
||||
InventoryItemsQuantitySyncService,
|
||||
InventoryTransactionsService,
|
||||
InventoryComputeCostService,
|
||||
InventoryCostApplication,
|
||||
StoreInventoryLotsCostService,
|
||||
ComputeItemCostProcessor,
|
||||
WriteInventoryTransactionsGLEntriesProcessor,
|
||||
InventoryAverageCostMethodService,
|
||||
InventoryItemCostService,
|
||||
InventoryItemOpeningAvgCostService,
|
||||
InventoryCostSubscriber,
|
||||
],
|
||||
exports: [
|
||||
...models,
|
||||
InventoryTransactionsService,
|
||||
InventoryItemCostService,
|
||||
InventoryComputeCostService,
|
||||
],
|
||||
})
|
||||
export class InventoryCostModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InventoryItemCostService } from './commands/InventoryCosts.service';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryCostApplication {
|
||||
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.
|
||||
* @param {number[]} itemsId
|
||||
* @param {Date} date
|
||||
* @returns {Promise<IInventoryItemCostMeta[]>}
|
||||
*/
|
||||
async getItemsInventoryValuation(
|
||||
itemsId: number[],
|
||||
date: Date,
|
||||
): Promise<any> {
|
||||
const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
|
||||
itemsId,
|
||||
date,
|
||||
);
|
||||
return [...itemsMap.values()];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { pick } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { InventoryTransaction } from '../models/InventoryTransaction';
|
||||
|
||||
export class InventoryAverageCostMethod {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Queue } from 'bullmq';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import Redis from 'ioredis';
|
||||
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 {
|
||||
ComputeItemCostQueue,
|
||||
ComputeItemCostQueueJob,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { InventoryAverageCostMethodService } from './InventoryAverageCostMethod.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { RedisService } from '@liaoliaots/nestjs-redis';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryComputeCostService {
|
||||
private readonly redisClient: Redis;
|
||||
|
||||
/**
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {InventoryAverageCostMethodService} inventoryAverageCostMethod - Inventory average cost method.
|
||||
* @param {RedisService} redisService - Redis service.
|
||||
* @param {ClsService} clsService - Cls service.
|
||||
* @param {Queue} computeItemCostProcessor - Compute item cost processor.
|
||||
* @param {TenantModelProxy<typeof Item>} itemModel - Item model.
|
||||
* @param {() => SettingsStore} settingsStore - Settings store.
|
||||
*/
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly inventoryAverageCostMethod: InventoryAverageCostMethodService,
|
||||
private readonly clsService: ClsService,
|
||||
private readonly redisService: RedisService,
|
||||
|
||||
@InjectQueue(ComputeItemCostQueue)
|
||||
private readonly computeItemCostProcessor: Queue,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
|
||||
@Inject(SETTINGS_PROVIDER)
|
||||
private readonly settingsStore: () => SettingsStore,
|
||||
) {
|
||||
this.redisClient = this.redisService.getOrThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} itemId
|
||||
* @param {Date} startingDate
|
||||
*/
|
||||
async scheduleComputeItemCost(itemId: number, startingDate: Date | string) {
|
||||
const debounceKey = `inventory-cost-compute-debounce:${itemId}`;
|
||||
const debounceTime = 1000 * 60; // 1 minute
|
||||
|
||||
// Generate a unique job ID or use a custom identifier
|
||||
const jobId = `task-${Date.now()}-${Math.random().toString(36).substring(2)}`;
|
||||
|
||||
// Check if there's an existing debounced job
|
||||
const existingJobId = await this.redisClient.get(debounceKey);
|
||||
|
||||
if (existingJobId) {
|
||||
// Attempt to remove or mark the previous job as skippable
|
||||
const existingJob =
|
||||
await this.computeItemCostProcessor.getJob(existingJobId);
|
||||
const state = await existingJob?.getState();
|
||||
|
||||
if (existingJob && ['waiting', 'delayed'].includes(state)) {
|
||||
await existingJob.remove(); // Remove the previous job if it's still waiting
|
||||
}
|
||||
}
|
||||
const organizationId = this.clsService.get('organizationId');
|
||||
const userId = this.clsService.get('userId');
|
||||
|
||||
// Add the new job with a delay (debounce period)
|
||||
const job = await this.computeItemCostProcessor.add(
|
||||
ComputeItemCostQueueJob,
|
||||
{ itemId, startingDate, jobId, organizationId, userId },
|
||||
{
|
||||
jobId, // Custom job ID
|
||||
delay: debounceTime, // Delay execution by 1 minute
|
||||
},
|
||||
);
|
||||
// Store the latest job ID in Redis with an expiration
|
||||
await this.redisClient.set(debounceKey, jobId, 'PX', debounceTime);
|
||||
|
||||
return { jobId, message: 'Task added with debounce' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 { TenantModelProxy } from '../../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryCostGLStorage {
|
||||
constructor(
|
||||
private readonly ledgerStorage: LedgerStorageService,
|
||||
|
||||
@Inject(AccountTransaction.name)
|
||||
private readonly accountTransactionModel: TenantModelProxy<
|
||||
typeof AccountTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Reverts the inventory cost GL entries from the given starting date.
|
||||
* @param {Date} startingDate - Starting date.
|
||||
* @param {Knex.Transaction} trx - Transaction.
|
||||
*/
|
||||
public async revertInventoryCostGLEntries(
|
||||
startingDate: Date,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieve transactions from specific date range and costable transactions only.
|
||||
const transactions = await this.accountTransactionModel()
|
||||
.query()
|
||||
.where('costable', true)
|
||||
.modify('filterDateRange', startingDate)
|
||||
.withGraphFetched('account');
|
||||
|
||||
// Transform transaction to ledger entries and reverse them.
|
||||
const reversedLedger = Ledger.fromTransactions(transactions).reverse();
|
||||
|
||||
// Deletes and reverts balances of the given ledger.
|
||||
await this.ledgerStorage.delete(reversedLedger, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { keyBy, get } from 'lodash';
|
||||
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';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryItemCostService {
|
||||
constructor(
|
||||
@Inject(InventoryCostLotTracker.name)
|
||||
private readonly inventoryCostLotTrackerModel: TenantModelProxy<
|
||||
typeof InventoryCostLotTracker
|
||||
>,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Map<number, IInventoryItemCostMeta>} INValuationMap -
|
||||
* @param {Map<number, IInventoryItemCostMeta>} OUTValuationMap -
|
||||
* @param {number} itemId
|
||||
*/
|
||||
private getItemInventoryMeta(
|
||||
INValuationMap: Map<number, IInventoryItemCostMeta>,
|
||||
OUTValuationMap: Map<number, IInventoryItemCostMeta>,
|
||||
itemId: number,
|
||||
) {
|
||||
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} itemsId
|
||||
* @param {Date} date
|
||||
* @returns
|
||||
*/
|
||||
private getItemsInventoryINAndOutAggregated = (
|
||||
itemsId: number[],
|
||||
date: Date,
|
||||
): Promise<any> => {
|
||||
const commonBuilder = (builder) => {
|
||||
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[]} 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 = (itemId: number) =>
|
||||
this.getItemInventoryMeta(INValuationMap, OUTValuationMap, itemId);
|
||||
|
||||
const itemsValuations = inventoryItemsIds.map((id) => getItemValuation(id));
|
||||
const itemsValuationsMap = new Map(
|
||||
itemsValuations.map((i) => [i.itemId, i]),
|
||||
);
|
||||
return itemsValuationsMap;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
|
||||
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryItemOpeningAvgCostService {
|
||||
constructor(
|
||||
@Inject(InventoryCostLotTracker.name)
|
||||
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();
|
||||
};
|
||||
interface QueryResult {
|
||||
cost: number;
|
||||
quantity: number;
|
||||
}
|
||||
// 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,
|
||||
])) as unknown as [QueryResult, QueryResult];
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { toSafeInteger } from 'lodash';
|
||||
import { IItemsQuantityChanges } from '../types/InventoryCost.types';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Item } from '../../Items/models/Item';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InventoryTransaction } from '../models/InventoryTransaction';
|
||||
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
|
||||
|
||||
/**
|
||||
* Syncs the inventory transactions with inventory items quantity.
|
||||
*/
|
||||
@Injectable()
|
||||
export class InventoryItemsQuantitySyncService {
|
||||
/**
|
||||
* @param {TenantModelProxy<typeof Item>} itemModel - Item model.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Reverse the given inventory transactions.
|
||||
* @param {IInventoryTransaction[]} inventroyTransactions
|
||||
* @return {IInventoryTransaction[]}
|
||||
*/
|
||||
public reverseInventoryTransactions(
|
||||
inventroyTransactions: InventoryTransaction[],
|
||||
): InventoryTransaction[] {
|
||||
return inventroyTransactions.map((transaction) => {
|
||||
const cloned = transaction.$clone();
|
||||
cloned.direction = cloned.direction === 'OUT' ? 'IN' : 'OUT';
|
||||
|
||||
return cloned;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the inventory transactions.
|
||||
* @param {IInventoryTransaction[]} inventroyTransactions -
|
||||
* @return {IItemsQuantityChanges[]}
|
||||
*/
|
||||
public getReverseItemsQuantityChanges(
|
||||
inventroyTransactions: InventoryTransaction[],
|
||||
): IItemsQuantityChanges[] {
|
||||
const reversedTransactions = this.reverseInventoryTransactions(
|
||||
inventroyTransactions,
|
||||
);
|
||||
return this.getItemsQuantityChanges(reversedTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the items quantity changes from the given inventory transactions.
|
||||
* @param {IInventoryTransaction[]} inventroyTransactions - Inventory transactions.
|
||||
* @return {IItemsQuantityChanges[]}
|
||||
*/
|
||||
public getItemsQuantityChanges(
|
||||
inventroyTransactions: InventoryTransaction[],
|
||||
): IItemsQuantityChanges[] {
|
||||
const balanceMap: { [itemId: number]: number } = {};
|
||||
|
||||
inventroyTransactions.forEach(
|
||||
(inventoryTransaction: InventoryTransaction) => {
|
||||
const { itemId, direction, quantity } = inventoryTransaction;
|
||||
|
||||
if (!balanceMap[itemId]) {
|
||||
balanceMap[itemId] = 0;
|
||||
}
|
||||
balanceMap[itemId] += direction === 'IN' ? quantity : 0;
|
||||
balanceMap[itemId] -= direction === 'OUT' ? quantity : 0;
|
||||
},
|
||||
);
|
||||
return Object.entries(balanceMap).map(([itemId, balanceChange]) => ({
|
||||
itemId: toSafeInteger(itemId),
|
||||
balanceChange,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the items quantity changes.
|
||||
* @param {IItemsQuantityChanges[]} itemsQuantity - Items quantity changes.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async changeItemsQuantity(
|
||||
itemsQuantity: IItemsQuantityChanges[],
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const opers = [];
|
||||
|
||||
itemsQuantity.forEach((itemQuantity: IItemsQuantityChanges) => {
|
||||
const changeQuantityOper = this.itemModel()
|
||||
.query(trx)
|
||||
.where({ id: itemQuantity.itemId, type: 'inventory' })
|
||||
.modify('updateQuantityOnHand', itemQuantity.balanceChange);
|
||||
|
||||
opers.push(changeQuantityOper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// @ts-nocheck
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IInventoryTransactionsDeletedPayload,
|
||||
TInventoryTransactionDirection,
|
||||
} 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';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryTransactionsService {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {TenantModelProxy<typeof InventoryTransaction>} inventoryTransactionModel - Inventory transaction model.
|
||||
* @param {TenantModelProxy<typeof InventoryCostLotTracker>} inventoryCostLotTracker - Inventory cost lot tracker model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
|
||||
@Inject(InventoryTransaction.name)
|
||||
private readonly inventoryTransactionModel: TenantModelProxy<
|
||||
typeof InventoryTransaction
|
||||
>,
|
||||
|
||||
@Inject(InventoryCostLotTracker.name)
|
||||
private readonly inventoryCostLotTracker: TenantModelProxy<
|
||||
typeof InventoryCostLotTracker
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Records the inventory transactions.
|
||||
* @param {InventoryTransaction[]} transactions - Inventory transactions.
|
||||
* @param {boolean} override - Override the existing transactions.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async recordInventoryTransactions(
|
||||
transactions: ModelObject<InventoryTransaction>[],
|
||||
override: boolean = false,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const bulkInsertOpers = [];
|
||||
|
||||
transactions.forEach((transaction: InventoryTransaction) => {
|
||||
const oper = this.recordInventoryTransaction(transaction, override, trx);
|
||||
bulkInsertOpers.push(oper);
|
||||
});
|
||||
const inventoryTransactions = await Promise.all(bulkInsertOpers);
|
||||
|
||||
// Triggers `onInventoryTransactionsCreated` event.
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.inventory.onInventoryTransactionsCreated,
|
||||
{
|
||||
inventoryTransactions,
|
||||
trx,
|
||||
} as IInventoryTransactionsCreatedPayload,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the inventory transactiosn on the storage from the given
|
||||
* inventory transactions entries.
|
||||
* @param {InventoryTransaction} inventoryEntry - Inventory transaction.
|
||||
* @param {boolean} deleteOld - Delete the existing inventory transactions.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @return {Promise<InventoryTransaction>}
|
||||
*/
|
||||
async recordInventoryTransaction(
|
||||
inventoryEntry: InventoryTransaction,
|
||||
deleteOld: boolean = false,
|
||||
trx: Knex.Transaction,
|
||||
): Promise<InventoryTransaction> {
|
||||
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: ItemEntry[];
|
||||
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 = transformItemEntriesToInventory(transaction);
|
||||
|
||||
// Records the inventory transactions of the given sale invoice.
|
||||
await this.recordInventoryTransactions(
|
||||
inventoryTranscations,
|
||||
override,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given inventory transactions.
|
||||
* @param {number} transactionId - Transaction id.
|
||||
* @param {string} transactionType - Transaction type.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @return {Promise<{ oldInventoryTransactions: IInventoryTransaction[] }>}
|
||||
*/
|
||||
public 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 {InventoryCostLotTracker} inventoryLotEntry
|
||||
* @return {Promise<InventoryCostLotTracker>}
|
||||
*/
|
||||
async recordInventoryCostLotTransaction(
|
||||
inventoryLotEntry: Partial<InventoryCostLotTracker>,
|
||||
): Promise<InventoryCostLotTracker> {
|
||||
return this.inventoryCostLotTracker.query().insert({
|
||||
...inventoryLotEntry,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { omit } from 'lodash';
|
||||
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
|
||||
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class StoreInventoryLotsCostService {
|
||||
constructor(
|
||||
@Inject(InventoryCostLotTracker.name)
|
||||
private readonly inventoryCostLotTracker: TenantModelProxy<
|
||||
typeof InventoryCostLotTracker
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Stores the inventory lots costs transactions in bulk.
|
||||
* @param {InventoryCostLotTracker[]} costLotsTransactions - Inventory lots costs transactions.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
public storeInventoryLotsCost(
|
||||
costLotsTransactions: InventoryCostLotTracker[],
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<object> {
|
||||
const opers: any = [];
|
||||
|
||||
costLotsTransactions.forEach((transaction: any) => {
|
||||
if (transaction.lotTransId && transaction.decrement) {
|
||||
const decrementOper = this.inventoryCostLotTracker()
|
||||
.query(trx)
|
||||
.where('id', transaction.lotTransId)
|
||||
.decrement('remaining', transaction.decrement);
|
||||
|
||||
opers.push(decrementOper);
|
||||
} else if (!transaction.lotTransId) {
|
||||
const operation = this.inventoryCostLotTracker()
|
||||
.query(trx)
|
||||
.insert({
|
||||
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
||||
});
|
||||
opers.push(operation);
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Model } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { unitOfTime } from 'moment';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt';
|
||||
import { Item } from '@/modules/Items/models/Item';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export class InventoryCostLotTracker extends BaseModel {
|
||||
date: Date;
|
||||
direction: string;
|
||||
itemId: number;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
remaining: number;
|
||||
cost: number;
|
||||
transactionType: string;
|
||||
transactionId: number;
|
||||
costAccountId: number;
|
||||
entryId: number;
|
||||
createdAt: Date;
|
||||
|
||||
exchangeRate: number;
|
||||
currencyCode: string;
|
||||
|
||||
warehouseId: number;
|
||||
|
||||
item?: Item;
|
||||
invoice?: SaleInvoice;
|
||||
receipt?: SaleReceipt;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'inventory_cost_lot_tracker';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
groupedEntriesCost(query) {
|
||||
query.select(['date', 'item_id', 'transaction_id', 'transaction_type']);
|
||||
query.sum('cost as cost');
|
||||
|
||||
query.groupBy('transaction_id');
|
||||
query.groupBy('transaction_type');
|
||||
query.groupBy('date');
|
||||
query.groupBy('item_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters transactions by the given date range.
|
||||
*/
|
||||
filterDateRange(
|
||||
query,
|
||||
startDate,
|
||||
endDate,
|
||||
type: unitOfTime.StartOf = 'day',
|
||||
) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
if (startDate) {
|
||||
query.where('date', '>=', fromDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where('date', '<=', toDate);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters transactions by the given branches.
|
||||
*/
|
||||
filterByBranches(query, branchesIds: number | Array<number>) {
|
||||
const formattedBranchesIds = castArray(branchesIds);
|
||||
|
||||
query.whereIn('branchId', formattedBranchesIds);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters transactions by the given warehosues.
|
||||
*/
|
||||
filterByWarehouses(query, branchesIds: number | Array<number>) {
|
||||
const formattedWarehousesIds = castArray(branchesIds);
|
||||
|
||||
query.whereIn('warehouseId', formattedWarehousesIds);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Item } = require('../../Items/models/Item');
|
||||
const { SaleInvoice } = require('../../SaleInvoices/models/SaleInvoice');
|
||||
const {
|
||||
ItemEntry,
|
||||
} = require('../../TransactionItemEntry/models/ItemEntry');
|
||||
const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt');
|
||||
|
||||
return {
|
||||
item: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Item,
|
||||
join: {
|
||||
from: 'inventory_cost_lot_tracker.itemId',
|
||||
to: 'items.id',
|
||||
},
|
||||
},
|
||||
invoice: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: SaleInvoice,
|
||||
join: {
|
||||
from: 'inventory_cost_lot_tracker.transactionId',
|
||||
to: 'sales_invoices.id',
|
||||
},
|
||||
},
|
||||
itemEntry: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ItemEntry,
|
||||
join: {
|
||||
from: 'inventory_cost_lot_tracker.entryId',
|
||||
to: 'items_entries.id',
|
||||
},
|
||||
},
|
||||
receipt: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: SaleReceipt,
|
||||
join: {
|
||||
from: 'inventory_cost_lot_tracker.transactionId',
|
||||
to: 'sales_receipts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Model, raw } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
|
||||
import { TInventoryTransactionDirection } from '../types/InventoryCost.types';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { InventoryTransactionMeta } from './InventoryTransactionMeta';
|
||||
|
||||
export class InventoryTransaction extends TenantBaseModel {
|
||||
date!: Date | string;
|
||||
direction!: TInventoryTransactionDirection;
|
||||
itemId!: number;
|
||||
quantity!: number | null;
|
||||
rate!: number;
|
||||
transactionType!: string;
|
||||
transactionId!: number;
|
||||
costAccountId?: number;
|
||||
entryId!: number;
|
||||
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
|
||||
warehouseId?: number;
|
||||
meta?: InventoryTransactionMeta;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'inventory_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve formatted reference type.
|
||||
* @return {string}
|
||||
*/
|
||||
get transcationTypeFormatted(): string {
|
||||
return getTransactionTypeLabel(this.transactionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterDateRange(
|
||||
query,
|
||||
startDate,
|
||||
endDate,
|
||||
type: moment.unitOfTime.StartOf = 'day',
|
||||
) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
if (startDate) {
|
||||
query.where('date', '>=', fromDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where('date', '<=', toDate);
|
||||
}
|
||||
},
|
||||
|
||||
itemsTotals(builder) {
|
||||
builder.select('itemId');
|
||||
builder.sum('rate as rate');
|
||||
builder.sum('quantity as quantity');
|
||||
builder.select(raw('SUM(`QUANTITY` * `RATE`) as COST'));
|
||||
builder.groupBy('itemId');
|
||||
},
|
||||
|
||||
INDirection(builder) {
|
||||
builder.where('direction', 'IN');
|
||||
},
|
||||
|
||||
OUTDirection(builder) {
|
||||
builder.where('direction', 'OUT');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters transactions by the given branches.
|
||||
*/
|
||||
filterByBranches(query, branchesIds) {
|
||||
const formattedBranchesIds = castArray(branchesIds);
|
||||
|
||||
query.whereIn('branch_id', formattedBranchesIds);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters transactions by the given warehosues.
|
||||
*/
|
||||
filterByWarehouses(query, warehousesIds) {
|
||||
const formattedWarehousesIds = castArray(warehousesIds);
|
||||
|
||||
query.whereIn('warehouse_id', formattedWarehousesIds);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Item } = require('../../Items/models/Item');
|
||||
const {
|
||||
ItemEntry,
|
||||
} = require('../../TransactionItemEntry/models/ItemEntry');
|
||||
const { InventoryTransactionMeta } = require('./InventoryTransactionMeta');
|
||||
const { InventoryCostLotTracker } = require('./InventoryCostLotTracker');
|
||||
|
||||
return {
|
||||
// Transaction meta.
|
||||
meta: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: InventoryTransactionMeta,
|
||||
join: {
|
||||
from: 'inventory_transactions.id',
|
||||
to: 'inventory_transaction_meta.inventoryTransactionId',
|
||||
},
|
||||
},
|
||||
// Item cost aggregated.
|
||||
itemCostAggregated: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: InventoryCostLotTracker,
|
||||
join: {
|
||||
from: 'inventory_transactions.itemId',
|
||||
to: 'inventory_cost_lot_tracker.itemId',
|
||||
},
|
||||
filter(query) {
|
||||
query.select('itemId');
|
||||
query.sum('cost as cost');
|
||||
query.sum('quantity as quantity');
|
||||
query.groupBy('itemId');
|
||||
},
|
||||
},
|
||||
costLotAggregated: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: InventoryCostLotTracker,
|
||||
join: {
|
||||
from: 'inventory_transactions.id',
|
||||
to: 'inventory_cost_lot_tracker.inventoryTransactionId',
|
||||
},
|
||||
filter(query) {
|
||||
query.sum('cost as cost');
|
||||
query.sum('quantity as quantity');
|
||||
query.groupBy('inventoryTransactionId');
|
||||
},
|
||||
},
|
||||
item: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Item,
|
||||
join: {
|
||||
from: 'inventory_transactions.itemId',
|
||||
to: 'items.id',
|
||||
},
|
||||
},
|
||||
itemEntry: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ItemEntry,
|
||||
join: {
|
||||
from: 'inventory_transactions.entryId',
|
||||
to: 'items_entries.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Model, raw } from 'objection';
|
||||
|
||||
export class InventoryTransactionMeta extends BaseModel {
|
||||
transactionNumber!: string;
|
||||
description!: string;
|
||||
inventoryTransactionId!: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'inventory_transaction_meta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { InventoryTransaction } = require('./InventoryTransaction');
|
||||
|
||||
return {
|
||||
inventoryTransaction: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: InventoryTransaction,
|
||||
join: {
|
||||
from: 'inventory_transaction_meta.inventoryTransactionId',
|
||||
to: 'inventory_transactions.inventoryTransactionId'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
ComputeItemCostQueue,
|
||||
ComputeItemCostQueueJob,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { Process } from '@nestjs/bull';
|
||||
|
||||
interface ComputeItemCostJobPayload extends TenantJobPayload {
|
||||
itemId: number;
|
||||
startingDate: Date;
|
||||
}
|
||||
@Processor({
|
||||
name: ComputeItemCostQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class ComputeItemCostProcessor extends WorkerHost {
|
||||
/**
|
||||
* @param {InventoryComputeCostService} inventoryComputeCostService -
|
||||
* @param {ClsService} clsService -
|
||||
* @param {EventEmitter2} eventEmitter -
|
||||
*/
|
||||
constructor(
|
||||
private readonly inventoryComputeCostService: InventoryComputeCostService,
|
||||
private readonly clsService: ClsService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the compute item cost job.
|
||||
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
|
||||
*/
|
||||
@Process(ComputeItemCostQueueJob)
|
||||
async process(job: Job<ComputeItemCostJobPayload>) {
|
||||
const { itemId, startingDate, organizationId, userId } = job.data;
|
||||
|
||||
console.log(`Compute item cost for item ${itemId} started`);
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
try {
|
||||
await this.inventoryComputeCostService.computeItemCost(
|
||||
startingDate,
|
||||
itemId,
|
||||
);
|
||||
// Emit job completed event
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.inventory.onComputeItemCostJobCompleted,
|
||||
{ startingDate, itemId, organizationId, userId },
|
||||
);
|
||||
|
||||
console.log(`Compute item cost for item ${itemId} completed`);
|
||||
} catch (error) {
|
||||
console.error('Error computing item cost:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Process } from '@nestjs/bull';
|
||||
import {
|
||||
WriteInventoryTransactionsGLEntriesQueue,
|
||||
WriteInventoryTransactionsGLEntriesQueueJob,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
@Processor({
|
||||
name: WriteInventoryTransactionsGLEntriesQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(WriteInventoryTransactionsGLEntriesQueueJob)
|
||||
async process() {}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { map, head } from 'lodash';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IComputeItemCostJobCompletedPayload,
|
||||
IInventoryTransactionsCreatedPayload,
|
||||
IInventoryTransactionsDeletedPayload,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { ImportAls } from '@/modules/Import/ImportALS';
|
||||
import { InventoryItemsQuantitySyncService } from '../commands/InventoryItemsQuantitySync.service';
|
||||
import { SaleInvoicesCost } from '@/modules/SaleInvoices/SalesInvoicesCost';
|
||||
import { events } from '@/common/events/events';
|
||||
import { runAfterTransaction } from '@/modules/Tenancy/TenancyDB/TransactionsHooks';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryCostSubscriber {
|
||||
constructor(
|
||||
private readonly saleInvoicesCost: SaleInvoicesCost,
|
||||
private readonly itemsQuantitySync: InventoryItemsQuantitySyncService,
|
||||
private readonly inventoryService: InventoryComputeCostService,
|
||||
private readonly importAls: ImportAls,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sync inventory items quantity once inventory transactions created.
|
||||
* @param {IInventoryTransactionsCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryTransactionsCreated)
|
||||
async syncItemsQuantityOnceInventoryTransactionsCreated({
|
||||
inventoryTransactions,
|
||||
trx,
|
||||
}: IInventoryTransactionsCreatedPayload) {
|
||||
const itemsQuantityChanges = this.itemsQuantitySync.getItemsQuantityChanges(
|
||||
inventoryTransactions,
|
||||
);
|
||||
await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles schedule compute inventory items cost once inventory transactions created.
|
||||
* @param {IInventoryTransactionsCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryTransactionsCreated)
|
||||
async handleScheduleItemsCostOnInventoryTransactionsCreated({
|
||||
inventoryTransactions,
|
||||
trx,
|
||||
}: IInventoryTransactionsCreatedPayload) {
|
||||
const inImportPreviewScope = this.importAls.isImportPreview;
|
||||
|
||||
// Avoid running the cost items job if the async process is in import preview.
|
||||
if (inImportPreviewScope) return;
|
||||
|
||||
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
|
||||
inventoryTransactions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks items cost compute running state.
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryTransactionsCreated)
|
||||
async markGlobalSettingsComputeItems({}) {
|
||||
await this.inventoryService.markItemsCostComputeRunning(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks items cost compute as completed.
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryCostEntriesWritten)
|
||||
async markGlobalSettingsComputeItemsCompeted({}) {
|
||||
await this.inventoryService.markItemsCostComputeRunning(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle run writing the journal entries once the compute items jobs completed.
|
||||
*/
|
||||
@OnEvent(events.inventory.onComputeItemCostJobCompleted)
|
||||
async onComputeItemCostJobFinished({
|
||||
itemId,
|
||||
startingDate,
|
||||
}: IComputeItemCostJobCompletedPayload) {
|
||||
// const dependsComputeJobs = await this.agenda.jobs({
|
||||
// name: 'compute-item-cost',
|
||||
// nextRunAt: { $ne: null },
|
||||
// 'data.tenantId': tenantId,
|
||||
// });
|
||||
// // There is no scheduled compute jobs waiting.
|
||||
// if (dependsComputeJobs.length === 0) {
|
||||
// await this.saleInvoicesCost.scheduleWriteJournalEntries(startingDate);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync inventory items quantity once inventory transactions deleted.
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryTransactionsDeleted)
|
||||
async syncItemsQuantityOnceInventoryTransactionsDeleted({
|
||||
oldInventoryTransactions,
|
||||
trx,
|
||||
}: IInventoryTransactionsDeletedPayload) {
|
||||
const itemsQuantityChanges =
|
||||
this.itemsQuantitySync.getReverseItemsQuantityChanges(
|
||||
oldInventoryTransactions,
|
||||
);
|
||||
await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules compute items cost once the inventory transactions deleted.
|
||||
*/
|
||||
@OnEvent(events.inventory.onInventoryTransactionsDeleted)
|
||||
async handleScheduleItemsCostOnInventoryTransactionsDeleted({
|
||||
transactionType,
|
||||
transactionId,
|
||||
oldInventoryTransactions,
|
||||
trx,
|
||||
}: IInventoryTransactionsDeletedPayload) {
|
||||
// Ignore compute item cost with theses transaction types.
|
||||
const ignoreWithTransactionTypes = ['OpeningItem'];
|
||||
|
||||
if (ignoreWithTransactionTypes.indexOf(transactionType) !== -1) {
|
||||
return;
|
||||
}
|
||||
const inventoryItemsIds = map(oldInventoryTransactions, 'itemId');
|
||||
const startingDates = map(oldInventoryTransactions, 'date');
|
||||
const startingDate: Date = head(startingDates);
|
||||
|
||||
runAfterTransaction(trx, async () => {
|
||||
try {
|
||||
await this.saleInvoicesCost.scheduleComputeCostByItemsIds(
|
||||
inventoryItemsIds,
|
||||
startingDate,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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 '../commands/InventoryCostGLStorage.service';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryCostGLBeforeWriteSubscriber {
|
||||
constructor(
|
||||
private readonly inventoryCostGLStorage: InventoryCostGLStorage,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes the receipts cost GL entries once the inventory cost lots be written.
|
||||
* @param {IInventoryCostLotsGLEntriesWriteEvent}
|
||||
*/
|
||||
@OnEvent(events.inventory.onCostLotsGLEntriesBeforeWrite)
|
||||
public async revertsInventoryCostGLEntries({
|
||||
trx,
|
||||
startingDate,
|
||||
}: IInventoryCostLotsGLEntriesWriteEvent) {
|
||||
await this.inventoryCostGLStorage.revertInventoryCostGLEntries(
|
||||
startingDate,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Knex } from 'knex';
|
||||
import { InventoryTransaction } from '../models/InventoryTransaction';
|
||||
|
||||
export const ComputeItemCostQueue = 'ComputeItemCostQueue';
|
||||
export const ComputeItemCostQueueJob = 'ComputeItemCostQueueJob';
|
||||
|
||||
export const WriteInventoryTransactionsGLEntriesQueue =
|
||||
'WriteInventoryTransactionsGLEntriesQueue';
|
||||
export const WriteInventoryTransactionsGLEntriesQueueJob =
|
||||
'WriteInventoryTransactionsGLEntriesQueueJob';
|
||||
|
||||
export interface IInventoryItemCostMeta {
|
||||
itemId: number;
|
||||
valuation: number;
|
||||
quantity: number;
|
||||
average: number;
|
||||
}
|
||||
|
||||
export interface IInventoryCostLotsGLEntriesWriteEvent {
|
||||
startingDate: Date;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export type TInventoryTransactionDirection = 'IN' | 'OUT';
|
||||
|
||||
export type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
||||
|
||||
export interface IInventoryTransactionMeta {
|
||||
id?: number;
|
||||
transactionNumber: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface IInventoryCostLotAggregated {
|
||||
cost: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface IItemsQuantityChanges {
|
||||
itemId: number;
|
||||
balanceChange: number;
|
||||
}
|
||||
|
||||
export interface IInventoryTransactionsCreatedPayload {
|
||||
inventoryTransactions: InventoryTransaction[];
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IInventoryTransactionsDeletedPayload {
|
||||
oldInventoryTransactions: InventoryTransaction[];
|
||||
transactionId: number;
|
||||
transactionType: string;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IInventoryItemCostScheduledPayload {
|
||||
startingDate: Date | string;
|
||||
itemId: number;
|
||||
}
|
||||
|
||||
export interface IComputeItemCostJobStartedPayload {
|
||||
startingDate: Date | string;
|
||||
itemId: number;
|
||||
}
|
||||
export interface IComputeItemCostJobCompletedPayload {
|
||||
startingDate: Date | string;
|
||||
itemId: number;
|
||||
}
|
||||
57
packages/server/src/modules/InventoryCost/utils.ts
Normal file
57
packages/server/src/modules/InventoryCost/utils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
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.
|
||||
* @param {IInventoryTransaction} invTransactions
|
||||
* @returns
|
||||
*/
|
||||
export function groupInventoryTransactionsByTypeId(
|
||||
transactions: { transactionType: string; transactionId: number }[],
|
||||
): { transactionType: string; transactionId: number }[][] {
|
||||
return chain(transactions)
|
||||
.groupBy((t) => `${t.transactionType}-${t.transactionId}`)
|
||||
.values()
|
||||
.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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user