feat: Concurrency control items cost compute.

This commit is contained in:
Ahmed Bouhuolia
2020-08-23 23:38:42 +02:00
parent 45088b2d3b
commit ab6bc0517f
28 changed files with 463 additions and 341 deletions

View File

@@ -13,13 +13,22 @@ 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
* @param {Date} fromDate -
* @param {number} itemId -
*/
static async computeItemCost(fromDate: Date, itemId: number) {
const costMethod: TCostMethod = 'FIFO';
const item = await Item.tenant().query()
.findById(itemId)
.withGraphFetched('category');
// 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.');
}
const costMethod: TCostMethod = item.category.costMethod;
let costMethodComputer: IInventoryCostMethod;
// Switch between methods based on the item cost method.
switch(costMethod) {
case 'FIFO':
case 'LIFO':
@@ -27,10 +36,9 @@ export default class InventoryService {
break;
case 'AVG':
costMethodComputer = new InventoryAverageCost(fromDate, itemId);
break
break;
}
await costMethodComputer.initialize();
await costMethodComputer.computeItemCost()
return costMethodComputer.computeItemCost();
}
/**
@@ -41,10 +49,6 @@ export default class InventoryService {
static async scheduleComputeItemCost(itemId: number, startingDate: Date|string) {
const agenda = Container.get('agenda');
// Delete the scheduled job in case has the same given data.
await agenda.cancel({
name: 'compute-item-cost',
});
return agenda.schedule('in 3 seconds', 'compute-item-cost', {
startingDate, itemId,
});

View File

@@ -1,36 +1,27 @@
import { Account, InventoryTransaction } from '@/models';
import { pick } from 'lodash';
import { InventoryTransaction } from '@/models';
import { IInventoryTransaction } from '@/interfaces';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalCommands from '@/services/Accounting/JournalCommands';
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
export default class InventoryAverageCostMethod implements IInventoryCostMethod {
journal: JournalPoster;
journalCommands: JournalCommands;
fromDate: Date;
export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod {
startingDate: Date;
itemId: number;
costTransactions: any[];
/**
* Constructor method.
* @param {Date} fromDate -
* @param {Date} startingDate -
* @param {number} itemId -
*/
constructor(
fromDate: Date,
startingDate: Date,
itemId: number,
) {
this.fromDate = fromDate;
super();
this.startingDate = startingDate;
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);
this.costTransactions = [];
}
/**
@@ -43,50 +34,41 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod
* after the given date.
* ----------
* @asycn
* @param {Date} fromDate
* @param {Date} startingDate
* @param {number} referenceId
* @param {string} referenceType
*/
public async computeItemCost() {
const openingAvgCost = await this.getOpeningAvaregeCost(this.fromDate, this.itemId);
const openingAvgCost = await this.getOpeningAvaregeCost(this.startingDate, this.itemId);
// @todo from `invTransactions`.
const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction
.tenant()
.query()
.where('date', '>=', this.fromDate)
// .where('direction', 'OUT')
.orderBy('date', 'asc')
.modify('filterDateRange', this.startingDate)
.orderBy('date', 'ASC')
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
.where('item_id', this.itemId)
.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(
// Tracking inventroy transactions and retrieve cost transactions
// based on average rate cost method.
const costTransactions = this.trackingCostTransactions(
afterInvTransactions,
openingAvgCost,
);
// Saves the new recorded journal entries to the storage.
await Promise.all([
this.journal.deleteEntries(),
this.journal.saveEntries(),
this.journal.saveBalance(),
]);
await this.storeInventoryLotsCost(costTransactions);
}
/**
* Get items Avarege cost from specific date from inventory transactions.
* @static
* @param {Date} fromDate
* @param {Date} startingDate
* @return {number}
*/
public async getOpeningAvaregeCost(fromDate: Date, itemId: number) {
public async getOpeningAvaregeCost(startingDate: Date, itemId: number) {
const commonBuilder = (builder: any) => {
if (fromDate) {
builder.where('date', '<', fromDate);
if (startingDate) {
builder.where('date', '<', startingDate);
}
builder.where('item_id', itemId);
builder.groupBy('rate');
@@ -155,53 +137,45 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod
* @param {number} referenceId
* @param {JournalCommand} journalCommands
*/
async jEntriesFromItemInvTransactions(
public trackingCostTransactions(
invTransactions: IInventoryTransaction[],
openingAverageCost: number,
) {
const transactions: any[] = [];
const costTransactions: any[] = [];
let accQuantity: number = 0;
let accCost: number = 0;
invTransactions.forEach((invTransaction: IInventoryTransaction) => {
const commonEntry = {
date: invTransaction.date,
referenceType: invTransaction.transactionType,
referenceId: invTransaction.transactionId,
invTransId: invTransaction.id,
...pick(invTransaction, ['date', 'direction', 'itemId', 'quantity', 'rate', 'entryId',
'transactionId', 'transactionType']),
};
switch(invTransaction.direction) {
case 'IN':
accQuantity += invTransaction.quantity;
accCost += invTransaction.rate * invTransaction.quantity;
const inventory = invTransaction.quantity * invTransaction.rate;
transactions.push({
costTransactions.push({
...commonEntry,
inventory,
inventoryAccount: invTransaction.item.inventoryAccountId,
});
break;
case 'OUT':
const income = invTransaction.quantity * invTransaction.rate;
case 'OUT':
const transactionAvgCost = accCost ? (accCost / accQuantity) : 0;
const averageCost = transactionAvgCost;
const cost = (invTransaction.quantity * averageCost);
const cost = (invTransaction.quantity * averageCost);
const income = (invTransaction.quantity * invTransaction.rate);
accQuantity -= invTransaction.quantity;
accCost -= accCost;
accCost -= income;
transactions.push({
costTransactions.push({
...commonEntry,
income,
cost,
incomeAccount: invTransaction.item.sellAccountId,
costAccount: invTransaction.item.costAccountId,
inventoryAccount: invTransaction.item.inventoryAccountId,
});
break;
}
});
this.journalCommands.inventoryEntries(transactions);
return costTransactions;
}
}

View File

@@ -3,20 +3,15 @@ import moment from 'moment';
import {
InventoryTransaction,
InventoryLotCostTracker,
Account,
Item,
} from "@/models";
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalCommands from '@/services/Accounting/JournalCommands';
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
type TCostMethod = 'FIFO' | 'LIFO';
export default class InventoryCostLotTracker implements IInventoryCostMethod {
journal: JournalPoster;
journalCommands: JournalCommands;
export default class InventoryCostLotTracker extends InventoryCostMethod implements IInventoryCostMethod {
startingDate: Date;
headDate: Date;
itemId: number;
costMethod: TCostMethod;
itemsById: Map<number, any>;
@@ -25,7 +20,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
costLotsTransactions: IInventoryLotCost[];
inTransactions: any[];
outTransactions: IInventoryTransaction[];
revertInvoiceTrans: any[];
revertJEntriesTransactions: IInventoryTransaction[];
/**
@@ -35,6 +29,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
* @param {string} costMethod -
*/
constructor(startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO') {
super();
this.startingDate = startingDate;
this.itemId = itemId;
this.costMethod = costMethod;
@@ -49,18 +45,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
this.inTransactions = [];
// Collects `OUT` transactions.
this.outTransactions = [];
// Collects journal entries reference id and type that should be reverted.
this.revertInvoiceTrans = [];
}
/**
* 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);
}
/**
@@ -80,30 +64,16 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
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,
);
// Remove and revert accounts balance journal entries from inventory transactions.
const revertJEntriesOper = this.revertJournalEntries(this.revertJEntriesTransactions);
// Records the journal entries operation.
this.recordJournalEntries(this.costLotsTransactions);
);
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(),
])),
]);
}
@@ -121,7 +91,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
const afterInvTransactions: IInventoryTransaction[] =
await InventoryTransaction.tenant()
.query()
.modify('filterDateRange', this.startingDate)
.modify('filterDateRange', this.startingDate)
.orderByRaw("FIELD(direction, 'IN', 'OUT')")
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
.onBuild(commonBuilder)
.withGraphFetched('item');
@@ -221,93 +192,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
return Promise.all([deleteInvLotsTrans, ...asyncOpers]);
}
/**
* Reverts the journal entries from inventory lots costs transaction.
* @param {} inventoryLots
*/
async revertJournalEntries(
transactions: IInventoryLotCost[],
) {
return this.journalCommands
.revertEntriesFromInventoryTransactions(transactions);
}
/**
* 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 inventory `IN` lots transactions.
* @public
@@ -352,7 +236,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
const commonLotTransaction: IInventoryLotCost = {
...pick(transaction, [
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId',
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId',
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
]),
};
@@ -373,6 +257,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
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(
@@ -383,6 +268,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
this.costLotsTransactions.push({
...commonLotTransaction,
cost,
quantity: maxDecrement,
lotNumber: _invINTransaction.lotNumber,
});
@@ -392,7 +278,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
}
return false;
});
if (invRemaining > 0) {
if (invRemaining > 0) {
this.costLotsTransactions.push({
...commonLotTransaction,
quantity: invRemaining,

View File

@@ -0,0 +1,31 @@
import { omit } from 'lodash';
import { IInventoryLotCost } from '@/interfaces';
import { InventoryLotCostTracker } from '@/models';
export default class InventoryCostMethod {
/**
* Stores the inventory lots costs transactions in bulk.
* @param {IInventoryLotCost[]} costLotsTransactions
* @return {Promise[]}
*/
public 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);
}
}