From ab6bc0517fbc1699ec8a37933be1bd53b2ec2369 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 23 Aug 2020 23:38:42 +0200 Subject: [PATCH] feat: Concurrency control items cost compute. --- ...822214306_create_items_categories_table.js | 6 + ...251_create_inventory_transactions_table.js | 3 +- ...create_inventory_cost_lot_tracker_table.js | 5 +- server/src/http/controllers/Authentication.js | 3 + server/src/http/controllers/Ping.ts | 1 + server/src/http/index.js | 1 - server/src/interfaces/IItem.ts | 7 + server/src/interfaces/InventoryCostMethod.ts | 2 +- server/src/interfaces/ItemEntry.ts | 14 ++ server/src/interfaces/SaleInvoice.ts | 8 + server/src/interfaces/index.ts | 4 + server/src/jobs/ComputeItemCost.ts | 4 +- server/src/jobs/writeInvoicesJEntries.ts | 22 +++ server/src/loaders/jobs.ts | 8 +- server/src/models/InventoryCostLotTracker.js | 10 ++ server/src/models/InventoryTransaction.js | 2 - server/src/models/ItemEntry.js | 15 ++ server/src/models/SaleInvoice.js | 34 ++++ server/src/services/Inventory/Inventory.ts | 24 +-- .../Inventory/InventoryAverageCost.ts | 102 +++++------- .../Inventory/InventoryCostLotTracker.ts | 138 ++--------------- .../services/Inventory/InventoryCostMethod.ts | 31 ++++ server/src/services/Items/ItemsCostService.ts | 5 + server/src/services/Items/ItemsService.js | 2 +- server/src/services/Purchases/Bills.js | 57 ++++--- server/src/services/Sales/HasItemsEntries.ts | 20 +++ server/src/services/Sales/SalesInvoices.ts | 131 ++++------------ .../src/services/Sales/SalesInvoicesCost.ts | 145 ++++++++++++++++++ 28 files changed, 463 insertions(+), 341 deletions(-) create mode 100644 server/src/interfaces/IItem.ts create mode 100644 server/src/interfaces/ItemEntry.ts create mode 100644 server/src/interfaces/SaleInvoice.ts create mode 100644 server/src/jobs/writeInvoicesJEntries.ts create mode 100644 server/src/services/Inventory/InventoryCostMethod.ts create mode 100644 server/src/services/Items/ItemsCostService.ts create mode 100644 server/src/services/Sales/SalesInvoicesCost.ts diff --git a/server/src/database/migrations/20190822214306_create_items_categories_table.js b/server/src/database/migrations/20190822214306_create_items_categories_table.js index dacea0fa6..a8b189137 100644 --- a/server/src/database/migrations/20190822214306_create_items_categories_table.js +++ b/server/src/database/migrations/20190822214306_create_items_categories_table.js @@ -6,6 +6,12 @@ exports.up = function (knex) { table.integer('parent_category_id').unsigned(); table.text('description'); table.integer('user_id').unsigned(); + + table.integer('cost_account_id').unsigned(); + table.integer('sell_account_id').unsigned(); + table.integer('inventory_account_id').unsigned(); + + table.string('cost_method'); 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 6555220c8..ede2040c4 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -13,8 +13,9 @@ exports.up = function(knex) { table.integer('lot_number'); table.string('transaction_type'); - table.integer('transaction_id'); + table.integer('transaction_id').unsigned(); + table.integer('entry_id').unsigned(); table.timestamps(); }); }; 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 1318515e3..51b31cf06 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 @@ -3,17 +3,18 @@ exports.up = function(knex) { return knex.schema.createTable('inventory_cost_lot_tracker', table => { table.increments(); table.date('date'); - table.string('direction'); table.integer('item_id').unsigned(); table.integer('quantity').unsigned(); table.decimal('rate', 13, 3); table.integer('remaining'); + table.integer('cost'); table.integer('lot_number'); table.string('transaction_type'); - table.integer('transaction_id'); + table.integer('transaction_id').unsigned(); + table.integer('entry_id').unsigned(); }); }; diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js index 70f37d0af..91f27a711 100644 --- a/server/src/http/controllers/Authentication.js +++ b/server/src/http/controllers/Authentication.js @@ -90,6 +90,9 @@ export default { } const lastLoginAt = moment().format('YYYY/MM/DD HH:mm:ss'); + const tenantDb = TenantsManager.knexInstance(user.tenant.organizationId); + TenantModel.knexBinded = tenantDb; + const updateTenantUser = TenantUser.tenant().query() .where('id', user.id) .update({ last_login_at: lastLoginAt }); diff --git a/server/src/http/controllers/Ping.ts b/server/src/http/controllers/Ping.ts index 7f38cc579..e6a593259 100644 --- a/server/src/http/controllers/Ping.ts +++ b/server/src/http/controllers/Ping.ts @@ -1,4 +1,5 @@ import { Router, Request, Response } from 'express'; +import { Container } from 'typedi'; export default class Ping { /** diff --git a/server/src/http/index.js b/server/src/http/index.js index dbe0c44be..3322cb83b 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -26,7 +26,6 @@ import Ping from '@/http/controllers/Ping'; import Agendash from '@/http/controllers/Agendash'; export default (app) => { - // app.use('/api/oauth2', OAuth2.router()); app.use('/api/auth', Authentication.router()); app.use('/api/invite', InviteUsers.router()); diff --git a/server/src/interfaces/IItem.ts b/server/src/interfaces/IItem.ts new file mode 100644 index 000000000..52a0d1b0c --- /dev/null +++ b/server/src/interfaces/IItem.ts @@ -0,0 +1,7 @@ + + +export interface IItem{ + id: number, + name: string, + type: string, +} \ No newline at end of file diff --git a/server/src/interfaces/InventoryCostMethod.ts b/server/src/interfaces/InventoryCostMethod.ts index 4804805f1..104edc5e3 100644 --- a/server/src/interfaces/InventoryCostMethod.ts +++ b/server/src/interfaces/InventoryCostMethod.ts @@ -2,5 +2,5 @@ interface IInventoryCostMethod { computeItemsCost(fromDate: Date): void, - initialize(): void, + storeInventoryLotsCost(transactions: any[]): void, } \ No newline at end of file diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts new file mode 100644 index 000000000..7b519c55f --- /dev/null +++ b/server/src/interfaces/ItemEntry.ts @@ -0,0 +1,14 @@ + + +export interface IItemEntry { + referenceType: string, + referenceId: number, + + index: number, + + itemId: number, + description: string, + discount: number, + quantity: number, + rate: number, +} \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts new file mode 100644 index 000000000..3942038df --- /dev/null +++ b/server/src/interfaces/SaleInvoice.ts @@ -0,0 +1,8 @@ + + +export interface ISaleInvoice { + id: number, + balance: number, + invoiceDate: Date, + entries: [], +} \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 03b016bcb..6a6edb126 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -1,6 +1,8 @@ import { IInventoryTransaction, IInventoryLotCost } from './InventoryTransaction'; import { IBillPaymentEntry, IBillPayment } from './BillPayment'; import { IInventoryCostMethod } from './IInventoryCostMethod'; +import { IItemEntry } from './ItemEntry'; +import { IItem } from './Item'; export { IBillPaymentEntry, @@ -8,4 +10,6 @@ export { IInventoryTransaction, IInventoryLotCost, IInventoryCostMethod, + IItemEntry + IItem, }; \ No newline at end of file diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index b2a3fe8ef..4e0cc07ae 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -6,9 +6,11 @@ export default class ComputeItemCostJob { const Logger = Container.get('logger'); const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data; + Logger.debug(`Compute item cost - started: ${job.attrs.data}`); + try { await InventoryService.computeItemCost(startingDate, itemId, costMethod); - Logger.debug(`Compute item cost: ${job.attrs.data}`); + Logger.debug(`Compute item cost - completed: ${job.attrs.data}`); done(); } catch(e) { console.log(e); diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts new file mode 100644 index 000000000..23606443b --- /dev/null +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -0,0 +1,22 @@ +import { Container } from 'typedi'; +import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; + +export default class WriteInvoicesJournalEntries { + + public async handler(job, done: Function): Promise { + const Logger = Container.get('logger'); + const { startingDate } = job.attrs.data; + + Logger.debug(`Write sales invoices journal entries - started: ${job.attrs.data}`); + + try { + await SalesInvoicesCost.writeJournalEntries(startingDate, true); + Logger.debug(`Write sales invoices journal entries - completed: ${job.attrs.data}`); + done(); + } catch(e) { + console.log(e); + Logger.error(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`); + done(e); + } + } +} \ No newline at end of file diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index dccd93bfb..a50f48361 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -1,6 +1,7 @@ import Agenda from 'agenda'; import WelcomeEmailJob from '@/Jobs/welcomeEmail'; import ComputeItemCost from '@/Jobs/ComputeItemCost'; +import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries'; export default ({ agenda }: { agenda: Agenda }) => { agenda.define( @@ -10,8 +11,13 @@ export default ({ agenda }: { agenda: Agenda }) => { ); agenda.define( 'compute-item-cost', - { priority: 'high' }, + { priority: 'high', concurrency: 20 }, new ComputeItemCost().handler, ); + agenda.define( + 'rewrite-invoices-journal-entries', + { priority: 'normal', concurrency: 1, }, + new RewriteInvoicesJournalEntries().handler, + ); agenda.start(); }; diff --git a/server/src/models/InventoryCostLotTracker.js b/server/src/models/InventoryCostLotTracker.js index 9ebcbfc44..31a3b3dea 100644 --- a/server/src/models/InventoryCostLotTracker.js +++ b/server/src/models/InventoryCostLotTracker.js @@ -22,6 +22,16 @@ export default class InventoryCostLotTracker extends TenantModel { */ static get modifiers() { return { + groupedEntriesCost(query) { + query.select(['entry_id', 'transaction_id', 'transaction_type']); + + query.groupBy('item_id'); + query.groupBy('entry_id'); + query.groupBy('transaction_id'); + query.groupBy('transaction_type'); + + query.sum('cost as cost'); + }, filterDateRange(query, startDate, endDate, type = 'day') { const dateFormat = 'YYYY-MM-DD HH:mm:ss'; const fromDate = moment(startDate).startOf(type).format(dateFormat); diff --git a/server/src/models/InventoryTransaction.js b/server/src/models/InventoryTransaction.js index cf2e3dc3e..37dd84e4d 100644 --- a/server/src/models/InventoryTransaction.js +++ b/server/src/models/InventoryTransaction.js @@ -17,7 +17,6 @@ export default class InventoryTransaction extends TenantModel { return ['createdAt', 'updatedAt']; } - /** * Model modifiers. */ @@ -38,7 +37,6 @@ export default class InventoryTransaction extends TenantModel { }; } - /** * Relationship mapping. */ diff --git a/server/src/models/ItemEntry.js b/server/src/models/ItemEntry.js index 7829b907c..4d4319a9f 100644 --- a/server/src/models/ItemEntry.js +++ b/server/src/models/ItemEntry.js @@ -32,4 +32,19 @@ export default class ItemEntry extends TenantModel { return discount ? total - (total * discount * 0.01) : total; } + + static get relationMappings() { + const Item = require('@/models/Item'); + + return { + item: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Item.default), + join: { + from: 'items_entries.itemId', + to: 'items.id', + }, + }, + }; + } } diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 331187ada..27062766b 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -3,6 +3,7 @@ import moment from 'moment'; import TenantModel from '@/models/TenantModel'; import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; import CachableModel from '@/lib/Cachable/CachableModel'; +import InventoryCostLotTracker from './InventoryCostLotTracker'; export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { /** @@ -26,6 +27,26 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { return ['created_at', 'updated_at']; } + /** + * Model modifiers. + */ + static get modifiers() { + return { + filterDateRange(query, startDate, endDate, type = 'day') { + const dateFormat = 'YYYY-MM-DD HH:mm:ss'; + const fromDate = moment(startDate).startOf(type).format(dateFormat); + const toDate = moment(endDate).endOf(type).format(dateFormat); + + if (startDate) { + query.where('invoice_date', '>=', fromDate); + } + if (endDate) { + query.where('invoice_date', '<=', toDate); + } + }, + }; + } + /** * Due amount of the given. */ @@ -40,6 +61,7 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { const AccountTransaction = require('@/models/AccountTransaction'); const ItemEntry = require('@/models/ItemEntry'); const Customer = require('@/models/Customer'); + const InventoryCostLotTracker = require('@/models/InventoryCostLotTracker'); return { entries: { @@ -73,6 +95,18 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { filter(builder) { builder.where('reference_type', 'SaleInvoice'); }, + }, + + costTransactions: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(InventoryCostLotTracker.default), + join: { + from: 'sales_invoices.id', + to: 'inventory_cost_lot_tracker.transactionId' + }, + filter(builder) { + builder.where('transaction_type', 'SaleInvoice'); + }, } }; } diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index f99e7fd50..d58806254 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -13,13 +13,22 @@ 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 + * @param {Date} fromDate - + * @param {number} itemId - */ static async computeItemCost(fromDate: Date, itemId: number) { - const costMethod: TCostMethod = 'FIFO'; + const item = await Item.tenant().query() + .findById(itemId) + .withGraphFetched('category'); + + // 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) { case 'FIFO': case 'LIFO': @@ -27,10 +36,9 @@ export default class InventoryService { break; case 'AVG': costMethodComputer = new InventoryAverageCost(fromDate, itemId); - break + break; } - await costMethodComputer.initialize(); - await costMethodComputer.computeItemCost() + return costMethodComputer.computeItemCost(); } /** @@ -41,10 +49,6 @@ export default class InventoryService { static async scheduleComputeItemCost(itemId: number, startingDate: Date|string) { const agenda = Container.get('agenda'); - // Delete the scheduled job in case has the same given data. - await agenda.cancel({ - name: 'compute-item-cost', - }); return agenda.schedule('in 3 seconds', 'compute-item-cost', { startingDate, itemId, }); diff --git a/server/src/services/Inventory/InventoryAverageCost.ts b/server/src/services/Inventory/InventoryAverageCost.ts index 586849192..87fad4e73 100644 --- a/server/src/services/Inventory/InventoryAverageCost.ts +++ b/server/src/services/Inventory/InventoryAverageCost.ts @@ -1,36 +1,27 @@ -import { Account, InventoryTransaction } from '@/models'; +import { pick } from 'lodash'; +import { InventoryTransaction } from '@/models'; import { IInventoryTransaction } from '@/interfaces'; -import JournalPoster from '@/services/Accounting/JournalPoster'; -import JournalCommands from '@/services/Accounting/JournalCommands'; +import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod'; -export default class InventoryAverageCostMethod implements IInventoryCostMethod { - journal: JournalPoster; - journalCommands: JournalCommands; - fromDate: Date; +export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod { + startingDate: Date; itemId: number; + costTransactions: any[]; /** * Constructor method. - * @param {Date} fromDate - + * @param {Date} startingDate - * @param {number} itemId - */ constructor( - fromDate: Date, + startingDate: Date, itemId: number, ) { - this.fromDate = fromDate; + super(); + + this.startingDate = startingDate; 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); + this.costTransactions = []; } /** @@ -43,50 +34,41 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod * after the given date. * ---------- * @asycn - * @param {Date} fromDate + * @param {Date} startingDate * @param {number} referenceId * @param {string} referenceType */ public async computeItemCost() { - const openingAvgCost = await this.getOpeningAvaregeCost(this.fromDate, this.itemId); + const openingAvgCost = await this.getOpeningAvaregeCost(this.startingDate, this.itemId); - // @todo from `invTransactions`. const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction .tenant() .query() - .where('date', '>=', this.fromDate) - // .where('direction', 'OUT') - .orderBy('date', 'asc') + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'ASC') + .orderByRaw("FIELD(direction, 'IN', 'OUT')") + .where('item_id', this.itemId) .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( + // Tracking inventroy transactions and retrieve cost transactions + // based on average rate cost method. + const costTransactions = this.trackingCostTransactions( afterInvTransactions, openingAvgCost, ); - // Saves the new recorded journal entries to the storage. - await Promise.all([ - this.journal.deleteEntries(), - this.journal.saveEntries(), - this.journal.saveBalance(), - ]); + await this.storeInventoryLotsCost(costTransactions); } /** * Get items Avarege cost from specific date from inventory transactions. * @static - * @param {Date} fromDate + * @param {Date} startingDate * @return {number} */ - public async getOpeningAvaregeCost(fromDate: Date, itemId: number) { + public async getOpeningAvaregeCost(startingDate: Date, itemId: number) { const commonBuilder = (builder: any) => { - if (fromDate) { - builder.where('date', '<', fromDate); + if (startingDate) { + builder.where('date', '<', startingDate); } builder.where('item_id', itemId); builder.groupBy('rate'); @@ -155,53 +137,45 @@ export default class InventoryAverageCostMethod implements IInventoryCostMethod * @param {number} referenceId * @param {JournalCommand} journalCommands */ - async jEntriesFromItemInvTransactions( + public trackingCostTransactions( invTransactions: IInventoryTransaction[], openingAverageCost: number, ) { - const transactions: any[] = []; + const costTransactions: any[] = []; let accQuantity: number = 0; let accCost: number = 0; invTransactions.forEach((invTransaction: IInventoryTransaction) => { const commonEntry = { - date: invTransaction.date, - referenceType: invTransaction.transactionType, - referenceId: invTransaction.transactionId, + invTransId: invTransaction.id, + ...pick(invTransaction, ['date', 'direction', 'itemId', 'quantity', 'rate', 'entryId', + 'transactionId', 'transactionType']), }; switch(invTransaction.direction) { case 'IN': accQuantity += invTransaction.quantity; accCost += invTransaction.rate * invTransaction.quantity; - const inventory = invTransaction.quantity * invTransaction.rate; - - transactions.push({ + costTransactions.push({ ...commonEntry, - inventory, - inventoryAccount: invTransaction.item.inventoryAccountId, }); break; - case 'OUT': - const income = invTransaction.quantity * invTransaction.rate; + case 'OUT': const transactionAvgCost = accCost ? (accCost / accQuantity) : 0; const averageCost = transactionAvgCost; - const cost = (invTransaction.quantity * averageCost); + const cost = (invTransaction.quantity * averageCost); + const income = (invTransaction.quantity * invTransaction.rate); accQuantity -= invTransaction.quantity; - accCost -= accCost; + accCost -= income; - transactions.push({ + costTransactions.push({ ...commonEntry, - income, cost, - incomeAccount: invTransaction.item.sellAccountId, - costAccount: invTransaction.item.costAccountId, - inventoryAccount: invTransaction.item.inventoryAccountId, }); break; } }); - this.journalCommands.inventoryEntries(transactions); + return costTransactions; } } \ No newline at end of file diff --git a/server/src/services/Inventory/InventoryCostLotTracker.ts b/server/src/services/Inventory/InventoryCostLotTracker.ts index b2b3a7515..041f70135 100644 --- a/server/src/services/Inventory/InventoryCostLotTracker.ts +++ b/server/src/services/Inventory/InventoryCostLotTracker.ts @@ -3,20 +3,15 @@ import moment from 'moment'; import { InventoryTransaction, InventoryLotCostTracker, - Account, Item, } from "@/models"; import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces"; -import JournalPoster from '@/services/Accounting/JournalPoster'; -import JournalCommands from '@/services/Accounting/JournalCommands'; +import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod'; type TCostMethod = 'FIFO' | 'LIFO'; -export default class InventoryCostLotTracker implements IInventoryCostMethod { - journal: JournalPoster; - journalCommands: JournalCommands; +export default class InventoryCostLotTracker extends InventoryCostMethod implements IInventoryCostMethod { startingDate: Date; - headDate: Date; itemId: number; costMethod: TCostMethod; itemsById: Map; @@ -25,7 +20,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { costLotsTransactions: IInventoryLotCost[]; inTransactions: any[]; outTransactions: IInventoryTransaction[]; - revertInvoiceTrans: any[]; revertJEntriesTransactions: IInventoryTransaction[]; /** @@ -35,6 +29,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { * @param {string} costMethod - */ constructor(startingDate: Date, itemId: number, costMethod: TCostMethod = 'FIFO') { + super(); + this.startingDate = startingDate; this.itemId = itemId; this.costMethod = costMethod; @@ -49,18 +45,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { this.inTransactions = []; // Collects `OUT` transactions. this.outTransactions = []; - // Collects journal entries reference id and type that should be reverted. - this.revertInvoiceTrans = []; - } - - /** - * 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); } /** @@ -80,30 +64,16 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { await this.fetchInvOUTTransactions(); await this.fetchRevertInvJReferenceIds(); await this.fetchItemsMapped(); - + this.trackingInventoryINLots(this.inTransactions); this.trackingInventoryOUTLots(this.outTransactions); // Re-tracking the inventory `IN` and `OUT` lots costs. const storedTrackedInvLotsOper = this.storeInventoryLotsCost( this.costLotsTransactions, - ); - - // Remove and revert accounts balance journal entries from inventory transactions. - const revertJEntriesOper = this.revertJournalEntries(this.revertJEntriesTransactions); - - // Records the journal entries operation. - this.recordJournalEntries(this.costLotsTransactions); - + ); 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(), - ])), ]); } @@ -121,7 +91,8 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { const afterInvTransactions: IInventoryTransaction[] = await InventoryTransaction.tenant() .query() - .modify('filterDateRange', this.startingDate) + .modify('filterDateRange', this.startingDate) + .orderByRaw("FIELD(direction, 'IN', 'OUT')") .orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC') .onBuild(commonBuilder) .withGraphFetched('item'); @@ -221,93 +192,6 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { return Promise.all([deleteInvLotsTrans, ...asyncOpers]); } - /** - * Reverts the journal entries from inventory lots costs transaction. - * @param {} inventoryLots - */ - async revertJournalEntries( - transactions: IInventoryLotCost[], - ) { - return this.journalCommands - .revertEntriesFromInventoryTransactions(transactions); - } - - /** - * 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 { - 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 inventory `IN` lots transactions. * @public @@ -352,7 +236,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { const commonLotTransaction: IInventoryLotCost = { ...pick(transaction, [ - 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', + 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId', 'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining' ]), }; @@ -373,6 +257,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0; const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining; const maxDecrement = Math.min(decrement, invRemaining); + const cost = maxDecrement * _invINTransaction.rate; _invINTransaction.decrement += maxDecrement; _invINTransaction.remaining = Math.max( @@ -383,6 +268,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { this.costLotsTransactions.push({ ...commonLotTransaction, + cost, quantity: maxDecrement, lotNumber: _invINTransaction.lotNumber, }); @@ -392,7 +278,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { } return false; }); - if (invRemaining > 0) { + if (invRemaining > 0) { this.costLotsTransactions.push({ ...commonLotTransaction, quantity: invRemaining, diff --git a/server/src/services/Inventory/InventoryCostMethod.ts b/server/src/services/Inventory/InventoryCostMethod.ts new file mode 100644 index 000000000..b93e860d3 --- /dev/null +++ b/server/src/services/Inventory/InventoryCostMethod.ts @@ -0,0 +1,31 @@ +import { omit } from 'lodash'; +import { IInventoryLotCost } from '@/interfaces'; +import { InventoryLotCostTracker } from '@/models'; + +export default class InventoryCostMethod { + /** + * Stores the inventory lots costs transactions in bulk. + * @param {IInventoryLotCost[]} costLotsTransactions + * @return {Promise[]} + */ + public storeInventoryLotsCost(costLotsTransactions: IInventoryLotCost[]): Promise { + 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); + } +} \ No newline at end of file diff --git a/server/src/services/Items/ItemsCostService.ts b/server/src/services/Items/ItemsCostService.ts new file mode 100644 index 000000000..6f028f808 --- /dev/null +++ b/server/src/services/Items/ItemsCostService.ts @@ -0,0 +1,5 @@ + + +export default class ItemsCostService { + +} \ No newline at end of file diff --git a/server/src/services/Items/ItemsService.js b/server/src/services/Items/ItemsService.js index 19511a7be..00e353bca 100644 --- a/server/src/services/Items/ItemsService.js +++ b/server/src/services/Items/ItemsService.js @@ -1,5 +1,5 @@ import { difference } from "lodash"; -import { Item } from '@/models'; +import { Item, ItemTransaction } from '@/models'; export default class ItemsService { diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index c44d9dd3d..1cdfcc766 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -14,13 +14,14 @@ import AccountsService from '@/services/Accounts/AccountsService'; import JournalPosterService from '@/services/Sales/JournalPosterService'; import InventoryService from '@/services/Inventory/Inventory'; import HasItemsEntries from '@/services/Sales/HasItemsEntries'; +import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; import { formatDateFields } from '@/utils'; /** * Vendor bills services. * @service */ -export default class BillsService { +export default class BillsService extends SalesInvoicesCost { /** * Creates a new bill and stored it to the storage. * @@ -52,13 +53,18 @@ export default class BillsService { bill.entries.forEach((entry) => { const oper = ItemEntry.tenant() .query() - .insert({ + .insertAndFetch({ reference_type: 'Bill', reference_id: storedBill.id, ...omit(entry, ['amount']), + }).then((itemEntry) => { + entry.id = itemEntry.id; }); saveEntriesOpers.push(oper); }); + // Await save all bill entries operations. + await Promise.all([...saveEntriesOpers]); + // Increments vendor balance. const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount); @@ -68,18 +74,16 @@ export default class BillsService { ); // Writes the journal entries for the given bill transaction. const writeJEntriesOper = this.recordJournalTransactions({ - id: storedBill.id, - ...bill + id: storedBill.id, ...bill, }); await Promise.all([ - ...saveEntriesOpers, - incrementOper, + incrementOper, writeInvTransactionsOper, writeJEntriesOper, - ]); + ]); // Schedule bill re-compute based on the item cost // method and starting date. - await this.scheduleComputeItemsCost(bill); + await this.scheduleComputeBillItemsCost(bill); return storedBill; } @@ -147,7 +151,7 @@ export default class BillsService { ]); // Schedule sale invoice re-compute based on the item cost // method and starting date. - await this.scheduleComputeItemsCost(bill); + await this.scheduleComputeBillItemsCost(bill); } /** @@ -192,10 +196,10 @@ export default class BillsService { ]); // Schedule sale invoice re-compute based on the item cost // method and starting date. - await this.scheduleComputeItemsCost(bill); + await this.scheduleComputeBillItemsCost(bill); } - /** + /** * Records the inventory transactions from the given bill input. * @param {Bill} bill * @param {number} billId @@ -209,6 +213,7 @@ export default class BillsService { transactionId: billId, direction: 'IN', date: bill.bill_date, + entryId: entry.id, })); return InventoryService.recordInventoryTransactions( @@ -284,24 +289,6 @@ export default class BillsService { ]); } - /** - * Schedule a job to re-compute the bill's items based on cost method - * of the each one. - * @param {Bill} bill - */ - static scheduleComputeItemsCost(bill) { - const asyncOpers = []; - - bill.entries.forEach((entry) => { - const oper = InventoryService.scheduleComputeItemCost( - entry.item_id || entry.itemId, - bill.bill_date || bill.billDate, - ); - asyncOpers.push(oper); - }); - return Promise.all(asyncOpers); - } - /** * Detarmines whether the bill exists on the storage. * @param {Integer} billId @@ -355,4 +342,16 @@ export default class BillsService { .withGraphFetched('entries') .first(); } + + /** + * Schedules compute bill items cost based on each item cost method. + * @param {IBill} bill + * @return {Promise} + */ + static scheduleComputeBillItemsCost(bill) { + return this.scheduleComputeItemsCost( + bill.entries.map((e) => e.item_id), + bill.bill_date, + ); + } } diff --git a/server/src/services/Sales/HasItemsEntries.ts b/server/src/services/Sales/HasItemsEntries.ts index cc18f7cfe..43c9de698 100644 --- a/server/src/services/Sales/HasItemsEntries.ts +++ b/server/src/services/Sales/HasItemsEntries.ts @@ -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 + )); + } } \ No newline at end of file diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index f3c45c75e..095d11701 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -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} - */ - 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, + ); + } } diff --git a/server/src/services/Sales/SalesInvoicesCost.ts b/server/src/services/Sales/SalesInvoicesCost.ts new file mode 100644 index 000000000..0dca3864f --- /dev/null +++ b/server/src/services/Sales/SalesInvoicesCost.ts @@ -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} + */ + 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 = this.scheduleWriteJournalEntries(startingDate); + + return Promise.all([...asyncOpers, writeJEntriesOper]); + } + + /** + * Schedule writing journal entries. + * @param {Date} startingDate + * @return {Promise} + */ + 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 = 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(), + ]); + } +} \ No newline at end of file