mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat: implement auto entries rates re-calculation after change the exchange rate
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { useExchangeRate } from '@/hooks/query';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import React from 'react';
|
||||
|
||||
interface AutoExchangeRateProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AutoExchangeRateProviderValue {
|
||||
autoExRateCurrency: string;
|
||||
isAutoExchangeRateLoading: boolean;
|
||||
}
|
||||
|
||||
const AutoExchangeRateContext = React.createContext(
|
||||
{} as AutoExchangeRateProviderValue,
|
||||
);
|
||||
|
||||
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
|
||||
const [autoExRateCurrency, setAutoExRateCurrency] =
|
||||
React.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: 0,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
const value = {
|
||||
autoExRateCurrency,
|
||||
setAutoExRateCurrency,
|
||||
isAutoExchangeRateLoading,
|
||||
autoExchangeRate,
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoExchangeRateContext.Provider value={value}>
|
||||
{children}
|
||||
</AutoExchangeRateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext);
|
||||
|
||||
export {
|
||||
useAutoExRateContext,
|
||||
AutoExchangeRateContext,
|
||||
AutoExchangeRateProvider,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { round } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { updateItemsEntriesTotal } from './utils';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
),
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates items entries on exchange rate change.
|
||||
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
|
||||
*/
|
||||
export const useUpdateEntriesOnExchangeRateChange = () => {
|
||||
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]);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
// @ts-nocheck
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useUpdateEntriesOnExchangeRateChange } from './useUpdateEntriesOnExchangeRateChange';
|
||||
import { useAutoExRateContext } from './AutoExchangeProvider';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
|
||||
/**
|
||||
* Re-calculate the item entries prices based on the old exchange rate.
|
||||
* @param {InvoiceExchangeRateInputFieldRoot} Component
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export const withExchangeRateItemEntriesPriceRecalc =
|
||||
(Component) => (props) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const updateChangeExRate = useUpdateEntriesOnExchangeRateChange();
|
||||
|
||||
return (
|
||||
<Component
|
||||
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateChangeExRate(oldExchangeRate, exchangeRate),
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the loading props to the exchange rate field.
|
||||
* @param Component
|
||||
* @returns {}
|
||||
*/
|
||||
export const withExchangeRateFetchingLoading = (Component) => (props) => {
|
||||
const { isAutoExchangeRateLoading } = useAutoExRateContext();
|
||||
|
||||
return (
|
||||
<Component
|
||||
isLoading={isAutoExchangeRateLoading}
|
||||
inputGroupProps={{
|
||||
disabled: isAutoExchangeRateLoading,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the customer currency code and exchange rate once you update the customer
|
||||
* then change the state to fetch the realtime exchange rate of the new selected currency.
|
||||
*/
|
||||
export const useCustomerUpdateExRate = () => {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
const { setAutoExRateCurrency } = useAutoExRateContext();
|
||||
|
||||
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||
const currentCompany = useCurrentOrganization();
|
||||
|
||||
const DEFAULT_EX_RATE = 1;
|
||||
|
||||
return useCallback(
|
||||
(customer) => {
|
||||
// Reset the auto exchange rate currency cycle.
|
||||
setAutoExRateCurrency(null);
|
||||
|
||||
// If the customer's currency code equals the same base currency.
|
||||
if (customer.currency_code === currentCompany.base_currency) {
|
||||
setFieldValue('exchange_rate', DEFAULT_EX_RATE + '');
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateEntriesOnExChange(values.exchange_rate, DEFAULT_EX_RATE),
|
||||
);
|
||||
} else {
|
||||
// Sets the currency code to fetch exchange rate of the given currency code.
|
||||
setAutoExRateCurrency(customer?.currency_code);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentCompany.base_currency,
|
||||
setAutoExRateCurrency,
|
||||
setFieldValue,
|
||||
updateEntriesOnExChange,
|
||||
values.exchange_rate,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
interface UseSyncExRateToFormProps {
|
||||
onSynced?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the realtime exchange rate to the Formik form and then re-calculates
|
||||
* the entries rate based on the given new and old ex. rate.
|
||||
* @param {UseSyncExRateToFormProps} props -
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
|
||||
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||
|
||||
// Sync the fetched real-time exchanage rate to the form.
|
||||
useEffect(() => {
|
||||
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
|
||||
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateEntriesOnExChange(
|
||||
values.exchange_rate,
|
||||
autoExchangeRate?.exchange_rate,
|
||||
),
|
||||
);
|
||||
onSynced?.();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user