mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: assign default sell/purchase tax rates to items (#261)
This commit is contained in:
62
packages/webapp/src/components/TaxRates/TaxRatesSelect.tsx
Normal file
62
packages/webapp/src/components/TaxRates/TaxRatesSelect.tsx
Normal 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);
|
||||
@@ -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')}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user