From 061b50c6719203d6a6b5b593fa49ded3a46dd400 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Tue, 22 Dec 2020 22:27:54 +0200 Subject: [PATCH] feat: average rate cost method. --- ...200715193633_create_sale_invoices_table.js | 1 + ...251_create_inventory_transactions_table.js | 2 +- ...create_inventory_cost_lot_tracker_table.js | 2 +- server/src/interfaces/InventoryTransaction.ts | 3 + server/src/jobs/ComputeItemCost.ts | 58 ++--- server/src/jobs/writeInvoicesJEntries.ts | 14 +- server/src/loaders/events.ts | 3 +- server/src/loaders/jobs.ts | 13 +- .../services/Accounting/JournalCommands.ts | 118 ++++++++++ server/src/services/Inventory/Inventory.ts | 138 +++++++---- .../Inventory/InventoryAverageCost.ts | 213 +++++++++++------ .../Inventory/InventoryCostLotTracker.ts | 4 +- .../services/Inventory/InventoryCostMethod.ts | 17 +- .../src/services/Items/ItemsEntriesService.ts | 36 ++- server/src/services/Purchases/Bills.ts | 75 +++--- server/src/services/Sales/SalesInvoices.ts | 217 ++++++++---------- .../src/services/Sales/SalesInvoicesCost.ts | 114 ++++----- server/src/subscribers/bills.ts | 21 +- server/src/subscribers/events.ts | 12 + server/src/subscribers/inventory.ts | 34 +++ server/src/subscribers/saleInvoices.ts | 101 +++++--- 21 files changed, 787 insertions(+), 409 deletions(-) create mode 100644 server/src/subscribers/inventory.ts diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js index 8544cc0d2..4629c4a3c 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -17,6 +17,7 @@ exports.up = function(knex) { table.string('inv_lot_number').index(); table.date('delivered_at').index(); + table.integer('user_id').unsigned(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js index a62b4d905..371a57ca6 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -10,7 +10,7 @@ exports.up = function(knex) { table.integer('quantity').unsigned(); table.decimal('rate', 13, 3).unsigned(); - table.integer('lot_number').index(); + table.string('lot_number').index(); table.string('transaction_type').index(); table.integer('transaction_id').unsigned().index(); diff --git a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js index 344bf30e4..102dec214 100644 --- a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js +++ b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js @@ -9,7 +9,7 @@ exports.up = function(knex) { table.integer('quantity').unsigned().index(); table.decimal('rate', 13, 3); table.integer('remaining'); - table.integer('cost'); + table.decimal('cost', 13, 3); table.integer('lot_number').index(); table.string('transaction_type').index(); diff --git a/server/src/interfaces/InventoryTransaction.ts b/server/src/interfaces/InventoryTransaction.ts index 5a896d708..fb7d3c5e3 100644 --- a/server/src/interfaces/InventoryTransaction.ts +++ b/server/src/interfaces/InventoryTransaction.ts @@ -10,6 +10,7 @@ export interface IInventoryTransaction { transactionType: string, transactionId: string, lotNumber: string, + entryId: number }; export interface IInventoryLotCost { @@ -19,7 +20,9 @@ export interface IInventoryLotCost { itemId: number, rate: number, remaining: number, + cost: number, lotNumber: string|number, transactionType: string, transactionId: string, + entryId: number } \ No newline at end of file diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index d025dd859..dc3126df3 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -1,20 +1,30 @@ import { Container } from 'typedi'; -import moment from 'moment'; +import {EventDispatcher} from "event-dispatch"; +// import { +// EventDispatcher, +// } from 'decorators/eventDispatcher'; +import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; -import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; export default class ComputeItemCostJob { - depends: number; agenda: any; - startingDate: Date; + eventDispatcher: EventDispatcher; + /** + * + * @param agenda + */ constructor(agenda) { this.agenda = agenda; - this.depends = 0; - this.startingDate = null; + this.eventDispatcher = new EventDispatcher(); - this.agenda.on('complete:compute-item-cost', this.onJobFinished.bind(this)); + agenda.define( + 'compute-item-cost', + { priority: 'high', concurrency: 1 }, + this.handler.bind(this), + ); this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this)); + this.agenda.on('complete:compute-item-cost', this.onJobCompleted.bind(this)); } /** @@ -23,12 +33,14 @@ export default class ComputeItemCostJob { */ public async handler(job, done: Function): Promise { const Logger = Container.get('logger'); - const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data; + const inventoryService = Container.get(InventoryService); + + const { startingDate, itemId, tenantId } = job.attrs.data; Logger.info(`Compute item cost - started: ${job.attrs.data}`); try { - await InventoryService.computeItemCost(startingDate, itemId, costMethod); + await inventoryService.computeItemCost(tenantId, startingDate, itemId); Logger.info(`Compute item cost - completed: ${job.attrs.data}`); done(); } catch(e) { @@ -42,30 +54,24 @@ export default class ComputeItemCostJob { * @param {Job} job - . */ async onJobStart(job) { - const { startingDate } = job.attrs.data; - this.depends += 1; + const { startingDate, itemId, tenantId } = job.attrs.data; - if (!this.startingDate || moment(this.startingDate).isBefore(startingDate)) { - this.startingDate = startingDate; - } + await this.eventDispatcher.dispatch( + events.inventory.onComputeItemCostJobStarted, + { startingDate, itemId, tenantId } + ); } /** * Handle job complete items cost finished. * @param {Job} job - */ - async onJobFinished() { - const agenda = Container.get('agenda'); - const startingDate = this.startingDate; + async onJobCompleted(job) { + const { startingDate, itemId, tenantId } = job.attrs.data; - this.depends = Math.max(this.depends - 1, 0); - - if (this.depends === 0) { - this.startingDate = null; - - await agenda.now('rewrite-invoices-journal-entries', { - startingDate, - }); - } + await this.eventDispatcher.dispatch( + events.inventory.onComputeItemCostJobCompleted, + { startingDate, itemId, tenantId }, + ); } } diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts index 46d832182..f48725c60 100644 --- a/server/src/jobs/writeInvoicesJEntries.ts +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -3,14 +3,24 @@ import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; export default class WriteInvoicesJournalEntries { + constructor(agenda) { + agenda.define( + 'rewrite-invoices-journal-entries', + { priority: 'normal', concurrency: 1, }, + this.handler.bind(this), + ); + } + public async handler(job, done: Function): Promise { const Logger = Container.get('logger'); - const { startingDate } = job.attrs.data; + const { startingDate, tenantId } = job.attrs.data; + + const salesInvoicesCost = Container.get(SalesInvoicesCost); Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`); try { - await SalesInvoicesCost.writeJournalEntries(startingDate, true); + await salesInvoicesCost.writeJournalEntries(tenantId, startingDate, true); Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`); done(); } catch(e) { diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index d10e9e4ca..6a59554d6 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -12,4 +12,5 @@ import 'subscribers/vendors'; import 'subscribers/paymentMades'; import 'subscribers/paymentReceives'; import 'subscribers/saleEstimates'; -import 'subscribers/saleReceipts'; \ No newline at end of file +import 'subscribers/saleReceipts'; +import 'subscribers/inventory'; \ No newline at end of file diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index 5596c4eb6..6a2df738e 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -19,17 +19,8 @@ export default ({ agenda }: { agenda: Agenda }) => { new UserInviteMailJob(agenda); new SendLicenseViaEmailJob(agenda); new SendLicenseViaPhoneJob(agenda); - - agenda.define( - 'compute-item-cost', - { priority: 'high', concurrency: 20 }, - new ComputeItemCost(agenda).handler, - ); - agenda.define( - 'rewrite-invoices-journal-entries', - { priority: 'normal', concurrency: 1, }, - new RewriteInvoicesJournalEntries().handler, - ); + new ComputeItemCost(agenda); + new RewriteInvoicesJournalEntries(agenda); agenda.define( 'send-sms-notification-subscribe-end', diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index e1d999bf3..bee128e0a 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -10,6 +10,9 @@ import { IExpense, IExpenseCategory, IItem, + ISaleInvoice, + IInventoryLotCost, + IItemEntry, } from 'interfaces'; interface IInventoryCostEntity { @@ -409,4 +412,119 @@ export default class JournalCommands{ } }); } + + /** + * Writes journal entries for given sale invoice. + * ---------- + * - Receivable accounts -> Debit -> XXXX + * - Income -> Credit -> XXXX + * + * - Cost of goods sold -> Debit -> YYYY + * - Inventory assets -> YYYY + * + * @param {ISaleInvoice} saleInvoice + * @param {JournalPoster} journal + */ + saleInvoice( + saleInvoice: ISaleInvoice & { + costTransactions: IInventoryLotCost[], + entries: IItemEntry & { item: IItem }, + }, + receivableAccountsId: number, + ) { + let inventoryTotal: number = 0; + + const commonEntry = { + referenceType: 'SaleInvoice', + referenceId: saleInvoice.id, + date: saleInvoice.invoiceDate, + }; + const costTransactions: Map = new Map( + saleInvoice.costTransactions.map((trans: IInventoryLotCost) => [ + trans.entryId, trans.cost, + ]), + ); + // XXX Debit - Receivable account. + const receivableEntry = new JournalEntry({ + ...commonEntry, + debit: saleInvoice.balance, + account: receivableAccountsId, + index: 1, + }); + this.journal.debit(receivableEntry); + + saleInvoice.entries.forEach((entry: IItemEntry & { item: IItem }, index) => { + 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, + index: index + 3, + }); + this.journal.debit(costEntry); + inventoryTotal += cost; + } + // XXX Credit - Income account. + const incomeEntry = new JournalEntry({ + ...commonEntry, + credit: income, + account: entry.item.sellAccountId, + note: entry.description, + index: index + 2, + }); + this.journal.credit(incomeEntry); + + if (inventoryTotal > 0) { + // XXX Credit - Inventory account. + const inventoryEntry = new JournalEntry({ + ...commonEntry, + credit: inventoryTotal, + account: entry.item.inventoryAccountId, + index: index + 4, + }); + this.journal.credit(inventoryEntry); + } + }); + } + + saleInvoiceNonInventory( + saleInvoice: ISaleInvoice & { + entries: IItemEntry & { item: IItem }, + }, + receivableAccountsId: number, + ) { + const commonEntry = { + referenceType: 'SaleInvoice', + referenceId: saleInvoice.id, + date: saleInvoice.invoiceDate, + }; + + // XXX Debit - Receivable account. + const receivableEntry = new JournalEntry({ + ...commonEntry, + debit: saleInvoice.balance, + account: receivableAccountsId, + index: 1, + }); + this.journal.debit(receivableEntry); + + saleInvoice.entries.forEach((entry: IItemEntry & { item: IItem }, index: number) => { + const income: number = entry.quantity * entry.rate; + + // XXX Credit - Income account. + const incomeEntry = new JournalEntry({ + ...commonEntry, + credit: income, + account: entry.item.sellAccountId, + note: entry.description, + index: index + 2, + }); + this.journal.credit(incomeEntry); + }); + } } \ No newline at end of file diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index 54f516b94..c9cdab37c 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -1,8 +1,14 @@ import { Container, Service, Inject } from 'typedi'; -import { IInventoryTransaction, IItem } from 'interfaces' +import { pick } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import { IInventoryTransaction, IItem, IItemEntry } from 'interfaces' import InventoryAverageCost from 'services/Inventory/InventoryAverageCost'; import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker'; import TenancyService from 'services/Tenancy/TenancyService'; +import events from 'subscribers/events'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; @@ -11,6 +17,31 @@ export default class InventoryService { @Inject() tenancy: TenancyService; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + /** + * Transforms the items entries to inventory transactions. + */ + transformItemEntriesToInventory( + itemEntries: IItemEntry[], + transactionType: string, + transactionId: number, + direction: 'IN'|'OUT', + date: Date|string, + lotNumber: number, + ) { + return itemEntries.map((entry: IItemEntry) => ({ + ...pick(entry, ['itemId', 'quantity', 'rate']), + lotNumber, + transactionType, + transactionId, + direction, + date, + entryId: entry.id, + })); + } + /** * 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. @@ -21,25 +52,23 @@ export default class InventoryService { async computeItemCost(tenantId: number, fromDate: Date, itemId: number) { const { Item } = this.tenancy.models(tenantId); - const item = await Item.query() - .findById(itemId) - .withGraphFetched('category'); + // Fetches the item with assocaited item category. + const item = await Item.query().findById(itemId); // 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) { + switch('AVG') { case 'FIFO': case 'LIFO': - costMethodComputer = new InventoryCostLotTracker(fromDate, itemId); + costMethodComputer = new InventoryCostLotTracker(tenantId, fromDate, itemId); break; case 'AVG': - costMethodComputer = new InventoryAverageCost(fromDate, itemId); + costMethodComputer = new InventoryAverageCost(tenantId, fromDate, itemId); break; } return costMethodComputer.computeItemCost(); @@ -54,9 +83,36 @@ export default class InventoryService { async scheduleComputeItemCost(tenantId: number, itemId: number, startingDate: Date|string) { const agenda = Container.get('agenda'); - return agenda.schedule('in 3 seconds', 'compute-item-cost', { - startingDate, itemId, tenantId, + // Cancel any `compute-item-cost` in the queue has upper starting date + // with the same given item. + await agenda.cancel({ + name: 'compute-item-cost', + nextRunAt: { $ne: null }, + 'data.tenantId': tenantId, + 'data.itemId': itemId, + 'data.startingDate': { "$gt": startingDate } }); + + // Retrieve any `compute-item-cost` in the queue has lower starting date + // with the same given item. + const dependsJobs = await agenda.jobs({ + name: 'compute-item-cost', + nextRunAt: { $ne: null }, + 'data.tenantId': tenantId, + 'data.itemId': itemId, + 'data.startingDate': { "$lte": startingDate } + }); + if (dependsJobs.length === 0) { + await agenda.schedule('in 30 seconds', 'compute-item-cost', { + startingDate, itemId, tenantId, + }); + + // Triggers `onComputeItemCostJobScheduled` event. + await this.eventDispatcher.dispatch( + events.inventory.onComputeItemCostJobScheduled, + { startingDate, itemId, tenantId }, + ); + } } /** @@ -68,24 +124,11 @@ export default class InventoryService { */ async recordInventoryTransactions( tenantId: number, - entries: IInventoryTransaction[], + inventoryEntries: IInventoryTransaction[], deleteOld: boolean, ): Promise { const { InventoryTransaction, Item } = this.tenancy.models(tenantId); - // Mapping the inventory entries items ids. - const entriesItemsIds = entries.map((e: any) => e.itemId); - const inventoryItems = await Item.query() - .whereIn('id', entriesItemsIds) - .where('type', 'inventory'); - - // Mapping the inventory items ids. - const inventoryItemsIds = inventoryItems.map((i: IItem) => i.id); - - // Filter the bill entries that have inventory items. - const inventoryEntries = entries.filter( - (entry: IInventoryTransaction) => inventoryItemsIds.indexOf(entry.itemId) !== -1 - ); inventoryEntries.forEach(async (entry: any) => { if (deleteOld) { await this.deleteInventoryTransactions( @@ -108,14 +151,14 @@ export default class InventoryService { * @param {number} transactionId * @return {Promise} */ - deleteInventoryTransactions( + async deleteInventoryTransactions( tenantId: number, transactionId: number, transactionType: string, - ) { + ): Promise { const { InventoryTransaction } = this.tenancy.models(tenantId); - return InventoryTransaction.query() + await InventoryTransaction.query() .where('transaction_type', transactionType) .where('transaction_id', transactionId) .delete(); @@ -125,22 +168,37 @@ export default class InventoryService { * Retrieve the lot number after the increment. * @param {number} tenantId - Tenant id. */ - async nextLotNumber(tenantId: number) { - const { Option } = this.tenancy.models(tenantId); + getNextLotNumber(tenantId: number) { + const settings = this.tenancy.settings(tenantId); const LOT_NUMBER_KEY = 'lot_number_increment'; - const effectRows = await Option.query() - .where('key', LOT_NUMBER_KEY) - .increment('value', 1); + const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY }); - if (effectRows === 0) { - await Option.query() - .insert({ - key: LOT_NUMBER_KEY, - value: 1, - }); + return (storedLotNumber && storedLotNumber.value) ? + parseInt(storedLotNumber.value, 10) : 1; + } + + /** + * Increment the next inventory LOT number. + * @param {number} tenantId + * @return {Promise} + */ + async incrementNextLotNumber(tenantId: number) { + const settings = this.tenancy.settings(tenantId); + + const LOT_NUMBER_KEY = 'lot_number_increment'; + const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY }); + + let lotNumber = 1; + + if (storedLotNumber && storedLotNumber.value) { + lotNumber = parseInt(storedLotNumber.value, 10); + lotNumber += 1; } - const options = await Option.query(); - return options.getMeta(LOT_NUMBER_KEY, 1); + settings.set({ key: LOT_NUMBER_KEY }, lotNumber); + + await settings.save(); + + return lotNumber; } } \ No newline at end of file diff --git a/server/src/services/Inventory/InventoryAverageCost.ts b/server/src/services/Inventory/InventoryAverageCost.ts index 8724eec09..9af75983e 100644 --- a/server/src/services/Inventory/InventoryAverageCost.ts +++ b/server/src/services/Inventory/InventoryAverageCost.ts @@ -1,8 +1,11 @@ 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 { +export default class InventoryAverageCostMethod + extends InventoryCostMethod + implements IInventoryCostMethod { startingDate: Date; itemId: number; costTransactions: any[]; @@ -13,13 +16,9 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl * @param {Date} startingDate - * @param {number} itemId - The given inventory item id. */ - constructor( - tenantId: number, - startingDate: Date, - itemId: number, - ) { - super(); - + constructor(tenantId: number, startingDate: Date, itemId: number) { + super(tenantId, startingDate, itemId); + this.startingDate = startingDate; this.itemId = itemId; this.costTransactions = []; @@ -27,154 +26,216 @@ export default class InventoryAverageCostMethod extends InventoryCostMethod impl /** * 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 + * - 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 + * - Re-compute the inventory transactions and re-write the journal entries * after the given date. * ---------- - * @asycn - * @param {Date} startingDate - * @param {number} referenceId - * @param {string} referenceType + * @async + * @param {Date} startingDate + * @param {number} referenceId + * @param {string} referenceType */ public async computeItemCost() { const { InventoryTransaction } = this.tenantModels; - const openingAvgCost = await this.getOpeningAvaregeCost(this.startingDate, this.itemId); + const { + averageCost, + openingQuantity, + openingCost, + } = await this.getOpeningAvaregeCost(this.startingDate, this.itemId); const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction.query() - .modify('filterDateRange', this.startingDate) + .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. + + // Tracking inventroy transactions and retrieve cost transactions based on + // average rate cost method. const costTransactions = this.trackingCostTransactions( afterInvTransactions, - openingAvgCost, + 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. - * @static - * @param {Date} startingDate + * @async + * @param {Date} closingDate * @return {number} */ - public async getOpeningAvaregeCost(startingDate: Date, itemId: number) { - const { InventoryTransaction } = this.tenantModels; + public async getOpeningAvaregeCost(closingDate: Date, itemId: number) { + const { InventoryCostLotTracker } = this.tenantModels; + const commonBuilder = (builder: any) => { - if (startingDate) { - builder.where('date', '<', startingDate); + if (closingDate) { + builder.where('date', '<', closingDate); } 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'); + builder.sum('cost as cost'); + builder.first(); }; // Calculates the total inventory total quantity and rate `IN` transactions. - - // @todo total `IN` transactions. - const inInvSumationOper: Promise = InventoryTransaction.query() + const inInvSumationOper: Promise = InventoryCostLotTracker.query() .onBuild(commonBuilder) - .where('direction', 'IN') - .first(); + .where('direction', 'IN'); // Calculates the total inventory total quantity and rate `OUT` transactions. - // @todo total `OUT` transactions. - const outInvSumationOper: Promise = InventoryTransaction.query() + const outInvSumationOper: Promise = InventoryCostLotTracker.query() .onBuild(commonBuilder) - .where('direction', 'OUT') - .first(); + .where('direction', 'OUT'); const [inInvSumation, outInvSumation] = await Promise.all([ inInvSumationOper, outInvSumationOper, ]); return this.computeItemAverageCost( - inInvSumation?.quantity || 0, - inInvSumation?.rate || 0, - outInvSumation?.quantity || 0, - outInvSumation?.rate || 0 + 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 + * @static + * @param {number} quantityIn + * @param {number} rateIn + * @param {number} quantityOut + * @param {number} rateOut */ public computeItemAverageCost( - quantityIn: number, - rateIn: number, + totalCostIn: number, + totalQuantityIn: number, - quantityOut: number, - rateOut: number, + totalCostOut: number, + totalQuantityOut: number ) { - const totalQuantity = (quantityIn - quantityOut); - const totalRate = (rateIn - rateOut); - const averageCost = (totalRate) ? (totalQuantity / totalRate) : totalQuantity; + const openingCost = totalCostIn - totalCostOut; + const openingQuantity = totalQuantityIn - totalQuantityOut; - return averageCost; + 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 + * @param {IInventoryTransaction[]} invTransactions + * @param {number} openingAverageCost + * @param {string} referenceType + * @param {number} referenceId + * @param {JournalCommand} journalCommands */ public trackingCostTransactions( invTransactions: IInventoryTransaction[], - openingAverageCost: number, + openingQuantity: number = 0, + openingCost: number = 0 ) { const costTransactions: any[] = []; - let accQuantity: number = 0; - let accCost: number = 0; + + // 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']), + ...pick(invTransaction, [ + 'date', + 'direction', + 'itemId', + 'quantity', + 'rate', + 'entryId', + 'transactionId', + 'transactionType', + 'lotNumber', + ]), }; - switch(invTransaction.direction) { + switch (invTransaction.direction) { case 'IN': + const inCost = invTransaction.rate * invTransaction.quantity; + + // Increases the quantity and cost in `IN` inventory transactions. accQuantity += invTransaction.quantity; - accCost += invTransaction.rate * invTransaction.quantity; + accCost += inCost; costTransactions.push({ ...commonEntry, + cost: inCost, }); break; case 'OUT': - const transactionAvgCost = accCost ? (accCost / accQuantity) : 0; - const averageCost = transactionAvgCost; - const cost = (invTransaction.quantity * averageCost); - const income = (invTransaction.quantity * invTransaction.rate); + // Average cost = Total cost / Total quantity + const averageCost = accQuantity ? accCost / accQuantity : 0; - accQuantity -= invTransaction.quantity; - accCost -= income; + 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; } -} \ No newline at end of file + + /** + * Reverts the inventory lots `OUT` transactions. + * @param {Date} openingDate - Opening date. + * @param {number} itemId - Item id. + * @returns {Promise} + */ + async revertTheInventoryOutLotTrans(): Promise { + const { InventoryCostLotTracker } = this.tenantModels; + + await InventoryCostLotTracker.query() + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'DESC') + .where('item_id', this.itemId) + .delete(); + } +} diff --git a/server/src/services/Inventory/InventoryCostLotTracker.ts b/server/src/services/Inventory/InventoryCostLotTracker.ts index 4e7aeee4a..f02384a98 100644 --- a/server/src/services/Inventory/InventoryCostLotTracker.ts +++ b/server/src/services/Inventory/InventoryCostLotTracker.ts @@ -29,7 +29,7 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme itemId: number, costMethod: TCostMethod = 'FIFO' ) { - super(); + super(tenantId, startingDate, itemId); this.startingDate = startingDate; this.itemId = itemId; @@ -129,7 +129,7 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme .withGraphFetched('item'); this.outTransactions = [ ...afterOUTTransactions ]; - } + } private async fetchItemsMapped() { const itemsIds = chain(this.inTransactions).map((e) => e.itemId).uniq().value(); diff --git a/server/src/services/Inventory/InventoryCostMethod.ts b/server/src/services/Inventory/InventoryCostMethod.ts index a24345bf6..12a3a0ecf 100644 --- a/server/src/services/Inventory/InventoryCostMethod.ts +++ b/server/src/services/Inventory/InventoryCostMethod.ts @@ -1,10 +1,9 @@ import { omit } from 'lodash'; -import { Inject } from 'typedi'; +import { Container } from 'typedi'; import TenancyService from 'services/Tenancy/TenancyService'; import { IInventoryLotCost } from 'interfaces'; export default class InventoryCostMethod { - @Inject() tenancy: TenancyService; tenantModels: any; @@ -13,27 +12,29 @@ export default class InventoryCostMethod { * @param {number} tenantId - The given tenant id. */ constructor(tenantId: number, startingDate: Date, itemId: number) { - this.tenantModels = this.tenantModels.models(tenantId); + const tenancyService = Container.get(TenancyService); + + this.tenantModels = tenancyService.models(tenantId); } /** * Stores the inventory lots costs transactions in bulk. - * @param {IInventoryLotCost[]} costLotsTransactions + * @param {IInventoryLotCost[]} costLotsTransactions * @return {Promise[]} */ public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise { - const { InventoryLotCostTracker } = this.tenantModels; + const { InventoryCostLotTracker } = this.tenantModels; const opers: any = []; - costLotsTransactions.forEach((transaction: IInventoryLotCost) => { + costLotsTransactions.forEach((transaction: any) => { if (transaction.lotTransId && transaction.decrement) { - const decrementOper = InventoryLotCostTracker.query() + const decrementOper = InventoryCostLotTracker.query() .where('id', transaction.lotTransId) .decrement('remaining', transaction.decrement); opers.push(decrementOper); } else if(!transaction.lotTransId) { - const operation = InventoryLotCostTracker.query() + const operation = InventoryCostLotTracker.query() .insert({ ...omit(transaction, ['decrement', 'invTransId', 'lotTransId']), }); diff --git a/server/src/services/Items/ItemsEntriesService.ts b/server/src/services/Items/ItemsEntriesService.ts index 6f2a3eee1..201ab2db3 100644 --- a/server/src/services/Items/ItemsEntriesService.ts +++ b/server/src/services/Items/ItemsEntriesService.ts @@ -1,4 +1,4 @@ -import { difference } from 'lodash'; +import { difference, map } from 'lodash'; import { Inject, Service } from 'typedi'; import { IItemEntry, @@ -7,6 +7,7 @@ import { } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; +import { ItemEntry } from 'models'; const ERRORS = { ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', @@ -20,6 +21,39 @@ export default class ItemsEntriesService { @Inject() tenancy: TenancyService; + /** + * Retrieve the inventory items entries of the reference id and type. + * @param {number} tenantId + * @param {string} referenceType + * @param {string} referenceId + * @return {Promise} + */ + public async getInventoryEntries( + tenantId: number, + referenceType: string, + referenceId: number, + ): Promise { + const { Item, ItemEntry } = this.tenancy.models(tenantId); + + const itemsEntries = await ItemEntry.query() + .where('reference_type', referenceType) + .where('reference_id', referenceId); + + // Inventory items. + const inventoryItems = await Item.query() + .whereIn('id', map(itemsEntries, 'itemId')) + .where('type', 'inventory'); + + // Inventory items ids. + const inventoryItemsIds = map(inventoryItems, 'id'); + + // Filtering the inventory items entries. + const inventoryItemsEntries = itemsEntries.filter( + (itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1 + ); + return inventoryItemsEntries; + } + /** * Validates the entries items ids. * @async diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index c165cd9a3..9471db469 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -1,4 +1,4 @@ -import { omit, sumBy, pick, difference, assignWith } from 'lodash'; +import { omit, sumBy, pick, map } from 'lodash'; import moment from 'moment'; import { Inject, Service } from 'typedi'; import { @@ -7,7 +7,6 @@ import { } from 'decorators/eventDispatcher'; import events from 'subscribers/events'; import JournalPoster from 'services/Accounting/JournalPoster'; -import JournalEntry from 'services/Accounting/JournalEntry'; import AccountsService from 'services/Accounts/AccountsService'; import InventoryService from 'services/Inventory/Inventory'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; @@ -24,6 +23,7 @@ import { IFilterMeta, IBillsFilter, IItemEntry, + IInventoryTransaction, } from 'interfaces'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; @@ -159,11 +159,7 @@ export default class BillsService extends SalesInvoicesCost { oldBill?: IBill ) { const { ItemEntry } = this.tenancy.models(tenantId); - let invLotNumber = oldBill?.invLotNumber; - // if (!invLotNumber) { - // invLotNumber = await this.inventoryService.nextLotNumber(tenantId); - // } const entries = billDTO.entries.map((entry) => ({ ...entry, amount: ItemEntry.calcAmount(entry), @@ -176,7 +172,6 @@ export default class BillsService extends SalesInvoicesCost { 'dueDate', ]), amount, - invLotNumber, entries: entries.map((entry) => ({ reference_type: 'Bill', ...omit(entry, ['amount', 'id']), @@ -369,29 +364,51 @@ export default class BillsService extends SalesInvoicesCost { /** * Records the inventory transactions from the given bill input. - * @param {Bill} bill - * @param {number} billId + * @param {Bill} bill - Bill model object. + * @param {number} billId - Bill id. + * @return {Promise} */ public async recordInventoryTransactions( tenantId: number, - bill: IBill, + billId: number, + billDate: Date, override?: boolean ): Promise { - const invTransactions = bill.entries.map((entry: IItemEntry) => ({ - ...pick(entry, ['itemId', 'quantity', 'rate']), - lotNumber: bill.invLotNumber, - transactionType: 'Bill', - transactionId: bill.id, - direction: 'IN', - date: bill.billDate, - entryId: entry.id, - })); + // Retrieve the next inventory lot number. + const lotNumber = this.inventoryService.getNextLotNumber(tenantId); + const inventoryEntries = await this.itemsEntriesService.getInventoryEntries( + tenantId, + 'Bill', + billId + ); + // Can't continue if there is no entries has inventory items in the bill. + if (inventoryEntries.length <= 0) return; + + // Inventory transactions. + const inventoryTranscations = this.inventoryService.transformItemEntriesToInventory( + inventoryEntries, + 'Bill', + billId, + 'IN', + billDate, + lotNumber, + ); + // Records the inventory transactions. await this.inventoryService.recordInventoryTransactions( tenantId, - invTransactions, + inventoryTranscations, override ); + // Save the next lot number settings. + await this.inventoryService.incrementNextLotNumber(tenantId); + + // Triggers `onInventoryTransactionsCreated` event. + this.eventDispatcher.dispatch(events.bill.onInventoryTransactionsCreated, { + tenantId, + billId, + billDate, + }); } /** @@ -404,7 +421,7 @@ export default class BillsService extends SalesInvoicesCost { await this.inventoryService.deleteInventoryTransactions( tenantId, billId, - 'Bill', + 'Bill' ); } @@ -519,22 +536,26 @@ export default class BillsService extends SalesInvoicesCost { * @param {IBill} bill - * @return {Promise} */ - public async scheduleComputeBillItemsCost(tenantId: number, bill) { - const { Item } = this.tenancy.models(tenantId); - const billItemsIds = bill.entries.map((entry) => entry.item_id); + public async scheduleComputeBillItemsCost(tenantId: number, billId: number) { + const { Item, Bill } = this.tenancy.models(tenantId); + + // Retrieve the bill with associated entries. + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries'); // Retrieves inventory items only. const inventoryItems = await Item.query() - .whereIn('id', billItemsIds) + .whereIn('id', map(bill.entries, 'itemId')) .where('type', 'inventory'); - const inventoryItemsIds = inventoryItems.map((i) => i.id); + const inventoryItemsIds = map(inventoryItems, 'id'); if (inventoryItemsIds.length > 0) { await this.scheduleComputeItemsCost( tenantId, inventoryItemsIds, - bill.bill_date + bill.billDate ); } } diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index ce4243787..99e72744b 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,6 +1,7 @@ import { Service, Inject } from 'typedi'; -import { omit, sumBy, pick, chain } from 'lodash'; +import { omit, sumBy, pick, map } from 'lodash'; import moment from 'moment'; +import uniqid from 'uniqid'; import { EventDispatcher, EventDispatcherInterface, @@ -16,7 +17,6 @@ import { IFilterMeta, } from 'interfaces'; import events from 'subscribers/events'; -import JournalPoster from 'services/Accounting/JournalPoster'; import InventoryService from 'services/Inventory/Inventory'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -71,7 +71,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { saleEstimatesService: SaleEstimateService; /** - * * Validate whether sale invoice number unqiue on the storage. */ async validateInvoiceNumberUnique( @@ -166,8 +165,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { ): Promise { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); - const invLotNumber = 1; - // Transform DTO object to model object. const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO); @@ -176,7 +173,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { tenantId, saleInvoiceDTO.customerId ); - // Validate sale invoice number uniquiness. if (saleInvoiceDTO.invoiceNo) { await this.validateInvoiceNumberUnique( @@ -258,13 +254,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost { tenantId, saleInvoiceDTO.entries ); - // Validate non-sellable entries items. await this.itemsEntriesService.validateNonSellableEntriesItems( tenantId, saleInvoiceDTO.entries ); - // Validate the items entries existance. await this.itemsEntriesService.validateEntriesIdsExistance( tenantId, @@ -337,7 +331,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { tenantId, saleInvoiceId ); - // Unlink the converted sale estimates from the given sale invoice. await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice( tenantId, @@ -360,91 +353,113 @@ export default class SaleInvoicesService extends SalesInvoicesCost { } /** - * Records the inventory transactions from the givne sale invoice input. - * @parma {number} tenantId - Tenant id. + * Records the inventory transactions of the given sale invoice in case + * the invoice has inventory entries only. + * + * @param {number} tenantId - Tenant id. * @param {SaleInvoice} saleInvoice - Sale invoice DTO. * @param {number} saleInvoiceId - Sale invoice id. * @param {boolean} override - Allow to override old transactions. + * @return {Promise} */ - public recordInventoryTranscactions( + public async recordInventoryTranscactions( tenantId: number, - saleInvoice: ISaleInvoice, + saleInvoiceId: number, + saleInvoiceDate: Date, override?: boolean - ) { - this.logger.info('[sale_invoice] saving inventory transactions'); - const invTransactions: IInventoryTransaction[] = saleInvoice.entries.map( - (entry: IItemEntry) => ({ - ...pick(entry, ['itemId', 'quantity', 'rate']), - lotNumber: 1, - transactionType: 'SaleInvoice', - transactionId: saleInvoice.id, - direction: 'OUT', - date: saleInvoice.invoiceDate, - entryId: entry.id, - }) - ); + ): Promise { + // Gets the next inventory lot number. + const lotNumber = this.inventoryService.getNextLotNumber(tenantId); - return this.inventoryService.recordInventoryTransactions( + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = await this.itemsEntriesService.getInventoryEntries( tenantId, - invTransactions, + 'SaleInvoice', + saleInvoiceId + ); + // Can't continue if there is no entries has inventory items in the invoice. + if (inventoryEntries.length <= 0) return; + + // Inventory transactions. + const inventoryTranscations = this.inventoryService.transformItemEntriesToInventory( + inventoryEntries, + 'SaleInvoice', + saleInvoiceId, + 'OUT', + saleInvoiceDate, + lotNumber + ); + // Records the inventory transactions of the given sale invoice. + await this.inventoryService.recordInventoryTransactions( + tenantId, + inventoryTranscations, override ); + // Increment and save the next lot number settings. + await this.inventoryService.incrementNextLotNumber(tenantId); + + // Triggers `onInventoryTransactionsCreated` event. + await this.eventDispatcher.dispatch( + events.saleInvoice.onInventoryTransactionsCreated, + { + tenantId, + saleInvoiceId, + } + ); + } + + /** + * Records the journal entries of the given sale invoice just + * in case the invoice has no inventory items entries. + * + * @param {number} tenantId - + * @param {number} saleInvoiceId + * @param {boolean} override + * @return {Promise} + */ + public async recordNonInventoryJournalEntries( + tenantId: number, + saleInvoiceId: number, + override: boolean = false + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = await this.itemsEntriesService.getInventoryEntries( + tenantId, + 'SaleInvoice', + saleInvoiceId + ); + // Can't continue if the sale invoice has inventory items entries. + if (inventoryEntries.length > 0) return; + + const saleInvoice = await SaleInvoice.query() + .findById(saleInvoiceId) + .withGraphFetched('entries.item'); + + await this.writeNonInventoryInvoiceEntries(tenantId, saleInvoice, override); } /** * Reverting the inventory transactions once the invoice deleted. * @param {number} tenantId - Tenant id. * @param {number} billId - Bill id. + * @return {Promise} */ - public revertInventoryTransactions( + public async revertInventoryTransactions( tenantId: number, - billId: number + saleInvoiceId: number ): Promise { - return this.inventoryService.deleteInventoryTransactions( + await this.inventoryService.deleteInventoryTransactions( tenantId, - billId, + saleInvoiceId, 'SaleInvoice' ); - } - - /** - * Deletes the inventory transactions. - * @param {string} transactionType - * @param {number} transactionId - */ - private async revertInventoryTransactions_( - tenantId: number, - inventoryTransactions: array - ) { - const { InventoryTransaction } = this.tenancy.models(tenantId); - const opers: Promise<[]>[] = []; - - this.logger.info('[sale_invoice] reverting inventory transactions'); - - inventoryTransactions.forEach((trans: any) => { - switch (trans.direction) { - case 'OUT': - if (trans.inventoryTransactionId) { - const revertRemaining = InventoryTransaction.query() - .where('id', trans.inventoryTransactionId) - .where('direction', 'OUT') - .increment('remaining', trans.quanitity); - - opers.push(revertRemaining); - } - break; - case 'IN': - const removeRelationOper = InventoryTransaction.query() - .where('inventory_transaction_id', trans.id) - .where('direction', 'IN') - .update({ - inventory_transaction_id: null, - }); - opers.push(removeRelationOper); - break; - } - }); - return Promise.all(opers); + // Triggers 'onInventoryTransactionsDeleted' event. + this.eventDispatcher.dispatch( + events.saleInvoice.onInventoryTransactionsDeleted, + { tenantId, saleInvoiceId }, + ); } /** @@ -480,63 +495,29 @@ export default class SaleInvoicesService extends SalesInvoicesCost { saleInvoiceId: number, override?: boolean ) { - const { SaleInvoice } = this.tenancy.models(tenantId); + const { SaleInvoice, Item } = this.tenancy.models(tenantId); + + // Retrieve the sale invoice with associated entries. const saleInvoice: ISaleInvoice = await SaleInvoice.query() .findById(saleInvoiceId) - .withGraphFetched('entries.item'); + .withGraphFetched('entries'); - const inventoryItemsIds = chain(saleInvoice.entries) - .filter((entry: IItemEntry) => entry.item.type === 'inventory') - .map((entry: IItemEntry) => entry.itemId) - .uniq() - .value(); + // Retrieve the inventory items that associated to the sale invoice entries. + const inventoryItems = await Item.query() + .whereIn('id', map(saleInvoice.entries, 'itemId')) + .where('type', 'inventory'); - if (inventoryItemsIds.length === 0) { - await this.writeNonInventoryInvoiceJournals( - tenantId, - saleInvoice, - override - ); - } else { + const inventoryItemsIds = map(inventoryItems, 'id'); + + if (inventoryItemsIds.length > 0) { await this.scheduleComputeItemsCost( tenantId, inventoryItemsIds, - saleInvoice.invoice_date + saleInvoice.invoiceDate ); } } - /** - * Writes the sale invoice journal entries. - * @param {SaleInvoice} saleInvoice - - */ - async writeNonInventoryInvoiceJournals( - tenantId: number, - saleInvoice: ISaleInvoice, - override: boolean - ) { - const { AccountTransaction } = this.tenancy.models(tenantId); - - const journal = new JournalPoster(tenantId); - - if (override) { - const oldTransactions = await AccountTransaction.query() - .where('reference_type', 'SaleInvoice') - .where('reference_id', saleInvoice.id) - .withGraphFetched('account.type'); - - journal.loadEntries(oldTransactions); - journal.removeEntries(); - } - this.saleInvoiceJournal(saleInvoice, journal); - - await Promise.all([ - journal.deleteEntries(), - journal.saveEntries(), - journal.saveBalance(), - ]); - } - /** * Retrieve sales invoices filterable and paginated list. * @param {Request} req diff --git a/server/src/services/Sales/SalesInvoicesCost.ts b/server/src/services/Sales/SalesInvoicesCost.ts index 73152122a..0ae69464f 100644 --- a/server/src/services/Sales/SalesInvoicesCost.ts +++ b/server/src/services/Sales/SalesInvoicesCost.ts @@ -3,7 +3,8 @@ import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import InventoryService from 'services/Inventory/Inventory'; import TenancyService from 'services/Tenancy/TenancyService'; -import { ISaleInvoice, IItemEntry } from 'interfaces'; +import { ISaleInvoice, IItemEntry, IInventoryLotCost, IItem } from 'interfaces'; +import JournalCommands from 'services/Accounting/JournalCommands'; @Service() export default class SaleInvoicesCost { @@ -45,6 +46,7 @@ export default class SaleInvoicesCost { */ scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) { const agenda = Container.get('agenda'); + return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', { startingDate, tenantId, }); @@ -58,16 +60,23 @@ export default class SaleInvoicesCost { */ async writeJournalEntries(tenantId: number, startingDate: Date, override: boolean) { const { AccountTransaction, SaleInvoice, Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); + + const receivableAccount = await accountRepository.findOne({ + slug: 'accounts-receivable', + }); const salesInvoices = await SaleInvoice.query() .onBuild((builder: any) => { builder.modify('filterDateRange', startingDate); builder.orderBy('invoice_date', 'ASC'); - builder.withGraphFetched('entries.item') + builder.withGraphFetched('entries.item'); builder.withGraphFetched('costTransactions(groupedEntriesCost)'); }); - const accountsDepGraph = await Account.depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); + const accountsDepGraph = await accountRepository.getDependencyGraph(); + const journal = new JournalPoster(tenantId, accountsDepGraph); + + const journalCommands = new JournalCommands(journal); if (override) { const oldTransactions = await AccountTransaction.query() @@ -77,12 +86,14 @@ export default class SaleInvoicesCost { }) .withGraphFetched('account.type'); - journal.loadEntries(oldTransactions); + journal.fromTransactions(oldTransactions); journal.removeEntries(); } - - salesInvoices.forEach((saleInvoice: ISaleInvoice) => { - this.saleInvoiceJournal(saleInvoice, journal); + salesInvoices.forEach((saleInvoice: ISaleInvoice & { + costTransactions: IInventoryLotCost[], + entries: IItemEntry & { item: IItem }, + }) => { + journalCommands.saleInvoice(saleInvoice, receivableAccount.id); }); return Promise.all([ journal.deleteEntries(), @@ -92,64 +103,39 @@ export default class SaleInvoicesCost { } /** - * Writes journal entries for given sale invoice. - * @param {ISaleInvoice} saleInvoice - * @param {JournalPoster} journal + * Writes the sale invoice journal entries. + * @param {SaleInvoice} saleInvoice - */ - saleInvoiceJournal(saleInvoice: ISaleInvoice, journal: JournalPoster) { - let inventoryTotal: number = 0; - const receivableAccount = { id: 10 }; - const commonEntry = { - referenceType: 'SaleInvoice', - referenceId: saleInvoice.id, - date: saleInvoice.invoiceDate, - }; - const costTransactions: Map = 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); + async writeNonInventoryInvoiceEntries( + tenantId: number, + saleInvoice: ISaleInvoice, + override: boolean + ) { + const { accountRepository } = this.tenancy.repositories(tenantId); + const { AccountTransaction } = this.tenancy.models(tenantId); - 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); - } + // Receivable account. + const receivableAccount = await accountRepository.findOne({ + slug: 'accounts-receivable', }); + const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); + + if (override) { + const oldTransactions = await AccountTransaction.query() + .where('reference_type', 'SaleInvoice') + .where('reference_id', saleInvoice.id) + .withGraphFetched('account.type'); + + journal.fromTransactions(oldTransactions); + journal.removeEntries(); + } + journalCommands.saleInvoiceNonInventory(saleInvoice, receivableAccount.id); + + await Promise.all([ + journal.deleteEntries(), + journal.saveEntries(), + journal.saveBalance(), + ]); } } \ No newline at end of file diff --git a/server/src/subscribers/bills.ts b/server/src/subscribers/bills.ts index 47ddbc8dc..eddf69d7b 100644 --- a/server/src/subscribers/bills.ts +++ b/server/src/subscribers/bills.ts @@ -117,7 +117,8 @@ export default class BillSubscriber { this.logger.info('[bill] writing the inventory transactions', { tenantId }); this.billsService.recordInventoryTransactions( tenantId, - bill, + bill.id, + bill.billDate, ); } @@ -129,7 +130,8 @@ export default class BillSubscriber { this.logger.info('[bill] overwriting the inventory transactions.', { tenantId }); this.billsService.recordInventoryTransactions( tenantId, - bill, + bill.id, + bill.billDate, true, ); } @@ -145,4 +147,19 @@ export default class BillSubscriber { billId, ); } + + /** + * Schedules items cost compute jobs once the inventory transactions created + * of the bill transaction. + */ + @On(events.bill.onInventoryTransactionsCreated) + public async handleComputeItemsCosts({ tenantId, billId }) { + this.logger.info('[bill] trying to compute the bill items cost.', { + tenantId, billId, + }); + await this.billsService.scheduleComputeBillItemsCost( + tenantId, + billId, + ); + } } diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 10aa2f529..b332cb1ba 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -82,6 +82,8 @@ export default { onDeleted: 'onSaleInvoiceDeleted', onBulkDelete: 'onSaleInvoiceBulkDeleted', onPublished: 'onSaleInvoicePublished', + onInventoryTransactionsCreated: 'onInvoiceInventoryTransactionsCreated', + onInventoryTransactionsDeleted: 'onInvoiceInventoryTransactionsDeleted', }, /** @@ -125,6 +127,7 @@ export default { onDeleted: 'onBillDeleted', onBulkDeleted: 'onBillBulkDeleted', onPublished: 'onBillPublished', + onInventoryTransactionsCreated: 'onBillInventoryTransactionsCreated' }, /** @@ -165,5 +168,14 @@ export default { onEdited: 'onItemEdited', onDeleted: 'onItemDeleted', onBulkDeleted: 'onItemBulkDeleted', + }, + + /** + * Inventory service. + */ + inventory: { + onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled', + onComputeItemCostJobStarted: 'onComputeItemCostJobStarted', + onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted' } } diff --git a/server/src/subscribers/inventory.ts b/server/src/subscribers/inventory.ts new file mode 100644 index 000000000..9d2ecdc23 --- /dev/null +++ b/server/src/subscribers/inventory.ts @@ -0,0 +1,34 @@ +import { Container } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import SaleInvoicesCost from 'services/Sales/SalesInvoicesCost'; + +@EventSubscriber() +export class InventorySubscriber { + depends: number = 0; + startingDate: Date; + + /** + * Handle run writing the journal entries once the compute items jobs completed. + */ + @On(events.inventory.onComputeItemCostJobCompleted) + async onComputeItemCostJobFinished({ itemId, tenantId, startingDate }) { + const saleInvoicesCost = Container.get(SaleInvoicesCost); + const agenda = Container.get('agenda'); + + const dependsComputeJobs = await agenda.jobs({ + name: 'compute-item-cost', + nextRunAt: { $ne: null }, + 'data.tenantId': tenantId, + }); + // There is no scheduled compute jobs waiting. + if (dependsComputeJobs.length === 0) { + this.startingDate = null; + + await saleInvoicesCost.scheduleWriteJournalEntries( + tenantId, + startingDate + ); + } + } +} diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index c035fbe80..9f13d1f83 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -60,6 +60,64 @@ export default class SaleInvoiceSubscriber { } } + /** + * Handles sale invoice next number increment once invoice created. + */ + @On(events.saleInvoice.onCreated) + public async handleInvoiceNextNumberIncrement({ + tenantId, + saleInvoiceId, + saleInvoice, + }) { + await this.settingsService.incrementNextNumber(tenantId, { + key: 'next_number', + group: 'sales_invoices', + }); + } + + /** + * Handles the writing inventory transactions once the invoice created. + */ + @On(events.saleInvoice.onCreated) + public async handleWritingInventoryTransactions({ tenantId, saleInvoice }) { + this.logger.info('[sale_invoice] trying to write inventory transactions.', { + tenantId, + }); + await this.saleInvoicesService.recordInventoryTranscactions( + tenantId, + saleInvoice.id, + saleInvoice.invoiceDate, + ); + } + + /** + * Records journal entries of the non-inventory invoice. + */ + @On(events.saleInvoice.onCreated) + @On(events.saleInvoice.onEdited) + public async handleWritingNonInventoryEntries({ tenantId, saleInvoice }) { + await this.saleInvoicesService.recordNonInventoryJournalEntries( + tenantId, + saleInvoice.id, + ); + } + + /** + * + */ + @On(events.saleInvoice.onEdited) + public async handleRewritingInventoryTransactions({ tenantId, saleInvoice }) { + this.logger.info('[sale_invoice] trying to write inventory transactions.', { + tenantId, + }); + await this.saleInvoicesService.recordInventoryTranscactions( + tenantId, + saleInvoice.id, + saleInvoice.invoiceDate, + true, + ); + } + /** * Handles customer balance diff balnace change once sale invoice edited. */ @@ -103,35 +161,6 @@ export default class SaleInvoiceSubscriber { ); } - /** - * Handles sale invoice next number increment once invoice created. - */ - @On(events.saleInvoice.onCreated) - public async handleInvoiceNextNumberIncrement({ - tenantId, - saleInvoiceId, - saleInvoice, - }) { - await this.settingsService.incrementNextNumber(tenantId, { - key: 'next_number', - group: 'sales_invoices', - }); - } - - /** - * Handles the writing inventory transactions once the invoice created. - */ - @On(events.saleInvoice.onCreated) - public async handleWritingInventoryTransactions({ tenantId, saleInvoice }) { - this.logger.info('[sale_invoice] trying to write inventory transactions.', { - tenantId, - }); - await this.saleInvoicesService.recordInventoryTranscactions( - tenantId, - saleInvoice, - ); - } - /** * Handles deleting the inventory transactions once the invoice deleted. */ @@ -145,4 +174,18 @@ export default class SaleInvoiceSubscriber { saleInvoiceId, ); } + + /** + * Schedules compute invoice items cost job. + */ + @On(events.saleInvoice.onInventoryTransactionsCreated) + public async handleComputeItemsCosts({ tenantId, saleInvoiceId }) { + this.logger.info('[sale_invoice] trying to compute the invoice items cost.', { + tenantId, saleInvoiceId, + }); + await this.saleInvoicesService.scheduleComputeInvoiceItemsCost( + tenantId, + saleInvoiceId, + ); + } }