feat: assign default sell/purchase tax rates to items (#261)

This commit is contained in:
Ahmed Bouhuolia
2023-10-08 23:55:59 +02:00
committed by GitHub
parent d40de4d22b
commit 1ed1c9ea1d
25 changed files with 400 additions and 18 deletions

View File

@@ -0,0 +1,62 @@
// @ts-nocheck
import * as R from 'ramda';
import intl from 'react-intl-universal';
import { FSelect } from '@/components';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MenuItem } from '@blueprintjs/core';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => ({ name });
/**
* Tax rates select field binded with Formik form.
* @returns {JSX.Element}
*/
function TaxRatesSelectRoot({
// #withDialogActions
openDialog,
// #ownProps
allowCreate,
...restProps
}) {
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
// Handles the create item click.
const handleCreateItemClick = () => {
openDialog(DialogsName.TaxRateForm);
};
return (
<FSelect
valueAccessor={'id'}
labelAccessor={'code'}
textAccessor={'name_formatted'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
onCreateItemSelect={handleCreateItemClick}
{...restProps}
/>
);
}
export const TaxRatesSelect = R.compose(withDialogActions)(TaxRatesSelectRoot);

View File

@@ -67,6 +67,14 @@ export default function ItemDetailHeader() {
label={intl.get('cost_account_id')}
children={defaultTo(item.cost_account?.name, '-')}
/>
<DetailItem
label={intl.get('item.details.sell_tax_rate')}
children={item?.sell_tax_rate?.name}
/>
<DetailItem
label={intl.get('item.details.purchase_tax_rate')}
children={item?.purchase_tax_rate?.name}
/>
<If condition={item.type === 'inventory'}>
<DetailItem
label={intl.get('inventory_account')}

View File

@@ -8,9 +8,9 @@ import { DataTableEditable } from '@/components';
import { useEditableItemsEntriesColumns } from './components';
import {
useFetchItemRow,
composeRowsOnNewRow,
useComposeRowsOnEditTableCell,
useComposeRowsOnRemoveTableRow,
useComposeRowsOnNewRow,
} from './utils';
import {
ItemEntriesTableProvider,
@@ -61,6 +61,7 @@ function ItemEntriesTableRoot() {
currencyCode,
landedCost,
taxRates,
itemType,
} = useItemEntriesTableContext();
// Editiable items entries columns.
@@ -68,11 +69,12 @@ function ItemEntriesTableRoot() {
const composeRowsOnEditCell = useComposeRowsOnEditTableCell();
const composeRowsOnDeleteRow = useComposeRowsOnRemoveTableRow();
const composeRowsOnNewRow = useComposeRowsOnNewRow();
// Handle the fetch item row details.
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
landedCost,
itemType: null,
itemType,
notifyNewRow: (newRow, rowIndex) => {
// Update the rate, description and quantity data of the row.
const newRows = composeRowsOnNewRow(rowIndex, newRow, localValue);

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash';
import { useItem } from '@/hooks/query';
@@ -116,19 +116,20 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
? item.purchase_description
: item.sell_description;
// Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item);
const taxRateId =
itemType === ITEM_TYPE.PURCHASABLE
? item.purchase_tax_rate_id
: item.sell_tax_rate_id;
// Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item);
// The new row.
const newRow = {
rate: price,
description,
quantity: 1,
tax_rate_id: taxRateId,
...(landedCost
? {
landed_cost: false,
@@ -164,13 +165,21 @@ export const composeRowsOnEditCell = R.curry(
/**
* Compose table rows when insert a new row to table rows.
*/
export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
return compose(
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});
export const useComposeRowsOnNewRow = () => {
const { taxRates, isInclusiveTax } = useItemEntriesTableContext();
return React.useMemo(() => {
return R.curry((rowIndex, newRow, rows) => {
return compose(
assignEntriesTaxAmount(isInclusiveTax),
assignEntriesTaxRate(taxRates),
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});
}, [isInclusiveTax, taxRates]);
};
/**
* Associate tax rate to entries.

View File

@@ -29,14 +29,16 @@ import {
costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate,
taxRateFieldShouldUpdate,
} from './utils';
import { compose, inputIntent } from '@/utils';
import { TaxRatesSelect } from '@/components/TaxRates/TaxRatesSelect';
/**
* Item form body.
*/
function ItemFormBody({ organization: { base_currency } }) {
const { accounts } = useItemFormContext();
const { accounts, taxRates } = useItemFormContext();
const { values } = useFormikContext();
return (
@@ -111,7 +113,20 @@ function ItemFormBody({ organization: { base_currency } }) {
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
fill={true}
allowCreate={true}
fastField={true}
fastField={true}
/>
</FFormGroup>
{/*------------- Sell Tax Rate ------------- */}
<FFormGroup
name={'sell_tax_rate_id'}
label={'Tax Rate'}
inline={true}
>
<TaxRatesSelect
name={'sell_tax_rate_id'}
items={taxRates}
allowCreate
/>
</FFormGroup>
@@ -213,6 +228,24 @@ function ItemFormBody({ organization: { base_currency } }) {
/>
</FFormGroup>
{/*------------- Purchase Tax Rate ------------- */}
<FFormGroup
name={'purchase_tax_rate_id'}
label={'Tax Rate'}
inline={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
shouldUpdate={taxRateFieldShouldUpdate}
>
<TaxRatesSelect
name={'purchase_tax_rate_id'}
items={taxRates}
allowCreate={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
/>
</FFormGroup>
<FastField
name={'purchase_description'}
purchasable={values.purchasable}

View File

@@ -10,6 +10,7 @@ import {
useAccounts,
} from '@/hooks/query';
import { useWatchItemError } from './utils';
import { useTaxRates } from '@/hooks/query/taxRates';
const ItemFormContext = createContext();
@@ -30,6 +31,8 @@ function ItemFormProvider({ itemId, ...props }) {
data: { itemsCategories },
} = useItemsCategories();
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
// Fetches the given item details.
const itemQuery = useItem(itemId || duplicateId, {
enabled: !!itemId || !!duplicateId,
@@ -69,6 +72,7 @@ function ItemFormProvider({ itemId, ...props }) {
accounts,
item,
itemsCategories,
taxRates,
submitPayload,
isNewMode,
@@ -76,6 +80,7 @@ function ItemFormProvider({ itemId, ...props }) {
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
isTaxRatesLoading,
createItemMutate,
editItemMutate,

View File

@@ -23,12 +23,14 @@ const defaultInitialValues = {
sell_price: '',
cost_account_id: '',
sell_account_id: '',
sell_tax_rate_id: '',
inventory_account_id: '',
category_id: '',
sellable: 1,
purchasable: true,
sell_description: '',
purchase_description: '',
purchase_tax_rate_id: '',
};
/**
@@ -187,6 +189,13 @@ export const purchaseDescFieldShouldUpdate = (newProps, oldProps) => {
);
};
export const taxRateFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.shouldUpdateDeps.taxRates !== oldProps.shouldUpdateDeps.taxRates ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export function transformItemsTableState(tableState) {
return {
...transformTableStateToQuery(tableState),

View File

@@ -5,6 +5,7 @@ import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import { entriesFieldShouldUpdate } from './utils';
import { TaxType } from '@/interfaces/TaxRates';
import { ITEM_TYPE } from '@/containers/Entries/utils';
/**
* Invoice items entries editor field.
@@ -31,6 +32,7 @@ export default function InvoiceItemsEntriesEditorField() {
}}
items={items}
taxRates={taxRates}
itemType={ITEM_TYPE.SELLABLE}
errors={error}
linesNumber={4}
currencyCode={values.currency_code}

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import { DialogContent } from '@/components';
import { useTaxRate, useTaxRates } from '@/hooks/query/taxRates';
import { useTaxRate } from '@/hooks/query/taxRates';
import { DialogsName } from '@/constants/dialogs';
const TaxRateFormDialogContext = React.createContext();

View File

@@ -59,6 +59,8 @@ export function useEditTaxRate(props) {
onSuccess: (res, id) => {
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries(QUERY_TYPES.ITEM);
queryClient.invalidateQueries(QUERY_TYPES.ITEMS);
},
...props,
},

View File

@@ -334,6 +334,8 @@
"currency_name_": "Currency name",
"cost_account_id": "Cost account",
"sell_account_id": "Sell account",
"item.details.sell_tax_rate": "Sell Tax Rate",
"item.details.purchase_tax_rate": "Purchase Tax Rate",
"item_type_": "Item type",
"item_name_": "Item name",
"organization_industry_": "Organization industry",