fix: allocate landed cost on inventory items.

This commit is contained in:
a.bouhuolia
2021-12-04 16:32:37 +02:00
parent 008faaece6
commit d3c5131020
15 changed files with 277 additions and 77 deletions

View File

@@ -1,6 +1,16 @@
import React from 'react';
import { defaultTo, get } from 'lodash';
import { DialogContent } from 'components';
import { useBill, useCreateLandedCost } from 'hooks/query';
import {
useBill,
useCreateLandedCost,
useLandedCostTransaction,
} from 'hooks/query';
import {
getEntriesByTransactionId,
getCostTransactionById,
getTransactionEntryById,
} from './utils';
const AllocateLandedCostDialogContext = React.createContext();
@@ -13,22 +23,79 @@ function AllocateLandedCostDialogProvider({
dialogName,
...props
}) {
const [transactionsType, setTransactionsType] = React.useState(null);
const [transactionId, setTransactionId] = React.useState(null);
const [transactionEntryId, setTransactionEntryId] = React.useState(null);
// Handle fetch bill details.
const { isLoading: isBillLoading, data: bill } = useBill(billId, {
enabled: !!billId,
});
// Retrieve the landed cost transactions based on the given transactions type.
const {
data: { transactions: landedCostTransactions },
} = useLandedCostTransaction(transactionsType, {
enabled: !!transactionsType,
});
// Landed cost selected transaction.
const costTransaction = React.useMemo(
() =>
transactionId
? getCostTransactionById(transactionId, landedCostTransactions)
: null,
[transactionId, landedCostTransactions],
);
// Retrieve the cost transaction entry.
const costTransactionEntry = React.useMemo(
() =>
costTransaction && transactionEntryId
? getTransactionEntryById(costTransaction, transactionEntryId)
: null,
[costTransaction, transactionEntryId],
);
// Retrieve entries of the given transaction id.
const costTransactionEntries = React.useMemo(
() =>
transactionId
? getEntriesByTransactionId(landedCostTransactions, transactionId)
: [],
[landedCostTransactions, transactionId],
);
// Create landed cost mutations.
const { mutateAsync: createLandedCostMutate } = useCreateLandedCost();
// provider payload.
// Retrieve the unallocate cost amount of cost transaction.
const unallocatedCostAmount = defaultTo(
get(costTransactionEntry, 'unallocated_cost_amount'),
0,
);
// Retrieve the unallocate cost amount of cost transaction.
const formattedUnallocatedCostAmount = defaultTo(
get(costTransactionEntry, 'formatted_unallocated_cost_amount'),
0,
);
// Provider payload.
const provider = {
isBillLoading,
bill,
dialogName,
query,
createLandedCostMutate,
costTransaction,
costTransactionEntries,
transactionsType,
landedCostTransactions,
setTransactionsType,
setTransactionId,
setTransactionEntryId,
costTransactionEntry,
transactionEntryId,
transactionId,
billId,
unallocatedCostAmount,
formattedUnallocatedCostAmount,
};
return (

View File

@@ -1,8 +1,13 @@
import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { Intent, Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import styled from 'styled-components';
import {
DialogFooter,
DialogFooterActions,
FormattedMessage as T,
} from 'components';
import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
@@ -13,7 +18,8 @@ function AllocateLandedCostFloatingActions({
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
const { dialogName } = useAllocateLandedConstDialogContext();
const { dialogName, costTransactionEntry, formattedUnallocatedCostAmount } =
useAllocateLandedConstDialogContext();
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
@@ -21,22 +27,41 @@ function AllocateLandedCostFloatingActions({
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<DialogFooter>
<DialogFooterActions alignment={'left'}>
{costTransactionEntry && (
<UnallocatedAmount>
Unallocated cost Amount:{' '}
<strong>{formattedUnallocatedCostAmount}</strong>
</UnallocatedAmount>
)}
</DialogFooterActions>
<DialogFooterActions alignment={'right'}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
style={{ minWidth: '85px' }}
style={{ minWidth: '100px' }}
type="submit"
loading={isSubmitting}
>
{<T id={'save'} />}
</Button>
</div>
</div>
</DialogFooterActions>
</DialogFooter>
);
}
export default compose(withDialogActions)(AllocateLandedCostFloatingActions);
const UnallocatedAmount = styled.div`
color: #3f5278;
align-self: center;
strong {
color: #353535;
padding-left: 4px;
}
`;

View File

@@ -2,8 +2,6 @@ import React from 'react';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import moment from 'moment';
import { sumBy } from 'lodash';
import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss';
@@ -14,20 +12,19 @@ import AllocateLandedCostFormContent from './AllocateLandedCostFormContent';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose, transformToForm } from 'utils';
const defaultInitialItem = {
entry_id: '',
cost: '',
};
// Default form initial values.
const defaultInitialValues = {
transaction_type: 'Bill',
transaction_date: moment(new Date()).format('YYYY-MM-DD'),
transaction_id: '',
transaction_entry_id: '',
amount: '',
allocation_method: 'quantity',
items: [
{
entry_id: '',
cost: '',
},
],
items: [defaultInitialItem],
};
/**
@@ -37,8 +34,13 @@ function AllocateLandedCostForm({
// #withDialogActions
closeDialog,
}) {
const { dialogName, bill, billId, createLandedCostMutate } =
useAllocateLandedConstDialogContext();
const {
dialogName,
bill,
billId,
createLandedCostMutate,
unallocatedCostAmount,
} = useAllocateLandedConstDialogContext();
// Initial form values.
const initialValues = {
@@ -49,11 +51,10 @@ function AllocateLandedCostForm({
cost: '',
})),
};
const amount = sumBy(initialValues.items, 'amount');
// Handle form submit.
const handleFormSubmit = (values, { setSubmitting }) => {
setSubmitting(false);
setSubmitting(true);
// Filters the entries has no cost.
const entries = values.items
@@ -81,13 +82,16 @@ function AllocateLandedCostForm({
// Handle the request error.
const onError = () => {
setSubmitting(false);
AppToaster.show({ message: 'Something went wrong!', intent: Intent.DANGER });
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
};
createLandedCostMutate([billId, form]).then(onSuccess).catch(onError);
};
// Computed validation schema.
const validationSchema = AllocateLandedCostFormSchema(amount);
const validationSchema = AllocateLandedCostFormSchema(unallocatedCostAmount);
return (
<Formik

View File

@@ -1,14 +1,19 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
export const AllocateLandedCostFormSchema = (minAmount) =>
export const AllocateLandedCostFormSchema = (maxAmount) =>
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().max(minAmount).label(intl.get('amount')),
allocation_method: Yup.string().trim(),
transaction_type: Yup.string()
.required()
.label(intl.get('transaction_type')),
transaction_id: Yup.string()
.required()
.label(intl.get('transaction_number')),
transaction_entry_id: Yup.string()
.required()
.label(intl.get('transaction_line')),
amount: Yup.number().max(maxAmount).label(intl.get('amount')),
allocation_method: Yup.string().required().trim(),
items: Yup.array().of(
Yup.object().shape({
entry_id: Yup.number().nullable(),

View File

@@ -1,16 +1,37 @@
import React from 'react';
import { Form } from 'formik';
import { Form, useFormikContext } from 'formik';
import { FormObserver } from 'components';
import AllocateLandedCostFormFields from './AllocateLandedCostFormFields';
import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider';
import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions';
/**
* Allocate landed cost form content.
*/
export default function AllocateLandedCostFormContent() {
const { values } = useFormikContext();
// Allocate landed cost dialog context.
const { setTransactionsType, setTransactionId, setTransactionEntryId } =
useAllocateLandedConstDialogContext();
// Handle the form change.
const handleFormChange = (values) => {
if (values.transaction_type) {
setTransactionsType(values.transaction_type);
}
if (values.transaction_id) {
setTransactionId(values.transaction_id);
}
if (values.transaction_entry_id) {
setTransactionEntryId(values.transaction_entry_id);
}
};
return (
<Form>
<AllocateLandedCostFormFields />
<AllocateLandedCostFloatingActions />
<FormObserver values={values} onChange={handleFormChange} />
</Form>
);
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { FastField, Field, ErrorMessage, useFormikContext } from 'formik';
import { FastField, Field, ErrorMessage } from 'formik';
import {
Classes,
FormGroup,
@@ -14,31 +14,31 @@ import { inputIntent, handleStringChange } from 'utils';
import { FieldRequiredHint, ListSelect } from 'components';
import { CLASSES } from 'common/classes';
import allocateLandedCostType from 'common/allocateLandedCostType';
import { useLandedCostTransaction } from 'hooks/query';
import AllocateLandedCostFormBody from './AllocateLandedCostFormBody';
import { getEntriesByTransactionId, allocateCostToEntries } from './utils';
import {
transactionsSelectShouldUpdate,
allocateCostToEntries,
resetAllocatedCostEntries,
} from './utils';
import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider';
/**
* Allocate landed cost form fields.
*/
export default function AllocateLandedCostFormFields() {
const { values } = useFormikContext();
const {
data: { transactions },
} = useLandedCostTransaction(values.transaction_type);
// Retrieve entries of the given transaction id.
const transactionEntries = React.useMemo(
() => getEntriesByTransactionId(transactions, values.transaction_id),
[transactions, values.transaction_id],
);
// Allocated landed cost dialog.
const { costTransactionEntries, landedCostTransactions } =
useAllocateLandedConstDialogContext();
return (
<div className={Classes.DIALOG_BODY}>
{/*------------Transaction type -----------*/}
<FastField name={'transaction_type'}>
<FastField
name={'transaction_type'}
transactions={allocateLandedCostType}
shouldUpdate={transactionsSelectShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
@@ -55,9 +55,14 @@ export default function AllocateLandedCostFormFields() {
<ListSelect
items={allocateLandedCostType}
onItemSelect={(type) => {
const { items } = values;
setFieldValue('transaction_type', type.value);
setFieldValue('transaction_id', '');
setFieldValue('transaction_entry_id', '');
setFieldValue('amount', '');
setFieldValue('items', resetAllocatedCostEntries(items));
}}
filterable={false}
selectedItem={value}
@@ -70,7 +75,11 @@ export default function AllocateLandedCostFormFields() {
</FastField>
{/*------------ Transaction -----------*/}
<Field name={'transaction_id'}>
<Field
name={'transaction_id'}
transactions={landedCostTransactions}
shouldUpdate={transactionsSelectShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_id'} />}
@@ -81,10 +90,14 @@ export default function AllocateLandedCostFormFields() {
inline={true}
>
<ListSelect
items={transactions}
items={landedCostTransactions}
onItemSelect={({ id }) => {
const { items } = form.values;
form.setFieldValue('transaction_id', id);
form.setFieldValue('transaction_entry_id', '');
form.setFieldValue('amount', '');
form.setFieldValue('items', resetAllocatedCostEntries(items));
}}
filterable={false}
selectedItem={value}
@@ -99,8 +112,12 @@ export default function AllocateLandedCostFormFields() {
</Field>
{/*------------ Transaction line -----------*/}
<If condition={transactionEntries.length > 0}>
<Field name={'transaction_entry_id'}>
<If condition={costTransactionEntries.length > 0}>
<Field
name={'transaction_entry_id'}
transactions={costTransactionEntries}
shouldUpdate={transactionsSelectShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_line'} />}
@@ -113,16 +130,20 @@ export default function AllocateLandedCostFormFields() {
inline={true}
>
<ListSelect
items={transactionEntries}
onItemSelect={({ id, amount }) => {
items={costTransactionEntries}
onItemSelect={({ id, unallocated_cost_amount }) => {
const { items, allocation_method } = form.values;
form.setFieldValue('amount', amount);
form.setFieldValue('transaction_entry_id', id);
form.setFieldValue('amount', unallocated_cost_amount);
form.setFieldValue(
'items',
allocateCostToEntries(amount, allocation_method, items),
allocateCostToEntries(
unallocated_cost_amount,
allocation_method,
items,
),
);
}}
filterable={false}
@@ -177,12 +198,12 @@ export default function AllocateLandedCostFormFields() {
>
<RadioGroup
onChange={handleStringChange((_value) => {
const { amount, items, allocation_method } = form.values;
const { amount, items } = form.values;
form.setFieldValue('allocation_method', _value);
form.setFieldValue(
'items',
allocateCostToEntries(amount, allocation_method, items),
allocateCostToEntries(amount, _value, items),
);
})}
selectedValue={value}

View File

@@ -1,5 +1,14 @@
import { sumBy, round } from 'lodash';
import * as R from 'ramda';
import { defaultFastFieldShouldUpdate } from 'utils';
/**
* Retrieve the landed cost transaction by the given id.
*/
export function getCostTransactionById(id, transactions) {
return transactions.find((trans) => trans.id === id);
}
/**
* Retrieve transaction entries of the given transaction id.
*/
@@ -8,6 +17,10 @@ export function getEntriesByTransactionId(transactions, id) {
return transaction ? transaction.entries : [];
}
export function getTransactionEntryById(transaction, transactionEntryId) {
return transaction.entries.find((entry) => entry.id === transactionEntryId);
}
export function allocateCostToEntries(total, allocateType, entries) {
return R.compose(
R.when(
@@ -60,3 +73,18 @@ export function allocateCostByQuantity(total, entries) {
cost: round(entry.percentageOfQuantity * total, 2),
}));
}
/**
* Detarmines the transactions selet field when should update.
*/
export function transactionsSelectShouldUpdate(newProps, oldProps) {
return (
newProps.transactions !== oldProps.transactions ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
}
export function resetAllocatedCostEntries(entries) {
return entries.map((entry) => ({ ...entry, cost: 0 }));
}

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { Intent, Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { DialogFooterActions, FormattedMessage as T } from 'components';
import {
DialogFooter,
DialogFooterActions,
FormattedMessage as T,
} from 'components';
import { useSMSMessageDialogContext } from './SMSMessageDialogProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
@@ -28,7 +31,7 @@ function SMSMessageFormFloatingActions({
};
return (
<div className={Classes.DIALOG_FOOTER}>
<DialogFooter>
<DialogFooterActions alignment={'left'}>
<Button
intent={Intent.PRIMARY}
@@ -42,7 +45,7 @@ function SMSMessageFormFloatingActions({
<T id={'cancel'} />
</Button>
</DialogFooterActions>
</div>
</DialogFooter>
);
}