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

@@ -28,7 +28,7 @@ interface ExchangeRateInputGroupProps {
inputGroupProps?: any; inputGroupProps?: any;
formGroupProps?: any; formGroupProps?: any;
popoverRecalcConfirm?: boolean; withPopoverRecalcConfirm?: boolean;
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void; onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
onCancel: (bag: ExchangeRateValuesBag) => void; onCancel: (bag: ExchangeRateValuesBag) => void;
@@ -47,7 +47,7 @@ export function ExchangeRateInputGroup({
inputGroupProps, inputGroupProps,
formGroupProps, formGroupProps,
popoverRecalcConfirm = false, withPopoverRecalcConfirm = false,
onRecalcConfirm, onRecalcConfirm,
onCancel, onCancel,
@@ -97,6 +97,7 @@ export function ExchangeRateInputGroup({
onChange={() => null} onChange={() => null}
onBlur={handleExchangeRateFieldBlur} onBlur={handleExchangeRateFieldBlur}
rightElement={isLoading && <Spinner size={16} />} rightElement={isLoading && <Spinner size={16} />}
decimalsLimit={5}
{...inputGroupProps} {...inputGroupProps}
name={name} name={name}
/> />
@@ -142,7 +143,7 @@ export function ExchangeRateInputGroup({
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} = <ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
</ExchangeRatePrepend> </ExchangeRatePrepend>
{popoverRecalcConfirm ? ( {withPopoverRecalcConfirm ? (
<Popover isOpen={isOpen} content={popoverConfirmContent}> <Popover isOpen={isOpen} content={popoverConfirmContent}>
{exchangeRateField} {exchangeRateField}
</Popover> </Popover>

View File

@@ -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,
};

View File

@@ -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]);
};

View File

@@ -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;
};

View File

@@ -38,7 +38,10 @@ import {
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { CreditNoteSyncIncrementSettingsToForm } from './components'; import {
CreditNoteExchangeRateSync,
CreditNoteSyncIncrementSettingsToForm,
} from './components';
/** /**
* Credit note form. * Credit note form.
@@ -169,6 +172,7 @@ function CreditNoteForm({
{/*-------- Effects --------*/} {/*-------- Effects --------*/}
<CreditNoteSyncIncrementSettingsToForm /> <CreditNoteSyncIncrementSettingsToForm />
<CreditNoteExchangeRateSync />
</Form> </Form>
</Formik> </Formik>
</div> </div>

View File

@@ -26,6 +26,7 @@ import {
inputIntent, inputIntent,
handleDateChange, handleDateChange,
} from '@/utils'; } from '@/utils';
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
/** /**
* Credit note form header fields. * Credit note form header fields.
@@ -37,10 +38,8 @@ export default function CreditNoteFormHeaderFields({}) {
<CreditNoteCustomersSelect /> <CreditNoteCustomersSelect />
{/* ----------- Exchange rate ----------- */} {/* ----------- Exchange rate ----------- */}
<CreditNoteExchangeRateInputField <CreditNoteExchangeRateInputField />
name={'exchange_rate'}
formGroupProps={{ label: ' ', inline: true }}
/>
{/* ----------- Credit note date ----------- */} {/* ----------- Credit note date ----------- */}
<FastField name={'credit_note_date'}> <FastField name={'credit_note_date'}>
{({ form, field: { value }, meta: { error, touched } }) => ( {({ form, field: { value }, meta: { error, touched } }) => (
@@ -93,8 +92,18 @@ export default function CreditNoteFormHeaderFields({}) {
*/ */
function CreditNoteCustomersSelect() { function CreditNoteCustomersSelect() {
// Credit note form context. // Credit note form context.
const { customers } = useCreditNoteFormContext();
const { setFieldValue, values } = useFormikContext(); const { setFieldValue, values } = useFormikContext();
const { customers } = useCreditNoteFormContext();
const updateEntries = useCustomerUpdateExRate();
// Handles item change.
const handleItemChange = (customer) => {
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
updateEntries(customer);
};
return ( return (
<FFormGroup <FFormGroup
@@ -110,10 +119,7 @@ function CreditNoteCustomersSelect() {
name={'customer_id'} name={'customer_id'}
items={customers} items={customers}
placeholder={<T id={'select_customer_account'} />} placeholder={<T id={'select_customer_account'} />}
onItemChange={(customer) => { onItemChange={handleItemChange}
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
}}
popoverFill={true} popoverFill={true}
allowCreate={true} allowCreate={true}
fastField={true} fastField={true}

View File

@@ -6,6 +6,7 @@ import '@/style/pages/CreditNote/PageForm.scss';
import CreditNoteForm from './CreditNoteForm'; import CreditNoteForm from './CreditNoteForm';
import { CreditNoteFormProvider } from './CreditNoteFormProvider'; import { CreditNoteFormProvider } from './CreditNoteFormProvider';
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
/** /**
* Credit note form page. * Credit note form page.
@@ -16,7 +17,9 @@ export default function CreditNoteFormPage() {
return ( return (
<CreditNoteFormProvider creditNoteId={idAsInteger}> <CreditNoteFormProvider creditNoteId={idAsInteger}>
<CreditNoteForm /> <AutoExchangeRateProvider>
<CreditNoteForm />
</AutoExchangeRateProvider>
</CreditNoteFormProvider> </CreditNoteFormProvider>
); );
} }

View File

@@ -1,21 +1,27 @@
// @ts-nocheck // @ts-nocheck
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import * as R from 'ramda'; import * as R from 'ramda';
import { ExchangeRateInputGroup } from '@/components'; import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { useCreditNoteIsForeignCustomer } from './utils'; import { useCreditNoteIsForeignCustomer, useCreditNoteTotals } from './utils';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils'; import { transactionNumber } from '@/utils';
import {
useSyncExRateToForm,
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
/** /**
* credit exchange rate input field. * Credit note exchange rate input field.
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export function CreditNoteExchangeRateInputField({ ...props }) { function CreditNoteExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization(); const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext(); const { values } = useFormikContext();
const isForeignCustomer = useCreditNoteIsForeignCustomer(); const isForeignCustomer = useCreditNoteIsForeignCustomer();
// Can't continue if the customer is not foreign. // Can't continue if the customer is not foreign.
@@ -24,13 +30,21 @@ export function CreditNoteExchangeRateInputField({ ...props }) {
} }
return ( return (
<ExchangeRateInputGroup <ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code} fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency} toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props} {...props}
/> />
); );
} }
export const CreditNoteExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(CreditNoteExchangeRateInputFieldRoot);
/** /**
* Syncs credit note auto-increment settings to form. * Syncs credit note auto-increment settings to form.
* @return {React.ReactNode} * @return {React.ReactNode}
@@ -56,3 +70,28 @@ export const CreditNoteSyncIncrementSettingsToForm = R.compose(
return null; return null;
}); });
/**
* Syncs the realtime exchange rate to the credit note form and shows up popup to the user
* as an indication the entries rates have been re-calculated.
* @returns {React.ReactNode}
*/
export const CreditNoteExchangeRateSync = R.compose(withDialogActions)(
({ openDialog }) => {
const { total } = useCreditNoteTotals();
const timeout = useRef();
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.InvoiceExchangeRateChangeNotice);
}, 500);
}
},
});
return null;
},
);

View File

@@ -1,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import classNames from 'classnames';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
@@ -19,7 +18,10 @@ import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter'; import EstimateFormFooter from './EstimateFormFooter';
import EstimateFormDialogs from './EstimateFormDialogs'; import EstimateFormDialogs from './EstimateFormDialogs';
import EstimtaeFormTopBar from './EstimtaeFormTopBar'; import EstimtaeFormTopBar from './EstimtaeFormTopBar';
import { EstimateIncrementSyncSettingsToForm } from './components'; import {
EstimateIncrementSyncSettingsToForm,
EstimateSyncAutoExRateToForm,
} from './components';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
@@ -170,6 +172,7 @@ function EstimateForm({
{/*------- Effects -------*/} {/*------- Effects -------*/}
<EstimateIncrementSyncSettingsToForm /> <EstimateIncrementSyncSettingsToForm />
<EstimateSyncAutoExRateToForm />
</Form> </Form>
</Formik> </Formik>
</div> </div>

View File

@@ -1,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormGroup, InputGroup, Position, Classes } from '@blueprintjs/core'; import { FormGroup, InputGroup, Position, Classes } from '@blueprintjs/core';
@@ -24,7 +23,6 @@ import {
import { customersFieldShouldUpdate } from './utils'; import { customersFieldShouldUpdate } from './utils';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { ProjectsSelect } from '@/containers/Projects/components'; import { ProjectsSelect } from '@/containers/Projects/components';
import { import {
EstimateExchangeRateInputField, EstimateExchangeRateInputField,
@@ -32,12 +30,13 @@ import {
} from './components'; } from './components';
import { EstimateFormEstimateNumberField } from './EstimateFormEstimateNumberField'; import { EstimateFormEstimateNumberField } from './EstimateFormEstimateNumberField';
import { useEstimateFormContext } from './EstimateFormProvider'; import { useEstimateFormContext } from './EstimateFormProvider';
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
/** /**
* Estimate form header. * Estimate form header.
*/ */
export default function EstimateFormHeader() { export default function EstimateFormHeader() {
const { customers, projects } = useEstimateFormContext(); const { projects } = useEstimateFormContext();
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
@@ -45,10 +44,8 @@ export default function EstimateFormHeader() {
<EstimateFormCustomerSelect /> <EstimateFormCustomerSelect />
{/* ----------- Exchange Rate ----------- */} {/* ----------- Exchange Rate ----------- */}
<EstimateExchangeRateInputField <EstimateExchangeRateInputField />
name={'exchange_rate'}
formGroupProps={{ label: ' ', inline: true }}
/>
{/* ----------- Estimate Date ----------- */} {/* ----------- Estimate Date ----------- */}
<FastField name={'estimate_date'}> <FastField name={'estimate_date'}>
{({ form, field: { value }, meta: { error, touched } }) => ( {({ form, field: { value }, meta: { error, touched } }) => (
@@ -151,6 +148,16 @@ function EstimateFormCustomerSelect() {
const { setFieldValue, values } = useFormikContext(); const { setFieldValue, values } = useFormikContext();
const { customers } = useEstimateFormContext(); const { customers } = useEstimateFormContext();
const updateEntries = useCustomerUpdateExRate();
// Handles the customer item change.
const handleItemChange = (customer) => {
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
updateEntries(customer);
};
return ( return (
<FFormGroup <FFormGroup
label={<T id={'customer_name'} />} label={<T id={'customer_name'} />}
@@ -165,10 +172,7 @@ function EstimateFormCustomerSelect() {
name={'customer_id'} name={'customer_id'}
items={customers} items={customers}
placeholder={<T id={'select_customer_account'} />} placeholder={<T id={'select_customer_account'} />}
onItemChange={(customer) => { onItemChange={handleItemChange}
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
}}
popoverFill={true} popoverFill={true}
allowCreate={true} allowCreate={true}
fastField={true} fastField={true}

View File

@@ -6,6 +6,7 @@ import '@/style/pages/SaleEstimate/PageForm.scss';
import EstimateForm from './EstimateForm'; import EstimateForm from './EstimateForm';
import { EstimateFormProvider } from './EstimateFormProvider'; import { EstimateFormProvider } from './EstimateFormProvider';
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
/** /**
* Estimate form page. * Estimate form page.
@@ -16,7 +17,9 @@ export default function EstimateFormPage() {
return ( return (
<EstimateFormProvider estimateId={idInteger}> <EstimateFormProvider estimateId={idInteger}>
<EstimateForm /> <AutoExchangeRateProvider>
<EstimateForm />
</AutoExchangeRateProvider>
</EstimateFormProvider> </EstimateFormProvider>
); );
} }

View File

@@ -1,24 +1,30 @@
// @ts-nocheck // @ts-nocheck
import React, { useEffect } from 'react'; import React, { useRef } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import * as R from 'ramda'; import * as R from 'ramda';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { ExchangeRateInputGroup } from '@/components'; import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { useEstimateIsForeignCustomer } from './utils'; import { useEstimateIsForeignCustomer, useEstimateTotals } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils'; import { transactionNumber } from '@/utils';
import { useUpdateEffect } from '@/hooks'; import { useUpdateEffect } from '@/hooks';
import withSettings from '@/containers/Settings/withSettings';
import {
useSyncExRateToForm,
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
/** /**
* Estimate exchange rate input field. * Estimate exchange rate input field.
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export function EstimateExchangeRateInputField({ ...props }) { function EstimateExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization(); const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext(); const { values } = useFormikContext();
const isForeignCustomer = useEstimateIsForeignCustomer(); const isForeignCustomer = useEstimateIsForeignCustomer();
// Can't continue if the customer is not foreign. // Can't continue if the customer is not foreign.
@@ -27,13 +33,26 @@ export function EstimateExchangeRateInputField({ ...props }) {
} }
return ( return (
<ExchangeRateInputGroup <ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code} fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency} toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props} {...props}
/> />
); );
} }
/**
* Renders the estimate exchange rate input field with exchange rate
* with item entries price re-calc once exchange rate change.
* @returns {JSX.Element}
*/
export const EstimateExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(EstimateExchangeRateInputFieldRoot);
/** /**
* Estimate project select. * Estimate project select.
* @returns {JSX.Element} * @returns {JSX.Element}
@@ -72,3 +91,32 @@ export const EstimateIncrementSyncSettingsToForm = R.compose(
return null; return null;
}); });
/**
* Syncs the auto exchange rate to the estimate form and shows up popup to user
* as an indication the entries rates have been changed.
* @returns {React.ReactNode}
*/
export const EstimateSyncAutoExRateToForm = R.compose(withDialogActions)(
({
// #withDialogActions
openDialog,
}) => {
const { total } = useEstimateTotals();
const timeout = useRef();
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.InvoiceExchangeRateChangeNotice);
}, 500);
}
},
});
return null;
},
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -398,85 +398,3 @@ export const useIsInvoiceTaxExclusive = () => {
return values.inclusive_exclusive_tax === TaxType.Exclusive; 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]);
};

View File

@@ -34,7 +34,7 @@ import {
transformFormValuesToRequest, transformFormValuesToRequest,
resetFormState, resetFormState,
} from './utils'; } from './utils';
import { ReceiptSyncIncrementSettingsToForm } from './components'; import { ReceiptSyncAutoExRateToForm, ReceiptSyncIncrementSettingsToForm } from './components';
/** /**
* Receipt form. * Receipt form.
@@ -171,6 +171,7 @@ function ReceiptForm({
{/*---------- Effects ---------*/} {/*---------- Effects ---------*/}
<ReceiptSyncIncrementSettingsToForm /> <ReceiptSyncIncrementSettingsToForm />
<ReceiptSyncAutoExRateToForm />
</Form> </Form>
</Formik> </Formik>
</div> </div>

View File

@@ -33,6 +33,7 @@ import {
ReceiptProjectSelectButton, ReceiptProjectSelectButton,
} from './components'; } from './components';
import { ReceiptFormReceiptNumberField } from './ReceiptFormReceiptNumberField'; import { ReceiptFormReceiptNumberField } from './ReceiptFormReceiptNumberField';
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
/** /**
* Receipt form header fields. * Receipt form header fields.
@@ -46,10 +47,7 @@ export default function ReceiptFormHeader() {
<ReceiptFormCustomerSelect /> <ReceiptFormCustomerSelect />
{/* ----------- Exchange rate ----------- */} {/* ----------- Exchange rate ----------- */}
<ReceiptExchangeRateInputField <ReceiptExchangeRateInputField />
name={'exchange_rate'}
formGroupProps={{ label: ' ', inline: true }}
/>
{/* ----------- Deposit account ----------- */} {/* ----------- Deposit account ----------- */}
<FFormGroup <FFormGroup
@@ -148,6 +146,16 @@ function ReceiptFormCustomerSelect() {
const { setFieldValue, values } = useFormikContext(); const { setFieldValue, values } = useFormikContext();
const { customers } = useReceiptFormContext(); const { customers } = useReceiptFormContext();
const updateEntries = useCustomerUpdateExRate();
// Handles the customer item change.
const handleItemChange = (customer) => {
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
updateEntries(customer);
};
return ( return (
<FFormGroup <FFormGroup
name={'customer_id'} name={'customer_id'}
@@ -162,10 +170,7 @@ function ReceiptFormCustomerSelect() {
name={'customer_id'} name={'customer_id'}
items={customers} items={customers}
placeholder={<T id={'select_customer_account'} />} placeholder={<T id={'select_customer_account'} />}
onItemChange={(customer) => { onItemChange={handleItemChange}
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
}}
popoverFill={true} popoverFill={true}
allowCreate={true} allowCreate={true}
fastField={true} fastField={true}

View File

@@ -6,6 +6,7 @@ import '@/style/pages/SaleReceipt/PageForm.scss';
import ReceiptFrom from './ReceiptForm'; import ReceiptFrom from './ReceiptForm';
import { ReceiptFormProvider } from './ReceiptFormProvider'; import { ReceiptFormProvider } from './ReceiptFormProvider';
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
/** /**
* Receipt form page. * Receipt form page.
@@ -16,7 +17,9 @@ export default function ReceiptFormPage() {
return ( return (
<ReceiptFormProvider receiptId={idInt}> <ReceiptFormProvider receiptId={idInt}>
<ReceiptFrom /> <AutoExchangeRateProvider>
<ReceiptFrom />
</AutoExchangeRateProvider>
</ReceiptFormProvider> </ReceiptFormProvider>
); );
} }

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useRef } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
@@ -7,20 +7,26 @@ import * as R from 'ramda';
import { ExchangeRateInputGroup } from '@/components'; import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { useReceiptIsForeignCustomer } from './utils'; import { useReceiptIsForeignCustomer, useReceiptTotals } from './utils';
import { useUpdateEffect } from '@/hooks'; import { useUpdateEffect } from '@/hooks';
import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils'; import { transactionNumber } from '@/utils';
import withSettings from '@/containers/Settings/withSettings';
import {
useSyncExRateToForm,
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
/** /**
* Receipt exchange rate input field. * Receipt exchange rate input field.
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export function ReceiptExchangeRateInputField({ ...props }) { function ReceiptExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization(); const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const isForeignCustomer = useReceiptIsForeignCustomer(); const isForeignCustomer = useReceiptIsForeignCustomer();
const { values } = useFormikContext();
// Can't continue if the customer is not foreign. // Can't continue if the customer is not foreign.
if (!isForeignCustomer) { if (!isForeignCustomer) {
@@ -28,13 +34,21 @@ export function ReceiptExchangeRateInputField({ ...props }) {
} }
return ( return (
<ExchangeRateInputGroup <ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code} fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency} toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props} {...props}
/> />
); );
} }
export const ReceiptExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(ReceiptExchangeRateInputFieldRoot);
/** /**
* Receipt project select. * Receipt project select.
* @returns {JSX.Element} * @returns {JSX.Element}
@@ -73,3 +87,31 @@ export const ReceiptSyncIncrementSettingsToForm = R.compose(
return null; return null;
}); });
/**
* Syncs the auto exchange rate to the receipt form and shows up popup to user
* as an indication the entries rates have been changed.
* @returns {React.ReactNode}
*/
export const ReceiptSyncAutoExRateToForm = R.compose(withDialogActions)(
({
// #withDialogActions
openDialog,
}) => {
const { total } = useReceiptTotals();
const timeout = useRef();
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.InvoiceExchangeRateChangeNotice);
}, 500);
}
},
});
return null;
},
);

View File

@@ -6,7 +6,9 @@ function getRandomItemFromArray(arr) {
const randomIndex = Math.floor(Math.random() * arr.length); const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex]; return arr[randomIndex];
} }
function delay(t, val) {
return new Promise((resolve) => setTimeout(resolve, t, val));
}
/** /**
* Retrieves tax rates. * Retrieves tax rates.
* @param {number} customerId - Customer id. * @param {number} customerId - Customer id.
@@ -18,12 +20,15 @@ export function useExchangeRate(
) { ) {
return useQuery( return useQuery(
[QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency], [QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency],
() => async () => {
Promise.resolve({ await delay(100);
return {
from_currency: fromCurrency, from_currency: fromCurrency,
to_currency: toCurrency, to_currency: toCurrency,
exchange_rate: getRandomItemFromArray([4.231, 2.231]), exchange_rate: 1.00,
}), };
},
props, props,
); );
} }