mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
feat(webapp): invoice tax rate
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const ItemEntriesTableContext = createContext();
|
||||
|
||||
function ItemEntriesTableProvider({ children, value }) {
|
||||
const provider = {
|
||||
...value,
|
||||
};
|
||||
return (
|
||||
<ItemEntriesTableContext.Provider value={provider}>
|
||||
{children}
|
||||
</ItemEntriesTableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useItemEntriesTableContext = () =>
|
||||
React.useContext(ItemEntriesTableContext);
|
||||
|
||||
export { ItemEntriesTableProvider, useItemEntriesTableContext };
|
||||
@@ -1,103 +1,104 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { DataTableEditable } from '@/components';
|
||||
|
||||
import { useEditableItemsEntriesColumns } from './components';
|
||||
import {
|
||||
saveInvoke,
|
||||
compose,
|
||||
updateMinEntriesLines,
|
||||
updateRemoveLineByIndex,
|
||||
} from '@/utils';
|
||||
import {
|
||||
useFetchItemRow,
|
||||
composeRowsOnNewRow,
|
||||
composeRowsOnEditCell,
|
||||
useComposeRowsOnEditTableCell,
|
||||
useComposeRowsOnRemoveTableRow,
|
||||
} from './utils';
|
||||
import {
|
||||
ItemEntriesTableProvider,
|
||||
useItemEntriesTableContext,
|
||||
} from './ItemEntriesTableProvider';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
|
||||
/**
|
||||
* Items entries table.
|
||||
*/
|
||||
function ItemsEntriesTable({
|
||||
// #ownProps
|
||||
items,
|
||||
entries,
|
||||
initialEntries,
|
||||
defaultEntry,
|
||||
errors,
|
||||
onUpdateData,
|
||||
currencyCode,
|
||||
itemType, // sellable or purchasable
|
||||
landedCost = false,
|
||||
minLinesNumber
|
||||
}) {
|
||||
const [rows, setRows] = React.useState(initialEntries);
|
||||
function ItemsEntriesTable(props) {
|
||||
const { value, initialValue, onChange } = props;
|
||||
|
||||
// Allows to observes `entries` to make table rows outside controlled.
|
||||
useEffect(() => {
|
||||
if (entries && entries !== rows) {
|
||||
setRows(entries);
|
||||
}
|
||||
}, [entries, rows]);
|
||||
const [localValue, handleChange] = useUncontrolled({
|
||||
value,
|
||||
initialValue,
|
||||
finalValue: [],
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<ItemEntriesTableProvider value={{ ...props, localValue, handleChange }}>
|
||||
<ItemEntriesTableRoot />
|
||||
</ItemEntriesTableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Items entries table logic.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function ItemEntriesTableRoot() {
|
||||
const {
|
||||
localValue,
|
||||
defaultEntry,
|
||||
handleChange,
|
||||
items,
|
||||
errors,
|
||||
currencyCode,
|
||||
landedCost,
|
||||
taxRates,
|
||||
} = useItemEntriesTableContext();
|
||||
|
||||
// Editiable items entries columns.
|
||||
const columns = useEditableItemsEntriesColumns({ landedCost });
|
||||
const columns = useEditableItemsEntriesColumns();
|
||||
|
||||
const composeRowsOnEditCell = useComposeRowsOnEditTableCell();
|
||||
const composeRowsOnDeleteRow = useComposeRowsOnRemoveTableRow();
|
||||
|
||||
// Handle the fetch item row details.
|
||||
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
|
||||
landedCost,
|
||||
itemType,
|
||||
itemType: null,
|
||||
notifyNewRow: (newRow, rowIndex) => {
|
||||
// Update the rate, description and quantity data of the row.
|
||||
const newRows = composeRowsOnNewRow(rowIndex, newRow, rows);
|
||||
|
||||
setRows(newRows);
|
||||
onUpdateData(newRows);
|
||||
const newRows = composeRowsOnNewRow(rowIndex, newRow, localValue);
|
||||
handleChange(newRows);
|
||||
},
|
||||
});
|
||||
|
||||
// Handles the editor data update.
|
||||
const handleUpdateData = useCallback(
|
||||
(rowIndex, columnId, value) => {
|
||||
if (columnId === 'item_id') {
|
||||
setItemRow({ rowIndex, columnId, itemId: value });
|
||||
}
|
||||
const composeEditCell = composeRowsOnEditCell(rowIndex, columnId);
|
||||
const newRows = composeEditCell(value, defaultEntry, rows);
|
||||
|
||||
setRows(newRows);
|
||||
onUpdateData(newRows);
|
||||
const newRows = composeRowsOnEditCell(rowIndex, columnId, value);
|
||||
handleChange(newRows);
|
||||
},
|
||||
[rows, defaultEntry, onUpdateData, setItemRow],
|
||||
[localValue, defaultEntry, handleChange],
|
||||
);
|
||||
|
||||
// Handle table rows removing by index.
|
||||
const handleRemoveRow = (rowIndex) => {
|
||||
const newRows = compose(
|
||||
// Ensure minimum lines count.
|
||||
updateMinEntriesLines(minLinesNumber, defaultEntry),
|
||||
// Remove the line by the given index.
|
||||
updateRemoveLineByIndex(rowIndex),
|
||||
)(rows);
|
||||
|
||||
setRows(newRows);
|
||||
saveInvoke(onUpdateData, newRows);
|
||||
const newRows = composeRowsOnDeleteRow(rowIndex);
|
||||
handleChange(newRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTableEditable
|
||||
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
data={localValue}
|
||||
sticky={true}
|
||||
progressBarLoading={isItemFetching}
|
||||
cellsLoading={isItemFetching}
|
||||
cellsLoadingCoords={cellsLoading}
|
||||
payload={{
|
||||
items,
|
||||
taxRates,
|
||||
errors: errors || [],
|
||||
updateData: handleUpdateData,
|
||||
removeRow: handleRemoveRow,
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
ProjectBillableEntriesCell,
|
||||
} from '@/components/DataTableCells';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { TaxRatesSuggestInputCell } from '@/components/TaxRates/TaxRatesSuggestInputCell';
|
||||
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
||||
|
||||
/**
|
||||
* Item header cell.
|
||||
@@ -43,7 +45,6 @@ export function ActionsCellRenderer({
|
||||
const onRemoveRole = () => {
|
||||
removeRow(index);
|
||||
};
|
||||
|
||||
const exampleMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
@@ -89,15 +90,17 @@ const LandedCostHeaderCell = () => {
|
||||
/**
|
||||
* Retrieve editable items entries columns.
|
||||
*/
|
||||
export function useEditableItemsEntriesColumns({ landedCost }) {
|
||||
export function useEditableItemsEntriesColumns() {
|
||||
const { featureCan } = useFeatureCan();
|
||||
const { landedCost } = useItemEntriesTableContext();
|
||||
|
||||
const isProjectsFeatureEnabled = featureCan(Features.Projects);
|
||||
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: ItemHeaderCell,
|
||||
id: 'item_id',
|
||||
Header: ItemHeaderCell,
|
||||
accessor: 'item_id',
|
||||
Cell: ItemsListCell,
|
||||
disableSortBy: true,
|
||||
@@ -129,6 +132,13 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
|
||||
width: 70,
|
||||
align: Align.Right,
|
||||
},
|
||||
{
|
||||
Header: 'Tax rate',
|
||||
accessor: 'tax_rate_id',
|
||||
Cell: TaxRatesSuggestInputCell,
|
||||
disableSortBy: true,
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
Header: intl.get('discount'),
|
||||
accessor: 'discount',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, isEmpty, last } from 'lodash';
|
||||
import { sumBy, isEmpty, last, keyBy } from 'lodash';
|
||||
|
||||
import { useItem } from '@/hooks/query';
|
||||
import {
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
orderingLinesIndexes,
|
||||
updateTableRow,
|
||||
} from '@/utils';
|
||||
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
SELLABLE: 'SELLABLE',
|
||||
PURCHASABLE: 'PURCHASABLE',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item entry total from the given rate, quantity and discount.
|
||||
@@ -39,11 +45,6 @@ export function updateItemsEntriesTotal(rows) {
|
||||
}));
|
||||
}
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
SELLABLE: 'SELLABLE',
|
||||
PURCHASABLE: 'PURCHASABLE',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve total of the given items entries.
|
||||
*/
|
||||
@@ -150,12 +151,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
||||
*/
|
||||
export const composeRowsOnEditCell = R.curry(
|
||||
(rowIndex, columnId, value, defaultEntry, rows) => {
|
||||
return compose(
|
||||
orderingLinesIndexes,
|
||||
updateAutoAddNewLine(defaultEntry, ['item_id']),
|
||||
updateItemsEntriesTotal,
|
||||
updateTableCell(rowIndex, columnId, value),
|
||||
)(rows);
|
||||
return compose()(rows);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -171,10 +167,102 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} entries
|
||||
* Associate tax rate to entries.
|
||||
*/
|
||||
export const assignEntriesTaxRate = R.curry((taxRates, entries) => {
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
return entries.map((entry) => {
|
||||
const taxRate = taxRatesById[entry.tax_rate_id];
|
||||
|
||||
return {
|
||||
...entry,
|
||||
tax_rate: taxRate?.rate || 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Assign tax amount to entries.
|
||||
* @param {boolean} isInclusiveTax
|
||||
* @param entries
|
||||
* @returns
|
||||
*/
|
||||
export const composeControlledEntries = (entries) => {
|
||||
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
|
||||
export const assignEntriesTaxAmount = R.curry(
|
||||
(isInclusiveTax: boolean, entries) => {
|
||||
return entries.map((entry) => {
|
||||
const taxAmount = isInclusiveTax
|
||||
? getInclusiveTaxAmount(entry.amount, entry.tax_rate)
|
||||
: getExlusiveTaxAmount(entry.amount, entry.tax_rate);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
tax_amount: taxAmount,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Get inclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getInclusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / (100 + taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get exclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getExlusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose rows when edit a table cell.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const useComposeRowsOnEditTableCell = () => {
|
||||
const { taxRates, isInclusiveTax, localValue, defaultEntry } =
|
||||
useItemEntriesTableContext();
|
||||
|
||||
return useCallback(
|
||||
(rowIndex, columnId, value) => {
|
||||
return R.compose(
|
||||
assignEntriesTaxAmount(isInclusiveTax),
|
||||
assignEntriesTaxRate(taxRates),
|
||||
orderingLinesIndexes,
|
||||
updateAutoAddNewLine(defaultEntry, ['item_id']),
|
||||
updateItemsEntriesTotal,
|
||||
updateTableCell(rowIndex, columnId, value),
|
||||
)(localValue);
|
||||
},
|
||||
[taxRates, isInclusiveTax, localValue, defaultEntry],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose rows when remove a table row.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const useComposeRowsOnRemoveTableRow = () => {
|
||||
const { minLinesNumber, defaultEntry, localValue } =
|
||||
useItemEntriesTableContext();
|
||||
|
||||
return useCallback(
|
||||
(rowIndex) => {
|
||||
return compose(
|
||||
// Ensure minimum lines count.
|
||||
updateMinEntriesLines(minLinesNumber, defaultEntry),
|
||||
// Remove the line by the given index.
|
||||
updateRemoveLineByIndex(rowIndex),
|
||||
)(localValue);
|
||||
},
|
||||
[minLinesNumber, defaultEntry, localValue],
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user