mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: Re-compute the given items cost job.
feat: Optimize the architecture.
This commit is contained in:
@@ -1,10 +1,36 @@
|
||||
import { InventoryTransaction, Item } from '@/models';
|
||||
import InventoryCostLotTracker from './InventoryCostLotTracker';
|
||||
import { IInventoryTransaction, IInventoryLotCost } from '@/interfaces/InventoryTransaction';
|
||||
import { IInventoryLotCost, IInventoryLotCost } from '../../interfaces/InventoryTransaction';
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
InventoryTransaction,
|
||||
Item
|
||||
} from '@/models';
|
||||
import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost';
|
||||
import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
|
||||
|
||||
export default class InventoryService {
|
||||
/**
|
||||
* 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
|
||||
* @param {number} itemId
|
||||
*/
|
||||
static async computeItemCost(fromDate: Date, itemId: number) {
|
||||
const costMethod: TCostMethod = 'FIFO';
|
||||
let costMethodComputer: IInventoryCostMethod;
|
||||
|
||||
switch(costMethod) {
|
||||
case 'FIFO':
|
||||
case 'LIFO':
|
||||
costMethodComputer = new InventoryCostLotTracker(fromDate, itemId);
|
||||
break;
|
||||
case 'AVG':
|
||||
costMethodComputer = new InventoryAverageCost(fromDate, itemId);
|
||||
break
|
||||
}
|
||||
await costMethodComputer.initialize();
|
||||
await costMethodComputer.computeItemCost()
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the inventory transactions.
|
||||
* @param {Bill} bill
|
||||
@@ -15,6 +41,7 @@ export default class InventoryService {
|
||||
date: Date,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
direction: string,
|
||||
) {
|
||||
const storedOpers: any = [];
|
||||
const entriesItemsIds = entries.map((e: any) => e.item_id);
|
||||
@@ -23,20 +50,19 @@ export default class InventoryService {
|
||||
.whereIn('id', entriesItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
const inventoryItemsIds = inventoryItems.map((i) => i.id);
|
||||
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
|
||||
|
||||
// Filter the bill entries that have inventory items.
|
||||
const inventoryEntries = entries.filter(
|
||||
(entry) => inventoryItemsIds.indexOf(entry.item_id) !== -1
|
||||
(entry: any) => inventoryItemsIds.indexOf(entry.item_id) !== -1
|
||||
);
|
||||
inventoryEntries.forEach((entry: any) => {
|
||||
const oper = InventoryTransaction.tenant().query().insert({
|
||||
date,
|
||||
|
||||
direction,
|
||||
item_id: entry.item_id,
|
||||
quantity: entry.quantity,
|
||||
rate: entry.rate,
|
||||
|
||||
transaction_type: transactionType,
|
||||
transaction_id: transactionId,
|
||||
});
|
||||
@@ -64,86 +90,4 @@ export default class InventoryService {
|
||||
revertInventoryLotsCost(fromDate?: Date) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the journal entries transactions.
|
||||
* @param {IInventoryLotCost[]} inventoryTransactions -
|
||||
*
|
||||
*/
|
||||
static async recordJournalEntries(inventoryLots: IInventoryLotCost[]) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking the given inventory transactions to lots costs transactions.
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
|
||||
* @return {IInventoryLotCost[]}
|
||||
*/
|
||||
static async trackingInventoryLotsCost(inventoryTransactions: IInventoryTransaction[]) {
|
||||
// Collect cost lots transactions to insert them to the storage in bulk.
|
||||
const costLotsTransactions: IInventoryLotCost[] = [];
|
||||
|
||||
// Collect inventory transactions by item id.
|
||||
const inventoryByItem: any = {};
|
||||
// Collection `IN` inventory tranaction by transaction id.
|
||||
const inventoryINTrans: any = {};
|
||||
|
||||
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||
const { itemId, id } = transaction;
|
||||
(inventoryByItem[itemId] || (inventoryByItem[itemId] = []));
|
||||
|
||||
const commonLotTransaction: IInventoryLotCost = {
|
||||
...pick(transaction, [
|
||||
'date', 'rate', 'itemId', 'quantity',
|
||||
'direction', 'transactionType', 'transactionId',
|
||||
]),
|
||||
};
|
||||
// Record inventory `IN` cost lot transaction.
|
||||
if (transaction.direction === 'IN') {
|
||||
inventoryByItem[itemId].push(id);
|
||||
inventoryINTrans[id] = {
|
||||
...commonLotTransaction,
|
||||
remaining: commonLotTransaction.quantity,
|
||||
};
|
||||
costLotsTransactions.push(inventoryINTrans[id]);
|
||||
|
||||
// Record inventory 'OUT' cost lots from 'IN' transactions.
|
||||
} else if (transaction.direction === 'OUT') {
|
||||
let invRemaining = transaction.quantity;
|
||||
|
||||
inventoryByItem?.[itemId]?.forEach((
|
||||
_invTransactionId: number,
|
||||
index: number,
|
||||
) => {
|
||||
const _invINTransaction = inventoryINTrans[_invTransactionId];
|
||||
|
||||
// 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;
|
||||
|
||||
_invINTransaction.remaining = Math.max(
|
||||
_invINTransaction.remaining - decrement, 0,
|
||||
);
|
||||
invRemaining = Math.max(invRemaining - decrement, 0);
|
||||
|
||||
costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
quantity: decrement,
|
||||
});
|
||||
// Pop the 'IN' lots that has zero remaining.
|
||||
if (_invINTransaction.remaining === 0) {
|
||||
inventoryByItem?.[itemId].splice(index, 1);
|
||||
}
|
||||
});
|
||||
if (invRemaining > 0) {
|
||||
costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
quantity: invRemaining,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return costLotsTransactions;
|
||||
}
|
||||
|
||||
}
|
||||
207
server/src/services/Inventory/InventoryAverageCost.ts
Normal file
207
server/src/services/Inventory/InventoryAverageCost.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Account, InventoryTransaction } from '@/models';
|
||||
import { IInventoryTransaction } from '@/interfaces';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalCommands from '@/services/Accounting/JournalCommands';
|
||||
|
||||
export default class InventoryAverageCostMethod implements IInventoryCostMethod {
|
||||
journal: JournalPoster;
|
||||
journalCommands: JournalCommands;
|
||||
fromDate: Date;
|
||||
itemId: number;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Date} fromDate -
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
constructor(
|
||||
fromDate: Date,
|
||||
itemId: number,
|
||||
) {
|
||||
this.fromDate = fromDate;
|
||||
this.itemId = itemId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the inventory average cost method.
|
||||
* @async
|
||||
*/
|
||||
async initialize() {
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
|
||||
this.journal = new JournalPoster(accountsDepGraph);
|
||||
this.journalCommands = new JournalCommands(this.journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ----------
|
||||
* @asycn
|
||||
* @param {Date} fromDate
|
||||
* @param {number} referenceId
|
||||
* @param {string} referenceType
|
||||
*/
|
||||
public async computeItemCost() {
|
||||
const openingAvgCost = await this.getOpeningAvaregeCost(this.fromDate, this.itemId);
|
||||
|
||||
// @todo from `invTransactions`.
|
||||
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction
|
||||
.tenant()
|
||||
.query()
|
||||
.where('date', '>=', this.fromDate)
|
||||
// .where('direction', 'OUT')
|
||||
.orderBy('date', 'asc')
|
||||
.withGraphFetched('item');
|
||||
|
||||
// Remove and revert accounts balance journal entries from
|
||||
// inventory transactions.
|
||||
await this.journalCommands
|
||||
.revertEntriesFromInventoryTransactions(afterInvTransactions);
|
||||
|
||||
// Re-write the journal entries from the new recorded inventory transactions.
|
||||
await this.jEntriesFromItemInvTransactions(
|
||||
afterInvTransactions,
|
||||
openingAvgCost,
|
||||
);
|
||||
// Saves the new recorded journal entries to the storage.
|
||||
await Promise.all([
|
||||
this.journal.deleteEntries(),
|
||||
this.journal.saveEntries(),
|
||||
this.journal.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items Avarege cost from specific date from inventory transactions.
|
||||
* @static
|
||||
* @param {Date} fromDate
|
||||
* @return {number}
|
||||
*/
|
||||
public async getOpeningAvaregeCost(fromDate: Date, itemId: number) {
|
||||
const commonBuilder = (builder: any) => {
|
||||
if (fromDate) {
|
||||
builder.where('date', '<', fromDate);
|
||||
}
|
||||
builder.where('item_id', itemId);
|
||||
builder.groupBy('rate');
|
||||
builder.groupBy('quantity');
|
||||
builder.groupBy('item_id');
|
||||
builder.groupBy('direction');
|
||||
builder.sum('rate as rate');
|
||||
builder.sum('quantity as quantity');
|
||||
};
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
|
||||
// @todo total `IN` transactions.
|
||||
const inInvSumationOper: Promise<any> = InventoryTransaction.tenant()
|
||||
.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'IN')
|
||||
.first();
|
||||
|
||||
// Calculates the total inventory total quantity and rate `OUT` transactions.
|
||||
// @todo total `OUT` transactions.
|
||||
const outInvSumationOper: Promise<any> = InventoryTransaction.tenant()
|
||||
.query()
|
||||
.onBuild(commonBuilder)
|
||||
.where('direction', 'OUT')
|
||||
.first();
|
||||
|
||||
const [inInvSumation, outInvSumation] = await Promise.all([
|
||||
inInvSumationOper,
|
||||
outInvSumationOper,
|
||||
]);
|
||||
return this.computeItemAverageCost(
|
||||
inInvSumation?.quantity || 0,
|
||||
inInvSumation?.rate || 0,
|
||||
outInvSumation?.quantity || 0,
|
||||
outInvSumation?.rate || 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the item average cost.
|
||||
* @static
|
||||
* @param {number} quantityIn
|
||||
* @param {number} rateIn
|
||||
* @param {number} quantityOut
|
||||
* @param {number} rateOut
|
||||
*/
|
||||
public computeItemAverageCost(
|
||||
quantityIn: number,
|
||||
rateIn: number,
|
||||
|
||||
quantityOut: number,
|
||||
rateOut: number,
|
||||
) {
|
||||
const totalQuantity = (quantityIn - quantityOut);
|
||||
const totalRate = (rateIn - rateOut);
|
||||
const averageCost = (totalRate) ? (totalQuantity / totalRate) : totalQuantity;
|
||||
|
||||
return averageCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the journal entries from specific item inventory transactions.
|
||||
* @param {IInventoryTransaction[]} invTransactions
|
||||
* @param {number} openingAverageCost
|
||||
* @param {string} referenceType
|
||||
* @param {number} referenceId
|
||||
* @param {JournalCommand} journalCommands
|
||||
*/
|
||||
async jEntriesFromItemInvTransactions(
|
||||
invTransactions: IInventoryTransaction[],
|
||||
openingAverageCost: number,
|
||||
) {
|
||||
const transactions: any[] = [];
|
||||
let accQuantity: number = 0;
|
||||
let accCost: number = 0;
|
||||
|
||||
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
|
||||
const commonEntry = {
|
||||
date: invTransaction.date,
|
||||
referenceType: invTransaction.transactionType,
|
||||
referenceId: invTransaction.transactionId,
|
||||
};
|
||||
switch(invTransaction.direction) {
|
||||
case 'IN':
|
||||
accQuantity += invTransaction.quantity;
|
||||
accCost += invTransaction.rate * invTransaction.quantity;
|
||||
|
||||
const inventory = invTransaction.quantity * invTransaction.rate;
|
||||
|
||||
transactions.push({
|
||||
...commonEntry,
|
||||
inventory,
|
||||
inventoryAccount: invTransaction.item.inventoryAccountId,
|
||||
});
|
||||
break;
|
||||
case 'OUT':
|
||||
const income = invTransaction.quantity * invTransaction.rate;
|
||||
const transactionAvgCost = accCost ? (accCost / accQuantity) : 0;
|
||||
const averageCost = transactionAvgCost;
|
||||
const cost = (invTransaction.quantity * averageCost);
|
||||
|
||||
accQuantity -= invTransaction.quantity;
|
||||
accCost -= accCost;
|
||||
|
||||
transactions.push({
|
||||
...commonEntry,
|
||||
income,
|
||||
cost,
|
||||
incomeAccount: invTransaction.item.sellAccountId,
|
||||
costAccount: invTransaction.item.costAccountId,
|
||||
inventoryAccount: invTransaction.item.inventoryAccountId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.journalCommands.inventoryEntries(transactions);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
export default class InventoryCostLotTracker {
|
||||
|
||||
recalcInventoryLotsCost(inventoryTransactions) {
|
||||
|
||||
}
|
||||
|
||||
deleteTransactionsFromDate(fromDate) {
|
||||
|
||||
}
|
||||
}
|
||||
318
server/src/services/Inventory/InventoryCostLotTracker.ts
Normal file
318
server/src/services/Inventory/InventoryCostLotTracker.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { omit, pick, chain } from 'lodash';
|
||||
import uniqid from 'uniqid';
|
||||
import {
|
||||
InventoryTransaction,
|
||||
InventoryLotCostTracker,
|
||||
Account,
|
||||
Item,
|
||||
} from "@/models";
|
||||
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalCommands from '@/services/Accounting/JournalCommands';
|
||||
|
||||
type TCostMethod = 'FIFO' | 'LIFO';
|
||||
|
||||
export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
||||
journal: JournalPoster;
|
||||
journalCommands: JournalCommands;
|
||||
startingDate: Date;
|
||||
headDate: Date;
|
||||
itemId: number;
|
||||
costMethod: TCostMethod;
|
||||
itemsById: Map<number, any>;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Date} startingDate -
|
||||
* @param {number} itemId -
|
||||
* @param {string} costMethod -
|
||||
*/
|
||||
constructor(startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO') {
|
||||
this.startingDate = startingDate;
|
||||
this.itemId = itemId;
|
||||
this.costMethod = costMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the inventory average cost method.
|
||||
* @async
|
||||
*/
|
||||
public async initialize() {
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
this.journal = new JournalPoster(accountsDepGraph);
|
||||
this.journalCommands = new JournalCommands(this.journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
const afterInvTransactions: IInventoryTransaction[] =
|
||||
await InventoryTransaction.tenant()
|
||||
.query()
|
||||
.where('date', '>=', this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.where('item_id', this.itemId)
|
||||
.withGraphFetched('item');
|
||||
|
||||
const availiableINLots: IInventoryLotCost[] =
|
||||
await InventoryLotCostTracker.tenant()
|
||||
.query()
|
||||
.where('date', '<', this.startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.where('item_id', this.itemId)
|
||||
.where('direction', 'IN')
|
||||
.whereNot('remaining', 0);
|
||||
|
||||
const merged = [
|
||||
...availiableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })),
|
||||
...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })),
|
||||
];
|
||||
const itemsIds = chain(merged).map(e => e.itemId).uniq().value();
|
||||
|
||||
const storedItems = await Item.tenant()
|
||||
.query()
|
||||
.where('type', 'inventory')
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
this.itemsById = new Map(storedItems.map((item: any) => [item.id, item]));
|
||||
|
||||
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
||||
const trackedInvLotsCosts = this.trackingInventoryLotsCost(merged);
|
||||
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(trackedInvLotsCosts);
|
||||
|
||||
// Remove and revert accounts balance journal entries from inventory transactions.
|
||||
const revertJEntriesOper = this.revertJournalEntries(afterInvTransactions);
|
||||
|
||||
// Records the journal entries operation.
|
||||
this.recordJournalEntries(trackedInvLotsCosts);
|
||||
|
||||
return Promise.all([
|
||||
storedTrackedInvLotsOper,
|
||||
revertJEntriesOper.then(() =>
|
||||
Promise.all([
|
||||
// Saves the new recorded journal entries to the storage.
|
||||
this.journal.deleteEntries(),
|
||||
this.journal.saveEntries(),
|
||||
this.journal.saveBalance(),
|
||||
])),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 asyncOpers: any[] = [];
|
||||
const inventoryLotsTrans = await InventoryLotCostTracker.tenant()
|
||||
.query()
|
||||
.orderBy('date', 'DESC')
|
||||
.where('item_id', this.itemId)
|
||||
.where('date', '>=', startingDate)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
const deleteInvLotsTrans = InventoryLotCostTracker.tenant()
|
||||
.query()
|
||||
.where('date', '>=', startingDate)
|
||||
.where('item_id', this.itemId)
|
||||
.delete();
|
||||
|
||||
inventoryLotsTrans.forEach((inventoryLot: IInventoryLotCost) => {
|
||||
if (!inventoryLot.lotNumber) { return; }
|
||||
|
||||
const incrementOper = InventoryLotCostTracker.tenant()
|
||||
.query()
|
||||
.where('lot_number', inventoryLot.lotNumber)
|
||||
.where('direction', 'IN')
|
||||
.increment('remaining', inventoryLot.quantity);
|
||||
|
||||
asyncOpers.push(incrementOper);
|
||||
});
|
||||
return Promise.all([deleteInvLotsTrans, ...asyncOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the journal entries from inventory lots costs transaction.
|
||||
* @param {} inventoryLots
|
||||
*/
|
||||
async revertJournalEntries(
|
||||
inventoryLots: IInventoryLotCost[],
|
||||
) {
|
||||
const invoiceTransactions = inventoryLots
|
||||
.filter(e => e.transactionType === 'SaleInvoice');
|
||||
|
||||
return this.journalCommands
|
||||
.revertEntriesFromInventoryTransactions(invoiceTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the journal entries transactions.
|
||||
* @async
|
||||
* @param {IInventoryLotCost[]} inventoryTransactions -
|
||||
* @param {string} referenceType -
|
||||
* @param {number} referenceId -
|
||||
* @param {Date} date -
|
||||
* @return {Promise}
|
||||
*/
|
||||
public recordJournalEntries(
|
||||
inventoryLots: IInventoryLotCost[],
|
||||
): void {
|
||||
const outTransactions: any[] = [];
|
||||
const inTransByLotNumber: any = {};
|
||||
const transactions: any = [];
|
||||
|
||||
inventoryLots.forEach((invTransaction: IInventoryLotCost) => {
|
||||
switch(invTransaction.direction) {
|
||||
case 'IN':
|
||||
inTransByLotNumber[invTransaction.lotNumber] = invTransaction;
|
||||
break;
|
||||
case 'OUT':
|
||||
outTransactions.push(invTransaction);
|
||||
break;
|
||||
}
|
||||
});
|
||||
outTransactions.forEach((outTransaction: IInventoryLotCost) => {
|
||||
const { lotNumber, quantity, rate, itemId } = outTransaction;
|
||||
const income = quantity * rate;
|
||||
const item = this.itemsById.get(itemId);
|
||||
|
||||
const transaction = {
|
||||
date: outTransaction.date,
|
||||
referenceType: outTransaction.transactionType,
|
||||
referenceId: outTransaction.transactionId,
|
||||
cost: 0,
|
||||
income,
|
||||
incomeAccount: item.sellAccountId,
|
||||
costAccount: item.costAccountId,
|
||||
inventoryAccount: item.inventoryAccountId,
|
||||
};
|
||||
if (lotNumber && inTransByLotNumber[lotNumber]) {
|
||||
const inInvTrans = inTransByLotNumber[lotNumber];
|
||||
transaction.cost = (outTransaction.quantity * inInvTrans.rate);
|
||||
}
|
||||
transactions.push(transaction);
|
||||
});
|
||||
this.journalCommands.inventoryEntries(transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the inventory lots costs transactions in bulk.
|
||||
* @param {IInventoryLotCost[]} costLotsTransactions
|
||||
* @return {Promise[]}
|
||||
*/
|
||||
storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise<object> {
|
||||
const opers: any = [];
|
||||
|
||||
costLotsTransactions.forEach((transaction: IInventoryLotCost) => {
|
||||
if (transaction.lotTransId && transaction.decrement) {
|
||||
const decrementOper = InventoryLotCostTracker.tenant()
|
||||
.query()
|
||||
.where('id', transaction.lotTransId)
|
||||
.decrement('remaining', transaction.decrement);
|
||||
opers.push(decrementOper);
|
||||
} else if(!transaction.lotTransId) {
|
||||
const operation = InventoryLotCostTracker.tenant().query()
|
||||
.insert({
|
||||
...omit(transaction, ['decrement', 'invTransId', 'lotTransId']),
|
||||
});
|
||||
opers.push(operation);
|
||||
}
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking the given inventory transactions to lots costs transactions.
|
||||
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
|
||||
* @return {IInventoryLotCost[]}
|
||||
*/
|
||||
public trackingInventoryLotsCost(
|
||||
inventoryTransactions: IInventoryTransaction[],
|
||||
) : IInventoryLotCost {
|
||||
// Collect cost lots transactions to insert them to the storage in bulk.
|
||||
const costLotsTransactions: IInventoryLotCost[] = [];
|
||||
// Collect inventory transactions by item id.
|
||||
const inventoryByItem: any = {};
|
||||
// Collection `IN` inventory tranaction by transaction id.
|
||||
const inventoryINTrans: any = {};
|
||||
|
||||
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||
const { itemId, id } = transaction;
|
||||
(inventoryByItem[itemId] || (inventoryByItem[itemId] = []));
|
||||
|
||||
const commonLotTransaction: IInventoryLotCost = {
|
||||
...pick(transaction, [
|
||||
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId',
|
||||
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||
]),
|
||||
};
|
||||
// Record inventory `IN` cost lot transaction.
|
||||
if (transaction.direction === 'IN') {
|
||||
inventoryByItem[itemId].push(id);
|
||||
inventoryINTrans[id] = {
|
||||
...commonLotTransaction,
|
||||
decrement: 0,
|
||||
remaining: commonLotTransaction.remaining || commonLotTransaction.quantity,
|
||||
lotNumber: commonLotTransaction.lotNumber || uniqid.time(),
|
||||
};
|
||||
costLotsTransactions.push(inventoryINTrans[id]);
|
||||
|
||||
// Record inventory 'OUT' cost lots from 'IN' transactions.
|
||||
} else if (transaction.direction === 'OUT') {
|
||||
let invRemaining = transaction.quantity;
|
||||
|
||||
inventoryByItem?.[itemId]?.some((
|
||||
_invTransactionId: number,
|
||||
index: number,
|
||||
) => {
|
||||
const _invINTransaction = inventoryINTrans[_invTransactionId];
|
||||
if (invRemaining <= 0) { 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;
|
||||
|
||||
_invINTransaction.decrement += decrement;
|
||||
_invINTransaction.remaining = Math.max(
|
||||
_invINTransaction.remaining - decrement,
|
||||
0,
|
||||
);
|
||||
invRemaining = Math.max(invRemaining - decrement, 0);
|
||||
|
||||
costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
quantity: decrement,
|
||||
lotNumber: _invINTransaction.lotNumber,
|
||||
});
|
||||
// Pop the 'IN' lots that has zero remaining.
|
||||
if (_invINTransaction.remaining === 0) {
|
||||
inventoryByItem?.[itemId].splice(index, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (invRemaining > 0) {
|
||||
costLotsTransactions.push({
|
||||
...commonLotTransaction,
|
||||
quantity: invRemaining,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return costLotsTransactions;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user