mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
add server to monorepo.
This commit is contained in:
377
packages/server/src/services/Inventory/Inventory.ts
Normal file
377
packages/server/src/services/Inventory/Inventory.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import { pick } from 'lodash';
|
||||
import config from '@/config';
|
||||
import {
|
||||
IInventoryLotCost,
|
||||
IInventoryTransaction,
|
||||
TInventoryTransactionDirection,
|
||||
IItemEntry,
|
||||
IItemEntryTransactionType,
|
||||
IInventoryTransactionsCreatedPayload,
|
||||
IInventoryTransactionsDeletedPayload,
|
||||
IInventoryItemCostScheduledPayload,
|
||||
} from '@/interfaces';
|
||||
import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost';
|
||||
import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import { Knex } from 'knex';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
||||
|
||||
@Service()
|
||||
export default class InventoryService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
@Inject()
|
||||
uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Transforms the items entries to inventory transactions.
|
||||
*/
|
||||
transformItemEntriesToInventory(transaction: {
|
||||
transactionId: number;
|
||||
transactionType: IItemEntryTransactionType;
|
||||
transactionNumber?: string;
|
||||
|
||||
exchangeRate?: number;
|
||||
|
||||
warehouseId: number | null;
|
||||
|
||||
date: Date | string;
|
||||
direction: TInventoryTransactionDirection;
|
||||
entries: IItemEntry[];
|
||||
createdAt: Date;
|
||||
}): IInventoryTransaction[] {
|
||||
const exchangeRate = transaction.exchangeRate || 1;
|
||||
|
||||
return transaction.entries.map((entry: IItemEntry) => ({
|
||||
...pick(entry, ['itemId', 'quantity']),
|
||||
rate: entry.rate * exchangeRate,
|
||||
transactionType: transaction.transactionType,
|
||||
transactionId: transaction.transactionId,
|
||||
direction: transaction.direction,
|
||||
date: transaction.date,
|
||||
entryId: entry.id,
|
||||
createdAt: transaction.createdAt,
|
||||
costAccountId: entry.costAccountId,
|
||||
|
||||
warehouseId: entry.warehouseId || transaction.warehouseId,
|
||||
meta: {
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
description: entry.description,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async computeItemCost(tenantId: number, fromDate: Date, itemId: number) {
|
||||
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
|
||||
return this.computeInventoryItemCost(tenantId, fromDate, itemId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the given item cost and records the inventory lots transactions
|
||||
* and journal entries based on the cost method FIFO, LIFO or average cost rate.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
async computeInventoryItemCost(
|
||||
tenantId: number,
|
||||
fromDate: Date,
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Fetches the item with assocaited item category.
|
||||
const item = await Item.query().findById(itemId);
|
||||
|
||||
// Cannot continue if the given item was not inventory item.
|
||||
if (item.type !== 'inventory') {
|
||||
throw new Error('You could not compute item cost has no inventory type.');
|
||||
}
|
||||
let costMethodComputer: IInventoryCostMethod;
|
||||
|
||||
// Switch between methods based on the item cost method.
|
||||
switch ('AVG') {
|
||||
case 'FIFO':
|
||||
case 'LIFO':
|
||||
costMethodComputer = new InventoryCostLotTracker(
|
||||
tenantId,
|
||||
fromDate,
|
||||
itemId
|
||||
);
|
||||
break;
|
||||
case 'AVG':
|
||||
costMethodComputer = new InventoryAverageCost(
|
||||
tenantId,
|
||||
fromDate,
|
||||
itemId,
|
||||
trx
|
||||
);
|
||||
break;
|
||||
}
|
||||
return costMethodComputer.computeItemCost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule item cost compute job.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @param {Date} startingDate
|
||||
*/
|
||||
async scheduleComputeItemCost(
|
||||
tenantId: number,
|
||||
itemId: number,
|
||||
startingDate: Date | string
|
||||
) {
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
// Cancel any `compute-item-cost` in the queue has upper starting date
|
||||
// with the same given item.
|
||||
await agenda.cancel({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $gt: startingDate },
|
||||
});
|
||||
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
||||
// with the same given item.
|
||||
const dependsJobs = await agenda.jobs({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $lte: startingDate },
|
||||
});
|
||||
if (dependsJobs.length === 0) {
|
||||
await agenda.schedule(
|
||||
config.scheduleComputeItemCost,
|
||||
'compute-item-cost',
|
||||
{
|
||||
startingDate,
|
||||
itemId,
|
||||
tenantId,
|
||||
}
|
||||
);
|
||||
// Triggers `onComputeItemCostJobScheduled` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onComputeItemCostJobScheduled,
|
||||
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
tenantId: number,
|
||||
transactions: IInventoryTransaction[],
|
||||
override: boolean = false,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const bulkInsertOpers = [];
|
||||
|
||||
transactions.forEach((transaction: IInventoryTransaction) => {
|
||||
const oper = this.recordInventoryTransaction(
|
||||
tenantId,
|
||||
transaction,
|
||||
override,
|
||||
trx
|
||||
);
|
||||
bulkInsertOpers.push(oper);
|
||||
});
|
||||
const inventoryTransactions = await Promise.all(bulkInsertOpers);
|
||||
|
||||
// Triggers `onInventoryTransactionsCreated` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onInventoryTransactionsCreated,
|
||||
{
|
||||
tenantId,
|
||||
inventoryTransactions,
|
||||
trx,
|
||||
} as IInventoryTransactionsCreatedPayload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the inventory transactiosn on the storage from the given
|
||||
* inventory transactions entries.
|
||||
*
|
||||
* @param {number} tenantId -
|
||||
* @param {IInventoryTransaction} inventoryEntry -
|
||||
* @param {boolean} deleteOld -
|
||||
*/
|
||||
async recordInventoryTransaction(
|
||||
tenantId: number,
|
||||
inventoryEntry: IInventoryTransaction,
|
||||
deleteOld: boolean = false,
|
||||
trx: Knex.Transaction
|
||||
): Promise<IInventoryTransaction> {
|
||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
if (deleteOld) {
|
||||
await this.deleteInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryEntry.transactionId,
|
||||
inventoryEntry.transactionType,
|
||||
trx
|
||||
);
|
||||
}
|
||||
return InventoryTransaction.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(
|
||||
tenantId: number,
|
||||
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(
|
||||
tenantId,
|
||||
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(
|
||||
tenantId: number,
|
||||
transactionId: number,
|
||||
transactionType: string,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> {
|
||||
const { InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the inventory transactions of the given sale invoice.
|
||||
const oldInventoryTransactions = await InventoryTransaction.query(
|
||||
trx
|
||||
).where({ transactionId, transactionType });
|
||||
|
||||
// Deletes the inventory transactions by the given transaction type and id.
|
||||
await InventoryTransaction.query(trx)
|
||||
.where({ transactionType, transactionId })
|
||||
.delete();
|
||||
|
||||
// Triggers `onInventoryTransactionsDeleted` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventory.onInventoryTransactionsDeleted,
|
||||
{
|
||||
tenantId,
|
||||
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> {
|
||||
const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||
|
||||
return InventoryCostLotTracker.query().insert({
|
||||
...inventoryLotEntry,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark item cost computing is running.
|
||||
* @param {number} tenantId -
|
||||
* @param {boolean} isRunning -
|
||||
*/
|
||||
async markItemsCostComputeRunning(
|
||||
tenantId: number,
|
||||
isRunning: boolean = true
|
||||
) {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
settings.set({
|
||||
key: 'cost_compute_running',
|
||||
group: 'inventory',
|
||||
value: isRunning,
|
||||
});
|
||||
await settings.save();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
*/
|
||||
isItemsCostComputeRunning(tenantId) {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
return settings.get({
|
||||
key: 'cost_compute_running',
|
||||
group: 'inventory',
|
||||
});
|
||||
}
|
||||
}
|
||||
226
packages/server/src/services/Inventory/InventoryAdjustmentGL.ts
Normal file
226
packages/server/src/services/Inventory/InventoryAdjustmentGL.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import * as R from 'ramda';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
AccountNormal,
|
||||
IInventoryAdjustment,
|
||||
IInventoryAdjustmentEntry,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class InventoryAdjustmentsGL {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Retrieves the inventory adjustment common GL entry.
|
||||
* @param {InventoryAdjustment} inventoryAdjustment -
|
||||
* @param {string} baseCurrency -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentGLCommonEntry = (
|
||||
inventoryAdjustment: IInventoryAdjustment,
|
||||
baseCurrency: string
|
||||
) => {
|
||||
return {
|
||||
currencyCode: baseCurrency,
|
||||
exchangeRate: 1,
|
||||
|
||||
transactionId: inventoryAdjustment.id,
|
||||
transactionType: 'InventoryAdjustment',
|
||||
referenceNumber: inventoryAdjustment.referenceNo,
|
||||
|
||||
date: inventoryAdjustment.date,
|
||||
|
||||
userId: inventoryAdjustment.userId,
|
||||
branchId: inventoryAdjustment.branchId,
|
||||
|
||||
createdAt: inventoryAdjustment.createdAt,
|
||||
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustment inventory GL entry.
|
||||
* @param {IInventoryAdjustment} inventoryAdjustment -Inventory adjustment model.
|
||||
* @param {string} baseCurrency - Base currency of the organization.
|
||||
* @param {IInventoryAdjustmentEntry} entry -
|
||||
* @param {number} index -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentGLInventoryEntry = R.curry(
|
||||
(
|
||||
inventoryAdjustment: IInventoryAdjustment,
|
||||
baseCurrency: string,
|
||||
entry: IInventoryAdjustmentEntry,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getAdjustmentGLCommonEntry(
|
||||
inventoryAdjustment,
|
||||
baseCurrency
|
||||
);
|
||||
const amount = entry.cost * entry.quantity;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: amount,
|
||||
accountId: entry.item.inventoryAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the inventory adjustment
|
||||
* @param {IInventoryAdjustment} inventoryAdjustment
|
||||
* @param {IInventoryAdjustmentEntry} entry
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getAdjustmentGLCostEntry = R.curry(
|
||||
(
|
||||
inventoryAdjustment: IInventoryAdjustment,
|
||||
baseCurrency: string,
|
||||
entry: IInventoryAdjustmentEntry,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getAdjustmentGLCommonEntry(
|
||||
inventoryAdjustment,
|
||||
baseCurrency
|
||||
);
|
||||
const amount = entry.cost * entry.quantity;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
accountId: inventoryAdjustment.adjustmentAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
credit: amount,
|
||||
index: index + 2,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustment GL item entry.
|
||||
* @param {InventoryAdjustment} adjustment
|
||||
* @param {string} baseCurrency
|
||||
* @param {InventoryAdjustmentEntry} entry
|
||||
* @param {number} index
|
||||
* @returns {}
|
||||
*/
|
||||
private getAdjustmentGLItemEntry = R.curry(
|
||||
(
|
||||
adjustment: IInventoryAdjustment,
|
||||
baseCurrency: string,
|
||||
entry: IInventoryAdjustmentEntry,
|
||||
index: number
|
||||
): ILedgerEntry[] => {
|
||||
const getInventoryEntry = this.getAdjustmentGLInventoryEntry(
|
||||
adjustment,
|
||||
baseCurrency
|
||||
);
|
||||
const inventoryEntry = getInventoryEntry(entry, index);
|
||||
const costEntry = this.getAdjustmentGLCostEntry(
|
||||
adjustment,
|
||||
baseCurrency,
|
||||
entry,
|
||||
index
|
||||
);
|
||||
return [inventoryEntry, costEntry];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes increment inventroy adjustment GL entries.
|
||||
* @param {InventoryAdjustment} inventoryAdjustment -
|
||||
* @param {JournalPoster} jorunal -
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getIncrementAdjustmentGLEntries(
|
||||
inventoryAdjustment: IInventoryAdjustment,
|
||||
baseCurrency: string
|
||||
): ILedgerEntry[] {
|
||||
const getItemEntry = this.getAdjustmentGLItemEntry(
|
||||
inventoryAdjustment,
|
||||
baseCurrency
|
||||
);
|
||||
return inventoryAdjustment.entries.map(getItemEntry).flat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes inventory increment adjustment GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
*/
|
||||
public writeAdjustmentGLEntries = async (
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { InventoryAdjustment } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the inventory adjustment with associated entries.
|
||||
const adjustment = await InventoryAdjustment.query(trx)
|
||||
.findById(inventoryAdjustmentId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Retrieves the inventory adjustment GL entries.
|
||||
const entries = this.getIncrementAdjustmentGLEntries(
|
||||
adjustment,
|
||||
tenantMeta.baseCurrency
|
||||
);
|
||||
const ledger = new Ledger(entries);
|
||||
|
||||
// Commits the ledger entries to the storage.
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the adjustment transactions GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public revertAdjustmentGLEntries = (
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
return this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
inventoryAdjustmentId,
|
||||
'InventoryAdjustment',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrite inventory adjustment GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewriteAdjustmentGLEntries = async (
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Reverts GL entries of the given inventory adjustment.
|
||||
await this.revertAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx);
|
||||
|
||||
// Writes GL entries of th egiven inventory adjustment.
|
||||
await this.writeAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IQuickInventoryAdjustmentDTO,
|
||||
IInventoryAdjustment,
|
||||
IPaginationMeta,
|
||||
IInventoryAdjustmentsFilter,
|
||||
ISystemUser,
|
||||
IInventoryTransaction,
|
||||
IInventoryAdjustmentEventCreatedPayload,
|
||||
IInventoryAdjustmentEventPublishedPayload,
|
||||
IInventoryAdjustmentEventDeletedPayload,
|
||||
IInventoryAdjustmentCreatingPayload,
|
||||
IInventoryAdjustmentDeletingPayload,
|
||||
IInventoryAdjustmentPublishingPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InventoryService from './Inventory';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import InventoryAdjustmentTransformer from './InventoryAdjustmentTransformer';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
const ERRORS = {
|
||||
INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND',
|
||||
ITEM_SHOULD_BE_INVENTORY_TYPE: 'ITEM_SHOULD_BE_INVENTORY_TYPE',
|
||||
INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED:
|
||||
'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class InventoryAdjustmentService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private inventoryService: InventoryService;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
||||
|
||||
@Inject()
|
||||
private transfromer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Transformes the quick inventory adjustment DTO to model object.
|
||||
* @param {IQuickInventoryAdjustmentDTO} adjustmentDTO -
|
||||
* @return {IInventoryAdjustment}
|
||||
*/
|
||||
private transformQuickAdjToModel(
|
||||
tenantId: number,
|
||||
adjustmentDTO: IQuickInventoryAdjustmentDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): IInventoryAdjustment {
|
||||
const entries = [
|
||||
{
|
||||
index: 1,
|
||||
itemId: adjustmentDTO.itemId,
|
||||
...('increment' === adjustmentDTO.type
|
||||
? {
|
||||
quantity: adjustmentDTO.quantity,
|
||||
cost: adjustmentDTO.cost,
|
||||
}
|
||||
: {}),
|
||||
...('decrement' === adjustmentDTO.type
|
||||
? {
|
||||
quantity: adjustmentDTO.quantity,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
];
|
||||
const initialDTO = {
|
||||
...omit(adjustmentDTO, ['quantity', 'cost', 'itemId', 'publish']),
|
||||
userId: authorizedUser.id,
|
||||
...(adjustmentDTO.publish
|
||||
? {
|
||||
publishedAt: moment().toMySqlDateTime(),
|
||||
}
|
||||
: {}),
|
||||
entries,
|
||||
};
|
||||
return R.compose(
|
||||
this.warehouseDTOTransform.transformDTO<IInventoryAdjustment>(tenantId),
|
||||
this.branchDTOTransform.transformDTO<IInventoryAdjustment>(tenantId)
|
||||
)(initialDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the item inventory type.
|
||||
* @param {IItem} item
|
||||
*/
|
||||
validateItemInventoryType(item) {
|
||||
if (item.type !== 'inventory') {
|
||||
throw new ServiceError(ERRORS.ITEM_SHOULD_BE_INVENTORY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustment or throw not found service error.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} adjustmentId -
|
||||
*/
|
||||
async getInventoryAdjustmentOrThrowError(
|
||||
tenantId: number,
|
||||
adjustmentId: number
|
||||
) {
|
||||
const { InventoryAdjustment } = this.tenancy.models(tenantId);
|
||||
|
||||
const inventoryAdjustment = await InventoryAdjustment.query()
|
||||
.findById(adjustmentId)
|
||||
.withGraphFetched('entries');
|
||||
|
||||
if (!inventoryAdjustment) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND);
|
||||
}
|
||||
return inventoryAdjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a quick inventory adjustment for specific item.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - qucik adjustment DTO.
|
||||
*/
|
||||
public async createQuickAdjustment(
|
||||
tenantId: number,
|
||||
quickAdjustmentDTO: IQuickInventoryAdjustmentDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<IInventoryAdjustment> {
|
||||
const { InventoryAdjustment, Account, Item } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the adjustment account or throw not found error.
|
||||
const adjustmentAccount = await Account.query()
|
||||
.findById(quickAdjustmentDTO.adjustmentAccountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Retrieve the item model or throw not found service error.
|
||||
const item = await Item.query()
|
||||
.findById(quickAdjustmentDTO.itemId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate item inventory type.
|
||||
this.validateItemInventoryType(item);
|
||||
|
||||
// Transform the DTO to inventory adjustment model.
|
||||
const invAdjustmentObject = this.transformQuickAdjToModel(
|
||||
tenantId,
|
||||
quickAdjustmentDTO,
|
||||
authorizedUser
|
||||
);
|
||||
// Writes inventory adjustment transaction with associated transactions
|
||||
// under unit-of-work envirment.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onInventoryAdjustmentCreating` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onQuickCreating,
|
||||
{
|
||||
tenantId,
|
||||
trx,
|
||||
quickAdjustmentDTO,
|
||||
} as IInventoryAdjustmentCreatingPayload
|
||||
);
|
||||
// Saves the inventory adjustment with assocaited entries to the storage.
|
||||
const inventoryAdjustment = await InventoryAdjustment.query(
|
||||
trx
|
||||
).upsertGraph({
|
||||
...invAdjustmentObject,
|
||||
});
|
||||
// Triggers `onInventoryAdjustmentQuickCreated` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onQuickCreated,
|
||||
{
|
||||
tenantId,
|
||||
inventoryAdjustment,
|
||||
inventoryAdjustmentId: inventoryAdjustment.id,
|
||||
trx,
|
||||
} as IInventoryAdjustmentEventCreatedPayload
|
||||
);
|
||||
return inventoryAdjustment;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the inventory adjustment transaction.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
|
||||
*/
|
||||
public async deleteInventoryAdjustment(
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number
|
||||
): Promise<void> {
|
||||
const { InventoryAdjustmentEntry, InventoryAdjustment } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the inventory adjustment or throw not found service error.
|
||||
const oldInventoryAdjustment =
|
||||
await this.getInventoryAdjustmentOrThrowError(
|
||||
tenantId,
|
||||
inventoryAdjustmentId
|
||||
);
|
||||
// Deletes the inventory adjustment transaction and associated transactions
|
||||
// under unit-of-work env.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onInventoryAdjustmentDeleting` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onDeleting,
|
||||
{
|
||||
trx,
|
||||
oldInventoryAdjustment,
|
||||
tenantId,
|
||||
} as IInventoryAdjustmentDeletingPayload
|
||||
);
|
||||
|
||||
// Deletes the inventory adjustment entries.
|
||||
await InventoryAdjustmentEntry.query(trx)
|
||||
.where('adjustment_id', inventoryAdjustmentId)
|
||||
.delete();
|
||||
|
||||
// Deletes the inventory adjustment transaction.
|
||||
await InventoryAdjustment.query(trx)
|
||||
.findById(inventoryAdjustmentId)
|
||||
.delete();
|
||||
|
||||
// Triggers `onInventoryAdjustmentDeleted` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onDeleted,
|
||||
{
|
||||
tenantId,
|
||||
inventoryAdjustmentId,
|
||||
oldInventoryAdjustment,
|
||||
trx,
|
||||
} as IInventoryAdjustmentEventDeletedPayload
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the inventory adjustment transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
*/
|
||||
public async publishInventoryAdjustment(
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number
|
||||
): Promise<void> {
|
||||
const { InventoryAdjustment } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the inventory adjustment or throw not found service error.
|
||||
const oldInventoryAdjustment =
|
||||
await this.getInventoryAdjustmentOrThrowError(
|
||||
tenantId,
|
||||
inventoryAdjustmentId
|
||||
);
|
||||
|
||||
// Validate adjustment not already published.
|
||||
this.validateAdjustmentTransactionsNotPublished(oldInventoryAdjustment);
|
||||
|
||||
// Publishes inventory adjustment with associated inventory transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onPublishing,
|
||||
{
|
||||
trx,
|
||||
tenantId,
|
||||
oldInventoryAdjustment,
|
||||
} as IInventoryAdjustmentPublishingPayload
|
||||
);
|
||||
|
||||
// Publish the inventory adjustment transaction.
|
||||
await InventoryAdjustment.query().findById(inventoryAdjustmentId).patch({
|
||||
publishedAt: moment().toMySqlDateTime(),
|
||||
});
|
||||
// Retrieve the inventory adjustment after the modification.
|
||||
const inventoryAdjustment = await InventoryAdjustment.query()
|
||||
.findById(inventoryAdjustmentId)
|
||||
.withGraphFetched('entries');
|
||||
|
||||
// Triggers `onInventoryAdjustmentDeleted` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.inventoryAdjustment.onPublished,
|
||||
{
|
||||
tenantId,
|
||||
inventoryAdjustmentId,
|
||||
inventoryAdjustment,
|
||||
oldInventoryAdjustment,
|
||||
trx,
|
||||
} as IInventoryAdjustmentEventPublishedPayload
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses inventory adjustments list filter DTO.
|
||||
* @param filterDTO -
|
||||
*/
|
||||
private parseListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustments paginated list.
|
||||
* @param {number} tenantId
|
||||
* @param {IInventoryAdjustmentsFilter} adjustmentsFilter
|
||||
*/
|
||||
public async getInventoryAdjustments(
|
||||
tenantId: number,
|
||||
filterDTO: IInventoryAdjustmentsFilter
|
||||
): Promise<{
|
||||
inventoryAdjustments: IInventoryAdjustment[];
|
||||
pagination: IPaginationMeta;
|
||||
}> {
|
||||
const { InventoryAdjustment } = this.tenancy.models(tenantId);
|
||||
|
||||
// Parses inventory adjustments list filter DTO.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
tenantId,
|
||||
InventoryAdjustment,
|
||||
filter
|
||||
);
|
||||
const { results, pagination } = await InventoryAdjustment.query()
|
||||
.onBuild((query) => {
|
||||
query.withGraphFetched('entries.item');
|
||||
query.withGraphFetched('adjustmentAccount');
|
||||
|
||||
dynamicFilter.buildQuery()(query);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Retrieves the transformed inventory adjustments.
|
||||
const inventoryAdjustments = await this.transfromer.transform(
|
||||
tenantId,
|
||||
results,
|
||||
new InventoryAdjustmentTransformer()
|
||||
);
|
||||
return {
|
||||
inventoryAdjustments,
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the inventory transactions from the inventory adjustment transaction.
|
||||
* @param {number} tenantId -
|
||||
* @param {IInventoryAdjustment} inventoryAdjustment -
|
||||
* @param {boolean} override -
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async writeInventoryTransactions(
|
||||
tenantId: number,
|
||||
inventoryAdjustment: IInventoryAdjustment,
|
||||
override: boolean = false,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const commonTransaction = {
|
||||
direction: inventoryAdjustment.inventoryDirection,
|
||||
date: inventoryAdjustment.date,
|
||||
transactionType: 'InventoryAdjustment',
|
||||
transactionId: inventoryAdjustment.id,
|
||||
createdAt: inventoryAdjustment.createdAt,
|
||||
costAccountId: inventoryAdjustment.adjustmentAccountId,
|
||||
|
||||
branchId: inventoryAdjustment.branchId,
|
||||
warehouseId: inventoryAdjustment.warehouseId,
|
||||
};
|
||||
const inventoryTransactions = [];
|
||||
|
||||
inventoryAdjustment.entries.forEach((entry) => {
|
||||
inventoryTransactions.push({
|
||||
...commonTransaction,
|
||||
itemId: entry.itemId,
|
||||
quantity: entry.quantity,
|
||||
rate: entry.cost,
|
||||
});
|
||||
});
|
||||
// Saves the given inventory transactions to the storage.
|
||||
await this.inventoryService.recordInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryTransactions,
|
||||
override,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the inventory transactions from the inventory adjustment transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
*/
|
||||
async revertInventoryTransactions(
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> {
|
||||
return this.inventoryService.deleteInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryAdjustmentId,
|
||||
'InventoryAdjustment',
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve specific inventory adjustment transaction details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAdjustmentId
|
||||
*/
|
||||
async getInventoryAdjustment(
|
||||
tenantId: number,
|
||||
inventoryAdjustmentId: number
|
||||
) {
|
||||
const { InventoryAdjustment } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve inventory adjustment transation with associated models.
|
||||
const inventoryAdjustment = await InventoryAdjustment.query()
|
||||
.findById(inventoryAdjustmentId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('adjustmentAccount');
|
||||
|
||||
// Throw not found if the given adjustment transaction not exists.
|
||||
this.throwIfAdjustmentNotFound(inventoryAdjustment);
|
||||
|
||||
return this.transfromer.transform(
|
||||
tenantId,
|
||||
inventoryAdjustment,
|
||||
new InventoryAdjustmentTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the adjustment transaction is exists.
|
||||
* @param {IInventoryAdjustment} inventoryAdjustment
|
||||
*/
|
||||
private throwIfAdjustmentNotFound(inventoryAdjustment: IInventoryAdjustment) {
|
||||
if (!inventoryAdjustment) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the adjustment transaction is not already published.
|
||||
* @param {IInventoryAdjustment} oldInventoryAdjustment
|
||||
*/
|
||||
private validateAdjustmentTransactionsNotPublished(
|
||||
oldInventoryAdjustment: IInventoryAdjustment
|
||||
) {
|
||||
if (oldInventoryAdjustment.isPublished) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IInventoryAdjustment } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export default class InventoryAdjustmentTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedType'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted and localized adjustment type.
|
||||
* @param {IInventoryAdjustment} inventoryAdjustment
|
||||
* @returns {string}
|
||||
*/
|
||||
formattedType(inventoryAdjustment: IInventoryAdjustment) {
|
||||
const types = {
|
||||
increment: 'inventory_adjustment.type.increment',
|
||||
decrement: 'inventory_adjustment.type.decrement',
|
||||
};
|
||||
return this.context.i18n.__(types[inventoryAdjustment.type] || '');
|
||||
}
|
||||
}
|
||||
257
packages/server/src/services/Inventory/InventoryAverageCost.ts
Normal file
257
packages/server/src/services/Inventory/InventoryAverageCost.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { pick } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { IInventoryTransaction } from '@/interfaces';
|
||||
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||
|
||||
export default class InventoryAverageCostMethod
|
||||
extends InventoryCostMethod
|
||||
implements IInventoryCostMethod
|
||||
{
|
||||
startingDate: Date;
|
||||
itemId: number;
|
||||
costTransactions: any[];
|
||||
trx: Knex.Transaction;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
* @param {Date} startingDate -
|
||||
* @param {number} itemId - The given inventory item id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
startingDate: Date,
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
super(tenantId, startingDate, itemId);
|
||||
|
||||
this.trx = trx;
|
||||
this.startingDate = startingDate;
|
||||
this.itemId = itemId;
|
||||
this.costTransactions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes items costs from the given date using average cost method.
|
||||
* ----------
|
||||
* - Calculate the items average cost in the given date.
|
||||
* - Remove the journal entries that associated to the inventory transacions
|
||||
* after the given date.
|
||||
* - Re-compute the inventory transactions and re-write the journal entries
|
||||
* after the given date.
|
||||
* ----------
|
||||
* @async
|
||||
* @param {Date} startingDate
|
||||
* @param {number} referenceId
|
||||
* @param {string} referenceType
|
||||
*/
|
||||
public async computeItemCost() {
|
||||
const { InventoryTransaction } = this.tenantModels;
|
||||
const { averageCost, openingQuantity, openingCost } =
|
||||
await this.getOpeningAvaregeCost(this.startingDate, this.itemId);
|
||||
|
||||
const afterInvTransactions: IInventoryTransaction[] =
|
||||
await InventoryTransaction.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||
.orderBy('createdAt', 'ASC')
|
||||
.where('item_id', this.itemId)
|
||||
.withGraphFetched('item');
|
||||
|
||||
// Tracking inventroy transactions and retrieve cost transactions based on
|
||||
// average rate cost method.
|
||||
const costTransactions = this.trackingCostTransactions(
|
||||
afterInvTransactions,
|
||||
openingQuantity,
|
||||
openingCost
|
||||
);
|
||||
// Revert the inveout out lots transactions
|
||||
await this.revertTheInventoryOutLotTrans();
|
||||
|
||||
// Store inventory lots cost transactions.
|
||||
await this.storeInventoryLotsCost(costTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items Avarege cost from specific date from inventory transactions.
|
||||
* @async
|
||||
* @param {Date} closingDate
|
||||
* @return {number}
|
||||
*/
|
||||
public async getOpeningAvaregeCost(closingDate: Date, itemId: number) {
|
||||
const { InventoryCostLotTracker } = this.tenantModels;
|
||||
|
||||
const commonBuilder = (builder: any) => {
|
||||
if (closingDate) {
|
||||
builder.where('date', '<', closingDate);
|
||||
}
|
||||
builder.where('item_id', itemId);
|
||||
builder.sum('rate as rate');
|
||||
builder.sum('quantity as quantity');
|
||||
builder.sum('cost as cost');
|
||||
builder.first();
|
||||
};
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
const inInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'IN');
|
||||
|
||||
// Calculates the total inventory total quantity and rate `OUT` transactions.
|
||||
const outInvSumationOper: Promise<any> = InventoryCostLotTracker.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
const [inInvSumation, outInvSumation] = await Promise.all([
|
||||
inInvSumationOper,
|
||||
outInvSumationOper,
|
||||
]);
|
||||
return this.computeItemAverageCost(
|
||||
inInvSumation?.cost || 0,
|
||||
inInvSumation?.quantity || 0,
|
||||
outInvSumation?.cost || 0,
|
||||
outInvSumation?.quantity || 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the item average cost.
|
||||
* @static
|
||||
* @param {number} quantityIn
|
||||
* @param {number} rateIn
|
||||
* @param {number} quantityOut
|
||||
* @param {number} rateOut
|
||||
*/
|
||||
public computeItemAverageCost(
|
||||
totalCostIn: number,
|
||||
totalQuantityIn: number,
|
||||
|
||||
totalCostOut: number,
|
||||
totalQuantityOut: number
|
||||
) {
|
||||
const openingCost = totalCostIn - totalCostOut;
|
||||
const openingQuantity = totalQuantityIn - totalQuantityOut;
|
||||
|
||||
const averageCost = openingQuantity ? openingCost / openingQuantity : 0;
|
||||
|
||||
return { averageCost, openingCost, openingQuantity };
|
||||
}
|
||||
|
||||
private getCost(rate: number, quantity: number) {
|
||||
return quantity ? rate * quantity : rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the journal entries from specific item inventory transactions.
|
||||
* @param {IInventoryTransaction[]} invTransactions
|
||||
* @param {number} openingAverageCost
|
||||
* @param {string} referenceType
|
||||
* @param {number} referenceId
|
||||
* @param {JournalCommand} journalCommands
|
||||
*/
|
||||
public trackingCostTransactions(
|
||||
invTransactions: IInventoryTransaction[],
|
||||
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: IInventoryTransaction) => {
|
||||
const commonEntry = {
|
||||
invTransId: invTransaction.id,
|
||||
...pick(invTransaction, [
|
||||
'date',
|
||||
'direction',
|
||||
'itemId',
|
||||
'quantity',
|
||||
'rate',
|
||||
'entryId',
|
||||
'transactionId',
|
||||
'transactionType',
|
||||
'createdAt',
|
||||
'costAccountId',
|
||||
'branchId',
|
||||
'warehouseId',
|
||||
]),
|
||||
inventoryTransactionId: invTransaction.id,
|
||||
};
|
||||
switch (invTransaction.direction) {
|
||||
case 'IN':
|
||||
const inCost = this.getCost(
|
||||
invTransaction.rate,
|
||||
invTransaction.quantity
|
||||
);
|
||||
// Increases the quantity and cost in `IN` inventory transactions.
|
||||
accQuantity += invTransaction.quantity;
|
||||
accCost += inCost;
|
||||
|
||||
costTransactions.push({
|
||||
...commonEntry,
|
||||
cost: inCost,
|
||||
});
|
||||
break;
|
||||
case 'OUT':
|
||||
// Average cost = Total cost / Total quantity
|
||||
const averageCost = accQuantity ? accCost / accQuantity : 0;
|
||||
|
||||
const quantity =
|
||||
accQuantity > 0
|
||||
? Math.min(invTransaction.quantity, accQuantity)
|
||||
: invTransaction.quantity;
|
||||
|
||||
// Cost = the transaction quantity * Average cost.
|
||||
const cost = this.getCost(averageCost, quantity);
|
||||
|
||||
// Revenue = transaction quanity * rate.
|
||||
// const revenue = quantity * invTransaction.rate;
|
||||
costTransactions.push({
|
||||
...commonEntry,
|
||||
quantity,
|
||||
cost,
|
||||
});
|
||||
accQuantity = Math.max(accQuantity - quantity, 0);
|
||||
accCost = Math.max(accCost - cost, 0);
|
||||
|
||||
if (invTransaction.quantity > quantity) {
|
||||
const remainingQuantity = Math.max(
|
||||
invTransaction.quantity - quantity,
|
||||
0
|
||||
);
|
||||
const remainingIncome = remainingQuantity * invTransaction.rate;
|
||||
|
||||
costTransactions.push({
|
||||
...commonEntry,
|
||||
quantity: remainingQuantity,
|
||||
cost: 0,
|
||||
});
|
||||
accQuantity = Math.max(accQuantity - remainingQuantity, 0);
|
||||
accCost = Math.max(accCost - remainingIncome, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
return costTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the inventory lots `OUT` transactions.
|
||||
* @param {Date} openingDate - Opening date.
|
||||
* @param {number} itemId - Item id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async revertTheInventoryOutLotTrans(): Promise<void> {
|
||||
const { InventoryCostLotTracker } = this.tenantModels;
|
||||
|
||||
await InventoryCostLotTracker.query(this.trx)
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderBy('date', 'DESC')
|
||||
.where('item_id', this.itemId)
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IInventoryItemCostMeta } from '@/interfaces';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { InventoryItemCostService } from './InventoryCostsService';
|
||||
|
||||
@Service()
|
||||
export class InventoryCostApplication {
|
||||
@Inject()
|
||||
inventoryCost: InventoryItemCostService;
|
||||
|
||||
/**
|
||||
* Retrieves the items inventory valuation list.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} itemsId
|
||||
* @param {Date} date
|
||||
* @returns {Promise<IInventoryItemCostMeta[]>}
|
||||
*/
|
||||
public getItemsInventoryValuationList = async (
|
||||
tenantId: number,
|
||||
itemsId: number[],
|
||||
date: Date
|
||||
): Promise<IInventoryItemCostMeta[]> => {
|
||||
const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
|
||||
tenantId,
|
||||
itemsId,
|
||||
date
|
||||
);
|
||||
return [...itemsMap.values()];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
|
||||
@Service()
|
||||
export class InventoryCostGLStorage {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Reverts the inventory cost GL entries from the given starting date.
|
||||
* @param {number} tenantId
|
||||
* @param {Date} startingDate
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertInventoryCostGLEntries = async (
|
||||
tenantId: number,
|
||||
startingDate: Date,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve transactions from specific date range and costable transactions only.
|
||||
const transactions = await AccountTransaction.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(tenantId, reversedLedger, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { pick, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IInventoryLotCost, IInventoryTransaction } from "interfaces";
|
||||
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO';
|
||||
|
||||
export default class InventoryCostLotTracker extends InventoryCostMethod implements IInventoryCostMethod {
|
||||
startingDate: Date;
|
||||
itemId: number;
|
||||
costMethod: TCostMethod;
|
||||
itemsById: Map<number, any>;
|
||||
inventoryINTrans: any;
|
||||
inventoryByItem: any;
|
||||
costLotsTransactions: IInventoryLotCost[];
|
||||
inTransactions: any[];
|
||||
outTransactions: IInventoryTransaction[];
|
||||
revertJEntriesTransactions: IInventoryTransaction[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Date} startingDate -
|
||||
* @param {number} itemId -
|
||||
* @param {string} costMethod -
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
startingDate: Date,
|
||||
itemId: number,
|
||||
costMethod: TCostMethod = 'FIFO'
|
||||
) {
|
||||
super(tenantId, startingDate, itemId);
|
||||
|
||||
this.startingDate = startingDate;
|
||||
this.itemId = itemId;
|
||||
this.costMethod = costMethod;
|
||||
|
||||
// Collect cost lots transactions to insert them to the storage in bulk.
|
||||
this.costLotsTransactions= [];
|
||||
// Collect inventory transactions by item id.
|
||||
this.inventoryByItem = {};
|
||||
// Collection `IN` inventory tranaction by transaction id.
|
||||
this.inventoryINTrans = {};
|
||||
// Collects `IN` transactions.
|
||||
this.inTransactions = [];
|
||||
// Collects `OUT` transactions.
|
||||
this.outTransactions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes items costs from the given date using FIFO or LIFO cost method.
|
||||
* --------
|
||||
* - Revert the inventory lots after the given date.
|
||||
* - Remove all the journal entries from the inventory transactions
|
||||
* after the given date.
|
||||
* - Re-tracking the inventory lots from inventory transactions.
|
||||
* - Re-write the journal entries from the given inventory transactions.
|
||||
* @async
|
||||
* @return {void}
|
||||
*/
|
||||
public async computeItemCost(): Promise<any> {
|
||||
await this.revertInventoryLots(this.startingDate);
|
||||
await this.fetchInvINTransactions();
|
||||
await this.fetchInvOUTTransactions();
|
||||
await this.fetchRevertInvJReferenceIds();
|
||||
await this.fetchItemsMapped();
|
||||
|
||||
this.trackingInventoryINLots(this.inTransactions);
|
||||
this.trackingInventoryOUTLots(this.outTransactions);
|
||||
|
||||
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
||||
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(
|
||||
this.costLotsTransactions,
|
||||
);
|
||||
return Promise.all([
|
||||
storedTrackedInvLotsOper,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetched inventory transactions that has date from the starting date and
|
||||
* fetches availiable IN LOTs transactions that has remaining bigger than zero.
|
||||
* @private
|
||||
*/
|
||||
private async fetchInvINTransactions() {
|
||||
const { InventoryTransaction, InventoryLotCostTracker } = this.tenantModels;
|
||||
|
||||
const commonBuilder = (builder: any) => {
|
||||
builder.orderBy('date', (this.costMethod === 'LIFO') ? 'DESC': 'ASC');
|
||||
builder.where('item_id', this.itemId);
|
||||
};
|
||||
const afterInvTransactions: IInventoryTransaction[] =
|
||||
await InventoryTransaction.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
|
||||
.onBuild(commonBuilder)
|
||||
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
||||
.withGraphFetched('item');
|
||||
|
||||
const availiableINLots: IInventoryLotCost[] =
|
||||
await InventoryLotCostTracker.query()
|
||||
.modify('filterDateRange', null, this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.where('direction', 'IN')
|
||||
.orderBy('lot_number', 'ASC')
|
||||
.onBuild(commonBuilder)
|
||||
.whereNot('remaining', 0);
|
||||
|
||||
this.inTransactions = [
|
||||
...availiableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })),
|
||||
...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches inventory OUT transactions that has date from the starting date.
|
||||
* @private
|
||||
*/
|
||||
private async fetchInvOUTTransactions() {
|
||||
const { InventoryTransaction } = this.tenantModels;
|
||||
|
||||
const afterOUTTransactions: IInventoryTransaction[] =
|
||||
await InventoryTransaction.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.orderBy('lot_number', 'ASC')
|
||||
.where('item_id', this.itemId)
|
||||
.where('direction', 'OUT')
|
||||
.withGraphFetched('item');
|
||||
|
||||
this.outTransactions = [ ...afterOUTTransactions ];
|
||||
}
|
||||
|
||||
private async fetchItemsMapped() {
|
||||
const itemsIds = chain(this.inTransactions).map((e) => e.itemId).uniq().value();
|
||||
const { Item } = this.tenantModels;
|
||||
const storedItems = await Item.query()
|
||||
.where('type', 'inventory')
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
this.itemsById = new Map(storedItems.map((item: any) => [item.id, item]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the inventory transactions that should revert its journal entries.
|
||||
* @private
|
||||
*/
|
||||
private async fetchRevertInvJReferenceIds() {
|
||||
const { InventoryTransaction } = this.tenantModels;
|
||||
const revertJEntriesTransactions: IInventoryTransaction[] =
|
||||
await InventoryTransaction.query()
|
||||
.select(['transactionId', 'transactionType'])
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.where('direction', 'OUT')
|
||||
.where('item_id', this.itemId);
|
||||
|
||||
this.revertJEntriesTransactions = revertJEntriesTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the inventory lots to the given date by removing the inventory lots
|
||||
* transactions after the given date and increment the remaining that
|
||||
* associate to lot number.
|
||||
* @async
|
||||
* @return {Promise}
|
||||
*/
|
||||
public async revertInventoryLots(startingDate: Date) {
|
||||
const { InventoryLotCostTracker } = this.tenantModels;
|
||||
const asyncOpers: any[] = [];
|
||||
const inventoryLotsTrans = await InventoryLotCostTracker.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.orderBy('date', 'DESC')
|
||||
.where('item_id', this.itemId)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
const deleteInvLotsTrans = InventoryLotCostTracker.query()
|
||||
.modify('filterDateRange', this.startingDate)
|
||||
.where('item_id', this.itemId)
|
||||
.delete();
|
||||
|
||||
inventoryLotsTrans.forEach((inventoryLot: IInventoryLotCost) => {
|
||||
if (!inventoryLot.lotNumber) { return; }
|
||||
|
||||
const incrementOper = InventoryLotCostTracker.query()
|
||||
.where('lot_number', inventoryLot.lotNumber)
|
||||
.where('direction', 'IN')
|
||||
.increment('remaining', inventoryLot.quantity);
|
||||
|
||||
asyncOpers.push(incrementOper);
|
||||
});
|
||||
return Promise.all([deleteInvLotsTrans, ...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking inventory `IN` lots transactions.
|
||||
* @public
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions -
|
||||
* @return {void}
|
||||
*/
|
||||
public trackingInventoryINLots(
|
||||
inventoryTransactions: IInventoryTransaction[],
|
||||
) {
|
||||
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||
const { itemId, id } = transaction;
|
||||
(this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = []));
|
||||
|
||||
const commonLotTransaction: IInventoryLotCost = {
|
||||
...pick(transaction, [
|
||||
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId',
|
||||
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||
]),
|
||||
};
|
||||
this.inventoryByItem[itemId].push(id);
|
||||
this.inventoryINTrans[id] = {
|
||||
...commonLotTransaction,
|
||||
decrement: 0,
|
||||
remaining: commonLotTransaction.remaining || commonLotTransaction.quantity,
|
||||
};
|
||||
this.costLotsTransactions.push(this.inventoryINTrans[id]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking inventory `OUT` lots transactions.
|
||||
* @public
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions -
|
||||
* @return {void}
|
||||
*/
|
||||
public trackingInventoryOUTLots(
|
||||
inventoryTransactions: IInventoryTransaction[],
|
||||
) {
|
||||
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||
const { itemId, id } = transaction;
|
||||
(this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = []));
|
||||
|
||||
const commonLotTransaction: IInventoryLotCost = {
|
||||
...pick(transaction, [
|
||||
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId',
|
||||
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||
]),
|
||||
};
|
||||
let invRemaining = transaction.quantity;
|
||||
const idsShouldDel: number[] = [];
|
||||
|
||||
this.inventoryByItem?.[itemId]?.some((_invTransactionId: number) => {
|
||||
const _invINTransaction = this.inventoryINTrans[_invTransactionId];
|
||||
|
||||
// Can't continue if the IN transaction remaining equals zero.
|
||||
if (invRemaining <= 0) { return true; }
|
||||
|
||||
// Can't continue if the IN transaction date is after the current transaction date.
|
||||
if (moment(_invINTransaction.date).isAfter(transaction.date)) {
|
||||
return true;
|
||||
}
|
||||
// Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction.
|
||||
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
|
||||
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
|
||||
const maxDecrement = Math.min(decrement, invRemaining);
|
||||
const cost = maxDecrement * _invINTransaction.rate;
|
||||
|
||||
_invINTransaction.decrement += maxDecrement;
|
||||
_invINTransaction.remaining = Math.max(
|
||||
_invINTransaction.remaining - maxDecrement,
|
||||
0,
|
||||
);
|
||||
invRemaining = Math.max(invRemaining - maxDecrement, 0);
|
||||
|
||||
this.costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
cost,
|
||||
quantity: maxDecrement,
|
||||
lotNumber: _invINTransaction.lotNumber,
|
||||
});
|
||||
// Pop the 'IN' lots that has zero remaining.
|
||||
if (_invINTransaction.remaining === 0) {
|
||||
idsShouldDel.push(_invTransactionId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (invRemaining > 0) {
|
||||
this.costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
quantity: invRemaining,
|
||||
});
|
||||
}
|
||||
this.removeInventoryItems(itemId, idsShouldDel);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inventory transactions for specific item id.
|
||||
* @private
|
||||
* @param {number} itemId
|
||||
* @param {number[]} idsShouldDel
|
||||
* @return {void}
|
||||
*/
|
||||
private removeInventoryItems(itemId: number, idsShouldDel: number[]) {
|
||||
// Remove the IN transactions that has zero remaining amount.
|
||||
this.inventoryByItem[itemId] = this.inventoryByItem?.[itemId]
|
||||
?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Container } from 'typedi';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { IInventoryLotCost } from '@/interfaces';
|
||||
|
||||
export default class InventoryCostMethod {
|
||||
tenancy: TenancyService;
|
||||
tenantModels: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(tenantId: number, startingDate: Date, itemId: number) {
|
||||
const tenancyService = Container.get(TenancyService);
|
||||
|
||||
this.tenantModels = tenancyService.models(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the inventory lots costs transactions in bulk.
|
||||
* @param {IInventoryLotCost[]} costLotsTransactions
|
||||
* @return {Promise[]}
|
||||
*/
|
||||
public storeInventoryLotsCost(
|
||||
costLotsTransactions: IInventoryLotCost[]
|
||||
): Promise<object> {
|
||||
const { InventoryCostLotTracker } = this.tenantModels;
|
||||
const opers: any = [];
|
||||
|
||||
costLotsTransactions.forEach((transaction: any) => {
|
||||
if (transaction.lotTransId && transaction.decrement) {
|
||||
const decrementOper = InventoryCostLotTracker.query(this.trx)
|
||||
.where('id', transaction.lotTransId)
|
||||
.decrement('remaining', transaction.decrement);
|
||||
opers.push(decrementOper);
|
||||
} else if (!transaction.lotTransId) {
|
||||
const operation = InventoryCostLotTracker.query(this.trx).insert({
|
||||
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
||||
});
|
||||
opers.push(operation);
|
||||
}
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
}
|
||||
147
packages/server/src/services/Inventory/InventoryCostsService.ts
Normal file
147
packages/server/src/services/Inventory/InventoryCostsService.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { keyBy, get } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { IInventoryItemCostMeta } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ItemWarehouseQuantity from 'models/ItemWarehouseQuantity';
|
||||
import { ModelUpdateOptions } from 'mongoose';
|
||||
|
||||
@Service()
|
||||
export class InventoryItemCostService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
* Common query of items inventory valuation.
|
||||
* @param {number[]} itemsIds -
|
||||
* @param {Date} date -
|
||||
* @param {Knex.QueryBuilder} builder -
|
||||
*/
|
||||
private itemsInventoryValuationCommonQuery = R.curry(
|
||||
(itemsIds: number[], date: Date, builder: Knex.QueryBuilder) => {
|
||||
if (date) {
|
||||
builder.where('date', '<', date);
|
||||
}
|
||||
builder.whereIn('item_id', itemsIds);
|
||||
builder.sum('rate as rate');
|
||||
builder.sum('quantity as quantity');
|
||||
builder.sum('cost as cost');
|
||||
|
||||
builder.groupBy('item_id');
|
||||
builder.select(['item_id']);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} INValuationMap -
|
||||
* @param {} OUTValuationMap -
|
||||
* @param {number} itemId
|
||||
*/
|
||||
private getItemInventoryMeta = R.curry(
|
||||
(
|
||||
INValuationMap,
|
||||
OUTValuationMap,
|
||||
itemId: number
|
||||
): IInventoryItemCostMeta => {
|
||||
const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
|
||||
const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
|
||||
|
||||
const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0);
|
||||
const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0);
|
||||
|
||||
const valuation = INCost - OUTCost;
|
||||
const quantity = INQuantity - OUTQuantity;
|
||||
const average = quantity ? valuation / quantity : 0;
|
||||
|
||||
return { itemId, valuation, quantity, average };
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemsId
|
||||
* @param {Date} date
|
||||
* @returns
|
||||
*/
|
||||
private getItemsInventoryINAndOutAggregated = (
|
||||
tenantId: number,
|
||||
itemsId: number[],
|
||||
date: Date
|
||||
): Promise<any> => {
|
||||
const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||
|
||||
const commonBuilder = this.itemsInventoryValuationCommonQuery(
|
||||
itemsId,
|
||||
date
|
||||
);
|
||||
const INValuationOper = InventoryCostLotTracker.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'IN');
|
||||
|
||||
const OUTValuationOper = InventoryCostLotTracker.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
return Promise.all([OUTValuationOper, INValuationOper]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId -
|
||||
* @param {number[]} itemsIds -
|
||||
* @param {Date} date -
|
||||
*/
|
||||
private getItemsInventoryInOutMap = async (
|
||||
tenantId: number,
|
||||
itemsId: number[],
|
||||
date: Date
|
||||
) => {
|
||||
const [OUTValuation, INValuation] =
|
||||
await this.getItemsInventoryINAndOutAggregated(tenantId, 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 (
|
||||
tenantId: number,
|
||||
itemsId: number[],
|
||||
date: Date
|
||||
): Promise<Map<number, IInventoryItemCostMeta>> => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the inventory items.
|
||||
const items = await Item.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(tenantId, itemsId, date);
|
||||
|
||||
const getItemValuation = this.getItemInventoryMeta(
|
||||
INValuationMap,
|
||||
OUTValuationMap
|
||||
);
|
||||
const itemsValuations = inventoryItemsIds.map(getItemValuation);
|
||||
const itemsValuationsMap = new Map(
|
||||
itemsValuations.map((i) => [i.itemId, i])
|
||||
);
|
||||
return itemsValuationsMap;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { toSafeInteger } from 'lodash';
|
||||
import { IInventoryTransaction, IItemsQuantityChanges } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Knex from 'knex';
|
||||
|
||||
/**
|
||||
* Syncs the inventory transactions with inventory items quantity.
|
||||
*/
|
||||
@Service()
|
||||
export default class InventoryItemsQuantitySync {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Reverse the given inventory transactions.
|
||||
* @param {IInventoryTransaction[]} inventroyTransactions
|
||||
* @return {IInventoryTransaction[]}
|
||||
*/
|
||||
reverseInventoryTransactions(
|
||||
inventroyTransactions: IInventoryTransaction[]
|
||||
): IInventoryTransaction[] {
|
||||
return inventroyTransactions.map((transaction) => ({
|
||||
...transaction,
|
||||
direction: transaction.direction === 'OUT' ? 'IN' : 'OUT',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the inventory transactions.
|
||||
* @param {IInventoryTransaction[]} inventroyTransactions -
|
||||
* @return {IItemsQuantityChanges[]}
|
||||
*/
|
||||
getReverseItemsQuantityChanges(
|
||||
inventroyTransactions: IInventoryTransaction[]
|
||||
): 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[]}
|
||||
*/
|
||||
getItemsQuantityChanges(
|
||||
inventroyTransactions: IInventoryTransaction[]
|
||||
): IItemsQuantityChanges[] {
|
||||
const balanceMap: { [itemId: number]: number } = {};
|
||||
|
||||
inventroyTransactions.forEach(
|
||||
(inventoryTransaction: IInventoryTransaction) => {
|
||||
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>}
|
||||
*/
|
||||
async changeItemsQuantity(
|
||||
tenantId: number,
|
||||
itemsQuantity: IItemsQuantityChanges[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const { itemRepository } = this.tenancy.repositories(tenantId);
|
||||
const opers = [];
|
||||
|
||||
itemsQuantity.forEach((itemQuantity: IItemsQuantityChanges) => {
|
||||
const changeQuantityOper = itemRepository.changeNumber(
|
||||
{ id: itemQuantity.itemId, type: 'inventory' },
|
||||
'quantityOnHand',
|
||||
itemQuantity.balanceChange,
|
||||
trx
|
||||
);
|
||||
opers.push(changeQuantityOper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces';
|
||||
import { InventoryCostGLStorage } from '../InventoryCostGLStorage';
|
||||
|
||||
@Service()
|
||||
export class InventoryCostGLBeforeWriteSubscriber {
|
||||
@Inject()
|
||||
private inventoryCostGLStorage: InventoryCostGLStorage;
|
||||
|
||||
/**
|
||||
* Attaches events.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.inventory.onCostLotsGLEntriesBeforeWrite,
|
||||
this.revertsInventoryCostGLEntries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the receipts cost GL entries once the inventory cost lots be written.
|
||||
* @param {IInventoryCostLotsGLEntriesWriteEvent}
|
||||
*/
|
||||
private revertsInventoryCostGLEntries = async ({
|
||||
trx,
|
||||
startingDate,
|
||||
tenantId,
|
||||
}: IInventoryCostLotsGLEntriesWriteEvent) => {
|
||||
await this.inventoryCostGLStorage.revertInventoryCostGLEntries(
|
||||
tenantId,
|
||||
startingDate,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
15
packages/server/src/services/Inventory/utils.ts
Normal file
15
packages/server/src/services/Inventory/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { chain } from 'lodash';
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user