From df2d2150714970698bbd1d8d5154a57763ef5b33 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 21 Feb 2021 13:00:06 +0200 Subject: [PATCH] refactoring: payment made form. --- .../Purchases/Bills/BillForm/BillForm.js | 4 +- .../Purchases/Bills/BillsLanding/BillsList.js | 2 + ...emsTable.js => PaymentMadeEntriesTable.js} | 48 +++++--- .../PaymentForm/PaymentMadeForm.js | 38 ++++--- .../PaymentForm/PaymentMadeFormBody.js | 23 ++++ .../PaymentForm/PaymentMadeFormHeader.js | 4 +- .../PaymentMadeFormHeaderFields.js | 58 +++++++--- .../PaymentForm/PaymentMadeFormProvider.js | 22 +--- .../PaymentForm/PaymentMadeInnerProvider.js | 47 ++++++++ .../PaymentMades/PaymentForm/components.js | 20 +++- .../PaymentMades/PaymentForm/utils.js | 8 +- .../PaymentsLanding/PaymentMadeList.js | 3 + client/src/hooks/query/paymentMades.js | 37 +++++-- client/src/hooks/useRequest.js | 2 +- client/src/style/pages/Bills/List.scss | 18 +++ client/src/style/pages/PaymentMade/List.scss | 18 +++ .../src/style/pages/PaymentMade/PageForm.scss | 14 ++- client/src/utils.js | 26 ++++- .../api/controllers/FinancialStatements.ts | 32 ++++-- .../controllers/Purchases/BillsPayments.ts | 63 +++++++++++ .../api/controllers/Sales/PaymentReceives.ts | 29 +++++ server/src/interfaces/BillPayment.ts | 11 ++ server/src/interfaces/PaymentReceive.ts | 4 +- server/src/services/Purchases/BillPayments.ts | 104 +++++++++++++----- server/src/services/Sales/PaymentsReceives.ts | 52 +++++---- 25 files changed, 542 insertions(+), 145 deletions(-) rename client/src/containers/Purchases/PaymentMades/PaymentForm/{PaymentMadeItemsTable.js => PaymentMadeEntriesTable.js} (67%) create mode 100644 client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormBody.js create mode 100644 client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeInnerProvider.js create mode 100644 client/src/style/pages/Bills/List.scss create mode 100644 client/src/style/pages/PaymentMade/List.scss diff --git a/client/src/containers/Purchases/Bills/BillForm/BillForm.js b/client/src/containers/Purchases/Bills/BillForm/BillForm.js index 33228c3ba..184a19053 100644 --- a/client/src/containers/Purchases/Bills/BillForm/BillForm.js +++ b/client/src/containers/Purchases/Bills/BillForm/BillForm.js @@ -4,7 +4,7 @@ import { Intent } from '@blueprintjs/core'; import classNames from 'classnames'; import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { sumBy, isEmpty, omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { CLASSES } from 'common/classes'; import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema'; @@ -68,7 +68,7 @@ export default function BillForm() { const entries = values.entries.filter( (item) => item.item_id && item.quantity, ); - const totalQuantity = safeSumBy(entries, (entry) => entry.quantity); + const totalQuantity = safeSumBy(entries, 'quantity'); if (totalQuantity === 0) { AppToaster.show({ diff --git a/client/src/containers/Purchases/Bills/BillsLanding/BillsList.js b/client/src/containers/Purchases/Bills/BillsLanding/BillsList.js index 3aad2bd11..e1723e8c4 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/BillsList.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/BillsList.js @@ -1,6 +1,8 @@ import React from 'react'; import { DashboardContentTable, DashboardPageContent } from 'components'; +import 'style/pages/Bills/List.scss'; + import { BillsListProvider } from './BillsListProvider'; import BillsActionsBar from './BillsActionsBar'; diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeItemsTable.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeEntriesTable.js similarity index 67% rename from client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeItemsTable.js rename to client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeEntriesTable.js index d9850583f..1f6c4a546 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeItemsTable.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeEntriesTable.js @@ -9,31 +9,34 @@ import { DataTableEditable } from 'components'; import { usePaymentMadeEntriesTableColumns } from './components'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; +import { compose, updateTableRow, safeSumBy } from 'utils'; +import withAlertActions from 'containers/Alert/withAlertActions'; /** * Payment made items table. */ -export default function PaymentMadeItemsTable() { +function PaymentMadeEntriesTable({ + onUpdateData, + entries, + + // #withAlertsActions + openAlert +}) { const { paymentVendorId, - dueBills, isDueBillsFetching, - isNewMode, } = usePaymentMadeFormContext(); const columns = usePaymentMadeEntriesTableColumns(); - - // Detarmines takes vendor payable bills entries in create mode - // or payment made entries in edit mode. - const computedTableEntries = useMemo(() => [], []); - - // Triggers `onUpdateData` event that passes changed entries. - const triggerUpdateData = useCallback((entries) => {}, []); - - const triggerOnFetchBillsSuccess = useCallback((bills) => {}, []); - + // Handle update data. - const handleUpdateData = useCallback((rows) => {}, []); + const handleUpdateData = useCallback((rowIndex, columnId, value) => { + const newRows = compose( + updateTableRow(rowIndex, columnId, value), + )(entries); + + onUpdateData(newRows); + }, [onUpdateData, entries]); // Detarmines the right no results message before selecting vendor and aftering // selecting vendor id. @@ -41,13 +44,22 @@ export default function PaymentMadeItemsTable() { ? 'There is no payable bills for this vendor that can be applied for this payment' : 'Please select a vendor to display all open bills for it.'; + // Handle clear all lines action. + const handleClearAllLines = () => { + const fullAmount = safeSumBy(entries, 'payment_amount'); + + if (fullAmount > 0) { + openAlert('clear-all-lines-payment-made'); + } + } + return ( @@ -68,3 +80,7 @@ export default function PaymentMadeItemsTable() { ); } + +export default compose( + withAlertActions +)(PaymentMadeEntriesTable); \ No newline at end of file diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.js index 3f5e11a85..13fafe03b 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.js @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { Formik, Form } from 'formik'; import { Intent } from '@blueprintjs/core'; import { useIntl } from 'react-intl'; -import { sumBy, omit } from 'lodash'; +import { sumBy, pick } from 'lodash'; import classNames from 'classnames'; import { useHistory } from 'react-router-dom'; @@ -11,7 +11,8 @@ import { AppToaster } from 'components'; import PaymentMadeHeader from './PaymentMadeFormHeader'; import PaymentMadeFloatingActions from './PaymentMadeFloatingActions'; import PaymentMadeFooter from './PaymentMadeFooter'; -import PaymentMadeItemsTable from './PaymentMadeItemsTable'; +import PaymentMadeFormBody from './PaymentMadeFormBody'; +import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider'; import withSettings from 'containers/Settings/withSettings'; import { @@ -32,7 +33,9 @@ function PaymentMadeForm() { // Payment made form context. const { isNewMode, - paymentMade, + paymentMadeId, + paymentMadeEditPage, + paymentEntriesEditPage, submitPayload, createPaymentMadeMutate, editPaymentMadeMutate, @@ -43,14 +46,14 @@ function PaymentMadeForm() { () => ({ ...(!isNewMode ? { - ...transformToEditForm(paymentMade, []), + ...transformToEditForm(paymentMadeEditPage, paymentEntriesEditPage), } : { ...defaultPaymentMade, entries: orderingLinesIndexes(defaultPaymentMade.entries), }), }), - [isNewMode, paymentMade], + [isNewMode, paymentMadeEditPage, paymentEntriesEditPage], ); // Handle the form submit. @@ -62,9 +65,9 @@ function PaymentMadeForm() { // Filters entries that have no `bill_id` or `payment_amount`. const entries = values.entries - .filter((item) => !item.bill_id || item.payment_amount) + .filter((item) => item.bill_id && item.payment_amount) .map((entry) => ({ - ...omit(entry, ['due_amount']), + ...pick(entry, ['payment_amount', 'bill_id']), })); // Total payment amount of entries. const totalPaymentAmount = sumBy(entries, 'payment_amount'); @@ -96,7 +99,11 @@ function PaymentMadeForm() { submitPayload.resetForm && resetForm(); }; - const onError = ({ response: { error: { data: errors } } }) => { + const onError = ({ + response: { + error: { data: errors }, + }, + }) => { const getError = (errorType) => errors.find((e) => e.type === errorType); if (getError(ERRORS.PAYMENT_NUMBER_NOT_UNIQUE)) { @@ -109,7 +116,7 @@ function PaymentMadeForm() { }; if (!isNewMode) { - editPaymentMadeMutate([paymentMade.id, form]) + editPaymentMadeMutate([paymentMadeId, form]) .then(onSaved) .catch(onError); } else { @@ -133,13 +140,12 @@ function PaymentMadeForm() { onSubmit={handleSubmitForm} >
- - -
- -
- - + + + + + + diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormBody.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormBody.js new file mode 100644 index 000000000..334303194 --- /dev/null +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormBody.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { FastField } from 'formik'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import PaymentMadeEntriesTable from './PaymentMadeEntriesTable'; + +export default function PaymentMadeFormBody() { + return ( +
+ + {({ form, field: { value }, meta: { error, touched } }) => ( + { + form.setFieldValue('entries', newEntries); + }} + /> + )} + + +
+ ) +} \ No newline at end of file diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.js index c2f98c3af..528524842 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.js @@ -18,10 +18,10 @@ function PaymentMadeFormHeader({ baseCurrency, }) { // Formik form context. - const { values } = useFormikContext(); + const { values: { entries } } = useFormikContext(); // Calculate the payment amount of the entries. - const amountPaid = useMemo(() => sumBy(values, 'payment_amount'), [values]); + const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]); return (
diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js index a64da7c51..886912dae 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js @@ -1,25 +1,27 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormGroup, InputGroup, Position, Classes, ControlGroup, + Button } from '@blueprintjs/core'; import { DateInput } from '@blueprintjs/datetime'; -import { FastField } from 'formik'; +import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { FormattedMessage as T } from 'react-intl'; +import { toSafeInteger } from 'lodash'; import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import { AccountsSelectList, ContactSelecetList, - ErrorMessage, FieldRequiredHint, InputPrependText, Money, Hint, Icon, + MoneyInputGroup } from 'components'; import withSettings from 'containers/Settings/withSettings'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; @@ -28,20 +30,43 @@ import { tansformDateValue, inputIntent, compose, + safeSumBy, + fullAmountPaymentEntries, + amountPaymentEntries, } from 'utils'; /** * Payment made form header fields. */ function PaymentMadeFormHeaderFields({ baseCurrency }) { + // Formik form context. + const { values: { entries }, setFieldValue } = useFormikContext(); + + // Payment made form context. const { vendors, accounts, isNewMode, setPaymentVendorId, } = usePaymentMadeFormContext(); + + // Sumation of payable full-amount. + const payableFullAmount = useMemo(() => safeSumBy(entries, 'due_amount'), [entries]); + + // Handle receive full-amount click. + const handleReceiveFullAmountClick = () => { + const newEntries = fullAmountPaymentEntries(entries); + const fullAmount = safeSumBy(newEntries, 'payment_amount'); - const payableFullAmount = 0; + setFieldValue('entries', newEntries); + setFieldValue('full_amount', fullAmount); + }; + + // Handles the full-amount field blur. + const onFullAmountBlur = (value) => { + const newEntries = amountPaymentEntries(toSafeInteger(value), entries); + setFieldValue('entries', newEntries); + }; return (
@@ -96,8 +121,8 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { {/* ------------ Full amount ------------ */} - - {({ form, field, meta: { error, touched } }) => ( + + {({ form, field: { value }, meta: { error, touched } }) => ( } inline={true} @@ -108,24 +133,27 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { > - { + setFieldValue('full_amount', value); + }} + onBlurValue={onFullAmountBlur} /> - Receive full amount ( ) - + )} - + {/* ------------ Payment number ------------ */} diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.js index c0af6177c..d1fd7a37e 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.js @@ -3,11 +3,10 @@ import { useAccounts, useVendors, useItems, - usePaymentMade, + usePaymentMadeEditPage, useSettings, useCreatePaymentMade, useEditPaymentMade, - useDueBills, } from 'hooks/query'; import { DashboardInsider } from 'components'; @@ -39,20 +38,13 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) { // Handle fetch specific payment made details. const { - data: { paymentMade, payableBills, paymentBills }, + data: { paymentMade: paymentMadeEditPage, entries: paymentEntriesEditPage }, isFetching: isPaymentFetching, isLoading: isPaymentLoading, - } = usePaymentMade(paymentMadeId, { + } = usePaymentMadeEditPage(paymentMadeId, { enabled: !!paymentMadeId, }); - // Retrieve the due bills of the given vendor. - const { - data: dueBills, - isLoading: isDueBillsLoading, - isFetching: isDueBillsFetching, - } = useDueBills(paymentVendorId, { enabled: !!paymentVendorId }); - // Fetch payment made settings. useSettings(); @@ -66,12 +58,10 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) { const provider = { paymentMadeId, accounts, - paymentMade, - payableBills, - paymentBills, + paymentEntriesEditPage, + paymentMadeEditPage, vendors, items, - dueBills, submitPayload, paymentVendorId, @@ -82,8 +72,6 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) { isVendorsFetching, isPaymentFetching, isPaymentLoading, - isDueBillsLoading, - isDueBillsFetching, createPaymentMadeMutate, editPaymentMadeMutate, diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeInnerProvider.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeInnerProvider.js new file mode 100644 index 000000000..9da5fe240 --- /dev/null +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeInnerProvider.js @@ -0,0 +1,47 @@ +import { useFormikContext } from 'formik'; +import { isEmpty } from 'lodash'; +import React, { createContext, useContext, useEffect } from 'react'; +import { usePaymentMadeNewPageEntries } from 'hooks/query'; +import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; + +const PaymentMadeInnerContext = createContext(); + +/** + * Payment made inner form provider. + */ +function PaymentMadeInnerProvider({ ...props }) { + const { isNewMode } = usePaymentMadeFormContext(); + + // Formik context. + const { + values: { vendor_id: vendorId }, + setFieldValue, + } = useFormikContext(); + + const { + data: newPageEntries, + isLoading: isNewEntriesLoading, + isFetching: isNewEntriesFetching, + } = usePaymentMadeNewPageEntries(vendorId, { + enabled: !!vendorId && isNewMode, + }); + + useEffect(() => { + if (!isNewEntriesFetching && !isEmpty(newPageEntries)) { + setFieldValue('entries', newPageEntries) + } + }, [isNewEntriesFetching, newPageEntries, setFieldValue]); + + // Provider payload. + const provider = { + newPageEntries, + isNewEntriesLoading, + isNewEntriesFetching + }; + + return ; +} + +const usePaymentMadeInnerContext = () => useContext(PaymentMadeInnerContext); + +export { PaymentMadeInnerProvider, usePaymentMadeInnerContext }; diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/components.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/components.js index 9e6383341..9b95d879c 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/components.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/components.js @@ -3,16 +3,17 @@ import { useIntl } from "react-intl"; import moment from 'moment'; import { Money } from 'components'; import { safeSumBy, formattedAmount } from 'utils'; +import { MoneyFieldCell } from 'components/DataTableCells'; function BillNumberAccessor(row) { - return `#${row?.bill_number || ''}` + return row?.bill_no ? row?.bill_no : '-'; } function IndexTableCell({ row: { index } }) { return ({index + 1}); } -function BillDateTableCell({ value }) { +function BillDateCell({ value }) { return moment(value).format('YYYY MMM DD'); } /** @@ -39,6 +40,14 @@ function PaymentAmountFooterCell({ rows }) { return { formattedAmount(totalPaymentAmount, 'USD') }; } +/** + * Mobey table cell. + */ +function MoneyTableCell({ value }) { + return +} + + /** * Payment made entries table columns */ @@ -54,13 +63,15 @@ export function usePaymentMadeEntriesTableColumns() { width: 40, disableResizing: true, disableSortBy: true, + className: 'index' }, { Header: formatMessage({ id: 'Date' }), id: 'bill_date', accessor: 'bill_date', - Cell: BillDateTableCell, + Cell: BillDateCell, disableSortBy: true, + width: 250, }, { Header: formatMessage({ id: 'bill_number' }), @@ -71,6 +82,7 @@ export function usePaymentMadeEntriesTableColumns() { { Header: formatMessage({ id: 'bill_amount' }), accessor: 'amount', + Cell: MoneyTableCell, Footer: AmountFooterCell, disableSortBy: true, className: '', @@ -78,6 +90,7 @@ export function usePaymentMadeEntriesTableColumns() { { Header: formatMessage({ id: 'amount_due' }), accessor: 'due_amount', + Cell: MoneyTableCell, Footer: DueAmountFooterCell, disableSortBy: true, className: '', @@ -85,6 +98,7 @@ export function usePaymentMadeEntriesTableColumns() { { Header: formatMessage({ id: 'payment_amount' }), accessor: 'payment_amount', + Cell: MoneyFieldCell, Footer: PaymentAmountFooterCell, disableSortBy: true, className: '', diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js index 244e4ae10..858d4d282 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js @@ -1,12 +1,10 @@ import moment from 'moment'; -import { sumBy } from 'lodash'; -import { transformToForm } from 'utils'; +import { safeSumBy, transformToForm } from 'utils'; export const ERRORS = { -PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE', + PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE', }; - // Default payment made entry values. export const defaultPaymentMadeEntry = { bill_id: '', @@ -30,7 +28,7 @@ export const defaultPaymentMade = { export const transformToEditForm = (paymentMade, paymentMadeEntries) => { return { ...transformToForm(paymentMade, defaultPaymentMade), - full_amount: sumBy(paymentMade.entries, 'payment_amount'), + full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'), entries: [ ...paymentMadeEntries.map((paymentMadeEntry) => ({ ...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry), diff --git a/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.js b/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.js index e6e597d2f..fa5ee8b18 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.js @@ -1,4 +1,7 @@ import React from 'react'; + +import 'style/pages/PaymentMade/List.scss'; + import { DashboardContentTable, DashboardPageContent } from 'components'; import PaymentMadeActionsBar from './PaymentMadeActionsBar'; import PaymentMadesAlerts from '../PaymentMadesAlerts'; diff --git a/client/src/hooks/query/paymentMades.js b/client/src/hooks/query/paymentMades.js index 994c7ed4b..00789db87 100644 --- a/client/src/hooks/query/paymentMades.js +++ b/client/src/hooks/query/paymentMades.js @@ -27,7 +27,7 @@ export function usePaymentMades(query, props) { data: defaultTo(states.data, { paymentMades: [], pagination: {}, - filterMeta: {} + filterMeta: {}, }), }; } @@ -42,8 +42,9 @@ export function useCreatePaymentMade(props) { return useMutation( (values) => apiRequest.post('purchases/bill_payments', values), { - onSuccess: () => { + onSuccess: (res, values) => { client.invalidateQueries('PAYMENT_MADES'); + client.invalidateQueries(['PAYMENT_MADE_NEW_PAGE_ENTRIES', values.vendor_id]); }, ...props, }, @@ -63,6 +64,8 @@ export function useEditPaymentMade(props) { onSuccess: (res, [id, values]) => { client.invalidateQueries('PAYMENT_MADES'); client.invalidateQueries(['PAYMENT_MADE', id]); + + client.invalidateQueries(['PAYMENT_MADE_NEW_PAGE_ENTRIES', values.vendor_id]); }, ...props, }, @@ -82,6 +85,7 @@ export function useDeletePaymentMade(props) { onSuccess: (res, id) => { client.invalidateQueries('PAYMENT_MADES'); client.invalidateQueries(['PAYMENT_MADE', id]); + }, ...props, }, @@ -91,17 +95,16 @@ export function useDeletePaymentMade(props) { /** * Retrieve specific payment made. */ -export function usePaymentMade(id, props) { +export function usePaymentMadeEditPage(id, props) { const apiRequest = useApiRequest(); const states = useQuery( ['PAYMENT_MADE', id], - () => apiRequest.get(`purchases/bill_payments/${id}`), + () => apiRequest.get(`purchases/bill_payments/${id}/edit-page`), { - select: res => ({ + select: (res) => ({ paymentMade: res.data.bill_payment, - payableBills: res.data.payable_bills, - paymentBills: res.data.payment_bills, + entries: res.data.entries, }), ...props, }, @@ -112,3 +115,23 @@ export function usePaymentMade(id, props) { data: defaultTo(states.data, {}), }; } + +/** + * Retreive payment made new page entries. + * @param {number} vendorId - + */ +export function usePaymentMadeNewPageEntries(vendorId, props) { + const apiRequest = useApiRequest(); + + return useQuery( + ['PAYMENT_MADE_NEW_PAGE_ENTRIES', vendorId], + () => + apiRequest.get(`purchases/bill_payments/new-page/entries`, { + params: { vendor_id: vendorId }, + }), + { + select: (res) => res.data.entries, + ...props, + }, + ); +} diff --git a/client/src/hooks/useRequest.js b/client/src/hooks/useRequest.js index cc53c4436..f3cc834c6 100644 --- a/client/src/hooks/useRequest.js +++ b/client/src/hooks/useRequest.js @@ -26,7 +26,7 @@ export default function useApiRequest() { const locale = 'en'; if (token) { - request.headers.common['x-access-token'] = token; + request.headers.common['X-Access-Token'] = token; } if (organizationId) { request.headers.common['organization-id'] = organizationId; diff --git a/client/src/style/pages/Bills/List.scss b/client/src/style/pages/Bills/List.scss new file mode 100644 index 000000000..f624d5afe --- /dev/null +++ b/client/src/style/pages/Bills/List.scss @@ -0,0 +1,18 @@ + +.dashboard__insider--bills{ + + .bigcapital-datatable{ + + .tbody{ + + .td.amount { + + .cell-inner{ + > span{ + font-weight: 600; + } + } + } + } + } +} \ No newline at end of file diff --git a/client/src/style/pages/PaymentMade/List.scss b/client/src/style/pages/PaymentMade/List.scss new file mode 100644 index 000000000..249dfd8d5 --- /dev/null +++ b/client/src/style/pages/PaymentMade/List.scss @@ -0,0 +1,18 @@ + +.dashboard__insider--payment-mades-list{ + + .bigcapital-datatable{ + + .tbody{ + + .td.amount { + + .cell-inner{ + > span{ + font-weight: 600; + } + } + } + } + } +} \ No newline at end of file diff --git a/client/src/style/pages/PaymentMade/PageForm.scss b/client/src/style/pages/PaymentMade/PageForm.scss index 0ab552952..164c2efeb 100644 --- a/client/src/style/pages/PaymentMade/PageForm.scss +++ b/client/src/style/pages/PaymentMade/PageForm.scss @@ -19,10 +19,18 @@ &.bp3-inline{ max-width: 470px; } - a.receive-full-amount{ + button.receive-full-amount{ + width: auto; + padding: 0; + min-height: auto; font-size: 12px; - margin-top: 6px; - display: inline-block; + margin-top: 4px; + background-color: transparent; + color: #0052cc; + + &:hover{ + text-decoration: underline; + } } } } diff --git a/client/src/utils.js b/client/src/utils.js index 7a595ba06..197a515d1 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -576,4 +576,28 @@ export function safeSumBy(entries, getter) { .map(row => toSafeNumber(_.get(row, getter))) .sum() .value(); -} \ No newline at end of file +} + + + +export const fullAmountPaymentEntries = (entries) => { + return entries.map((item) => ({ + ...item, + payment_amount: item.due_amount, + })); +} + + +export const amountPaymentEntries = (amount, entries) => { + let total = amount; + + return entries.map((item) => { + const diff = Math.min(item.due_amount, total); + total -= Math.max(diff, 0); + + return { + ...item, + payment_amount: diff, + }; + }); +}; \ No newline at end of file diff --git a/server/src/api/controllers/FinancialStatements.ts b/server/src/api/controllers/FinancialStatements.ts index eddad6615..43a0eb9b1 100644 --- a/server/src/api/controllers/FinancialStatements.ts +++ b/server/src/api/controllers/FinancialStatements.ts @@ -17,14 +17,32 @@ export default class FinancialStatementsService { router() { const router = Router(); - router.use('/balance_sheet', Container.get(BalanceSheetController).router()); - router.use('/profit_loss_sheet', Container.get(ProfitLossController).router()); - router.use('/general_ledger', Container.get(GeneralLedgerController).router()); - router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router()); + router.use( + '/balance_sheet', + Container.get(BalanceSheetController).router() + ); + router.use( + '/profit_loss_sheet', + Container.get(ProfitLossController).router() + ); + router.use( + '/general_ledger', + Container.get(GeneralLedgerController).router() + ); + router.use( + '/trial_balance_sheet', + Container.get(TrialBalanceSheetController).router() + ); router.use('/journal', Container.get(JournalSheetController).router()); - router.use('/receivable_aging_summary', Container.get(ARAgingSummary).router()); - router.use('/payable_aging_summary', Container.get(APAgingSummary).router()); + router.use( + '/receivable_aging_summary', + Container.get(ARAgingSummary).router() + ); + router.use( + '/payable_aging_summary', + Container.get(APAgingSummary).router() + ); return router; } -}; +} diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index dddddf8d2..16b5d34fe 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -8,6 +8,7 @@ import BillPaymentsService from 'services/Purchases/BillPayments'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import AccountsService from 'services/Accounts/AccountsService'; import ResourceController from '../Resources'; +import { Request } from 'express-validator/src/base'; /** * Bills payments controller. @@ -30,6 +31,20 @@ export default class BillsPayments extends BaseController { router() { const router = Router(); + router.get( + '/new-page/entries', + [query('vendor_id').exists()], + this.validationResult, + asyncMiddleware(this.getBillPaymentNewPageEntries.bind(this)), + this.handleServiceError + ); + router.get( + '/:id/edit-page', + this.specificBillPaymentValidateSchema, + this.validationResult, + asyncMiddleware(this.getBillPaymentEditPage.bind(this)), + this.handleServiceError + ); router.post( '/', [...this.billPaymentSchemaValidation], @@ -76,6 +91,7 @@ export default class BillsPayments extends BaseController { this.handleServiceError, this.dynamicListService.handlerErrorsToResponse ); + return router; } @@ -118,6 +134,53 @@ export default class BillsPayments extends BaseController { ]; } + /** + * Retrieve bill payment new page entries. + * @param {Request} req - + * @param {Response} res - + */ + async getBillPaymentNewPageEntries(req: Request, res: Response) { + const { tenantId } = req; + const { vendorId } = this.matchedQueryData(req); + + try { + const entries = await this.billPaymentService.getNewPageEntries( + tenantId, + vendorId + ); + return res.status(200).send({ + entries: this.transfromToResponse(entries), + }); + } catch (error) {} + } + + /** + * Retrieve the bill payment edit page details. + * @param {Request} req + * @param {Response} res + */ + async getBillPaymentEditPage(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: paymentReceiveId } = req.params; + + try { + const { + billPayment, + entries, + } = await this.billPaymentService.getBillPaymentEditPage( + tenantId, + paymentReceiveId, + ); + + return res.status(200).send({ + bill_payment: this.transfromToResponse(billPayment), + entries: this.transfromToResponse(entries), + }); + } catch (error) { + next(error); + } + } + /** * Creates a bill payment. * @async diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 282a2888e..8f9b1849a 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -54,6 +54,15 @@ export default class PaymentReceivesController extends BaseController { asyncMiddleware(this.getPaymentReceiveEditPage.bind(this)), this.handleServiceErrors ); + router.get( + '/new-page/entries', + [ + query('customer_id').exists().isNumeric().toInt(), + ], + this.validationResult, + asyncMiddleware(this.getPaymentReceiveNewPageEntries.bind(this)), + this.getPaymentReceiveNewPageEntries.bind(this) + ); router.get( '/', this.validatePaymentReceiveList, @@ -303,6 +312,26 @@ export default class PaymentReceivesController extends BaseController { } } + /** + * Retrieve payment receive new page receivable entries. + * @param {Request} req - Request. + * @param {Response} res - Response. + */ + async getPaymentReceiveNewPageEntries(req, res) { + const { tenantId } = req; + const { customerId } = this.matchedQueryData(req); + + try { + const entries = await this.paymentReceiveService.getNewPageEntries( + tenantId, + customerId + ); + return res.status(200).send({ + entries: this.transfromToResponse(entries), + }); + } catch (error) {} + } + /** * Handles service errors. * @param error diff --git a/server/src/interfaces/BillPayment.ts b/server/src/interfaces/BillPayment.ts index 4c31923f8..0de2cbd7e 100644 --- a/server/src/interfaces/BillPayment.ts +++ b/server/src/interfaces/BillPayment.ts @@ -32,4 +32,15 @@ export interface IBillPaymentDTO { description: string, reference: string, entries: IBillPaymentEntryDTO[], +}; + +export interface IBillReceivePageEntry { + billId: number, + entryType: string, + billNo: string, + dueAmount: number, + amount: number, + totalPaymentAmount: number, + paymentAmount: number, + date: Date|string, }; \ No newline at end of file diff --git a/server/src/interfaces/PaymentReceive.ts b/server/src/interfaces/PaymentReceive.ts index 9c04496fa..a545b8a01 100644 --- a/server/src/interfaces/PaymentReceive.ts +++ b/server/src/interfaces/PaymentReceive.ts @@ -52,7 +52,7 @@ export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string, } -export interface IPaymentReceiveEditPageEntry { +export interface IPaymentReceivePageEntry { invoiceId: number, entryType: string, invoiceNo: string, @@ -65,5 +65,5 @@ export interface IPaymentReceiveEditPageEntry { export interface IPaymentReceiveEditPage { paymentReceive: IPaymentReceive, - entries: IPaymentReceiveEditPageEntry[]; + entries: IPaymentReceivePageEntry[]; }; \ No newline at end of file diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index c66aa678a..b126b3144 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -15,6 +15,7 @@ import { IPaginationMeta, IFilterMeta, IBillPaymentEntry, + IBillReceivePageEntry, } from 'interfaces'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; @@ -26,6 +27,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { entriesAmountDiff, formatDateFields } from 'utils'; import { ServiceError } from 'exceptions'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; +import PayableAgingSummaryService from 'services/FinancialStatements/AgingSummary/APAgingSummaryService'; const ERRORS = { BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', @@ -598,48 +600,43 @@ export default class BillPaymentsService { * @param {number} billPaymentId - The bill payment id. * @return {object} */ - public async getBillPayment( + public async getBillPaymentEditPage( tenantId: number, billPaymentId: number ): Promise<{ - billPayment: IBillPayment; - payableBills: IBill[]; - paymentMadeBills: IBill[]; + billPayment: Omit; + entries: IBillReceivePageEntry[]; }> { const { BillPayment, Bill } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query() .findById(billPaymentId) - .withGraphFetched('entries.bill') - .withGraphFetched('vendor') - .withGraphFetched('paymentAccount'); + .withGraphFetched('entries.bill'); + // Throw not found the bill payment. if (!billPayment) { throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); } - 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'); - - // Retrieve all payment made assocaited bills. - const paymentMadeBills = billPayment.entries.map((entry) => ({ - ...entry.bill, - dueAmount: entry.bill.dueAmount + entry.paymentAmount, + const paymentEntries = billPayment.entries.map((entry) => ({ + ...this.mapBillToPageEntry(entry.bill), + paymentAmount: entry.paymentAmount, })); + const resPayableBills = await Bill.query() + .modify('dueBills') + .where('vendor_id', billPayment.vendorId) + .whereNotIn( + 'id', + billPayment.entries.map((e) => e.billId), + ) + .orderBy('bill_date', 'ASC'); + + // Mapping the payable bills to entries. + const restPayableEntries = resPayableBills.map(this.mapBillToPageEntry); + const entries = [...paymentEntries, ...restPayableEntries]; + return { - billPayment: { - ...billPayment, - entries: billPayment.entries.map((entry) => ({ - ...omit(entry, ['bill']), - })), - }, - payableBills, - paymentMadeBills, + billPayment: omit(billPayment, ['entries']), + entries }; } @@ -678,4 +675,55 @@ export default class BillPaymentsService { ); await Promise.all(opers); } + + /** + * Retrive edit page invoices entries from the given sale invoices models. + * @param {ISaleInvoice[]} invoices - Invoices. + * @return {IPaymentReceiveEditPageEntry} + */ + public mapBillToPageEntry(bill: IBill): IBillReceivePageEntry { + return { + entryType: 'invoice', + billId: bill.id, + dueAmount: bill.dueAmount + bill.paymentAmount, + amount: bill.amount, + billNo: bill.billNumber, + totalPaymentAmount: bill.paymentAmount, + paymentAmount: bill.paymentAmount, + date: bill.billDate, + }; + } + + public mapBillToNewPageEntry(bill: IBill): IBillReceivePageEntry { + return { + entryType: 'invoice', + billId: bill.id, + dueAmount: bill.dueAmount, + amount: bill.amount, + billNo: bill.billNumber, + date: bill.billDate, + totalPaymentAmount: bill.paymentAmount, + paymentAmount: 0, + } + } + + /** + * Retrieve the payable entries of the new page once vendor be selected. + * @param {number} tenantId + * @param {number} vendorId + */ + async getNewPageEntries( + tenantId: number, + vendorId: number, + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + // Retrieve all payable bills that assocaited to the payment made transaction. + const payableBills = await Bill.query() + .modify('dueBills') + .where('vendor_id', vendorId) + .orderBy('bill_date', 'ASC'); + + return payableBills.map(this.mapBillToNewPageEntry); + } } diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 4a0616e41..99be07bd3 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -18,7 +18,7 @@ import { ISaleInvoice, ISystemService, ISystemUser, - IPaymentReceiveEditPageEntry, + IPaymentReceivePageEntry, } from 'interfaces'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; @@ -471,7 +471,7 @@ export default class PaymentReceiveService { * @param {ISaleInvoice[]} invoices - Invoices. * @return {IPaymentReceiveEditPageEntry} */ - public invoicesToEditPageEntries( + public invoiceToPageEntry( invoice: ISaleInvoice ): IPaymentReceiveEditPageEntry { return { @@ -485,7 +485,7 @@ export default class PaymentReceiveService { date: invoice.invoiceDate, }; } - + /** * Retrieve the payment receive details of the given id. * @param {number} tenantId - Tenant id. @@ -494,9 +494,10 @@ export default class PaymentReceiveService { public async getPaymentReceiveEditPage( tenantId: number, paymentReceiveId: number, + authorizedUser: ISystemUser ): Promise<{ - paymentReceive: IPaymentReceive; - entries: IPaymentReceiveEditPageEntry[]; + paymentReceive: Omit; + entries: IPaymentReceivePageEntry[]; }> { const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); @@ -509,14 +510,8 @@ export default class PaymentReceiveService { if (!paymentReceive) { throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); } - - // Mapping the entries invoices. - const entriesInvoicesIds = paymentReceive.entries.map( - (entry) => entry.invoiceId - ); - const paymentEntries = paymentReceive.entries.map((entry) => ({ - ...this.invoicesToEditPageEntries(entry.invoice), + ...this.invoiceToPageEntry(entry.invoice), paymentAmount: entry.paymentAmount, })); @@ -524,17 +519,16 @@ export default class PaymentReceiveService { const restReceivableInvoices = await SaleInvoice.query() .modify('dueInvoices') .where('customer_id', paymentReceive.customerId) - .whereNotIn('id', entriesInvoicesIds) + .whereNotIn( + 'id', + paymentReceive.entries.map((entry) => entry.invoiceId) + ) .orderBy('invoice_date', 'ASC'); const restReceivableEntries = restReceivableInvoices.map( - this.invoicesToEditPageEntries + this.invoiceToPageEntry ); - - const entries = [ - ...paymentEntries, - ...restReceivableEntries, - ]; + const entries = [...paymentEntries, ...restReceivableEntries]; return { paymentReceive: omit(paymentReceive, ['entries']), @@ -616,6 +610,7 @@ export default class PaymentReceiveService { paymentReceiveId: number ) { const { PaymentReceive } = this.tenancy.models(tenantId); + return PaymentReceive.query() .where('id', paymentReceiveId) .withGraphFetched('invoices') @@ -739,7 +734,6 @@ export default class PaymentReceiveService { if (diffEntry.paymentAmount === 0) { return; } - const oper = SaleInvoice.changePaymentAmount( diffEntry.invoiceId, diffEntry.paymentAmount @@ -748,4 +742,22 @@ export default class PaymentReceiveService { }); await Promise.all([...opers]); } + + /** + * Retrieve payment receive new page receivable entries. + * @param {number} tenantId - Tenant id. + * @param {number} vendorId - Vendor id. + * @return {IPaymentReceivePageEntry[]} + */ + async getNewPageEntries(tenantId: number, customerId: number) { + const { SaleInvoice } = this.tenancy.models(tenantId); + + // Retrieve due invoices. + const entries = await SaleInvoice.query() + .modify('dueInvoices') + .where('customer_id', customerId) + .orderBy('invoice_date', 'ASC'); + + return entries.map(this.invoiceToPageEntry); + } }