From 74321a2886117522c85c6484f95c994425fcdf37 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 29 Aug 2020 22:11:42 +0200 Subject: [PATCH] - fix: Schedule write journal entries after item compute cost. - fix: active vouchers query. - fix: remove babel loader in server-side. --- server/package.json | 1 + server/src/http/controllers/Items.ts | 3 - server/src/interfaces/SaleInvoice.ts | 14 +- server/src/interfaces/index.ts | 7 + server/src/jobs/ComputeItemCost.ts | 53 ++++++- server/src/loaders/jobs.ts | 4 +- .../Inventory/InventoryCostLotTracker.ts | 8 +- server/src/services/Purchases/Bills.js | 18 ++- server/src/services/Sales/SalesInvoices.ts | 68 +++++++-- .../src/services/Sales/SalesInvoicesCost.ts | 138 +++++++++--------- .../system/models/Subscriptions/Voucher.ts | 1 - server/webpack.config.js | 6 - 12 files changed, 217 insertions(+), 104 deletions(-) diff --git a/server/package.json b/server/package.json index f1e37ce8b..c42adc525 100644 --- a/server/package.json +++ b/server/package.json @@ -58,6 +58,7 @@ "nodemon": "^1.19.1", "objection": "^2.0.10", "reflect-metadata": "^0.1.13", + "ts-transformer-keys": "^0.4.2", "tsyringe": "^4.3.0", "uniqid": "^5.2.0", "winston": "^3.2.1" diff --git a/server/src/http/controllers/Items.ts b/server/src/http/controllers/Items.ts index a54a72dab..550addb25 100644 --- a/server/src/http/controllers/Items.ts +++ b/server/src/http/controllers/Items.ts @@ -79,7 +79,6 @@ export default class ItemsController { check('type').exists().trim().escape() .isIn(['service', 'non-inventory', 'inventory']), check('sku').optional({ nullable: true }).trim().escape(), - // Purchase attributes. check('purchasable').optional().isBoolean().toBoolean(), check('cost_price') @@ -92,7 +91,6 @@ export default class ItemsController { .exists() .isInt() .toInt(), - // Sell attributes. check('sellable').optional().isBoolean().toBoolean(), check('sell_price') @@ -105,7 +103,6 @@ export default class ItemsController { .exists() .isInt() .toInt(), - check('inventory_account_id') .if(check('type').equals('inventory')) .exists() diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 3942038df..1628c9da0 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -1,8 +1,18 @@ - export interface ISaleInvoice { id: number, balance: number, + paymentAmount: number, invoiceDate: Date, - entries: [], + dueDate: Date, + entries: any[], +} + +export interface ISaleInvoiceOTD { + invoiceDate: Date, + dueDate: Date, + referenceNo: string, + invoiceMessage: string, + termsConditions: string, + entries: any[], } \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index aa7ff0ee1..cce52ff2c 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -12,6 +12,10 @@ import { IVoucherPaymentMethod, IPaymentContext, } from './Payment'; +import { + ISaleInvoice, + ISaleInvoiceOTD, +} from './SaleInvoice'; export { IBillPaymentEntry, @@ -31,4 +35,7 @@ export { IPaymentContext, IVoucherPaymentModel, IVoucherPaymentMethod, + + ISaleInvoice, + ISaleInvoiceOTD, }; \ No newline at end of file diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index 4e0cc07ae..1049a6986 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -1,7 +1,26 @@ import { Container } from 'typedi'; +import moment from 'moment'; import InventoryService from '@/services/Inventory/Inventory'; +import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; export default class ComputeItemCostJob { + depends: number; + agenda: any; + startingDate: Date; + + constructor(agenda) { + this.agenda = agenda; + this.depends = 0; + this.startingDate = null; + + this.agenda.on('complete:compute-item-cost', this.onJobFinished.bind(this)); + this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this)); + } + + /** + * The job handler. + * @param {} - + */ public async handler(job, done: Function): Promise { const Logger = Container.get('logger'); const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data; @@ -17,6 +36,38 @@ export default class ComputeItemCostJob { Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`); done(e); } - + } + + /** + * Handle the job started. + * @param {Job} job - . + */ + async onJobStart(job) { + const { startingDate } = job.attrs.data; + this.depends += 1; + + if (!this.startingDate || moment(this.startingDate).isBefore(startingDate)) { + this.startingDate = startingDate; + } + } + + /** + * Handle job complete items cost finished. + * @param {Job} job - + */ + async onJobFinished() { + const agenda = Container.get('agenda'); + const startingDate = this.startingDate; + this.depends = Math.max(this.depends - 1, 0); + + console.log(startingDate); + + if (this.depends === 0) { + this.startingDate = null; + + await agenda.now('rewrite-invoices-journal-entries', { + startingDate, + }); + } } } diff --git a/server/src/loaders/jobs.ts b/server/src/loaders/jobs.ts index c8e9e44bd..f88686e0b 100644 --- a/server/src/loaders/jobs.ts +++ b/server/src/loaders/jobs.ts @@ -14,12 +14,12 @@ export default ({ agenda }: { agenda: Agenda }) => { agenda.define( 'compute-item-cost', { priority: 'high', concurrency: 20 }, - new ComputeItemCost().handler, + new ComputeItemCost(agenda).handler, ); agenda.define( 'rewrite-invoices-journal-entries', { priority: 'normal', concurrency: 1, }, - new RewriteInvoicesJournalEntries().handler, + new RewriteInvoicesJournalEntries(agenda).handler, ); agenda.define( 'send-voucher-via-phone', diff --git a/server/src/services/Inventory/InventoryCostLotTracker.ts b/server/src/services/Inventory/InventoryCostLotTracker.ts index 041f70135..98048f060 100644 --- a/server/src/services/Inventory/InventoryCostLotTracker.ts +++ b/server/src/services/Inventory/InventoryCostLotTracker.ts @@ -84,8 +84,7 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme */ private async fetchInvINTransactions() { const commonBuilder = (builder: any) => { - builder.where('direction', 'IN'); - builder.orderBy('date', 'ASC'); + builder.orderBy('date', (this.costMethod === 'LIFO') ? 'DESC': 'ASC'); builder.where('item_id', this.itemId); }; const afterInvTransactions: IInventoryTransaction[] = @@ -93,8 +92,8 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme .query() .modify('filterDateRange', this.startingDate) .orderByRaw("FIELD(direction, 'IN', 'OUT')") - .orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC') .onBuild(commonBuilder) + .orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC') .withGraphFetched('item'); const availiableINLots: IInventoryLotCost[] = @@ -102,7 +101,8 @@ export default class InventoryCostLotTracker extends InventoryCostMethod impleme .query() .modify('filterDateRange', null, this.startingDate) .orderBy('date', 'ASC') - .orderBy('lot_number', 'ASC') + .where('direction', 'IN') + .orderBy('lot_number', 'ASC') .onBuild(commonBuilder) .whereNot('remaining', 0); diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index 1cdfcc766..ff9953f65 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -348,10 +348,18 @@ export default class BillsService extends SalesInvoicesCost { * @param {IBill} bill * @return {Promise} */ - static scheduleComputeBillItemsCost(bill) { - return this.scheduleComputeItemsCost( - bill.entries.map((e) => e.item_id), - bill.bill_date, - ); + static async scheduleComputeBillItemsCost(bill) { + const billItemsIds = bill.entries.map((entry) => entry.item_id); + + // Retrieves inventory items only. + const inventoryItems = await Item.tenant().query() + .whereIn('id', billItemsIds) + .where('type', 'inventory'); + + const inventoryItemsIds = inventoryItems.map(i => i.id); + + if (inventoryItemsIds.length > 0) { + await this.scheduleComputeItemsCost(inventoryItemsIds, bill.bill_date); + } } } diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 8653d57a4..72b4aa941 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,4 +1,4 @@ -import { omit, sumBy, difference, pick } from 'lodash'; +import { omit, sumBy, difference, pick, chain } from 'lodash'; import { SaleInvoice, AccountTransaction, @@ -6,12 +6,14 @@ import { Account, ItemEntry, Customer, + Item, } from '@/models'; 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 { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from '@/interfaces'; import { formatDateFields } from '@/utils'; /** @@ -26,11 +28,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {ISaleInvoice} * @return {ISaleInvoice} */ - static async createSaleInvoice(saleInvoiceDTO: any) { + static async createSaleInvoice(saleInvoiceDTO: ISaleInvoiceOTD) { const balance = sumBy(saleInvoiceDTO.entries, 'amount'); const invLotNumber = await InventoryService.nextLotNumber(); - const saleInvoice = { - ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), + const saleInvoice: ISaleInvoice = { + ...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']), balance, paymentAmount: 0, invLotNumber, @@ -70,7 +72,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { ); // Schedule sale invoice re-compute based on the item cost // method and starting date. - await this.scheduleComputeInvoiceItemsCost(saleInvoice); + await this.scheduleComputeInvoiceItemsCost(storedInvoice.id); return storedInvoice; } @@ -92,7 +94,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { balance, invLotNumber: oldSaleInvoice.invLotNumber, }; - const updatedSaleInvoices = await SaleInvoice.tenant() + const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.tenant() .query() .where('id', saleInvoiceId) .update({ @@ -124,10 +126,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { changeCustomerBalanceOper, recordInventoryTransOper, ]); - // Schedule sale invoice re-compute based on the item cost // method and starting date. - await this.scheduleComputeInvoiceItemsCost(saleInvoice); + await this.scheduleComputeInvoiceItemsCost(saleInvoiceId, true); } /** @@ -313,10 +314,51 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {ISaleInvoice} saleInvoice * @return {Promise} */ - static scheduleComputeInvoiceItemsCost(saleInvoice) { - return this.scheduleComputeItemsCost( - saleInvoice.entries.map((e) => e.item_id), - saleInvoice.invoice_date, - ); + static async scheduleComputeInvoiceItemsCost(saleInvoiceId: number, override?: boolean) { + const saleInvoice: ISaleInvoice = await SaleInvoice.tenant() + .query() + .findById(saleInvoiceId) + .withGraphFetched('entries.item'); + + const inventoryItemsIds = chain(saleInvoice.entries) + .filter((entry: IItemEntry) => entry.item.type === 'inventory') + .map((entry: IItemEntry) => entry.itemId) + .uniq().value(); + + if (inventoryItemsIds.length === 0) { + await this.writeNonInventoryInvoiceJournals(saleInvoice, override); + } else { + await this.scheduleComputeItemsCost( + inventoryItemsIds, + saleInvoice.invoice_date, + ); + } + } + + /** + * Writes the sale invoice journal entries. + * @param {SaleInvoice} saleInvoice - + */ + static async writeNonInventoryInvoiceJournals(saleInvoice: ISaleInvoice, override: boolean) { + const accountsDepGraph = await Account.tenant().depGraph().query(); + const journal = new JournalPoster(accountsDepGraph); + + if (override) { + const oldTransactions = await AccountTransaction.tenant() + .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(), + ]); } } diff --git a/server/src/services/Sales/SalesInvoicesCost.ts b/server/src/services/Sales/SalesInvoicesCost.ts index 0dca3864f..0aec941dc 100644 --- a/server/src/services/Sales/SalesInvoicesCost.ts +++ b/server/src/services/Sales/SalesInvoicesCost.ts @@ -9,31 +9,27 @@ import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; import InventoryService from '@/services/Inventory/Inventory'; import { ISaleInvoice, IItemEntry, IItem } from '@/interfaces'; +import { ISaleInvoice } 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 - + * @param {number[]} itemIds - Inventory items ids. + * @param {Date} startingDate - Starting compute cost date. * @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'); + static async scheduleComputeItemsCost(inventoryItemsIds: number[], startingDate: Date) { const asyncOpers: Promise<[]>[] = []; - inventoryItems.forEach((item: IItem) => { + inventoryItemsIds.forEach((inventoryItemId: number) => { const oper: Promise<[]> = InventoryService.scheduleComputeItemCost( - item.id, + inventoryItemId, startingDate, ); asyncOpers.push(oper); }); - const writeJEntriesOper: Promise = this.scheduleWriteJournalEntries(startingDate); - - return Promise.all([...asyncOpers, writeJEntriesOper]); + return Promise.all([...asyncOpers]); } /** @@ -63,7 +59,6 @@ export default class SaleInvoicesCost { builder.withGraphFetched('entries.item') builder.withGraphFetched('costTransactions(groupedEntriesCost)'); }); - const accountsDepGraph = await Account.tenant().depGraph().query(); const journal = new JournalPoster(accountsDepGraph); @@ -79,62 +74,9 @@ export default class SaleInvoicesCost { 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); - } - }); + salesInvoices.forEach((saleInvoice: ISaleInvoice) => { + this.saleInvoiceJournal(saleInvoice, journal); }); return Promise.all([ journal.deleteEntries(), @@ -142,4 +84,66 @@ export default class SaleInvoicesCost { journal.saveBalance(), ]); } + + /** + * Writes journal entries for given sale invoice. + * @param {ISaleInvoice} saleInvoice + * @param {JournalPoster} journal + */ + static 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); + + 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); + } + }); + } } \ No newline at end of file diff --git a/server/src/system/models/Subscriptions/Voucher.ts b/server/src/system/models/Subscriptions/Voucher.ts index 4e0a8f6d2..1178e362f 100644 --- a/server/src/system/models/Subscriptions/Voucher.ts +++ b/server/src/system/models/Subscriptions/Voucher.ts @@ -27,7 +27,6 @@ export default class Voucher extends mixin(SystemModel) { filterActiveVoucher(query) { query.where('disabled', false); query.where('used', false); - query.where('sent', false); }, // Find voucher by its code or id. diff --git a/server/webpack.config.js b/server/webpack.config.js index 14403ae0f..86374706e 100644 --- a/server/webpack.config.js +++ b/server/webpack.config.js @@ -8,7 +8,6 @@ function resolve(dir) { module.exports = { mode: 'development', entry: [ - '@babel/plugin-transform-runtime', '@/server.js', ], target: 'node', @@ -43,11 +42,6 @@ module.exports = { // // emitWarning: !config.dev.showEslintErrorsInOverlay // }, // }, - { - use: 'babel-loader', - exclude: /(node_modules)/, - test: /\.js$/, - }, { test: /\.tsx?$/, use: 'ts-loader',