fix: BIG-206 Reconcilate credit note and vendor credit with invoices should amount to credit be max the invoice remaining amount.

This commit is contained in:
a.bouhuolia
2021-12-30 15:39:22 +02:00
parent 0bd11419bb
commit 0173630e80
6 changed files with 105 additions and 13 deletions

View File

@@ -1,9 +1,16 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { defaultTo } from 'lodash';
import { DataTableEditable } from 'components'; import { DataTableEditable } from 'components';
import { compose, updateTableCell } from 'utils'; import { compose, updateTableCell } from 'utils';
import { useReconcileCreditNoteTableColumns } from './utils'; import { useDeepCompareEffect } from 'hooks/utils';
import {
useReconcileCreditNoteTableColumns,
maxAmountCreditFromRemaining,
maxCreditNoteAmountEntries,
} from './utils';
import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider';
/** /**
* Reconcile credit note entries table. * Reconcile credit note entries table.
@@ -16,6 +23,11 @@ export default function ReconcileCreditNoteEntriesTable({
// Retrieve the reconcile credit note table columns. // Retrieve the reconcile credit note table columns.
const columns = useReconcileCreditNoteTableColumns(); const columns = useReconcileCreditNoteTableColumns();
// Reconcile credit note context provider.
const {
creditNote: { credits_remaining },
} = useReconcileCreditNoteContext();
// Handle update data. // Handle update data.
const handleUpdateData = React.useCallback( const handleUpdateData = React.useCallback(
(rowIndex, columnId, value) => { (rowIndex, columnId, value) => {
@@ -26,6 +38,15 @@ export default function ReconcileCreditNoteEntriesTable({
}, },
[onUpdateData, entries], [onUpdateData, entries],
); );
// Deep compare entries to modify new entries.
useDeepCompareEffect(() => {
const newRows = compose(
maxCreditNoteAmountEntries(defaultTo(credits_remaining, 0)),
maxAmountCreditFromRemaining,
)(entries);
onUpdateData(newRows);
}, [entries]);
return ( return (
<ReconcileCreditNoteEditableTable <ReconcileCreditNoteEditableTable

View File

@@ -58,7 +58,6 @@ function ReconcileCreditNoteForm({
...values, ...values,
entries: entries, entries: entries,
}; };
// Handle the request success. // Handle the request success.
const onSuccess = (response) => { const onSuccess = (response) => {
AppToaster.show({ AppToaster.show({
@@ -68,7 +67,6 @@ function ReconcileCreditNoteForm({
setSubmitting(false); setSubmitting(false);
closeDialog(dialogName); closeDialog(dialogName);
}; };
// Handle the request error. // Handle the request error.
const onError = ({ const onError = ({
response: { response: {

View File

@@ -57,10 +57,12 @@ export default function ReconcileCreditNoteFormFields() {
* @returns {React.JSX} * @returns {React.JSX}
*/ */
function ReconcileCreditNoteTotalLines() { function ReconcileCreditNoteTotalLines() {
// Reconcile credit note context.
const { const {
creditNote: { credits_remaining, currency_code }, creditNote: { credits_remaining, currency_code },
} = useReconcileCreditNoteContext(); } = useReconcileCreditNoteContext();
// Formik form context.
const { values } = useFormikContext(); const { values } = useFormikContext();
// Calculate the total amount of credit entries. // Calculate the total amount of credit entries.
@@ -68,7 +70,6 @@ function ReconcileCreditNoteTotalLines() {
() => getEntriesTotal(values.entries), () => getEntriesTotal(values.entries),
[values.entries], [values.entries],
); );
// Calculate the total amount of credit remaining. // Calculate the total amount of credit remaining.
const creditsRemaining = subtract(credits_remaining, totalAmount); const creditsRemaining = subtract(credits_remaining, totalAmount);

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Callout, Intent, Classes } from '@blueprintjs/core'; import { Callout, Intent, Classes } from '@blueprintjs/core';
import * as R from 'ramda';
import clsx from 'classnames'; import clsx from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
@@ -9,27 +10,32 @@ import { MoneyFieldCell, FormatDateCell, AppToaster, T } from 'components';
export const transformErrors = (errors, { setErrors }) => { export const transformErrors = (errors, { setErrors }) => {
if (errors.some((e) => e.type === 'INVOICES_HAS_NO_REMAINING_AMOUNT')) { if (errors.some((e) => e.type === 'INVOICES_HAS_NO_REMAINING_AMOUNT')) {
AppToaster.show({ AppToaster.show({
message: 'INVOICES_HAS_NO_REMAINING_AMOUNT', message: 'The amount credit from the given invoice has no remaining amount.',
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
} }
if ( if (
errors.find((error) => error.type === 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT') errors.find((error) => error.type === 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT')
) { ) {
AppToaster.show({ AppToaster.show({
message: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT', message: 'The total amount bigger than from remaining credit note amount',
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
} }
}; };
/**
* Empty status callout.
* @returns {React.JSX}
*/
export function EmptyStatuCallout() { export function EmptyStatuCallout() {
return ( return (
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<Callout intent={Intent.PRIMARY}> <Callout intent={Intent.PRIMARY}>
<p> <p>
<T id={'reconcile_credit_note.alert.there_is_no_open_sale_invoices'} /> <T
id={'reconcile_credit_note.alert.there_is_no_open_sale_invoices'}
/>
</p> </p>
</Callout> </Callout>
</div> </div>
@@ -80,5 +86,34 @@ export const useReconcileCreditNoteTableColumns = () => {
}, },
], ],
[], [],
) );
} };
/**
* Sets max credit amount from sale invoicue balance.
*/
export const maxAmountCreditFromRemaining = (entries) => {
return entries.map((entry) => ({
...entry,
amount: entry.amount ? Math.min(entry.balance, entry.amount) : '',
}));
};
/**
* Adjusts entries amount based on the given total.
*/
export const maxCreditNoteAmountEntries = R.curry((total, entries) => {
let balance = total;
return entries.map((entry) => {
const oldBalance = balance;
balance -= entry.amount ? entry.amount : 0;
return {
...entry,
amount: entry.amount
? Math.max(Math.min(entry.amount, oldBalance), 0)
: '',
};
});
});

View File

@@ -1,10 +1,22 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import * as R from 'ramda';
import { defaultTo } from 'lodash';
import { useDeepCompareEffect } from 'hooks/utils';
import { DataTableEditable } from 'components'; import { DataTableEditable } from 'components';
import { compose, updateTableCell } from 'utils'; import { compose, updateTableCell } from 'utils';
import { useReconcileVendorCreditTableColumns } from './utils'; import {
useReconcileVendorCreditTableColumns,
maxAmountCreditFromRemaining,
} from './utils';
import { maxCreditNoteAmountEntries } from '../ReconcileCreditNoteDialog/utils';
import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider';
/**
* Reconcile vendor credit entries table.
*/
export default function ReconcileVendorCreditEntriesTable({ export default function ReconcileVendorCreditEntriesTable({
onUpdateData, onUpdateData,
entries, entries,
@@ -13,6 +25,11 @@ export default function ReconcileVendorCreditEntriesTable({
// Reconcile vendor credit table columns. // Reconcile vendor credit table columns.
const columns = useReconcileVendorCreditTableColumns(); const columns = useReconcileVendorCreditTableColumns();
// Reconcile vendor credit context.
const {
vendorCredit: { credits_remaining },
} = useReconcileVendorCreditContext();
// Handle update data. // Handle update data.
const handleUpdateData = React.useCallback( const handleUpdateData = React.useCallback(
(rowIndex, columnId, value) => { (rowIndex, columnId, value) => {
@@ -24,6 +41,16 @@ export default function ReconcileVendorCreditEntriesTable({
[onUpdateData, entries], [onUpdateData, entries],
); );
// Watches deeply entries to compose a new entries.
useDeepCompareEffect(() => {
const newEntries = R.compose(
maxCreditNoteAmountEntries(defaultTo(credits_remaining, 0)),
maxAmountCreditFromRemaining,
)(entries);
onUpdateData(newEntries);
}, [entries]);
return ( return (
<ReconcileVendorCreditEditableTable <ReconcileVendorCreditEditableTable
columns={columns} columns={columns}

View File

@@ -66,3 +66,13 @@ export const useReconcileVendorCreditTableColumns = () => {
[], [],
); );
}; };
/**
* Sets max amount credit from purchase due amount.
*/
export const maxAmountCreditFromRemaining = (entries) => {
return entries.map((entry) => ({
...entry,
amount: entry.amount ? Math.min(entry.due_amount, entry.amount) : '',
}));
};