feat: Auto re-calculate the items rate once changing the invoice exchange rate.

This commit is contained in:
Ahmed Bouhuolia
2023-10-16 19:14:27 +02:00
parent 1ed1c9ea1d
commit 9531730d7a
32 changed files with 473 additions and 1010 deletions

View File

@@ -34,7 +34,7 @@ function CustomerSelectRoot({
<FSelect
items={items}
textAccessor={'display_name'}
labelAccessor={'code'}
labelAccessor={'currency_code'}
valueAccessor={'id'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
createNewItemRenderer={maybeCreateNewItemRenderer}

View File

@@ -3,8 +3,6 @@ import InviteUserDialog from '@/containers/Dialogs/InviteUserDialog';
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';
import ItemCategoryDialog from '@/containers/Dialogs/ItemCategoryDialog';
import CurrencyFormDialog from '@/containers/Dialogs/CurrencyFormDialog';
import ExchangeRateFormDialog from '@/containers/Dialogs/ExchangeRateFormDialog';
import InventoryAdjustmentDialog from '@/containers/Dialogs/InventoryAdjustmentFormDialog';
import PaymentViaVoucherDialog from '@/containers/Dialogs/PaymentViaVoucherDialog';
import KeyboardShortcutsDialog from '@/containers/Dialogs/keyboardShortcutsDialog';
@@ -47,6 +45,7 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs';
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
/**
* Dialogs container.
@@ -58,7 +57,6 @@ export default function DialogsContainer() {
<CurrencyFormDialog dialogName={DialogsName.CurrencyForm} />
<InviteUserDialog dialogName={DialogsName.InviteForm} />
<UserFormDialog dialogName={DialogsName.UserForm} />
<ExchangeRateFormDialog dialogName={DialogsName.ExchangeRateForm} />
<ItemCategoryDialog dialogName={DialogsName.ItemCategoryForm} />
<InventoryAdjustmentDialog
dialogName={DialogsName.InventoryAdjustmentForm}
@@ -137,6 +135,9 @@ export default function DialogsContainer() {
dialogName={DialogsName.ProjectBillableEntriesForm}
/>
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
<InvoiceExchangeRateChangeDialog
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
/>
</div>
);
}

View File

@@ -1,30 +1,154 @@
// @ts-nocheck
import React from 'react';
import { useState } from 'react';
import styled from 'styled-components';
import { ControlGroup } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import {
Button,
Classes,
ControlGroup,
Intent,
Popover,
Spinner,
} from '@blueprintjs/core';
import { FlagIcon } from '../Tags';
import { FMoneyInputGroup, FFormGroup } from '../Forms';
import { useUncontrolled } from '@/hooks/useUncontrolled';
interface ExchangeRateValuesBag {
oldExchangeRate: string;
exchangeRate: string;
}
interface ExchangeRateInputGroupProps {
name: string;
fromCurrency: string;
toCurrency: string;
isLoading?: boolean;
inputGroupProps?: any;
formGroupProps?: any;
popoverRecalcConfirm?: boolean;
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
onCancel: (bag: ExchangeRateValuesBag) => void;
isConfirmPopoverOpen?: boolean;
initialConfirmPopoverOpen?: boolean;
onConfirmPopoverOpen?: (isOpen: boolean) => void;
}
export function ExchangeRateInputGroup({
name,
fromCurrency,
toCurrency,
isLoading,
inputGroupProps,
formGroupProps,
name,
}) {
popoverRecalcConfirm = false,
onRecalcConfirm,
onCancel,
isConfirmPopoverOpen,
initialConfirmPopoverOpen,
onConfirmPopoverOpen,
}: ExchangeRateInputGroupProps) {
const [isOpen, handlePopoverOpen] = useUncontrolled<boolean>({
value: isConfirmPopoverOpen,
initialValue: initialConfirmPopoverOpen,
finalValue: false,
onChange: onConfirmPopoverOpen,
});
const { values, setFieldValue } = useFormikContext();
const [oldExchangeRate, setOldExchangeRate] = useState<string>('');
const exchangeRate = values[name];
const exchangeRateValuesBag: ExchangeRateValuesBag = {
exchangeRate,
oldExchangeRate,
};
// Handle re-calc confirm button click.
const handleRecalcConfirmBtn = () => {
handlePopoverOpen(false);
onRecalcConfirm && onRecalcConfirm(exchangeRateValuesBag);
};
// Handle cancel button click.
const handleCancelBtn = () => {
handlePopoverOpen(false);
onCancel && onCancel(exchangeRateValuesBag);
};
// Handle exchange rate field blur.
const handleExchangeRateFieldBlur = (value: string) => {
if (value !== values[name]) {
handlePopoverOpen(true);
setFieldValue(name, value);
setOldExchangeRate(values[name]);
}
};
const exchangeRateField = (
<ExchangeRateField
allowDecimals={true}
allowNegativeValue={true}
asyncControl={true}
onChange={() => null}
onBlur={handleExchangeRateFieldBlur}
rightElement={isLoading && <Spinner size={16} />}
{...inputGroupProps}
name={name}
/>
);
const popoverConfirmContent = (
<PopoverContent>
<p>
Are you want to re-calculate item prices based on this exchange rate
</p>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
marginTop: 15,
}}
>
<Button
className={Classes.POPOVER_DISMISS}
style={{ marginRight: 10 }}
onClick={handleCancelBtn}
small
minimal
>
Cancel
</Button>
<Button
intent={Intent.WARNING}
className={Classes.POPOVER_DISMISS}
onClick={handleRecalcConfirmBtn}
small
>
Re-calculate
</Button>
</div>
</PopoverContent>
);
return (
<FFormGroup inline={true} {...formGroupProps} name={name}>
<ControlGroup>
<ExchangeRatePrepend>
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
</ExchangeRatePrepend>
<ExchangeRateField
allowDecimals={true}
allowNegativeValue={true}
{...inputGroupProps}
name={name}
/>
{popoverRecalcConfirm ? (
<Popover isOpen={isOpen} content={popoverConfirmContent}>
{exchangeRateField}
</Popover>
) : (
exchangeRateField
)}
<ExchangeRateAppend>
<ExchangeFlagIcon currencyCode={toCurrency} /> {toCurrency}
</ExchangeRateAppend>
@@ -34,7 +158,7 @@ export function ExchangeRateInputGroup({
}
const ExchangeRateField = styled(FMoneyInputGroup)`
max-width: 75px;
max-width: 85px;
`;
const ExchangeRateSideIcon = styled.div`
@@ -57,3 +181,8 @@ const ExchangeFlagIcon = styled(FlagIcon)`
margin-left: 5px;
display: inline-block;
`;
const PopoverContent = styled('div')`
padding: 20px;
width: 300px;
`;

View File

@@ -48,4 +48,5 @@ export enum DialogsName {
ProjectBillableEntriesForm = 'project-billable-entries',
InvoiceNumberSettings = 'InvoiceNumberSettings',
TaxRateForm = 'tax-rate-form',
InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice'
}

View File

@@ -12,7 +12,6 @@ import PaymentMadesAlerts from '@/containers/Purchases/PaymentMades/PaymentMades
import CustomersAlerts from '@/containers/Customers/CustomersAlerts';
import VendorsAlerts from '@/containers/Vendors/VendorsAlerts';
import ManualJournalsAlerts from '@/containers/Accounting/JournalsLanding/ManualJournalsAlerts';
import ExchangeRatesAlerts from '@/containers/ExchangeRates/ExchangeRatesAlerts';
import ExpensesAlerts from '@/containers/Expenses/ExpensesAlerts';
import AccountTransactionsAlerts from '@/containers/CashFlow/AccountTransactions/AccountTransactionsAlerts';
import UsersAlerts from '@/containers/Preferences/Users/UsersAlerts';
@@ -41,7 +40,6 @@ export default [
...CustomersAlerts,
...VendorsAlerts,
...ManualJournalsAlerts,
...ExchangeRatesAlerts,
...ExpensesAlerts,
...AccountTransactionsAlerts,
...UsersAlerts,
@@ -54,5 +52,5 @@ export default [
...WarehousesTransfersAlerts,
...BranchesAlerts,
...ProjectAlerts,
...TaxRatesAlerts
...TaxRatesAlerts,
];

View File

@@ -1,19 +0,0 @@
// @ts-nocheck
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
const Schema = Yup.object().shape({
exchange_rate: Yup.number()
.required()
.label(intl.get('exchange_rate_')),
currency_code: Yup.string()
.max(3)
.required(intl.get('currency_code_')),
date: Yup.date()
.required()
.label(intl.get('date')),
});
export const CreateExchangeRateFormSchema = Schema;
export const EditExchangeRateFormSchema = Schema;

View File

@@ -1,114 +0,0 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { AppToaster } from '@/components';
import {
CreateExchangeRateFormSchema,
EditExchangeRateFormSchema,
} from './ExchangeRateForm.schema';
import ExchangeRateFormContent from './ExchangeRateFormContent';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose, transformToForm } from '@/utils';
const defaultInitialValues = {
exchange_rate: '',
currency_code: '',
date: moment(new Date()).format('YYYY-MM-DD'),
};
/**
* Exchange rate form.
*/
function ExchangeRateForm({
// #withDialogActions
closeDialog,
}) {
const {
createExchangeRateMutate,
editExchangeRateMutate,
isNewMode,
dialogName,
exchangeRate,
} = useExchangeRateFromContext();
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
? CreateExchangeRateFormSchema
: EditExchangeRateFormSchema;
const initialValues = useMemo(
() => ({
...defaultInitialValues,
...transformToForm(exchangeRate, defaultInitialValues),
}),
[],
);
// Transformers response errors.
const transformErrors = (errors, { setErrors }) => {
if (
errors.find((error) => error.type === 'EXCHANGE.RATE.DATE.PERIOD.DEFINED')
) {
setErrors({
exchange_rate: intl.get(
'there_is_exchange_rate_in_this_date_with_the_same_currency',
),
});
}
};
// Handle the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle close the dialog after success response.
const afterSubmit = () => {
closeDialog(dialogName);
};
const onSuccess = ({ response }) => {
AppToaster.show({
message: intl.get(
!isNewMode
? 'the_exchange_rate_has_been_edited_successfully'
: 'the_exchange_rate_has_been_created_successfully',
),
intent: Intent.SUCCESS,
});
afterSubmit(response);
};
// Handle the response error.
const onError = (error) => {
const {
response: {
data: { errors },
},
} = error;
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
createExchangeRateMutate(values).then(onSuccess).catch(onError);
} else {
editExchangeRateMutate([exchangeRate.id, values])
.then(onSuccess)
.catch(onError);
}
};
return (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ExchangeRateFormContent />
</Formik>
);
}
export default compose(withDialogActions)(ExchangeRateForm);

View File

@@ -1,14 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Form } from 'formik';
import ExchangeRateFormFields from './ExchangeRateFormFields';
import ExchangeRateFormFooter from './ExchangeRateFormFooter';
export default function ExchangeRateFormContent() {
return (
<Form>
<ExchangeRateFormFields />
<ExchangeRateFormFooter />
</Form>
);
}

View File

@@ -1,27 +0,0 @@
// @ts-nocheck
import React from 'react';
import ExchangeRateForm from './ExchangeRateForm';
import { ExchangeRateFormProvider } from './ExchangeRateFormProvider';
import '@/style/pages/ExchangeRate/ExchangeRateDialog.scss';
/**
* Exchange rate form content.
*/
export default function ExchangeRateFormDialogContent({
// #ownProp
action,
exchangeRateId,
dialogName,
}) {
return (
<ExchangeRateFormProvider
dialogName={dialogName}
exchangeRate={exchangeRateId}
action={action}
>
<ExchangeRateForm />
</ExchangeRateFormProvider>
);
}

View File

@@ -1,89 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Classes, FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { FastField } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import {
momentFormatter,
tansformDateValue,
handleDateChange,
inputIntent,
} from '@/utils';
import {
ErrorMessage,
FieldRequiredHint,
CurrencySelectList,
} from '@/components';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
export default function ExchangeRateFormFields() {
const { action, currencies } = useExchangeRateFromContext();
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- Date ----------- */}
<FastField name={'date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'date'} />}
labelInfo={FieldRequiredHint}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="date" />}
inline={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={action === 'edit'}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Currency Code ----------- */}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency_code'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--currency', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_code" />}
inline={true}
>
<CurrencySelectList
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={({ currency_code }) => {
form.setFieldValue('currency_code', currency_code);
}}
disabled={action === 'edit'}
/>
</FormGroup>
)}
</FastField>
{/*------------ Exchange Rate -----------*/}
<FastField name={'exchange_rate'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'exchange_rate'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="exchange_rate" />}
inline={true}
>
<InputGroup intent={inputIntent({ error, touched })} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -1,36 +0,0 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function ExchangeRateFormFooter({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
const { dialogName, action } = useExchangeRateFromContext();
const handleClose = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ExchangeRateFormFooter);

View File

@@ -1,53 +0,0 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import {
useCreateExchangeRate,
useEdiExchangeRate,
useCurrencies,
useExchangeRates,
} from '@/hooks/query';
import { DialogContent } from '@/components';
const ExchangeRateFormContext = createContext();
/**
* Exchange rate Form page provider.
*/
function ExchangeRateFormProvider({
exchangeRate,
action,
dialogName,
...props
}) {
// Create and edit exchange rate mutations.
const { mutateAsync: createExchangeRateMutate } = useCreateExchangeRate();
const { mutateAsync: editExchangeRateMutate } = useEdiExchangeRate();
// Load Currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
const { isFetching: isExchangeRatesLoading } = useExchangeRates();
const isNewMode = !exchangeRate;
// Provider state.
const provider = {
createExchangeRateMutate,
editExchangeRateMutate,
dialogName,
exchangeRate,
action,
currencies,
isExchangeRatesLoading,
isNewMode,
};
return (
<DialogContent isLoading={isCurrenciesLoading} name={'exchange-rate-form'}>
<ExchangeRateFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useExchangeRateFromContext = () => useContext(ExchangeRateFormContext);
export { ExchangeRateFormProvider, useExchangeRateFromContext };

View File

@@ -1,45 +0,0 @@
// @ts-nocheck
import React, { lazy } from 'react';
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ExchangeRateFormDialogContent = lazy(
() => import('./ExchangeRateFormDialogContent'),
);
/**
* Exchange rate form dialog.
*/
function ExchangeRateFormDialog({
dialogName,
payload = { action: '', id: null, exchangeRate: '' },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_exchange_rate'} />
) : (
<T id={'new_exchange_rate'} />
)
}
className={'dialog--exchangeRate-form'}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
>
<DialogSuspense>
<ExchangeRateFormDialogContent
dialogName={dialogName}
action={payload.action}
exchangeRateId={payload.exchangeRate}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(ExchangeRateFormDialog);

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
import React, { useCallback, useState, useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
Alignment,
} from '@blueprintjs/core';
import {
Icon,
If,
DashboardActionsBar,
FormattedMessage as T,
} from '@/components';
import { connect } from 'react-redux';
import { useRefreshExchangeRate } from '@/hooks/query/exchangeRates';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withResourceDetail from '@/containers/Resources/withResourceDetails';
import withExchangeRatesActions from './withExchangeRatesActions';
import { compose } from '@/utils';
/**
* Exchange rate actions bar.
*/
function ExchangeRateActionsBar({
// #withDialogActions.
openDialog,
// #withResourceDetail
resourceFields,
//#withExchangeRatesActions
addExchangeRatesTableQueries,
// #ownProps
selectedRows = [],
onDeleteExchangeRate,
onFilterChanged,
onBulkDelete,
}) {
const [filterCount, setFilterCount] = useState(0);
const onClickNewExchangeRate = () => {
openDialog('exchangeRate-form', {});
};
// Exchange rates refresh action.
const { refresh } = useRefreshExchangeRate();
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => {
refresh();
};
const hasSelectedRows = useMemo(
() => selectedRows.length > 0,
[selectedRows],
);
const handelBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_exchange_rate'} />}
onClick={onClickNewExchangeRate}
/>
<NavbarDivider />
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handelBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
const mapStateToProps = (state, props) => ({
resourceName: '',
});
const withExchangeRateActionBar = connect(mapStateToProps);
export default compose(
withExchangeRateActionBar,
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withExchangeRatesActions,
)(ExchangeRateActionsBar);

View File

@@ -1,110 +0,0 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import {
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
} from '@/components';
import { useExchangeRatesContext } from './ExchangeRatesProvider';
import { useExchangeRatesTableColumns, ActionMenuList } from './components';
import withExchangeRates from './withExchangeRates';
import withExchangeRatesActions from './withExchangeRatesActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
/**
* Exchange rates table.
*/
function ExchangeRateTable({
// #ownProps
tableProps,
// #withDialogActions.
openDialog,
// #withAlertActions
openAlert,
// #withExchangeRatesActions
setExchangeRateTableState,
// #withExchangeRates
exchangeRatesTableState,
}) {
const {
isExchangeRatesFetching,
isExchangeRatesLoading,
exchangesRates,
pagination,
} = useExchangeRatesContext();
// Table columns.
const columns = useExchangeRatesTableColumns();
// Handle delete exchange rate.
const handleDeleteExchangeRate = ({ id }) => {
openAlert('exchange-rate-delete', { exchangeRateId: id });
};
// Handle Edit exchange rate.
const handelEditExchangeRate = (exchangeRate) => {
openDialog('exchangeRate-form', {
action: 'edit',
exchangeRate: exchangeRate,
});
};
const handleFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
setExchangeRateTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setExchangeRateTableState],
);
return (
<DataTable
noInitialFetch={true}
columns={columns}
data={exchangesRates}
initialState={exchangeRatesTableState}
loading={isExchangeRatesLoading}
headerLoading={isExchangeRatesLoading}
progressBarLoading={isExchangeRatesFetching}
selectionColumn={true}
expandable={true}
sticky={true}
manualSortBy={true}
onFetchData={handleFetchData}
pagination={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionMenuList}
payload={{
onDeleteExchangeRate: handleDeleteExchangeRate,
onEditExchangeRate: handelEditExchangeRate,
}}
{...tableProps}
/>
);
}
export default compose(
withDialogActions,
withAlertActions,
withExchangeRates(({ exchangeRatesTableState }) => ({
exchangeRatesTableState,
})),
withExchangeRatesActions,
)(ExchangeRateTable);

View File

@@ -1,10 +0,0 @@
// @ts-nocheck
import React from 'react';
const ExchangeRateDeleteAlert = React.lazy(
() => import('@/containers/Alerts/ExchangeRates/ExchangeRateDeleteAlert'),
);
export default [
{ name: 'exchange-rate-delete', component: ExchangeRateDeleteAlert },
];

View File

@@ -1,38 +0,0 @@
// @ts-nocheck
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from '@/components';
import ExchangeRateTable from './ExchangeRateTable';
import ExchangeRateActionsBar from './ExchangeRateActionsBar';
import { ExchangeRatesProvider } from './ExchangeRatesProvider';
import { transformTableStateToQuery, compose } from '@/utils';
import withExchangeRates from './withExchangeRates';
/**
* Exchange Rates list.
*/
function ExchangeRatesList({
// #withExchangeRates
exchangeRatesTableState,
}) {
return (
<ExchangeRatesProvider
query={transformTableStateToQuery(exchangeRatesTableState)}
>
<ExchangeRateActionsBar />
<DashboardPageContent>
<DashboardContentTable>
<ExchangeRateTable />
</DashboardContentTable>
</DashboardPageContent>
</ExchangeRatesProvider>
);
}
export default compose(
withExchangeRates(({ exchangeRatesTableState }) => ({
exchangeRatesTableState,
})),
)(ExchangeRatesList);

View File

@@ -1,42 +0,0 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { transformTableQueryToParams } from '@/utils';
import { DashboardInsider } from '@/components';
import { useExchangeRates } from '@/hooks/query';
const ExchangesRatesContext = createContext();
/**
* Exchanges rates list provider.
*/
function ExchangeRatesProvider({ query, ...props }) {
const {
data: { exchangesRates, pagination, filterMeta },
isFetching: isExchangeRatesFetching,
isLoading: isExchangeRatesLoading,
} = useExchangeRates(
{
...transformTableQueryToParams(query),
},
{ keepPreviousData: true },
);
const state = {
isExchangeRatesFetching,
isExchangeRatesLoading,
exchangesRates,
pagination,
};
return (
<DashboardInsider name={'exchange-rate'}>
<ExchangesRatesContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useExchangeRatesContext = () => React.useContext(ExchangesRatesContext);
export { ExchangeRatesProvider, useExchangeRatesContext };

View File

@@ -1,91 +0,0 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import {
Menu,
Popover,
Button,
Position,
MenuItem,
MenuDivider,
Intent,
} from '@blueprintjs/core';
import { Icon, Money } from '@/components';
import { safeCallback } from '@/utils';
/**
* Row actions menu list.
*/
export function ActionMenuList({
row: { original },
payload: { onEditExchangeRate, onDeleteExchangeRate },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_exchange_rate')}
onClick={safeCallback(onEditExchangeRate, original)}
/>
<MenuDivider />
<MenuItem
text={intl.get('delete_exchange_rate')}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteExchangeRate, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Table actions cell.
*/
export function TableActionsCell(props) {
return (
<Popover
content={<ActionMenuList {...props} />}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
export function useExchangeRatesTableColumns() {
return useMemo(
() => [
{
id: 'date',
Header: intl.get('date'),
accessor: (r) => moment(r.date).format('YYYY MMM DD'),
width: 150,
},
{
id: 'currency_code',
Header: intl.get('currency_code'),
accessor: 'currency_code',
className: 'currency_code',
width: 150,
},
{
id: 'exchange_rate',
Header: intl.get('exchange_rate'),
accessor: (r) => (
<Money amount={r.exchange_rate} currency={r.currency_code} />
),
className: 'exchange_rate',
width: 150,
},
{
id: 'actions',
Header: '',
Cell: TableActionsCell,
className: 'actions',
width: 50,
},
],
[],
);
}

View File

@@ -1,9 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getExchangeRateById } from '@/store/ExchangeRate/exchange.selector';
const mapStateToProps = (state, props) => ({
exchangeRate: getExchangeRateById(state, props),
});
export default connect(mapStateToProps);

View File

@@ -1,16 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getExchangeRatesTableStateFactory } from '@/store/ExchangeRate/exchange.selector';
export default (mapState) => {
const getExchangeRatesTableState = getExchangeRatesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
exchangeRatesTableState: getExchangeRatesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,10 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { setExchangeRateTableState } from '@/store/ExchangeRate/exchange.actions';
export const mapDispatchToProps = (dispatch) => ({
setExchangeRateTableState: (queries) =>
dispatch(setExchangeRateTableState(queries)),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,63 @@
// @ts-nocheck
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
import { Button, Classes, Intent } from '@blueprintjs/core';
/**
* Invoice number dialog.
*/
function InvoiceExchangeRateChangeDialog({
dialogName,
payload: { initialFormValues },
isOpen,
onConfirm,
// #withDialogActions
closeDialog,
}) {
const handleConfirm = () => {
closeDialog(dialogName);
};
return (
<Dialog
title={'Please take care of the following'}
name={dialogName}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
onClose={() => {}}
>
<DialogSuspense>
<div className={Classes.DIALOG_BODY}>
<p>
You have changed customers's currency after adding items to the
Invoice.
</p>
<p>
The item rates have been adjusted to the new currency using exchange
rate feeds.
</p>
<p>
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}>
Ok
</Button>
</div>
</DialogSuspense>
</Dialog>
);
}
export default compose(
withDialogRedux(),
withDialogActions,
)(InvoiceExchangeRateChangeDialog);

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import { DialogsName } from '@/constants/dialogs';
import React from 'react';
const InvoiceExchangeRateChangeAlert = React.lazy(
() => import('./InvoiceExchangeRateChangeDialog'),
);
const Dialogs = [
{
name: DialogsName.InvoiceExchangeRateChangeNotice,
component: InvoiceExchangeRateChangeAlert,
},
];
export default Dialogs;

View File

@@ -34,7 +34,7 @@ import {
transformValueToRequest,
resetFormState,
} from './utils';
import { InvoiceNoSyncSettingsToForm } from './components';
import { InvoiceExchangeRateSync, InvoiceNoSyncSettingsToForm } from './components';
/**
* Invoice form.
@@ -180,6 +180,7 @@ function InvoiceForm({
{/*---------- Effects ----------*/}
<InvoiceNoSyncSettingsToForm />
<InvoiceExchangeRateSync />
</Form>
</Formik>
</div>

View File

@@ -23,7 +23,10 @@ import {
handleDateChange,
} from '@/utils';
import { CLASSES } from '@/constants/classes';
import { customerNameFieldShouldUpdate } from './utils';
import {
customerNameFieldShouldUpdate,
useInvoiceEntriesOnExchangeRateChange,
} from './utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import {
@@ -36,6 +39,7 @@ import {
ProjectBillableEntriesLink,
} from '@/containers/Projects/components';
import { Features } from '@/constants';
import { useCurrentOrganization } from '@/hooks/state';
/**
* Invoice form header fields.
@@ -161,8 +165,29 @@ export default function InvoiceFormHeaderFields() {
* @returns {React.ReactNode}
*/
function InvoiceFormCustomerSelect() {
const { customers } = useInvoiceFormContext();
const { values, setFieldValue } = useFormikContext();
const { customers, setAutoExRateCurrency } = useInvoiceFormContext();
const currentComapny = useCurrentOrganization();
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
// Handles the customer item change.
const handleItemChange = (customer) => {
setAutoExRateCurrency(null);
// If the customer id has changed change the customer id and currency code.
if (values.customer_id !== customer.id) {
setFieldValue('customer_id', customer.id);
setFieldValue('currency_code', customer?.currency_code);
}
// If the customer's currency code is the same the base currency.
if (customer?.currency_code === currentComapny.base_currency) {
setFieldValue('exchange_rate', '1');
setFieldValue('entries', composeEntriesOnExChange(values.exchange_rate, 1));
} else {
// Sets the currency code to fetch auto-exchange rate.
setAutoExRateCurrency(customer?.currency_code);
}
};
return (
<FFormGroup
@@ -178,10 +203,7 @@ function InvoiceFormCustomerSelect() {
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}
allowCreate={true}
fastField={true}
shouldUpdate={customerNameFieldShouldUpdate}

View File

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

View File

@@ -1,23 +1,53 @@
// @ts-nocheck
import React from 'react';
import { useEffect, useRef } from 'react';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import { useInvoiceIsForeignCustomer } from './utils';
import {
useInvoiceEntriesOnExchangeRateChange,
useInvoiceIsForeignCustomer,
useInvoiceTotal,
} from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { useUpdateEffect } from '@/hooks';
import { transactionNumber } from '@/utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
/**
* Re-calculate the item entries prices based on the old exchange rate.
* @param {InvoiceExchangeRateInputFieldRoot} Component
* @returns {JSX.Element}
*/
const withExchangeRateItemEntriesPriceRecalc = (Component) => (props) => {
const { setFieldValue } = useFormikContext();
const composeChangeExRate = useInvoiceEntriesOnExchangeRateChange();
return (
<Component
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
setFieldValue(
'entries',
composeChangeExRate(oldExchangeRate, exchangeRate),
);
}}
{...props}
/>
);
};
/**
* Invoice exchange rate input field.
* @returns {JSX.Element}
*/
export function InvoiceExchangeRateInputField({ ...props }) {
const InvoiceExchangeRateInputFieldRoot = ({ ...props }) => {
const currentOrganization = useCurrentOrganization();
const { values } = useFormikContext();
const { isAutoExchangeRateLoading } = useInvoiceFormContext();
const isForeignCustomer = useInvoiceIsForeignCustomer();
@@ -27,12 +57,22 @@ export function InvoiceExchangeRateInputField({ ...props }) {
}
return (
<ExchangeRateInputGroup
name={'exchange_rate'}
fromCurrency={values.currency_code}
toCurrency={currentOrganization.base_currency}
isLoading={isAutoExchangeRateLoading}
{...props}
/>
);
}
};
/**
* Invoice exchange rate input field.
* @returns {JSX.Element}
*/
export const InvoiceExchangeRateInputField = R.compose(
withExchangeRateItemEntriesPriceRecalc,
)(InvoiceExchangeRateInputFieldRoot);
/**
* Invoice project select.
@@ -66,3 +106,42 @@ export const InvoiceNoSyncSettingsToForm = R.compose(
return null;
});
/**
* Syncs the fetched real-time exchange rate to the form.
* @returns {JSX.Element}
*/
export const InvoiceExchangeRateSync = R.compose(withDialogActions)(
({ openDialog }) => {
const { setFieldValue, values } = useFormikContext();
const { autoExRateCurrency, autoExchangeRate } = useInvoiceFormContext();
const composeEntriesOnExChange = useInvoiceEntriesOnExchangeRateChange();
const total = useInvoiceTotal();
const timeout = useRef();
// Sync the fetched real-time exchanage rate to the form.
useEffect(() => {
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
setFieldValue(
'entries',
composeEntriesOnExChange(
values.exchange_rate,
autoExchangeRate?.exchange_rate,
),
);
// If the total bigger then zero show alert to the user after adjusting entries.
if (total > 0) {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
openDialog(DialogsName.InvoiceExchangeRateChange);
}, 500);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
return null;
},
);

View File

@@ -5,7 +5,7 @@ import intl from 'react-intl-universal';
import moment from 'moment';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
import { omit, first, sumBy } from 'lodash';
import { omit, first, sumBy, round } from 'lodash';
import {
compose,
transformToForm,
@@ -57,7 +57,7 @@ export const defaultInvoice = {
reference_no: '',
invoice_message: '',
terms_conditions: '',
exchange_rate: 1,
exchange_rate: '1',
currency_code: '',
branch_id: '',
warehouse_id: '',
@@ -398,3 +398,85 @@ export const useIsInvoiceTaxExclusive = () => {
return values.inclusive_exclusive_tax === TaxType.Exclusive;
};
/**
* Convert the given rate to the local currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const convertToForeignCurrency = (
rate: number,
exchangeRate: number,
) => {
return rate * exchangeRate;
};
/**
* Converts the given rate to the base currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
return rate / exchangeRate;
};
/**
* Reverts the given rate from the old exchange rate and covert it to the new
* currency based on the given new exchange rate.
* @param {number} rate -
* @param {number} oldExchangeRate - Old exchange rate.
* @param {number} newExchangeRate - New exchange rate.
* @returns {number}
*/
const revertAndConvertExchangeRate = (
rate: number,
oldExchangeRate: number,
newExchangeRate: number,
) => {
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
return round(newValue, 3);
};
/**
* Assign the new item entry rate after converting to the new exchange rate.
* @params {number} oldExchangeRate -
* @params {number} newExchangeRate -
* @params {IItemEntry} entries -
*/
const assignRateRevertAndCovertExchangeRate = R.curry(
(oldExchangeRate: number, newExchangeRate: number, entries: IItemEntry[]) => {
return entries.map((entry) => ({
...entry,
rate: revertAndConvertExchangeRate(
entry.rate,
oldExchangeRate,
newExchangeRate,
),
}));
},
);
/**
* Compose invoice entries on exchange rate change.
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
*/
export const useInvoiceEntriesOnExchangeRateChange = () => {
const {
values: { entries },
} = useFormikContext();
return React.useMemo(() => {
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
return R.compose(
// Updates entries total.
updateItemsEntriesTotal,
// Assign a new rate of the given new exchange rate from the old exchange rate.
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
)(entries);
});
}, [entries]);
};

View File

@@ -1,102 +1,29 @@
// @ts-nocheck
import { useMutation, useQueryClient } from 'react-query';
import { defaultTo } from 'lodash';
import { useQueryTenant } from '../useQueryRequest';
import { transformPagination } from '@/utils';
import useApiRequest from '../useRequest';
import { useQuery } from 'react-query';
import QUERY_TYPES from './types';
const defaultPagination = {
pageSize: 20,
page: 0,
pagesCount: 0,
};
/**
* Creates a new exchange rate.
*/
export function useCreateExchangeRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((values) => apiRequest.post('exchange_rates', values), {
onSuccess: () => {
queryClient.invalidateQueries('EXCHANGES_RATES');
},
...props,
});
function getRandomItemFromArray(arr) {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
/**
* Edits the exchange rate.
* Retrieves tax rates.
* @param {number} customerId - Customer id.
*/
export function useEdiExchangeRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`exchange_rates/${id}`, values),
{
onSuccess: () => {
queryClient.invalidateQueries('EXCHANGES_RATES');
},
...props,
},
);
}
/**
* Deletes the exchange rate.
*/
export function useDeleteExchangeRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`exchange_rates/${id}`), {
onSuccess: () => {
queryClient.invalidateQueries('EXCHANGES_RATES');
},
...props,
});
}
/**
* Retrieve the exchange rate list.
*/
export function useExchangeRates(query, props) {
const apiRequest = useApiRequest();
const states = useQueryTenant(
['EXCHANGES_RATES', query],
() => apiRequest.get('exchange_rates', { params: query }),
{
select: (res) => ({
exchangesRates: res.data.exchange_rates.results,
pagination: transformPagination(res.data.exchange_rates.pagination),
filterMeta: res.data.filter_meta,
export function useExchangeRate(
fromCurrency: string,
toCurrency: string,
props,
) {
return useQuery(
[QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency],
() =>
Promise.resolve({
from_currency: fromCurrency,
to_currency: toCurrency,
exchange_rate: getRandomItemFromArray([4.231, 2.231]),
}),
...props,
},
props,
);
return {
...states,
data: defaultTo(states.data, {
exchangesRates: [],
pagination: {
page: 1,
pageSize: 20,
total: 0,
},
filterMeta: {},
}),
};
}
export function useRefreshExchangeRate() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries('EXCHANGES_RATES');
},
};
}

View File

@@ -32,7 +32,7 @@ const FINANCIAL_REPORTS = {
REALIZED_GAIN_OR_LOSS: 'REALIZED_GAIN_OR_LOSS',
UNREALIZED_GAIN_OR_LOSS: 'UNREALIZED_GAIN_OR_LOSS',
PROJECT_PROFITABILITY_SUMMARY: 'PROJECT_PROFITABILITY_SUMMARY',
SALES_TAX_LIABILITY_SUMMARY: 'SALES_TAX_LIABILITY_SUMMARY'
SALES_TAX_LIABILITY_SUMMARY: 'SALES_TAX_LIABILITY_SUMMARY',
};
const BILLS = {
@@ -222,12 +222,17 @@ const DASHBOARD = {
};
const ORGANIZATION = {
ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES: 'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES',
ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES:
'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES',
};
export const TAX_RATES = {
TAX_RATES: 'TAX_RATES',
}
};
export const EXCHANGE_RATE = {
EXCHANGE_RATE: 'EXCHANGE_RATE',
};
export default {
...Authentication,
@@ -262,5 +267,6 @@ export default {
...BRANCHES,
...DASHBOARD,
...ORGANIZATION,
...TAX_RATES
...TAX_RATES,
...EXCHANGE_RATE,
};

View File

@@ -473,16 +473,6 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('all_financial_reports'),
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Exchange Rates
// {
// path: `/exchange-rates`,
// component: lazy(
// () => import('@/containers/ExchangeRates/ExchangeRatesList'),
// ),
// breadcrumb: intl.get('exchange_rates_list'),
// pageTitle: intl.get('exchange_rates_list'),
// subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
// },
// Expenses.
{
path: `/expenses/new`,