feat: Re-compute the given items cost job.

feat: Optimize the architecture.
This commit is contained in:
Ahmed Bouhuolia
2020-08-18 02:28:08 +02:00
parent 4e68a7db71
commit d423365a19
44 changed files with 1605 additions and 798 deletions

View File

@@ -0,0 +1,135 @@
import { sumBy, chain } from 'lodash';
import JournalPoster from "./JournalPoster";
import JournalEntry from "./JournalEntry";
import { AccountTransaction } from '@/models';
import { IInventoryTransaction } from '@/interfaces';
import AccountsService from '../Accounts/AccountsService';
import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
interface IInventoryCostEntity {
date: Date,
referenceType: string,
referenceId: number,
costAccount: number,
incomeAccount: number,
inventoryAccount: number,
inventory: number,
cost: number,
income: number,
};
export default class JournalCommands{
journal: JournalPoster;
/**
* Constructor method.
* @param {JournalPoster} journal -
*/
constructor(journal: JournalPoster) {
this.journal = journal;
Object.assign(this, arguments[1]);
}
/**
* Removes and revert accounts balance journal entries that associated
* to the given inventory transactions.
* @param {IInventoryTransaction[]} inventoryTransactions
* @param {Journal} journal
*/
revertEntriesFromInventoryTransactions(inventoryTransactions: IInventoryTransaction[]) {
const groupedInvTransactions = chain(inventoryTransactions)
.groupBy((invTransaction: IInventoryTransaction) => invTransaction.transactionType)
.map((groupedTrans: IInventoryTransaction[], transType: string) => [groupedTrans, transType])
.value();
console.log(groupedInvTransactions);
return Promise.all(
groupedInvTransactions.map(async (grouped: [IInventoryTransaction[], string]) => {
const [invTransGroup, referenceType] = grouped;
const referencesIds = invTransGroup.map((trans: IInventoryTransaction) => trans.transactionId);
const _transactions = await AccountTransaction.tenant()
.query()
.where('reference_type', referenceType)
.whereIn('reference_id', referencesIds)
.withGraphFetched('account.type');
console.log(_transactions, referencesIds);
if (_transactions.length > 0) {
this.journal.loadEntries(_transactions);
this.journal.removeEntries(_transactions.map((t: any) => t.id));
}
})
);
}
/**
*
* @param {string} referenceType -
* @param {number} referenceId -
* @param {ISaleInvoice[]} sales -
*/
public async inventoryEntries(
transactions: IInventoryCostEntity[],
) {
const receivableAccount = { id: 10 };
const payableAccount = { id: 11 };
transactions.forEach((sale: IInventoryCostEntity) => {
const commonEntry = {
date: sale.date,
referenceId: sale.referenceId,
referenceType: sale.referenceType,
};
switch(sale.referenceType) {
case 'Bill':
const inventoryDebit: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.inventory,
account: sale.inventoryAccount,
});
const payableEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.inventory,
account: payableAccount.id,
});
this.journal.debit(inventoryDebit);
this.journal.credit(payableEntry);
break;
case 'SaleInvoice':
const receivableEntry: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.income,
account: receivableAccount.id,
});
const incomeEntry: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.income,
account: sale.incomeAccount,
});
// Cost journal transaction.
const costEntry: JournalEntry = new JournalEntry({
...commonEntry,
debit: sale.cost,
account: sale.costAccount,
});
const inventoryCredit: JournalEntry = new JournalEntry({
...commonEntry,
credit: sale.cost,
account: sale.inventoryAccount,
});
this.journal.debit(receivableEntry);
this.journal.debit(costEntry);
this.journal.credit(incomeEntry);
this.journal.credit(inventoryCredit);
break;
}
});
}
}

View File

@@ -248,6 +248,17 @@ export default class JournalPoster {
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
}
/**
* Revert the given transactions.
* @param {*} entries
*/
removeTransactions(entries) {
this.loadEntries(entries);
this.deletedEntriesIds.push(...entriesIDsShouldDel);
}
/**
* Delete all the stacked entries.
*/

View File

@@ -17,8 +17,6 @@ export default class AccountsService {
.where('account_type_id', accountType.id)
.first();
console.log(account);
return account;
}
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -1,11 +0,0 @@
export default class InventoryCostLotTracker {
recalcInventoryLotsCost(inventoryTransactions) {
}
deleteTransactionsFromDate(fromDate) {
}
}

View 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;
}
}

View File

