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

@@ -58,4 +58,24 @@ export default class HasItemEntries {
});
return Promise.all([...opers]);
}
static filterNonInventoryEntries(entries: [], items: []) {
const nonInventoryItems = items.filter((item: any) => item.type !== 'inventory');
const nonInventoryItemsIds = nonInventoryItems.map((i: any) => i.id);
return entries
.filter((entry: any) => (
(nonInventoryItemsIds.indexOf(entry.item_id)) !== -1
));
}
static filterInventoryEntries(entries: [], items: []) {
const inventoryItems = items.filter((item: any) => item.type === 'inventory');
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
return entries
.filter((entry: any) => (
(inventoryItemsIds.indexOf(entry.item_id)) !== -1
));
}
}

View File

@@ -11,35 +11,14 @@ import JournalPoster from '@/services/Accounting/JournalPoster';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import CustomerRepository from '@/repositories/CustomerRepository';
import InventoryService from '@/services/Inventory/Inventory';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import { formatDateFields } from '@/utils';
import { Item } from '../../models';
import JournalCommands from '../Accounting/JournalCommands';
/**
* Sales invoices service
* @service
*/
export default class SaleInvoicesService {
static filterNonInventoryEntries(entries: [], items: []) {
const nonInventoryItems = items.filter((item: any) => item.type !== 'inventory');
const nonInventoryItemsIds = nonInventoryItems.map((i: any) => i.id);
return entries
.filter((entry: any) => (
(nonInventoryItemsIds.indexOf(entry.item_id)) !== -1
));
}
static filterInventoryEntries(entries: [], items: []) {
const inventoryItems = items.filter((item: any) => item.type === 'inventory');
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
return entries
.filter((entry: any) => (
(inventoryItemsIds.indexOf(entry.item_id)) !== -1
));
}
export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -66,81 +45,36 @@ export default class SaleInvoicesService {
saleInvoice.entries.forEach((entry: any) => {
const oper = ItemEntry.tenant()
.query()
.insert({
.insertAndFetch({
reference_type: 'SaleInvoice',
reference_id: storedInvoice.id,
...omit(entry, ['amount', 'id']),
}).then((itemEntry) => {
entry.id = itemEntry.id;
});
opers.push(oper);
});
// Increment the customer balance after deliver the sale invoice.
const incrementOper = Customer.incrementBalance(
saleInvoice.customer_id,
balance,
);
// Records the inventory transactions for inventory items.
const recordInventoryTransOpers = this.recordInventoryTranscactions(
saleInvoice, storedInvoice.id
);
// Records the non-inventory transactions of the entries items.
const recordNonInventoryJEntries = this.recordNonInventoryEntries(
saleInvoice, storedInvoice.id,
);
// Await all async operations.
await Promise.all([
...opers,
incrementOper,
recordNonInventoryJEntries,
recordInventoryTransOpers,
...opers, incrementOper,
]);
// Records the inventory transactions for inventory items.
await this.recordInventoryTranscactions(
saleInvoice, storedInvoice.id
);
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
// await this.scheduleComputeItemsCost(saleInvoice);
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
return storedInvoice;
}
/**
* Records the journal entries for non-inventory entries.
* @param {SaleInvoice} saleInvoice
*/
static async recordNonInventoryEntries(saleInvoice: any, saleInvoiceId: number) {
const saleInvoiceItems = saleInvoice.entries.map((entry: any) => entry.item_id);
// Retrieves items data to detarmines whether the item type.
const itemsMeta = await Item.tenant().query().whereIn('id', saleInvoiceItems);
const storedItemsMap = new Map(itemsMeta.map((item) => [item.id, item]));
// Filters the non-inventory and inventory entries based on the item type.
const nonInventoryEntries: any[] = this.filterNonInventoryEntries(saleInvoice.entries, itemsMeta);
const transactions: any = [];
const common = {
referenceType: 'SaleInvoice',
referenceId: saleInvoiceId,
date: saleInvoice.invoice_date,
};
nonInventoryEntries.forEach((entry) => {
const item = storedItemsMap.get(entry.item_id);
transactions.push({
...common,
income: entry.amount,
incomeAccountId: item.incomeAccountId,
})
});
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
const journalCommands = new JournalCommands(journal);
journalCommands.nonInventoryEntries(transactions);
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
]);
}
/**
* Edit the given sale invoice.
* @async
@@ -193,7 +127,7 @@ export default class SaleInvoicesService {
// Schedule sale invoice re-compute based on the item cost
// method and starting date.
await this.scheduleComputeItemsCost(saleInvoice);
await this.scheduleComputeInvoiceItemsCost(saleInvoice);
}
/**
@@ -260,12 +194,13 @@ export default class SaleInvoicesService {
static recordInventoryTranscactions(saleInvoice, saleInvoiceId: number, override?: boolean){
const inventortyTransactions = saleInvoice.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
...pick(entry, ['item_id', 'quantity', 'rate',]),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
entryId: entry.id,
}));
return InventoryService.recordInventoryTransactions(
@@ -273,27 +208,6 @@ export default class SaleInvoicesService {
);
}
/**
* Schedule sale invoice re-compute based on the item
* cost method and starting date
*
* @private
* @param {SaleInvoice} saleInvoice -
* @return {Promise<Agenda>}
*/
private static scheduleComputeItemsCost(saleInvoice: any) {
const asyncOpers: Promise<[]>[] = [];
saleInvoice.entries.forEach((entry: any) => {
const oper: Promise<[]> = InventoryService.scheduleComputeItemCost(
entry.item_id || entry.itemId,
saleInvoice.bill_date || saleInvoice.billDate,
);
asyncOpers.push(oper);
});
return Promise.all(asyncOpers);
}
/**
* Deletes the inventory transactions.
* @param {string} transactionType
@@ -392,4 +306,17 @@ export default class SaleInvoicesService {
const notStoredInvoices = difference(invoicesIds, storedInvoicesIds);
return notStoredInvoices;
}
/**
* Schedules compute sale invoice items cost based on each item
* cost method.
* @param {ISaleInvoice} saleInvoice
* @return {Promise}
*/
static scheduleComputeInvoiceItemsCost(saleInvoice) {
return this.scheduleComputeItemsCost(
saleInvoice.entries.map((e) => e.item_id),
saleInvoice.invoice_date,
);
}
}

