From 52d01b4ed8c072e632b6bda1c478cac6cf762fea Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 19 Aug 2020 00:13:53 +0200 Subject: [PATCH] fix: Date format in sales/purchases APIs. fix: Algorithm FIFO cost calculate method. --- server/package.json | 2 +- ...200715193633_create_sale_invoices_table.js | 1 + ...s => 20200719152005_create_bills_table.js} | 1 + ...251_create_inventory_transactions_table.js | 2 + ...create_inventory_cost_lot_tracker_table.js | 2 +- .../src/http/middleware/prettierMiddleware.ts | 35 ++++++++ server/src/jobs/ComputeItemCost.ts | 11 ++- server/src/models/InventoryTransaction.js | 2 +- server/src/models/index.js | 2 + .../services/Accounting/JournalCommands.ts | 4 - server/src/services/Inventory/Inventory.ts | 52 ++++++++---- .../Inventory/InventoryCostLotTracker.ts | 20 +++-- server/src/services/Purchases/BillPayments.js | 27 ++++--- server/src/services/Purchases/Bills.js | 80 ++++++++++++------- server/src/services/Sales/PaymentsReceives.ts | 5 +- server/src/services/Sales/SalesEstimate.ts | 57 +++++++------ server/src/services/Sales/SalesInvoices.ts | 62 +++++++++++--- server/src/services/Sales/SalesReceipts.ts | 43 ++++++---- server/src/utils/index.js | 12 +++ server/tests/routes/accounts.test.js | 2 - server/tests/routes/views.test.js | 2 - 21 files changed, 291 insertions(+), 133 deletions(-) rename server/src/database/migrations/{20200719152005_reate_bills_table.js => 20200719152005_create_bills_table.js} (93%) create mode 100644 server/src/http/middleware/prettierMiddleware.ts diff --git a/server/package.json b/server/package.json index 48ed24091..b29facc3b 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "build": "webpack", "start": "npm-run-all --parallel watch:server watch:build", "watch:build": "webpack --watch", - "watch:server": "nodemon \"./dist/bundle.js\" --watch \"./dist\" ", + "watch:server": "nodemon --inspect=\"9229\" \"./dist/bundle.js\" --watch \"./dist\" ", "test": "cross-env NODE_ENV=test mocha-webpack --webpack-config webpack.config.js \"tests/**/*.test.js\"", "test:watch": "cross-env NODE_ENV=test mocha-webpack --watch --webpack-config webpack.config.js --timeout=30000 tests/**/*.test.js" }, diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js index bd66a66a9..7f5b394b8 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -15,6 +15,7 @@ exports.up = function(knex) { table.decimal('balance', 13, 3); table.decimal('payment_amount', 13, 3); + table.string('inv_lot_number'); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200719152005_reate_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js similarity index 93% rename from server/src/database/migrations/20200719152005_reate_bills_table.js rename to server/src/database/migrations/20200719152005_create_bills_table.js index 007f55b4d..d6f3c2f7a 100644 --- a/server/src/database/migrations/20200719152005_reate_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -13,6 +13,7 @@ exports.up = function(knex) { table.decimal('amount', 13, 3).defaultTo(0); table.decimal('payment_amount', 13, 3).defaultTo(0); + table.string('inv_lot_number'); 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 ae1fa39b9..6555220c8 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -9,6 +9,8 @@ exports.up = function(knex) { table.integer('item_id').unsigned(); table.integer('quantity').unsigned(); table.decimal('rate', 13, 3).unsigned(); + + table.integer('lot_number'); table.string('transaction_type'); table.integer('transaction_id'); 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 26c72be2b..1318515e3 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 @@ -10,7 +10,7 @@ exports.up = function(knex) { table.integer('quantity').unsigned(); table.decimal('rate', 13, 3); table.integer('remaining'); - table.string('lot_number'); + table.integer('lot_number'); table.string('transaction_type'); table.integer('transaction_id'); diff --git a/server/src/http/middleware/prettierMiddleware.ts b/server/src/http/middleware/prettierMiddleware.ts new file mode 100644 index 000000000..c7a6b7814 --- /dev/null +++ b/server/src/http/middleware/prettierMiddleware.ts @@ -0,0 +1,35 @@ +import { camelCase, snakeCase } from 'lodash'; + +/** + * create a middleware to change json format from snake case to camelcase in request + * then change back to snake case in response + * + */ +export default function createMiddleware() { + return function (req, res, next) { + /** + * camelize req.body + */ + if (req.body && typeof req.body === 'object') { + req.body = camelCase(req.body); + } + + /** + * camelize req.query + */ + if (req.query && typeof req.query === 'object') { + req.query = camelCase(req.query); + } + + /** + * wrap res.json() + */ + const sendJson = res.json; + + res.json = (data) => { + return sendJson.call(res, snakeCase(data)); + } + + return next(); + } +} \ No newline at end of file diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index c471fb48b..09b069e03 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -6,7 +6,14 @@ export default class ComputeItemCostJob { const Logger = Container.get('logger'); const { startingDate, itemId, costMethod } = job.attrs.data; - await InventoryService.computeItemCost(startingDate, itemId, costMethod); - done(); + try { + await InventoryService.computeItemCost(startingDate, itemId, costMethod); + Logger.log(`Compute item cost: ${job.attrs.data}`); + done(); + } catch(e) { + Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`); + done(e); + } + } } diff --git a/server/src/models/InventoryTransaction.js b/server/src/models/InventoryTransaction.js index 3b1a7fc7d..d108dd3a6 100644 --- a/server/src/models/InventoryTransaction.js +++ b/server/src/models/InventoryTransaction.js @@ -12,7 +12,7 @@ export default class InventoryTransaction extends TenantModel { /** * Model timestamps. */ - static get timestamps() { + get timestamps() { return ['createdAt', 'updatedAt']; } diff --git a/server/src/models/index.js b/server/src/models/index.js index ddd03d173..cc025cbb0 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -1,5 +1,6 @@ import Customer from './Customer'; import Vendor from './Vendor'; +import Option from './Option'; import SaleEstimate from './SaleEstimate'; import SaleEstimateEntry from './SaleEstimateEntry'; import SaleReceipt from './SaleReceipt'; @@ -44,4 +45,5 @@ export { InventoryTransaction, InventoryLotCostTracker, AccountType, + Option, }; \ No newline at end of file diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index ecac13c90..52d4709f1 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -45,8 +45,6 @@ export default class JournalCommands{ .map((groupedTrans: IInventoryTransaction[], transType: string) => [groupedTrans, transType]) .value(); - console.log(groupedInvTransactions); - return Promise.all( groupedInvTransactions.map(async (grouped: [IInventoryTransaction[], string]) => { const [invTransGroup, referenceType] = grouped; @@ -58,8 +56,6 @@ export default class JournalCommands{ .whereIn('reference_id', referencesIds) .withGraphFetched('account.type'); - console.log(_transactions, referencesIds); - if (_transactions.length > 0) { this.journal.loadEntries(_transactions); this.journal.removeEntries(_transactions.map((t: any) => t.id)); diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index fe7e2290b..381b37a4f 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -1,9 +1,11 @@ import { InventoryTransaction, - Item + Item, + Option, } from '@/models'; import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost'; import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker'; +import { option } from 'commander'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; @@ -38,10 +40,7 @@ export default class InventoryService { */ static async recordInventoryTransactions( entries: [], - date: Date, - transactionType: string, - transactionId: number, - direction: string, + deleteOld: boolean, ) { const storedOpers: any = []; const entriesItemsIds = entries.map((e: any) => e.item_id); @@ -56,19 +55,22 @@ export default class InventoryService { const inventoryEntries = entries.filter( (entry: any) => inventoryItemsIds.indexOf(entry.item_id) !== -1 ); - inventoryEntries.forEach((entry: any) => { + inventoryEntries.forEach(async (entry: any) => { + if (deleteOld) { + await this.deleteInventoryTransactions( + entry.transactionId, + entry.transactionType, + ); + } const oper = InventoryTransaction.tenant().query().insert({ - date, - direction, - item_id: entry.item_id, - quantity: entry.quantity, - rate: entry.rate, - transaction_type: transactionType, - transaction_id: transactionId, + ...entry, + lotNumber: entry.lotNumber, }); storedOpers.push(oper); - }); - return Promise.all(storedOpers); + }); + return Promise.all([ + ...storedOpers, + ]); } /** @@ -90,4 +92,24 @@ export default class InventoryService { revertInventoryLotsCost(fromDate?: Date) { } + + /** + * Retrieve the lot number after the increment. + */ + static async nextLotNumber() { + const LOT_NUMBER_KEY = 'lot_number_increment'; + const effectRows = await Option.tenant().query() + .where('key', LOT_NUMBER_KEY) + .increment('value', 1); + + if (effectRows) { + await Option.tenant().query() + .insert({ + key: LOT_NUMBER_KEY, + value: 1, + }); + } + const options = await Option.tenant().query(); + return options.getMeta(LOT_NUMBER_KEY, 1); + } } \ No newline at end of file diff --git a/server/src/services/Inventory/InventoryCostLotTracker.ts b/server/src/services/Inventory/InventoryCostLotTracker.ts index 4e64b3f40..db4701680 100644 --- a/server/src/services/Inventory/InventoryCostLotTracker.ts +++ b/server/src/services/Inventory/InventoryCostLotTracker.ts @@ -1,5 +1,4 @@ import { omit, pick, chain } from 'lodash'; -import uniqid from 'uniqid'; import { InventoryTransaction, InventoryLotCostTracker, @@ -62,6 +61,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { .query() .where('date', '>=', this.startingDate) .orderBy('date', 'ASC') + .orderBy('lot_number', 'ASC') .where('item_id', this.itemId) .withGraphFetched('item'); @@ -70,6 +70,7 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { .query() .where('date', '<', this.startingDate) .orderBy('date', 'ASC') + .orderBy('lot_number', 'ASC') .where('item_id', this.itemId) .where('direction', 'IN') .whereNot('remaining', 0); @@ -267,17 +268,16 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { ...commonLotTransaction, decrement: 0, remaining: commonLotTransaction.remaining || commonLotTransaction.quantity, - lotNumber: commonLotTransaction.lotNumber || uniqid.time(), }; costLotsTransactions.push(inventoryINTrans[id]); // Record inventory 'OUT' cost lots from 'IN' transactions. } else if (transaction.direction === 'OUT') { let invRemaining = transaction.quantity; + const idsShouldDel: number[] = []; inventoryByItem?.[itemId]?.some(( _invTransactionId: number, - index: number, ) => { const _invINTransaction = inventoryINTrans[_invTransactionId]; if (invRemaining <= 0) { return true; } @@ -285,22 +285,23 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { // 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; + const maxDecrement = Math.min(decrement, invRemaining); - _invINTransaction.decrement += decrement; + _invINTransaction.decrement += maxDecrement; _invINTransaction.remaining = Math.max( - _invINTransaction.remaining - decrement, + _invINTransaction.remaining - maxDecrement, 0, ); - invRemaining = Math.max(invRemaining - decrement, 0); + invRemaining = Math.max(invRemaining - maxDecrement, 0); costLotsTransactions.push({ ...commonLotTransaction, - quantity: decrement, + quantity: maxDecrement, lotNumber: _invINTransaction.lotNumber, }); // Pop the 'IN' lots that has zero remaining. if (_invINTransaction.remaining === 0) { - inventoryByItem?.[itemId].splice(index, 1); + idsShouldDel.push(_invTransactionId); } return false; }); @@ -310,6 +311,9 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod { quantity: invRemaining, }); } + // Remove the IN transactions that has zero remaining amount. + inventoryByItem[itemId] = inventoryByItem?.[itemId] + ?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1); } }); return costLotsTransactions; diff --git a/server/src/services/Purchases/BillPayments.js b/server/src/services/Purchases/BillPayments.js index a5dc9f1a8..65dd1e117 100644 --- a/server/src/services/Purchases/BillPayments.js +++ b/server/src/services/Purchases/BillPayments.js @@ -14,6 +14,7 @@ import AccountsService from '@/services/Accounts/AccountsService'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; import JournalPosterService from '@/services/Sales/JournalPosterService'; +import { formatDateFields } from '@/utils'; /** * Bill payments service. @@ -32,14 +33,16 @@ export default class BillPaymentsService { * - Decrement the vendor balance. * - Records payment journal entries. * - * @param {IBillPayment} billPayment + * @param {BillPaymentDTO} billPayment */ - static async createBillPayment(billPayment) { - const amount = sumBy(billPayment.entries, 'payment_amount'); + static async createBillPayment(billPaymentDTO) { + const billPayment = { + amount: sumBy(billPaymentDTO.entries, 'payment_amount'), + ...formatDateFields(billPaymentDTO, ['payment_date']), + } const storedBillPayment = await BillPayment.tenant() .query() .insert({ - amount, ...omit(billPayment, ['entries']), }); const storeOpers = []; @@ -62,7 +65,7 @@ export default class BillPaymentsService { // Decrement the vendor balance after bills payments. const vendorDecrementOper = Vendor.changeBalance( billPayment.vendor_id, - amount * -1, + billPayment.amount * -1, ); // Records the journal transactions after bills payment // and change diff acoount balance. @@ -92,24 +95,24 @@ export default class BillPaymentsService { * - Update the diff bill payment amount. * * @param {Integer} billPaymentId - * @param {IBillPayment} billPayment + * @param {BillPaymentDTO} billPayment * @param {IBillPayment} oldBillPayment */ - static async editBillPayment(billPaymentId, billPayment, oldBillPayment) { - const amount = sumBy(billPayment.entries, 'payment_amount'); + static async editBillPayment(billPaymentId, billPaymentDTO, oldBillPayment) { + const billPayment = { + amount: sumBy(billPaymentDTO.entries, 'payment_amount'), + ...formatDateFields(billPaymentDTO, ['payment_date']), + }; const updateBillPayment = await BillPayment.tenant() .query() .where('id', billPaymentId) .update({ - amount, ...omit(billPayment, ['entries']), }); const opers = []; - const entriesHasIds = billpayment.entries.filter((i) => i.id); + const entriesHasIds = billPayment.entries.filter((i) => i.id); const entriesHasNoIds = billPayment.entries.filter((e) => !e.id); - const entriesIds = entriesHasIds.map((e) => e.id); - const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( oldBillPayment.entries, entriesHasIds diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index 58efb39ba..b115b732c 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -1,4 +1,4 @@ -import { omit, sumBy } from 'lodash'; +import { omit, sumBy, pick } from 'lodash'; import moment from 'moment'; import { Container } from 'typedi'; import { @@ -7,7 +7,6 @@ import { Vendor, ItemEntry, Item, - InventoryTransaction, AccountTransaction, } from '@/models'; import JournalPoster from '@/services/Accounting/JournalPoster'; @@ -16,14 +15,16 @@ 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 { formatDateFields } from '@/utils'; /** * Vendor bills services. + * @service */ export default class BillsService { /** * Creates a new bill and stored it to the storage. - *| + * * Precedures. * ---- * - Insert bill transactions to the storage. @@ -35,16 +36,18 @@ export default class BillsService { * @param {IBill} bill - * @return {void} */ - static async createBill(bill) { - const agenda = Container.get('agenda'); - - const amount = sumBy(bill.entries, 'amount'); + static async createBill(billDTO) { + const invLotNumber = await InventoryService.nextLotNumber(); + const bill = { + ...formatDateFields(billDTO, ['bill_date', 'due_date']), + amount: sumBy(billDTO.entries, 'amount'), + invLotNumber: billDTO.invLotNumber || invLotNumber + }; const saveEntriesOpers = []; const storedBill = await Bill.tenant() .query() .insert({ - amount, ...omit(bill, ['entries']), }); bill.entries.forEach((entry) => { @@ -58,11 +61,11 @@ export default class BillsService { saveEntriesOpers.push(oper); }); // Increments vendor balance. - const incrementOper = Vendor.changeBalance(bill.vendor_id, amount); + const incrementOper = Vendor.changeBalance(bill.vendor_id, bill.amount); // Rewrite the inventory transactions for inventory items. - const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( - bill.entries, bill.bill_date, 'Bill', storedBill.id, 'IN', + const writeInvTransactionsOper = this.recordInventoryTransactions( + bill, storedBill.id ); // Writes the journal entries for the given bill transaction. const writeJEntriesOper = this.recordJournalTransactions({ @@ -75,7 +78,6 @@ export default class BillsService { writeInvTransactionsOper, writeJEntriesOper, ]); - // Schedule bill re-compute based on the item cost // method and starting date. await this.scheduleComputeItemsCost(bill); @@ -83,7 +85,14 @@ export default class BillsService { return storedBill; } - scheduleComputeItemCost(bill) { + /** + * 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 agenda = Container.get('agenda'); + return agenda.schedule('in 1 second', 'compute-item-cost', { startingDate: bill.bill_date || bill.billDate, itemId: bill.entries[0].item_id || bill.entries[0].itemId, @@ -91,6 +100,25 @@ export default class BillsService { }); } + /** + * Records the inventory transactions from the given bill input. + * @param {Bill} bill + * @param {number} billId + */ + static recordInventoryTransactions(bill, billId, override) { + const inventoryTransactions = bill.entries + .map((entry) => ({ + ...pick(entry, ['item_id', 'quantity', 'rate']), + lotNumber: bill.invLotNumber, + transactionType: 'Bill', + transactionId: billId, + direction: 'IN', + date: bill.bill_date, + })); + + return InventoryService.recordInventoryTransactions(inventoryTransactions, override); + } + /** * Edits details of the given bill id with associated entries. * @@ -106,19 +134,20 @@ export default class BillsService { * @param {Integer} billId - The given bill id. * @param {IBill} bill - The given new bill details. */ - static async editBill(billId, bill) { + static async editBill(billId, billDTO) { const oldBill = await Bill.tenant().query().findById(billId); - const amount = sumBy(bill.entries, 'amount'); - + const bill = { + ...formatDateFields(billDTO, ['bill_date', 'due_date']), + amount: sumBy(billDTO.entries, 'amount'), + invLotNumber: oldBill.invLotNumber, + }; // Update the bill transaction. const updatedBill = await Bill.tenant() .query() .where('id', billId) .update({ - amount, - ...omit(bill, ['entries']) + ...omit(bill, ['entries', 'invLotNumber']) }); - // Old stored entries. const storedEntries = await ItemEntry.tenant() .query() @@ -133,17 +162,12 @@ export default class BillsService { const changeVendorBalanceOper = Vendor.changeDiffBalance( bill.vendor_id, oldBill.vendorId, - amount, + bill.amount, oldBill.amount, ); // Re-write the inventory transactions for inventory items. - const writeInvTransactionsOper = InventoryService.recordInventoryTransactions( - bill.entries, bill.bill_date, 'Bill', billId, 'IN' - ); - // Delete bill associated inventory transactions. - const deleteInventoryTransOper = InventoryService.deleteInventoryTransactions( - billId, 'Bill' - ); + const writeInvTransactionsOper = this.recordInventoryTransactions(bill, billId, true); + // Writes the journal entries for the given bill transaction. const writeJEntriesOper = this.recordJournalTransactions({ id: billId, @@ -154,10 +178,8 @@ export default class BillsService { patchEntriesOper, changeVendorBalanceOper, writeInvTransactionsOper, - deleteInventoryTransOper, writeJEntriesOper, ]); - // Schedule sale invoice re-compute based on the item cost // method and starting date. await this.scheduleComputeItemsCost(bill); diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 1cd6ed9e9..5b26bcfae 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -15,6 +15,7 @@ import JournalPosterService from '@/services/Sales/JournalPosterService'; import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository'; import CustomerRepository from '@/repositories/CustomerRepository'; +import { formatDateFields } from '@/utils'; /** * Payment receive service. @@ -33,7 +34,7 @@ export default class PaymentReceiveService { .query() .insert({ amount: paymentAmount, - ...omit(paymentReceive, ['entries']), + ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), }); const storeOpers: Array = []; @@ -97,7 +98,7 @@ export default class PaymentReceiveService { .where('id', paymentReceiveId) .update({ amount: paymentAmount, - ...omit(paymentReceive, ['entries']), + ...formatDateFields(omit(paymentReceive, ['entries']), ['payment_date']), }); const opers = []; const entriesIds = paymentReceive.entries.filter((i: any) => i.id); diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index c74ab7691..7bd725335 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -2,21 +2,23 @@ import { omit, difference, sumBy, mixin } from 'lodash'; import moment from 'moment'; import { SaleEstimate, ItemEntry } from '@/models'; import HasItemsEntries from '@/services/Sales/HasItemsEntries'; +import { formatDateFields } from '@/utils'; export default class SaleEstimateService { /** * Creates a new estimate with associated entries. * @async - * @param {IEstimate} estimate + * @param {EstimateDTO} estimate * @return {void} */ - static async createEstimate(estimate: any) { - const amount = sumBy(estimate.entries, 'amount'); - + static async createEstimate(estimateDTO: any) { + const estimate = { + amount: sumBy(estimateDTO.entries, 'amount'), + ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), + }; const storedEstimate = await SaleEstimate.tenant() .query() .insert({ - amount, ...omit(estimate, ['entries']), }); const storeEstimateEntriesOpers: any[] = []; @@ -36,34 +38,21 @@ export default class SaleEstimateService { return storedEstimate; } - /** - * Deletes the given estimate id with associated entries. - * @async - * @param {IEstimate} estimateId - * @return {void} - */ - static async deleteEstimate(estimateId: number) { - await ItemEntry.tenant() - .query() - .where('reference_id', estimateId) - .where('reference_type', 'SaleEstimate') - .delete(); - await SaleEstimate.tenant().query().where('id', estimateId).delete(); - } - /** * Edit details of the given estimate with associated entries. * @async * @param {Integer} estimateId - * @param {IEstimate} estimate + * @param {EstimateDTO} estimate * @return {void} */ - static async editEstimate(estimateId: number, estimate: any) { - const amount = sumBy(estimate.entries, 'amount'); + static async editEstimate(estimateId: number, estimateDTO: any) { + const estimate = { + amount: sumBy(estimateDTO.entries, 'amount'), + ...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']), + }; const updatedEstimate = await SaleEstimate.tenant() .query() .update({ - amount, ...omit(estimate, ['entries']), }); const storedEstimateEntries = await ItemEntry.tenant() @@ -79,6 +68,26 @@ export default class SaleEstimateService { ]); } + /** + * Deletes the given estimate id with associated entries. + * @async + * @param {IEstimate} estimateId + * @return {void} + */ + static async deleteEstimate(estimateId: number) { + await ItemEntry.tenant() + .query() + .where('reference_id', estimateId) + .where('reference_type', 'SaleEstimate') + .delete(); + + await SaleEstimate.tenant() + .query() + .where('id', estimateId) + .delete(); + } + + /** * Validates the given estimate ID exists. * @async diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 82461d5bc..913fcca43 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,4 +1,4 @@ -import { omit, sumBy, difference } from 'lodash'; +import { omit, sumBy, difference, pick } from 'lodash'; import { Container } from 'typedi'; import { SaleInvoice, @@ -12,6 +12,7 @@ import JournalPoster from '@/services/Accounting/JournalPoster'; import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import CustomerRepository from '@/repositories/CustomerRepository'; import InventoryService from '@/services/Inventory/Inventory'; +import { formatDateFields } from '@/utils'; /** * Sales invoices service @@ -25,14 +26,19 @@ export default class SaleInvoicesService { * @param {ISaleInvoice} * @return {ISaleInvoice} */ - static async createSaleInvoice(saleInvoice: any) { - const balance = sumBy(saleInvoice.entries, 'amount'); + static async createSaleInvoice(saleInvoiceDTO: any) { + const balance = sumBy(saleInvoiceDTO.entries, 'amount'); + const invLotNumber = await InventoryService.nextLotNumber(); + const saleInvoice = { + ...formatDateFields(saleInvoiceDTO, ['invoide_date', 'due_date']), + balance, + paymentAmount: 0, + invLotNumber, + }; const storedInvoice = await SaleInvoice.tenant() .query() .insert({ ...omit(saleInvoice, ['entries']), - balance, - payment_amount: 0, }); const opers: Array = []; @@ -52,8 +58,8 @@ export default class SaleInvoicesService { balance, ); // Records the inventory transactions for inventory items. - const recordInventoryTransOpers = InventoryService.recordInventoryTransactions( - saleInvoice.entries, saleInvoice.invoice_date, 'SaleInvoice', storedInvoice.id, 'OUT', + const recordInventoryTransOpers = this.recordInventoryTranscactions( + saleInvoice, storedInvoice.id ); // Await all async operations. await Promise.all([ @@ -79,11 +85,33 @@ export default class SaleInvoicesService { } + /** + * Records the inventory transactions from the givne sale invoice input. + * @param {SaleInvoice} saleInvoice - + * @param {number} saleInvoiceId - + * @param {boolean} override - + */ + static recordInventoryTranscactions(saleInvoice, saleInvoiceId: number, override?: boolean){ + const inventortyTransactions = saleInvoice.entries + .map((entry) => ({ + ...pick(entry, ['item_id', 'quantity', 'rate']), + lotNumber: saleInvoice.invLotNumber, + transactionType: 'SaleInvoice', + transactionId: saleInvoiceId, + direction: 'OUT', + date: saleInvoice.invoice_date, + })); + + return InventoryService.recordInventoryTransactions( + inventortyTransactions, override, + ); + } + /** * Schedule sale invoice re-compute based on the item * cost method and starting date * - * @param saleInvoice + * @param {SaleInvoice} saleInvoice - * @return {Promise} */ static scheduleComputeItemsCost(saleInvoice) { @@ -102,18 +130,22 @@ export default class SaleInvoicesService { * @param {Number} saleInvoiceId - * @param {ISaleInvoice} saleInvoice - */ - static async editSaleInvoice(saleInvoiceId: number, saleInvoice: any) { - const balance = sumBy(saleInvoice.entries, 'amount'); + static async editSaleInvoice(saleInvoiceId: number, saleInvoiceDTO: any) { + const balance = sumBy(saleInvoiceDTO.entries, 'amount'); const oldSaleInvoice = await SaleInvoice.tenant().query() .where('id', saleInvoiceId) .first(); + const saleInvoice = { + ...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']), + balance, + invLotNumber: oldSaleInvoice.invLotNumber, + }; const updatedSaleInvoices = await SaleInvoice.tenant() .query() .where('id', saleInvoiceId) .update({ - balance, - ...omit(saleInvoice, ['entries']), + ...omit(saleInvoice, ['entries', 'invLotNumber']), }); // Fetches the sale invoice items entries. const storedEntries = await ItemEntry.tenant() @@ -132,9 +164,14 @@ export default class SaleInvoicesService { balance, oldSaleInvoice.balance, ); + // Records the inventory transactions for inventory items. + const recordInventoryTransOper = this.recordInventoryTranscactions( + saleInvoice, saleInvoiceId, true, + ); await Promise.all([ patchItemsEntriesOper, changeCustomerBalanceOper, + recordInventoryTransOper, ]); // Schedule sale invoice re-compute based on the item cost @@ -221,7 +258,6 @@ export default class SaleInvoicesService { const revertInventoryTransactionsOper = this.revertInventoryTransactions( inventoryTransactions ); - // Await all async operations. await Promise.all([ journal.deleteEntries(), diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index f3ba7ae62..c50707816 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -7,6 +7,7 @@ import { import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPosterService from '@/services/Sales/JournalPosterService'; import HasItemEntries from '@/services/Sales/HasItemsEntries'; +import { formatDateFields } from '@/utils'; export default class SalesReceipt { /** @@ -15,12 +16,14 @@ export default class SalesReceipt { * @param {ISaleReceipt} saleReceipt * @return {Object} */ - static async createSaleReceipt(saleReceipt: any) { - const amount = sumBy(saleReceipt.entries, 'amount'); + static async createSaleReceipt(saleReceiptDTO: any) { + const saleReceipt = { + amount: sumBy(saleReceiptDTO.entries, 'amount'); + ...formatDateFields(saleReceiptDTO, ['receipt_date']) + }; const storedSaleReceipt = await SaleReceipt.tenant() .query() .insert({ - amount, ...omit(saleReceipt, ['entries']), }); const storeSaleReceiptEntriesOpers: Array = []; @@ -39,29 +42,21 @@ export default class SalesReceipt { return storedSaleReceipt; } - /** - * Records journal transactions for sale receipt. - * @param {ISaleReceipt} saleReceipt - * @return {Promise} - */ - static async _recordJournalTransactions(saleReceipt: any) { - const accountsDepGraph = await Account.tenant().depGraph().query(); - const journalPoster = new JournalPoster(accountsDepGraph); - } - /** * Edit details sale receipt with associated entries. * @param {Integer} saleReceiptId * @param {ISaleReceipt} saleReceipt * @return {void} */ - static async editSaleReceipt(saleReceiptId: number, saleReceipt: any) { - const amount = sumBy(saleReceipt.entries, 'amount'); + static async editSaleReceipt(saleReceiptId: number, saleReceiptDTO: any) { + const saleReceipt = { + amount: sumBy(saleReceiptDTO.entries, 'amount'), + ...formatDateFields(saleReceiptDTO, ['receipt_date']) + }; const updatedSaleReceipt = await SaleReceipt.tenant() .query() .where('id', saleReceiptId) .update({ - amount, ...omit(saleReceipt, ['entries']), }); const storedSaleReceiptEntries = await ItemEntry.tenant() @@ -82,7 +77,11 @@ export default class SalesReceipt { * @return {void} */ static async deleteSaleReceipt(saleReceiptId: number) { - const deleteSaleReceiptOper = SaleReceipt.tenant().query().where('id', saleReceiptId).delete(); + const deleteSaleReceiptOper = SaleReceipt.tenant() + .query() + .where('id', saleReceiptId) + .delete(); + const deleteItemsEntriesOper = ItemEntry.tenant() .query() .where('reference_id', saleReceiptId) @@ -148,4 +147,14 @@ export default class SalesReceipt { return saleReceipt; } + + /** + * Records journal transactions for sale receipt. + * @param {ISaleReceipt} saleReceipt + * @return {Promise} + */ + static async _recordJournalTransactions(saleReceipt: any) { + const accountsDepGraph = await Account.tenant().depGraph().query(); + const journalPoster = new JournalPoster(accountsDepGraph); + } } diff --git a/server/src/utils/index.js b/server/src/utils/index.js index 5d6959e86..0b2067854 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.js @@ -144,6 +144,17 @@ function applyMixins(derivedCtor, baseCtors) { }); } +const formatDateFields = (inputDTO, fields, format = 'YYYY-DD-MM') => { + const _inputDTO = { ...inputDTO }; + + fields.forEach((field) => { + if (_inputDTO[field]) { + _inputDTO[field] = moment(_inputDTO[field]).format(format); + } + }); + return _inputDTO; +}; + export { hashPassword, origin, @@ -156,4 +167,5 @@ export { itemsStartWith, getTotalDeep, applyMixins, + formatDateFields, }; diff --git a/server/tests/routes/accounts.test.js b/server/tests/routes/accounts.test.js index bcaa5c166..6c9a9418c 100644 --- a/server/tests/routes/accounts.test.js +++ b/server/tests/routes/accounts.test.js @@ -204,8 +204,6 @@ describe('routes: /accounts/', () => { code: '123', }); - console.log(res.body); - expect(res.status).equals(200); }); }); diff --git a/server/tests/routes/views.test.js b/server/tests/routes/views.test.js index f42f01784..36652d731 100644 --- a/server/tests/routes/views.test.js +++ b/server/tests/routes/views.test.js @@ -802,8 +802,6 @@ describe('routes: `/views`', () => { value: '100', }], }); - - // console.log(res.status, res.body); const foundViewColumns = await ViewColumn.tenant().query().where('id', viewColumn.id); expect(foundViewColumns.length).equals(0); });