diff --git a/client/src/common/allocateLandedCostType.js b/client/src/common/allocateLandedCostType.js
new file mode 100644
index 000000000..4e915bfec
--- /dev/null
+++ b/client/src/common/allocateLandedCostType.js
@@ -0,0 +1,6 @@
+import intl from 'react-intl-universal';
+
+export default [
+ { name: intl.get('bills'), value: 'bills' },
+ { name: intl.get('expenses'), value: 'expenses' },
+]
\ No newline at end of file
diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js
index 1419f7638..203a769b6 100644
--- a/client/src/components/DialogsContainer.js
+++ b/client/src/components/DialogsContainer.js
@@ -13,6 +13,7 @@ import KeyboardShortcutsDialog from 'containers/Dialogs/keyboardShortcutsDialog'
import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog';
import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog';
import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog';
+import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog';
/**
* Dialogs container.
@@ -32,6 +33,7 @@ export default function DialogsContainer() {
+
);
}
diff --git a/client/src/components/index.js b/client/src/components/index.js
index c8d56dfe9..8567a384b 100644
--- a/client/src/components/index.js
+++ b/client/src/components/index.js
@@ -56,6 +56,7 @@ import DrawerHeaderContent from './Drawer/DrawerHeaderContent';
import Postbox from './Postbox';
import AccountsSuggestField from './AccountsSuggestField';
import MaterialProgressBar from './MaterialProgressBar';
+import { MoneyFieldCell } from './DataTableCells';
const Hint = FieldHint;
@@ -123,4 +124,5 @@ export {
Postbox,
AccountsSuggestField,
MaterialProgressBar,
+ MoneyFieldCell,
};
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js
new file mode 100644
index 000000000..1f574d9fa
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { AllocateLandedCostDialogProvider } from './AllocateLandedCostDialogProvider';
+import AllocateLandedCostForm from './AllocateLandedCostForm';
+
+/**
+ * Allocate landed cost dialog content.
+ */
+export default function AllocateLandedCostDialogContent({
+ // #ownProps
+ dialogName,
+ bill,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js
new file mode 100644
index 000000000..aec4f85eb
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { DialogContent } from 'components';
+import { useBill } from 'hooks/query';
+
+import { pick } from 'lodash';
+
+const AllocateLandedCostDialogContext = React.createContext();
+
+/**
+ * Allocate landed cost provider.
+ */
+function AllocateLandedCostDialogProvider({ billId, dialogName, ...props }) {
+ // Handle fetch bill details.
+ const { isLoading: isBillLoading, data: bill } = useBill(billId, {
+ enabled: !!billId,
+ });
+
+ // provider payload.
+ const provider = {
+ bill: {
+ ...pick(bill, ['entries']),
+ },
+ dialogName,
+ };
+ return (
+
+
+
+ );
+}
+
+const useAllocateLandedConstDialogContext = () =>
+ React.useContext(AllocateLandedCostDialogContext);
+
+export {
+ AllocateLandedCostDialogProvider,
+ useAllocateLandedConstDialogContext,
+};
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js
new file mode 100644
index 000000000..2275883ad
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { DataTable, MoneyFieldCell, DataTableEditable } from 'components';
+import { compose, updateTableRow } from 'utils';
+
+/**
+ * Allocate landed cost entries table.
+ */
+export default function AllocateLandedCostEntriesTable({
+ onUpdateData,
+ entries,
+}) {
+ // allocate landed cost entries table columns.
+ const columns = React.useMemo(
+ () => [
+ {
+ Header: intl.get('item'),
+ accessor: 'item_id',
+ disableSortBy: true,
+ width: '150',
+ },
+ {
+ Header: intl.get('quantity'),
+ accessor: 'quantity',
+ disableSortBy: true,
+ width: '100',
+ },
+ {
+ Header: intl.get('rate'),
+ accessor: 'rate',
+ disableSortBy: true,
+ width: '100',
+ },
+ {
+ Header: intl.get('amount'),
+ accessor: 'amount',
+ disableSortBy: true,
+ width: '100',
+ },
+ {
+ Header: intl.get('cost'),
+ accessor: 'cost',
+ width: '150',
+ Cell: MoneyFieldCell,
+ disableSortBy: true,
+ },
+ ],
+ [],
+ );
+
+ // Handle update data.
+ const handleUpdateData = React.useCallback(
+ (rowIndex, columnId, value) => {
+ const newRows = compose(updateTableRow(rowIndex, columnId, value))(
+ entries,
+ );
+ onUpdateData(newRows);
+ },
+ [onUpdateData, entries],
+ );
+
+ const LL = [
+ {
+ item_id: 'ITEM',
+ quantity: '30000',
+ rate: '100000',
+ amount: '400',
+ },
+ ];
+
+ return ;
+}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js
new file mode 100644
index 000000000..ddc400903
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+import { FormattedMessage as T } from 'components';
+
+import { useFormikContext } from 'formik';
+import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+function AllocateLandedCostFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ const { dialogName } = useAllocateLandedConstDialogContext();
+
+ // Handle cancel button click.
+ const handleCancelBtnClick = (event) => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(AllocateLandedCostFloatingActions);
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js
new file mode 100644
index 000000000..5ccffca48
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Formik } from 'formik';
+import moment from 'moment';
+
+import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss';
+
+import { AllocateLandedCostFormSchema } from './AllocateLandedCostForm.schema';
+import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider';
+import AllocateLandedCostFormContent from './AllocateLandedCostFormContent';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+import { compose } from 'utils';
+
+const defaultInitialValues = {
+ transaction_type: 'bills',
+ transaction_date: moment(new Date()).format('YYYY-MM-DD'),
+ transaction_id: '',
+ transaction_entry_id: '',
+ amount: '',
+ allocation_method: 'quantity',
+ entries: {
+ entry_id: '',
+ cost: '',
+ },
+};
+
+/**
+ * Allocate landed cost form.
+ */
+function AllocateLandedCostForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ const { bill, dialogName } = useAllocateLandedConstDialogContext();
+
+ // Initial form values.
+ const initialValues = {
+ ...defaultInitialValues,
+ ...bill,
+ };
+
+
+ // Handle form submit.
+ const handleFormSubmit = (values, { setSubmitting }) => {};
+
+ return (
+
+
+
+ );
+}
+
+export default compose(withDialogActions)(AllocateLandedCostForm);
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js
new file mode 100644
index 000000000..176514808
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js
@@ -0,0 +1,19 @@
+import * as Yup from 'yup';
+import intl from 'react-intl-universal';
+
+const Schema = Yup.object().shape({
+ transaction_type: Yup.string().label(intl.get('transaction_type')),
+ transaction_date: Yup.date().label(intl.get('transaction_date')),
+ transaction_id: Yup.string().label(intl.get('transaction_number')),
+ transaction_entry_id: Yup.string().label(intl.get('transaction_line')),
+ amount: Yup.number().label(intl.get('amount')),
+ allocation_method: Yup.string().trim(),
+ entries: Yup.array().of(
+ Yup.object().shape({
+ entry_id: Yup.number().nullable(),
+ cost: Yup.number().nullable(),
+ }),
+ ),
+});
+
+export const AllocateLandedCostFormSchema = Schema;
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js
new file mode 100644
index 000000000..13029efd5
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import { FastField } from 'formik';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import AllocateLandedCostEntriesTable from './AllocateLandedCostEntriesTable';
+
+export default function AllocateLandedCostFormBody() {
+ return (
+
+
+ {({
+ form: { setFieldValue, values },
+ field: { value },
+ meta: { error, touched },
+ }) => (
+ {
+ setFieldValue('entries', newEntries);
+ }}
+ />
+ )}
+
+
+ );
+}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js
new file mode 100644
index 000000000..c06d05a67
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Form } from 'formik';
+import AllocateLandedCostFormFields from './AllocateLandedCostFormFields';
+import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions';
+/**
+ * Allocate landed cost form content.
+ */
+export default function AllocateLandedCostFormContent() {
+ return (
+
+ );
+}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js
new file mode 100644
index 000000000..a1639ca96
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js
@@ -0,0 +1,171 @@
+import React from 'react';
+import { FastField, ErrorMessage } from 'formik';
+import {
+ Classes,
+ FormGroup,
+ RadioGroup,
+ Radio,
+ InputGroup,
+ Position,
+} from '@blueprintjs/core';
+import { DateInput } from '@blueprintjs/datetime';
+import classNames from 'classnames';
+import { FormattedMessage as T } from 'components';
+import intl from 'react-intl-universal';
+import {
+ inputIntent,
+ momentFormatter,
+ tansformDateValue,
+ handleDateChange,
+ handleStringChange,
+} from 'utils';
+import { FieldRequiredHint, ListSelect } from 'components';
+import { CLASSES } from 'common/classes';
+import allocateLandedCostType from 'common/allocateLandedCostType';
+import AccountsSuggestField from 'components/AccountsSuggestField';
+import AllocateLandedCostFormBody from './AllocateLandedCostFormBody';
+
+/**
+ * Allocate landed cost form fields.
+ */
+export default function AllocateLandedCostFormFields() {
+ return (
+
+ {/*------------Transaction type -----------*/}
+
+ {({
+ form: { values, setFieldValue },
+ field: { value },
+ meta: { error, touched },
+ }) => (
+ }
+ labelInfo={}
+ helperText={}
+ intent={inputIntent({ error, touched })}
+ inline={true}
+ className={classNames(CLASSES.FILL, 'form-group--transaction_type')}
+ >
+ {
+ setFieldValue('transaction_type', type.value);
+ }}
+ filterable={false}
+ selectedItem={value}
+ selectedItemProp={'value'}
+ textProp={'name'}
+ popoverProps={{ minimal: true }}
+ />
+
+ )}
+
+
+ {/*------------Transaction date -----------*/}
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+ }
+ // labelInfo={}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ minimal={true}
+ className={classNames(CLASSES.FILL, 'form-group--transaction_date')}
+ inline={true}
+ >
+ {
+ form.setFieldValue('transaction_date', formattedDate);
+ })}
+ value={tansformDateValue(value)}
+ popoverProps={{
+ position: Position.BOTTOM,
+ minimal: true,
+ }}
+ />
+
+ )}
+
+ {/*------------ Transaction -----------*/}
+
+ {({ form, field, meta: { error, touched } }) => (
+ }
+ // labelInfo={}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ className={'form-group--transaction_id'}
+ inline={true}
+ >
+
+ form.setFieldValue('transaction_id', id)
+ }
+ inputProps={{
+ placeholder: intl.get('select_transaction'),
+ }}
+ />
+
+ )}
+
+ {/*------------ Transaction line -----------*/}
+
+ {({ form, field, meta: { error, touched } }) => (
+ }
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ className={'form-group--transaction_entry_id'}
+ inline={true}
+ >
+
+
+ )}
+
+ {/*------------ Amount -----------*/}
+
+ {({ form, field, meta: { error, touched } }) => (
+ }
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ className={'form-group--amount'}
+ inline={true}
+ >
+
+
+ )}
+
+ {/*------------ Allocation method -----------*/}
+
+ {({ form, field: { value }, meta: { touched, error } }) => (
+ }
+ labelInfo={}
+ className={'form-group--allocation_method'}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ inline={true}
+ >
+ {
+ form.setFieldValue('allocation_method', _value);
+ })}
+ selectedValue={value}
+ inline={true}
+ >
+ } value="quantity" />
+ } value="valuation" />
+
+
+ )}
+
+
+ {/*------------ Allocate Landed cost Table -----------*/}
+
+
+ );
+}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js
new file mode 100644
index 000000000..5798a5aba
--- /dev/null
+++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js
@@ -0,0 +1,36 @@
+import React, { lazy } from 'react';
+import { FormattedMessage as T, Dialog, DialogSuspense } from 'components';
+import withDialogRedux from 'components/DialogReduxConnect';
+import { compose } from 'utils';
+
+const AllocateLandedCostDialogContent = lazy(() =>
+ import('./AllocateLandedCostDialogContent'),
+);
+
+/**
+ * Allocate landed cost dialog.
+ */
+function AllocateLandedCostDialog({
+ dialogName,
+ payload = { billId: null },
+ isOpen,
+}) {
+ return (
+ }
+ canEscapeKeyClose={true}
+ isOpen={isOpen}
+ className="dialog--allocate-landed-cost-form"
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(AllocateLandedCostDialog);
diff --git a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js
index cbf514012..03f49bb09 100644
--- a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js
+++ b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js
@@ -34,13 +34,8 @@ function BillsDataTable({
openDialog,
}) {
// Bills list context.
- const {
- bills,
- pagination,
- isBillsLoading,
- isBillsFetching,
- isEmptyStatus,
- } = useBillsListContext();
+ const { bills, pagination, isBillsLoading, isBillsFetching, isEmptyStatus } =
+ useBillsListContext();
const history = useHistory();
@@ -78,6 +73,11 @@ function BillsDataTable({
openDialog('quick-payment-made', { billId: id });
};
+ // handle allocate landed cost.
+ const handleAllocateLandedCost = ({ id }) => {
+ openDialog('allocate-landed-cost', { billId: id });
+ };
+
if (isEmptyStatus) {
return ;
}
@@ -105,6 +105,7 @@ function BillsDataTable({
onEdit: handleEditBill,
onOpen: handleOpenBill,
onQuick: handleQuickPaymentMade,
+ onAllocateLandedCost: handleAllocateLandedCost,
}}
/>
);
diff --git a/client/src/containers/Purchases/Bills/BillsLanding/components.js b/client/src/containers/Purchases/Bills/BillsLanding/components.js
index 34d299000..fa4c2a76b 100644
--- a/client/src/containers/Purchases/Bills/BillsLanding/components.js
+++ b/client/src/containers/Purchases/Bills/BillsLanding/components.js
@@ -20,7 +20,7 @@ import moment from 'moment';
* Actions menu.
*/
export function ActionsMenu({
- payload: { onEdit, onOpen, onDelete, onQuick },
+ payload: { onEdit, onOpen, onDelete, onQuick, onAllocateLandedCost },
row: { original },
}) {
return (
@@ -50,7 +50,11 @@ export function ActionsMenu({
onClick={safeCallback(onQuick, original)}
/>
-
+ }
+ text={intl.get('allocate_landed_coast')}
+ onClick={safeCallback(onAllocateLandedCost, original)}
+ />