From 9531730d7a584ff9f4ec72314bd85cfe88684f78 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 16 Oct 2023 19:14:27 +0200 Subject: [PATCH 1/3] feat: Auto re-calculate the items rate once changing the invoice exchange rate. --- .../components/Customers/CustomersSelect.tsx | 2 +- .../src/components/DialogsContainer.tsx | 7 +- .../ExchangeRate/ExchangeRateInput.tsx | 153 ++++++++++++++++-- packages/webapp/src/constants/dialogs.ts | 1 + .../containers/AlertsContainer/registered.tsx | 4 +- .../ExchangeRateForm.schema.tsx | 19 --- .../ExchangeRateForm.tsx | 114 ------------- .../ExchangeRateFormContent.tsx | 14 -- .../ExchangeRateFormDialogContent.tsx | 27 ---- .../ExchangeRateFormFields.tsx | 89 ---------- .../ExchangeRateFormFooter.tsx | 36 ----- .../ExchangeRateFormProvider.tsx | 53 ------ .../Dialogs/ExchangeRateFormDialog/index.tsx | 45 ------ .../ExchangeRates/ExchangeRateActionsBar.tsx | 147 ----------------- .../ExchangeRates/ExchangeRateTable.tsx | 110 ------------- .../ExchangeRates/ExchangeRatesAlerts.tsx | 10 -- .../ExchangeRates/ExchangeRatesList.tsx | 38 ----- .../ExchangeRates/ExchangeRatesProvider.tsx | 42 ----- .../containers/ExchangeRates/components.tsx | 91 ----------- .../ExchangeRates/withExchangeRateDetail.tsx | 9 -- .../ExchangeRates/withExchangeRates.tsx | 16 -- .../withExchangeRatesActions.tsx | 10 -- .../InvoiceExchangeRateChangeDialog.tsx | 63 ++++++++ .../Invoices/InvoiceForm/Dialogs/index.ts | 16 ++ .../Invoices/InvoiceForm/InvoiceForm.tsx | 3 +- .../InvoiceForm/InvoiceFormHeaderFields.tsx | 34 +++- .../InvoiceForm/InvoiceFormProvider.tsx | 20 ++- .../Sales/Invoices/InvoiceForm/components.tsx | 87 +++++++++- .../Sales/Invoices/InvoiceForm/utils.tsx | 86 +++++++++- .../webapp/src/hooks/query/exchangeRates.tsx | 113 +++---------- packages/webapp/src/hooks/query/types.tsx | 14 +- packages/webapp/src/routes/dashboard.tsx | 10 -- 32 files changed, 473 insertions(+), 1010 deletions(-) delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.schema.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormContent.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFields.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFooter.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormProvider.tsx delete mode 100644 packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/index.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/ExchangeRateActionsBar.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/ExchangeRateTable.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/ExchangeRatesAlerts.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/ExchangeRatesList.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/ExchangeRatesProvider.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/components.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/withExchangeRateDetail.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/withExchangeRates.tsx delete mode 100644 packages/webapp/src/containers/ExchangeRates/withExchangeRatesActions.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/index.ts diff --git a/packages/webapp/src/components/Customers/CustomersSelect.tsx b/packages/webapp/src/components/Customers/CustomersSelect.tsx index 60e0303147..60feeebe0f 100644 --- a/packages/webapp/src/components/Customers/CustomersSelect.tsx +++ b/packages/webapp/src/components/Customers/CustomersSelect.tsx @@ -34,7 +34,7 @@ function CustomerSelectRoot({ - + ); } diff --git a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx index 9877f5dee4..6f5bcdcec5 100644 --- a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx +++ b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx @@ -1,30 +1,154 @@ // @ts-nocheck -import React from 'react'; +import { useState } from 'react'; import styled from 'styled-components'; -import { ControlGroup } from '@blueprintjs/core'; - +import { useFormikContext } from 'formik'; +import { + Button, + Classes, + ControlGroup, + Intent, + Popover, + Spinner, +} from '@blueprintjs/core'; import { FlagIcon } from '../Tags'; import { FMoneyInputGroup, FFormGroup } from '../Forms'; +import { useUncontrolled } from '@/hooks/useUncontrolled'; + +interface ExchangeRateValuesBag { + oldExchangeRate: string; + exchangeRate: string; +} + +interface ExchangeRateInputGroupProps { + name: string; + fromCurrency: string; + toCurrency: string; + isLoading?: boolean; + + inputGroupProps?: any; + formGroupProps?: any; + + popoverRecalcConfirm?: boolean; + + onRecalcConfirm: (bag: ExchangeRateValuesBag) => void; + onCancel: (bag: ExchangeRateValuesBag) => void; + + isConfirmPopoverOpen?: boolean; + initialConfirmPopoverOpen?: boolean; + onConfirmPopoverOpen?: (isOpen: boolean) => void; +} export function ExchangeRateInputGroup({ + name, fromCurrency, toCurrency, + isLoading, + inputGroupProps, formGroupProps, - name, -}) { + + popoverRecalcConfirm = false, + + onRecalcConfirm, + onCancel, + + isConfirmPopoverOpen, + initialConfirmPopoverOpen, + onConfirmPopoverOpen, +}: ExchangeRateInputGroupProps) { + const [isOpen, handlePopoverOpen] = useUncontrolled({ + value: isConfirmPopoverOpen, + initialValue: initialConfirmPopoverOpen, + finalValue: false, + onChange: onConfirmPopoverOpen, + }); + const { values, setFieldValue } = useFormikContext(); + const [oldExchangeRate, setOldExchangeRate] = useState(''); + + const exchangeRate = values[name]; + const exchangeRateValuesBag: ExchangeRateValuesBag = { + exchangeRate, + oldExchangeRate, + }; + // Handle re-calc confirm button click. + const handleRecalcConfirmBtn = () => { + handlePopoverOpen(false); + onRecalcConfirm && onRecalcConfirm(exchangeRateValuesBag); + }; + // Handle cancel button click. + const handleCancelBtn = () => { + handlePopoverOpen(false); + onCancel && onCancel(exchangeRateValuesBag); + }; + // Handle exchange rate field blur. + const handleExchangeRateFieldBlur = (value: string) => { + if (value !== values[name]) { + handlePopoverOpen(true); + setFieldValue(name, value); + setOldExchangeRate(values[name]); + } + }; + + const exchangeRateField = ( + null} + onBlur={handleExchangeRateFieldBlur} + rightElement={isLoading && } + {...inputGroupProps} + name={name} + /> + ); + + const popoverConfirmContent = ( + +

