mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
refactor: inventory transfers to nestjs
This commit is contained in:
@@ -8,6 +8,18 @@ import { InventoryItemsQuantitySyncService } from './commands/InventoryItemsQuan
|
||||
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';
|
||||
|
||||
const models = [
|
||||
RegisterTenancyModel(InventoryCostLotTracker),
|
||||
@@ -15,14 +27,28 @@ const models = [
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [LedgerModule, ...models],
|
||||
imports: [
|
||||
LedgerModule,
|
||||
...models,
|
||||
BullModule.registerQueue({ name: ComputeItemCostQueue }),
|
||||
BullModule.registerQueue({
|
||||
name: WriteInventoryTransactionsGLEntriesQueue,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
InventoryCostGLBeforeWriteSubscriber,
|
||||
InventoryCostGLStorage,
|
||||
InventoryItemsQuantitySyncService,
|
||||
InventoryTransactionsService,
|
||||
InventoryComputeCostService,
|
||||
InventoryCostApplication,
|
||||
StoreInventoryLotsCostService,
|
||||
ComputeItemCostProcessor,
|
||||
WriteInventoryTransactionsGLEntriesProcessor,
|
||||
InventoryAverageCostMethodService,
|
||||
InventoryItemCostService,
|
||||
InventoryItemOpeningAvgCostService,
|
||||
],
|
||||
exports: [...models, InventoryTransactionsService],
|
||||
exports: [...models, InventoryTransactionsService, InventoryItemCostService],
|
||||
})
|
||||
export class InventoryCostModule {}
|
||||
|
||||
@@ -3,12 +3,6 @@ import { Knex } from 'knex';
|
||||
import { InventoryTransaction } from '../models/InventoryTransaction';
|
||||
|
||||
export class InventoryAverageCostMethod {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {Date} startingDate -
|
||||
* @param {number} itemId - The given inventory item id.
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,7 +72,6 @@ export class InventoryComputeCostService {
|
||||
* @param {Date} startingDate
|
||||
*/
|
||||
async scheduleComputeItemCost(
|
||||
tenantId: number,
|
||||
itemId: number,
|
||||
startingDate: Date | string,
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { keyBy, get } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import * as R from 'ramda';
|
||||
import { IInventoryItemCostMeta } from '../types/InventoryCost.types';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { InventoryTransaction } from '../models/InventoryTransaction';
|
||||
@@ -22,33 +20,30 @@ export class InventoryItemCostService {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} INValuationMap -
|
||||
* @param {} OUTValuationMap -
|
||||
* @param {Map<number, IInventoryItemCostMeta>} INValuationMap -
|
||||
* @param {Map<number, IInventoryItemCostMeta>} OUTValuationMap -
|
||||
* @param {number} itemId
|
||||
*/
|
||||
private getItemInventoryMeta = R.curry(
|
||||
(
|
||||
INValuationMap,
|
||||
OUTValuationMap,
|
||||
itemId: number,
|
||||
): IInventoryItemCostMeta => {
|
||||
const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
|
||||
const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
|
||||
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 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;
|
||||
const valuation = INCost - OUTCost;
|
||||
const quantity = INQuantity - OUTQuantity;
|
||||
const average = quantity ? valuation / quantity : 0;
|
||||
|
||||
return { itemId, valuation, quantity, average };
|
||||
},
|
||||
);
|
||||
return { itemId, valuation, quantity, average };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemsId
|
||||
* @param {Date} date
|
||||
* @returns
|
||||
@@ -57,7 +52,7 @@ export class InventoryItemCostService {
|
||||
itemsId: number[],
|
||||
date: Date,
|
||||
): Promise<any> => {
|
||||
const commonBuilder = (builder: Knex.QueryBuilder) => {
|
||||
const commonBuilder = (builder) => {
|
||||
if (date) {
|
||||
builder.where('date', '<', date);
|
||||
}
|
||||
@@ -84,7 +79,6 @@ export class InventoryItemCostService {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId -
|
||||
* @param {number[]} itemsIds -
|
||||
* @param {Date} date -
|
||||
*/
|
||||
@@ -122,11 +116,10 @@ export class InventoryItemCostService {
|
||||
const [OUTValuationMap, INValuationMap] =
|
||||
await this.getItemsInventoryInOutMap(itemsId, date);
|
||||
|
||||
const getItemValuation = this.getItemInventoryMeta(
|
||||
INValuationMap,
|
||||
OUTValuationMap,
|
||||
);
|
||||
const itemsValuations = inventoryItemsIds.map(getItemValuation);
|
||||
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]),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
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
|
||||
>,
|
||||
@@ -31,6 +32,10 @@ export class InventoryItemOpeningAvgCostService {
|
||||
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()
|
||||
@@ -43,10 +48,11 @@ export class InventoryItemOpeningAvgCostService {
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
const [inInvSumation, outInvSumation] = await Promise.all([
|
||||
const [inInvSumation, outInvSumation] = (await Promise.all([
|
||||
inInvSumationOper,
|
||||
outInvSumationOper,
|
||||
]);
|
||||
])) as unknown as [QueryResult, QueryResult];
|
||||
|
||||
return this.computeItemAverageCost(
|
||||
inInvSumation?.cost || 0,
|
||||
inInvSumation?.quantity || 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ export class InventoryTransactionsService {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async recordInventoryTransactions(
|
||||
transactions: InventoryTransaction[],
|
||||
transactions: ModelObject<InventoryTransaction>[],
|
||||
override: boolean = false,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -7,21 +7,20 @@ 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;
|
||||
date!: Date | string;
|
||||
direction!: TInventoryTransactionDirection;
|
||||
itemId!: number;
|
||||
quantity!: number | null;
|
||||
rate!: number;
|
||||
transactionType!: string;
|
||||
transactionId!: number;
|
||||
costAccountId?: number;
|
||||
entryId: number;
|
||||
entryId!: number;
|
||||
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
|
||||
warehouseId?: number;
|
||||
|
||||
meta?: InventoryTransactionMeta;
|
||||
|
||||
/**
|
||||
@@ -34,7 +33,7 @@ export class InventoryTransaction extends TenantBaseModel {
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,58 @@
|
||||
import { JOB_REF, Processor } from '@nestjs/bullmq';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JOB_REF, Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ComputeItemCostQueueJob } from '../types/InventoryCost.types';
|
||||
|
||||
interface ComputeItemCostJobPayload extends TenantJobPayload {
|
||||
itemId: number;
|
||||
startingDate: Date;
|
||||
}
|
||||
|
||||
@Processor({
|
||||
name: 'compute-item-cost',
|
||||
name: ComputeItemCostQueueJob,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class ComputeItemCostProcessor {
|
||||
export class ComputeItemCostProcessor extends WorkerHost {
|
||||
/**
|
||||
* @param {InventoryComputeCostService} inventoryComputeCostService -
|
||||
* @param {ClsService} clsService -
|
||||
* @param {EventEmitter2} eventEmitter -
|
||||
*/
|
||||
constructor(
|
||||
private readonly inventoryComputeCostService: InventoryComputeCostService,
|
||||
private readonly clsService: ClsService,
|
||||
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<ComputeItemCostJobPayload>,
|
||||
) {}
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle compute item cost job.
|
||||
* Process the compute item cost job.
|
||||
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
|
||||
*/
|
||||
async handleComputeItemCost() {
|
||||
const { itemId, startingDate, organizationId, userId } = this.jobRef.data;
|
||||
async process(job: Job<ComputeItemCostJobPayload>) {
|
||||
const { itemId, startingDate, organizationId, userId } = job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
await this.inventoryComputeCostService.computeItemCost(
|
||||
startingDate,
|
||||
itemId,
|
||||
);
|
||||
try {
|
||||
await this.inventoryComputeCostService.computeItemCost(
|
||||
startingDate,
|
||||
itemId,
|
||||
);
|
||||
// Emit job completed event
|
||||
await this.eventEmitter.emitAsync(
|
||||
events.inventory.onComputeItemCostJobCompleted,
|
||||
{ startingDate, itemId, organizationId, userId },
|
||||
);
|
||||
} 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 default class InventorySubscriber {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
import { InventoryTransaction } from "../models/InventoryTransaction";
|
||||
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;
|
||||
@@ -10,8 +17,8 @@ export interface IInventoryItemCostMeta {
|
||||
}
|
||||
|
||||
export interface IInventoryCostLotsGLEntriesWriteEvent {
|
||||
startingDate: Date,
|
||||
trx: Knex.Transaction
|
||||
startingDate: Date;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export type TInventoryTransactionDirection = 'IN' | 'OUT';
|
||||
|
||||
Reference in New Issue
Block a user