diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index fa41d397d..13553ae4b 100644 --- a/client/src/containers/Items/ItemForm.js +++ b/client/src/containers/Items/ItemForm.js @@ -6,7 +6,7 @@ import { } from '@blueprintjs/core'; import { queryCache } from 'react-query'; import { useHistory } from 'react-router-dom'; -import { pick } from 'lodash'; +import { pick, pickBy } from 'lodash'; import { useIntl } from 'react-intl'; import classNames from 'classnames'; @@ -23,7 +23,23 @@ import useMedia from 'hooks/useMedia'; import withItemDetail from 'containers/Items/withItemDetail'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; -import { compose } from 'utils'; +import { compose, transformToForm } from 'utils'; + +const defaultInitialValues = { + active: true, + name: '', + type: 'service', + sku: '', + cost_price: '', + sell_price: '', + cost_account_id: '', + sell_account_id: '', + inventory_account_id: '', + category_id: '', + note: '', + sellable: true, + purchasable: true, +}; /** * Item form. @@ -71,54 +87,61 @@ function ItemForm({ .required() .label(formatMessage({ id: 'item_type_' })), sku: Yup.string().trim(), - cost_price: Yup.number(), - sell_price: Yup.number(), + cost_price: Yup.number() + .when(['purchasable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), + sell_price: Yup.number() + .when(['sellable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), cost_account_id: Yup.number() - .required() + .when(['purchasable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }) .label(formatMessage({ id: 'cost_account_id' })), sell_account_id: Yup.number() - .required() + .when(['sellable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(), + }) .label(formatMessage({ id: 'sell_account_id' })), - inventory_account_id: Yup.number().when('type', { - is: (value) => value === 'inventory', - then: Yup.number().required(), - otherwise: Yup.number().nullable(), - }), - category_id: Yup.number().nullable(), + inventory_account_id: Yup.number() + .when(['type'], { + is: (value) => value === 'inventory', + then: Yup.number().required(), + otherwise: Yup.number().nullable(), + }) + .label(formatMessage({ id: 'Inventory account' })), + category_id: Yup.number().positive().nullable(), stock: Yup.string() || Yup.boolean(), sellable: Yup.boolean().required(), purchasable: Yup.boolean().required(), }); - const defaultInitialValues = useMemo( - () => ({ - active: true, - name: '', - type: 'service', - sku: '', - cost_price: 0, - sell_price: 0, - cost_account_id: null, - sell_account_id: null, - inventory_account_id: null, - category_id: null, - note: '', - sellable: true, - purchasable: true, - }), - [], - ); + + /** + * Initial values in create and edit mode. + */ const initialValues = useMemo( () => ({ - ...(itemDetail - ? { - ...pick(itemDetail, Object.keys(defaultInitialValues)), - } - : { - ...defaultInitialValues, - }), + ...defaultInitialValues, + + /** + * We only care about the fields in the form. Previously unfilled optional + * values such as `notes` come back from the API as null, so remove those + * as well. + */ + ...transformToForm(itemDetail, defaultInitialValues), }), - [itemDetail, defaultInitialValues], + [], ); useEffect(() => { @@ -141,7 +164,7 @@ function ItemForm({ 'the_item_has_been_successfully_edited', }, { - number: itemDetail.id, + number: itemId, }, ), intent: Intent.SUCCESS, @@ -156,7 +179,7 @@ function ItemForm({ if (isNewMode) { requestSubmitItem(form).then(onSuccess).catch(onError); } else { - requestEditItem(form).then(onSuccess).catch(onError); + requestEditItem(itemId, form).then(onSuccess).catch(onError); } }; diff --git a/client/src/containers/Items/ItemFormBody.js b/client/src/containers/Items/ItemFormBody.js index d1e5aa4a4..c9b07d671 100644 --- a/client/src/containers/Items/ItemFormBody.js +++ b/client/src/containers/Items/ItemFormBody.js @@ -36,15 +36,15 @@ function ItemFormBody({
- {/*------------- Sellable checkbox ------------- */} - + } + label={ +

+ +

+ } checked={values.sellable} {...getFieldProps('sellable')} /> @@ -54,26 +54,22 @@ function ItemFormBody({ } className={'form-group--item-selling-price'} - intent={ - errors.selling_price && touched.selling_price && Intent.DANGER - } + intent={errors.sell_price && touched.sell_price && Intent.DANGER} helperText={ - + } inline={true} > { - setFieldValue('selling_price', value); + setFieldValue('sell_price', value); }} inputGroupProps={{ medium: true, intent: - errors.selling_price && - touched.selling_price && - Intent.DANGER, + errors.sell_price && touched.sell_price && Intent.DANGER, }} disabled={!values.sellable} /> @@ -107,18 +103,18 @@ function ItemFormBody({ filterByTypes={['income']} /> - {/*------------- Purchasable checbox ------------- */} - + } + label={ +

+ +

+ } defaultChecked={values.purchasable} {...getFieldProps('purchasable')} /> @@ -169,7 +165,7 @@ function ItemFormBody({ { - setFieldValue('cost_account_id', account.id) + setFieldValue('cost_account_id', account.id); }} defaultSelectText={} selectedAccountId={values.cost_account_id} @@ -187,4 +183,4 @@ export default compose( withAccounts(({ accountsList }) => ({ accountsList, })), -)(ItemFormBody); \ No newline at end of file +)(ItemFormBody); diff --git a/client/src/containers/Items/ItemFormInventorySection.js b/client/src/containers/Items/ItemFormInventorySection.js index ab6db5888..6321e7200 100644 --- a/client/src/containers/Items/ItemFormInventorySection.js +++ b/client/src/containers/Items/ItemFormInventorySection.js @@ -1,11 +1,17 @@ import React from 'react'; -import { FormGroup, Intent, InputGroup, Classes } from '@blueprintjs/core'; -import { AccountsSelectList, ErrorMessage, Col, Row } from 'components'; +import { + FormGroup, + Intent, + InputGroup, + Position, +} from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import { AccountsSelectList, ErrorMessage, Col, Row, Hint } from 'components'; +import { CLASSES } from 'common/classes'; import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; import withAccounts from 'containers/Accounts/withAccounts'; - -import { compose } from 'utils'; +import { compose, tansformDateValue, momentFormatter } from 'utils'; /** * Item form inventory sections. @@ -21,12 +27,12 @@ function ItemFormInventorySection({ }) { return (
+

+ +

+ -

- -

- {/*------------- Inventory account ------------- */} } @@ -45,7 +51,7 @@ function ItemFormInventorySection({ className={classNames( 'form-group--item-inventory_account', 'form-group--select-list', - Classes.FILL, + CLASSES.FILL, )} > } - className={'form-group--item-stock'} + label={} + labelInfo={} + className={'form-group--opening_quantity'} inline={true} > } + labelInfo={} + className={classNames( + 'form-group--select-list', + 'form-group--opening_date', + CLASSES.FILL, + )} + inline={true} + > + { + setFieldValue('opening_date', value); + }} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + /> + + + + + } + className={'form-group--opening_average_rate'} inline={true} > diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js index 99957e731..f5843cbce 100644 --- a/client/src/containers/Items/ItemFormPrimarySection.js +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -39,6 +39,14 @@ function ItemFormPrimarySection({ // #withItemCategories categoriesList, }) { + + const itemTypeHintContent = ( + <> +
{'Service: '}{'Services that you provide to customers. '}
+
{'Inventory: '}{'Products you buy and/or sell and that you track quantities of.'}
+
{'Non-Inventory: '}{'Products you buy and/or sell but don’t need to (or can’t) track quantities of, for example, nuts and bolts used in an installation.'}
+ ); + return (
@@ -47,7 +55,12 @@ function ItemFormPrimarySection({ } - labelInfo={} + labelInfo={ + + + + + } className={'form-group--item-type'} intent={errors.type && touched.type && Intent.DANGER} helperText={} @@ -56,48 +69,20 @@ function ItemFormPrimarySection({ { - setFieldValue('item_type', value); + setFieldValue('type', value); })} - selectedValue={values.item_type} + selectedValue={values.type} > - - - } + label={} value="service" /> - - - } + label={} value="inventory" /> - - - } + label={} value="non_inventory" /> @@ -121,7 +106,7 @@ function ItemFormPrimarySection({ {/*----------- SKU ----------*/} } + label={} labelInfo={} className={'form-group--item-sku'} intent={errors.sku && touched.sku && Intent.DANGER} diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index d6fbc28f7..f719a047e 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -100,7 +100,7 @@ export default { cost_price: 'Cost Price', inventory_information: 'Inventory Information', inventory_account: 'Inventory Account', - opening_stock: 'Opening Stock', + opening_quantity: 'Opening quantity', save: 'Save', save_as_draft: 'Save as Draft', active: 'Active', @@ -810,4 +810,6 @@ export default { i_purchase_this_item: 'I purchase this item from a vendor.', i_sell_this_item: 'I sell this item to a customer.', select_display_name_as:'Select display name as', + opening_date: 'Opening date', + item_code: 'Item code', }; diff --git a/client/src/utils.js b/client/src/utils.js index 7c2b2f7fa..8abe4c145 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -277,4 +277,11 @@ export const itemsStartWith = (items, char) => { export const saveInvoke = (func, ...rest) => { return func && func(...rest); +} + +export const transformToForm = (obj, emptyInitialValues) => { + return _.pickBy( + obj, + (val, key) => val !== null && Object.keys(emptyInitialValues).includes(key), + ) } \ No newline at end of file