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:
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
145
server/src/services/Sales/SalesInvoicesCost.ts
Normal file
145
server/src/services/Sales/SalesInvoicesCost.ts
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user