Files
bigcapital/server/src/services/Inventory/InventoryAverageCost.ts
2020-12-22 22:27:54 +02:00

242 lines
7.4 KiB
TypeScript

import { pick } from 'lodash';
import { raw } from 'objection';
import { IInventoryTransaction } from 'interfaces';
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
export default class InventoryAverageCostMethod
extends InventoryCostMethod
implements IInventoryCostMethod {
startingDate: Date;
itemId: number;
costTransactions: any[];
/**
* 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) {
super(tenantId, startingDate, itemId);
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('lot_number', '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 };
}
/**
* 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',
'lotNumber',
]),
};
switch (invTransaction.direction) {
case 'IN':
const inCost = 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 = quantity * averageCost;
// 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()
.modify('filterDateRange', this.startingDate)
.orderBy('date', 'DESC')
.where('item_id', this.itemId)
.delete();
}
}