feat: implement auto entries rates re-calculation after change the exchange rate

This commit is contained in:
Ahmed Bouhuolia
2024-01-14 14:44:48 +02:00
parent 2cb8c2932f
commit 2b03ac7f16
24 changed files with 522 additions and 241 deletions

View File

@@ -22,7 +22,6 @@ function InvoiceExchangeRateChangeDialog({
return (
<Dialog
title={'Please take care of the following'}
name={dialogName}
autoFocus={true}
canEscapeKeyClose={true}
@@ -32,7 +31,7 @@ function InvoiceExchangeRateChangeDialog({
<DialogSuspense>
<div className={Classes.DIALOG_BODY}>
<p>
You have changed customers's currency after adding items to the
You have changed customer's currency after adding items to the
Invoice.
</p>
@@ -41,14 +40,14 @@ function InvoiceExchangeRateChangeDialog({
rate feeds.
</p>
<p>
<p style={{ marginBottom: '30px' }}>
Before saving the transaction, ensure that the item rates align with
the current exchange rate of the newly selected currency.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<Button onClick={handleConfirm} intent={Intent.PRIMARY}>
<Button onClick={handleConfirm} intent={Intent.PRIMARY} fill>
Ok
</Button>
</div>

View File

@@ -70,6 +70,7 @@ export default function InvoiceFloatingActions() {
history.goBack();
};
// Handle clear button click.
const handleClearBtnClick = (event) => {
resetForm();
};

View File

@@ -23,10 +23,7 @@ import {
handleDateChange,
} from '@/utils';
import { CLASSES } from '@/constants/classes';
import {
customerNameFieldShouldUpdate,
useInvoiceEntriesOnExchangeRateChange,
} from './utils';
import { customerNameFieldShouldUpdate } from './utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import {
@@ -39,7 +36,7 @@ import {
ProjectBillableEntriesLink,
} from '@/containers/Projects/components';
import { Features } from '@/constants';
import { useCurrentOrganization } from '@/hooks/state';
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
/**
* Invoice form header fields.
@@ -55,10 +52,8 @@ export default function InvoiceFormHeaderFields() {
<InvoiceFormCustomerSelect />
{/* ----------- Exchange rate ----------- */}
<InvoiceExchangeRateInputField
name={'exchange_rate'}
formGroupProps={{ label: ' ', inline: true }}
/>
<InvoiceExchangeRateInputField />
<Row>
<Col xs={6}>
{/* ----------- Invoice date ----------- */}
@@ -166,27 +161,18 @@ export default function InvoiceFormHeaderFields() {
*/
function InvoiceFormCustomerSelect() {
const { values, setFieldValue } = useFormikContext();
const { customers, setAutoExRateCurrency } = useInvoiceFormContext();
const currentComapny = useCurrentOrganization();
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
const { customers } = useInvoiceFormContext();
const updateEntries = useCustomerUpdateExRate();
// Handles the customer item change.
const handleItemChange = (customer) => {
setAutoExRateCurrency(null);
// If the customer id has changed change the customer id and currency code.
if (values.customer_id !== customer.id) {
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
}
// If the customer's currency code is the same the base currency.
if (customer?.currency_code === currentComapny.base_currency) {
setFieldValue('exchange_rate', '1');
setFieldValue('entries', composeEntriesOnExChange(values.exchange_rate, 1));
} else {
// Sets the currency code to fetch auto-exchange rate.
setAutoExRateCurrency(customer?.currency_code);
}
updateEntries(customer);
};
return (

View File

@@ -6,6 +6,7 @@ import '@/style/pages/SaleInvoice/PageForm.scss';
import InvoiceForm from './InvoiceForm';
import { InvoiceFormProvider } from './InvoiceFormProvider';
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
/**
* Invoice form page.
@@ -16,7 +17,9 @@ export default function InvoiceFormPage() {
return (
<InvoiceFormProvider invoiceId={idAsInteger}>
<InvoiceForm />
<AutoExchangeRateProvider>
<InvoiceForm />
</AutoExchangeRateProvider>
</InvoiceFormProvider>
);
}

View File

@@ -3,7 +3,7 @@ import React, { createContext, useState } from 'react';
import { isEmpty, pick } from 'lodash';
import { useLocation } from 'react-router-dom';
import { Features } from '@/constants';
import { useCurrentOrganization, useFeatureCan } from '@/hooks/state';
import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components/Dashboard';
import { transformToEditForm, ITEMS_FILTER_ROLES_QUERY } from './utils';
import {
@@ -16,7 +16,6 @@ import {
useEditInvoice,
useSettingsInvoices,
useEstimate,
useExchangeRate,
} from '@/hooks/query';
import { useProjects } from '@/containers/Projects/hooks';
import { useTaxRates } from '@/hooks/query/taxRates';
@@ -94,18 +93,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
// Handle fetching settings.
const { isLoading: isSettingsLoading } = useSettingsInvoices();
const [autoExRateCurrency, setAutoExRateCurrency] = useState<string>('');
const currentOrganization = useCurrentOrganization();
// Retrieves the exchange rate.
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
refetchOnWindowFocus: false,
staleTime: Infinity,
cacheTime: Infinity,
});
// Create and edit invoice mutations.
const { mutateAsync: createInvoiceMutate } = useCreateInvoice();
const { mutateAsync: editInvoiceMutate } = useEditInvoice();
@@ -132,7 +119,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
warehouses,
projects,
taxRates,
autoExchangeRate,
isInvoiceLoading,
isItemsLoading,
@@ -149,10 +135,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
editInvoiceMutate,
setSubmitPayload,
isNewMode,
autoExRateCurrency,
setAutoExRateCurrency,
isAutoExchangeRateLoading,
};
return (

View File

@@ -1,44 +1,22 @@
// @ts-nocheck
import { useEffect, useRef } from 'react';
import { useRef } from 'react';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import {
useInvoiceEntriesOnExchangeRateChange,
useInvoiceIsForeignCustomer,
useInvoiceTotal,
} from './utils';
import { useInvoiceIsForeignCustomer, useInvoiceTotal } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { useUpdateEffect } from '@/hooks';
import { transactionNumber } from '@/utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
/**
* Re-calculate the item entries prices based on the old exchange rate.
* @param {InvoiceExchangeRateInputFieldRoot} Component
* @returns {JSX.Element}
*/
const withExchangeRateItemEntriesPriceRecalc = (Component) => (props) => {
const { setFieldValue } = useFormikContext();
const composeChangeExRate = useInvoiceEntriesOnExchangeRateChange();
return (
<Component
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
setFieldValue(
'entries',
composeChangeExRate(oldExchangeRate, exchangeRate),
);
}}
{...props}
/>
);
};
import {
useSyncExRateToForm,
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
/**
* Invoice exchange rate input field.
@@ -47,8 +25,6 @@ const withExchangeRateItemEntriesPriceRecalc = (Component) => (props) => {
const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const { isAutoExchangeRateLoading } = useInvoiceFormContext();
const isForeignCustomer = useInvoiceIsForeignCustomer();
// Can't continue if the customer is not foreign.
@@ -60,7 +36,8 @@ const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
name={'exchange_rate'}
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
isLoading={isAutoExchangeRateLoading}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props}
/>
);
@@ -71,6 +48,7 @@ const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
* @returns {JSX.Element}
*/
export const InvoiceExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(InvoiceExchangeRateInputFieldRoot);
@@ -108,40 +86,26 @@ export const InvoiceNoSyncSettingsToForm = R.compose(
});
/**
* Syncs the fetched real-time exchange rate to the form.
* @returns {JSX.Element}
* Syncs the realtime exchange rate to the invoice form and shows up popup to the user
* as an indication the entries rates have been re-calculated.
* @returns {React.ReactNode}
*/
export const InvoiceExchangeRateSync = R.compose(withDialogActions)(
({ openDialog }) => {
const { setFieldValue, values } = useFormikContext();
const { autoExRateCurrency, autoExchangeRate } = useInvoiceFormContext();
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
const total = useInvoiceTotal();
const timeout = useRef();
// Sync the fetched real-time exchanage rate to the form.
useEffect(() => {
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
setFieldValue(
'entries',
composeEntriesOnExChange(
values.exchange_rate,
autoExchangeRate?.exchange_rate,
),
);
useSyncExRateToForm({
onSynced: () => {
// If the total bigger then zero show alert to the user after adjusting entries.
if (total > 0) {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
openDialog(DialogsName.InvoiceExchangeRateChange);
openDialog(DialogsName.InvoiceExchangeRateChangeNotice);
}, 500);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
},
});
return null;
},
);

View File

@@ -398,85 +398,3 @@ export const useIsInvoiceTaxExclusive = () => {
return values.inclusive_exclusive_tax === TaxType.Exclusive;
};
/**
* Convert the given rate to the local currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const convertToForeignCurrency = (
rate: number,
exchangeRate: number,
) => {
return rate * exchangeRate;
};
/**
* Converts the given rate to the base currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
return rate / exchangeRate;
};
/**
* Reverts the given rate from the old exchange rate and covert it to the new
* currency based on the given new exchange rate.
* @param {number} rate -
* @param {number} oldExchangeRate - Old exchange rate.
* @param {number} newExchangeRate - New exchange rate.
* @returns {number}
*/
const revertAndConvertExchangeRate = (
rate: number,
oldExchangeRate: number,
newExchangeRate: number,
) => {
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
return round(newValue, 3);
};
/**
* Assign the new item entry rate after converting to the new exchange rate.
* @params {number} oldExchangeRate -
* @params {number} newExchangeRate -
* @params {IItemEntry} entries -
*/
const assignRateRevertAndCovertExchangeRate = R.curry(
(oldExchangeRate: number, newExchangeRate: number, entries: IItemEntry[]) => {
return entries.map((entry) => ({
...entry,
rate: revertAndConvertExchangeRate(
entry.rate,
oldExchangeRate,
newExchangeRate,
),
}));
},
);
/**
* Compose invoice entries on exchange rate change.
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
*/
export const useInvoiceEntriesOnExchangeRateChange = () => {
const {
values: { entries },
} = useFormikContext();
return React.useMemo(() => {
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
return R.compose(
// Updates entries total.
updateItemsEntriesTotal,
// Assign a new rate of the given new exchange rate from the old exchange rate.
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
)(entries);
});
}, [entries]);
};