mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +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;
|
inputGroupProps?: any;
|
||||||
formGroupProps?: any;
|
formGroupProps?: any;
|
||||||
|
|
||||||
popoverRecalcConfirm?: boolean;
|
withPopoverRecalcConfirm?: boolean;
|
||||||
|
|
||||||
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
|
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
|
||||||
onCancel: (bag: ExchangeRateValuesBag) => void;
|
onCancel: (bag: ExchangeRateValuesBag) => void;
|
||||||
@@ -47,7 +47,7 @@ export function ExchangeRateInputGroup({
|
|||||||
inputGroupProps,
|
inputGroupProps,
|
||||||
formGroupProps,
|
formGroupProps,
|
||||||
|
|
||||||
popoverRecalcConfirm = false,
|
withPopoverRecalcConfirm = false,
|
||||||
|
|
||||||
onRecalcConfirm,
|
onRecalcConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -97,6 +97,7 @@ export function ExchangeRateInputGroup({
|
|||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
onBlur={handleExchangeRateFieldBlur}
|
onBlur={handleExchangeRateFieldBlur}
|
||||||
rightElement={isLoading && <Spinner size={16} />}
|
rightElement={isLoading && <Spinner size={16} />}
|
||||||
|
decimalsLimit={5}
|
||||||
{...inputGroupProps}
|
{...inputGroupProps}
|
||||||
name={name}
|
name={name}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +143,7 @@ export function ExchangeRateInputGroup({
|
|||||||
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
|
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
|
||||||
</ExchangeRatePrepend>
|
</ExchangeRatePrepend>
|
||||||
|
|
||||||
{popoverRecalcConfirm ? (
|
{withPopoverRecalcConfirm ? (
|
||||||
<Popover isOpen={isOpen} content={popoverConfirmContent}>
|
<Popover isOpen={isOpen} content={popoverConfirmContent}>
|
||||||
{exchangeRateField}
|
{exchangeRateField}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -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 withSettings from '@/containers/Settings/withSettings';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
import { CreditNoteSyncIncrementSettingsToForm } from './components';
|
import {
|
||||||
|
CreditNoteExchangeRateSync,
|
||||||
|
CreditNoteSyncIncrementSettingsToForm,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credit note form.
|
* Credit note form.
|
||||||
@@ -169,6 +172,7 @@ function CreditNoteForm({
|
|||||||
|
|
||||||
{/*-------- Effects --------*/}
|
{/*-------- Effects --------*/}
|
||||||
<CreditNoteSyncIncrementSettingsToForm />
|
<CreditNoteSyncIncrementSettingsToForm />
|
||||||
|
<CreditNoteExchangeRateSync />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
inputIntent,
|
inputIntent,
|
||||||
handleDateChange,
|
handleDateChange,
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
|
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credit note form header fields.
|
* Credit note form header fields.
|
||||||
@@ -37,10 +38,8 @@ export default function CreditNoteFormHeaderFields({}) {
|
|||||||
<CreditNoteCustomersSelect />
|
<CreditNoteCustomersSelect />
|
||||||
|
|
||||||
{/* ----------- Exchange rate ----------- */}
|
{/* ----------- Exchange rate ----------- */}
|
||||||
<CreditNoteExchangeRateInputField
|
<CreditNoteExchangeRateInputField />
|
||||||
name={'exchange_rate'}
|
|
||||||
formGroupProps={{ label: ' ', inline: true }}
|
|
||||||
/>
|
|
||||||
{/* ----------- Credit note date ----------- */}
|
{/* ----------- Credit note date ----------- */}
|
||||||
<FastField name={'credit_note_date'}>
|
<FastField name={'credit_note_date'}>
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||||
@@ -93,8 +92,18 @@ export default function CreditNoteFormHeaderFields({}) {
|
|||||||
*/
|
*/
|
||||||
function CreditNoteCustomersSelect() {
|
function CreditNoteCustomersSelect() {
|
||||||
// Credit note form context.
|
// Credit note form context.
|
||||||
const { customers } = useCreditNoteFormContext();
|
|
||||||
const { setFieldValue, values } = useFormikContext();
|
const { setFieldValue, values } = useFormikContext();
|
||||||
|
const { customers } = useCreditNoteFormContext();
|
||||||
|
|
||||||
|
const updateEntries = useCustomerUpdateExRate();
|
||||||
|
|
||||||
|
// Handles item change.
|
||||||
|
const handleItemChange = (customer) => {
|
||||||
|
setFieldValue('customer_id', customer.id);
|
||||||
|
setFieldValue('currency_code', customer?.currency_code);
|
||||||
|
|
||||||
|
updateEntries(customer);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
@@ -110,10 +119,7 @@ function CreditNoteCustomersSelect() {
|
|||||||
name={'customer_id'}
|
name={'customer_id'}
|
||||||
items={customers}
|
items={customers}
|
||||||
placeholder={<T id={'select_customer_account'} />}
|
placeholder={<T id={'select_customer_account'} />}
|
||||||
onItemChange={(customer) => {
|
onItemChange={handleItemChange}
|
||||||
setFieldValue('customer_id', customer.id);
|
|
||||||
setFieldValue('currency_code', customer?.currency_code);
|
|
||||||
}}
|
|
||||||
popoverFill={true}
|
popoverFill={true}
|
||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
fastField={true}
|
fastField={true}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '@/style/pages/CreditNote/PageForm.scss';
|
|||||||
|
|
||||||
import CreditNoteForm from './CreditNoteForm';
|
import CreditNoteForm from './CreditNoteForm';
|
||||||
import { CreditNoteFormProvider } from './CreditNoteFormProvider';
|
import { CreditNoteFormProvider } from './CreditNoteFormProvider';
|
||||||
|
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credit note form page.
|
* Credit note form page.
|
||||||
@@ -16,7 +17,9 @@ export default function CreditNoteFormPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CreditNoteFormProvider creditNoteId={idAsInteger}>
|
<CreditNoteFormProvider creditNoteId={idAsInteger}>
|
||||||
<CreditNoteForm />
|
<AutoExchangeRateProvider>
|
||||||
|
<CreditNoteForm />
|
||||||
|
</AutoExchangeRateProvider>
|
||||||
</CreditNoteFormProvider>
|
</CreditNoteFormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { ExchangeRateInputGroup } from '@/components';
|
import { ExchangeRateInputGroup } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import { useCreditNoteIsForeignCustomer } from './utils';
|
import { useCreditNoteIsForeignCustomer, useCreditNoteTotals } from './utils';
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import { transactionNumber } from '@/utils';
|
import { transactionNumber } from '@/utils';
|
||||||
|
import {
|
||||||
|
useSyncExRateToForm,
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* credit exchange rate input field.
|
* Credit note exchange rate input field.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export function CreditNoteExchangeRateInputField({ ...props }) {
|
function CreditNoteExchangeRateInputFieldRoot({ ...props }) {
|
||||||
const currentOrganization = useCurrentOrganization();
|
const currentOrganization = useCurrentOrganization();
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
const isForeignCustomer = useCreditNoteIsForeignCustomer();
|
const isForeignCustomer = useCreditNoteIsForeignCustomer();
|
||||||
|
|
||||||
// Can't continue if the customer is not foreign.
|
// Can't continue if the customer is not foreign.
|
||||||
@@ -24,13 +30,21 @@ export function CreditNoteExchangeRateInputField({ ...props }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ExchangeRateInputGroup
|
<ExchangeRateInputGroup
|
||||||
|
name={'exchange_rate'}
|
||||||
fromCurrency={values.currency_code}
|
fromCurrency={values.currency_code}
|
||||||
toCurrency={currentOrganization.base_currency}
|
toCurrency={currentOrganization.base_currency}
|
||||||
|
formGroupProps={{ label: ' ', inline: true }}
|
||||||
|
withPopoverRecalcConfirm
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CreditNoteExchangeRateInputField = R.compose(
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
)(CreditNoteExchangeRateInputFieldRoot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs credit note auto-increment settings to form.
|
* Syncs credit note auto-increment settings to form.
|
||||||
* @return {React.ReactNode}
|
* @return {React.ReactNode}
|
||||||
@@ -56,3 +70,28 @@ export const CreditNoteSyncIncrementSettingsToForm = R.compose(
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the realtime exchange rate to the credit note form and shows up popup to the user
|
||||||
|
* as an indication the entries rates have been re-calculated.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export const CreditNoteExchangeRateSync = R.compose(withDialogActions)(
|
||||||
|
({ openDialog }) => {
|
||||||
|
const { total } = useCreditNoteTotals();
|
||||||
|
const timeout = useRef();
|
||||||
|
|
||||||
|
useSyncExRateToForm({
|
||||||
|
onSynced: () => {
|
||||||
|
// If the total bigger then zero show alert to the user after adjusting entries.
|
||||||
|
if (total > 0) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
openDialog(DialogsName.InvoiceExchangeRateChangeNotice);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
@@ -19,7 +18,10 @@ import EstimateFloatingActions from './EstimateFloatingActions';
|
|||||||
import EstimateFormFooter from './EstimateFormFooter';
|
import EstimateFormFooter from './EstimateFormFooter';
|
||||||
import EstimateFormDialogs from './EstimateFormDialogs';
|
import EstimateFormDialogs from './EstimateFormDialogs';
|
||||||
import EstimtaeFormTopBar from './EstimtaeFormTopBar';
|
import EstimtaeFormTopBar from './EstimtaeFormTopBar';
|
||||||
import { EstimateIncrementSyncSettingsToForm } from './components';
|
import {
|
||||||
|
EstimateIncrementSyncSettingsToForm,
|
||||||
|
EstimateSyncAutoExRateToForm,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
@@ -170,6 +172,7 @@ function EstimateForm({
|
|||||||
|
|
||||||
{/*------- Effects -------*/}
|
{/*------- Effects -------*/}
|
||||||
<EstimateIncrementSyncSettingsToForm />
|
<EstimateIncrementSyncSettingsToForm />
|
||||||
|
<EstimateSyncAutoExRateToForm />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormGroup, InputGroup, Position, Classes } from '@blueprintjs/core';
|
import { FormGroup, InputGroup, Position, Classes } from '@blueprintjs/core';
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
import { customersFieldShouldUpdate } from './utils';
|
import { customersFieldShouldUpdate } from './utils';
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
|
|
||||||
import { ProjectsSelect } from '@/containers/Projects/components';
|
import { ProjectsSelect } from '@/containers/Projects/components';
|
||||||
import {
|
import {
|
||||||
EstimateExchangeRateInputField,
|
EstimateExchangeRateInputField,
|
||||||
@@ -32,12 +30,13 @@ import {
|
|||||||
} from './components';
|
} from './components';
|
||||||
import { EstimateFormEstimateNumberField } from './EstimateFormEstimateNumberField';
|
import { EstimateFormEstimateNumberField } from './EstimateFormEstimateNumberField';
|
||||||
import { useEstimateFormContext } from './EstimateFormProvider';
|
import { useEstimateFormContext } from './EstimateFormProvider';
|
||||||
|
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate form header.
|
* Estimate form header.
|
||||||
*/
|
*/
|
||||||
export default function EstimateFormHeader() {
|
export default function EstimateFormHeader() {
|
||||||
const { customers, projects } = useEstimateFormContext();
|
const { projects } = useEstimateFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
||||||
@@ -45,10 +44,8 @@ export default function EstimateFormHeader() {
|
|||||||
<EstimateFormCustomerSelect />
|
<EstimateFormCustomerSelect />
|
||||||
|
|
||||||
{/* ----------- Exchange Rate ----------- */}
|
{/* ----------- Exchange Rate ----------- */}
|
||||||
<EstimateExchangeRateInputField
|
<EstimateExchangeRateInputField />
|
||||||
name={'exchange_rate'}
|
|
||||||
formGroupProps={{ label: ' ', inline: true }}
|
|
||||||
/>
|
|
||||||
{/* ----------- Estimate Date ----------- */}
|
{/* ----------- Estimate Date ----------- */}
|
||||||
<FastField name={'estimate_date'}>
|
<FastField name={'estimate_date'}>
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||||
@@ -151,6 +148,16 @@ function EstimateFormCustomerSelect() {
|
|||||||
const { setFieldValue, values } = useFormikContext();
|
const { setFieldValue, values } = useFormikContext();
|
||||||
const { customers } = useEstimateFormContext();
|
const { customers } = useEstimateFormContext();
|
||||||
|
|
||||||
|
const updateEntries = useCustomerUpdateExRate();
|
||||||
|
|
||||||
|
// Handles the customer item change.
|
||||||
|
const handleItemChange = (customer) => {
|
||||||
|
setFieldValue('customer_id', customer.id);
|
||||||
|
setFieldValue('currency_code', customer?.currency_code);
|
||||||
|
|
||||||
|
updateEntries(customer);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={<T id={'customer_name'} />}
|
label={<T id={'customer_name'} />}
|
||||||
@@ -165,10 +172,7 @@ function EstimateFormCustomerSelect() {
|
|||||||
name={'customer_id'}
|
name={'customer_id'}
|
||||||
items={customers}
|
items={customers}
|
||||||
placeholder={<T id={'select_customer_account'} />}
|
placeholder={<T id={'select_customer_account'} />}
|
||||||
onItemChange={(customer) => {
|
onItemChange={handleItemChange}
|
||||||
setFieldValue('customer_id', customer.id);
|
|
||||||
setFieldValue('currency_code', customer?.currency_code);
|
|
||||||
}}
|
|
||||||
popoverFill={true}
|
popoverFill={true}
|
||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
fastField={true}
|
fastField={true}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '@/style/pages/SaleEstimate/PageForm.scss';
|
|||||||
|
|
||||||
import EstimateForm from './EstimateForm';
|
import EstimateForm from './EstimateForm';
|
||||||
import { EstimateFormProvider } from './EstimateFormProvider';
|
import { EstimateFormProvider } from './EstimateFormProvider';
|
||||||
|
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate form page.
|
* Estimate form page.
|
||||||
@@ -16,7 +17,9 @@ export default function EstimateFormPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EstimateFormProvider estimateId={idInteger}>
|
<EstimateFormProvider estimateId={idInteger}>
|
||||||
<EstimateForm />
|
<AutoExchangeRateProvider>
|
||||||
|
<EstimateForm />
|
||||||
|
</AutoExchangeRateProvider>
|
||||||
</EstimateFormProvider>
|
</EstimateFormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useEffect } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Button } from '@blueprintjs/core';
|
import { Button } from '@blueprintjs/core';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { ExchangeRateInputGroup } from '@/components';
|
import { ExchangeRateInputGroup } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import { useEstimateIsForeignCustomer } from './utils';
|
import { useEstimateIsForeignCustomer, useEstimateTotals } from './utils';
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
|
||||||
import { transactionNumber } from '@/utils';
|
import { transactionNumber } from '@/utils';
|
||||||
import { useUpdateEffect } from '@/hooks';
|
import { useUpdateEffect } from '@/hooks';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
|
import {
|
||||||
|
useSyncExRateToForm,
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate exchange rate input field.
|
* Estimate exchange rate input field.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export function EstimateExchangeRateInputField({ ...props }) {
|
function EstimateExchangeRateInputFieldRoot({ ...props }) {
|
||||||
const currentOrganization = useCurrentOrganization();
|
const currentOrganization = useCurrentOrganization();
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
const isForeignCustomer = useEstimateIsForeignCustomer();
|
const isForeignCustomer = useEstimateIsForeignCustomer();
|
||||||
|
|
||||||
// Can't continue if the customer is not foreign.
|
// Can't continue if the customer is not foreign.
|
||||||
@@ -27,13 +33,26 @@ export function EstimateExchangeRateInputField({ ...props }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ExchangeRateInputGroup
|
<ExchangeRateInputGroup
|
||||||
|
name={'exchange_rate'}
|
||||||
fromCurrency={values.currency_code}
|
fromCurrency={values.currency_code}
|
||||||
toCurrency={currentOrganization.base_currency}
|
toCurrency={currentOrganization.base_currency}
|
||||||
|
formGroupProps={{ label: ' ', inline: true }}
|
||||||
|
withPopoverRecalcConfirm
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the estimate exchange rate input field with exchange rate
|
||||||
|
* with item entries price re-calc once exchange rate change.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export const EstimateExchangeRateInputField = R.compose(
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
)(EstimateExchangeRateInputFieldRoot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate project select.
|
* Estimate project select.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
@@ -72,3 +91,32 @@ export const EstimateIncrementSyncSettingsToForm = R.compose(
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the auto exchange rate to the estimate form and shows up popup to user
|
||||||
|
* as an indication the entries rates have been changed.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export const EstimateSyncAutoExRateToForm = R.compose(withDialogActions)(
|
||||||
|
({
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
|
}) => {
|
||||||
|
const { total } = useEstimateTotals();
|
||||||
|
const timeout = useRef();
|
||||||
|
|
||||||
|
useSyncExRateToForm({
|
||||||
|
onSynced: () => {
|
||||||
|
// If the total bigger then zero show alert to the user after adjusting entries.
|
||||||
|
if (total > 0) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
openDialog(DialogsName.InvoiceExchangeRateChangeNotice);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ function InvoiceExchangeRateChangeDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={'Please take care of the following'}
|
|
||||||
name={dialogName}
|
name={dialogName}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
canEscapeKeyClose={true}
|
canEscapeKeyClose={true}
|
||||||
@@ -32,7 +31,7 @@ function InvoiceExchangeRateChangeDialog({
|
|||||||
<DialogSuspense>
|
<DialogSuspense>
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
<p>
|
<p>
|
||||||
You have changed customers's currency after adding items to the
|
You have changed customer's currency after adding items to the
|
||||||
Invoice.
|
Invoice.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -41,14 +40,14 @@ function InvoiceExchangeRateChangeDialog({
|
|||||||
rate feeds.
|
rate feeds.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p style={{ marginBottom: '30px' }}>
|
||||||
Before saving the transaction, ensure that the item rates align with
|
Before saving the transaction, ensure that the item rates align with
|
||||||
the current exchange rate of the newly selected currency.
|
the current exchange rate of the newly selected currency.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
<Button onClick={handleConfirm} intent={Intent.PRIMARY}>
|
<Button onClick={handleConfirm} intent={Intent.PRIMARY} fill>
|
||||||
Ok
|
Ok
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export default function InvoiceFloatingActions() {
|
|||||||
history.goBack();
|
history.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle clear button click.
|
||||||
const handleClearBtnClick = (event) => {
|
const handleClearBtnClick = (event) => {
|
||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ import {
|
|||||||
handleDateChange,
|
handleDateChange,
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import {
|
import { customerNameFieldShouldUpdate } from './utils';
|
||||||
customerNameFieldShouldUpdate,
|
|
||||||
useInvoiceEntriesOnExchangeRateChange,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +36,7 @@ import {
|
|||||||
ProjectBillableEntriesLink,
|
ProjectBillableEntriesLink,
|
||||||
} from '@/containers/Projects/components';
|
} from '@/containers/Projects/components';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice form header fields.
|
* Invoice form header fields.
|
||||||
@@ -55,10 +52,8 @@ export default function InvoiceFormHeaderFields() {
|
|||||||
<InvoiceFormCustomerSelect />
|
<InvoiceFormCustomerSelect />
|
||||||
|
|
||||||
{/* ----------- Exchange rate ----------- */}
|
{/* ----------- Exchange rate ----------- */}
|
||||||
<InvoiceExchangeRateInputField
|
<InvoiceExchangeRateInputField />
|
||||||
name={'exchange_rate'}
|
|
||||||
formGroupProps={{ label: ' ', inline: true }}
|
|
||||||
/>
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={6}>
|
<Col xs={6}>
|
||||||
{/* ----------- Invoice date ----------- */}
|
{/* ----------- Invoice date ----------- */}
|
||||||
@@ -166,27 +161,18 @@ export default function InvoiceFormHeaderFields() {
|
|||||||
*/
|
*/
|
||||||
function InvoiceFormCustomerSelect() {
|
function InvoiceFormCustomerSelect() {
|
||||||
const { values, setFieldValue } = useFormikContext();
|
const { values, setFieldValue } = useFormikContext();
|
||||||
const { customers, setAutoExRateCurrency } = useInvoiceFormContext();
|
const { customers } = useInvoiceFormContext();
|
||||||
const currentComapny = useCurrentOrganization();
|
|
||||||
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
|
const updateEntries = useCustomerUpdateExRate();
|
||||||
|
|
||||||
// Handles the customer item change.
|
// Handles the customer item change.
|
||||||
const handleItemChange = (customer) => {
|
const handleItemChange = (customer) => {
|
||||||
setAutoExRateCurrency(null);
|
|
||||||
|
|
||||||
// If the customer id has changed change the customer id and currency code.
|
// If the customer id has changed change the customer id and currency code.
|
||||||
if (values.customer_id !== customer.id) {
|
if (values.customer_id !== customer.id) {
|
||||||
setFieldValue('customer_id', customer.id);
|
setFieldValue('customer_id', customer.id);
|
||||||
setFieldValue('currency_code', customer?.currency_code);
|
setFieldValue('currency_code', customer?.currency_code);
|
||||||
}
|
}
|
||||||
// If the customer's currency code is the same the base currency.
|
updateEntries(customer);
|
||||||
if (customer?.currency_code === currentComapny.base_currency) {
|
|
||||||
setFieldValue('exchange_rate', '1');
|
|
||||||
setFieldValue('entries', composeEntriesOnExChange(values.exchange_rate, 1));
|
|
||||||
} else {
|
|
||||||
// Sets the currency code to fetch auto-exchange rate.
|
|
||||||
setAutoExRateCurrency(customer?.currency_code);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '@/style/pages/SaleInvoice/PageForm.scss';
|
|||||||
|
|
||||||
import InvoiceForm from './InvoiceForm';
|
import InvoiceForm from './InvoiceForm';
|
||||||
import { InvoiceFormProvider } from './InvoiceFormProvider';
|
import { InvoiceFormProvider } from './InvoiceFormProvider';
|
||||||
|
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice form page.
|
* Invoice form page.
|
||||||
@@ -16,7 +17,9 @@ export default function InvoiceFormPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InvoiceFormProvider invoiceId={idAsInteger}>
|
<InvoiceFormProvider invoiceId={idAsInteger}>
|
||||||
<InvoiceForm />
|
<AutoExchangeRateProvider>
|
||||||
|
<InvoiceForm />
|
||||||
|
</AutoExchangeRateProvider>
|
||||||
</InvoiceFormProvider>
|
</InvoiceFormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { createContext, useState } from 'react';
|
|||||||
import { isEmpty, pick } from 'lodash';
|
import { isEmpty, pick } from 'lodash';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { useCurrentOrganization, useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
import { DashboardInsider } from '@/components/Dashboard';
|
import { DashboardInsider } from '@/components/Dashboard';
|
||||||
import { transformToEditForm, ITEMS_FILTER_ROLES_QUERY } from './utils';
|
import { transformToEditForm, ITEMS_FILTER_ROLES_QUERY } from './utils';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
useEditInvoice,
|
useEditInvoice,
|
||||||
useSettingsInvoices,
|
useSettingsInvoices,
|
||||||
useEstimate,
|
useEstimate,
|
||||||
useExchangeRate,
|
|
||||||
} from '@/hooks/query';
|
} from '@/hooks/query';
|
||||||
import { useProjects } from '@/containers/Projects/hooks';
|
import { useProjects } from '@/containers/Projects/hooks';
|
||||||
import { useTaxRates } from '@/hooks/query/taxRates';
|
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||||
@@ -94,18 +93,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
// Handle fetching settings.
|
// Handle fetching settings.
|
||||||
const { isLoading: isSettingsLoading } = useSettingsInvoices();
|
const { isLoading: isSettingsLoading } = useSettingsInvoices();
|
||||||
|
|
||||||
const [autoExRateCurrency, setAutoExRateCurrency] = useState<string>('');
|
|
||||||
const currentOrganization = useCurrentOrganization();
|
|
||||||
|
|
||||||
// Retrieves the exchange rate.
|
|
||||||
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
|
|
||||||
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
|
|
||||||
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
cacheTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and edit invoice mutations.
|
// Create and edit invoice mutations.
|
||||||
const { mutateAsync: createInvoiceMutate } = useCreateInvoice();
|
const { mutateAsync: createInvoiceMutate } = useCreateInvoice();
|
||||||
const { mutateAsync: editInvoiceMutate } = useEditInvoice();
|
const { mutateAsync: editInvoiceMutate } = useEditInvoice();
|
||||||
@@ -132,7 +119,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
warehouses,
|
warehouses,
|
||||||
projects,
|
projects,
|
||||||
taxRates,
|
taxRates,
|
||||||
autoExchangeRate,
|
|
||||||
|
|
||||||
isInvoiceLoading,
|
isInvoiceLoading,
|
||||||
isItemsLoading,
|
isItemsLoading,
|
||||||
@@ -149,10 +135,6 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
editInvoiceMutate,
|
editInvoiceMutate,
|
||||||
setSubmitPayload,
|
setSubmitPayload,
|
||||||
isNewMode,
|
isNewMode,
|
||||||
|
|
||||||
autoExRateCurrency,
|
|
||||||
setAutoExRateCurrency,
|
|
||||||
isAutoExchangeRateLoading,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,44 +1,22 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Button } from '@blueprintjs/core';
|
import { Button } from '@blueprintjs/core';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { ExchangeRateInputGroup } from '@/components';
|
import { ExchangeRateInputGroup } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import {
|
import { useInvoiceIsForeignCustomer, useInvoiceTotal } from './utils';
|
||||||
useInvoiceEntriesOnExchangeRateChange,
|
|
||||||
useInvoiceIsForeignCustomer,
|
|
||||||
useInvoiceTotal,
|
|
||||||
} from './utils';
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import { useUpdateEffect } from '@/hooks';
|
import { useUpdateEffect } from '@/hooks';
|
||||||
import { transactionNumber } from '@/utils';
|
import { transactionNumber } from '@/utils';
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import {
|
||||||
/**
|
useSyncExRateToForm,
|
||||||
* Re-calculate the item entries prices based on the old exchange rate.
|
withExchangeRateFetchingLoading,
|
||||||
* @param {InvoiceExchangeRateInputFieldRoot} Component
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
* @returns {JSX.Element}
|
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
*/
|
|
||||||
const withExchangeRateItemEntriesPriceRecalc = (Component) => (props) => {
|
|
||||||
const { setFieldValue } = useFormikContext();
|
|
||||||
const composeChangeExRate = useInvoiceEntriesOnExchangeRateChange();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
|
|
||||||
setFieldValue(
|
|
||||||
'entries',
|
|
||||||
composeChangeExRate(oldExchangeRate, exchangeRate),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice exchange rate input field.
|
* Invoice exchange rate input field.
|
||||||
@@ -47,8 +25,6 @@ const withExchangeRateItemEntriesPriceRecalc = (Component) => (props) => {
|
|||||||
const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
|
const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
|
||||||
const currentOrganization = useCurrentOrganization();
|
const currentOrganization = useCurrentOrganization();
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
const { isAutoExchangeRateLoading } = useInvoiceFormContext();
|
|
||||||
|
|
||||||
const isForeignCustomer = useInvoiceIsForeignCustomer();
|
const isForeignCustomer = useInvoiceIsForeignCustomer();
|
||||||
|
|
||||||
// Can't continue if the customer is not foreign.
|
// Can't continue if the customer is not foreign.
|
||||||
@@ -60,7 +36,8 @@ const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
|
|||||||
name={'exchange_rate'}
|
name={'exchange_rate'}
|
||||||
fromCurrency={values.currency_code}
|
fromCurrency={values.currency_code}
|
||||||
toCurrency={currentOrganization.base_currency}
|
toCurrency={currentOrganization.base_currency}
|
||||||
isLoading={isAutoExchangeRateLoading}
|
formGroupProps={{ label: ' ', inline: true }}
|
||||||
|
withPopoverRecalcConfirm
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -71,6 +48,7 @@ const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
|
|||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export const InvoiceExchangeRateInputField = R.compose(
|
export const InvoiceExchangeRateInputField = R.compose(
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
withExchangeRateItemEntriesPriceRecalc,
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
)(InvoiceExchangeRateInputFieldRoot);
|
)(InvoiceExchangeRateInputFieldRoot);
|
||||||
|
|
||||||
@@ -108,40 +86,26 @@ export const InvoiceNoSyncSettingsToForm = R.compose(
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs the fetched real-time exchange rate to the form.
|
* Syncs the realtime exchange rate to the invoice form and shows up popup to the user
|
||||||
* @returns {JSX.Element}
|
* as an indication the entries rates have been re-calculated.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
*/
|
*/
|
||||||
export const InvoiceExchangeRateSync = R.compose(withDialogActions)(
|
export const InvoiceExchangeRateSync = R.compose(withDialogActions)(
|
||||||
({ openDialog }) => {
|
({ openDialog }) => {
|
||||||
const { setFieldValue, values } = useFormikContext();
|
|
||||||
const { autoExRateCurrency, autoExchangeRate } = useInvoiceFormContext();
|
|
||||||
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
|
|
||||||
|
|
||||||
const total = useInvoiceTotal();
|
const total = useInvoiceTotal();
|
||||||
const timeout = useRef();
|
const timeout = useRef();
|
||||||
|
|
||||||
// Sync the fetched real-time exchanage rate to the form.
|
useSyncExRateToForm({
|
||||||
useEffect(() => {
|
onSynced: () => {
|
||||||
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
|
|
||||||
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
|
|
||||||
setFieldValue(
|
|
||||||
'entries',
|
|
||||||
composeEntriesOnExChange(
|
|
||||||
values.exchange_rate,
|
|
||||||
autoExchangeRate?.exchange_rate,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// If the total bigger then zero show alert to the user after adjusting entries.
|
// If the total bigger then zero show alert to the user after adjusting entries.
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
clearTimeout(timeout.current);
|
clearTimeout(timeout.current);
|
||||||
timeout.current = setTimeout(() => {
|
timeout.current = setTimeout(() => {
|
||||||
openDialog(DialogsName.InvoiceExchangeRateChange);
|
openDialog(DialogsName.InvoiceExchangeRateChangeNotice);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
});
|
||||||
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -398,85 +398,3 @@ export const useIsInvoiceTaxExclusive = () => {
|
|||||||
|
|
||||||
return values.inclusive_exclusive_tax === TaxType.Exclusive;
|
return values.inclusive_exclusive_tax === TaxType.Exclusive;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the given rate to the local currency.
|
|
||||||
* @param {number} rate
|
|
||||||
* @param {number} exchangeRate
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const convertToForeignCurrency = (
|
|
||||||
rate: number,
|
|
||||||
exchangeRate: number,
|
|
||||||
) => {
|
|
||||||
return rate * exchangeRate;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given rate to the base currency.
|
|
||||||
* @param {number} rate
|
|
||||||
* @param {number} exchangeRate
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
|
|
||||||
return rate / exchangeRate;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverts the given rate from the old exchange rate and covert it to the new
|
|
||||||
* currency based on the given new exchange rate.
|
|
||||||
* @param {number} rate -
|
|
||||||
* @param {number} oldExchangeRate - Old exchange rate.
|
|
||||||
* @param {number} newExchangeRate - New exchange rate.
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
const revertAndConvertExchangeRate = (
|
|
||||||
rate: number,
|
|
||||||
oldExchangeRate: number,
|
|
||||||
newExchangeRate: number,
|
|
||||||
) => {
|
|
||||||
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
|
|
||||||
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
|
|
||||||
|
|
||||||
return round(newValue, 3);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign the new item entry rate after converting to the new exchange rate.
|
|
||||||
* @params {number} oldExchangeRate -
|
|
||||||
* @params {number} newExchangeRate -
|
|
||||||
* @params {IItemEntry} entries -
|
|
||||||
*/
|
|
||||||
const assignRateRevertAndCovertExchangeRate = R.curry(
|
|
||||||
(oldExchangeRate: number, newExchangeRate: number, entries: IItemEntry[]) => {
|
|
||||||
return entries.map((entry) => ({
|
|
||||||
...entry,
|
|
||||||
rate: revertAndConvertExchangeRate(
|
|
||||||
entry.rate,
|
|
||||||
oldExchangeRate,
|
|
||||||
newExchangeRate,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose invoice entries on exchange rate change.
|
|
||||||
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
|
|
||||||
*/
|
|
||||||
export const useInvoiceEntriesOnExchangeRateChange = () => {
|
|
||||||
const {
|
|
||||||
values: { entries },
|
|
||||||
} = useFormikContext();
|
|
||||||
|
|
||||||
return React.useMemo(() => {
|
|
||||||
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
|
|
||||||
return R.compose(
|
|
||||||
// Updates entries total.
|
|
||||||
updateItemsEntriesTotal,
|
|
||||||
// Assign a new rate of the given new exchange rate from the old exchange rate.
|
|
||||||
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
|
|
||||||
)(entries);
|
|
||||||
});
|
|
||||||
}, [entries]);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
transformFormValuesToRequest,
|
transformFormValuesToRequest,
|
||||||
resetFormState,
|
resetFormState,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { ReceiptSyncIncrementSettingsToForm } from './components';
|
import { ReceiptSyncAutoExRateToForm, ReceiptSyncIncrementSettingsToForm } from './components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt form.
|
* Receipt form.
|
||||||
@@ -171,6 +171,7 @@ function ReceiptForm({
|
|||||||
|
|
||||||
{/*---------- Effects ---------*/}
|
{/*---------- Effects ---------*/}
|
||||||
<ReceiptSyncIncrementSettingsToForm />
|
<ReceiptSyncIncrementSettingsToForm />
|
||||||
|
<ReceiptSyncAutoExRateToForm />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
ReceiptProjectSelectButton,
|
ReceiptProjectSelectButton,
|
||||||
} from './components';
|
} from './components';
|
||||||
import { ReceiptFormReceiptNumberField } from './ReceiptFormReceiptNumberField';
|
import { ReceiptFormReceiptNumberField } from './ReceiptFormReceiptNumberField';
|
||||||
|
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt form header fields.
|
* Receipt form header fields.
|
||||||
@@ -46,10 +47,7 @@ export default function ReceiptFormHeader() {
|
|||||||
<ReceiptFormCustomerSelect />
|
<ReceiptFormCustomerSelect />
|
||||||
|
|
||||||
{/* ----------- Exchange rate ----------- */}
|
{/* ----------- Exchange rate ----------- */}
|
||||||
<ReceiptExchangeRateInputField
|
<ReceiptExchangeRateInputField />
|
||||||
name={'exchange_rate'}
|
|
||||||
formGroupProps={{ label: ' ', inline: true }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ----------- Deposit account ----------- */}
|
{/* ----------- Deposit account ----------- */}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
@@ -148,6 +146,16 @@ function ReceiptFormCustomerSelect() {
|
|||||||
const { setFieldValue, values } = useFormikContext();
|
const { setFieldValue, values } = useFormikContext();
|
||||||
const { customers } = useReceiptFormContext();
|
const { customers } = useReceiptFormContext();
|
||||||
|
|
||||||
|
const updateEntries = useCustomerUpdateExRate();
|
||||||
|
|
||||||
|
// Handles the customer item change.
|
||||||
|
const handleItemChange = (customer) => {
|
||||||
|
setFieldValue('customer_id', customer.id);
|
||||||
|
setFieldValue('currency_code', customer?.currency_code);
|
||||||
|
|
||||||
|
updateEntries(customer);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'customer_id'}
|
name={'customer_id'}
|
||||||
@@ -162,10 +170,7 @@ function ReceiptFormCustomerSelect() {
|
|||||||
name={'customer_id'}
|
name={'customer_id'}
|
||||||
items={customers}
|
items={customers}
|
||||||
placeholder={<T id={'select_customer_account'} />}
|
placeholder={<T id={'select_customer_account'} />}
|
||||||
onItemChange={(customer) => {
|
onItemChange={handleItemChange}
|
||||||
setFieldValue('customer_id', customer.id);
|
|
||||||
setFieldValue('currency_code', customer?.currency_code);
|
|
||||||
}}
|
|
||||||
popoverFill={true}
|
popoverFill={true}
|
||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
fastField={true}
|
fastField={true}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '@/style/pages/SaleReceipt/PageForm.scss';
|
|||||||
|
|
||||||
import ReceiptFrom from './ReceiptForm';
|
import ReceiptFrom from './ReceiptForm';
|
||||||
import { ReceiptFormProvider } from './ReceiptFormProvider';
|
import { ReceiptFormProvider } from './ReceiptFormProvider';
|
||||||
|
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt form page.
|
* Receipt form page.
|
||||||
@@ -16,7 +17,9 @@ export default function ReceiptFormPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReceiptFormProvider receiptId={idInt}>
|
<ReceiptFormProvider receiptId={idInt}>
|
||||||
<ReceiptFrom />
|
<AutoExchangeRateProvider>
|
||||||
|
<ReceiptFrom />
|
||||||
|
</AutoExchangeRateProvider>
|
||||||
</ReceiptFormProvider>
|
</ReceiptFormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Button } from '@blueprintjs/core';
|
import { Button } from '@blueprintjs/core';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
@@ -7,20 +7,26 @@ import * as R from 'ramda';
|
|||||||
|
|
||||||
import { ExchangeRateInputGroup } from '@/components';
|
import { ExchangeRateInputGroup } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import { useReceiptIsForeignCustomer } from './utils';
|
import { useReceiptIsForeignCustomer, useReceiptTotals } from './utils';
|
||||||
import { useUpdateEffect } from '@/hooks';
|
import { useUpdateEffect } from '@/hooks';
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
|
||||||
import { transactionNumber } from '@/utils';
|
import { transactionNumber } from '@/utils';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
|
import {
|
||||||
|
useSyncExRateToForm,
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
} from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt exchange rate input field.
|
* Receipt exchange rate input field.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export function ReceiptExchangeRateInputField({ ...props }) {
|
function ReceiptExchangeRateInputFieldRoot({ ...props }) {
|
||||||
const currentOrganization = useCurrentOrganization();
|
const currentOrganization = useCurrentOrganization();
|
||||||
const { values } = useFormikContext();
|
|
||||||
|
|
||||||
const isForeignCustomer = useReceiptIsForeignCustomer();
|
const isForeignCustomer = useReceiptIsForeignCustomer();
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
// Can't continue if the customer is not foreign.
|
// Can't continue if the customer is not foreign.
|
||||||
if (!isForeignCustomer) {
|
if (!isForeignCustomer) {
|
||||||
@@ -28,13 +34,21 @@ export function ReceiptExchangeRateInputField({ ...props }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ExchangeRateInputGroup
|
<ExchangeRateInputGroup
|
||||||
|
name={'exchange_rate'}
|
||||||
fromCurrency={values.currency_code}
|
fromCurrency={values.currency_code}
|
||||||
toCurrency={currentOrganization.base_currency}
|
toCurrency={currentOrganization.base_currency}
|
||||||
|
formGroupProps={{ label: ' ', inline: true }}
|
||||||
|
withPopoverRecalcConfirm
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ReceiptExchangeRateInputField = R.compose(
|
||||||
|
withExchangeRateFetchingLoading,
|
||||||
|
withExchangeRateItemEntriesPriceRecalc,
|
||||||
|
)(ReceiptExchangeRateInputFieldRoot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt project select.
|
* Receipt project select.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
@@ -73,3 +87,31 @@ export const ReceiptSyncIncrementSettingsToForm = R.compose(
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the auto exchange rate to the receipt form and shows up popup to user
|
||||||
|
* as an indication the entries rates have been changed.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export const ReceiptSyncAutoExRateToForm = R.compose(withDialogActions)(
|
||||||
|
({
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
|
}) => {
|
||||||
|
const { total } = useReceiptTotals();
|
||||||
|
const timeout = useRef();
|
||||||
|
|
||||||
|
useSyncExRateToForm({
|
||||||
|
onSynced: () => {
|
||||||
|
// If the total bigger then zero show alert to the user after adjusting entries.
|
||||||
|
if (total > 0) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
openDialog(DialogsName.InvoiceExchangeRateChangeNotice);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ function getRandomItemFromArray(arr) {
|
|||||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||||
return arr[randomIndex];
|
return arr[randomIndex];
|
||||||
}
|
}
|
||||||
|
function delay(t, val) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, t, val));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Retrieves tax rates.
|
* Retrieves tax rates.
|
||||||
* @param {number} customerId - Customer id.
|
* @param {number} customerId - Customer id.
|
||||||
@@ -18,12 +20,15 @@ export function useExchangeRate(
|
|||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
[QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency],
|
[QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency],
|
||||||
() =>
|
async () => {
|
||||||
Promise.resolve({
|
await delay(100);
|
||||||
|
|
||||||
|
return {
|
||||||
from_currency: fromCurrency,
|
from_currency: fromCurrency,
|
||||||
to_currency: toCurrency,
|
to_currency: toCurrency,
|
||||||
exchange_rate: getRandomItemFromArray([4.231, 2.231]),
|
exchange_rate: 1.00,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user