feat(webapp): invoice tax rate

This commit is contained in:
Ahmed Bouhuolia
2023-09-11 23:17:27 +02:00
parent 6abae43c6f
commit b98b73ad98
21 changed files with 615 additions and 126 deletions

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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',

View File

@@ -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],
);
};