+ Are you want to re-calculate item prices based on this exchange rate +

+
+ + +
+
+ ); + return ( 1 {fromCurrency} = - + + {popoverRecalcConfirm ? ( + + {exchangeRateField} + + ) : ( + exchangeRateField + )} {toCurrency} @@ -34,7 +158,7 @@ export function ExchangeRateInputGroup({ } const ExchangeRateField = styled(FMoneyInputGroup)` - max-width: 75px; + max-width: 85px; `; const ExchangeRateSideIcon = styled.div` @@ -57,3 +181,8 @@ const ExchangeFlagIcon = styled(FlagIcon)` margin-left: 5px; display: inline-block; `; + +const PopoverContent = styled('div')` + padding: 20px; + width: 300px; +`; diff --git a/packages/webapp/src/constants/dialogs.ts b/packages/webapp/src/constants/dialogs.ts index 115c25af27..e9c82bdd42 100644 --- a/packages/webapp/src/constants/dialogs.ts +++ b/packages/webapp/src/constants/dialogs.ts @@ -48,4 +48,5 @@ export enum DialogsName { ProjectBillableEntriesForm = 'project-billable-entries', InvoiceNumberSettings = 'InvoiceNumberSettings', TaxRateForm = 'tax-rate-form', + InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice' } diff --git a/packages/webapp/src/containers/AlertsContainer/registered.tsx b/packages/webapp/src/containers/AlertsContainer/registered.tsx index 417583f60d..d686662539 100644 --- a/packages/webapp/src/containers/AlertsContainer/registered.tsx +++ b/packages/webapp/src/containers/AlertsContainer/registered.tsx @@ -12,7 +12,6 @@ import PaymentMadesAlerts from '@/containers/Purchases/PaymentMades/PaymentMades import CustomersAlerts from '@/containers/Customers/CustomersAlerts'; import VendorsAlerts from '@/containers/Vendors/VendorsAlerts'; import ManualJournalsAlerts from '@/containers/Accounting/JournalsLanding/ManualJournalsAlerts'; -import ExchangeRatesAlerts from '@/containers/ExchangeRates/ExchangeRatesAlerts'; import ExpensesAlerts from '@/containers/Expenses/ExpensesAlerts'; import AccountTransactionsAlerts from '@/containers/CashFlow/AccountTransactions/AccountTransactionsAlerts'; import UsersAlerts from '@/containers/Preferences/Users/UsersAlerts'; @@ -41,7 +40,6 @@ export default [ ...CustomersAlerts, ...VendorsAlerts, ...ManualJournalsAlerts, - ...ExchangeRatesAlerts, ...ExpensesAlerts, ...AccountTransactionsAlerts, ...UsersAlerts, @@ -54,5 +52,5 @@ export default [ ...WarehousesTransfersAlerts, ...BranchesAlerts, ...ProjectAlerts, - ...TaxRatesAlerts + ...TaxRatesAlerts, ]; diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.schema.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.schema.tsx deleted file mode 100644 index 7ff0ff63d3..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.schema.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -import * as Yup from 'yup'; -import intl from 'react-intl-universal'; -import { DATATYPES_LENGTH } from '@/constants/dataTypes'; - -const Schema = Yup.object().shape({ - exchange_rate: Yup.number() - .required() - .label(intl.get('exchange_rate_')), - currency_code: Yup.string() - .max(3) - .required(intl.get('currency_code_')), - date: Yup.date() - .required() - .label(intl.get('date')), -}); - -export const CreateExchangeRateFormSchema = Schema; -export const EditExchangeRateFormSchema = Schema; diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.tsx deleted file mode 100644 index b0cb405184..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateForm.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-nocheck -import React, { useMemo } from 'react'; -import intl from 'react-intl-universal'; -import moment from 'moment'; -import { Intent } from '@blueprintjs/core'; -import { Formik } from 'formik'; -import { AppToaster } from '@/components'; -import { - CreateExchangeRateFormSchema, - EditExchangeRateFormSchema, -} from './ExchangeRateForm.schema'; -import ExchangeRateFormContent from './ExchangeRateFormContent'; -import { useExchangeRateFromContext } from './ExchangeRateFormProvider'; -import withDialogActions from '@/containers/Dialog/withDialogActions'; - -import { compose, transformToForm } from '@/utils'; - -const defaultInitialValues = { - exchange_rate: '', - currency_code: '', - date: moment(new Date()).format('YYYY-MM-DD'), -}; - -/** - * Exchange rate form. - */ -function ExchangeRateForm({ - // #withDialogActions - closeDialog, -}) { - const { - createExchangeRateMutate, - editExchangeRateMutate, - isNewMode, - dialogName, - exchangeRate, - } = useExchangeRateFromContext(); - - // Form validation schema in create and edit mode. - const validationSchema = isNewMode - ? CreateExchangeRateFormSchema - : EditExchangeRateFormSchema; - const initialValues = useMemo( - () => ({ - ...defaultInitialValues, - ...transformToForm(exchangeRate, defaultInitialValues), - }), - [], - ); - - // Transformers response errors. - const transformErrors = (errors, { setErrors }) => { - if ( - errors.find((error) => error.type === 'EXCHANGE.RATE.DATE.PERIOD.DEFINED') - ) { - setErrors({ - exchange_rate: intl.get( - 'there_is_exchange_rate_in_this_date_with_the_same_currency', - ), - }); - } - }; - - // Handle the form submit. - const handleFormSubmit = (values, { setSubmitting, setErrors }) => { - setSubmitting(true); - - // Handle close the dialog after success response. - const afterSubmit = () => { - closeDialog(dialogName); - }; - const onSuccess = ({ response }) => { - AppToaster.show({ - message: intl.get( - !isNewMode - ? 'the_exchange_rate_has_been_edited_successfully' - : 'the_exchange_rate_has_been_created_successfully', - ), - intent: Intent.SUCCESS, - }); - afterSubmit(response); - }; - // Handle the response error. - const onError = (error) => { - const { - response: { - data: { errors }, - }, - } = error; - - transformErrors(errors, { setErrors }); - setSubmitting(false); - }; - if (isNewMode) { - createExchangeRateMutate(values).then(onSuccess).catch(onError); - } else { - editExchangeRateMutate([exchangeRate.id, values]) - .then(onSuccess) - .catch(onError); - } - }; - - return ( - - - - ); -} - -export default compose(withDialogActions)(ExchangeRateForm); diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormContent.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormContent.tsx deleted file mode 100644 index 07ecc1ebc8..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormContent.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import React from 'react'; -import { Form } from 'formik'; -import ExchangeRateFormFields from './ExchangeRateFormFields'; -import ExchangeRateFormFooter from './ExchangeRateFormFooter'; - -export default function ExchangeRateFormContent() { - return ( -
- - - - ); -} diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.tsx deleted file mode 100644 index 2cad6e0c38..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-nocheck -import React from 'react'; - -import ExchangeRateForm from './ExchangeRateForm'; -import { ExchangeRateFormProvider } from './ExchangeRateFormProvider'; - -import '@/style/pages/ExchangeRate/ExchangeRateDialog.scss'; - -/** - * Exchange rate form content. - */ -export default function ExchangeRateFormDialogContent({ - // #ownProp - action, - exchangeRateId, - dialogName, -}) { - return ( - - - - ); -} diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFields.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFields.tsx deleted file mode 100644 index 58c3eb262b..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFields.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-nocheck -import React from 'react'; -import { Classes, FormGroup, InputGroup, Position } from '@blueprintjs/core'; -import { FastField } from 'formik'; -import { DateInput } from '@blueprintjs/datetime'; -import { FormattedMessage as T } from '@/components'; -import classNames from 'classnames'; -import { - momentFormatter, - tansformDateValue, - handleDateChange, - inputIntent, -} from '@/utils'; -import { - ErrorMessage, - FieldRequiredHint, - CurrencySelectList, -} from '@/components'; -import { useExchangeRateFromContext } from './ExchangeRateFormProvider'; - - -export default function ExchangeRateFormFields() { - const { action, currencies } = useExchangeRateFromContext(); - - return ( -
- {/* ----------- Date ----------- */} - - {({ form, field: { value }, meta: { error, touched } }) => ( - } - labelInfo={FieldRequiredHint} - className={classNames('form-group--select-list', Classes.FILL)} - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > - { - form.setFieldValue('date', formattedDate); - })} - popoverProps={{ position: Position.BOTTOM, minimal: true }} - disabled={action === 'edit'} - /> - - )} - - {/* ----------- Currency Code ----------- */} - - {({ form, field: { value }, meta: { error, touched } }) => ( - } - labelInfo={} - className={classNames('form-group--currency', Classes.FILL)} - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > - { - form.setFieldValue('currency_code', currency_code); - }} - disabled={action === 'edit'} - /> - - )} - - - {/*------------ Exchange Rate -----------*/} - - {({ form, field, meta: { error, touched } }) => ( - } - labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > - - - )} - -
- ); -} diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFooter.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFooter.tsx deleted file mode 100644 index ef66f7674d..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormFooter.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -import React from 'react'; -import { useFormikContext } from 'formik'; - -import { Button, Classes, Intent } from '@blueprintjs/core'; -import { FormattedMessage as T } from '@/components'; -import { useExchangeRateFromContext } from './ExchangeRateFormProvider'; -import withDialogActions from '@/containers/Dialog/withDialogActions'; -import { compose } from '@/utils'; - -function ExchangeRateFormFooter({ - // #withDialogActions - closeDialog, -}) { - const { isSubmitting } = useFormikContext(); - const { dialogName, action } = useExchangeRateFromContext(); - - const handleClose = () => { - closeDialog(dialogName); - }; - - return ( -
-
- - -
-
- ); -} - -export default compose(withDialogActions)(ExchangeRateFormFooter); diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormProvider.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormProvider.tsx deleted file mode 100644 index 90bdf8e968..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -import React, { createContext, useContext } from 'react'; -import { - useCreateExchangeRate, - useEdiExchangeRate, - useCurrencies, - useExchangeRates, -} from '@/hooks/query'; -import { DialogContent } from '@/components'; - -const ExchangeRateFormContext = createContext(); - -/** - * Exchange rate Form page provider. - */ -function ExchangeRateFormProvider({ - exchangeRate, - action, - dialogName, - ...props -}) { - // Create and edit exchange rate mutations. - const { mutateAsync: createExchangeRateMutate } = useCreateExchangeRate(); - const { mutateAsync: editExchangeRateMutate } = useEdiExchangeRate(); - - // Load Currencies list. - const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies(); - const { isFetching: isExchangeRatesLoading } = useExchangeRates(); - - const isNewMode = !exchangeRate; - - // Provider state. - const provider = { - createExchangeRateMutate, - editExchangeRateMutate, - dialogName, - exchangeRate, - action, - currencies, - isExchangeRatesLoading, - isNewMode, - }; - - return ( - - - - ); -} - -const useExchangeRateFromContext = () => useContext(ExchangeRateFormContext); - -export { ExchangeRateFormProvider, useExchangeRateFromContext }; diff --git a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/index.tsx b/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/index.tsx deleted file mode 100644 index 3bbc719545..0000000000 --- a/packages/webapp/src/containers/Dialogs/ExchangeRateFormDialog/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-nocheck -import React, { lazy } from 'react'; -import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components'; -import withDialogRedux from '@/components/DialogReduxConnect'; -import { compose } from '@/utils'; - -const ExchangeRateFormDialogContent = lazy( - () => import('./ExchangeRateFormDialogContent'), -); - -/** - * Exchange rate form dialog. - */ -function ExchangeRateFormDialog({ - dialogName, - payload = { action: '', id: null, exchangeRate: '' }, - isOpen, -}) { - return ( - - ) : ( - - ) - } - className={'dialog--exchangeRate-form'} - isOpen={isOpen} - autoFocus={true} - canEscapeKeyClose={true} - > - - - - - ); -} - -export default compose(withDialogRedux())(ExchangeRateFormDialog); diff --git a/packages/webapp/src/containers/ExchangeRates/ExchangeRateActionsBar.tsx b/packages/webapp/src/containers/ExchangeRates/ExchangeRateActionsBar.tsx deleted file mode 100644 index e2fadd8b4e..0000000000 --- a/packages/webapp/src/containers/ExchangeRates/ExchangeRateActionsBar.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// @ts-nocheck -import React, { useCallback, useState, useMemo } from 'react'; -import intl from 'react-intl-universal'; -import classNames from 'classnames'; -import { - NavbarGroup, - NavbarDivider, - Button, - Classes, - Intent, - Popover, - Position, - PopoverInteractionKind, - Alignment, -} from '@blueprintjs/core'; -import { - Icon, - If, - DashboardActionsBar, - FormattedMessage as T, -} from '@/components'; -import { connect } from 'react-redux'; - -import { useRefreshExchangeRate } from '@/hooks/query/exchangeRates'; -import withDialogActions from '@/containers/Dialog/withDialogActions'; -import withResourceDetail from '@/containers/Resources/withResourceDetails'; -import withExchangeRatesActions from './withExchangeRatesActions'; -import { compose } from '@/utils'; - -/** - * Exchange rate actions bar. - */ -function ExchangeRateActionsBar({ - // #withDialogActions. - openDialog, - - // #withResourceDetail - resourceFields, - - //#withExchangeRatesActions - addExchangeRatesTableQueries, - - // #ownProps - selectedRows = [], - onDeleteExchangeRate, - onFilterChanged, - onBulkDelete, -}) { - const [filterCount, setFilterCount] = useState(0); - - const onClickNewExchangeRate = () => { - openDialog('exchangeRate-form', {}); - }; - - // Exchange rates refresh action. - const { refresh } = useRefreshExchangeRate(); - - // Handle click a refresh sale estimates - const handleRefreshBtnClick = () => { - refresh(); - }; - - const hasSelectedRows = useMemo( - () => selectedRows.length > 0, - [selectedRows], - ); - - const handelBulkDelete = useCallback(() => { - onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id)); - }, [onBulkDelete, selectedRows]); - - return ( - - - + + + + ); +} + +export default compose( + withDialogRedux(), + withDialogActions, +)(InvoiceExchangeRateChangeDialog); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/index.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/index.ts new file mode 100644 index 0000000000..cda7f24dcd --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/index.ts @@ -0,0 +1,16 @@ +// @ts-nocheck +import { DialogsName } from '@/constants/dialogs'; +import React from 'react'; + +const InvoiceExchangeRateChangeAlert = React.lazy( + () => import('./InvoiceExchangeRateChangeDialog'), +); + +const Dialogs = [ + { + name: DialogsName.InvoiceExchangeRateChangeNotice, + component: InvoiceExchangeRateChangeAlert, + }, +]; + +export default Dialogs; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.tsx index a8463619b8..7a975551d0 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.tsx @@ -34,7 +34,7 @@ import { transformValueToRequest, resetFormState, } from './utils'; -import { InvoiceNoSyncSettingsToForm } from './components'; +import { InvoiceExchangeRateSync, InvoiceNoSyncSettingsToForm } from './components'; /** * Invoice form. @@ -180,6 +180,7 @@ function InvoiceForm({ {/*---------- Effects ----------*/} + diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx index 8697e52bc5..51ba306fab 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx @@ -23,7 +23,10 @@ import { handleDateChange, } from '@/utils'; import { CLASSES } from '@/constants/classes'; -import { customerNameFieldShouldUpdate } from './utils'; +import { + customerNameFieldShouldUpdate, + useInvoiceEntriesOnExchangeRateChange, +} from './utils'; import { useInvoiceFormContext } from './InvoiceFormProvider'; import { @@ -36,6 +39,7 @@ import { ProjectBillableEntriesLink, } from '@/containers/Projects/components'; import { Features } from '@/constants'; +import { useCurrentOrganization } from '@/hooks/state'; /** * Invoice form header fields. @@ -161,8 +165,29 @@ export default function InvoiceFormHeaderFields() { * @returns {React.ReactNode} */ function InvoiceFormCustomerSelect() { - const { customers } = useInvoiceFormContext(); const { values, setFieldValue } = useFormikContext(); + const { customers, setAutoExRateCurrency } = useInvoiceFormContext(); + const currentComapny = useCurrentOrganization(); + const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange(); + + // 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); + } + }; return ( } - onItemChange={(customer) => { - setFieldValue('customer_id', customer.id); - setFieldValue('currency_code', customer?.currency_code); - }} + onItemChange={handleItemChange} allowCreate={true} fastField={true} shouldUpdate={customerNameFieldShouldUpdate} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx index fd1b3b0b2b..1969abd611 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx @@ -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 { useFeatureCan } from '@/hooks/state'; +import { useCurrentOrganization, useFeatureCan } from '@/hooks/state'; import { DashboardInsider } from '@/components/Dashboard'; import { transformToEditForm, ITEMS_FILTER_ROLES_QUERY } from './utils'; import { @@ -16,6 +16,7 @@ import { useEditInvoice, useSettingsInvoices, useEstimate, + useExchangeRate, } from '@/hooks/query'; import { useProjects } from '@/containers/Projects/hooks'; import { useTaxRates } from '@/hooks/query/taxRates'; @@ -93,6 +94,18 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) { // Handle fetching settings. const { isLoading: isSettingsLoading } = useSettingsInvoices(); + const [autoExRateCurrency, setAutoExRateCurrency] = useState(''); + 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(); @@ -119,6 +132,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) { warehouses, projects, taxRates, + autoExchangeRate, isInvoiceLoading, isItemsLoading, @@ -135,6 +149,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) { editInvoiceMutate, setSubmitPayload, isNewMode, + + autoExRateCurrency, + setAutoExRateCurrency, + isAutoExchangeRateLoading, }; return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx index 0020a7e8d2..e25b389d7d 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx @@ -1,23 +1,53 @@ // @ts-nocheck -import React from 'react'; +import { useEffect, 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 { useInvoiceIsForeignCustomer } from './utils'; +import { + useInvoiceEntriesOnExchangeRateChange, + 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 ( + { + setFieldValue( + 'entries', + composeChangeExRate(oldExchangeRate, exchangeRate), + ); + }} + {...props} + /> + ); +}; /** * Invoice exchange rate input field. * @returns {JSX.Element} */ -export function InvoiceExchangeRateInputField({ ...props }) { +const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => { const currentOrganization = useCurrentOrganization(); const { values } = useFormikContext(); + const { isAutoExchangeRateLoading } = useInvoiceFormContext(); const isForeignCustomer = useInvoiceIsForeignCustomer(); @@ -27,12 +57,22 @@ export function InvoiceExchangeRateInputField({ ...props }) { } return ( ); -} +}; + +/** + * Invoice exchange rate input field. + * @returns {JSX.Element} + */ +export const InvoiceExchangeRateInputField = R.compose( + withExchangeRateItemEntriesPriceRecalc, +)(InvoiceExchangeRateInputFieldRoot); /** * Invoice project select. @@ -66,3 +106,42 @@ export const InvoiceNoSyncSettingsToForm = R.compose( return null; }); + +/** + * Syncs the fetched real-time exchange rate to the form. + * @returns {JSX.Element} + */ +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, + ), + ); + // 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); + }, 500); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoExchangeRate?.exchange_rate, autoExRateCurrency]); + + return null; + }, +); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index 4186cbc63d..b059a48fa1 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -5,7 +5,7 @@ import intl from 'react-intl-universal'; import moment from 'moment'; import * as R from 'ramda'; import { Intent } from '@blueprintjs/core'; -import { omit, first, sumBy } from 'lodash'; +import { omit, first, sumBy, round } from 'lodash'; import { compose, transformToForm, @@ -57,7 +57,7 @@ export const defaultInvoice = { reference_no: '', invoice_message: '', terms_conditions: '', - exchange_rate: 1, + exchange_rate: '1', currency_code: '', branch_id: '', warehouse_id: '', @@ -398,3 +398,85 @@ 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]); +}; diff --git a/packages/webapp/src/hooks/query/exchangeRates.tsx b/packages/webapp/src/hooks/query/exchangeRates.tsx index f38b66737f..b861d7c7d0 100644 --- a/packages/webapp/src/hooks/query/exchangeRates.tsx +++ b/packages/webapp/src/hooks/query/exchangeRates.tsx @@ -1,102 +1,29 @@ // @ts-nocheck -import { useMutation, useQueryClient } from 'react-query'; -import { defaultTo } from 'lodash'; -import { useQueryTenant } from '../useQueryRequest'; -import { transformPagination } from '@/utils'; -import useApiRequest from '../useRequest'; +import { useQuery } from 'react-query'; +import QUERY_TYPES from './types'; -const defaultPagination = { - pageSize: 20, - page: 0, - pagesCount: 0, -}; -/** - * Creates a new exchange rate. - */ -export function useCreateExchangeRate(props) { - const queryClient = useQueryClient(); - const apiRequest = useApiRequest(); - - return useMutation((values) => apiRequest.post('exchange_rates', values), { - onSuccess: () => { - queryClient.invalidateQueries('EXCHANGES_RATES'); - }, - ...props, - }); -} - -/** - * Edits the exchange rate. - */ -export function useEdiExchangeRate(props) { - const queryClient = useQueryClient(); - const apiRequest = useApiRequest(); - - return useMutation( - ([id, values]) => apiRequest.post(`exchange_rates/${id}`, values), - { - onSuccess: () => { - queryClient.invalidateQueries('EXCHANGES_RATES'); - }, - ...props, - }, - ); +function getRandomItemFromArray(arr) { + const randomIndex = Math.floor(Math.random() * arr.length); + return arr[randomIndex]; } /** - * Deletes the exchange rate. + * Retrieves tax rates. + * @param {number} customerId - Customer id. */ -export function useDeleteExchangeRate(props) { - const queryClient = useQueryClient(); - const apiRequest = useApiRequest(); - - return useMutation((id) => apiRequest.delete(`exchange_rates/${id}`), { - onSuccess: () => { - queryClient.invalidateQueries('EXCHANGES_RATES'); - }, - ...props, - }); -} - -/** - * Retrieve the exchange rate list. - */ -export function useExchangeRates(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( - ['EXCHANGES_RATES', query], - () => apiRequest.get('exchange_rates', { params: query }), - { - select: (res) => ({ - exchangesRates: res.data.exchange_rates.results, - pagination: transformPagination(res.data.exchange_rates.pagination), - filterMeta: res.data.filter_meta, +export function useExchangeRate( + fromCurrency: string, + toCurrency: string, + props, +) { + return useQuery( + [QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency], + () => + Promise.resolve({ + from_currency: fromCurrency, + to_currency: toCurrency, + exchange_rate: getRandomItemFromArray([4.231, 2.231]), }), - ...props, - }, + props, ); - - return { - ...states, - data: defaultTo(states.data, { - exchangesRates: [], - pagination: { - page: 1, - pageSize: 20, - total: 0, - }, - filterMeta: {}, - }), - }; -} - -export function useRefreshExchangeRate() { - const queryClient = useQueryClient(); - - return { - refresh: () => { - queryClient.invalidateQueries('EXCHANGES_RATES'); - }, - }; } diff --git a/packages/webapp/src/hooks/query/types.tsx b/packages/webapp/src/hooks/query/types.tsx index c0173cab75..fbce84ae3f 100644 --- a/packages/webapp/src/hooks/query/types.tsx +++ b/packages/webapp/src/hooks/query/types.tsx @@ -32,7 +32,7 @@ const FINANCIAL_REPORTS = { REALIZED_GAIN_OR_LOSS: 'REALIZED_GAIN_OR_LOSS', UNREALIZED_GAIN_OR_LOSS: 'UNREALIZED_GAIN_OR_LOSS', PROJECT_PROFITABILITY_SUMMARY: 'PROJECT_PROFITABILITY_SUMMARY', - SALES_TAX_LIABILITY_SUMMARY: 'SALES_TAX_LIABILITY_SUMMARY' + SALES_TAX_LIABILITY_SUMMARY: 'SALES_TAX_LIABILITY_SUMMARY', }; const BILLS = { @@ -222,12 +222,17 @@ const DASHBOARD = { }; const ORGANIZATION = { - ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES: 'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES', + ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES: + 'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES', }; export const TAX_RATES = { TAX_RATES: 'TAX_RATES', -} +}; + +export const EXCHANGE_RATE = { + EXCHANGE_RATE: 'EXCHANGE_RATE', +}; export default { ...Authentication, @@ -262,5 +267,6 @@ export default { ...BRANCHES, ...DASHBOARD, ...ORGANIZATION, - ...TAX_RATES + ...TAX_RATES, + ...EXCHANGE_RATE, }; diff --git a/packages/webapp/src/routes/dashboard.tsx b/packages/webapp/src/routes/dashboard.tsx index 5193c63f85..c1137bc5b8 100644 --- a/packages/webapp/src/routes/dashboard.tsx +++ b/packages/webapp/src/routes/dashboard.tsx @@ -473,16 +473,6 @@ export const getDashboardRoutes = () => [ pageTitle: intl.get('all_financial_reports'), subscriptionActive: [SUBSCRIPTION_TYPE.MAIN], }, - // Exchange Rates - // { - // path: `/exchange-rates`, - // component: lazy( - // () => import('@/containers/ExchangeRates/ExchangeRatesList'), - // ), - // breadcrumb: intl.get('exchange_rates_list'), - // pageTitle: intl.get('exchange_rates_list'), - // subscriptionActive: [SUBSCRIPTION_TYPE.MAIN], - // }, // Expenses. { path: `/expenses/new`, From 2b03ac7f161f46b6b39cf81a32a916a8fa0eeec7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 14 Jan 2024 14:44:48 +0200 Subject: [PATCH 2/3] feat: implement auto entries rates re-calculation after change the exchange rate --- .../ExchangeRate/ExchangeRateInput.tsx | 7 +- .../Entries/AutoExchangeProvider.tsx | 52 ++++++++ .../useUpdateEntriesOnExchangeRateChange.ts | 88 +++++++++++++ .../withExRateItemEntriesPriceRecalc.tsx | 121 ++++++++++++++++++ .../CreditNoteForm/CreditNoteForm.tsx | 6 +- .../CreditNoteFormHeaderFields.tsx | 24 ++-- .../CreditNoteForm/CreditNoteFormPage.tsx | 5 +- .../CreditNotes/CreditNoteForm/components.tsx | 49 ++++++- .../Estimates/EstimateForm/EstimateForm.tsx | 7 +- .../EstimateForm/EstimateFormHeaderFields.tsx | 26 ++-- .../EstimateForm/EstimateFormPage.tsx | 5 +- .../Estimates/EstimateForm/components.tsx | 58 ++++++++- .../InvoiceExchangeRateChangeDialog.tsx | 7 +- .../InvoiceForm/InvoiceFloatingActions.tsx | 1 + .../InvoiceForm/InvoiceFormHeaderFields.tsx | 30 ++--- .../Invoices/InvoiceForm/InvoiceFormPage.tsx | 5 +- .../InvoiceForm/InvoiceFormProvider.tsx | 20 +-- .../Sales/Invoices/InvoiceForm/components.tsx | 72 +++-------- .../Sales/Invoices/InvoiceForm/utils.tsx | 82 ------------ .../Receipts/ReceiptForm/ReceiptForm.tsx | 3 +- .../ReceiptForm/ReceiptFormHeaderFields.tsx | 21 +-- .../Receipts/ReceiptForm/ReceiptFormPage.tsx | 5 +- .../Sales/Receipts/ReceiptForm/components.tsx | 54 +++++++- .../webapp/src/hooks/query/exchangeRates.tsx | 15 ++- 24 files changed, 522 insertions(+), 241 deletions(-) create mode 100644 packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx create mode 100644 packages/webapp/src/containers/Entries/useUpdateEntriesOnExchangeRateChange.ts create mode 100644 packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx diff --git a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx index 6f5bcdcec5..caf2f3d58e 100644 --- a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx +++ b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx @@ -28,7 +28,7 @@ interface ExchangeRateInputGroupProps { inputGroupProps?: any; formGroupProps?: any; - popoverRecalcConfirm?: boolean; + withPopoverRecalcConfirm?: boolean; onRecalcConfirm: (bag: ExchangeRateValuesBag) => void; onCancel: (bag: ExchangeRateValuesBag) => void; @@ -47,7 +47,7 @@ export function ExchangeRateInputGroup({ inputGroupProps, formGroupProps, - popoverRecalcConfirm = false, + withPopoverRecalcConfirm = false, onRecalcConfirm, onCancel, @@ -97,6 +97,7 @@ export function ExchangeRateInputGroup({ onChange={() => null} onBlur={handleExchangeRateFieldBlur} rightElement={isLoading && } + decimalsLimit={5} {...inputGroupProps} name={name} /> @@ -142,7 +143,7 @@ export function ExchangeRateInputGroup({ 1 {fromCurrency} = - {popoverRecalcConfirm ? ( + {withPopoverRecalcConfirm ? ( {exchangeRateField} diff --git a/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx b/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx new file mode 100644 index 0000000000..6554b85a15 --- /dev/null +++ b/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx @@ -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(''); + 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 ( + + {children} + + ); +} + +const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext); + +export { + useAutoExRateContext, + AutoExchangeRateContext, + AutoExchangeRateProvider, +}; diff --git a/packages/webapp/src/containers/Entries/useUpdateEntriesOnExchangeRateChange.ts b/packages/webapp/src/containers/Entries/useUpdateEntriesOnExchangeRateChange.ts new file mode 100644 index 0000000000..a4e42877c5 --- /dev/null +++ b/packages/webapp/src/containers/Entries/useUpdateEntriesOnExchangeRateChange.ts @@ -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]); +}; diff --git a/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx b/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx new file mode 100644 index 0000000000..490ef20b5e --- /dev/null +++ b/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx @@ -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 ( + { + 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 ( + + ); +}; + +/** + * 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; +}; diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.tsx index 1d51ecbc6e..a52afcac4a 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.tsx @@ -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 --------*/} + diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormHeaderFields.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormHeaderFields.tsx index cd2fc1d1a5..3c89f2f049 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormHeaderFields.tsx @@ -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({}) { {/* ----------- Exchange rate ----------- */} - + + {/* ----------- 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 ( } - onItemChange={(customer) => { - setFieldValue('customer_id', customer.id); - setFieldValue('currency_code', customer?.currency_code); - }} + onItemChange={handleItemChange} popoverFill={true} allowCreate={true} fastField={true} diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormPage.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormPage.tsx index bb4c7a2cde..872c2f053e 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormPage.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteFormPage.tsx @@ -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 ( - + + + ); } diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/components.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/components.tsx index 2902299fac..afe139f486 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/components.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteForm/components.tsx @@ -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 ( ); } +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; + }, +); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateForm.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateForm.tsx index f9cd246736..aff0888d65 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateForm.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateForm.tsx @@ -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 -------*/} + diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.tsx index e3bfabb9a0..eba6eb5dab 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.tsx @@ -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 (
@@ -45,10 +44,8 @@ export default function EstimateFormHeader() { {/* ----------- Exchange Rate ----------- */} - + + {/* ----------- 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 ( } @@ -165,10 +172,7 @@ function EstimateFormCustomerSelect() { name={'customer_id'} items={customers} placeholder={} - onItemChange={(customer) => { - setFieldValue('customer_id', customer.id); - setFieldValue('currency_code', customer?.currency_code); - }} + onItemChange={handleItemChange} popoverFill={true} allowCreate={true} fastField={true} diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormPage.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormPage.tsx index 6f02669db7..0ca5d4e372 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormPage.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/EstimateFormPage.tsx @@ -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 ( - + + + ); } diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/components.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/components.tsx index 65a1c9cd24..3cace61f27 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateForm/components.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateForm/components.tsx @@ -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 ( ); } +/** + * 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; + }, +); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx index 00a86b3260..50771d9137 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx @@ -22,7 +22,6 @@ function InvoiceExchangeRateChangeDialog({ return (

- You have changed customers's currency after adding items to the + You have changed customer's currency after adding items to the Invoice.

@@ -41,14 +40,14 @@ function InvoiceExchangeRateChangeDialog({ rate feeds.

-

+

Before saving the transaction, ensure that the item rates align with the current exchange rate of the newly selected currency.

-
diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFloatingActions.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFloatingActions.tsx index 3de6999793..e212c8b7d2 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFloatingActions.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFloatingActions.tsx @@ -70,6 +70,7 @@ export default function InvoiceFloatingActions() { history.goBack(); }; + // Handle clear button click. const handleClearBtnClick = (event) => { resetForm(); }; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx index 51ba306fab..17bd197e5f 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.tsx @@ -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() { {/* ----------- Exchange rate ----------- */} - + + {/* ----------- 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 ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormPage.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormPage.tsx index eba22308e9..42190b42a7 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormPage.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormPage.tsx @@ -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 ( - + + + ); } diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx index 1969abd611..fd1b3b0b2b 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx @@ -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(''); - 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 ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx index e25b389d7d..788cd2c5c7 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/components.tsx @@ -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 ( - { - 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; }, ); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index f77bfce9df..8c32f017ce 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -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]); -}; diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptForm.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptForm.tsx index ca7dd26f6c..62206bdbf1 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptForm.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptForm.tsx @@ -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 ---------*/} +
diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.tsx index 92ef297cb7..1536c97851 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.tsx @@ -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() { {/* ----------- Exchange rate ----------- */} - + {/* ----------- Deposit account ----------- */} { + setFieldValue('customer_id', customer.id); + setFieldValue('currency_code', customer?.currency_code); + + updateEntries(customer); + }; + return ( } - onItemChange={(customer) => { - setFieldValue('customer_id', customer.id); - setFieldValue('currency_code', customer?.currency_code); - }} + onItemChange={handleItemChange} popoverFill={true} allowCreate={true} fastField={true} diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormPage.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormPage.tsx index dddd093ab5..da66da72b5 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormPage.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormPage.tsx @@ -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 ( - + + + ); } diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/components.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/components.tsx index 937a4f937c..7d47998e33 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/components.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptForm/components.tsx @@ -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 ( ); } +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; + }, +); diff --git a/packages/webapp/src/hooks/query/exchangeRates.tsx b/packages/webapp/src/hooks/query/exchangeRates.tsx index b861d7c7d0..f569580405 100644 --- a/packages/webapp/src/hooks/query/exchangeRates.tsx +++ b/packages/webapp/src/hooks/query/exchangeRates.tsx @@ -6,7 +6,9 @@ function getRandomItemFromArray(arr) { const randomIndex = Math.floor(Math.random() * arr.length); return arr[randomIndex]; } - +function delay(t, val) { + return new Promise((resolve) => setTimeout(resolve, t, val)); +} /** * Retrieves tax rates. * @param {number} customerId - Customer id. @@ -18,12 +20,15 @@ export function useExchangeRate( ) { return useQuery( [QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency], - () => - Promise.resolve({ + async () => { + await delay(100); + + return { from_currency: fromCurrency, to_currency: toCurrency, - exchange_rate: getRandomItemFromArray([4.231, 2.231]), - }), + exchange_rate: 1.00, + }; + }, props, ); } From 1e4b29f83c8caba2ce6aec7f7fe8dd6e5d21796c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 14 Jan 2024 15:59:25 +0200 Subject: [PATCH 3/3] fix: content tweaks in rates re-calc popover --- .../ExchangeRate/ExchangeRateInput.tsx | 24 +++++++++++-------- .../InvoiceExchangeRateChangeDialog.tsx | 16 ++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx index caf2f3d58e..d96d91231a 100644 --- a/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx +++ b/packages/webapp/src/components/ExchangeRate/ExchangeRateInput.tsx @@ -8,6 +8,7 @@ import { ControlGroup, Intent, Popover, + Position, Spinner, } from '@blueprintjs/core'; import { FlagIcon } from '../Tags'; @@ -106,31 +107,30 @@ export function ExchangeRateInputGroup({ const popoverConfirmContent = (

- Are you want to re-calculate item prices based on this exchange rate + Are you want to re-calculate item prices based on this exchange rate.

@@ -144,7 +144,11 @@ export function ExchangeRateInputGroup({ {withPopoverRecalcConfirm ? ( - + {exchangeRateField} ) : ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx index 50771d9137..2880ac383a 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog.tsx @@ -10,9 +10,7 @@ import { Button, Classes, Intent } from '@blueprintjs/core'; */ function InvoiceExchangeRateChangeDialog({ dialogName, - payload: { initialFormValues }, isOpen, - onConfirm, // #withDialogActions closeDialog, }) { @@ -23,6 +21,7 @@ function InvoiceExchangeRateChangeDialog({ return (

- You have changed customer's currency after adding items to the - Invoice. -

- -

- The item rates have been adjusted to the new currency using exchange - rate feeds. + The item rates have been adjusted to the new + currency using realtime exchange rate.

- Before saving the transaction, ensure that the item rates align with - the current exchange rate of the newly selected currency. + Make sure to check that the item rates match the current exchange + rate of the newly selected currency before saving the transaction.