View File

@@ -0,0 +1,145 @@
import { Container } from 'typedi';
import {
SaleInvoice,
Account,
AccountTransaction,
Item,
} from '@/models';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import InventoryService from '@/services/Inventory/Inventory';
import { ISaleInvoice, IItemEntry, IItem } from '@/interfaces';
export default class SaleInvoicesCost {
/**
* Schedule sale invoice re-compute based on the item
* cost method and starting date.
* @param {number[]} itemIds -
* @param {Date} startingDate -
* @return {Promise<Agenda>}
*/
static async scheduleComputeItemsCost(itemIds: number[], startingDate: Date) {
const items: IItem[] = await Item.tenant().query().whereIn('id', itemIds);
const inventoryItems: IItem[] = items.filter((item: IItem) => item.type === 'inventory');
const asyncOpers: Promise<[]>[] = [];
inventoryItems.forEach((item: IItem) => {
const oper: Promise<[]> = InventoryService.scheduleComputeItemCost(
item.id,
startingDate,
);
asyncOpers.push(oper);
});
const writeJEntriesOper: Promise<any> = this.scheduleWriteJournalEntries(startingDate);
return Promise.all([...asyncOpers, writeJEntriesOper]);
}
/**
* Schedule writing journal entries.
* @param {Date} startingDate
* @return {Promise<agenda>}
*/
static scheduleWriteJournalEntries(startingDate?: Date) {
const agenda = Container.get('agenda');
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
startingDate,
});
}
/**
* Writes journal entries from sales invoices.
* @param {Date} startingDate
* @param {boolean} override
*/
static async writeJournalEntries(startingDate: Date, override: boolean) {
const salesInvoices = await SaleInvoice.tenant()
.query()
.onBuild((builder: any) => {
builder.modify('filterDateRange', startingDate);
builder.orderBy('invoice_date', 'ASC');
builder.withGraphFetched('entries.item')
builder.withGraphFetched('costTransactions(groupedEntriesCost)');
});
const accountsDepGraph = await Account.tenant().depGraph().query();
const journal = new JournalPoster(accountsDepGraph);
if (override) {
const oldTransactions = await AccountTransaction.tenant()
.query()
.whereIn('reference_type', ['SaleInvoice'])
.onBuild((builder: any) => {
builder.modify('filterDateRange', startingDate);
})
.withGraphFetched('account.type');
journal.loadEntries(oldTransactions);
journal.removeEntries();
}
const receivableAccount = { id: 10 };
salesInvoices.forEach((saleInvoice: ISaleInvoice) => {
let inventoryTotal: number = 0;
const commonEntry = {
referenceType: 'SaleInvoice',
referenceId: saleInvoice.id,
date: saleInvoice.invoiceDate,
};
const costTransactions: Map<number, number> = new Map(
saleInvoice.costTransactions.map((trans: IItemEntry) => [
trans.entryId, trans.cost,
]),
);
// XXX Debit - Receivable account.
const receivableEntry = new JournalEntry({
...commonEntry,
debit: saleInvoice.balance,
account: receivableAccount.id,
});
journal.debit(receivableEntry);
saleInvoice.entries.forEach((entry: IItemEntry) => {
const cost: number = costTransactions.get(entry.id);
const income: number = entry.quantity * entry.rate;
if (entry.item.type === 'inventory' && cost) {
// XXX Debit - Cost account.
const costEntry = new JournalEntry({
...commonEntry,
debit: cost,
account: entry.item.costAccountId,
note: entry.description,
});
journal.debit(costEntry);
inventoryTotal += cost;
}
// XXX Credit - Income account.
const incomeEntry = new JournalEntry({
...commonEntry,
credit: income,
account: entry.item.sellAccountId,
note: entry.description,
});
journal.credit(incomeEntry);
if (inventoryTotal > 0) {
// XXX Credit - Inventory account.
const inventoryEntry = new JournalEntry({
...commonEntry,
credit: inventoryTotal,
account: entry.item.inventoryAccountId,
});
journal.credit(inventoryEntry);
}
});
});
return Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
]);
}
}