From db28cd2aefd9b6cb24324c82307e13847fd43275 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 3 Aug 2020 22:46:50 +0200 Subject: [PATCH] - feat: Sales estimates. - feat: Sales invoices. - feat: Sales payment receives. - feat: Purchases bills. - feat: Purchases bills payments that made to the vendors. --- server/package.json | 3 + .../20200607212203_create_customers_table.js | 3 +- .../20200608192614_create_vendors_table.js | 2 +- ...0713192127_create_sales_estimates_table.js | 3 + ...50_create_sales_estimates_entries_table.js | 16 - ...200713213303_create_sales_receipt_table.js | 1 + ...200715193633_create_sale_invoices_table.js | 2 + ...715194514_create_payment_receives_table.js | 1 + ...846_create_sales_invoices_entries_table.js | 17 - .../20200719152005_reate_bills_table.js | 8 +- ...00719153909_create_bills_payments_table.js | 1 + ...251_create_inventory_transactions_table.js | 22 + ...00722173423_create_items_entries_table.js} | 9 +- ...0728161617_create_bill_payments_entries.js | 14 + .../src/database/seeds/seed_account_types.js | 4 +- server/src/database/seeds/seed_accounts.js | 10 + server/src/database/seeds/seed_resources.js | 2 + .../database/seeds/seed_resources_fields.js | 148 +++++ server/src/http/controllers/Customers.js | 1 + server/src/http/controllers/Items.js | 4 - .../src/http/controllers/Purchases/Bills.js | 297 ++++++++-- .../controllers/Purchases/BillsPayments.js | 140 ----- .../controllers/Purchases/BillsPayments.ts | 338 ++++++++++++ .../http/controllers/Sales/PaymentReceives.js | 301 ++++++++-- .../http/controllers/Sales/SalesEstimates.js | 498 +++++++++-------- .../http/controllers/Sales/SalesInvoices.js | 514 +++++++++++------- .../http/controllers/Sales/SalesReceipt.js | 469 +++++++++------- server/src/interfaces/BillPayment.ts | 13 + .../QueryBuilder.js | 27 + server/src/models/Bill.js | 13 +- server/src/models/BillPayment.js | 31 +- server/src/models/BillPaymentEntry.js | 18 + server/src/models/Customer.js | 38 ++ server/src/models/InventoryTransaction.js | 18 + server/src/models/ItemEntry.js | 35 ++ server/src/models/PaymentReceive.js | 9 +- server/src/models/SaleEstimate.js | 15 +- server/src/models/SaleInvoice.js | 33 +- server/src/models/SaleReceipt.js | 13 +- server/src/models/Vendor.js | 37 ++ server/src/models/index.js | 8 + server/src/repositories/CustomerRepository.js | 26 + .../src/repositories/ItemEntryRepository.js | 0 .../PaymentReceiveEntryRepository.js | 55 ++ .../repositories/PaymentReceiveRepository.js | 7 + .../src/services/Accounts/AccountsService.js | 21 +- server/src/services/Inventory/Inventory.js | 16 + server/src/services/Purchases/BillPayments.js | 246 ++++++++- server/src/services/Purchases/Bills.js | 326 +++++++++-- .../services/Sales/JournalPosterService.js | 7 +- server/src/services/Sales/PaymentReceive.js | 353 ++++++++++-- server/src/services/Sales/SaleInvoice.js | 112 ++-- server/src/services/Sales/SalesEstimate.js | 73 ++- server/src/services/Sales/SalesReceipt.js | 103 ++-- server/tsconfig.json | 11 + server/webpack.config.js | 6 + 56 files changed, 3290 insertions(+), 1208 deletions(-) delete mode 100644 server/src/database/migrations/20200713193250_create_sales_estimates_entries_table.js delete mode 100644 server/src/database/migrations/20200716092846_create_sales_invoices_entries_table.js create mode 100644 server/src/database/migrations/20200722164251_create_inventory_transactions_table.js rename server/src/database/migrations/{20200713213311_create_sales_receipt_entries_table.js => 20200722173423_create_items_entries_table.js} (59%) create mode 100644 server/src/database/migrations/20200728161617_create_bill_payments_entries.js delete mode 100644 server/src/http/controllers/Purchases/BillsPayments.js create mode 100644 server/src/http/controllers/Purchases/BillsPayments.ts create mode 100644 server/src/interfaces/BillPayment.ts create mode 100644 server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js create mode 100644 server/src/models/BillPaymentEntry.js create mode 100644 server/src/models/InventoryTransaction.js create mode 100644 server/src/models/ItemEntry.js create mode 100644 server/src/repositories/CustomerRepository.js create mode 100644 server/src/repositories/ItemEntryRepository.js create mode 100644 server/src/repositories/PaymentReceiveEntryRepository.js create mode 100644 server/src/repositories/PaymentReceiveRepository.js create mode 100644 server/src/services/Inventory/Inventory.js create mode 100644 server/tsconfig.json diff --git a/server/package.json b/server/package.json index d98472f3a..2641e42a1 100644 --- a/server/package.json +++ b/server/package.json @@ -60,6 +60,7 @@ "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.5.5", "@babel/runtime": "^7.5.5", + "@types/lodash": "^4.14.158", "babel-loader": "^8.0.6", "chai": "^4.2.0", "chai-http": "^4.3.0", @@ -81,6 +82,8 @@ "npm-run-all": "^4.1.5", "nyc": "^14.1.1", "sinon": "^7.4.2", + "ts-loader": "^8.0.1", + "typescript": "^3.9.7", "webpack": "^4.0.0", "webpack-cli": "^3.3.7", "webpack-node-externals": "^1.7.2" diff --git a/server/src/database/migrations/20200607212203_create_customers_table.js b/server/src/database/migrations/20200607212203_create_customers_table.js index d1a61cc40..4ea255665 100644 --- a/server/src/database/migrations/20200607212203_create_customers_table.js +++ b/server/src/database/migrations/20200607212203_create_customers_table.js @@ -4,7 +4,7 @@ exports.up = function(knex) { table.increments(); table.string('customer_type'); - table.decimal('balance', 13, 3); + table.decimal('balance', 13, 3).defaultTo(0); table.string('first_name').nullable(); table.string('last_name').nullable(); @@ -36,6 +36,7 @@ exports.up = function(knex) { table.text('note'); table.boolean('active').defaultTo(true); + table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200608192614_create_vendors_table.js b/server/src/database/migrations/20200608192614_create_vendors_table.js index e30c0edee..3b1f56160 100644 --- a/server/src/database/migrations/20200608192614_create_vendors_table.js +++ b/server/src/database/migrations/20200608192614_create_vendors_table.js @@ -4,7 +4,7 @@ exports.up = function(knex) { table.increments(); table.string('customer_type'); - table.decimal('balance', 13, 3); + table.decimal('balance', 13, 3).defaultTo(0); table.string('first_name').nullable(); table.string('last_name').nullable(); diff --git a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js index bee6a9c4b..2dbce39ec 100644 --- a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js +++ b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js @@ -2,6 +2,7 @@ exports.up = function(knex) { return knex.schema.createTable('sales_estimates', (table) => { table.increments(); + table.decimal('amount', 13, 3); table.integer('customer_id').unsigned(); table.date('estimate_date'); table.date('expiration_date'); @@ -9,6 +10,8 @@ exports.up = function(knex) { table.string('estimate_number'); table.text('note'); table.text('terms_conditions'); + + table.integer('user_id').unsigned(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200713193250_create_sales_estimates_entries_table.js b/server/src/database/migrations/20200713193250_create_sales_estimates_entries_table.js deleted file mode 100644 index 66ebe37e6..000000000 --- a/server/src/database/migrations/20200713193250_create_sales_estimates_entries_table.js +++ /dev/null @@ -1,16 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('sales_estimate_entries', table => { - table.increments(); - table.integer('estimate_id').unsigned(); - table.integer('item_id').unsigned(); - table.text('description'); - table.integer('discount').unsigned(); - table.integer('quantity').unsigned(); - table.integer('rate').unsigned(); - }) -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('sales_estimate_entries'); -}; diff --git a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js index b318d3bc7..3adb04ba1 100644 --- a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js +++ b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js @@ -2,6 +2,7 @@ exports.up = function(knex) { return knex.schema.createTable('sales_receipts', table => { table.increments(); + table.decimal('amount', 13, 3); table.integer('deposit_account_id').unsigned(); table.integer('customer_id').unsigned(); table.date('receipt_date'); 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 96c2a548a..bd66a66a9 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -13,6 +13,8 @@ exports.up = function(knex) { table.text('terms_conditions'); table.decimal('balance', 13, 3); + table.decimal('payment_amount', 13, 3); + table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200715194514_create_payment_receives_table.js b/server/src/database/migrations/20200715194514_create_payment_receives_table.js index 054f822e0..554bad85c 100644 --- a/server/src/database/migrations/20200715194514_create_payment_receives_table.js +++ b/server/src/database/migrations/20200715194514_create_payment_receives_table.js @@ -5,6 +5,7 @@ exports.up = function(knex) { table.increments(); table.integer('customer_id').unsigned(); table.date('payment_date'); + table.decimal('amount', 13, 3).defaultTo(0); table.string('reference_no'); table.integer('deposit_account_id').unsigned(); table.string('payment_receive_no'); diff --git a/server/src/database/migrations/20200716092846_create_sales_invoices_entries_table.js b/server/src/database/migrations/20200716092846_create_sales_invoices_entries_table.js deleted file mode 100644 index 60c074b06..000000000 --- a/server/src/database/migrations/20200716092846_create_sales_invoices_entries_table.js +++ /dev/null @@ -1,17 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('sales_invoices_entries', table => { - table.increments(); - table.integer('sale_invoice_id').unsigned(); - table.integer('item_id').unsigned(); - table.integer('index').unsigned(); - table.text('description'); - table.integer('discount').unsigned(); - table.integer('quantity').unsigned(); - table.integer('rate').unsigned(); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('sales_invoices_entries'); -}; diff --git a/server/src/database/migrations/20200719152005_reate_bills_table.js b/server/src/database/migrations/20200719152005_reate_bills_table.js index 12455ac6a..007f55b4d 100644 --- a/server/src/database/migrations/20200719152005_reate_bills_table.js +++ b/server/src/database/migrations/20200719152005_reate_bills_table.js @@ -2,11 +2,17 @@ exports.up = function(knex) { return knex.schema.createTable('bills', (table) => { table.increments(); + table.integer('vendor_id').unsigned(); table.string('bill_number'); table.date('bill_date'); table.date('due_date'); - table.integer('vendor_id').unsigned(); + table.string('reference_no'); + table.string('status'); table.text('note'); + + table.decimal('amount', 13, 3).defaultTo(0); + table.decimal('payment_amount', 13, 3).defaultTo(0); + table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200719153909_create_bills_payments_table.js b/server/src/database/migrations/20200719153909_create_bills_payments_table.js index 48b4c6881..8cddb3163 100644 --- a/server/src/database/migrations/20200719153909_create_bills_payments_table.js +++ b/server/src/database/migrations/20200719153909_create_bills_payments_table.js @@ -6,6 +6,7 @@ exports.up = function(knex) { table.string('payment_number'); table.date('payment_date'); table.string('payment_method'); + table.string('reference'); table.integer('user_id').unsigned(); table.text('description'); 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 new file mode 100644 index 000000000..7f7957741 --- /dev/null +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -0,0 +1,22 @@ + +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(); + }); +}; + +exports.down = function(knex) { + +}; diff --git a/server/src/database/migrations/20200713213311_create_sales_receipt_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js similarity index 59% rename from server/src/database/migrations/20200713213311_create_sales_receipt_entries_table.js rename to server/src/database/migrations/20200722173423_create_items_entries_table.js index bfb662d70..902ef253d 100644 --- a/server/src/database/migrations/20200713213311_create_sales_receipt_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -1,17 +1,20 @@ exports.up = function(knex) { - return knex.schema.createTable('sales_receipt_entries', table => { + return knex.schema.createTable('items_entries', (table) => { table.increments(); - table.integer('sale_receipt_id').unsigned(); + table.string('reference_type'); + table.string('reference_id'); + table.integer('index').unsigned(); table.integer('item_id'); table.text('description'); table.integer('discount').unsigned(); table.integer('quantity').unsigned(); table.integer('rate').unsigned(); + table.timestamps(); }); }; exports.down = function(knex) { - return knex.schema.dropTableIfExists('sales_receipt_entries') ; + return knex.schema.dropTableIfExists('items_entries'); }; diff --git a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js new file mode 100644 index 000000000..7dac8c241 --- /dev/null +++ b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js @@ -0,0 +1,14 @@ + +exports.up = function(knex) { + return knex.schema.createTable('bills_payments_entries', table => { + table.increments(); + + table.integer('bill_payment_id').unsigned(); + table.integer('bill_id').unsigned(); + table.decimal('payment_amount', 13, 3).unsigned(); + }) +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('bills_payments_entries'); +}; diff --git a/server/src/database/seeds/seed_account_types.js b/server/src/database/seeds/seed_account_types.js index 36df5ad45..a306d9167 100644 --- a/server/src/database/seeds/seed_account_types.js +++ b/server/src/database/seeds/seed_account_types.js @@ -119,8 +119,8 @@ exports.seed = (knex) => { name: 'Cost of Goods Sold (COGS)', key: 'cost_of_goods_sold', normal: 'debit', - root_type: 'asset', - child_type: 'current_asset', + root_type: 'expenses', + child_type: 'expenses', balance_sheet: true, income_sheet: false, }, diff --git a/server/src/database/seeds/seed_accounts.js b/server/src/database/seeds/seed_accounts.js index 881b6215a..5fb1d0e7e 100644 --- a/server/src/database/seeds/seed_accounts.js +++ b/server/src/database/seeds/seed_accounts.js @@ -126,6 +126,16 @@ exports.seed = (knex) => { index: 1, predefined: 1, }, + { + id: 12, + name: 'Cost of Goods Sold (COGS)', + account_type_id: 12, + predefined: 1, + parent_account_id: null, + index: 1, + active: 1, + description: 1, + } ]); }); }; diff --git a/server/src/database/seeds/seed_resources.js b/server/src/database/seeds/seed_resources.js index 63199b7d8..481397c6e 100644 --- a/server/src/database/seeds/seed_resources.js +++ b/server/src/database/seeds/seed_resources.js @@ -18,6 +18,8 @@ exports.seed = (knex) => { { id: 10, name: 'sales_receipts' }, { id: 11, name: 'sales_invoices' }, { id: 12, name: 'sales_payment_receives' }, + { id: 13, name: 'bills' }, + { id: 14, name: 'bill_payments' }, ]); }); }; diff --git a/server/src/database/seeds/seed_resources_fields.js b/server/src/database/seeds/seed_resources_fields.js index f9eb9c248..a5f8b89cd 100644 --- a/server/src/database/seeds/seed_resources_fields.js +++ b/server/src/database/seeds/seed_resources_fields.js @@ -271,6 +271,154 @@ exports.seed = (knex) => { predefined: 1, columnable: true, }, + + + // Sales Estimates + { + label_name: 'Customer name', + key: 'customer_name', + }, + { + label_name: 'Amount', + key: 'amount', + }, + { + label_name: 'Estimate number', + key: 'estimate_number', + }, + { + label_name: 'Estimate date', + key: 'estimate_date', + }, + { + label_name: 'Expiration date', + key: 'expiration_date', + }, + { + label_name: 'Reference', + key: 'reference', + }, + { + label_name: 'Terms and conditions', + key: 'terms_conditions', + }, + { + label_name: 'Note', + key: 'note', + }, + + // Sales invoices + // { + // label_name: 'Customer name', + // ley: 'customer_name', + // }, + // { + // label_name: 'Amount', + // ley: 'amount', + // }, + // { + // label_name: 'Invoice number', + // ley: 'invoice_no', + // }, + // { + // label_name: 'Invoice date', + // ley: 'invoice_date', + // }, + // { + // label_name: 'Reference', + // ley: 'reference', + // }, + // { + // label_name: 'Payment amount', + // ley: 'payment_amount', + // }, + // { + // label_name: 'Invoice message', + // ley: 'invoice_no', + // }, + // { + // label_name: 'Terms and conditions', + // key: 'terms_conditions', + // }, + + // // Sales receipts + // { + // label_name: 'Deposit account', + // key: 'deposit_account', + // }, + // { + // label_name: 'Customer name', + // key: 'customer_name', + // }, + // { + // label_name: 'Receipt date', + // key: 'receipt_date', + // }, + // { + // label_name: 'Reference No', + // key: 'reference', + // }, + // { + // label_name: 'Receipt message', + // key: 'receipt_message', + // }, + // { + // label_name: 'Sent to email', + // key: 'email_send_to', + // }, + + // // Payment Receives + // { + // label_name: 'Customer name', + // key: 'customer_name', + // }, + // { + // label_name: 'Payment date', + // key: 'payment_date', + // }, + // { + // label_name: 'Amount', + // key: 'amount', + // }, + // { + // label_name: 'Reference No', + // key: 'reference', + // }, + // { + // label_name: 'Deposit account', + // key: 'deposit_account', + // }, + // { + // label_name: 'Payment receive no.', + // key: 'payment_receive_no', + // }, + + // // Purchases bills. + // { + // label_name: 'Bill number', + // key: 'bill_number' + // }, + + // { + // label_name: 'Bill date', + // key: 'bill_date' + // }, + // { + // label_name: 'Amount', + // key: 'amount' + // }, + // { + // label_name: 'Vendor name', + // key: 'vendor_name' + // }, + // { + // label_name: 'Due date', + // key: 'due_date' + // }, + // { + // label_name: 'Note', + // key: 'note' + // }, ]); }); }; diff --git a/server/src/http/controllers/Customers.js b/server/src/http/controllers/Customers.js index 8c68f14b1..6daa912fb 100644 --- a/server/src/http/controllers/Customers.js +++ b/server/src/http/controllers/Customers.js @@ -225,6 +225,7 @@ export default { const form = { ...req.body }; const customer = await Customer.query().insertAndFetch({ + balance: 0, ...pick(form, [ 'customer_type', 'first_name', diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js index d38005c23..4dc2e69db 100644 --- a/server/src/http/controllers/Items.js +++ b/server/src/http/controllers/Items.js @@ -29,22 +29,18 @@ export default { this.editItem.validation, asyncMiddleware(this.editItem.handler) ); - router.post('/', this.newItem.validation, asyncMiddleware(this.newItem.handler) ); - router.delete('/:id', this.deleteItem.validation, asyncMiddleware(this.deleteItem.handler) ); - router.delete('/', this.bulkDeleteItems.validation, asyncMiddleware(this.bulkDeleteItems.handler) ); - router.get('/', this.listItems.validation, asyncMiddleware(this.listItems.handler) diff --git a/server/src/http/controllers/Purchases/Bills.js b/server/src/http/controllers/Purchases/Bills.js index ae63f54c9..6be018720 100644 --- a/server/src/http/controllers/Purchases/Bills.js +++ b/server/src/http/controllers/Purchases/Bills.js @@ -1,10 +1,15 @@ -import express from "express"; -import { check, param } from 'express-validator'; +import express from 'express'; +import { check, param, query } from 'express-validator'; import validateMiddleware from '@/http/middleware/validateMiddleware'; -import BillsService from "@/services/Purchases/Bills"; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import BillsService from '@/services/Purchases/Bills'; import BaseController from '@/http/controllers/BaseController'; import VendorsServices from '@/services/Vendors/VendorsService'; import ItemsService from '@/services/Items/ItemsService'; +import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder'; +import DynamicListing from '@/services/DynamicListing/DynamicListing'; +import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/HasDynamicListing'; +import { difference } from 'lodash'; export default class BillsController extends BaseController { /** @@ -13,31 +18,44 @@ export default class BillsController extends BaseController { static router() { const router = express.Router(); - router.post('/', [ - ...this.validationSchema, - ], + router.post( + '/', + [...this.billValidationSchema], validateMiddleware, - this.validateVendorExistance, - this.validateItemsIds, - this.validateBillNumberExists, - this.newBill, + asyncMiddleware(this.validateVendorExistance), + asyncMiddleware(this.validateItemsIds), + asyncMiddleware(this.validateBillNumberExists), + asyncMiddleware(this.newBill) ); - // router.post('/:id', [ - // ...this.billValidationSchema, - // ...this.validationSchema, - // ], - // validateMiddleware, - // this.validateBillExistance, - // this.validateVendorExistance, - // this.validateItemsIds, - // this.editBill, - // ); - router.delete('/:id', [ - ...this.billValidationSchema, - ], + router.post( + '/:id', + [...this.billValidationSchema, ...this.specificBillValidationSchema], validateMiddleware, - this.validateBillExistance, - this.deleteBill + asyncMiddleware(this.validateBillExistance), + asyncMiddleware(this.validateVendorExistance), + asyncMiddleware(this.validateItemsIds), + asyncMiddleware(this.validateEntriesIdsExistance), + asyncMiddleware(this.editBill) + ); + router.get( + '/:id', + [...this.specificBillValidationSchema], + validateMiddleware, + asyncMiddleware(this.validateBillExistance), + asyncMiddleware(this.getBill) + ); + router.get( + '/', + [...this.billsListingValidationSchema], + validateMiddleware, + asyncMiddleware(this.listingBills) + ); + router.delete( + '/:id', + [...this.specificBillValidationSchema], + validateMiddleware, + asyncMiddleware(this.validateBillExistance), + asyncMiddleware(this.deleteBill) ); return router; } @@ -45,7 +63,7 @@ export default class BillsController extends BaseController { /** * Common validation schema. */ - static get validationSchema() { + static get billValidationSchema() { return [ check('bill_number').exists().trim().escape(), check('bill_date').exists().isISO8601(), @@ -53,24 +71,50 @@ export default class BillsController extends BaseController { check('vendor_id').exists().isNumeric().toInt(), check('note').optional().trim().escape(), check('entries').isArray({ min: 1 }), + + check('entries.*.id').optional().isNumeric().toInt(), + check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount').optional().isNumeric().toFloat(), check('entries.*.description').optional().trim().escape(), - ] - } - - static get billValidationSchema() { - return [ - param('id').exists().isNumeric().toInt(), ]; } + /** + * Bill validation schema. + */ + static get specificBillValidationSchema() { + return [param('id').exists().isNumeric().toInt()]; + } + + /** + * Bills list validation schema. + */ + static get billsListingValidationSchema() { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + ]; + } + + /** + * Validates whether the vendor is exist. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ static async validateVendorExistance(req, res, next) { - const isVendorExists = await VendorsServices.isVendorExists(req.body.vendor_id); + const isVendorExists = await VendorsServices.isVendorExists( + req.body.vendor_id + ); if (!isVendorExists) { - return res.status(400).send({ + return res.status(400).send({ errors: [{ type: 'VENDOR.ID.NOT.FOUND', code: 300 }], }); } @@ -79,9 +123,9 @@ export default class BillsController extends BaseController { /** * Validates the given bill existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validateBillExistance(req, res, next) { const isBillExists = await BillsService.isBillExists(req.params.id); @@ -94,16 +138,14 @@ export default class BillsController extends BaseController { } /** - * Validates the entries items ids. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * Validates the entries items ids. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validateItemsIds(req, res, next) { const itemsIds = req.body.entries.map((e) => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists( - itemsIds - ); + const notFoundItemsIds = await ItemsService.isItemsIdsExists(itemsIds); if (notFoundItemsIds.length > 0) { return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }], @@ -114,12 +156,14 @@ export default class BillsController extends BaseController { /** * Validates the bill number existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validateBillNumberExists(req, res, next) { - const isBillNoExists = await BillsService.isBillNoExists(req.body.bill_number); + const isBillNoExists = await BillsService.isBillNoExists( + req.body.bill_number + ); if (isBillNoExists) { return res.status(400).send({ @@ -130,16 +174,85 @@ export default class BillsController extends BaseController { } /** - * Creates a new bill and records journal transactions. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * Validates the entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEntriesIdsExistance(req, res, next) { + const { id: billId } = req.params; + const bill = { ...req.body }; + const { ItemEntry } = req.models; + + const entriesIds = bill.entries.filter((e) => e.id).map((e) => e.id); + + const storedEntries = await ItemEntry.tenant() + .query() + .whereIn('reference_id', [billId]) + .whereIn('reference_type', ['Bill']); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + if (notFoundEntriesIds.length > 0) { + return res.status(400).send({ + errors: [{ type: 'BILL.ENTRIES.IDS.NOT.FOUND', code: 600 }], + }); + } + next(); + } + + /** + * Creates a new bill and records journal transactions. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async newBill(req, res, next) { - const bill = { ...req.body }; + const { ItemEntry } = req.models; + + const bill = { + ...req.body, + entries: req.body.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })), + }; const storedBill = await BillsService.createBill(bill); - return res.status(200).send({ id: storedBill }); + return res.status(200).send({ id: storedBill.id }); + } + + /** + * Edit bill details with associated entries and rewrites journal transactions. + * @param {Request} req + * @param {Response} res + */ + static async editBill(req, res) { + const { ItemEntry } = req.models; + const { id: billId } = req.params; + const bill = { + ...req.body, + entries: req.body.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })), + }; + const editedBill = await BillsService.editBill(billId, bill); + + return res.status(200).send({ id: billId }); + } + + /** + * Retrieve the given bill details with associated item entries. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + static async getBill(req, res) { + const { id: billId } = req.params; + const bill = await BillsService.getBill(billId); + + return res.status(200).send({ bill }); } /** @@ -154,4 +267,78 @@ export default class BillsController extends BaseController { return res.status(200).send({ id: billId }); } -} \ No newline at end of file + + /** + * Listing bills with pagination meta. + * @param {Request} req - + * @param {Response} res - + * @return {Response} + */ + static async listingBills(req, res) { + const filter = { + filter_roles: [], + sort_order: 'asc', + page: 1, + page_size: 10, + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { Bill, View, Resource } = req.models; + const resource = await Resource.query() + .remember() + .where('name', 'bills') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'BILLS_RESOURCE_NOT_FOUND', code: 200 }], + }); + } + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addModelClass(Bill); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + listingBuilder.addView(viewMeta); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + const bills = await Bill.query() + .onBuild((builder) => { + dynamicListing.buildQuery()(builder); + return builder; + }) + .pagination(filter.page - 1, filter.page_size); + + return res.status(200).send({ + bills: { + ...bills, + ...(viewMeta + ? { + view_meta: { + customViewId: viewMeta.id, + }, + } + : {}), + }, + }); + } +} diff --git a/server/src/http/controllers/Purchases/BillsPayments.js b/server/src/http/controllers/Purchases/BillsPayments.js deleted file mode 100644 index 08243a80e..000000000 --- a/server/src/http/controllers/Purchases/BillsPayments.js +++ /dev/null @@ -1,140 +0,0 @@ - -import express from 'express'; -import { check, param } from 'express-validator'; -import BaseController from '@/http/controllers/BaseController'; -import BillPaymentsService from '@/services/Purchases/BillPayments'; - -export default class BillsPayments extends BaseController { - /** - * Router constructor. - */ - static router() { - const router = express.Router(); - - router.post('/', [ - ...this.billPaymentSchemaValidation, - ], - this.validatePaymentAccount, - this.validatePaymentNumber, - this.validateItemsIds, - this.createBillPayment, - ); - router.delete('/:id', - this.validateBillPaymentExistance, - this.deleteBillPayment, - ); - return router; - } - - /** - * Bill payments schema validation. - */ - static get billPaymentSchemaValidation() { - return [ - check('payment_account_id').exists().isNumeric().toInt(), - check('payment_number').exists().trim().escape(), - check('payment_date').exists(), - check('description').optional().trim().escape(), - check('entries').exists().isArray({ min: 1 }), - check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toFloat(), - check('entries.*.discount').optional().isNumeric().toFloat(), - check('entries.*.description').optional().trim().escape(), - ]; - } - - /** - * Validates the bill payment existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - static async validateBillPaymentExistance(req, res, next) { - const foundBillPayment = await BillPaymentsService.isBillPaymentExists(req.params.id); - - if (!foundBillPayment) { - return res.status(404).sned({ - errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }], - }); - } - next(req, res, next); - } - - /** - * Validates the payment account. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - static async validatePaymentAccount(req, res, next) { - const isAccountExists = AccountsService.isAccountExists(req.body.payment_account_id); - - if (!isAccountExists) { - return res.status(400).send({ - errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }], - }); - } - next(req, res, next); - } - - /** - * Validates the payment number uniqness. - * @param {Request} req - * @param {Response} res - * @param {Function} res - */ - static async validatePaymentNumber(req, res, next) { - const isNumberExists = await BillPaymentsService.isBillNoExists(req.body.payment_number); - - if (!isNumberExists) { - return res.status(400).send({ - errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }], - }); - } - next(req, res, next); - } - - /** - * validate entries items ids existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - static async validateItemsIds(req, res, next) { - const itemsIds = req.body.entries.map((e) => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists( - itemsIds - ); - if (notFoundItemsIds.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }], - }); - } - next(); - } - - /** - * Creates a bill payment. - * @async - * @param {Request} req - * @param {Response} res - * @param {Response} res - */ - static async createBillPayment(req, res) { - const billPayment = { ...req.body }; - const storedPayment = await BillPaymentsService.createBillPayment(billPayment); - - return res.status(200).send({ id: storedPayment.id }); - } - - /** - * - * @param {Request} req - * @param {Response} res - * @return {Response} res - */ - static async deleteBillPayment(req, res) { - - } -} \ No newline at end of file diff --git a/server/src/http/controllers/Purchases/BillsPayments.ts b/server/src/http/controllers/Purchases/BillsPayments.ts new file mode 100644 index 000000000..4ef0d5a2f --- /dev/null +++ b/server/src/http/controllers/Purchases/BillsPayments.ts @@ -0,0 +1,338 @@ + +import { Router } from 'express'; +import { check, param, query, ValidationChain } from 'express-validator'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import BaseController from '@/http/controllers/BaseController'; +import BillPaymentsService from '@/services/Purchases/BillPayments'; +import AccountsService from '@/services/Accounts/AccountsService'; +import ItemsService from '@/services/Items/ItemsService'; +import { IBillPaymentEntry, IBillPayment } from '@/interfaces/BillPayment'; +import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder'; +import DynamicListing from '@/services/DynamicListing/DynamicListing'; +import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing'; + +export default class BillsPayments extends BaseController { + /** + * Router constructor. + */ + static router() { + const router = Router(); + + router.post('/', [ + ...this.billPaymentSchemaValidation, + ], + asyncMiddleware(this.validateBillPaymentVendorExistance), + asyncMiddleware(this.validatePaymentAccount), + asyncMiddleware(this.validatePaymentNumber), + asyncMiddleware(this.validateItemsIds), + asyncMiddleware(this.createBillPayment), + ); + router.post('/:id', [ + ...this.billPaymentSchemaValidation, + ...this.specificBillPaymentValidateSchema, + ], + asyncMiddleware(this.validateBillPaymentVendorExistance), + asyncMiddleware(this.validatePaymentAccount), + asyncMiddleware(this.validatePaymentNumber), + asyncMiddleware(this.validateItemsIds), + asyncMiddleware(this.validateEntriesIds), + asyncMiddleware(this.editBillPayment), + ) + router.delete('/:id', + this.specificBillPaymentValidateSchema, + asyncMiddleware(this.validateBillPaymentExistance), + asyncMiddleware(this.deleteBillPayment), + ); + router.get('/:id', + this.specificBillPaymentValidateSchema, + asyncMiddleware(this.validateBillPaymentExistance), + asyncMiddleware(this.getBillPayment), + ); + router.get('/', + this.listingValidationSchema, + asyncMiddleware(this.getBillsPayments) + ); + + return router; + } + + /** + * Bill payments schema validation. + */ + static get billPaymentSchemaValidation(): ValidationChain[] { + return [ + check('vendor_id').exists().isNumeric().toInt(), + check('payment_account_id').exists().isNumeric().toInt(), + check('payment_number').exists().trim().escape(), + check('payment_date').exists(), + check('description').optional().trim().escape(), + check('reference').optional().trim().escape(), + + check('entries').exists().isArray({ min: 1 }), + check('entries.*.id').optional().isNumeric().toInt(), + check('entries.*.index').exists().isNumeric().toInt(), + check('entries.*.item_id').exists().isNumeric().toInt(), + check('entries.*.rate').exists().isNumeric().toFloat(), + check('entries.*.quantity').exists().isNumeric().toFloat(), + check('entries.*.discount').optional().isNumeric().toFloat(), + check('entries.*.description').optional().trim().escape(), + ]; + } + + /** + * Specific bill payment schema validation. + */ + static get specificBillPaymentValidateSchema(): ValidationChain[] { + return [ + param('id').exists().isNumeric().toInt(), + ]; + } + + /** + * Validate whether the bill payment vendor exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateBillPaymentVendorExistance(req: Request, res: Response, next: any ) { + const billPayment = req.body; + const { Vendor } = req.models; + const isVendorExists = await Vendor.query('id', billPayment.vendor_id).first(); + + if (!isVendorExists) { + return res.status(400).send({ + errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }], + }); + } + next(); + } + + /** + * Validates the bill payment existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateBillPaymentExistance(req: Request, res: Response, next: any ) { + const foundBillPayment = await BillPaymentsService.isBillPaymentExists(req.params.id); + + if (!foundBillPayment) { + return res.status(404).sned({ + errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }], + }); + } + next(req, res, next); + } + + /** + * Validates the payment account. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validatePaymentAccount(req: Request, res: Response, next: any) { + const billPayment = { ...req.body }; + const isAccountExists = AccountsService.isAccountExists(billPayment); + + if (!isAccountExists) { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }], + }); + } + next(req, res, next); + } + + /** + * Validates the payment number uniqness. + * @param {Request} req + * @param {Response} res + * @param {Function} res + */ + static async validatePaymentNumber(req: Request, res: Response, next: any) { + const billPayment = { ...req.body }; + const isNumberExists = await BillPaymentsService.isBillNoExists(billPayment); + + if (!isNumberExists) { + return res.status(400).send({ + errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }], + }); + } + next(req, res, next); + } + + /** + * validate entries items ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateItemsIds(req: Request, res: Response, next: Function) { + const billPayment: any = { ...req.body }; + const itemsIds = billPayment.entries.map((e) => e.item_id); + const notFoundItemsIds = await ItemsService.isItemsIdsExists( + itemsIds + ); + if (notFoundItemsIds.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }], + }); + } + next(); + } + + /** + * Validates the entries ids in edit bill payment. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + static async validateEntriesIds(req: Request, res: Response, next: Function) { + const { BillPaymentEntry } = req.models; + const { id: billPaymentId } = req.params; + const billPayment = { id: billPaymentId, ...req.body }; + + const entriesIds = billPayment.entries + .filter((entry: IBillPaymentEntry) => entry.id) + .map((entry: IBillPaymentEntry) => entry.id); + + const storedEntries = await BillPaymentEntry.tenant().query() + .where('bill_payment_id', billPaymentId); + + const storedEntriesIds = storedEntries.map((entry: IBillPaymentEntry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }], + }); + } + next(); + } + + /** + * Creates a bill payment. + * @async + * @param {Request} req + * @param {Response} res + * @param {Response} res + */ + static async createBillPayment(req: Request, res: Response) { + const billPayment = { ...req.body }; + const storedPayment = await BillPaymentsService.createBillPayment(billPayment); + + return res.status(200).send({ id: storedPayment.id }); + } + + /** + * Edits the given bill payment details. + * @param {Request} req + * @param {Response} res + */ + static async editBillPayment(req: Request, res: Response) { + const billPayment = { ...req.body }; + + return res.status(200).send({ id: 1 }); + } + + /** + * Deletes the bill payment and revert the journal + * transactions with accounts balance. + * @param {Request} req - + * @param {Response} res - + * @return {Response} res - + */ + static async deleteBillPayment(req: Request, res: Response) { + const { id: billPaymentId } = req.params; + const billPayment = req.body; + + await BillPaymentsService.deleteBillPayment(billPaymentId); + + return res.status(200).send({ id: billPaymentId }); + } + + static async getBillPayment(req: Request, res: Response) { + + } + + /** + * Bills payment list validation schema. + */ + static get listingValidationSchema(): ValidationChain[] { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + + /** + * Retrieve bills payments listing with pagination metadata. + * @param {Request} req - + * @param {Response} res - + * @return {Response} + */ + static async getBillsPayments(req: Request, res: Response) { + const filter = { + filter_roles: [], + sort_order: 'asc', + page: 1, + page_size: 10, + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { BillPayment, View, Resource } = req.models; + + const resource = await Resource.query() + .where('name', 'bill_payments') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'BILL.PAYMENTS.RESOURCE.NOT_FOUND', code: 200 }], + }); + } + + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addModelClass(BillPayment); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + listingBuilder.addView(viewMeta); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + const billPayments = await BillPayment.query().onBuild((builder) => { + dynamicListing.buildQuery()(builder); + return builder; + }); + return res.status(200).send({ + billPayments, + ...(viewMeta + ? { + customViewId: viewMeta.id, + } + : {}), + }); + } +} \ 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 9b4dad591..df8cc8d10 100644 --- a/server/src/http/controllers/Sales/PaymentReceives.js +++ b/server/src/http/controllers/Sales/PaymentReceives.js @@ -1,58 +1,78 @@ import express from 'express'; -import { check, param } from 'express-validator'; +import { check, param, query } from 'express-validator'; +import { difference } from 'lodash'; +import BaseController from '@/http/controllers/BaseController'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import PaymentReceiveService from '@/services/Sales/PaymentReceive'; import CustomersService from '@/services/Customers/CustomersService'; import SaleInvoicesService from '@/services/Sales/SaleInvoice'; import AccountsService from '@/services/Accounts/AccountsService'; +import { PaymentReceiveEntry } from '@/models'; -export default class PaymentReceivesController { +export default class PaymentReceivesController extends BaseController { /** * Router constructor. */ static router() { const router = express.Router(); - router.post('/', - this.newPaymentReceiveValidation, - validateMiddleware, - this.validatePaymentReceiveNoExistance, - this.validateCustomerExistance, - this.validateDepositAccount, - this.validateInvoicesIDs, - asyncMiddleware(this.newPaymentReceive), - ); - router.post('/:id', + router.post( + '/:id', this.editPaymentReceiveValidation, validateMiddleware, - this.validatePaymentReceiveNoExistance, - this.validateCustomerExistance, - this.validateDepositAccount, - this.validateInvoicesIDs, - asyncMiddleware(this.editPaymentReceive), + asyncMiddleware(this.validatePaymentReceiveExistance), + asyncMiddleware(this.validatePaymentReceiveNoExistance), + asyncMiddleware(this.validateCustomerExistance), + asyncMiddleware(this.validateDepositAccount), + asyncMiddleware(this.validateInvoicesIDs), + asyncMiddleware(this.validateEntriesIdsExistance), + asyncMiddleware(this.editPaymentReceive) ); - router.get('/:id', + router.post( + '/', + this.newPaymentReceiveValidation, + validateMiddleware, + asyncMiddleware(this.validatePaymentReceiveNoExistance), + asyncMiddleware(this.validateCustomerExistance), + asyncMiddleware(this.validateDepositAccount), + asyncMiddleware(this.validateInvoicesIDs), + asyncMiddleware(this.validateInvoicesPaymentsAmount), + asyncMiddleware(this.newPaymentReceive) + ); + router.get( + '/:id', this.paymentReceiveValidation, validateMiddleware, - this.validatePaymentReceiveExistance, - asyncMiddleware(this.getPaymentReceive), + asyncMiddleware(this.validatePaymentReceiveExistance), + asyncMiddleware(this.getPaymentReceive) ); - router.delete('/:id', + router.get( + '/', + this.validatePaymentReceiveList, + validateMiddleware, + asyncMiddleware(this.getPaymentReceiveList), + ); + router.delete( + '/:id', this.paymentReceiveValidation, validateMiddleware, - this.validatePaymentReceiveExistance, - asyncMiddleware(this.deletePaymentReceive), + asyncMiddleware(this.validatePaymentReceiveExistance), + asyncMiddleware(this.deletePaymentReceive) ); return router; } /** * Validates the payment receive number existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validatePaymentReceiveNoExistance(req, res, next) { const isPaymentNoExists = await PaymentReceiveService.isPaymentReceiveNoExists( req.body.payment_receive_no, + req.params.id, ); if (isPaymentNoExists) { return res.status(400).send({ @@ -64,10 +84,13 @@ export default class PaymentReceivesController { /** * Validates the payment receive existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validatePaymentReceiveExistance(req, res, next) { const isPaymentNoExists = await PaymentReceiveService.isPaymentReceiveExists( - req.params.id, + req.params.id ); if (!isPaymentNoExists) { return res.status(400).send({ @@ -79,10 +102,13 @@ export default class PaymentReceivesController { /** * Validate the deposit account id existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validateDepositAccount(req, res, next) { const isDepositAccExists = await AccountsService.isAccountExists( - req.body.deposit_account_id, + req.body.deposit_account_id ); if (!isDepositAccExists) { return res.status(400).send({ @@ -94,13 +120,13 @@ export default class PaymentReceivesController { /** * Validates the `customer_id` existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @param {Function} next */ static async validateCustomerExistance(req, res, next) { const isCustomerExists = await CustomersService.isCustomerExists( - req.body.customer_id, + req.body.customer_id ); if (!isCustomerExists) { return res.status(400).send({ @@ -112,11 +138,15 @@ export default class PaymentReceivesController { /** * Validates the invoices IDs existance. + * @param {Request} req - + * @param {Response} res - + * @param {Function} next - */ static async validateInvoicesIDs(req, res, next) { const invoicesIds = req.body.entries.map((e) => e.invoice_id); - const notFoundInvoicesIDs = await SaleInvoicesService.isInvoicesExist(invoicesIds); - + const notFoundInvoicesIDs = await SaleInvoicesService.isInvoicesExist( + invoicesIds + ); if (notFoundInvoicesIDs.length > 0) { return res.status(400).send({ errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }], @@ -125,6 +155,72 @@ export default class PaymentReceivesController { next(); } + /** + * Validates entries invoice payment amount. + * @param {Request} req - + * @param {Response} res - + * @param {Function} next - + */ + static async validateInvoicesPaymentsAmount(req, res, next) { + const { SaleInvoice } = req.models; + const invoicesIds = req.body.entries.map((e) => e.invoice_id); + const storedInvoices = await SaleInvoice.tenant() + .query() + .whereIn('id', invoicesIds); + + const storedInvoicesMap = new Map( + storedInvoices.map((invoice) => [invoice.id, invoice]) + ); + const hasWrongPaymentAmount = []; + + req.body.entries.forEach((entry, index) => { + const entryInvoice = storedInvoicesMap.get(entry.invoice_id); + const { dueAmount } = entryInvoice; + + if (dueAmount < entry.payment_amount) { + hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); + } + }); + if (hasWrongPaymentAmount.length > 0) { + return res.status(400).send({ + errors: [ + { + type: 'INVOICE.PAYMENT.AMOUNT', + code: 200, + indexes: hasWrongPaymentAmount, + }, + ], + }); + } + next(); + } + + /** + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + static async validateEntriesIdsExistance(req, res, next) { + const paymentReceive = { id: req.params.id, ...req.body }; + const entriesIds = paymentReceive.entries + .filter(entry => entry.id) + .map(entry => entry.id); + + const storedEntries = await PaymentReceiveEntry.tenant().query() + .where('payment_receive_id', paymentReceive.id); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }], + }); + } + next(); + } + /** * Payment receive schema. * @return {Array} @@ -136,12 +232,19 @@ export default class PaymentReceivesController { check('reference_no').optional(), check('deposit_account_id').exists().isNumeric().toInt(), check('payment_receive_no').exists().trim().escape(), + check('statement').optional().trim().escape(), + check('entries').isArray({ min: 1 }), + check('entries.*.invoice_id').exists().isNumeric().toInt(), check('entries.*.payment_amount').exists().isNumeric().toInt(), ]; } + /** + * New payment receive validation schema. + * @return {Array} + */ static get newPaymentReceiveValidation() { return [...this.paymentReceiveSchema]; } @@ -151,8 +254,9 @@ export default class PaymentReceivesController { */ static async newPaymentReceive(req, res) { const paymentReceive = { ...req.body }; - const storedPaymentReceive = await PaymentReceiveService.createPaymentReceive(paymentReceive); - + const storedPaymentReceive = await PaymentReceiveService.createPaymentReceive( + paymentReceive + ); return res.status(200).send({ id: storedPaymentReceive.id }); } @@ -167,15 +271,27 @@ export default class PaymentReceivesController { } /** - * Edit the given payment receive. - * @param {Request} req - * @param {Response} res + * Edit the given payment receive. + * @param {Request} req + * @param {Response} res + * @return {Response} */ static async editPaymentReceive(req, res) { const paymentReceive = { ...req.body }; const { id: paymentReceiveId } = req.params; - await PaymentReceiveService.editPaymentReceive(paymentReceiveId, paymentReceive); + const { PaymentReceive } = req.models; + // Retrieve the payment receive before updating. + const oldPaymentReceive = await PaymentReceive.query() + .where('id', paymentReceiveId) + .withGraphFetched('entries') + .first(); + + await PaymentReceiveService.editPaymentReceive( + paymentReceiveId, + paymentReceive, + oldPaymentReceive, + ); return res.status(200).send({ id: paymentReceiveId }); } @@ -183,20 +299,27 @@ export default class PaymentReceivesController { * Validate payment receive parameters. */ static get paymentReceiveValidation() { - return [ - param('id').exists().isNumeric().toInt(), - ]; + return [param('id').exists().isNumeric().toInt()]; } /** * Delets the given payment receive id. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ static async deletePaymentReceive(req, res) { const { id: paymentReceiveId } = req.params; - await PaymentReceiveService.deletePaymentReceive(paymentReceiveId); + const { PaymentReceive } = req.models; + const storedPaymentReceive = await PaymentReceive.query() + .where('id', paymentReceiveId) + .withGraphFetched('entries') + .first(); + + await PaymentReceiveService.deletePaymentReceive( + paymentReceiveId, + storedPaymentReceive + ); return res.status(200).send({ id: paymentReceiveId }); } @@ -208,8 +331,94 @@ export default class PaymentReceivesController { */ static async getPaymentReceive(req, res) { const { id: paymentReceiveId } = req.params; - const paymentReceive = await PaymentReceiveService.getPaymentReceive(paymentReceiveId); - + const paymentReceive = await PaymentReceiveService.getPaymentReceive( + paymentReceiveId + ); return res.status(200).send({ paymentReceive }); } -} \ No newline at end of file + + /** + * Payment receive list validation schema. + */ + static async validatePaymentReceiveList() { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ] + } + + /** + * Retrieve payment receive list with pagination metadata. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + static async getPaymentReceiveList(req, res) { + const filter = { + filter_roles: [], + sort_order: 'asc', + page: 1, + page_size: 10, + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { Resource, PaymentReceive, View } = req.models; + const resource = await Resource.query() + .remember() + .where('name', 'payment_receives') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'PAYMENT_RECEIVES_RESOURCE_NOT_FOUND', code: 200 }], + }); + } + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addModelClass(Bill); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + listingBuilder.addView(viewMeta); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + const paymentReceives = await PaymentReceive.query().onBuild((builder) => { + dynamicListing.buildQuery()(builder); + return builder; + }); + return res.status(200).send({ + payment_receives: { + ...paymentReceives, + ...(viewMeta + ? { + viewMeta: { + customViewId: viewMeta.id, + } + } + : {}), + }, + }); + } +} diff --git a/server/src/http/controllers/Sales/SalesEstimates.js b/server/src/http/controllers/Sales/SalesEstimates.js index e5111c4ec..f9ca33e86 100644 --- a/server/src/http/controllers/Sales/SalesEstimates.js +++ b/server/src/http/controllers/Sales/SalesEstimates.js @@ -1,5 +1,7 @@ import express from 'express'; import { check, param, query } from 'express-validator'; +import { ItemEntry } from '@/models'; +import BaseController from '@/http/controllers/BaseController' import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import CustomersService from '@/services/Customers/CustomersService'; @@ -8,278 +10,334 @@ import ItemsService from '@/services/Items/ItemsService'; import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder'; import DynamicListing from '@/services/DynamicListing/DynamicListing'; -export default { - router() { +export default class SalesEstimatesController extends BaseController { + /** + * Router constructor. + */ + static router() { const router = express.Router(); router.post( '/', - this.newEstimate.validation, + this.estimateValidationSchema, validateMiddleware, - asyncMiddleware(this.newEstimate.handler) + asyncMiddleware(this.validateEstimateCustomerExistance), + asyncMiddleware(this.validateEstimateNumberExistance), + asyncMiddleware(this.validateEstimateEntriesItemsExistance), + asyncMiddleware(this.newEstimate) ); router.post( - '/:id', - this.editEstimate.validation, + '/:id', [ + ...this.validateSpecificEstimateSchema, + ...this.estimateValidationSchema, + ], validateMiddleware, - asyncMiddleware(this.editEstimate.handler) + asyncMiddleware(this.validateEstimateIdExistance), + asyncMiddleware(this.validateEstimateCustomerExistance), + asyncMiddleware(this.validateEstimateNumberExistance), + asyncMiddleware(this.validateEstimateEntriesItemsExistance), + asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance), + asyncMiddleware(this.editEstimate) ); router.delete( - '/:id', - this.deleteEstimate.validation, + '/:id', [ + this.validateSpecificEstimateSchema, + ], validateMiddleware, - asyncMiddleware(this.deleteEstimate.handler) + asyncMiddleware(this.validateEstimateIdExistance), + asyncMiddleware(this.deleteEstimate) ); router.get( '/:id', - this.getEstimate.validation, + this.validateSpecificEstimateSchema, validateMiddleware, - asyncMiddleware(this.getEstimate.handler) + asyncMiddleware(this.validateEstimateIdExistance), + asyncMiddleware(this.getEstimate) ); router.get( '/', - this.getEstimates.validation, + this.validateEstimateListSchema, validateMiddleware, - asyncMiddleware(this.getEstimates.handler) + asyncMiddleware(this.getEstimates) ); - return router; - }, + } /** - * Handle create a new estimate with associated entries. + * Estimate validation schema. */ - newEstimate: { - validation: [ + static get estimateValidationSchema() { + return [ check('customer_id').exists().isNumeric().toInt(), check('estimate_date').exists().isISO8601(), check('expiration_date').optional().isISO8601(), check('reference').optional(), check('estimate_number').exists().trim().escape(), + check('entries').exists().isArray({ min: 1 }), + check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.description').optional().trim().escape(), check('entries.*.quantity').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.discount').optional().isNumeric().toFloat(), + check('note').optional().trim().escape(), check('terms_conditions').optional().trim().escape(), - ], - async handler(req, res) { - const estimate = { ...req.body }; - const isCustomerExists = await CustomersService.isCustomerExists( - estimate.customer_id - ); - - if (!isCustomerExists) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], - }); - } - const isEstNumberUnqiue = await SaleEstimateService.isEstimateNumberUnique( - estimate.estimate_number - ); - if (isEstNumberUnqiue) { - return res.boom.badRequest(null, { - errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], - }); - } - // Validate items ids in estimate entries exists. - const estimateItemsIds = estimate.entries.map(e => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds); - - if (notFoundItemsIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], - }); - } - const storedEstimate = await SaleEstimateService.createEstimate(estimate); - - return res.status(200).send({ id: storedEstimate.id }); - }, - }, + ]; + } /** - * Handle update estimate details with associated entries. + * Specific sale estimate validation schema. */ - editEstimate: { - validation: [ + static get validateSpecificEstimateSchema() { + return [ param('id').exists().isNumeric().toInt(), - - check('customer_id').exists().isNumeric().toInt(), - check('estimate_date').exists().isISO8601(), - check('expiration_date').optional().isISO8601(), - check('reference').optional(), - check('estimate_number').exists().trim().escape(), - check('entries').exists().isArray({ min: 1 }), - check('entries.*.id').optional().isNumeric().toInt(), - check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.description').optional().trim().escape(), - check('entries.*.quantity').exists().isNumeric().toInt(), - check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.discount').optional().isNumeric().toFloat(), - check('note').optional().trim().escape(), - check('terms_conditions').optional().trim().escape(), - ], - async handler(req, res) { - const { id: estimateId } = req.params; - const estimate = { ...req.body }; - const storedEstimate = await SaleEstimateService.getEstimate(estimateId); - - if (!storedEstimate) { - return res.status(404).send({ - errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], - }); - } - const isCustomerExists = await CustomersService.isCustomerExists( - estimate.customer_id - ); - if (!isCustomerExists) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], - }); - } - // Validate the estimate number is unique except on the current estimate id. - const foundEstimateNumbers = await SaleEstimateService.isEstimateNumberUnique( - estimate.estimate_number, - storedEstimate.id, // Exclude the given estimate id. - ); - if (foundEstimateNumbers) { - return res.boom.badRequest(null, { - errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], - }); - } - // Validate items ids in estimate entries exists. - const estimateItemsIds = estimate.entries.map(e => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds); - - if (notFoundItemsIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], - }); - } - // Validate the sale estimate entries IDs that not found. - const notFoundEntriesIds = await SaleEstimateService.isEstimateEntriesIDsExists( - storedEstimate.id, - estimate - ); - if (notFoundEntriesIds.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ESTIMATE.NOT.FOUND.ENTRIES.IDS', code: 500 }], - }); - } - // Update estimate with associated estimate entries. - await SaleEstimateService.editEstimate(estimateId, estimate); - - return res.status(200).send({ id: estimateId }); - }, - }, + ]; + } /** - * Deletes the given estimate with associated entries. + * Sales estimates list validation schema. */ - deleteEstimate: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id: estimateId } = req.params; - const isEstimateExists = await SaleEstimateService.isEstimateExists(estimateId); - - if (!isEstimateExists) { - return res.status(404).send({ - errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], - }); - } - await SaleEstimateService.deleteEstimate(estimateId); - - return res.status(200).send({ id: estimateId }); - }, - }, - - /** - * Retrieve the given estimate with associated entries. - */ - getEstimate: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id: estimateId } = req.params; - const estimate = await SaleEstimateService.getEstimateWithEntries(estimateId); - - if (!estimate) { - return res.status(404).send({ - errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], - }); - } - return res.status(200).send({ estimate }); - }, - }, - - /** - * Retrieve estimates with pagination metadata. - */ - getEstimates: { - validation: [ + static get validateEstimateListSchema() { + return [ query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), query('column_sort_by').optional(), query('sort_order').optional().isIn(['desc', 'asc']), - ], - async handler(req, res) { - const filter = { - filter_roles: [], - sort_order: 'asc', - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleEstimate, Resource, View } = req.models; - const resource = await Resource.tenant().query() - .remember() - .where('name', 'sales_estimates') - .withGraphFetched('fields') - .first(); + ] + } - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addView(viewMeta); - listingBuilder.addModelClass(SaleEstimate); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const salesEstimates = await SaleEstimate.query().onBuild((builder) => { - dynamicListing.buildQuery()(builder); - return builder; + /** + * Validate whether the estimate customer exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEstimateCustomerExistance(req, res, next) { + const estimate = { ...req.body }; + const isCustomerExists = await CustomersService.isCustomerExists( + estimate.customer_id + ); + if (!isCustomerExists) { + return res.status(404).send({ + errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }], }); + } + next(); + } - return res.status(200).send({ - sales_estimates: salesEstimates, + /** + * Validate the estimate number unique on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEstimateNumberExistance(req, res, next) { + const estimate = { ...req.body }; + + const isEstNumberUnqiue = await SaleEstimateService.isEstimateNumberUnique( + estimate.estimate_number, + req.params.id, + ); + if (isEstNumberUnqiue) { + return res.boom.badRequest(null, { + errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }], + }); + } + next(); + } + + /** + * Validate the estimate entries items ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEstimateEntriesItemsExistance(req, res, next) { + const estimate = { ...req.body }; + const estimateItemsIds = estimate.entries.map(e => e.item_id); + + // Validate items ids in estimate entries exists. + const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds); + + if (notFoundItemsIds.length > 0) { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }], + }); + } + next(); + } + + /** + * Validate whether the sale estimate id exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEstimateIdExistance(req, res, next) { + const { id: estimateId } = req.params; + const storedEstimate = await SaleEstimateService.getEstimate(estimateId); + + if (!storedEstimate) { + return res.status(404).send({ + errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }], + }); + } + next(); + } + + /** + * Validate sale invoice entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async valdiateInvoiceEntriesIdsExistance(req, res, next) { + const { id: saleInvoiceId } = req.params; + const saleInvoice = { ...req.body }; + const entriesIds = saleInvoice.entries + .filter(e => e.id) + .map((e) => e.id); + + const foundEntries = await ItemEntry.query() + .whereIn('id', entriesIds) + .where('reference_type', 'SaleInvoice') + .where('reference_id', saleInvoiceId); + + if (foundEntries.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + + /** + * Handle create a new estimate with associated entries. + * @param {Request} req - + * @param {Response} res - + * @return {Response} res - + */ + static async newEstimate(req, res) { + const estimate = { + ...req.body, + entries: req.body.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })), + }; + const storedEstimate = await SaleEstimateService.createEstimate(estimate); + + return res.status(200).send({ id: storedEstimate.id }); + } + + /** + * Handle update estimate details with associated entries. + * @param {Request} req + * @param {Response} res + */ + static async editEstimate(req, res) { + const { id: estimateId } = req.params; + const estimate = { ...req.body }; + + // Update estimate with associated estimate entries. + await SaleEstimateService.editEstimate(estimateId, estimate); + + return res.status(200).send({ id: estimateId }); + } + + /** + * Deletes the given estimate with associated entries. + * @param {Request} req + * @param {Response} res + */ + static async deleteEstimate(req, res) { + const { id: estimateId } = req.params; + await SaleEstimateService.deleteEstimate(estimateId); + + return res.status(200).send({ id: estimateId }); + } + + /** + * Retrieve the given estimate with associated entries. + */ + static async getEstimate(req, res) { + const { id: estimateId } = req.params; + const estimate = await SaleEstimateService.getEstimateWithEntries(estimateId); + + return res.status(200).send({ estimate }); + } + + /** + * Retrieve estimates with pagination metadata. + * @param {Request} req + * @param {Response} res + */ + static async getEstimates(req, res) { + const filter = { + filter_roles: [], + sort_order: 'asc', + page: 1, + page_size: 10, + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { SaleEstimate, Resource, View } = req.models; + const resource = await Resource.tenant().query() + .remember() + .where('name', 'sales_estimates') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], + }); + } + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addView(viewMeta); + listingBuilder.addModelClass(SaleEstimate); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + + const salesEstimates = await SaleEstimate.query().onBuild((query) => { + dynamicListing.buildQuery()(builder); + return builder; + }).pagination(filter.page - 1, filter.page_size); + + return res.status(200).send({ + sales_estimates: { + ...salesEstimates, ...(viewMeta ? { - custom_view_id: viewMeta.id, + viewMeta: { + custom_view_id: viewMeta.id, + }, } : {}), - }); - }, - }, + }, + + }); + } }; diff --git a/server/src/http/controllers/Sales/SalesInvoices.js b/server/src/http/controllers/Sales/SalesInvoices.js index 02b26b4ac..703761e36 100644 --- a/server/src/http/controllers/Sales/SalesInvoices.js +++ b/server/src/http/controllers/Sales/SalesInvoices.js @@ -1,52 +1,73 @@ import express from 'express'; import { check, param, query } from 'express-validator'; +import { ItemEntry } from '@/models'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import SaleInvoiceService from '@/services/Sales/SaleInvoice'; import ItemsService from '@/services/Items/ItemsService'; import CustomersService from '@/services/Customers/CustomersService'; -import { SaleInvoice } from '@/models'; -import DynamicListing, { DYNAMIC_LISTING_ERRORS } from '@/services/DynamicListing/DynamicListing'; -import DynamicListingBuilder from '../../../services/DynamicListing/DynamicListingBuilder'; -import { - dynamicListingErrorsToResponse -} from '@/services/DynamicListing/hasDynamicListing'; +import DynamicListing from '@/services/DynamicListing/DynamicListing'; +import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder'; +import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing'; +import { SaleInvoice } from '../../../models'; +import { difference } from 'lodash'; -export default { - router() { +export default class SaleInvoicesController { + /** + * Router constructor. + */ + static router() { const router = express.Router(); router.post( '/', - this.newSaleInvoice.validation, + this.saleInvoiceValidationSchema, validateMiddleware, - asyncMiddleware(this.newSaleInvoice.handler) + asyncMiddleware(this.validateInvoiceNumberUnique), + asyncMiddleware(this.validateInvoiceItemsIdsExistance), + asyncMiddleware(this.newSaleInvoice) ); router.post( '/:id', - this.editSaleInvoice.validation, + [ + ...this.saleInvoiceValidationSchema, + ...this.specificSaleInvoiceValidation, + ], validateMiddleware, - asyncMiddleware(this.editSaleInvoice.handler) + asyncMiddleware(this.validateInvoiceExistance), + asyncMiddleware(this.validateInvoiceNumberUnique), + asyncMiddleware(this.validateInvoiceItemsIdsExistance), + asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance), + asyncMiddleware(this.validateEntriesIdsExistance), + asyncMiddleware(this.editSaleInvoice) ); router.delete( '/:id', - this.deleteSaleInvoice.validation, + this.specificSaleInvoiceValidation, validateMiddleware, - asyncMiddleware(this.deleteSaleInvoice.handler) + asyncMiddleware(this.validateInvoiceExistance), + asyncMiddleware(this.deleteSaleInvoice) + ); + router.get( + '/:id', + this.specificSaleInvoiceValidation, + validateMiddleware, + asyncMiddleware(this.validateInvoiceExistance), + asyncMiddleware(this.getSaleInvoice) ); router.get( '/', - this.getSalesInvoices.validation, - asyncMiddleware(this.getSalesInvoices.handler) - ); + this.saleInvoiceListValidationSchema, + asyncMiddleware(this.getSalesInvoices) + ); return router; - }, + } /** - * Creates a new sale invoice. + * Sale invoice validation schema. */ - newSaleInvoice: { - validation: [ + static get saleInvoiceValidationSchema() { + return [ check('customer_id').exists().isNumeric().toInt(), check('invoice_date').exists().isISO8601(), check('due_date').exists().isISO8601(), @@ -58,204 +79,295 @@ export default { check('terms_conditions').optional().trim().escape(), check('entries').exists().isArray({ min: 1 }), + + check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.discount').optional().isNumeric().toFloat(), check('entries.*.description').optional().trim().escape(), - ], - async handler(req, res) { - const errorReasons = []; - const saleInvoice = { ...req.body }; - const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists( - saleInvoice.invoice_no - ); - if (isInvoiceNoExists) { - errorReasons.push({ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }); - } - const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); - const isItemsIdsExists = await ItemsService.isItemsIdsExists( - entriesItemsIds - ); - if (isItemsIdsExists.length > 0) { - errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }); - } - // Validate the customer id exists. - const isCustomerIDExists = await CustomersService.isCustomerExists( - saleInvoice.customer_id - ); - if (!isCustomerIDExists) { - errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Creates a new sale invoice with associated entries. - const storedSaleInvoice = await SaleInvoiceService.createSaleInvoice( - saleInvoice - ); - return res.status(200).send({ id: storedSaleInvoice.id }); - }, - }, + ]; + } /** - * Edit sale invoice details. + * Specific sale invoice validation schema. */ - editSaleInvoice: { - validation: [ - param('id').exists().isNumeric().toInt(), + static get specificSaleInvoiceValidation() { + return [param('id').exists().isNumeric().toInt()]; + } - check('customer_id').exists().isNumeric().toInt(), - check('invoice_date').exists(), - check('due_date').exists(), - check('invoice_no').exists().trim().escape(), - check('reference_no').optional().trim().escape(), - check('status').exists().trim().escape(), - - check('invoice_message').optional().trim().escape(), - check('terms_conditions').optional().trim().escape(), - - check('entries').exists().isArray({ min: 1 }), - check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.quantity').exists().isNumeric().toFloat(), - check('entries.*.discount').optional().isNumeric().toFloat(), - check('entries.*.description').optional().trim().escape(), - ], - async handler(req, res) { - const { id: saleInvoiceId } = req.params; - const saleInvoice = { ...req.body }; - const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists( - saleInvoiceId - ); - if (!isSaleInvoiceExists) { - return res - .status(404) - .send({ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }); - } - const errorReasons = []; - - // Validate the invoice number uniqness. - const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists( - saleInvoice.invoice_no, - saleInvoiceId - ); - if (isInvoiceNoExists) { - errorReasons.push({ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }); - } - // Validate sale invoice entries items IDs. - const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); - const isItemsIdsExists = await ItemsService.isItemsIdsExists( - entriesItemsIds - ); - if (isItemsIdsExists.length > 0) { - errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }); - } - // Validate the customer id exists. - const isCustomerIDExists = await CustomersService.isCustomerExists( - saleInvoice.customer_id - ); - if (!isCustomerIDExists) { - errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Update the given sale invoice details. - await SaleInvoiceService.editSaleInvoice(saleInvoiceId, saleInvoice); - - return res.status(200).send({ id: saleInvoice.id }); - }, - }, - - /** - * Deletes the sale invoice with associated entries and journal transactions. - */ - deleteSaleInvoice: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id: saleInvoiceId } = req.params; - const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists( - saleInvoiceId - ); - if (!isSaleInvoiceExists) { - return res - .status(404) - .send({ errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }] }); - } - // Deletes the sale invoice with associated entries and journal transaction. - await SaleInvoiceService.deleteSaleInvoice(saleInvoiceId); - - return res.status(200).send(); - }, - }, - - /** - * Retrieve paginated sales invoices with custom view metadata. - */ - getSalesInvoices: { - validation: [ + static get saleInvoiceListValidationSchema() { + return [ query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), query('column_sort_by').optional(), query('sort_order').optional().isIn(['desc', 'asc']), - ], - async handler(req, res) { - const filter = { - filter_roles: [], - sort_order: 'asc', - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleInvoice, Resource } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'sales_invoices') - .withGraphFetched('fields') - .first(); + ]; + } - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'SALES_INVOICES_RESOURCE_NOT_FOUND', code: 200 }], - }); - } - const viewMeta = View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(SaleInvoice); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(dynamicListingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const salesInvoices = await SaleInvoice.query().onBuild((builder) => { - dynamicListing.buildQuery()(builder); + /** + * Validate whether sale invoice customer exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateInvoiceCustomerExistance(req, res, next) { + const saleInvoice = { ...req.body }; + const isCustomerIDExists = await CustomersService.isCustomerExists( + saleInvoice.customer_id + ); + if (!isCustomerIDExists) { + return res.status(400).send({ + errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }], }); + } + next(); + } - return res.status(200).send({ - sales_invoices: salesInvoices, + /** + * Validate whether sale invoice items ids esits on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateInvoiceItemsIdsExistance(req, res, next) { + const saleInvoice = { ...req.body }; + const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); + const isItemsIdsExists = await ItemsService.isItemsIdsExists( + entriesItemsIds + ); + if (isItemsIdsExists.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + + /** + * Validate whether sale invoice number unqiue on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateInvoiceNumberUnique(req, res, next) { + const saleInvoice = { ...req.body }; + const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists( + saleInvoice.invoice_no, + req.params.id + ); + if (isInvoiceNoExists) { + return res + .status(400) + .send({ + errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }], + }); + } + next(); + } + + /** + * Validate whether sale invoice exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateInvoiceExistance(req, res, next) { + const { id: saleInvoiceId } = req.params; + const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists( + saleInvoiceId + ); + if (!isSaleInvoiceExists) { + return res + .status(404) + .send({ errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }] }); + } + next(); + } + + /** + * Validate sale invoice entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async valdiateInvoiceEntriesIdsExistance(req, res, next) { + const saleInvoice = { ...req.body }; + const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id); + + const isItemsIdsExists = await ItemsService.isItemsIdsExists( + entriesItemsIds + ); + if (isItemsIdsExists.length > 0) { + return res.status(400).send({ + errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + + /** + * Validate whether the sale estimate entries IDs exist on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateEntriesIdsExistance(req, res, next) { + const { id: saleInvoiceId } = req.params; + const saleInvoice = { ...req.body }; + const entriesIds = saleInvoice.entries + .filter(e => e.id) + .map(e => e.id); + + const storedEntries = await ItemEntry.tenant().query() + .whereIn('reference_id', [saleInvoiceId]) + .whereIn('reference_type', ['SaleInvoice']); + + const storedEntriesIds = storedEntries.map((entry) => entry.id); + const notFoundEntriesIds = difference( + entriesIds, + storedEntriesIds, + ); + if (notFoundEntriesIds.length > 0) { + return res.boom.badRequest(null, { + errors: [{ type: 'SALE.INVOICE.ENTRIES.IDS.NOT.FOUND', code: 500 }], + }); + } + next(); + } + + /** + * Creates a new sale invoice. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async newSaleInvoice(req, res) { + const errorReasons = []; + const saleInvoice = { + ...req.body, + entries: req.body.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })), + }; + // Creates a new sale invoice with associated entries. + const storedSaleInvoice = await SaleInvoiceService.createSaleInvoice( + saleInvoice + ); + return res.status(200).send({ id: storedSaleInvoice.id }); + } + + /** + * Edit sale invoice details. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async editSaleInvoice(req, res) { + const { id: saleInvoiceId } = req.params; + const saleInvoice = { ...req.body }; + + // Update the given sale invoice details. + await SaleInvoiceService.editSaleInvoice(saleInvoiceId, saleInvoice); + + return res.status(200).send({ id: saleInvoice.id }); + } + + /** + * Deletes the sale invoice with associated entries and journal transactions. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async deleteSaleInvoice(req, res) { + const { id: saleInvoiceId } = req.params; + // Deletes the sale invoice with associated entries and journal transaction. + await SaleInvoiceService.deleteSaleInvoice(saleInvoiceId); + + return res.status(200).send({ id: saleInvoiceId }); + } + + /** + * Retrieve the sale invoice with associated entries. + * @param {Request} req + * @param {Response} res + */ + static async getSaleInvoice(req, res) { + const { id: saleInvoiceId } = req.params; + const saleInvoice = await SaleInvoiceService.getSaleInvoiceWithEntries( + saleInvoiceId + ); + return res.status(200).send({ sale_invoice: saleInvoice }); + } + + /** + * Retrieve paginated sales invoices with custom view metadata. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async getSalesInvoices(req, res) { + const filter = { + filter_roles: [], + sort_order: 'asc', + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { SaleInvoice, View, Resource } = req.models; + const resource = await Resource.query() + .remember() + .where('name', 'sales_invoices') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'SALES_INVOICES_RESOURCE_NOT_FOUND', code: 200 }], + }); + } + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addModelClass(SaleInvoice); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + listingBuilder.addView(viewMeta); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + const salesInvoices = await SaleInvoice.query().onBuild((builder) => { + builder.withGraphFetched('entries'); + dynamicListing.buildQuery()(builder); + }).pagination(filter.page - 1, filter.page_size); + + return res.status(200).send({ + sales_invoices: { + ...salesInvoices, ...(viewMeta ? { + view_meta: { customViewId: viewMeta.id, } + } : {}), - }); - }, - }, -}; + }, + }); + } +} diff --git a/server/src/http/controllers/Sales/SalesReceipt.js b/server/src/http/controllers/Sales/SalesReceipt.js index 52f40f8d2..8162b4532 100644 --- a/server/src/http/controllers/Sales/SalesReceipt.js +++ b/server/src/http/controllers/Sales/SalesReceipt.js @@ -1,5 +1,6 @@ import express from 'express'; import { check, param, query } from 'express-validator'; +import { ItemEntry } from '@/models'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import CustomersService from '@/services/Customers/CustomersService'; @@ -12,45 +13,57 @@ import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/HasDynamicListing'; -export default { +export default class SalesReceiptsController { /** * Router constructor. */ - router() { + static router() { const router = express.Router(); router.post( - '/:id', - this.editSaleReceipt.validation, + '/:id', [ + ...this.specificReceiptValidationSchema, + ...this.salesReceiptsValidationSchema, + ], validateMiddleware, - asyncMiddleware(this.editSaleReceipt.handler) + asyncMiddleware(this.validateSaleReceiptExistance), + asyncMiddleware(this.validateReceiptCustomerExistance), + asyncMiddleware(this.validateReceiptDepositAccountExistance), + asyncMiddleware(this.validateReceiptItemsIdsExistance), + asyncMiddleware(this.validateReceiptEntriesIds), + asyncMiddleware(this.editSaleReceipt) ); router.post( '/', - this.newSaleReceipt.validation, + this.salesReceiptsValidationSchema, validateMiddleware, - asyncMiddleware(this.newSaleReceipt.handler) + asyncMiddleware(this.validateReceiptCustomerExistance), + asyncMiddleware(this.validateReceiptDepositAccountExistance), + asyncMiddleware(this.validateReceiptItemsIdsExistance), + asyncMiddleware(this.newSaleReceipt) ); router.delete( '/:id', - this.deleteSaleReceipt.handler, + this.specificReceiptValidationSchema, validateMiddleware, - asyncMiddleware(this.deleteSaleReceipt.handler) + asyncMiddleware(this.validateSaleReceiptExistance), + asyncMiddleware(this.deleteSaleReceipt) ); router.get( '/', - this.listingSalesReceipts.validation, + this.listingSalesReceipts, validateMiddleware, - asyncMiddleware(this.listingSalesReceipts.handler) + asyncMiddleware(this.listingSalesReceipts) ); return router; - }, + } /** - * Creates a new receipt. + * Sales receipt validation schema. + * @return {Array} */ - newSaleReceipt: { - validation: [ + static get salesReceiptsValidationSchema() { + return [ check('customer_id').exists().isNumeric().toInt(), check('deposit_account_id').exists().isNumeric().toInt(), check('receipt_date').exists().isISO8601(), @@ -58,92 +71,9 @@ export default { check('reference_no').optional().trim().escape(), check('entries').exists().isArray({ min: 1 }), - - check('entries.*.item_id').exists().isNumeric().toInt(), - check('entries.*.description').optional().trim().escape(), - check('entries.*.quantity').exists().isNumeric().toInt(), - check('entries.*.rate').exists().isNumeric().toInt(), - check('entries.*.discount').optional().isNumeric().toInt(), - - check('receipt_message').optional().trim().escape(), - check('statement').optional().trim().escape(), - ], - async handler(req, res) { - const saleReceipt = { ...req.body }; - - const isCustomerExists = await CustomersService.isCustomerExists( - saleReceipt.customer_id - ); - const isDepositAccountExists = await AccountsService.isAccountExists( - saleReceipt.deposit_account_id - ); - const errorReasons = []; - - if (!isCustomerExists) { - errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }); - } - if (!isDepositAccountExists) { - errorReasons.push({ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }); - } - // Validate items ids in estimate entries exists. - const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists( - estimateItemsIds - ); - if (notFoundItemsIds.length > 0) { - errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Store the given sale receipt details with associated entries. - const storedSaleReceipt = await SaleReceiptService.createSaleReceipt( - saleReceipt - ); - - return res.status(200).send({ id: storedSaleReceipt.id }); - }, - }, - - /** - * Deletes the sale receipt with associated entries and journal transactions. - */ - deleteSaleReceipt: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id: saleReceiptId } = req.params; - const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists( - saleReceiptId - ); - - if (!isSaleReceiptExists) { - return res.status(404).send({ - errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }], - }); - } - // Deletes the sale receipt. - await SaleReceiptService.deleteSaleReceipt(saleReceiptId); - - return res.status(200).send({ id: saleReceiptId }); - }, - }, - - /** - * Edit the sale receipt details with associated entries and re-write - * journal transaction on the same date. - */ - editSaleReceipt: { - validation: [ - param('id').exists().isNumeric().toInt(), - - check('customer_id').exists().isNumeric().toInt(), - check('deposit_account_id').exists().isNumeric().toInt(), - check('receipt_date').exists().isISO8601(), - check('send_to_email').optional().isEmail(), - check('reference_no').optional().trim().escape(), - - check('entries').exists().isArray({ min: 1 }), + check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), + check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.description').optional().trim().escape(), check('entries.*.quantity').exists().isNumeric().toInt(), @@ -152,125 +82,244 @@ export default { check('receipt_message').optional().trim().escape(), check('statement').optional().trim().escape(), - ], - async handler(req, res) { - const { id: saleReceiptId } = req.params; - const saleReceipt = { ...req.body }; - - const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists( - saleReceiptId - ); - if (!isSaleReceiptExists) { - return res.status(404).send({ - errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }], - }); - } - const isCustomerExists = await CustomersService.isCustomerExists( - saleReceipt.customer_id - ); - const isDepositAccountExists = await AccountsService.isAccountsExists( - saleReceipt.deposit_account_id - ); - const errorReasons = []; - - if (!isCustomerExists) { - errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }); - } - if (!isDepositAccountExists) { - errorReasons.push({ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }); - } - // Validate items ids in estimate entries exists. - const entriesItemsIDs = saleReceipt.entries.map((e) => e.item_id); - const notFoundItemsIds = await ItemsService.isItemsIdsExists( - entriesItemsIDs - ); - if (notFoundItemsIds.length > 0) { - errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }); - } - // Validate the entries IDs that not stored or associated to the sale receipt. - const notExistsEntriesIds = await SaleReceiptService.isSaleReceiptEntriesIDsExists( - saleReceiptId, - saleReceipt - ); - if (notExistsEntriesIds.length > 0) { - errorReasons.push({ - type: 'ENTRIES.IDS.NOT.FOUND', - code: 500, - }); - } - // Handle all errors with reasons messages. - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - // Update the given sale receipt details. - await SaleReceiptService.editSaleReceipt(saleReceiptId, saleReceipt); - - return res.status(200).send(); - }, - }, + ]; + } /** - * Listing sales receipts. + * Specific sale receipt validation schema. */ - listingSalesReceipts: { - validation: [ + static get specificReceiptValidationSchema() { + return [ + param('id').exists().isNumeric().toInt() + ]; + } + + /** + * List sales receipts validation schema. + */ + static get listSalesReceiptsValidationSchema() { + return [ query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), query('column_sort_by').optional(), query('sort_order').optional().isIn(['desc', 'asc']), - ], - async handler(req, res) { - const filter = { - filter_roles: [], - sort_order: 'asc', - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleReceipt, Resource, View } = req.models; - const resource = await Resource.tenant().query() - .remember() - .where('name', 'sales_receipts') - .withGraphFetched('fields') - .first(); + ]; + } - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addView(viewMeta); - listingBuilder.addModelClass(SaleReceipt); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - const salesReceipts = await SaleReceipt.query().onBuild((builder) => { - dynamicListing.buildQuery()(builder); - return builder; + /** + * Validate whether sale receipt exists on the storage. + * @param {Request} req + * @param {Response} res + */ + static async validateSaleReceiptExistance(req, res, next) { + const { id: saleReceiptId } = req.params; + const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists( + saleReceiptId + ); + if (!isSaleReceiptExists) { + return res.status(404).send({ + errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }], }); + } + next(); + } - return res.status(200).send({ - sales_receipts: salesReceipts, - ...(viewMeta ? { - customViewId: viewMeta.id, - } : {}), + /** + * Validate whether sale receipt customer exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateReceiptCustomerExistance(req, res, next) { + const saleReceipt = { ...req.body }; + const isCustomerExists = await CustomersService.isCustomerExists( + saleReceipt.customer_id + ); + if (!isCustomerExists) { + return res.status(400).send({ + errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }], }); - }, - }, + } + next(); + } + + /** + * Validate whether sale receipt deposit account exists on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateReceiptDepositAccountExistance(req, res, next) { + const saleReceipt = { ...req.body }; + const isDepositAccountExists = await AccountsService.isAccountExists( + saleReceipt.deposit_account_id + ); + if (!isDepositAccountExists) { + return res.status(400).send({ + errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }], + }); + } + next(); + } + + /** + * Validate whether receipt items ids exist on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateReceiptItemsIdsExistance(req, res, next) { + const saleReceipt = { ...req.body }; + const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id); + const notFoundItemsIds = await ItemsService.isItemsIdsExists( + estimateItemsIds + ); + if (notFoundItemsIds.length > 0) { + return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] }); + } + next(); + } + + /** + * Validate receipt entries ids existance on the storage. + * @param {Request} req + * @param {Response} res + * @param {Function} next + */ + static async validateReceiptEntriesIds(req, res, next) { + const saleReceipt = { ...req.body }; + const { id: saleReceiptId } = req.params; + + // Validate the entries IDs that not stored or associated to the sale receipt. + const notExistsEntriesIds = await SaleReceiptService.isSaleReceiptEntriesIDsExists( + saleReceiptId, + saleReceipt + ); + if (notExistsEntriesIds.length > 0) { + return res.status(400).send({ errors: [{ + type: 'ENTRIES.IDS.NOT.FOUND', + code: 500, + }] + }); + } + next(); + } + + /** + * Creates a new receipt. + * @param {Request} req + * @param {Response} res + */ + static async newSaleReceipt(req, res) { + const saleReceipt = { + ...req.body, + entries: req.body.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })), + }; + + // Store the given sale receipt details with associated entries. + const storedSaleReceipt = await SaleReceiptService.createSaleReceipt( + saleReceipt + ); + return res.status(200).send({ id: storedSaleReceipt.id }); + } + + /** + * Deletes the sale receipt with associated entries and journal transactions. + * @param {Request} req + * @param {Response} res + */ + static async deleteSaleReceipt(req, res) { + const { id: saleReceiptId } = req.params; + + // Deletes the sale receipt. + await SaleReceiptService.deleteSaleReceipt(saleReceiptId); + + return res.status(200).send({ id: saleReceiptId }); + } + + /** + * Edit the sale receipt details with associated entries and re-write + * journal transaction on the same date. + * @param {Request} req + * @param {Response} res + */ + static async editSaleReceipt(req, res) { + const { id: saleReceiptId } = req.params; + const saleReceipt = { ...req.body }; + const errorReasons = []; + + // Handle all errors with reasons messages. + if (errorReasons.length > 0) { + return res.boom.badRequest(null, { errors: errorReasons }); + } + // Update the given sale receipt details. + await SaleReceiptService.editSaleReceipt(saleReceiptId, saleReceipt); + + return res.status(200).send(); + } + + /** + * Listing sales receipts. + * @param {Request} req + * @param {Response} res + */ + static async listingSalesReceipts(req, res) { + const filter = { + filter_roles: [], + sort_order: 'asc', + page: 1, + page_size: 10, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + const { SaleReceipt, Resource, View } = req.models; + const resource = await Resource.tenant().query() + .remember() + .where('name', 'sales_receipts') + .withGraphFetched('fields') + .first(); + + if (!resource) { + return res.status(400).send({ + errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], + }); + } + const viewMeta = await View.query() + .modify('allMetadata') + .modify('specificOrFavourite', filter.custom_view_id) + .where('resource_id', resource.id) + .first(); + + const listingBuilder = new DynamicListingBuilder(); + const errorReasons = []; + + listingBuilder.addView(viewMeta); + listingBuilder.addModelClass(SaleReceipt); + listingBuilder.addCustomViewId(filter.custom_view_id); + listingBuilder.addFilterRoles(filter.filter_roles); + listingBuilder.addSortBy(filter.sort_by, filter.sort_order); + + const dynamicListing = new DynamicListing(listingBuilder); + + if (dynamicListing instanceof Error) { + const errors = dynamicListingErrorsToResponse(dynamicListing); + errorReasons.push(...errors); + } + const salesReceipts = await SaleReceipt.query().onBuild((builder) => { + builder.withGraphFetched('entries'); + dynamicListing.buildQuery()(builder); + return builder; + }).pagination(filter.page - 1, filter.page_size); + + return res.status(200).send({ + sales_receipts: salesReceipts, + ...(viewMeta ? { + customViewId: viewMeta.id, + } : {}), + }); + } }; diff --git a/server/src/interfaces/BillPayment.ts b/server/src/interfaces/BillPayment.ts new file mode 100644 index 000000000..8b6c22098 --- /dev/null +++ b/server/src/interfaces/BillPayment.ts @@ -0,0 +1,13 @@ + + +export interface IBillPaymentEntry { + billId: number, + paymentAmount: number, +}; + +export interface IBillPayment { + amount: number, + reference: string, + billNo: string, + entries: IBillPaymentEntry[], +} \ No newline at end of file diff --git a/server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js b/server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js new file mode 100644 index 000000000..6aa08ab82 --- /dev/null +++ b/server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js @@ -0,0 +1,27 @@ +import { QueryBuilder } from "knex" +import { QueryBuilder } from 'objection'; + +export default class BulkOperationsQueryBuilder extends QueryBuilder { + + bulkInsert(collection) { + const opers = []; + + collection.forEach((dataset) => { + const insertOper = this.insert({ ...dataset }); + opers.push(insertOper); + }); + return Promise.all(opers); + } + + bulkDelete(rowsIds) { + + } + + bulkUpdate(dataset, whereColumn) { + + } + + bulkPatch(newDataset, oldDataset) { + + } +} \ No newline at end of file diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index e182d5a02..b4a6e6e09 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -6,6 +6,13 @@ import CachableModel from '@/lib/Cachable/CachableModel'; export default class Bill extends mixin(TenantModel, [CachableModel]) { + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['dueAmount']; + } + /** * Table name */ @@ -21,9 +28,9 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { } /** - * Extend query builder model. + * Due amount of the given. */ - static get QueryBuilder() { - return CachableQueryBuilder; + get dueAmount() { + return Math.max(this.balance - this.paymentAmount, 0); } } diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 3b07d61fc..920161756 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -1,4 +1,4 @@ -import { mixin } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; import CachableModel from '@/lib/Cachable/CachableModel'; @@ -25,4 +25,33 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { static get QueryBuilder() { return CachableQueryBuilder; } + + static changePaymentAmount(billId, amount) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + return this.tenant() + .query() + .where('id', billId) + [changeMethod]('payment_amount', Math.abs(amount)); + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const BillPaymentEntry = require('@/models/BillPaymentEntry'); + + return { + /** + * Account model may belongs to account type. + */ + entries: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(BillPaymentEntry.default), + join: { + from: 'bills_payments.id', + to: 'bills_payments_entries.billPaymentId', + }, + }, + }; + } } diff --git a/server/src/models/BillPaymentEntry.js b/server/src/models/BillPaymentEntry.js new file mode 100644 index 000000000..39efdc1c0 --- /dev/null +++ b/server/src/models/BillPaymentEntry.js @@ -0,0 +1,18 @@ +import { mixin } from 'objection'; +import TenantModel from '@/models/TenantModel'; + +export default class BillPaymentEntry extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'bills_payments_entries'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } +} diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js index 03c306bba..579411741 100644 --- a/server/src/models/Customer.js +++ b/server/src/models/Customer.js @@ -26,4 +26,42 @@ export default class Customer extends TenantModel { }, }; } + + /** + * Change vendor balance. + * @param {Integer} customerId + * @param {Numeric} amount + */ + static async changeBalance(customerId, amount) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + + await this.tenant() + .query() + .where('id', customerId) + [changeMethod]('balance', Math.abs(amount)); + } + + /** + * Increment the given customer balance. + * @param {Integer} customerId + * @param {Integer} amount + */ + static async incrementBalance(customerId, amount) { + await this.tenant() + .query() + .where('id', customerId) + .increment('balance', amount); + } + + /** + * Decrement the given customer balance. + * @param {integer} customerId - + * @param {integer} amount - + */ + static async decrementBalance(customerId, amount) { + await this.tenant() + .query() + .where('id', customerId) + .decrement('balance', amount); + } } diff --git a/server/src/models/InventoryTransaction.js b/server/src/models/InventoryTransaction.js new file mode 100644 index 000000000..56b19b8d0 --- /dev/null +++ b/server/src/models/InventoryTransaction.js @@ -0,0 +1,18 @@ +import { Model } from 'objection'; +import TenantModel from '@/models/TenantModel'; + +export default class InventoryTransaction extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'inventory_transactions'; + } + + /** + * Model timestamps. + */ + static get timestamps() { + return ['createdAt', 'updatedAt']; + } +} diff --git a/server/src/models/ItemEntry.js b/server/src/models/ItemEntry.js new file mode 100644 index 000000000..7829b907c --- /dev/null +++ b/server/src/models/ItemEntry.js @@ -0,0 +1,35 @@ +import path from 'path'; +import { Model } from 'objection'; +import TenantModel from '@/models/TenantModel'; + +export default class ItemEntry extends TenantModel { + /** + * Table name. + */ + static get tableName() { + return 'items_entries'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + return { + + }; + } + + static calcAmount(itemEntry) { + const { discount, quantity, rate } = itemEntry; + const total = quantity * rate; + + return discount ? total - (total * discount * 0.01) : total; + } +} diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index 714f5366a..2ef72cc7b 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -19,13 +19,6 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel]) return ['created_at', 'updated_at']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ @@ -38,7 +31,7 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel]) modelClass: this.relationBindKnex(PaymentReceiveEntry.default), join: { from: 'payment_receives.id', - to: 'payment_receives_entries.payment_receive_id', + to: 'payment_receives_entries.paymentReceiveId', }, }, }; diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index b3a750930..09c9d9681 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -1,10 +1,8 @@ import { Model, mixin } from 'objection'; -import moment from 'moment'; import TenantModel from '@/models/TenantModel'; import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; import CachableModel from '@/lib/Cachable/CachableModel'; - export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) { /** * Table name @@ -20,26 +18,19 @@ export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) { return ['createdAt', 'updatedAt']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ static get relationMappings() { - const SaleEstimateEntry = require('@/models/SaleEstimateEntry'); + const ItemEntry = require('@/models/ItemEntry'); return { entries: { relation: Model.HasManyRelation, - modelClass: this.relationBindKnex(SaleEstimateEntry.default), + modelClass: this.relationBindKnex(ItemEntry.default), join: { from: 'sales_estimates.id', - to: 'sales_estimate_entries.id', + to: 'items_entries.referenceId', }, }, }; diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index f20f92ea1..d08efb8bd 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -5,6 +5,13 @@ import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; import CachableModel from '@/lib/Cachable/CachableModel'; export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['dueAmount']; + } + /** * Table name */ @@ -20,27 +27,41 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { } /** - * Extend query builder model. + * Due amount of the given. */ - static get QueryBuilder() { - return CachableQueryBuilder; + get dueAmount() { + return Math.max(this.balance - this.paymentAmount, 0); } /** * Relationship mapping. */ static get relationMappings() { - const SaleInvoiceEntry = require('@/models/SaleInvoiceEntry'); + const ItemEntry = require('@/models/ItemEntry'); return { entries: { relation: Model.HasManyRelation, - modelClass: this.relationBindKnex(SaleInvoiceEntry.default), + modelClass: this.relationBindKnex(ItemEntry.default), join: { from: 'sales_invoices.id', - to: 'sales_invoices_entries.sale_invoice_id', + to: 'items_entries.referenceId', }, }, }; } + + /** + * Change payment amount. + * @param {Integer} invoiceId + * @param {Numeric} amount + */ + static async changePaymentAmount(invoiceId, amount) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + + await this.tenant() + .query() + .where('id', invoiceId) + [changeMethod]('payment_amount', Math.abs(amount)); + } } diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index da0e0568d..ac15cc1b4 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -19,26 +19,19 @@ export default class SaleReceipt extends mixin(TenantModel, [CachableModel]) { return ['created_at', 'updated_at']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ static get relationMappings() { - const SaleReceiptEntry = require('@/models/SaleReceiptEntry'); + const ItemEntry = require('@/models/ItemEntry'); return { entries: { relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(SaleReceiptEntry.default), + modelClass: this.relationBindKnex(ItemEntry.default), join: { from: 'sales_receipts.id', - to: 'sales_receipt_entries.sale_receipt_id', + to: 'items_entries.referenceId', }, }, }; diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js index e7bf4c815..035a6fd67 100644 --- a/server/src/models/Vendor.js +++ b/server/src/models/Vendor.js @@ -15,4 +15,41 @@ export default class Vendor extends TenantModel { static get timestamps() { return ['createdAt', 'updatedAt']; } + + /** + * Changes the vendor balance. + * @param {Integer} customerId + * @param {Number} amount + * @return {Promise} + */ + static async changeBalance(vendorId, amount) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + + return this.tenant() + .query() + .where('id', vendorId) + [changeMethod]('balance', Math.abs(amount)); + } + + static changeDiffBalance(vendorId, oldVendorId, amount, oldAmount) { + const diffAmount = (amount - oldAmount) * -1; + const asyncOpers = []; + + if (vendorId != oldVendorId) { + const oldVendorOper = Vendor.changeBalance( + oldVendorId, + oldAmount + ); + const vendorOper = Vendor.changeBalance( + vendorId, + (amount + diffAmount) * -1 + ); + asyncOpers.push(vendorOper); + asyncOpers.push(oldVendorOper); + } else { + const balanceChangeOper = Vendor.changeBalance(vendorId, diffAmount); + asyncOpers.push(balanceChangeOper); + } + return Promise.all(asyncOpers); + } } diff --git a/server/src/models/index.js b/server/src/models/index.js index afd64545b..e46b03571 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -13,8 +13,12 @@ import PaymentReceive from './PaymentReceive'; import PaymentReceiveEntry from './PaymentReceiveEntry'; import Bill from './Bill'; import BillPayment from './BillPayment'; +import BillPaymentEntry from './BillPaymentEntry'; import Resource from './Resource'; import View from './View'; +import ItemEntry from './ItemEntry'; +import InventoryTransaction from './InventoryTransaction'; +import AccountType from './AccountType'; export { Customer, @@ -32,6 +36,10 @@ export { PaymentReceiveEntry, Bill, BillPayment, + BillPaymentEntry, Resource, View, + ItemEntry, + InventoryTransaction, + AccountType, }; \ No newline at end of file diff --git a/server/src/repositories/CustomerRepository.js b/server/src/repositories/CustomerRepository.js new file mode 100644 index 000000000..bb41dee8f --- /dev/null +++ b/server/src/repositories/CustomerRepository.js @@ -0,0 +1,26 @@ +import { Customer } from '@/models'; + +export default class CustomerRepository { + + static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) { + const diffAmount = (amount - oldAmount) * -1; + const asyncOpers = []; + + if (customerId != oldCustomerId) { + const oldCustomerOper = Customer.changeBalance( + oldCustomerId, + oldAmount + ); + const customerOper = Customer.changeBalance( + customerId, + (amount + diffAmount) * -1 + ); + asyncOpers.push(customerOper); + asyncOpers.push(oldCustomerOper); + } else { + const balanceChangeOper = Customer.changeBalance(customerId, diffAmount); + asyncOpers.push(balanceChangeOper); + } + return Promise.all(asyncOpers); + } +} diff --git a/server/src/repositories/ItemEntryRepository.js b/server/src/repositories/ItemEntryRepository.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/repositories/PaymentReceiveEntryRepository.js b/server/src/repositories/PaymentReceiveEntryRepository.js new file mode 100644 index 000000000..f7503c6fe --- /dev/null +++ b/server/src/repositories/PaymentReceiveEntryRepository.js @@ -0,0 +1,55 @@ +import { omit } from 'lodash'; +import BaseModelRepository from '@/repositories/BaseModelRepository'; +import { PaymentReceiveEntry } from '@/models'; + +export default class PaymentReceiveEntryRepository extends BaseModelRepository { + /** + * Insert payment receive entries in bulk. + * @param {Array} entries + * @param {Integr} paymentReceiveId + * @return {Promise} + */ + static insertBulk(entries, paymentReceiveId) { + const opers = []; + entries.forEach((entry) => { + const insertOper = PaymentReceiveEntry.tenant() + .query() + .insert({ + payment_receive_id: paymentReceiveId, + ...entry, + }); + opers.push(insertOper); + }); + return Promise.all(opers); + } + + /** + * Update payment receive entries in bulk. + * @param {Array} entries + * @return {Promise} + */ + static updateBulk(entries) { + const opers = []; + entries.forEach((entry) => { + const updateOper = PaymentReceiveEntry.tenant() + .query() + .patchAndFetchById(entry.id, { + ...omit(entry, ['id', 'index']), + }); + opers.push(updateOper); + }); + return Promise.all(opers); + } + + /** + * Deletes the given payment receive entries ids in bulk. + * @param {Array} entriesIds + * @return {Promise} + */ + static deleteBulk(entriesIds) { + return PaymentReceiveEntry.tenant() + .query() + .whereIn('id', entriesIds) + .delete(); + } +} \ No newline at end of file diff --git a/server/src/repositories/PaymentReceiveRepository.js b/server/src/repositories/PaymentReceiveRepository.js new file mode 100644 index 000000000..71680a4fd --- /dev/null +++ b/server/src/repositories/PaymentReceiveRepository.js @@ -0,0 +1,7 @@ +import { omit } from 'lodash'; +import { PaymentReceiveEntry } from '@/models'; +import BaseModelRepository from '@/repositories/BaseModelRepository'; + +export default class PaymentReceiveRepository extends BaseModelRepository { + +} diff --git a/server/src/services/Accounts/AccountsService.js b/server/src/services/Accounts/AccountsService.js index ad5fc73ea..700a53be8 100644 --- a/server/src/services/Accounts/AccountsService.js +++ b/server/src/services/Accounts/AccountsService.js @@ -1,9 +1,24 @@ -import { Account } from '@/models'; +import { Account, AccountType } from '@/models'; export default class AccountsService { - static async isAccountExists(accountId) { const foundAccounts = await Account.tenant().query().where('id', accountId); return foundAccounts.length > 0; } -} \ No newline at end of file + + static async getAccountByType(accountTypeKey) { + const accountType = await AccountType.tenant() + .query() + .where('key', accountTypeKey) + .first(); + + const account = await Account.tenant() + .query() + .where('account_type_id', accountType.id) + .first(); + + console.log(account); + + return account; + } +} diff --git a/server/src/services/Inventory/Inventory.js b/server/src/services/Inventory/Inventory.js new file mode 100644 index 000000000..90d09a638 --- /dev/null +++ b/server/src/services/Inventory/Inventory.js @@ -0,0 +1,16 @@ +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/Purchases/BillPayments.js b/server/src/services/Purchases/BillPayments.js index 0e0a600f2..b0c3b8d3d 100644 --- a/server/src/services/Purchases/BillPayments.js +++ b/server/src/services/Purchases/BillPayments.js @@ -1,30 +1,244 @@ -import { omit } from "lodash"; -import { BillPayment } from '@/models'; - -export default class BillPaymentsService { +import express from 'express'; +import { omit } from 'lodash'; +import { check, query, validationResult, param } from 'express-validator'; +import { BillPayment, BillPaymentEntry, Vendor } from '@/models'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import ServiceItemsEntries from '../Sales/ServiceItemsEntries'; +import AccountsService from '../Accounts/AccountsService'; +import JournalPoster from '../Accounting/JournalPoster'; +import JournalEntry from '../Accounting/JournalEntry'; +export default class BillPaymentsService { + /** + * Creates a new bill payment transcations and store it to the storage + * with associated bills entries and journal transactions. + * + * Precedures + * ------ + * - Records the bill payment transaction. + * - Records the bill payment associated entries. + * - Increment the payment amount of the given vendor bills. + * - Decrement the vendor balance. + * - Records payment journal entries. + * + * @param {IBillPayment} billPayment + */ static async createBillPayment(billPayment) { - const storedBillPayment = await BillPayment.tenant().query().insert({ - ...omit(billPayment, ['entries']), + const amount = sumBy(billPayment.entries, 'paymentAmount'); + const storedBillPayment = await BillPayment.tenant() + .query() + .insert({ + amount, + ...omit(billPayment, ['entries']), + }); + const storeOpers = []; + + billPayment.entries.forEach((entry) => { + const oper = BillPaymentEntry.tenant() + .query() + .insert({ + bill_payment_id: storedBillPayment.id, + ...entry, + }); + // Increment the bill payment amount. + const billOper = BillPayment.changePaymentAmount( + entry.billId, + entry.paymentAmount + ); + storeOpers.push(billOper); + storeOpers.push(oper); }); - + // Decrement the vendor balance after bills payments. + const vendorDecrementOper = Vendor.changeBalanace( + billPayment.vendor_id, + amount * -1 + ); + // Records the journal transactions after bills payment + // and change diff acoount balance. + const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({ + id: storedBillPayment.id, + ...billPayment, + }); + await Promise.all([ + ...storeOpers, + recordJournalTransaction, + vendorDecrementOper, + ]); + return storedBillPayment; } - editBillPayment(billPaymentId, billPayment) { + /** + * Edits the details of the given bill payment. + * + * Preceducres. + * ------- + * - Update the bill payment transaction. + * - Insert the new bill payment entries that have no ids. + * - Update the bill paymeny entries that have ids. + * - Delete the bill payment entries that not presented. + * - Re-insert the journal transactions and update the diff accounts balance. + * - Update the diff vendor balance. + * - Update the diff bill payment amount. + * + * @param {Integer} billPaymentId + * @param {IBillPayment} billPayment + * @param {IBillPayment} oldBillPayment + */ + static async editBillPayment(billPaymentId, billPayment, oldBillPayment) { + const amount = sumBy(bilPayment.entries, 'payment_amount'); + 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 entriesHasNoIds = billPayment.entries.filter((e) => !e.id); + const entriesIds = entriesHasIds.map((e) => e.id); + + const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( + oldBillPayment.entries, + entriesHasIds + ); + if (entriesIdsShouldDelete.length > 0) { + const deleteOper = BillPaymentEntry.tenant() + .query() + .bulkDelete(entriesIdsShouldDelete); + opers.push(deleteOper); + } + // Entries that should be update to the storage. + if (entriesHasIds.length > 0) { + const updateOper = BillPaymentEntry.tenant() + .query() + .bulkUpdate(entriesHasIds, { where: 'id' }); + opers.push(updateOper); + } + // Entries that should be inserted to the storage. + if (entriesHasNoIds.length > 0) { + const insertOper = BillPaymentEntry.tenant() + .query() + .bulkInsert( + entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId })) + ); + opers.push(insertOper); + } + // Records the journal transactions after bills payment and change + // different acoount balance. + const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({ + id: storedBillPayment.id, + ...billPayment, + }); + // Change the different vendor balance between the new and old one. + const changeDiffBalance = Vendor.changeDiffBalance( + billPayment.vendor_id, + oldBillPayment.vendor_id, + billPayment.amount, + oldBillPayment.amount + ); + await Promise.all([ + ...opers, + recordJournalTransaction, + changeDiffBalance, + ]); } - static async isBillPaymentExists(billPaymentId) { - const foundBillPayments = await BillPayment.tenant().query().where('id', billPaymentId); - return foundBillPayments.lengh > 0; + /** + * Deletes the bill payment and associated transactions. + * @param {Integer} billPaymentId - + * @return {Promise} + */ + static async deleteBillPayment(billPaymentId) { + const billPayment = await BillPayment.tenant().query().where('id', billPaymentId).first(); + + await BillPayment.tenant().query().where('id', billPaymentId).delete(); + await BillPaymentEntry.tenant() + .query() + .where('bill_payment_id', billPaymentId) + .delete(); + + const deleteTransactionsOper = this.deleteJournalTransactions( + billPaymentId, + 'BillPayment' + ); + const revertVendorBalance = Vendor.changeBalanace( + billpayment.vendor_id, + billPayment.amount * -1, + ); + return Promise.all([ + deleteTransactionsOper, + revertVendorBalance, + ]); } - static async isBillPaymentNumberExists(billPaymentNumber) { - const foundPayments = await Bill.tenant().query().where('bill_payment_number', billPaymentNumber); - return foundPayments.length > 0; + /** + * Records bill payment receive journal transactions. + * @param {BillPayment} billPayment + * @param {Integer} billPaymentId + */ + static async recordPaymentReceiveJournalEntries(billPayment) { + const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); + const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD'); + const payableAccount = await AccountsService.getAccountByType( + 'accounts_payable' + ); + + const accountsDepGraph = await Account.tenant().depGraph().query(); + const journal = new JournalPoster(accountsDepGraph); + + const commonJournal = { + debit: 0, + credit: 0, + referenceId: billPayment.id, + referenceType: 'BillPayment', + date: formattedDate, + }; + if (billPayment.id) { + const transactions = await AccountTransaction.tenant() + .query() + .whereIn('reference_type', ['BillPayment']) + .where('reference_id', billPayment.id) + .withGraphFetched('account.type'); + + journal.loadEntries(transactions); + journal.removeEntries(); + } + const debitReceivable = new JournalEntry({ + ...commonJournal, + debit: paymentAmount, + contactType: 'Vendor', + contactId: billpayment.vendor_id, + account: payableAccount.id, + }); + const creditPaymentAccount = new JournalEntry({ + ...commonJournal, + credit: paymentAmount, + account: billPayment.payment_account_id, + }); + journal.debit(debitReceivable); + journal.credit(creditPaymentAccount); + + await Promise.all([ + journal.deleteEntries(), + journal.saveEntries(), + journal.saveBalance(), + ]); } - isBillPaymentsExist(billPaymentIds) { + static async getBillPayment(billPaymentId) { } -} \ No newline at end of file + + /** + * Detarmines whether the bill payment exists on the storage. + * @param {Integer} billPaymentId + */ + static async isBillPaymentExists(billPaymentId) { + const billPayment = await BillPayment.tenant().query() + .where('id', billPaymentId) + .first(); + return billPayment.length > 0; + } +} diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index 7b331e156..335eb760b 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -1,96 +1,316 @@ -import { omit } from 'lodash'; -import { Bill, BillPayment } from '@/models'; -import { Item } from '@/models'; -import { Account } from '../../models'; -import JournalPoster from '../Accounting/JournalPoster'; +import { omit, sumBy, difference } from 'lodash'; +import moment from 'moment'; +import { + Bill, + Vendor, + InventoryTransaction, + ItemEntry, + Item, + Account, +} from '@/models'; +import JournalPoster from '@/services/Accounting/JournalPoster'; +import JournalEntry from '@/services/Accounting/JournalEntry'; +import AccountsService from '@/services/Accounts/AccountsService'; +import JournalPosterService from '@/services/Sales/JournalPosterService'; +import InventoryService from '../Inventory/Inventory'; +import { AccountTransaction } from '../../models'; +/** + * Vendor bills services. + */ export default class BillsService { /** * Creates a new bill and stored it to the storage. + * + * Precedures. + * ---- + * - Insert bill transactions to the storage. + * - Insert bill entries to the storage. + * - Increment the given vendor id. + * - Record bill journal transactions on the given accounts. + * - Record bill items inventory transactions. + * * @param {IBill} bill - * @return {void} */ static async createBill(bill) { - const storedBill = await Bill.tenant().query().insert({ - ...omit(bill, ['entries']), + const amount = sumBy(bill.entries, 'amount'); + const saveEntriesOpers = []; + + const storedBill = await Bill.tenant() + .query() + .insert({ + amount, + ...omit(bill, ['entries']), + }); + bill.entries.forEach((entry) => { + const oper = ItemEntry.tenant() + .query() + .insert({ + reference_type: 'Bill', + reference_id: storedBill.id, + ...omit(entry, ['amount']), + }); + saveEntriesOpers.push(oper); }); + // Increment vendor balance. + const incrementOper = Vendor.changeBalance(bill.vendor_id, amount); + + await Promise.all([ + ...saveEntriesOpers, + incrementOper, + this.recordInventoryTransactions(bill, storedBill.id), + this.recordJournalTransactions({ ...bill, id: storedBill.id }), + ]); + return storedBill; } + /** + * Patch items entries to the storage. + * + * @param {Array} newEntries + * @param {Array} oldEntries + * @param {String} referenceType + * + * @return {Promise} + */ + static async patchItemsEntries(newEntries, oldEntries, referenceType, billId) { + const entriesHasIds = newEntries.filter((entry) => entry.id); + const entriesHasNoIds = newEntries.filter((entry) => !entry.id); + + const entriesIds = entriesHasIds.map(entry => entry.id); + + const oldEntriesIds = oldEntries.map((e) => e.id); + const opers = []; + + const entriesIdsShouldDelete = difference( + oldEntriesIds, + entriesIds, + ); + if (entriesIdsShouldDelete.length > 0) { + const deleteOper = ItemEntry.tenant() + .query() + .whereIn('id', entriesIdsShouldDelete) + .delete(); + opers.push(deleteOper); + } + entriesHasIds.forEach((entry) => { + const updateOper = ItemEntry.tenant() + .query() + .where('id', entry.id) + .update({ + ...omit(entry, ['id']), + }); + opers.push(updateOper); + }); + entriesHasNoIds.forEach((entry) => { + const insertOper = ItemEntry.tenant() + .query() + .insert({ + reference_id: billId, + reference_type: referenceType, + ...omit(entry, ['id', 'amount']), + }); + opers.push(insertOper); + }); + return Promise.all([...opers]); + }; + /** * Edits details of the given bill id with associated entries. - * @param {Integer} billId - * @param {IBill} bill + * + * Precedures: + * ------- + * - Update the bill transaction on the storage. + * - Update the bill entries on the storage and insert the not have id and delete + * once that not presented. + * - Increment the diff amount on the given vendor id. + * - Re-write the inventory transactions. + * - Re-write the bill journal transactions. + * + * @param {Integer} billId + * @param {IBill} bill */ static async editBill(billId, bill) { - const updatedBill = await Bill.tenant().query().insert({ - ...omit(bill, ['entries']), + const amount = sumBy(bill.entries, 'amount'); + + // Update the bill transaction. + const updatedBill = await Bill.tenant() + .query() + .where('id', billId) + .update({ + amount, + ...omit(bill, ['entries']) + }); + + // Old stored entries. + const storedEntries = await ItemEntry.tenant() + .query() + .where('reference_id', billId) + .where('reference_type', 'Bill'); + + // Patch the bill entries. + const patchEntriesOper = this.patchItemsEntries(bill.entries, storedEntries, 'Bill', billId); + + // Record bill journal transactions. + const recordTransactionsOper = this.recordJournalTransactions(bill, billId); + + await Promise.all([ + patchEntriesOper, + recordTransactionsOper, + ]); + } + + /** + * 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. - * @param {IBill} bill + * @async + * @param {IBill} bill + * @param {Integer} billId */ - async recordJournalTransactions(bill) { - const entriesItemsIds = bill.entries.map(entry => entry.item_id); - const payableTotal = sumBy(bill, 'entries.total'); - const storedItems = await Item.tenant().query().whereIn('id', entriesItemsIds); + static async recordJournalTransactions(bill, billId) { + const entriesItemsIds = bill.entries.map((entry) => entry.item_id); + const payableTotal = sumBy(bill.entries, 'amount'); + const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD'); - const payableAccount = await Account.tenant().query(); - const formattedDate = moment(saleInvoice.invoice_date).format('YYYY-MM-DD'); + const storedItems = await Item.tenant() + .query() + .whereIn('id', entriesItemsIds); - const accountsDepGraph = await Account.depGraph().query().remember(); + const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); + const payableAccount = await AccountsService.getAccountByType( + 'accounts_payable' + ); + if (!payableAccount) { + throw new Error('New payable account on the storage.'); + } + const accountsDepGraph = await Account.tenant().depGraph().query(); const journal = new JournalPoster(accountsDepGraph); const commonJournalMeta = { debit: 0, credit: 0, - referenceId: bill.id, + referenceId: billId, referenceType: 'Bill', date: formattedDate, accural: true, }; - const payableEntry = await JournalEntry({ + if (billId) { + const transactions = await AccountTransaction.tenant() + .query() + .whereIn('reference_type', ['Bill']) + .whereIn('reference_id', [billId]) + .withGraphFetched('account.type'); + + journal.loadEntries(transactions); + journal.removeEntries(); + } + const payableEntry = new JournalEntry({ ...commonJournalMeta, credit: payableTotal, - contactId: bill.vendorId, + account: payableAccount.id, + contactId: bill.vendor_id, contactType: 'Vendor', }); journal.credit(payableEntry); - bill.entries.forEach((item) => { - if (['inventory'].indexOf(item.type) !== -1) { - const inventoryEntry = new JournalEntry({ - ...commonJournalMeta, - account: item.inventoryAccountId, - }); - journal.debit(inventoryEntry); - } else { - const costEntry = new JournalEntry({ - ...commonJournalMeta, - account: item.costAccountId, - }); - journal.debit(costEntry); - } + bill.entries.forEach((entry) => { + const item = storedItemsMap.get(entry.item_id); + + const debitEntry = new JournalEntry({ + ...commonJournalMeta, + debit: entry.amount, + account: + ['inventory'].indexOf(item.type) !== -1 + ? item.inventoryAccountId + : item.costAccountId, + }); + journal.debit(debitEntry); }); await Promise.all([ + journal.deleteEntries(), journal.saveEntries(), journal.saveBalance(), - ]) + ]); } /** * Deletes the bill with associated entries. - * @param {Integer} billId + * @param {Integer} billId * @return {void} */ static async deleteBill(billId) { - await BillPayment.tenant().query().where('id', billId); + const bill = await Bill.tenant().query().where('id', billId).first(); + + // Delete all associated bill entries. + const deleteBillEntriesOper = ItemEntry.tenant() + .query() + .where('reference_type', 'Bill') + .where('reference_id', billId) + .delete(); + + // Delete the bill transaction. + const deleteBillOper = Bill.tenant().query().where('id', billId).delete(); + + // Delete associated bill journal transactions. + const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions( + billId, + 'Bill' + ); + // Delete bill associated inventory transactions. + const deleteInventoryTransOper = InventoryService.deleteTransactions( + billId, + 'Bill' + ); + // Revert vendor balance. + const revertVendorBalance = Vendor.changeBalance(billId, bill.amount * -1); + + await Promise.all([ + deleteBillOper, + deleteBillEntriesOper, + deleteTransactionsOper, + deleteInventoryTransOper, + revertVendorBalance, + ]); } /** * Detarmines whether the bill exists on the storage. - * @param {Integer} billId + * @param {Integer} billId * @return {Boolean} */ static async isBillExists(billId) { @@ -100,15 +320,31 @@ export default class BillsService { /** * Detarmines whether the given bills exist on the storage in bulk. - * @param {Array} billsIds + * @param {Array} billsIds * @return {Boolean} */ - isBillsExist(billsIds) { - + static async isBillsExist(billsIds) { + const bills = await Bill.tenant().query().whereIn('id', billsIds); + return bills.length > 0; } + /** + * Detarmines whether the given bill id exists on the storage. + * @param {Integer} billNumber + */ static async isBillNoExists(billNumber) { - const foundBills = await Bill.tenant().query().where('bill_number', billNumber); + const foundBills = await Bill.tenant() + .query() + .where('bill_number', billNumber); return foundBills.length > 0; } -} \ No newline at end of file + + /** + * Retrieve the given bill details with associated items entries. + * @param {Integer} billId - + * @returns {Promise} + */ + static getBill(billId) { + return Bill.tenant().query().where('id', billId).first(); + } +} diff --git a/server/src/services/Sales/JournalPosterService.js b/server/src/services/Sales/JournalPosterService.js index f2e96ead8..d7678b657 100644 --- a/server/src/services/Sales/JournalPosterService.js +++ b/server/src/services/Sales/JournalPosterService.js @@ -6,10 +6,10 @@ export default class JournalPosterService { /** * Deletes the journal transactions that associated to the given reference id. */ - static async deleteJournalTransactions(referenceId) { + static async deleteJournalTransactions(referenceId, referenceType) { const transactions = await AccountTransaction.tenant() .query() - .whereIn('reference_type', ['SaleInvoice']) + .whereIn('reference_type', [referenceType]) .where('reference_id', referenceId) .withGraphFetched('account.type'); @@ -21,5 +21,4 @@ export default class JournalPosterService { await Promise.all([journal.deleteEntries(), journal.saveBalance()]); } -} - +} \ No newline at end of file diff --git a/server/src/services/Sales/PaymentReceive.js b/server/src/services/Sales/PaymentReceive.js index ca8faa34d..2c20128bc 100644 --- a/server/src/services/Sales/PaymentReceive.js +++ b/server/src/services/Sales/PaymentReceive.js @@ -1,116 +1,371 @@ -import { omit } from 'lodash'; -import { PaymentReceive, PaymentReceiveEntry } from '@/models'; +import { omit, sumBy, mapValues, groupBy, chain } from 'lodash'; +import moment, { updateLocale } from 'moment'; +import { + AccountTransaction, + PaymentReceive, + PaymentReceiveEntry, + SaleInvoice, + Customer, + Account, +} from '@/models'; +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 ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; +import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository'; +import CustomerRepository from '@/repositories/CustomerRepository'; export default class PaymentReceiveService extends JournalPosterService { /** * Creates a new payment receive and store it to the storage * with associated invoices payment and journal transactions. * @async - * @param {IPaymentReceive} paymentReceive + * @param {IPaymentReceive} paymentReceive */ static async createPaymentReceive(paymentReceive) { + const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); const storedPaymentReceive = await PaymentReceive.tenant() .query() .insert({ + amount: paymentAmount, ...omit(paymentReceive, ['entries']), }); const storeOpers = []; - paymentReceive.entries.forEach((invoice) => { - const oper = PaymentReceiveEntry.tenant().query().insert({ - payment_receive_id: storedPaymentReceive.id, - ...invoice, - }); - storeOpers.push(oper); - }); - await Promise.all([ ...storeOpers ]); + paymentReceive.entries.forEach((entry) => { + const oper = PaymentReceiveEntry.tenant() + .query() + .insert({ + payment_receive_id: storedPaymentReceive.id, + ...entry, + }); + // Increment the invoice payment amount. + const invoice = SaleInvoice.tenant() + .query() + .where('id', entry.invoice_id) + .increment('payment_amount', entry.payment_amount); + storeOpers.push(oper); + storeOpers.push(invoice); + }); + const customerIncrementOper = Customer.decrementBalance( + paymentReceive.customer_id, + paymentAmount + ); + const recordJournalTransactions = this.recordPaymentReceiveJournalEntries({ + id: storedPaymentReceive.id, + ...paymentReceive, + }); + await Promise.all([ + ...storeOpers, + customerIncrementOper, + recordJournalTransactions, + ]); return storedPaymentReceive; } /** * Edit details the given payment receive with associated entries. + * ------ + * - Update the payment receive transactions. + * - Insert the new payment receive entries. + * - Update the given payment receive entries. + * - Delete the not presented payment receive entries. + * - Re-insert the journal transactions and update the different accounts balance. + * - Update the different customer balances. + * - Update the different invoice payment amount. * @async - * @param {Integer} paymentReceiveId - * @param {IPaymentReceive} paymentReceive + * @param {Integer} paymentReceiveId + * @param {IPaymentReceive} paymentReceive + * @param {IPaymentReceive} oldPaymentReceive */ - static async editPaymentReceive(paymentReceiveId, paymentReceive) { - const updatePaymentReceive = await PaymentReceive.tenant().query() + static async editPaymentReceive( + paymentReceiveId, + paymentReceive, + oldPaymentReceive + ) { + const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); + // Update the payment receive transaction. + const updatePaymentReceive = await PaymentReceive.tenant() + .query() .where('id', paymentReceiveId) .update({ + amount: paymentAmount, ...omit(paymentReceive, ['entries']), }); - const storedEntries = await PaymentReceiveEntry.tenant().query() - .where('payment_receive_id', paymentReceiveId); - - const entriesIds = paymentReceive.entries.filter(i => i.id); const opers = []; + const entriesIds = paymentReceive.entries.filter((i) => i.id); + const entriesShouldInsert = paymentReceive.entries.filter((i) => !i.id); - const entriesIdsShouldDelete = this.entriesShouldDeleted( - storedEntries, - entriesIds, + // Detarmines which entries ids should be deleted. + const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted( + oldPaymentReceive.entries, + entriesIds ); if (entriesIdsShouldDelete.length > 0) { - const deleteOper = PaymentReceiveEntry.tenant().query() - .whereIn('id', entriesIdsShouldDelete) - .delete(); + // Deletes the given payment receive entries. + const deleteOper = PaymentReceiveEntryRepository.deleteBulk( + entriesIdsShouldDelete + ); opers.push(deleteOper); } - entriesIds.forEach((entry) => { - const updateOper = PaymentReceiveEntry.tenant() - .query() - .pathAndFetchById(entry.id, { - ...omit(entry, ['id']), - }); + // Entries that should be updated to the storage. + if (entriesIds.length > 0) { + const updateOper = PaymentReceiveEntryRepository.updateBulk(entriesIds); opers.push(updateOper); - }); - await Promise.all([...opers]); + } + // Entries should insert to the storage. + if (entriesShouldInsert.length > 0) { + const insertOper = PaymentReceiveEntryRepository.insertBulk( + entriesShouldInsert, + paymentReceiveId + ); + opers.push(insertOper); + } + // Re-write the journal transactions of the given payment receive. + const recordJournalTransactions = this.recordPaymentReceiveJournalEntries( + { + id: oldPaymentReceive.id, + ...paymentReceive, + }, + paymentReceiveId + ); + // Increment/decrement the customer balance after calc the diff + // between old and new value. + const changeCustomerBalance = CustomerRepository.changeDiffBalance( + paymentReceive.customer_id, + oldPaymentReceive.customerId, + paymentAmount, + oldPaymentReceive.amount, + ); + // Change the difference between the old and new invoice payment amount. + const diffInvoicePaymentAmount = this.saveChangeInvoicePaymentAmount( + oldPaymentReceive.entries, + paymentReceive.entries + ); + // Await the async operations. + await Promise.all([ + ...opers, + recordJournalTransactions, + changeCustomerBalance, + diffInvoicePaymentAmount, + ]); } /** - * Deletes the given payment receive with associated entries + * Deletes the given payment receive with associated entries * and journal transactions. - * @param {Integer} paymentReceiveId + * ----- + * - Deletes the payment receive transaction. + * - Deletes the payment receive associated entries. + * - Deletes the payment receive associated journal transactions. + * - Revert the customer balance. + * - Revert the payment amount of the associated invoices. + * @async + * @param {Integer} paymentReceiveId */ - static async deletePaymentReceive(paymentReceiveId) { - await PaymentReceive.tenant().query().where('id', paymentReceiveId).delete(); - await PaymentReceiveEntry.tenant().query().where('payment_receive_id', paymentReceiveId).delete(); + static async deletePaymentReceive(paymentReceiveId, paymentReceive) { + // Deletes the payment receive transaction. + await PaymentReceive.tenant() + .query() + .where('id', paymentReceiveId) + .delete(); - await this.deleteJournalTransactions(paymentReceiveId); + // Deletes the payment receive associated entries. + await PaymentReceiveEntry.tenant() + .query() + .where('payment_receive_id', paymentReceiveId) + .delete(); + + // Delete all associated journal transactions to payment receive transaction. + const deleteTransactionsOper = this.deleteJournalTransactions( + paymentReceiveId, + 'PaymentReceive' + ); + // Revert the customer balance. + const revertCustomerBalance = Customer.incrementBalance( + paymentReceive.customerId, + paymentReceive.amount + ); + // Revert the invoices payments amount. + const revertInvoicesPaymentAmount = this.revertInvoicePaymentAmount( + paymentReceive.entries.map((entry) => ({ + invoiceId: entry.invoiceId, + revertAmount: entry.paymentAmount, + })) + ); + await Promise.all([ + deleteTransactionsOper, + revertCustomerBalance, + revertInvoicesPaymentAmount, + ]); } /** * Retrieve the payment receive details of the given id. - * @param {Integer} paymentReceiveId + * @param {Integer} paymentReceiveId */ static async getPaymentReceive(paymentReceiveId) { - const paymentReceive = await PaymentReceive.tenant().query().where('id', paymentReceiveId).first(); + const paymentReceive = await PaymentReceive.tenant() + .query() + .where('id', paymentReceiveId) + .withGraphFetched('entries') + .first(); return paymentReceive; } /** * Retrieve the payment receive details with associated invoices. - * @param {Integer} paymentReceiveId + * @param {Integer} paymentReceiveId */ static async getPaymentReceiveWithInvoices(paymentReceiveId) { - const paymentReceive = await PaymentReceive.tenant().query() + return PaymentReceive.tenant() + .query() .where('id', paymentReceiveId) .withGraphFetched('invoices') .first(); - return paymentReceive; } - + + /** + * Detarmines whether the payment receive exists on the storage. + * @param {Integer} paymentReceiveId + */ static async isPaymentReceiveExists(paymentReceiveId) { - const paymentReceives = await PaymentReceive.tenant().query().where('id', paymentReceiveId) + const paymentReceives = await PaymentReceive.tenant() + .query() + .where('id', paymentReceiveId); return paymentReceives.length > 0; } /** * Detarmines the payment receive number existance. + * @async + * @param {Integer} paymentReceiveNumber - Payment receive number. + * @param {Integer} paymentReceiveId - Payment receive id. */ - static async isPaymentReceiveNoExists(paymentReceiveNumber) { - const paymentReceives = await PaymentReceive.tenant().query().where('payment_receive_no', paymentReceiveNumber); + static async isPaymentReceiveNoExists( + paymentReceiveNumber, + paymentReceiveId + ) { + const paymentReceives = await PaymentReceive.tenant() + .query() + .where('payment_receive_no', paymentReceiveNumber) + .onBuild((query) => { + if (paymentReceiveId) { + query.whereNot('id', paymentReceiveId); + } + }); return paymentReceives.length > 0; } -} \ No newline at end of file + + /** + * Records payment receive journal transactions. + * @async + * @param {IPaymentReceive} paymentReceive + */ + static async recordPaymentReceiveJournalEntries( + paymentReceive, + paymentReceiveId + ) { + const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); + const formattedDate = moment(paymentReceive.payment_date).format( + 'YYYY-MM-DD' + ); + const receivableAccount = await AccountsService.getAccountByType( + 'accounts_receivable' + ); + const accountsDepGraph = await Account.tenant().depGraph().query(); + const journal = new JournalPoster(accountsDepGraph); + const commonJournal = { + debit: 0, + credit: 0, + referenceId: paymentReceive.id, + referenceType: 'PaymentReceive', + date: formattedDate, + }; + if (paymentReceiveId) { + const transactions = await AccountTransaction.tenant() + .query() + .whereIn('reference_type', ['PaymentReceive']) + .where('reference_id', paymentReceiveId) + .withGraphFetched('account.type'); + + journal.loadEntries(transactions); + journal.removeEntries(); + } + const creditReceivable = new JournalEntry({ + ...commonJournal, + credit: paymentAmount, + contactType: 'Customer', + contactId: paymentReceive.customer_id, + account: receivableAccount.id, + }); + const debitDepositAccount = new JournalEntry({ + ...commonJournal, + debit: paymentAmount, + account: paymentReceive.deposit_account_id, + }); + journal.credit(creditReceivable); + journal.debit(debitDepositAccount); + + await Promise.all([ + journal.deleteEntries(), + journal.saveEntries(), + journal.saveBalance(), + ]); + } + + /** + * Revert the payment amount of the given invoices ids. + * @param {Array} revertInvoices + */ + static async revertInvoicePaymentAmount(revertInvoices) { + const opers = []; + + revertInvoices.forEach((revertInvoice) => { + const { revertAmount, invoiceId } = revertInvoice; + const oper = SaleInvoice.tenant() + .query() + .where('id', invoiceId) + .decrement('payment_amount', revertAmount); + opers.push(oper); + }); + await Promise.all(opers); + } + + /** + * Saves difference changing between old and new invoice payment amount. + * @param {Array} paymentReceiveEntries + * @param {Array} newPaymentReceiveEntries + * @return + */ + static async saveChangeInvoicePaymentAmount( + paymentReceiveEntries, + newPaymentReceiveEntries + ) { + const opers = []; + const newEntriesTable = chain(newPaymentReceiveEntries) + .groupBy('invoice_id') + .mapValues((group) => (sumBy(group, 'payment_amount') || 0) * -1) + .value(); + + const diffEntries = chain(paymentReceiveEntries) + .groupBy('invoiceId') + .mapValues((group) => (sumBy(group, 'paymentAmount') || 0) * -1) + .mapValues((value, key) => value - (newEntriesTable[key] || 0)) + .mapValues((value, key) => ({ invoice_id: key, payment_amount: value })) + .filter((entry) => entry.payment_amount != 0) + .values() + .value(); + + diffEntries.forEach((diffEntry) => { + const oper = SaleInvoice.changePaymentAmount( + diffEntry.invoice_id, + diffEntry.payment_amount + ); + opers.push(oper); + }); + return Promise.all([ ...opers ]); + } +} diff --git a/server/src/services/Sales/SaleInvoice.js b/server/src/services/Sales/SaleInvoice.js index 047db240e..e62e2c878 100644 --- a/server/src/services/Sales/SaleInvoice.js +++ b/server/src/services/Sales/SaleInvoice.js @@ -1,10 +1,11 @@ -import { omit, update, difference } from 'lodash'; +import { omit, sumBy, difference } from 'lodash'; import { SaleInvoice, - SaleInvoiceEntry, AccountTransaction, Account, Item, + ItemEntry, + Customer, } from '@/models'; import JournalPoster from '@/services/Accounting/JournalPoster'; import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; @@ -17,47 +18,37 @@ export default class SaleInvoicesService extends ServiceItemsEntries { * @return {ISaleInvoice} */ static async createSaleInvoice(saleInvoice) { + const balance = sumBy(saleInvoice.entries, 'amount'); const storedInvoice = await SaleInvoice.tenant() .query() .insert({ ...omit(saleInvoice, ['entries']), + balance, + payment_amount: 0, }); const opers = []; saleInvoice.entries.forEach((entry) => { - const oper = SaleInvoiceEntry.tenant() + const oper = ItemEntry.tenant() .query() .insert({ - sale_invoice_id: storedInvoice.id, - ...entry, + reference_type: 'SaleInvoice', + reference_id: storedInvoice.id, + ...omit(entry, ['amount', 'id']), }); opers.push(oper); }); - await Promise.all([ - ...opers, - this.recordCreateJournalEntries(saleInvoice), - ]); + const incrementOper = Customer.incrementBalance( + saleInvoice.customer_id, + balance, + ); + await Promise.all([...opers, incrementOper]); return storedInvoice; } - /** - * Calculates total of the sale invoice entries. - * @param {ISaleInvoice} saleInvoice - * @return {ISaleInvoice} - */ - calcSaleInvoiceEntriesTotal(saleInvoice) { - return { - ...saleInvoice, - entries: saleInvoice.entries.map((entry) => ({ - ...entry, - total: 0, - })), - }; - } - /** * Records the journal entries of sale invoice. - * @param {ISaleInvoice} saleInvoice + * @param {ISaleInvoice} saleInvoice * @return {void} */ async recordJournalEntries(saleInvoice) { @@ -69,8 +60,10 @@ export default class SaleInvoicesService extends ServiceItemsEntries { const formattedDate = moment(saleInvoice.invoice_date).format('YYYY-MM-DD'); const saleItemsIds = saleInvoice.entries.map((e) => e.item_id); - const storedInvoiceItems = await Item.tenant().query().whereIn('id', saleItemsIds) - + const storedInvoiceItems = await Item.tenant() + .query() + .whereIn('id', saleItemsIds); + const commonJournalMeta = { debit: 0, credit: 0, @@ -111,7 +104,6 @@ export default class SaleInvoicesService extends ServiceItemsEntries { accountNormal: 'debit', note: '', }); - journal.debit(costEntry); } journal.credit(incomeEntry); @@ -129,9 +121,10 @@ export default class SaleInvoicesService extends ServiceItemsEntries { */ static async deleteSaleInvoice(saleInvoiceId) { await SaleInvoice.tenant().query().where('id', saleInvoiceId).delete(); - await SaleInvoiceEntry.tenant() + await ItemEntry.tenant() .query() - .where('sale_invoice_id', saleInvoiceId) + .where('reference_id', saleInvoiceId) + .where('reference_type', 'SaleInvoice') .delete(); const invoiceTransactions = await AccountTransaction.tenant() @@ -151,39 +144,69 @@ export default class SaleInvoicesService extends ServiceItemsEntries { /** * Edit the given sale invoice. - * @param {Integer} saleInvoiceId - - * @param {ISaleInvoice} saleInvoice - + * @param {Integer} saleInvoiceId - + * @param {ISaleInvoice} saleInvoice - */ static async editSaleInvoice(saleInvoiceId, saleInvoice) { - const updatedSaleInvoices = await SaleInvoice.tenant().query() + const updatedSaleInvoices = await SaleInvoice.tenant() + .query() .where('id', saleInvoiceId) .update({ ...omit(saleInvoice, ['entries']), }); const opers = []; const entriesIds = saleInvoice.entries.filter((entry) => entry.id); - const storedEntries = await SaleInvoiceEntry.tenant().query() - .where('sale_invoice_id', saleInvoiceId); + const entriesNoIds = saleInvoice.entries.filter((entry) => !entry.id); + + const storedEntries = await ItemEntry.tenant() + .query() + .where('reference_id', saleInvoiceId) + .where('reference_type', 'SaleInvoice'); const entriesIdsShouldDelete = this.entriesShouldDeleted( storedEntries, - entriesIds, + entriesIds ); if (entriesIdsShouldDelete.length > 0) { - const updateOper = SaleInvoiceEntry.tenant().query().where('id', entriesIdsShouldDelete); + const updateOper = ItemEntry.tenant() + .query() + .whereIn('id', entriesIdsShouldDelete) + .delete(); opers.push(updateOper); } entriesIds.forEach((entry) => { - const updateOper = SaleInvoiceEntry.tenant() + const updateOper = ItemEntry.tenant() .query() - .patchAndFetchById(entry.id, { + .where('id', entry.id) + .update({ ...omit(entry, ['id']), }); opers.push(updateOper); }); + entriesNoIds.forEach((entry) => { + const insertOper = ItemEntry.tenant() + .query() + .insert({ + reference_type: 'SaleInvoice', + reference_id: saleInvoiceId, + ...omit(entry, ['id']), + }); + opers.push(insertOper); + }) await Promise.all([...opers]); } + /** + * Retrieve sale invoice with associated entries. + * @param {Integer} saleInvoiceId + */ + static async getSaleInvoiceWithEntries(saleInvoiceId) { + return SaleInvoice.tenant().query() + .where('id', saleInvoiceId) + .withGraphFetched('entries') + .first(); + } + /** * Detarmines the sale invoice number id exists on the storage. * @param {Integer} saleInvoiceId @@ -208,7 +231,7 @@ export default class SaleInvoicesService extends ServiceItemsEntries { query.where('invoice_no', saleInvoiceNumber); if (saleInvoiceId) { - query.whereNot('id', saleInvoiceId) + query.whereNot('id', saleInvoiceId); } return query; }); @@ -217,7 +240,7 @@ export default class SaleInvoicesService extends ServiceItemsEntries { /** * Detarmine the invoices IDs in bulk and returns the not found ones. - * @param {Array} invoicesIds + * @param {Array} invoicesIds * @return {Array} */ static async isInvoicesExist(invoicesIds) { @@ -227,11 +250,8 @@ export default class SaleInvoicesService extends ServiceItemsEntries { builder.whereIn('id', invoicesIds); return builder; }); - const storedInvoicesIds = storedInvoices.map(i => i.id); - const notStoredInvoices = difference( - invoicesIds, - storedInvoicesIds, - ); + const storedInvoicesIds = storedInvoices.map((i) => i.id); + const notStoredInvoices = difference(invoicesIds, storedInvoicesIds); return notStoredInvoices; } } diff --git a/server/src/services/Sales/SalesEstimate.js b/server/src/services/Sales/SalesEstimate.js index 1c5efa3de..4de127d66 100644 --- a/server/src/services/Sales/SalesEstimate.js +++ b/server/src/services/Sales/SalesEstimate.js @@ -1,9 +1,8 @@ -import { omit, difference } from 'lodash'; -import { SaleEstimate, SaleEstimateEntry } from '@/models'; - -export default class SaleEstimateService { - constructor() {} +import { omit, difference, sumBy } from 'lodash'; +import { SaleEstimate, ItemEntry } from '@/models'; +import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; +export default class SaleEstimateService extends ServiceItemsEntries { /** * Creates a new estimate with associated entries. * @async @@ -11,23 +10,27 @@ export default class SaleEstimateService { * @return {void} */ static async createEstimate(estimate) { + const amount = sumBy(estimate.entries, 'amount'); const storedEstimate = await SaleEstimate.tenant() .query() .insert({ + amount, ...omit(estimate, ['entries']), }); const storeEstimateEntriesOpers = []; estimate.entries.forEach((entry) => { - const oper = SaleEstimateEntry.tenant() + const oper = ItemEntry.tenant() .query() .insert({ - estimate_id: storedEstimate.id, - ...entry, + reference_type: 'SaleEstimate', + reference_id: storedEstimate.id, + ...omit(entry, ['total', 'amount']), }); storeEstimateEntriesOpers.push(oper); }); await Promise.all([...storeEstimateEntriesOpers]); + return storedEstimate; } @@ -38,9 +41,10 @@ export default class SaleEstimateService { * @return {void} */ static async deleteEstimate(estimateId) { - await SaleEstimateEntry.tenant() + await ItemEntry.tenant() .query() - .where('estimate_id', estimateId) + .where('reference_id', estimateId) + .where('reference_type', 'SaleEstimate') .delete(); await SaleEstimate.tenant().query().where('id', estimateId).delete(); } @@ -53,43 +57,57 @@ export default class SaleEstimateService { * @return {void} */ static async editEstimate(estimateId, estimate) { + const amount = sumBy(estimate.entries, 'amount'); const updatedEstimate = await SaleEstimate.tenant() .query() .update({ + amount, ...omit(estimate, ['entries']), }); - const storedEstimateEntries = await SaleEstimateEntry.tenant() + const storedEstimateEntries = await ItemEntry.tenant() .query() - .where('estimate_id', estimateId); + .where('reference_id', estimateId) + .where('reference_type', 'SaleEstimate'); const opers = []; - const storedEstimateEntriesIds = storedEstimateEntries.map((e) => e.id); - const estimateEntriesHasID = estimate.entries.filter((entry) => entry.id); - const formEstimateEntriesIds = estimateEntriesHasID.map( - (entry) => entry.id - ); + const entriesHasID = estimate.entries.filter((entry) => entry.id); + const entriesHasNoIDs = estimate.entries.filter((entry) => !entry.id); + + const storedEntriesIds = storedEstimateEntries.map((e) => e.id); + const formEstimateEntriesIds = entriesHasID.map((entry) => entry.id); + const entriesIdsShouldBeDeleted = difference( - storedEstimateEntriesIds, + storedEntriesIds, formEstimateEntriesIds, ); - - console.log(entriesIdsShouldBeDeleted); + // Deletes the given sale estimate entries ids. if (entriesIdsShouldBeDeleted.length > 0) { - const oper = SaleEstimateEntry.tenant() + const oper = ItemEntry.tenant() .query() - .where('id', entriesIdsShouldBeDeleted) + .whereIn('id', entriesIdsShouldBeDeleted) .delete(); opers.push(oper); } - estimateEntriesHasID.forEach((entry) => { - const oper = SaleEstimateEntry.tenant() + // Insert the new sale estimate entries. + entriesHasNoIDs.forEach((entry) => { + const oper = ItemEntry.tenant() + .query() + .insert({ + reference_type: 'SaleEstimate', + reference_id: estimateId, + ...entry, + }); + opers.push(oper); + }); + entriesHasID.forEach((entry) => { + const oper = ItemEntry.tenant() .query() .patchAndFetchById(entry.id, { ...omit(entry, ['id']), }); opers.push(oper); }); - await Promise.all([...opers]); + return Promise.all([...opers]); } /** @@ -116,10 +134,11 @@ export default class SaleEstimateService { .filter((e) => e.id) .map((e) => e.id); - const estimateEntries = await SaleEstimateEntry.tenant() + const estimateEntries = await ItemEntry.tenant() .query() .whereIn('id', estimateEntriesIds) - .where('estimate_id', estimateId); + .where('reference_id', estimateId) + .where('reference_type', 'SaleEstimate'); const storedEstimateEntriesIds = estimateEntries.map((e) => e.id); const notFoundEntriesIDs = difference( diff --git a/server/src/services/Sales/SalesReceipt.js b/server/src/services/Sales/SalesReceipt.js index f646574cd..d38c61884 100644 --- a/server/src/services/Sales/SalesReceipt.js +++ b/server/src/services/Sales/SalesReceipt.js @@ -1,33 +1,36 @@ -import { omit, difference } from 'lodash'; +import { omit, difference, sumBy } from 'lodash'; import { SaleReceipt, - SaleReceiptEntry, - AccountTransaction, Account, } from '@/models'; import JournalPoster from '@/services/Accounting/JournalPoster'; +import ItemEntry from '../../models/ItemEntry'; +import JournalPosterService from '@/services/Sales/JournalPosterService'; -export default class SalesReceipt { - constructor() {} - +export default class SalesReceipt extends JournalPosterService { /** * Creates a new sale receipt with associated entries. + * @async * @param {ISaleReceipt} saleReceipt + * @return {Object} */ static async createSaleReceipt(saleReceipt) { + const amount = sumBy(saleReceipt.entries, 'amount'); const storedSaleReceipt = await SaleReceipt.tenant() .query() .insert({ + amount, ...omit(saleReceipt, ['entries']), }); const storeSaleReceiptEntriesOpers = []; saleReceipt.entries.forEach((entry) => { - const oper = SaleReceiptEntry.tenant() + const oper = ItemEntry.tenant() .query() .insert({ - sale_receipt_id: storedSaleReceipt.id, - ...entry, + reference_type: 'SaleReceipt', + reference_id: storedSaleReceipt.id, + ...omit(entry, ['id', 'amount']), }); storeSaleReceiptEntriesOpers.push(oper); }); @@ -38,34 +41,11 @@ export default class SalesReceipt { /** * Records journal transactions for sale receipt. * @param {ISaleReceipt} saleReceipt + * @return {Promise} */ static async _recordJournalTransactions(saleReceipt) { const accountsDepGraph = await Account.tenant().depGraph().query(); const journalPoster = new JournalPoster(accountsDepGraph); - - const creditEntry = new journalEntry({ - debit: 0, - credit: saleReceipt.total, - account: saleReceipt.incomeAccountId, - referenceType: 'SaleReceipt', - referenceId: saleReceipt.id, - note: saleReceipt.note, - }); - const debitEntry = new journalEntry({ - debit: saleReceipt.total, - credit: 0, - account: saleReceipt.incomeAccountId, - referenceType: 'SaleReceipt', - referenceId: saleReceipt.id, - note: saleReceipt.note, - }); - journalPoster.credit(creditEntry); - journalPoster.credit(debitEntry); - - await Promise.all([ - journalPoster.saveEntries(), - journalPoster.saveBalance(), - ]); } /** @@ -75,42 +55,45 @@ export default class SalesReceipt { * @return {void} */ static async editSaleReceipt(saleReceiptId, saleReceipt) { + const amount = sumBy(saleReceipt.entries, 'amount'); const updatedSaleReceipt = await SaleReceipt.tenant() .query() .where('id', saleReceiptId) .update({ + amount, ...omit(saleReceipt, ['entries']), }); - const storedSaleReceiptEntries = await SaleReceiptEntry.tenant() + const storedSaleReceiptEntries = await ItemEntry.tenant() .query() - .where('sale_receipt_id', saleReceiptId); + .where('reference_id', saleReceiptId) + .where('reference_type', 'SaleReceipt'); const storedSaleReceiptsIds = storedSaleReceiptEntries.map((e) => e.id); const entriesHasID = saleReceipt.entries.filter((entry) => entry.id); const entriesIds = entriesHasID.map((e) => e.id); + const opers = []; const entriesIdsShouldBeDeleted = difference( storedSaleReceiptsIds, entriesIds ); - const opers = []; - if (entriesIdsShouldBeDeleted.length > 0) { - const deleteOper = SaleReceiptEntry.tenant() + const deleteOper = ItemEntry.tenant() .query() - .where('id', entriesIdsShouldBeDeleted) + .whereIn('id', entriesIdsShouldBeDeleted) + .where('reference_type', 'SaleReceipt') .delete(); opers.push(deleteOper); } entriesHasID.forEach((entry) => { - const updateOper = SaleReceiptEntry.tenant() + const updateOper = ItemEntry.tenant() .query() .patchAndFetchById(entry.id, { ...omit(entry, ['id']), }); opers.push(updateOper); }); - await Promise.all([...opers]); + return Promise.all([...opers]); } /** @@ -120,27 +103,20 @@ export default class SalesReceipt { */ static async deleteSaleReceipt(saleReceiptId) { await SaleReceipt.tenant().query().where('id', saleReceiptId).delete(); - await SaleReceiptEntry.tenant() + await ItemEntry.tenant() .query() - .where('sale_receipt_id', saleReceiptId) + .where('reference_id', saleReceiptId) + .where('reference_type', 'SaleReceipt') .delete(); - const receiptTransactions = await AccountTransaction.tenant() - .query() - .whereIn('reference_type', ['SaleReceipt']) - .where('reference_id', saleReceiptId) - .withGraphFetched('account.type'); - - const accountsDepGraph = await Account.tenant() - .depGraph() - .query() - .remember(); - const journal = new JournalPoster(accountsDepGraph); - - journal.loadEntries(receiptTransactions); - journal.removeEntries(); - - await Promise.all([journal.deleteEntries(), journal.saveBalance()]); + // Delete all associated journal transactions to payment receive transaction. + const deleteTransactionsOper = this.deleteJournalTransactions( + saleReceiptId, + 'SaleReceipt' + ); + return Promise.all([ + deleteTransactionsOper, + ]); } /** @@ -165,10 +141,11 @@ export default class SalesReceipt { .filter((e) => e.id) .map((e) => e.id); - const storedEntries = await SaleReceiptEntry.tenant() + const storedEntries = await ItemEntry.tenant() .query() .whereIn('id', entriesIDs) - .where('sale_receipt_id', saleReceiptId); + .where('reference_id', saleReceiptId) + .where('reference_type', 'SaleReceipt'); const storedEntriesIDs = storedEntries.map((e) => e.id); const notFoundEntriesIDs = difference( @@ -178,6 +155,10 @@ export default class SalesReceipt { return notFoundEntriesIDs; } + /** + * Retrieve sale receipt with associated entries. + * @param {Integer} saleReceiptId + */ static async getSaleReceiptWithEntries(saleReceiptId) { const saleReceipt = await SaleReceipt.tenant().query() .where('id', saleReceiptId) diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 000000000..1753c9ea7 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "noImplicitAny": true, + "module": "commonjs", + "target": "es5", + "jsx": "react", + "allowJs": true + } +} \ No newline at end of file diff --git a/server/webpack.config.js b/server/webpack.config.js index 1289e3bc6..14403ae0f 100644 --- a/server/webpack.config.js +++ b/server/webpack.config.js @@ -28,6 +28,7 @@ module.exports = { '@': path.resolve(__dirname, 'src'), '~': path.resolve(__dirname, 'tests'), }, + extensions: [ '.tsx', '.ts', '.js' ], }, module: { rules: [ @@ -47,6 +48,11 @@ module.exports = { exclude: /(node_modules)/, test: /\.js$/, }, + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, ], }, };