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

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

View File

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

View File

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

View File

@@ -1,21 +1,27 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { useFormikContext } from 'formik';
import * as R from 'ramda';
import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import { useCreditNoteIsForeignCustomer } from './utils';
import { useCreditNoteIsForeignCustomer, useCreditNoteTotals } from './utils';
import withSettings from '@/containers/Settings/withSettings';
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}
*/
export function CreditNoteExchangeRateInputField({ ...props }) {
function CreditNoteExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const isForeignCustomer = useCreditNoteIsForeignCustomer();
// Can't continue if the customer is not foreign.
@@ -24,13 +30,21 @@ export function CreditNoteExchangeRateInputField({ ...props }) {
}
return (
<ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props}
/>
);
}
export const CreditNoteExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(CreditNoteExchangeRateInputFieldRoot);
/**
* Syncs credit note auto-increment settings to form.
* @return {React.ReactNode}
@@ -56,3 +70,28 @@ export const CreditNoteSyncIncrementSettingsToForm = R.compose(
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
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
@@ -19,7 +18,10 @@ import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter';
import EstimateFormDialogs from './EstimateFormDialogs';
import EstimtaeFormTopBar from './EstimtaeFormTopBar';
import { EstimateIncrementSyncSettingsToForm } from './components';
import {
EstimateIncrementSyncSettingsToForm,
EstimateSyncAutoExRateToForm,
} from './components';
import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
@@ -170,6 +172,7 @@ function EstimateForm({
{/*------- Effects -------*/}
<EstimateIncrementSyncSettingsToForm />
<EstimateSyncAutoExRateToForm />
</Form>
</Formik>
</div>

View File

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

View File

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

View File

@@ -1,24 +1,30 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import React, { useRef } from 'react';
import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core';
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import { useEstimateIsForeignCustomer } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { useEstimateIsForeignCustomer, useEstimateTotals } from './utils';
import { transactionNumber } from '@/utils';
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.
* @returns {JSX.Element}
*/
export function EstimateExchangeRateInputField({ ...props }) {
function EstimateExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const isForeignCustomer = useEstimateIsForeignCustomer();
// Can't continue if the customer is not foreign.
@@ -27,13 +33,26 @@ export function EstimateExchangeRateInputField({ ...props }) {
}
return (
<ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...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.
* @returns {JSX.Element}
@@ -72,3 +91,32 @@ export const EstimateIncrementSyncSettingsToForm = R.compose(
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 (
<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]);
};

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React from 'react';
import React, { useRef } from 'react';
import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
@@ -7,20 +7,26 @@ import * as R from 'ramda';
import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import { useReceiptIsForeignCustomer } from './utils';
import { useReceiptIsForeignCustomer, useReceiptTotals } from './utils';
import { useUpdateEffect } from '@/hooks';
import withSettings from '@/containers/Settings/withSettings';
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.
* @returns {JSX.Element}
*/
export function ReceiptExchangeRateInputField({ ...props }) {
function ReceiptExchangeRateInputFieldRoot({ ...props }) {
const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const isForeignCustomer = useReceiptIsForeignCustomer();
const { values } = useFormikContext();
// Can't continue if the customer is not foreign.
if (!isForeignCustomer) {
@@ -28,13 +34,21 @@ export function ReceiptExchangeRateInputField({ ...props }) {
}
return (
<ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
formGroupProps={{ label: ' ', inline: true }}
withPopoverRecalcConfirm
{...props}
/>
);
}
export const ReceiptExchangeRateInputField = R.compose(
withExchangeRateFetchingLoading,
withExchangeRateItemEntriesPriceRecalc,
)(ReceiptExchangeRateInputFieldRoot);
/**
* Receipt project select.
* @returns {JSX.Element}
@@ -73,3 +87,31 @@ export const ReceiptSyncIncrementSettingsToForm = R.compose(
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;
},
);