From 1738a333c7aa1be053a0208908aa7f1627db3c08 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 4 Nov 2020 00:23:58 +0200 Subject: [PATCH 1/2] fix: validate payment made entries ids exists. fix: retrieve payment made and receive details. --- .../controllers/Purchases/BillsPayments.ts | 13 ++-- .../api/controllers/Sales/PaymentReceives.ts | 22 +++++- .../api/controllers/Sales/SalesInvoices.ts | 2 +- server/src/models/BillPaymentEntry.js | 20 +++++- server/src/services/Purchases/BillPayments.ts | 43 ++++++++---- server/src/services/Sales/PaymentsReceives.ts | 70 ++++++++++++++----- 6 files changed, 132 insertions(+), 38 deletions(-) diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index cfa954993..b13013e94 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -198,13 +198,16 @@ export default class BillsPayments extends BaseController { const { id: billPaymentId } = req.params; try { - const { billPayment, payableBills } = await this.billPaymentService.getBillPayment(tenantId, billPaymentId); + const { + billPayment, + payableBills, + paymentMadeBills, + } = await this.billPaymentService.getBillPayment(tenantId, billPaymentId); return res.status(200).send({ - bill_payment: { - ...this.transfromToResponse({ ...billPayment }), - payable_bills: payableBills, - }, + bill_payment: this.transfromToResponse({ ...billPayment }), + payable_bills: this.transfromToResponse([ ...payableBills ]), + payment_bills: this.transfromToResponse([ ...paymentMadeBills ]), }); } catch (error) { next(error); diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 37d66dc56..965515d9b 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -7,6 +7,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import PaymentReceiveService from 'services/Sales/PaymentsReceives'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { ServiceError } from 'exceptions'; +import HasItemEntries from 'services/Sales/HasItemsEntries'; /** * Payments receives controller. @@ -209,10 +210,19 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const paymentReceive = await this.paymentReceiveService.getPaymentReceive( + const { + paymentReceive, + receivableInvoices, + paymentReceiveInvoices, + } = await this.paymentReceiveService.getPaymentReceive( tenantId, paymentReceiveId ); - return res.status(200).send({ paymentReceive }); + + return res.status(200).send({ + payment_receive: this.transfromToResponse({ ...paymentReceive }), + receivable_invoices: this.transfromToResponse([ ...receivableInvoices ]), + payment_invoices: this.transfromToResponse([ ...paymentReceiveInvoices ]), + }); } catch (error) { next(error); } @@ -314,7 +324,7 @@ export default class PaymentReceivesController extends BaseController { errors: [{ type: 'INVOICES_IDS_NOT_FOUND', code: 300 }], }); } - if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') { + if (error.errorType === 'ENTRIES_IDS_NOT_EXISTS') { return res.boom.badRequest(null, { errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 300 }], }); @@ -324,6 +334,12 @@ export default class PaymentReceivesController extends BaseController { errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 300 }], }); } + if (error.errorType === 'INVALID_PAYMENT_AMOUNT') { + return res.boom.badRequest(null, { + errors: [{ type: 'INVALID_PAYMENT_AMOUNT', code: 1000 }], + }); + } + console.log(error.errorType); } next(error); } diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index ae1a1aa60..29bbd53b0 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -84,7 +84,7 @@ export default class SaleInvoicesController extends BaseController{ check('customer_id').exists().isNumeric().toInt(), check('invoice_date').exists().isISO8601(), check('due_date').exists().isISO8601(), - check('invoice_no').exists().trim().escape(), + check('invoice_no').optional().trim().escape(), check('reference_no').optional().trim().escape(), check('status').exists().trim().escape(), diff --git a/server/src/models/BillPaymentEntry.js b/server/src/models/BillPaymentEntry.js index 88414bdb4..3167ec553 100644 --- a/server/src/models/BillPaymentEntry.js +++ b/server/src/models/BillPaymentEntry.js @@ -1,4 +1,4 @@ -import { mixin } from 'objection'; +import { mixin, Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class BillPaymentEntry extends TenantModel { @@ -15,4 +15,22 @@ export default class BillPaymentEntry extends TenantModel { get timestamps() { return []; } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Bill = require('models/Bill'); + + return { + bill: { + relation: Model.BelongsToOneRelation, + modelClass: Bill.default, + join: { + from: 'bills_payments_entries.billId', + to: 'bills.id', + }, + }, + }; + } } diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index c6f8dba45..821de0115 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -1,5 +1,5 @@ import { Inject, Service } from 'typedi'; -import { entries, omit, sumBy, difference } from 'lodash'; +import { omit, sumBy, difference } from 'lodash'; import { EventDispatcher, EventDispatcherInterface, @@ -488,27 +488,46 @@ export default class BillPaymentsService { * @param {number} billPaymentId - The bill payment id. * @return {object} */ - public async getBillPayment(tenantId: number, billPaymentId: number) { + public async getBillPayment(tenantId: number, billPaymentId: number): Promise<{ + billPayment: IBillPayment, + payableBills: IBill[], + paymentMadeBills: IBill[], + }> { const { BillPayment, Bill } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query() .findById(billPaymentId) - .withGraphFetched('entries') + .withGraphFetched('entries.bill') .withGraphFetched('vendor') .withGraphFetched('paymentAccount'); - + if (!billPayment) { throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); } + const billsIds = billPayment.entries.map((entry) => entry.billId); - const payableBills = await Bill.query().onBuild((builder) => { - const billsIds = billPayment.entries.map((entry) => entry.billId); + // Retrieve all payable bills that assocaited to the payment made transaction. + const payableBills = await Bill.query() + .modify('dueBills') + .whereNotIn('id', billsIds) + .where('vendor_id', billPayment.vendorId) + .orderBy('bill_date', 'ASC'); - builder.where('vendor_id', billPayment.vendorId); - builder.orWhereIn('id', billsIds); - builder.orderByRaw(`FIELD(id, ${billsIds.join(', ')}) DESC`); - builder.orderBy('bill_date', 'ASC'); - }) - return { billPayment, payableBills }; + // Retrieve all payment made assocaited bills. + const paymentMadeBills = billPayment.entries.map((entry) => ({ + ...(entry.bill), + dueAmount: (entry.bill.dueAmount + entry.paymentAmount), + })); + + return { + billPayment: { + ...billPayment, + entries: billPayment.entries.map((entry) => ({ + ...omit(entry, ['bill']), + })), + }, + payableBills, + paymentMadeBills, + }; } /** diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index f2c3112c3..06919570f 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -33,7 +33,8 @@ const ERRORS = { DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT', - INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND' + INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND', + ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS' }; /** * Payment receive service. @@ -180,6 +181,34 @@ export default class PaymentReceiveService { throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); } } + + /** + * Validate the payment receive entries IDs existance. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries + */ + private async validateEntriesIdsExistance( + tenantId: number, + paymentReceiveId: number, + paymentReceiveEntries: IPaymentReceiveEntryDTO[], + ) { + const { PaymentReceiveEntry } = this.tenancy.models(tenantId); + + const entriesIds = paymentReceiveEntries + .filter((entry) => entry.id) + .map((entry) => entry.id); + + const storedEntries = await PaymentReceiveEntry.query() + .where('payment_receive_id', paymentReceiveId); + + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); + + if (notFoundEntriesIds.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS); + } + } /** * Creates a new payment receive and store it to the storage @@ -266,9 +295,8 @@ export default class PaymentReceiveService { await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId); // Validate the entries ids existance on payment receive type. - await this.itemsEntries.validateEntriesIdsExistance( - tenantId, paymentReceiveId, 'PaymentReceive', paymentReceiveDTO.entries - ); + await this.validateEntriesIdsExistance(tenantId, paymentReceiveId, paymentReceiveDTO.entries); + // Validate payment receive invoices IDs existance and associated to the given customer id. await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.customerId, paymentReceiveDTO.entries); @@ -329,27 +357,37 @@ export default class PaymentReceiveService { public async getPaymentReceive( tenantId: number, paymentReceiveId: number - ): Promise<{ paymentReceive: IPaymentReceive[], receivableInvoices: ISaleInvoice }> { + ): Promise<{ + paymentReceive: IPaymentReceive, + receivableInvoices: ISaleInvoice[], + paymentReceiveInvoices: ISaleInvoice[], + }> { const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() .findById(paymentReceiveId) - .withGraphFetched('entries') + .withGraphFetched('entries.invoice') .withGraphFetched('customer') .withGraphFetched('depositAccount'); - + if (!paymentReceive) { throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); } - // Receivable open invoices. - const receivableInvoices = await SaleInvoice.query().onBuild((builder) => { - const invoicesIds = paymentReceive.entries.map((entry) => entry.invoiceId); + const invoicesIds = paymentReceive.entries.map((entry) => entry.invoiceId); - builder.where('customer_id', paymentReceive.customerId); - builder.orWhereIn('id', invoicesIds); - builder.orderByRaw(`FIELD(id, ${invoicesIds.join(', ')}) DESC`); - builder.orderBy('invoice_date', 'ASC'); - }); - return { paymentReceive, receivableInvoices }; + // Retrieves all receivable bills that associated to the payment receive transaction. + const receivableInvoices = await SaleInvoice.query() + .modify('dueInvoices') + .where('customer_id', paymentReceive.customerId) + .whereNotIn('id', invoicesIds) + .orderBy('invoice_date', 'ASC'); + + // Retrieve all payment receive associated invoices. + const paymentReceiveInvoices = paymentReceive.entries.map((entry) => ({ + ...(entry.invoice), + dueAmount: (entry.invoice.dueAmount + entry.paymentAmount), + })); + + return { paymentReceive, receivableInvoices, paymentReceiveInvoices }; } /** From 69e7612b624cb962660cc414bfe62113c245b3ff Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 5 Nov 2020 12:16:28 +0200 Subject: [PATCH 2/2] feat: payment receive and made form. --- client/src/common/classes.js | 3 + .../src/containers/Purchases/Bill/BillForm.js | 18 +- .../Purchases/Bill/BillFormHeader.js | 6 + .../containers/Purchases/Bill/withBills.js | 11 +- .../Purchases/PaymentMades/PaymentMade.js | 1 + .../PaymentMades/PaymentMadeFooter.js | 32 ++ .../Purchases/PaymentMades/PaymentMadeForm.js | 175 +++++++--- .../PaymentMades/PaymentMadeFormHeader.js | 288 ++++++++-------- .../PaymentMades/PaymentMadeItemsTable.js | 106 +++--- .../PaymentMadeItemsTableEditor.js | 10 +- .../Purchases/PaymentMades/withPaymentMade.js | 7 +- .../containers/Sales/Invoice/InvoiceForm.js | 1 - .../Sales/Invoice/InvoiceFormHeader.js | 1 - .../containers/Sales/Invoice/withInvoices.js | 12 +- .../PaymentReceive/PaymentReceiveForm.js | 188 +++++++---- .../PaymentReceiveFormFooter.js | 32 ++ .../PaymentReceiveFormHeader.js | 308 +++++++++--------- .../PaymentReceive/PaymentReceiveFormPage.js | 13 +- .../PaymentReceiveItemsTable.js | 91 +++--- .../PaymentReceiveItemsTableEditor.js | 11 +- .../Sales/PaymentReceive/PaymentReceives.js | 103 ------ .../withPaymentReceiveDetail.js | 5 +- client/src/lang/en/index.js | 3 +- client/src/routes/dashboard.js | 4 +- client/src/store/Bills/bills.reducer.js | 8 + client/src/store/Bills/bills.selectors.js | 60 ++-- client/src/store/Bills/bills.type.js | 2 +- client/src/store/Invoice/invoices.actions.js | 1 - client/src/store/Invoice/invoices.reducer.js | 10 +- client/src/store/Invoice/invoices.selector.js | 45 +-- client/src/store/Invoice/invoices.types.js | 3 +- .../store/PaymentMades/paymentMade.actions.js | 19 +- .../PaymentMades/paymentMade.selector.js | 50 ++- .../PaymentReceive/paymentReceive.actions.js | 21 +- .../PaymentReceive/paymentReceive.selector.js | 94 ++++-- client/src/style/App.scss | 34 ++ client/src/style/pages/payment-made.scss | 14 + client/src/style/pages/payment-receive.scss | 14 + client/src/utils.js | 12 + server/src/interfaces/SaleInvoice.ts | 1 + server/src/services/Purchases/BillPayments.ts | 16 +- server/src/services/Sales/PaymentsReceives.ts | 17 +- 42 files changed, 1100 insertions(+), 750 deletions(-) create mode 100644 client/src/containers/Purchases/PaymentMades/PaymentMadeFooter.js create mode 100644 client/src/containers/Sales/PaymentReceive/PaymentReceiveFormFooter.js delete mode 100644 client/src/containers/Sales/PaymentReceive/PaymentReceives.js diff --git a/client/src/common/classes.js b/client/src/common/classes.js index f6112b1a8..ca94a067a 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -8,6 +8,9 @@ const CLASSES = { PAGE_FORM: 'page-form', PAGE_FORM_HEADER: 'page-form__header', PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section', + PAGE_FORM_HEADER_FIELDS: 'page-form__header-fields', + PAGE_FORM_HEADER_BIG_NUMBERS: 'page-form__big-numbers', + PAGE_FORM_FOOTER: 'page-form__footer', PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions', diff --git a/client/src/containers/Purchases/Bill/BillForm.js b/client/src/containers/Purchases/Bill/BillForm.js index 47c800e5a..9a05536eb 100644 --- a/client/src/containers/Purchases/Bill/BillForm.js +++ b/client/src/containers/Purchases/Bill/BillForm.js @@ -13,10 +13,12 @@ import classNames from 'classnames'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { pick } from 'lodash'; import { CLASSES } from 'common/classes'; + import BillFormHeader from './BillFormHeader'; import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable'; import BillFloatingActions from './BillFloatingActions'; import BillFormFooter from './BillFormFooter'; + import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withMediaActions from 'containers/Media/withMediaActions'; import withBillActions from './withBillActions'; @@ -41,6 +43,7 @@ function BillForm({ //#withDashboard changePageTitle, + changePageSubtitle, //#withBillDetail bill, @@ -235,6 +238,7 @@ function BillForm({ setSubmitting(false); saveBillSubmit({ action: 'update', ...payload }); resetForm(); + changePageSubtitle(''); }) .catch((errors) => { handleErrors(errors, { setErrors }); @@ -302,10 +306,22 @@ function BillForm({ orderingIndex([...formik.values.entries, defaultBill]), ); }; + + const handleBillNumberChanged = useCallback((billNumber) => { + changePageSubtitle(billNumber); + }, []); + + // Clear page subtitle once unmount bill form page. + useEffect(() => () => { + changePageSubtitle(''); + }, [changePageSubtitle]); + return (
- + { + onBillNumberChanged && onBillNumberChanged(event.currentTarget.value); + }; + return (
@@ -166,6 +171,7 @@ function BillFormHeader({ intent={errors.bill_number && touched.bill_number && Intent.DANGER} minimal={true} {...getFieldProps('bill_number')} + onBlur={handleBillNumberBlur} /> diff --git a/client/src/containers/Purchases/Bill/withBills.js b/client/src/containers/Purchases/Bill/withBills.js index b3c4729f1..94bebc7e2 100644 --- a/client/src/containers/Purchases/Bill/withBills.js +++ b/client/src/containers/Purchases/Bill/withBills.js @@ -5,8 +5,7 @@ import { getBillPaginationMetaFactory, getBillTableQueryFactory, getVendorPayableBillsFactory, - getPayableBillsByPaymentMadeFactory, - getPaymentMadeFormPayableBillsFactory + getVendorPayableBillsEntriesFactory, } from 'store/Bills/bills.selectors'; export default (mapState) => { @@ -14,8 +13,7 @@ export default (mapState) => { const getBillsPaginationMeta = getBillPaginationMetaFactory(); const getBillTableQuery = getBillTableQueryFactory(); const getVendorPayableBills = getVendorPayableBillsFactory(); - const getPayableBillsByPaymentMade = getPayableBillsByPaymentMadeFactory(); - const getPaymentMadeFormPayableBills = getPaymentMadeFormPayableBillsFactory(); + const getVendorPayableBillsEntries = getVendorPayableBillsEntriesFactory(); const mapStateToProps = (state, props) => { const tableQuery = getBillTableQuery(state, props); @@ -29,9 +27,8 @@ export default (mapState) => { billsPageination: getBillsPaginationMeta(state, props, tableQuery), billsLoading: state.bills.loading, nextBillNumberChanged: state.bills.nextBillNumberChanged, - - // vendorPayableBills: getVendorPayableBills(state, props), - paymentMadePayableBills: getPaymentMadeFormPayableBills(state, props), + vendorPayableBills: getVendorPayableBills(state, props), + vendorPayableBillsEntries: getVendorPayableBillsEntries(state, props), }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMade.js b/client/src/containers/Purchases/PaymentMades/PaymentMade.js index 7eee9a8fb..6afad957b 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentMade.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentMade.js @@ -83,6 +83,7 @@ function PaymentMade({ > ); diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMadeFooter.js b/client/src/containers/Purchases/PaymentMades/PaymentMadeFooter.js new file mode 100644 index 000000000..d91bd7606 --- /dev/null +++ b/client/src/containers/Purchases/PaymentMades/PaymentMadeFooter.js @@ -0,0 +1,32 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FormGroup, TextArea } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; +import { Row, Col } from 'components'; +import { CLASSES } from 'common/classes'; + +/** + * Payment made form footer. + */ +export default function PaymentMadeFooter({ + getFieldProps +}) { + return ( +
+ + + {/* --------- Statement --------- */} + } + className={'form-group--statement'} + > +