mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: Concurrency control items cost compute.
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
31
server/src/services/Inventory/InventoryCostMethod.ts
Normal file
31
server/src/services/Inventory/InventoryCostMethod.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user