mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: implement auto entries rates re-calculation after change the exchange rate
This commit is contained in:
@@ -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 && <Spinner size={16} />}
|
||||
decimalsLimit={5}
|
||||
{...inputGroupProps}
|
||||
name={name}
|
||||
/>
|
||||
@@ -142,7 +143,7 @@ export function ExchangeRateInputGroup({
|
||||
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
|
||||
</ExchangeRatePrepend>
|
||||
|
||||
{popoverRecalcConfirm ? (
|
||||
{withPopoverRecalcConfirm ? (
|
||||
<Popover isOpen={isOpen} content={popoverConfirmContent}>
|
||||
{exchangeRateField}
|
||||
</Popover>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useExchangeRate } from '@/hooks/query';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import React from 'react';
|
||||
|
||||
interface AutoExchangeRateProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AutoExchangeRateProviderValue {
|
||||
autoExRateCurrency: string;
|
||||
isAutoExchangeRateLoading: boolean;
|
||||
}
|
||||
|
||||
const AutoExchangeRateContext = React.createContext(
|
||||
{} as AutoExchangeRateProviderValue,
|
||||
);
|
||||
|
||||
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
|
||||
const [autoExRateCurrency, setAutoExRateCurrency] =
|
||||
React.useState<string>('');
|
||||
const currentOrganization = useCurrentOrganization();
|
||||
|
||||
// Retrieves the exchange rate.
|
||||
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
|
||||
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
|
||||
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 0,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
const value = {
|
||||
autoExRateCurrency,
|
||||
setAutoExRateCurrency,
|
||||
isAutoExchangeRateLoading,
|
||||
autoExchangeRate,
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoExchangeRateContext.Provider value={value}>
|
||||
{children}
|
||||
</AutoExchangeRateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext);
|
||||
|
||||
export {
|
||||
useAutoExRateContext,
|
||||
AutoExchangeRateContext,
|
||||
AutoExchangeRateProvider,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { round } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { updateItemsEntriesTotal } from './utils';
|
||||
|
||||
/**
|
||||
* Convert the given rate to the local currency.
|
||||
* @param {number} rate
|
||||
* @param {number} exchangeRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const convertToForeignCurrency = (
|
||||
rate: number,
|
||||
exchangeRate: number,
|
||||
) => {
|
||||
return rate * exchangeRate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the given rate to the base currency.
|
||||
* @param {number} rate
|
||||
* @param {number} exchangeRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
|
||||
return rate / exchangeRate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given rate from the old exchange rate and covert it to the new
|
||||
* currency based on the given new exchange rate.
|
||||
* @param {number} rate -
|
||||
* @param {number} oldExchangeRate - Old exchange rate.
|
||||
* @param {number} newExchangeRate - New exchange rate.
|
||||
* @returns {number}
|
||||
*/
|
||||
const revertAndConvertExchangeRate = (
|
||||
rate: number,
|
||||
oldExchangeRate: number,
|
||||
newExchangeRate: number,
|
||||
) => {
|
||||
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
|
||||
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
|
||||
|
||||
return round(newValue, 3);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the new item entry rate after converting to the new exchange rate.
|
||||
* @params {number} oldExchangeRate -
|
||||
* @params {number} newExchangeRate -
|
||||
* @params {IItemEntry} entries -
|
||||
*/
|
||||
const assignRateRevertAndCovertExchangeRate = R.curry(
|
||||
(oldExchangeRate: number, newExchangeRate: number, entries: IITemEntry[]) => {
|
||||
return entries.map((entry) => ({
|
||||
...entry,
|
||||
rate: revertAndConvertExchangeRate(
|
||||
entry.rate,
|
||||
oldExchangeRate,
|
||||
newExchangeRate,
|
||||
),
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates items entries on exchange rate change.
|
||||
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
|
||||
*/
|
||||
export const useUpdateEntriesOnExchangeRateChange = () => {
|
||||
const {
|
||||
values: { entries },
|
||||
} = useFormikContext();
|
||||
|
||||
return React.useMemo(() => {
|
||||
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
|
||||
return R.compose(
|
||||
// Updates entries total.
|
||||
updateItemsEntriesTotal,
|
||||
// Assign a new rate of the given new exchange rate from the old exchange rate.
|
||||
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
|
||||
)(entries);
|
||||
});
|
||||
}, [entries]);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
// @ts-nocheck
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useUpdateEntriesOnExchangeRateChange } from './useUpdateEntriesOnExchangeRateChange';
|
||||
import { useAutoExRateContext } from './AutoExchangeProvider';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
|
||||
/**
|
||||
* Re-calculate the item entries prices based on the old exchange rate.
|
||||
* @param {InvoiceExchangeRateInputFieldRoot} Component
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export const withExchangeRateItemEntriesPriceRecalc =
|
||||
(Component) => (props) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const updateChangeExRate = useUpdateEntriesOnExchangeRateChange();
|
||||
|
||||
return (
|
||||
<Component
|
||||
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateChangeExRate(oldExchangeRate, exchangeRate),
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the loading props to the exchange rate field.
|
||||
* @param Component
|
||||
* @returns {}
|
||||
*/
|
||||
export const withExchangeRateFetchingLoading = (Component) => (props) => {
|
||||
const { isAutoExchangeRateLoading } = useAutoExRateContext();
|
||||
|
||||
return (
|
||||
<Component
|
||||
isLoading={isAutoExchangeRateLoading}
|
||||
inputGroupProps={{
|
||||
disabled: isAutoExchangeRateLoading,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the customer currency code and exchange rate once you update the customer
|
||||
* then change the state to fetch the realtime exchange rate of the new selected currency.
|
||||
*/
|
||||
export const useCustomerUpdateExRate = () => {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
const { setAutoExRateCurrency } = useAutoExRateContext();
|
||||
|
||||
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||
const currentCompany = useCurrentOrganization();
|
||||
|
||||
const DEFAULT_EX_RATE = 1;
|
||||
|
||||
return useCallback(
|
||||
(customer) => {
|
||||
// Reset the auto exchange rate currency cycle.
|
||||
setAutoExRateCurrency(null);
|
||||
|
||||
// If the customer's currency code equals the same base currency.
|
||||
if (customer.currency_code === currentCompany.base_currency) {
|
||||
setFieldValue('exchange_rate', DEFAULT_EX_RATE + '');
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateEntriesOnExChange(values.exchange_rate, DEFAULT_EX_RATE),
|
||||
);
|
||||
} else {
|
||||
// Sets the currency code to fetch exchange rate of the given currency code.
|
||||
setAutoExRateCurrency(customer?.currency_code);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentCompany.base_currency,
|
||||
setAutoExRateCurrency,
|
||||
setFieldValue,
|
||||
updateEntriesOnExChange,
|
||||
values.exchange_rate,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
interface UseSyncExRateToFormProps {
|
||||
onSynced?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the realtime exchange rate to the Formik form and then re-calculates
|
||||
* the entries rate based on the given new and old ex. rate.
|
||||
* @param {UseSyncExRateToFormProps} props -
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
|
||||
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||
|
||||
// Sync the fetched real-time exchanage rate to the form.
|
||||
useEffect(() => {
|
||||
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
|
||||
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
|
||||
setFieldValue(
|
||||
'entries',
|
||||
updateEntriesOnExChange(
|
||||
values.exchange_rate,
|
||||
autoExchangeRate?.exchange_rate,
|
||||
),
|
||||
);
|
||||
onSynced?.();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -70,6 +70,7 @@ export default function InvoiceFloatingActions() {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
// Handle clear button click.
|
||||
const handleClearBtnClick = (event) => {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user