@@ -3,6 +3,44 @@ import { Item } from '@/models';
export default class ItemsService {
static async newItem(item) {
const storedItem = await Item.tenant()
.query()
.insertAndFetch({
...item,
});
return storedItem;
}
static async editItem(item, itemId) {
const updateItem = await Item.tenant()
.query()
.findById(itemId)
.patch({
...item,
});
return updateItem;
}
static async deleteItem(itemId) {
return Item.tenant()
.query()
.findById(itemId)
.delete();
}
static async getItemWithMetadata(itemId) {
return Item.tenant()
.query()
.findById(itemId)
.withGraphFetched(
'costAccount',
'sellAccount',
'inventoryAccount',
'category'
);
}
/**
* Validates the given items IDs exists or not returns the not found ones.
* @param {Array} itemsIDs

View File

@@ -1,5 +1,6 @@
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { Container } from 'typedi';
import {
Account,
Bill,
@@ -22,7 +23,7 @@ import HasItemsEntries from '@/services/Sales/HasItemsEntries';
export default class BillsService {
/**
* Creates a new bill and stored it to the storage.
*
*|
* Precedures.
* ----
* - Insert bill transactions to the storage.
@@ -30,11 +31,13 @@ export default class BillsService {
* - Increment the given vendor id.
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
*
* ----
* @param {IBill} bill -
* @return {void}
*/
static async createBill(bill) {
const agenda = Container.get('agenda');
const amount = sumBy(bill.entries, 'amount');
const saveEntriesOpers = [];
@@ -57,20 +60,37 @@ export default class BillsService {
// Increments vendor balance.
const incrementOper = Vendor.changeBalance(bill.vendor_id, amount);
// // Rewrite the inventory transactions for inventory items.
// const writeInvTransactionsOper = InventoryService.recordInventoryTransactions(
// bill.entries, 'Bill', billId,
// );
// Rewrite the inventory transactions for inventory items.
const writeInvTransactionsOper = InventoryService.recordInventoryTransactions(
bill.entries, bill.bill_date, 'Bill', storedBill.id, 'IN',
);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({
id: storedBill.id,
...bill
});
await Promise.all([
...saveEntriesOpers,
incrementOper,
// this.recordInventoryTransactions(bill, storedBill.id),
this.recordJournalTransactions({ ...bill, id: storedBill.id }),
// writeInvTransactionsOper,
]);
incrementOper,
writeInvTransactionsOper,
writeJEntriesOper,
]);
// Schedule bill re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(bill);
return storedBill;
}
scheduleComputeItemCost(bill) {
return agenda.schedule('in 1 second', 'compute-item-cost', {
startingDate: bill.bill_date || bill.billDate,
itemId: bill.entries[0].item_id || bill.entries[0].itemId,
costMethod: 'FIFO',
});
}
/**
* Edits details of the given bill id with associated entries.
*
@@ -116,21 +136,31 @@ export default class BillsService {
amount,
oldBill.amount,
);
// // Deletes the old inventory transactions.
// const deleteInvTransactionsOper = InventorySevice.deleteInventoryTransactions(
// billId, 'Bill',
// );
// // Re-write the inventory transactions for inventory items.
// const writeInvTransactionsOper = InventoryService.recordInventoryTransactions(
// bill.entries, 'Bill', billId,
// );
// Re-write the inventory transactions for inventory items.
const writeInvTransactionsOper = InventoryService.recordInventoryTransactions(
bill.entries, bill.bill_date, 'Bill', billId, 'IN'
);
// Delete bill associated inventory transactions.
const deleteInventoryTransOper = InventoryService.deleteInventoryTransactions(
billId, 'Bill'
);
// Writes the journal entries for the given bill transaction.
const writeJEntriesOper = this.recordJournalTransactions({
id: billId,
...bill,
}, billId);
await Promise.all([
patchEntriesOper,
recordTransactionsOper,
changeVendorBalanceOper,
// deleteInvTransactionsOper,
// writeInvTransactionsOper,
writeInvTransactionsOper,
deleteInventoryTransOper,
writeJEntriesOper,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(bill);
}
/**
@@ -149,19 +179,15 @@ export default class BillsService {
.whereIn('id', entriesItemsIds);
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
const payableAccount = await AccountsService.getAccountByType(
'accounts_payable'
);
if (!payableAccount) {
throw new Error('New payable account on the storage.');
}
const payableAccount = await AccountsService.getAccountByType('accounts_payable');
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
const commonJournalMeta = {
debit: 0,
credit: 0,
referenceId: billId,
referenceId: bill.id,
referenceType: 'Bill',
date: formattedDate,
accural: true,
@@ -198,7 +224,7 @@ export default class BillsService {
});
journal.debit(debitEntry);
});
await Promise.all([
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
@@ -211,7 +237,10 @@ export default class BillsService {
* @return {void}
*/
static async deleteBill(billId) {
const bill = await Bill.tenant().query().where('id', billId).first();
const bill = await Bill.tenant().query()
.where('id', billId)
.withGraphFetched('entries')
.first();
// Delete all associated bill entries.
const deleteBillEntriesOper = ItemEntry.tenant()
@@ -242,6 +271,9 @@ export default class BillsService {
deleteInventoryTransOper,
revertVendorBalance,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(bill);
}
/**
@@ -284,7 +316,6 @@ export default class BillsService {
return Bill.tenant().query().where('id', billId).first();
}
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId -

View File

@@ -1,4 +1,5 @@
import { omit, difference, sumBy, mixin } from 'lodash';
import moment from 'moment';
import { SaleEstimate, ItemEntry } from '@/models';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
@@ -11,6 +12,7 @@ export default class SaleEstimateService {
*/
static async createEstimate(estimate: any) {
const amount = sumBy(estimate.entries, 'amount');
const storedEstimate = await SaleEstimate.tenant()
.query()
.insert({

View File

@@ -1,4 +1,5 @@
import { omit, sumBy, difference, chain, sum } from 'lodash';
import { omit, sumBy, difference } from 'lodash';
import { Container } from 'typedi';
import {
SaleInvoice,
AccountTransaction,
@@ -10,7 +11,7 @@ import {
import JournalPoster from '@/services/Accounting/JournalPoster';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import CustomerRepository from '@/repositories/CustomerRepository';
import moment from 'moment';
import InventoryService from '@/services/Inventory/Inventory';
/**
* Sales invoices service
@@ -51,8 +52,8 @@ export default class SaleInvoicesService {
balance,
);
// Records the inventory transactions for inventory items.
const recordInventoryTransOpers = this.recordInventoryTransactions(
saleInvoice, storedInvoice.id
const recordInventoryTransOpers = InventoryService.recordInventoryTransactions(
saleInvoice.entries, saleInvoice.invoice_date, 'SaleInvoice', storedInvoice.id, 'OUT',
);
// Await all async operations.
await Promise.all([
@@ -60,19 +61,13 @@ export default class SaleInvoicesService {
incrementOper,
recordInventoryTransOpers,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(saleInvoice);
return storedInvoice;
}
/**
* Records the inventory items transactions.
* @param {SaleInvoice} saleInvoice -
* @param {number} saleInvoiceId -
* @return {Promise}
*/
static async recordInventoryTransactions(saleInvoice, saleInvoiceId) {
}
/**
* Records the sale invoice journal entries and calculate the items cost
* based on the given cost method in the options FIFO, LIFO or average cost rate.
@@ -84,6 +79,23 @@ export default class SaleInvoicesService {
}
/**
* Schedule sale invoice re-compute based on the item
* cost method and starting date
*
* @param saleInvoice
* @return {Promise<Agenda>}
*/
static scheduleComputeItemsCost(saleInvoice) {
const agenda = Container.get('agenda');
return agenda.schedule('in 1 second', 'compute-item-cost', {
startingDate: saleInvoice.invoice_date || saleInvoice.invoiceDate,
itemId: saleInvoice.entries[0].item_id || saleInvoice.entries[0].itemId,
costMethod: 'FIFO',
});
}
/**
* Edit the given sale invoice.
* @async
@@ -124,12 +136,10 @@ export default class SaleInvoicesService {
patchItemsEntriesOper,
changeCustomerBalanceOper,
]);
}
async recalcInventoryTransactionsCost(inventoryTransactions: array) {
const inventoryTransactionsMap = this.mapInventoryTransByItem(inventoryTransactions);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(saleInvoice);
}
/**
@@ -138,7 +148,7 @@ export default class SaleInvoicesService {
* @param {number} transactionId
*/
static async revertInventoryTransactions(inventoryTransactions: array) {
const opers = [];
const opers: Promise<[]>[] = [];
inventoryTransactions.forEach((trans: any) => {
switch(trans.direction) {
@@ -175,7 +185,9 @@ export default class SaleInvoicesService {
* @param {Number} saleInvoiceId
*/
static async deleteSaleInvoice(saleInvoiceId: number) {
const oldSaleInvoice = await SaleInvoice.tenant().query().findById(saleInvoiceId);
const oldSaleInvoice = await SaleInvoice.tenant().query()
.findById(saleInvoiceId)
.withGraphFetched('entries');
await SaleInvoice.tenant().query().where('id', saleInvoiceId).delete();
await ItemEntry.tenant()
@@ -206,14 +218,20 @@ export default class SaleInvoicesService {
.where('transaction_id', saleInvoiceId);
// Revert inventory transactions.
const revertInventoryTransactionsOper = this.revertInventoryTransactions(inventoryTransactions);
const revertInventoryTransactionsOper = this.revertInventoryTransactions(
inventoryTransactions
);
// Await all async operations.
await Promise.all([
journal.deleteEntries(),
journal.saveBalance(),
revertCustomerBalanceOper,
revertInventoryTransactionsOper,
]);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(oldSaleInvoice)
}
/**
@@ -261,7 +279,7 @@ export default class SaleInvoicesService {
static async isSaleInvoiceNumberExists(saleInvoiceNumber: string|number, saleInvoiceId: number) {
const foundSaleInvoice = await SaleInvoice.tenant()
.query()
.onBuild((query) => {
.onBuild((query: any) => {
query.where('invoice_no', saleInvoiceNumber);
if (saleInvoiceId) {
@@ -269,7 +287,7 @@ export default class SaleInvoicesService {
}
return query;
});
return foundSaleInvoice.length !== 0;
return (foundSaleInvoice.length !== 0);
}
/**
@@ -280,7 +298,7 @@ export default class SaleInvoicesService {
static async isInvoicesExist(invoicesIds: Array<number>) {
const storedInvoices = await SaleInvoice.tenant()
.query()
.onBuild((builder) => {
.onBuild((builder: any) => {
builder.whereIn('id', invoicesIds);
return builder;
});