From 42569c89e4605a909db6cd54a1aaa8c579f4c115 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 11 Aug 2020 01:00:33 +0200 Subject: [PATCH] fix: Graph fetch relations with sales & purchases models. feat: Inventory tracker algorithm lots with FIFO or LIFO cost method. --- ...251_create_inventory_transactions_table.js | 6 +- ...create_inventory_cost_lot_tracker_table.js | 20 +++ server/src/http/controllers/Ping.ts | 62 ++++++++ .../http/controllers/Sales/PaymentReceives.js | 3 + .../http/controllers/Sales/SalesEstimates.js | 1 + .../http/controllers/Sales/SalesInvoices.js | 3 + .../http/controllers/Sales/SalesReceipts.js | 2 + server/src/http/index.js | 2 + server/src/interfaces/InventoryTransaction.ts | 23 +++ server/src/models/Bill.js | 9 +- server/src/models/BillPayment.js | 22 +-- server/src/models/InventoryLotCostTracker.js | 18 +++ server/src/models/PaymentReceive.js | 33 ++++ server/src/models/SaleEstimate.js | 13 ++ server/src/models/SaleInvoice.js | 22 ++- server/src/models/SaleReceipt.js | 38 ++++- server/src/services/Inventory/Inventory.js | 16 -- server/src/services/Inventory/Inventory.ts | 149 ++++++++++++++++++ .../Inventory/InventoryCostLotTracker.js | 11 ++ server/src/services/Purchases/BillPayments.js | 2 +- server/src/services/Purchases/Bills.js | 65 +++----- server/src/services/Sales/SalesEstimate.ts | 5 +- server/src/services/Sales/SalesInvoices.ts | 88 ++++++++++- server/tests/routes/bills.test.js | 4 +- server/tests/routes/sales_estimates.test.js | 2 +- 25 files changed, 526 insertions(+), 93 deletions(-) create mode 100644 server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js create mode 100644 server/src/http/controllers/Ping.ts create mode 100644 server/src/interfaces/InventoryTransaction.ts create mode 100644 server/src/models/InventoryLotCostTracker.js delete mode 100644 server/src/services/Inventory/Inventory.js create mode 100644 server/src/services/Inventory/Inventory.ts create mode 100644 server/src/services/Inventory/InventoryCostLotTracker.js 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 7f7957741..c004fe02f 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -3,16 +3,16 @@ exports.up = function(knex) { return knex.schema.createTable('inventory_transactions', table => { table.increments('id'); table.date('date'); + table.string('direction'); + table.integer('item_id'); table.integer('quantity'); table.decimal('rate', 13, 3); - table.integer('remaining'); - + table.string('transaction_type'); table.integer('transaction_id'); - table.integer('inventory_transaction_id'); 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 new file mode 100644 index 000000000..676799164 --- /dev/null +++ b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js @@ -0,0 +1,20 @@ + +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'); + table.decimal('rate', 13, 3); + table.integer('remaining'); + + table.string('transaction_type'); + table.integer('transaction_id'); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('inventory_cost_lot_tracker'); +}; diff --git a/server/src/http/controllers/Ping.ts b/server/src/http/controllers/Ping.ts new file mode 100644 index 000000000..8bf3003bb --- /dev/null +++ b/server/src/http/controllers/Ping.ts @@ -0,0 +1,62 @@ +import { Router, Request, Response } from 'express'; +import InventoryService from '@/services/Inventory/Inventory'; + +export default class Ping { + + /** + * Router constur + */ + static router() { + const router = Router(); + + router.get( + '/', + this.ping, + ); + return router; + } + + /** + * + * @param {Request} req + * @param {Response} res + */ + static async ping(req: Request, res: Response) { + + const result = await InventoryService.trackingInventoryLotsCost([ + { + id: 1, + date: '2020-02-02', + direction: 'IN', + itemId: 1, + quantity: 100, + rate: 10, + transactionType: 'Bill', + transactionId: 1, + remaining: 100, + }, + { + id: 2, + date: '2020-02-02', + direction: 'OUT', + itemId: 1, + quantity: 80, + rate: 10, + transactionType: 'SaleInvoice', + transactionId: 1, + }, + { + id: 3, + date: '2020-02-02', + direction: 'OUT', + itemId: 2, + quantity: 500, + rate: 10, + transactionType: 'SaleInvoice', + transactionId: 2, + }, + ]); + + return res.status(200).send({ id: 1231231 }); + } +} \ No newline at end of file diff --git a/server/src/http/controllers/Sales/PaymentReceives.js b/server/src/http/controllers/Sales/PaymentReceives.js index 632eadc5f..973eeb00e 100644 --- a/server/src/http/controllers/Sales/PaymentReceives.js +++ b/server/src/http/controllers/Sales/PaymentReceives.js @@ -415,6 +415,9 @@ export default class PaymentReceivesController extends BaseController { return res.status(400).send({ errors: errorReasons }); } const paymentReceives = await PaymentReceive.query().onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); + dynamicListing.buildQuery()(builder); return builder; }).pagination(filter.page - 1, filter.page_size); diff --git a/server/src/http/controllers/Sales/SalesEstimates.js b/server/src/http/controllers/Sales/SalesEstimates.js index 3d1d4b38f..3885aa8d0 100644 --- a/server/src/http/controllers/Sales/SalesEstimates.js +++ b/server/src/http/controllers/Sales/SalesEstimates.js @@ -326,6 +326,7 @@ export default class SalesEstimatesController extends BaseController { const salesEstimates = await SaleEstimate.query().onBuild((builder) => { dynamicListing.buildQuery()(builder); + builder.withGraphFetched('customer'); return builder; }).pagination(filter.page - 1, filter.page_size); diff --git a/server/src/http/controllers/Sales/SalesInvoices.js b/server/src/http/controllers/Sales/SalesInvoices.js index efab8d8bd..23d129dbb 100644 --- a/server/src/http/controllers/Sales/SalesInvoices.js +++ b/server/src/http/controllers/Sales/SalesInvoices.js @@ -262,6 +262,9 @@ export default class SaleInvoicesController { const storedSaleInvoice = await SaleInvoiceService.createSaleInvoice( saleInvoice ); + + // InventoryService.trackingInventoryLotsCost(); + return res.status(200).send({ id: storedSaleInvoice.id }); } diff --git a/server/src/http/controllers/Sales/SalesReceipts.js b/server/src/http/controllers/Sales/SalesReceipts.js index 1fdd3267a..09a6ba3df 100644 --- a/server/src/http/controllers/Sales/SalesReceipts.js +++ b/server/src/http/controllers/Sales/SalesReceipts.js @@ -313,6 +313,8 @@ export default class SalesReceiptsController { errorReasons.push(...errors); } const salesReceipts = await SaleReceipt.query().onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); builder.withGraphFetched('entries'); dynamicListing.buildQuery()(builder); }).pagination(filter.page - 1, filter.page_size); diff --git a/server/src/http/index.js b/server/src/http/index.js index 6a7c005cd..d78c5c37a 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -22,11 +22,13 @@ import ExchangeRates from '@/http/controllers/ExchangeRates'; import Media from '@/http/controllers/Media'; import JWTAuth from '@/http/middleware/jwtAuth'; import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import Ping from '@/http/controllers/Ping'; export default (app) => { // app.use('/api/oauth2', OAuth2.router()); app.use('/api/auth', Authentication.router()); app.use('/api/invite', InviteUsers.router()); + app.use('/api/ping', Ping.router()); const dashboard = express.Router(); diff --git a/server/src/interfaces/InventoryTransaction.ts b/server/src/interfaces/InventoryTransaction.ts new file mode 100644 index 000000000..fd4a45822 --- /dev/null +++ b/server/src/interfaces/InventoryTransaction.ts @@ -0,0 +1,23 @@ + + +export interface IInventoryTransaction { + id?: number, + date: Date, + direction: string, + itemId: number, + quantity: number, + rate: number, + transactionType: string, + transactionId: string, +}; + +export interface IInventoryLotCost { + id?: number, + date: Date, + direction: string, + itemId: number, + rate: number, + remaining: number, + transactionType: string, + transactionId: string, +} \ No newline at end of file diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index 0586f6897..94801da06 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -44,9 +44,6 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { const ItemEntry = require('@/models/ItemEntry'); return { - /** - * - */ vendor: { relation: Model.BelongsToOneRelation, modelClass: this.relationBindKnex(Vendor.default), @@ -56,9 +53,6 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { }, }, - /** - * - */ entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(ItemEntry.default), @@ -66,6 +60,9 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { from: 'bills.id', to: 'items_entries.referenceId', }, + filter(builder) { + builder.where('reference_type', 'SaleReceipt'); + }, }, }; } diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 2ca657e8c..585e7fa1a 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -24,13 +24,11 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { */ static get relationMappings() { const BillPaymentEntry = require('@/models/BillPaymentEntry'); + const AccountTransaction = require('@/models/AccountTransaction'); const Vendor = require('@/models/Vendor'); const Account = require('@/models/Account'); return { - /** - * - */ entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(BillPaymentEntry.default), @@ -40,9 +38,6 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { }, }, - /** - * - */ vendor: { relation: Model.BelongsToOneRelation, modelClass: this.relationBindKnex(Vendor.default), @@ -52,9 +47,6 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { }, }, - /** - * - */ paymentAccount: { relation: Model.BelongsToOneRelation, modelClass: this.relationBindKnex(Account.default), @@ -63,6 +55,18 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { to: 'accounts.id', }, }, + + transactions: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(AccountTransaction.default), + join: { + from: 'bills_payments.id', + to: 'accounts_transactions.referenceId' + }, + filter(builder) { + builder.where('reference_type', 'BillPayment'); + }, + } }; } } diff --git a/server/src/models/InventoryLotCostTracker.js b/server/src/models/InventoryLotCostTracker.js new file mode 100644 index 000000000..3ead24f31 --- /dev/null +++ b/server/src/models/InventoryLotCostTracker.js @@ -0,0 +1,18 @@ +import { Model } from 'objection'; +import TenantModel from '@/models/TenantModel'; + +export default class InventoryLotCostTracker extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'inventory_cost_lot_tracker'; + } + + /** + * Model timestamps. + */ + static get timestamps() { + return []; + } +} diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index 2ef72cc7b..8d76b3202 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -24,8 +24,29 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel]) */ static get relationMappings() { const PaymentReceiveEntry = require('@/models/PaymentReceiveEntry'); + const AccountTransaction = require('@/models/AccountTransaction'); + const Customer = require('@/models/Customer'); + const Account = require('@/models/Account'); return { + customer: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Customer.default), + join: { + from: 'payment_receives.customerId', + to: 'customers.id', + }, + }, + + depositAccount: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Account.default), + join: { + from: 'payment_receives.depositAccountId', + to: 'accounts.id', + }, + }, + entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(PaymentReceiveEntry.default), @@ -34,6 +55,18 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel]) to: 'payment_receives_entries.paymentReceiveId', }, }, + + transactions: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(AccountTransaction.default), + join: { + from: 'payment_receives.id', + to: 'accounts_transactions.referenceId' + }, + filter(builder) { + builder.where('reference_type', 'PaymentReceive'); + }, + } }; } } diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index 09c9d9681..c17f562b2 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -23,8 +23,18 @@ export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) { */ static get relationMappings() { const ItemEntry = require('@/models/ItemEntry'); + const Customer = require('@/models/Customer'); return { + customer: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Customer.default), + join: { + from: 'sales_estimates.customerId', + to: 'customers.id', + }, + }, + entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(ItemEntry.default), @@ -32,6 +42,9 @@ export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) { from: 'sales_estimates.id', to: 'items_entries.referenceId', }, + filter(builder) { + builder.where('reference_type', 'SaleEstimate'); + }, }, }; } diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 96114cff6..331187ada 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -37,13 +37,11 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { * Relationship mapping. */ static get relationMappings() { + const AccountTransaction = require('@/models/AccountTransaction'); const ItemEntry = require('@/models/ItemEntry'); const Customer = require('@/models/Customer'); return { - /** - * - */ entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(ItemEntry.default), @@ -51,11 +49,11 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { from: 'sales_invoices.id', to: 'items_entries.referenceId', }, + filter(builder) { + builder.where('reference_type', 'SaleInvoice'); + }, }, - /** - * - */ customer: { relation: Model.BelongsToOneRelation, modelClass: this.relationBindKnex(Customer.default), @@ -64,6 +62,18 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { to: 'customers.id', }, }, + + transactions: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(AccountTransaction.default), + join: { + from: 'sales_invoices.id', + to: 'accounts_transactions.referenceId' + }, + filter(builder) { + builder.where('reference_type', 'SaleInvoice'); + }, + } }; } diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index ac15cc1b4..160634847 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -23,17 +23,53 @@ export default class SaleReceipt extends mixin(TenantModel, [CachableModel]) { * Relationship mapping. */ static get relationMappings() { + const Customer = require('@/models/Customer'); + const Account = require('@/models/Account'); + const AccountTransaction = require('@/models/AccountTransaction'); const ItemEntry = require('@/models/ItemEntry'); return { - entries: { + customer: { relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Customer.default), + join: { + from: 'sales_receipts.customerId', + to: 'customers.id', + }, + }, + + depositAccount: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Account.default), + join: { + from: 'sales_receipts.depositAccountId', + to: 'accounts.id', + }, + }, + + entries: { + relation: Model.HasManyRelation, modelClass: this.relationBindKnex(ItemEntry.default), join: { from: 'sales_receipts.id', to: 'items_entries.referenceId', }, + filter(builder) { + builder.where('reference_type', 'SaleReceipt'); + }, }, + + transactions: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(AccountTransaction.default), + join: { + from: 'sales_receipts.id', + to: 'accounts_transactions.referenceId' + }, + filter(builder) { + builder.where('reference_type', 'SaleReceipt'); + }, + } }; } } diff --git a/server/src/services/Inventory/Inventory.js b/server/src/services/Inventory/Inventory.js deleted file mode 100644 index 90d09a638..000000000 --- a/server/src/services/Inventory/Inventory.js +++ /dev/null @@ -1,16 +0,0 @@ -import { InventoryTransaction } from "../../models"; - - -export default class InventoryService { - - async isInventoryPurchaseSold(transactionType, transactionId) { - - } - - static deleteTransactions(transactionId, transactionType) { - return InventoryTransaction.tenant().query() - .where('transaction_type', transactionType) - .where('transaction_id', transactionId) - .delete(); - } -} \ No newline at end of file diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts new file mode 100644 index 000000000..437a0f156 --- /dev/null +++ b/server/src/services/Inventory/Inventory.ts @@ -0,0 +1,149 @@ +import { InventoryTransaction, Item } from '@/models'; +import InventoryCostLotTracker from './InventoryCostLotTracker'; +import { IInventoryTransaction, IInventoryLotCost } from '@/interfaces/InventoryTransaction'; +import { IInventoryLotCost, IInventoryLotCost } from '../../interfaces/InventoryTransaction'; +import { pick } from 'lodash'; + +export default class InventoryService { + /** + * Records the inventory transactions. + * @param {Bill} bill + * @param {number} billId + */ + static async recordInventoryTransactions( + entries: [], + date: Date, + transactionType: string, + transactionId: number, + ) { + const storedOpers: any = []; + const entriesItemsIds = entries.map((e: any) => e.item_id); + const inventoryItems = await Item.tenant() + .query() + .whereIn('id', entriesItemsIds) + .where('type', 'inventory'); + + const inventoryItemsIds = inventoryItems.map((i) => i.id); + + // Filter the bill entries that have inventory items. + const inventoryEntries = entries.filter( + (entry) => inventoryItemsIds.indexOf(entry.item_id) !== -1 + ); + inventoryEntries.forEach((entry: any) => { + const oper = InventoryTransaction.tenant().query().insert({ + date, + + item_id: entry.item_id, + quantity: entry.quantity, + rate: entry.rate, + + transaction_type: transactionType, + transaction_id: transactionId, + }); + storedOpers.push(oper); + }); + return Promise.all(storedOpers); + } + + /** + * Deletes the given inventory transactions. + * @param {string} transactionType + * @param {number} transactionId + * @return {Promise} + */ + static deleteInventoryTransactions( + transactionId: number, + transactionType: string, + ) { + return InventoryTransaction.tenant().query() + .where('transaction_type', transactionType) + .where('transaction_id', transactionId) + .delete(); + } + + revertInventoryLotsCost(fromDate?: Date) { + + } + + /** + * Records the journal entries transactions. + * @param {IInventoryLotCost[]} inventoryTransactions - + * + */ + static async recordJournalEntries(inventoryLots: IInventoryLotCost[]) { + + } + + /** + * Tracking the given inventory transactions to lots costs transactions. + * @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions. + * @return {IInventoryLotCost[]} + */ + static async trackingInventoryLotsCost(inventoryTransactions: IInventoryTransaction[]) { + // Collect cost lots transactions to insert them to the storage in bulk. + const costLotsTransactions: IInventoryLotCost[] = []; + + // Collect inventory transactions by item id. + const inventoryByItem: any = {}; + // Collection `IN` inventory tranaction by transaction id. + const inventoryINTrans: any = {}; + + inventoryTransactions.forEach((transaction: IInventoryTransaction) => { + const { itemId, id } = transaction; + (inventoryByItem[itemId] || (inventoryByItem[itemId] = [])); + + const commonLotTransaction: IInventoryLotCost = { + ...pick(transaction, [ + 'date', 'rate', 'itemId', 'quantity', + 'direction', 'transactionType', 'transactionId', + ]), + }; + // Record inventory `IN` cost lot transaction. + if (transaction.direction === 'IN') { + inventoryByItem[itemId].push(id); + inventoryINTrans[id] = { + ...commonLotTransaction, + remaining: commonLotTransaction.quantity, + }; + costLotsTransactions.push(inventoryINTrans[id]); + + // Record inventory 'OUT' cost lots from 'IN' transactions. + } else if (transaction.direction === 'OUT') { + let invRemaining = transaction.quantity; + + inventoryByItem?.[itemId]?.forEach(( + _invTransactionId: number, + index: number, + ) => { + const _invINTransaction = inventoryINTrans[_invTransactionId]; + + // Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction. + const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0; + const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining; + + _invINTransaction.remaining = Math.max( + _invINTransaction.remaining - decrement, 0, + ); + invRemaining = Math.max(invRemaining - decrement, 0); + + costLotsTransactions.push({ + ...commonLotTransaction, + quantity: decrement, + }); + // Pop the 'IN' lots that has zero remaining. + if (_invINTransaction.remaining === 0) { + inventoryByItem?.[itemId].splice(index, 1); + } + }); + if (invRemaining > 0) { + costLotsTransactions.push({ + ...commonLotTransaction, + quantity: invRemaining, + }); + } + } + }); + return costLotsTransactions; + } + +} \ No newline at end of file diff --git a/server/src/services/Inventory/InventoryCostLotTracker.js b/server/src/services/Inventory/InventoryCostLotTracker.js new file mode 100644 index 000000000..a689ccc82 --- /dev/null +++ b/server/src/services/Inventory/InventoryCostLotTracker.js @@ -0,0 +1,11 @@ + +export default class InventoryCostLotTracker { + + recalcInventoryLotsCost(inventoryTransactions) { + + } + + deleteTransactionsFromDate(fromDate) { + + } +} \ No newline at end of file diff --git a/server/src/services/Purchases/BillPayments.js b/server/src/services/Purchases/BillPayments.js index a72727f93..a5dc9f1a8 100644 --- a/server/src/services/Purchases/BillPayments.js +++ b/server/src/services/Purchases/BillPayments.js @@ -96,7 +96,7 @@ export default class BillPaymentsService { * @param {IBillPayment} oldBillPayment */ static async editBillPayment(billPaymentId, billPayment, oldBillPayment) { - const amount = sumBy(bilPayment.entries, 'payment_amount'); + const amount = sumBy(billPayment.entries, 'payment_amount'); const updateBillPayment = await BillPayment.tenant() .query() .where('id', billPaymentId) diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index 9c9019b9b..086ff0986 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -54,19 +54,23 @@ export default class BillsService { }); saveEntriesOpers.push(oper); }); - // Increment vendor balance. + // Increments vendor balance. const incrementOper = Vendor.changeBalance(bill.vendor_id, amount); - + + // // Rewrite the inventory transactions for inventory items. + // const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( + // bill.entries, 'Bill', billId, + // ); await Promise.all([ ...saveEntriesOpers, incrementOper, - this.recordInventoryTransactions(bill, storedBill.id), + // this.recordInventoryTransactions(bill, storedBill.id), this.recordJournalTransactions({ ...bill, id: storedBill.id }), + // writeInvTransactionsOper, ]); return storedBill; } - /** * Edits details of the given bill id with associated entries. * @@ -112,51 +116,23 @@ export default class BillsService { amount, oldBill.amount, ); - // Record bill journal transactions. - const recordTransactionsOper = this.recordJournalTransactions(bill, billId); - + // // Deletes the old inventory transactions. + // const deleteInvTransactionsOper = InventorySevice.deleteInventoryTransactions( + // billId, 'Bill', + // ); + // // Re-write the inventory transactions for inventory items. + // const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( + // bill.entries, 'Bill', billId, + // ); await Promise.all([ patchEntriesOper, recordTransactionsOper, changeVendorBalanceOper, + // deleteInvTransactionsOper, + // writeInvTransactionsOper, ]); } - /** - * Records inventory transactions. - * @param {IBill} bill - - * @return {void} - */ - static async recordInventoryTransactions(bill, billId) { - const storeInventoryTransactions = []; - const entriesItemsIds = bill.entries.map((e) => e.item_id); - const inventoryItems = await Item.tenant() - .query() - .whereIn('id', entriesItemsIds) - .where('type', 'inventory'); - - const inventoryItemsIds = inventoryItems.map((i) => i.id); - const inventoryEntries = bill.entries.filter( - (entry) => inventoryItemsIds.indexOf(entry.item_id) !== -1 - ); - inventoryEntries.forEach((entry) => { - const oper = InventoryTransaction.tenant().query().insert({ - direction: 'IN', - date: bill.bill_date, - - item_id: entry.item_id, - quantity: entry.quantity, - rate: entry.rate, - remaining: entry.quantity, - - transaction_type: 'Bill', - transaction_id: billId, - }); - storeInventoryTransactions.push(oper); - }); - return Promise.all([...storeInventoryTransactions]); - } - /** * Records the bill journal transactions. * @async @@ -253,9 +229,8 @@ export default class BillsService { 'Bill' ); // Delete bill associated inventory transactions. - const deleteInventoryTransOper = InventoryService.deleteTransactions( - billId, - 'Bill' + const deleteInventoryTransOper = InventoryService.deleteInventoryTransactions( + billId, 'Bill' ); // Revert vendor balance. const revertVendorBalance = Vendor.changeBalance(bill.vendorId, bill.amount * -1); diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index d4e2e7739..61a4514a3 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -98,8 +98,8 @@ export default class SaleEstimateService { */ static async isEstimateEntriesIDsExists(estimateId: number, estimate: any) { const estimateEntriesIds = estimate.entries - .filter((e) => e.id) - .map((e) => e.id); + .filter((e: any) => e.id) + .map((e: any) => e.id); const estimateEntries = await ItemEntry.tenant() .query() @@ -138,6 +138,7 @@ export default class SaleEstimateService { .query() .where('id', estimateId) .withGraphFetched('entries') + .withGraphFetched('customer') .first(); return estimate; diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index b0d7abaa3..1a87f27ce 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,7 +1,8 @@ -import { omit, sumBy, difference } from 'lodash'; +import { omit, sumBy, difference, chain, sum } from 'lodash'; import { SaleInvoice, AccountTransaction, + InventoryTransaction, Account, ItemEntry, Customer, @@ -9,6 +10,7 @@ import { import JournalPoster from '@/services/Accounting/JournalPoster'; import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import CustomerRepository from '@/repositories/CustomerRepository'; +import moment from 'moment'; /** * Sales invoices service @@ -48,10 +50,40 @@ export default class SaleInvoicesService { saleInvoice.customer_id, balance, ); - await Promise.all([...opers, incrementOper]); + // Records the inventory transactions for inventory items. + const recordInventoryTransOpers = this.recordInventoryTransactions( + saleInvoice, storedInvoice.id + ); + // Await all async operations. + await Promise.all([ + ...opers, + incrementOper, + recordInventoryTransOpers, + ]); return storedInvoice; } + /** + * Records the inventory items transactions. + * @param {SaleInvoice} saleInvoice - + * @param {number} saleInvoiceId - + * @return {Promise} + */ + static async recordInventoryTransactions(saleInvoice, saleInvoiceId) { + + } + + /** + * Records the sale invoice journal entries and calculate the items cost + * based on the given cost method in the options FIFO, LIFO or average cost rate. + * + * @param {SaleInvoice} saleInvoice - + * @param {Array} inventoryTransactions - + */ + static async recordJournalEntries(saleInvoice: any, inventoryTransactions: array[]) { + + } + /** * Edit the given sale invoice. * @async @@ -94,6 +126,48 @@ export default class SaleInvoicesService { ]); } + async recalcInventoryTransactionsCost(inventoryTransactions: array) { + const inventoryTransactionsMap = this.mapInventoryTransByItem(inventoryTransactions); + + + } + + /** + * Deletes the inventory transactions. + * @param {string} transactionType + * @param {number} transactionId + */ + static async revertInventoryTransactions(inventoryTransactions: array) { + const opers = []; + + inventoryTransactions.forEach((trans: any) => { + switch(trans.direction) { + case 'OUT': + if (trans.inventoryTransactionId) { + const revertRemaining = InventoryTransaction.tenant() + .query() + .where('id', trans.inventoryTransactionId) + .where('direction', 'OUT') + .increment('remaining', trans.quanitity); + + opers.push(revertRemaining); + } + break; + case 'IN': + const removeRelationOper = InventoryTransaction.tenant() + .query() + .where('inventory_transaction_id', trans.id) + .where('direction', 'IN') + .update({ + inventory_transaction_id: null, + }); + opers.push(removeRelationOper); + break; + } + }); + return Promise.all(opers); + } + /** * Deletes the given sale invoice with associated entries * and journal transactions. @@ -126,10 +200,19 @@ export default class SaleInvoicesService { journal.loadEntries(invoiceTransactions); journal.removeEntries(); + const inventoryTransactions = await InventoryTransaction.tenant() + .query() + .where('transaction_type', 'SaleInvoice') + .where('transaction_id', saleInvoiceId); + + // Revert inventory transactions. + const revertInventoryTransactionsOper = this.revertInventoryTransactions(inventoryTransactions); + await Promise.all([ journal.deleteEntries(), journal.saveBalance(), revertCustomerBalanceOper, + revertInventoryTransactionsOper, ]); } @@ -152,6 +235,7 @@ export default class SaleInvoicesService { return SaleInvoice.tenant().query() .where('id', saleInvoiceId) .withGraphFetched('entries') + .withGraphFetched('customer') .first(); } diff --git a/server/tests/routes/bills.test.js b/server/tests/routes/bills.test.js index a8b681b50..1e5ad10ca 100644 --- a/server/tests/routes/bills.test.js +++ b/server/tests/routes/bills.test.js @@ -8,7 +8,7 @@ import { loginRes } from '~/dbInit'; -describe('route: `/api/purchases/bills`', () => { +describe.only('route: `/api/purchases/bills`', () => { describe('POST: `/api/purchases/bills`', () => { it('Should `bill_number` be required.', async () => { const res = await request() @@ -207,6 +207,8 @@ describe('route: `/api/purchases/bills`', () => { expect(res.status).equals(200); }); + + }); describe('DELETE: `/api/purchases/bills/:id`', () => { diff --git a/server/tests/routes/sales_estimates.test.js b/server/tests/routes/sales_estimates.test.js index 81ab03577..54e20241a 100644 --- a/server/tests/routes/sales_estimates.test.js +++ b/server/tests/routes/sales_estimates.test.js @@ -426,7 +426,7 @@ describe('route: `/sales/estimates`', () => { describe('GET: `/sales/estimates`', () => { - it.only('Should retrieve sales estimates.', async () => { + it('Should retrieve sales estimates.', async () => { const res = await request() .get('/api/sales/estimates') .set('x-access-token', loginRes.body.token)