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 8cddb3163..e069d0472 100644 --- a/server/src/database/migrations/20200719153909_create_bills_payments_table.js +++ b/server/src/database/migrations/20200719153909_create_bills_payments_table.js @@ -2,6 +2,8 @@ exports.up = function(knex) { return knex.schema.createTable('bills_payments', table => { table.increments(); + table.integer('vendor_id').unsigned(); + table.decimal('amount', 13, 3).defaultTo(0); table.integer('payment_account_id'); table.string('payment_number'); table.date('payment_date'); diff --git a/server/src/http/controllers/Purchases/Bills.js b/server/src/http/controllers/Purchases/Bills.js index 6be018720..cf81b57ab 100644 --- a/server/src/http/controllers/Purchases/Bills.js +++ b/server/src/http/controllers/Purchases/Bills.js @@ -250,7 +250,7 @@ export default class BillsController extends BaseController { */ static async getBill(req, res) { const { id: billId } = req.params; - const bill = await BillsService.getBill(billId); + const bill = await BillsService.getBillWithMetadata(billId); return res.status(200).send({ bill }); } @@ -324,6 +324,7 @@ export default class BillsController extends BaseController { const bills = await Bill.query() .onBuild((builder) => { dynamicListing.buildQuery()(builder); + builder.withGraphFetched('vendor'); return builder; }) .pagination(filter.page - 1, filter.page_size); diff --git a/server/src/http/controllers/Purchases/BillsPayments.ts b/server/src/http/controllers/Purchases/BillsPayments.ts index 28489a237..f08db0e45 100644 --- a/server/src/http/controllers/Purchases/BillsPayments.ts +++ b/server/src/http/controllers/Purchases/BillsPayments.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { check, param, query, ValidationChain } from 'express-validator'; +import { difference } from 'lodash'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import validateMiddleware from '@/http/middleware/validateMiddleware'; import BaseController from '@/http/controllers/BaseController'; @@ -40,6 +41,7 @@ export default class BillsPayments extends BaseController { asyncMiddleware(this.validateBillPaymentVendorExistance), asyncMiddleware(this.validatePaymentAccount), asyncMiddleware(this.validatePaymentNumber), + asyncMiddleware(this.validateEntriesIdsExistance), asyncMiddleware(this.validateEntriesBillsExistance), asyncMiddleware(this.validateVendorsDueAmount), asyncMiddleware(this.editBillPayment), @@ -117,10 +119,12 @@ export default class BillsPayments extends BaseController { * @param {Function} next */ static async validateBillPaymentExistance(req: Request, res: Response, next: any ) { - const foundBillPayment = await BillPaymentsService.isBillPaymentExists(req.params.id); + const { id: billPaymentId } = req.params; + const { BillPayment } = req.models; + const foundBillPayment = await BillPayment.query().findById(billPaymentId); if (!foundBillPayment) { - return res.status(404).sned({ + return res.status(404).send({ errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }], }); } @@ -178,8 +182,11 @@ export default class BillsPayments extends BaseController { const billPayment = { ...req.body }; const entriesBillsIds = billPayment.entries.map((e: any) => e.bill_id); - const notFoundBillsIds = await Bill.getNotFoundBills(entriesBillsIds); - + // Retrieve not found bills that associated to the given vendor id. + const notFoundBillsIds = await Bill.getNotFoundBills( + entriesBillsIds, + billPayment.vendor_id, + ); if (notFoundBillsIds.length > 0) { return res.status(400).send({ errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 600 }], @@ -227,6 +234,34 @@ export default class BillsPayments extends BaseController { next(); } + /** + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + static async validateEntriesIdsExistance(req: Request, res: Response, next: Function) { + const { BillPaymentEntry } = req.models; + + const billPayment = { id: req.params.id, ...req.body }; + const entriesIds = billPayment.entries + .filter((entry: any) => entry.id) + .map((entry: any) => entry.id); + + const storedEntries = await BillPaymentEntry.tenant().query() + .where('bill_payment_id', billPayment.id); + + const storedEntriesIds = storedEntries.map((entry: any) => 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 @@ -268,8 +303,16 @@ export default class BillsPayments extends BaseController { return res.status(200).send({ id: billPaymentId }); } + /** + * Retrieve the bill payment. + * @param {Request} req + * @param {Response} res + */ static async getBillPayment(req: Request, res: Response) { + const { id: billPaymentId } = req.params; + const billPayment = await BillPaymentsService.getBillPaymentWithMetadata(billPaymentId); + return res.status(200).send({ bill_payment: { ...billPayment } }); } /** @@ -341,15 +384,22 @@ export default class BillsPayments extends BaseController { } const billPayments = await BillPayment.query().onBuild((builder) => { dynamicListing.buildQuery()(builder); + builder.withGraphFetched('vendor'); + builder.withGraphFetched('paymentAccount'); return builder; - }); + }).pagination(filter.page - 1, filter.page_size); + return res.status(200).send({ - billPayments, - ...(viewMeta - ? { - customViewId: viewMeta.id, + bill_payments: { + ...billPayments, + ...(viewMeta + ? { + view_meta: { + 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 19199e834..632eadc5f 100644 --- a/server/src/http/controllers/Sales/PaymentReceives.js +++ b/server/src/http/controllers/Sales/PaymentReceives.js @@ -151,9 +151,11 @@ export default class PaymentReceivesController extends BaseController { * @param {Function} next - */ static async validateInvoicesIDs(req, res, next) { - const invoicesIds = req.body.entries.map((e) => e.invoice_id); + const paymentReceive = { ...req.body }; + const invoicesIds = paymentReceive.entries.map((e) => e.invoice_id); const notFoundInvoicesIDs = await SaleInvoicesService.isInvoicesExist( - invoicesIds + invoicesIds, + paymentReceive.customer_id, ); if (notFoundInvoicesIDs.length > 0) { return res.status(400).send({ @@ -376,7 +378,7 @@ export default class PaymentReceivesController extends BaseController { if (filter.stringified_filter_roles) { filter.filter_roles = JSON.parse(filter.stringified_filter_roles); } - const { Resource, PaymentReceive, View } = req.models; + const { Resource, PaymentReceive, View, Bill } = req.models; const resource = await Resource.query() .remember() .where('name', 'payment_receives') diff --git a/server/src/http/controllers/Sales/SalesInvoices.js b/server/src/http/controllers/Sales/SalesInvoices.js index 1598d3706..efab8d8bd 100644 --- a/server/src/http/controllers/Sales/SalesInvoices.js +++ b/server/src/http/controllers/Sales/SalesInvoices.js @@ -368,6 +368,7 @@ export default class SaleInvoicesController { } const salesInvoices = await SaleInvoice.query().onBuild((builder) => { builder.withGraphFetched('entries'); + builder.withGraphFetched('customer'); dynamicListing.buildQuery()(builder); }).pagination(filter.page - 1, filter.page_size); diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index 484e2bb9e..0586f6897 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -37,12 +37,55 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { } /** - * Retrieve the not found bills ids as array. + * Relationship mapping. + */ + static get relationMappings() { + const Vendor = require('@/models/Vendor'); + const ItemEntry = require('@/models/ItemEntry'); + + return { + /** + * + */ + vendor: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Vendor.default), + join: { + from: 'bills.vendorId', + to: 'vendors.id', + }, + }, + + /** + * + */ + entries: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(ItemEntry.default), + join: { + from: 'bills.id', + to: 'items_entries.referenceId', + }, + }, + }; + } + + /** + * Retrieve the not found bills ids as array that associated to the given vendor. * @param {Array} billsIds + * @param {number} vendorId - * @return {Array} */ - static async getNotFoundBills(billsIds) { - const storedBills = await this.tenant().query().whereIn('id', billsIds); + static async getNotFoundBills(billsIds, vendorId) { + const storedBills = await this.tenant().query() + .onBuild((builder) => { + builder.whereIn('id', billsIds); + + if (vendorId) { + builder.where('vendor_id', vendorId); + } + }); + const storedBillsIds = storedBills.map((t) => t.id); const notFoundBillsIds = difference( @@ -51,4 +94,12 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) { ); return notFoundBillsIds; } + + static changePaymentAmount(billId, amount) { + const changeMethod = amount > 0 ? 'increment' : 'decrement'; + return this.tenant() + .query() + .where('id', billId) + [changeMethod]('payment_amount', Math.abs(amount)); + } } diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index c417fd55a..2ca657e8c 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -19,40 +19,27 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { return ['createdAt', 'updatedAt']; } - /** - * Extend query builder model. - */ - 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'); const Vendor = require('@/models/Vendor'); + const Account = require('@/models/Account'); return { /** * */ entries: { - relation: Model.BelongsToOneRelation, + relation: Model.HasManyRelation, modelClass: this.relationBindKnex(BillPaymentEntry.default), join: { from: 'bills_payments.id', to: 'bills_payments_entries.billPaymentId', }, }, + /** * */ @@ -63,7 +50,19 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) { from: 'bills_payments.vendorId', to: 'vendors.id', }, - } + }, + + /** + * + */ + paymentAccount: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Account.default), + join: { + from: 'bills_payments.paymentAccountId', + to: 'accounts.id', + }, + }, }; } } diff --git a/server/src/models/BillPaymentEntry.js b/server/src/models/BillPaymentEntry.js index 39efdc1c0..de1bbe6a1 100644 --- a/server/src/models/BillPaymentEntry.js +++ b/server/src/models/BillPaymentEntry.js @@ -13,6 +13,6 @@ export default class BillPaymentEntry extends TenantModel { * Timestamps columns. */ get timestamps() { - return ['createdAt', 'updatedAt']; + return []; } } diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index d08efb8bd..96114cff6 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -38,8 +38,12 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { */ static get relationMappings() { const ItemEntry = require('@/models/ItemEntry'); + const Customer = require('@/models/Customer'); return { + /** + * + */ entries: { relation: Model.HasManyRelation, modelClass: this.relationBindKnex(ItemEntry.default), @@ -48,6 +52,18 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { to: 'items_entries.referenceId', }, }, + + /** + * + */ + customer: { + relation: Model.BelongsToOneRelation, + modelClass: this.relationBindKnex(Customer.default), + join: { + from: 'sales_invoices.customerId', + to: 'customers.id', + }, + }, }; } @@ -64,4 +80,6 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { .where('id', invoiceId) [changeMethod]('payment_amount', Math.abs(amount)); } + + } diff --git a/server/src/services/Purchases/BillPayments.js b/server/src/services/Purchases/BillPayments.js index 6f2ff9aae..49d7e8478 100644 --- a/server/src/services/Purchases/BillPayments.js +++ b/server/src/services/Purchases/BillPayments.js @@ -1,17 +1,30 @@ import express from 'express'; import { omit, sumBy } from 'lodash'; -import { BillPayment, BillPaymentEntry, Vendor } from '@/models'; -import ServiceItemsEntries from '../Sales/ServiceItemsEntries'; -import AccountsService from '../Accounts/AccountsService'; -import JournalPoster from '../Accounting/JournalPoster'; -import JournalEntry from '../Accounting/JournalEntry'; +import moment from 'moment'; +import { + BillPayment, + BillPaymentEntry, + Vendor, + Bill, + Account, + AccountTransaction, +} from '@/models'; +import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries'; +import AccountsService from '@/services/Accounts/AccountsService'; +import JournalPoster from '@/services/Accounting/JournalPoster'; +import JournalEntry from '@/services/Accounting/JournalEntry'; +import JournalPosterService from '@/services/Sales/JournalPosterService'; +/** + * Bill payments service. + * @service + */ export default class BillPaymentsService { /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. * - * Precedures + * Precedures:- * ------ * - Records the bill payment transaction. * - Records the bill payment associated entries. @@ -22,7 +35,7 @@ export default class BillPaymentsService { * @param {IBillPayment} billPayment */ static async createBillPayment(billPayment) { - const amount = sumBy(billPayment.entries, 'paymentAmount'); + const amount = sumBy(billPayment.entries, 'payment_amount'); const storedBillPayment = await BillPayment.tenant() .query() .insert({ @@ -39,17 +52,17 @@ export default class BillPaymentsService { ...entry, }); // Increment the bill payment amount. - const billOper = BillPayment.changePaymentAmount( - entry.billId, - entry.paymentAmount + const billOper = Bill.changePaymentAmount( + entry.bill_id, + entry.payment_amount, ); storeOpers.push(billOper); storeOpers.push(oper); }); // Decrement the vendor balance after bills payments. - const vendorDecrementOper = Vendor.changeBalanace( + const vendorDecrementOper = Vendor.changeBalance( billPayment.vendor_id, - amount * -1 + amount * -1, ); // Records the journal transactions after bills payment // and change diff acoount balance. @@ -68,8 +81,8 @@ export default class BillPaymentsService { /** * Edits the details of the given bill payment. * - * Preceducres. - * ------- + * Preceducres: + * ------ * - Update the bill payment transaction. * - Insert the new bill payment entries that have no ids. * - Update the bill paymeny entries that have ids. @@ -145,29 +158,32 @@ export default class BillPaymentsService { /** * Deletes the bill payment and associated transactions. - * @param {Integer} billPaymentId - + * @param {Integer} billPaymentId - The given bill payment id. * @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 BillPayment.tenant().query() + .where('id', billPaymentId) + .delete(); + await BillPaymentEntry.tenant() .query() .where('bill_payment_id', billPaymentId) .delete(); - const deleteTransactionsOper = this.deleteJournalTransactions( + const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions( billPaymentId, - 'BillPayment' + 'BillPayment', ); - const revertVendorBalance = Vendor.changeBalanace( - billpayment.vendor_id, - billPayment.amount * -1, + const revertVendorBalanceOper = Vendor.changeBalance( + billPayment.vendorId, + billPayment.amount, ); return Promise.all([ deleteTransactionsOper, - revertVendorBalance, + revertVendorBalanceOper, ]); } @@ -207,7 +223,7 @@ export default class BillPaymentsService { ...commonJournal, debit: paymentAmount, contactType: 'Vendor', - contactId: billpayment.vendor_id, + contactId: billPayment.vendor_id, account: payableAccount.id, }); const creditPaymentAccount = new JournalEntry({ @@ -225,18 +241,32 @@ export default class BillPaymentsService { ]); } - static async getBillPayment(billPaymentId) { - + /** + * Retrieve bill payment with associated metadata. + * @param {number} billPaymentId + * @return {object} + */ + static async getBillPaymentWithMetadata(billPaymentId) { + const billPayment = await BillPayment.tenant().query() + .where('id', billPaymentId) + .withGraphFetched('entries') + .withGraphFetched('vendor') + .withGraphFetched('paymentAccount') + .first(); + + return billPayment; } /** * Detarmines whether the bill payment exists on the storage. * @param {Integer} billPaymentId + * @return {boolean} */ static async isBillPaymentExists(billPaymentId) { const billPayment = await BillPayment.tenant().query() .where('id', billPaymentId) .first(); - return billPayment.length > 0; + + return (billPayment.length > 0); } } diff --git a/server/src/services/Purchases/Bills.js b/server/src/services/Purchases/Bills.js index a03d8946d..bf5ddd857 100644 --- a/server/src/services/Purchases/Bills.js +++ b/server/src/services/Purchases/Bills.js @@ -299,4 +299,19 @@ export default class BillsService { static getBill(billId) { return Bill.tenant().query().where('id', billId).first(); } + + + /** + * Retrieve the given bill details with associated items entries. + * @param {Integer} billId - + * @returns {Promise} + */ + static getBillWithMetadata(billId) { + return Bill.tenant() + .query() + .where('id', billId) + .withGraphFetched('vendor') + .withGraphFetched('entries') + .first(); + } } diff --git a/server/src/services/Sales/HasItemsEntries.ts b/server/src/services/Sales/HasItemsEntries.ts index 6fb312a82..cc18f7cfe 100644 --- a/server/src/services/Sales/HasItemsEntries.ts +++ b/server/src/services/Sales/HasItemsEntries.ts @@ -12,7 +12,12 @@ export default class HasItemEntries { * * @return {Promise} */ - static async patchItemsEntries(newEntries: Array, oldEntries: Array, referenceType: string, referenceId: string|number) { + static async patchItemsEntries( + newEntries: Array, + oldEntries: Array, + referenceType: string, + referenceId: string|number + ) { const entriesHasIds = newEntries.filter((entry) => entry.id); const entriesHasNoIds = newEntries.filter((entry) => !entry.id); const entriesIds = entriesHasIds.map(entry => entry.id);