mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +00:00
feat(webapp): invoice tax rate
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
|||||||
EditableText,
|
EditableText,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from '@blueprintjs-formik/core';
|
} from '@blueprintjs-formik/core';
|
||||||
import { MultiSelect } from '@blueprintjs-formik/select';
|
import { MultiSelect, SuggestField } from '@blueprintjs-formik/select';
|
||||||
import { DateInput } from '@blueprintjs-formik/datetime';
|
import { DateInput } from '@blueprintjs-formik/datetime';
|
||||||
import { FSelect } from './Select';
|
import { FSelect } from './Select';
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export {
|
|||||||
FSelect,
|
FSelect,
|
||||||
MultiSelect as FMultiSelect,
|
MultiSelect as FMultiSelect,
|
||||||
EditableText as FEditableText,
|
EditableText as FEditableText,
|
||||||
|
SuggestField as FSuggest,
|
||||||
TextArea as FTextArea,
|
TextArea as FTextArea,
|
||||||
DateInput as FDateInput,
|
DateInput as FDateInput,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Suggest } from '@blueprintjs-formik/select';
|
||||||
|
import { FormGroup } from '@blueprintjs/core';
|
||||||
|
import { CellType } from '@/constants';
|
||||||
|
|
||||||
|
export function TaxRatesSuggestInputCell({
|
||||||
|
column: { id, suggestProps, formGroupProps },
|
||||||
|
row: { index },
|
||||||
|
cell: { value: cellValue },
|
||||||
|
payload: { errors, updateData, taxRates },
|
||||||
|
}) {
|
||||||
|
const error = errors?.[index]?.[id];
|
||||||
|
|
||||||
|
// Handle the item selected.
|
||||||
|
const handleItemSelected = useCallback(
|
||||||
|
(value, taxRate) => {
|
||||||
|
updateData(index, id, taxRate.id);
|
||||||
|
},
|
||||||
|
[updateData, index, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup intent={error ? Intent.DANGER : null} {...formGroupProps}>
|
||||||
|
<Suggest<any>
|
||||||
|
selectedValue={cellValue}
|
||||||
|
items={taxRates}
|
||||||
|
valueAccessor={'id'}
|
||||||
|
labelAccessor={'code'}
|
||||||
|
textAccessor={'name'}
|
||||||
|
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||||
|
inputProps={{ placeholder: '' }}
|
||||||
|
fill={true}
|
||||||
|
onItemChange={handleItemSelected}
|
||||||
|
{...suggestProps}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaxRatesSuggestInputCell.cellType = CellType.Field;
|
||||||
@@ -23,22 +23,30 @@ export function InvoiceDetailTableFooter() {
|
|||||||
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice.details.subtotal'} />}
|
title={<T id={'invoice.details.subtotal'} />}
|
||||||
value={<FormatNumber value={invoice.balance} />}
|
value={<FormatNumber value={invoice.subtotal_formatted} />}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
/>
|
/>
|
||||||
|
{invoice.taxes.map((taxRate) => (
|
||||||
|
<TotalLine
|
||||||
|
key={taxRate.id}
|
||||||
|
title={`${taxRate.name} (${taxRate.tax_rate}%)`}
|
||||||
|
value={taxRate.tax_rate_amount_formatted}
|
||||||
|
textStyle={TotalLineTextStyle.Regular}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice.details.total'} />}
|
title={<T id={'invoice.details.total'} />}
|
||||||
value={invoice.formatted_amount}
|
value={invoice.total_formatted}
|
||||||
borderStyle={TotalLineBorderStyle.DoubleDark}
|
borderStyle={TotalLineBorderStyle.DoubleDark}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice.details.payment_amount'} />}
|
title={<T id={'invoice.details.payment_amount'} />}
|
||||||
value={invoice.formatted_payment_amount}
|
value={invoice.payment_amount_formatted}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice.details.due_amount'} />}
|
title={<T id={'invoice.details.due_amount'} />}
|
||||||
value={invoice.formatted_due_amount}
|
value={invoice.due_amount_formatted}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
</InvoiceTotalLines>
|
</InvoiceTotalLines>
|
||||||
|
|||||||
@@ -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
|
// @ts-nocheck
|
||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { DataTableEditable } from '@/components';
|
import { DataTableEditable } from '@/components';
|
||||||
|
|
||||||
import { useEditableItemsEntriesColumns } from './components';
|
import { useEditableItemsEntriesColumns } from './components';
|
||||||
import {
|
|
||||||
saveInvoke,
|
|
||||||
compose,
|
|
||||||
updateMinEntriesLines,
|
|
||||||
updateRemoveLineByIndex,
|
|
||||||
} from '@/utils';
|
|
||||||
import {
|
import {
|
||||||
useFetchItemRow,
|
useFetchItemRow,
|
||||||
composeRowsOnNewRow,
|
composeRowsOnNewRow,
|
||||||
composeRowsOnEditCell,
|
useComposeRowsOnEditTableCell,
|
||||||
|
useComposeRowsOnRemoveTableRow,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import {
|
||||||
|
ItemEntriesTableProvider,
|
||||||
|
useItemEntriesTableContext,
|
||||||
|
} from './ItemEntriesTableProvider';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Items entries table.
|
* Items entries table.
|
||||||
*/
|
*/
|
||||||
function ItemsEntriesTable({
|
function ItemsEntriesTable(props) {
|
||||||
// #ownProps
|
const { value, initialValue, onChange } = props;
|
||||||
items,
|
|
||||||
entries,
|
|
||||||
initialEntries,
|
|
||||||
defaultEntry,
|
|
||||||
errors,
|
|
||||||
onUpdateData,
|
|
||||||
currencyCode,
|
|
||||||
itemType, // sellable or purchasable
|
|
||||||
landedCost = false,
|
|
||||||
minLinesNumber
|
|
||||||
}) {
|
|
||||||
const [rows, setRows] = React.useState(initialEntries);
|
|
||||||
|
|
||||||
// Allows to observes `entries` to make table rows outside controlled.
|
const [localValue, handleChange] = useUncontrolled({
|
||||||
useEffect(() => {
|
value,
|
||||||
if (entries && entries !== rows) {
|
initialValue,
|
||||||
setRows(entries);
|
finalValue: [],
|
||||||
}
|
onChange,
|
||||||
}, [entries, rows]);
|
});
|
||||||
|
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.
|
// Editiable items entries columns.
|
||||||
const columns = useEditableItemsEntriesColumns({ landedCost });
|
const columns = useEditableItemsEntriesColumns();
|
||||||
|
|
||||||
|
const composeRowsOnEditCell = useComposeRowsOnEditTableCell();
|
||||||
|
const composeRowsOnDeleteRow = useComposeRowsOnRemoveTableRow();
|
||||||
|
|
||||||
// Handle the fetch item row details.
|
// Handle the fetch item row details.
|
||||||
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
|
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
|
||||||
landedCost,
|
landedCost,
|
||||||
itemType,
|
itemType: null,
|
||||||
notifyNewRow: (newRow, rowIndex) => {
|
notifyNewRow: (newRow, rowIndex) => {
|
||||||
// Update the rate, description and quantity data of the row.
|
// Update the rate, description and quantity data of the row.
|
||||||
const newRows = composeRowsOnNewRow(rowIndex, newRow, rows);
|
const newRows = composeRowsOnNewRow(rowIndex, newRow, localValue);
|
||||||
|
handleChange(newRows);
|
||||||
setRows(newRows);
|
|
||||||
onUpdateData(newRows);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handles the editor data update.
|
// Handles the editor data update.
|
||||||
const handleUpdateData = useCallback(
|
const handleUpdateData = useCallback(
|
||||||
(rowIndex, columnId, value) => {
|
(rowIndex, columnId, value) => {
|
||||||
if (columnId === 'item_id') {
|
if (columnId === 'item_id') {
|
||||||
setItemRow({ rowIndex, columnId, itemId: value });
|
setItemRow({ rowIndex, columnId, itemId: value });
|
||||||
}
|
}
|
||||||
const composeEditCell = composeRowsOnEditCell(rowIndex, columnId);
|
const newRows = composeRowsOnEditCell(rowIndex, columnId, value);
|
||||||
const newRows = composeEditCell(value, defaultEntry, rows);
|
handleChange(newRows);
|
||||||
|
|
||||||
setRows(newRows);
|
|
||||||
onUpdateData(newRows);
|
|
||||||
},
|
},
|
||||||
[rows, defaultEntry, onUpdateData, setItemRow],
|
[localValue, defaultEntry, handleChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle table rows removing by index.
|
// Handle table rows removing by index.
|
||||||
const handleRemoveRow = (rowIndex) => {
|
const handleRemoveRow = (rowIndex) => {
|
||||||
const newRows = compose(
|
const newRows = composeRowsOnDeleteRow(rowIndex);
|
||||||
// Ensure minimum lines count.
|
handleChange(newRows);
|
||||||
updateMinEntriesLines(minLinesNumber, defaultEntry),
|
|
||||||
// Remove the line by the given index.
|
|
||||||
updateRemoveLineByIndex(rowIndex),
|
|
||||||
)(rows);
|
|
||||||
|
|
||||||
setRows(newRows);
|
|
||||||
saveInvoke(onUpdateData, newRows);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTableEditable
|
<DataTableEditable
|
||||||
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
|
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={localValue}
|
||||||
sticky={true}
|
sticky={true}
|
||||||
progressBarLoading={isItemFetching}
|
progressBarLoading={isItemFetching}
|
||||||
cellsLoading={isItemFetching}
|
cellsLoading={isItemFetching}
|
||||||
cellsLoadingCoords={cellsLoading}
|
cellsLoadingCoords={cellsLoading}
|
||||||
payload={{
|
payload={{
|
||||||
items,
|
items,
|
||||||
|
taxRates,
|
||||||
errors: errors || [],
|
errors: errors || [],
|
||||||
updateData: handleUpdateData,
|
updateData: handleUpdateData,
|
||||||
removeRow: handleRemoveRow,
|
removeRow: handleRemoveRow,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
ProjectBillableEntriesCell,
|
ProjectBillableEntriesCell,
|
||||||
} from '@/components/DataTableCells';
|
} from '@/components/DataTableCells';
|
||||||
import { useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
|
import { TaxRatesSuggestInputCell } from '@/components/TaxRates/TaxRatesSuggestInputCell';
|
||||||
|
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item header cell.
|
* Item header cell.
|
||||||
@@ -43,7 +45,6 @@ export function ActionsCellRenderer({
|
|||||||
const onRemoveRole = () => {
|
const onRemoveRole = () => {
|
||||||
removeRow(index);
|
removeRow(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exampleMenu = (
|
const exampleMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -89,15 +90,17 @@ const LandedCostHeaderCell = () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve editable items entries columns.
|
* Retrieve editable items entries columns.
|
||||||
*/
|
*/
|
||||||
export function useEditableItemsEntriesColumns({ landedCost }) {
|
export function useEditableItemsEntriesColumns() {
|
||||||
const { featureCan } = useFeatureCan();
|
const { featureCan } = useFeatureCan();
|
||||||
|
const { landedCost } = useItemEntriesTableContext();
|
||||||
|
|
||||||
const isProjectsFeatureEnabled = featureCan(Features.Projects);
|
const isProjectsFeatureEnabled = featureCan(Features.Projects);
|
||||||
|
|
||||||
return React.useMemo(
|
return React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
Header: ItemHeaderCell,
|
|
||||||
id: 'item_id',
|
id: 'item_id',
|
||||||
|
Header: ItemHeaderCell,
|
||||||
accessor: 'item_id',
|
accessor: 'item_id',
|
||||||
Cell: ItemsListCell,
|
Cell: ItemsListCell,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
@@ -129,6 +132,13 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
|
|||||||
width: 70,
|
width: 70,
|
||||||
align: Align.Right,
|
align: Align.Right,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Header: 'Tax rate',
|
||||||
|
accessor: 'tax_rate_id',
|
||||||
|
Cell: TaxRatesSuggestInputCell,
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('discount'),
|
Header: intl.get('discount'),
|
||||||
accessor: 'discount',
|
accessor: 'discount',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { sumBy, isEmpty, last } from 'lodash';
|
import { sumBy, isEmpty, last, keyBy } from 'lodash';
|
||||||
|
|
||||||
import { useItem } from '@/hooks/query';
|
import { useItem } from '@/hooks/query';
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +13,12 @@ import {
|
|||||||
orderingLinesIndexes,
|
orderingLinesIndexes,
|
||||||
updateTableRow,
|
updateTableRow,
|
||||||
} from '@/utils';
|
} 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.
|
* 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.
|
* Retrieve total of the given items entries.
|
||||||
*/
|
*/
|
||||||
@@ -150,12 +151,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
|||||||
*/
|
*/
|
||||||
export const composeRowsOnEditCell = R.curry(
|
export const composeRowsOnEditCell = R.curry(
|
||||||
(rowIndex, columnId, value, defaultEntry, rows) => {
|
(rowIndex, columnId, value, defaultEntry, rows) => {
|
||||||
return compose(
|
return compose()(rows);
|
||||||
orderingLinesIndexes,
|
|
||||||
updateAutoAddNewLine(defaultEntry, ['item_id']),
|
|
||||||
updateItemsEntriesTotal,
|
|
||||||
updateTableCell(rowIndex, columnId, value),
|
|
||||||
)(rows);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -171,10 +167,102 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Associate tax rate to entries.
|
||||||
* @param {*} 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
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const composeControlledEntries = (entries) => {
|
export const assignEntriesTaxAmount = R.curry(
|
||||||
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
|
(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],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import moment from 'moment';
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
|
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
|
||||||
import { isBlank } from '@/utils';
|
import { isBlank } from '@/utils';
|
||||||
|
import { TaxType } from '@/interfaces/TaxRates';
|
||||||
|
|
||||||
const getSchema = () =>
|
const getSchema = () =>
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
@@ -35,6 +36,10 @@ const getSchema = () =>
|
|||||||
.max(DATATYPES_LENGTH.TEXT)
|
.max(DATATYPES_LENGTH.TEXT)
|
||||||
.label(intl.get('note')),
|
.label(intl.get('note')),
|
||||||
exchange_rate: Yup.number(),
|
exchange_rate: Yup.number(),
|
||||||
|
inclusive_exclusive_tax: Yup.string().oneOf([
|
||||||
|
TaxType.Inclusive,
|
||||||
|
TaxType.Exclusive,
|
||||||
|
]),
|
||||||
branch_id: Yup.string(),
|
branch_id: Yup.string(),
|
||||||
warehouse_id: Yup.string(),
|
warehouse_id: Yup.string(),
|
||||||
project_id: Yup.string(),
|
project_id: Yup.string(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
@@ -26,6 +26,7 @@ import withCurrentOrganization from '@/containers/Organization/withCurrentOrgani
|
|||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { compose, orderingLinesIndexes, transactionNumber } from '@/utils';
|
import { compose, orderingLinesIndexes, transactionNumber } from '@/utils';
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||||
|
import { InvoiceFormActions } from './InvoiceFormActions';
|
||||||
import {
|
import {
|
||||||
transformToEditForm,
|
transformToEditForm,
|
||||||
defaultInvoice,
|
defaultInvoice,
|
||||||
@@ -166,7 +167,11 @@ function InvoiceForm({
|
|||||||
<Form>
|
<Form>
|
||||||
<InvoiceFormTopBar />
|
<InvoiceFormTopBar />
|
||||||
<InvoiceFormHeader />
|
<InvoiceFormHeader />
|
||||||
<InvoiceItemsEntriesEditorField />
|
|
||||||
|
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||||
|
<InvoiceFormActions />
|
||||||
|
<InvoiceItemsEntriesEditorField />
|
||||||
|
</div>
|
||||||
<InvoiceFormFooter />
|
<InvoiceFormFooter />
|
||||||
<InvoiceFloatingActions />
|
<InvoiceFloatingActions />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { InclusiveButtonOptions } from './constants';
|
||||||
|
import { Box, FFormGroup, FSelect } from '@/components';
|
||||||
|
import { composeEntriesOnEditInclusiveTax } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice form actions.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export function InvoiceFormActions() {
|
||||||
|
return (
|
||||||
|
<InvoiceFormActionsRoot>
|
||||||
|
<InvoiceExclusiveInclusiveSelect />
|
||||||
|
</InvoiceFormActionsRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice exclusive/inclusive select.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export function InvoiceExclusiveInclusiveSelect(props) {
|
||||||
|
const { values, setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
const handleItemSelect = (item) => {
|
||||||
|
const newEntries = composeEntriesOnEditInclusiveTax(
|
||||||
|
item.key,
|
||||||
|
values.entries,
|
||||||
|
);
|
||||||
|
setFieldValue('inclusive_exclusive_tax', item.key);
|
||||||
|
setFieldValue('entries', newEntries);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InclusiveFormGroup
|
||||||
|
name={'inclusive_exclusive_tax'}
|
||||||
|
label={'Amounts are'}
|
||||||
|
inline={true}
|
||||||
|
>
|
||||||
|
<InclusiveSelect
|
||||||
|
name={'inclusive_exclusive_tax'}
|
||||||
|
items={InclusiveButtonOptions}
|
||||||
|
textAccessor={'label'}
|
||||||
|
labelAccessor={() => ''}
|
||||||
|
valueAccessor={'key'}
|
||||||
|
popoverProps={{ minimal: true, usePortal: true, inline: false }}
|
||||||
|
buttonProps={{ small: true }}
|
||||||
|
onItemSelect={handleItemSelect}
|
||||||
|
filterable={false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</InclusiveFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const InclusiveFormGroup = styled(FFormGroup)`
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
&.bp3-form-group.bp3-inline label.bp3-label {
|
||||||
|
line-height: 1.25;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InclusiveSelect = styled(FSelect)`
|
||||||
|
.bp3-button {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InvoiceFormActionsRoot = styled(Box)`
|
||||||
|
padding-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
T,
|
T,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useInvoiceTotals } from './utils';
|
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
|
||||||
|
|
||||||
export function InvoiceFormFooterRight() {
|
export function InvoiceFormFooterRight() {
|
||||||
// Calculate the total due amount of invoice entries.
|
// Calculate the total due amount of invoice entries.
|
||||||
@@ -20,15 +21,34 @@ export function InvoiceFormFooterRight() {
|
|||||||
formattedPaymentTotal,
|
formattedPaymentTotal,
|
||||||
} = useInvoiceTotals();
|
} = useInvoiceTotals();
|
||||||
|
|
||||||
|
const {
|
||||||
|
values: { inclusive_exclusive_tax },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
const taxEntries = useInvoiceAggregatedTaxRates();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice_form.label.subtotal'} />}
|
title={
|
||||||
|
<>
|
||||||
|
{inclusive_exclusive_tax === 'inclusive'
|
||||||
|
? 'Subtotal (Tax Inclusive)'
|
||||||
|
: 'Subtotal'}
|
||||||
|
</>
|
||||||
|
}
|
||||||
value={formattedSubtotal}
|
value={formattedSubtotal}
|
||||||
borderStyle={TotalLineBorderStyle.None}
|
|
||||||
/>
|
/>
|
||||||
|
{taxEntries.map((tax, index) => (
|
||||||
|
<TotalLine
|
||||||
|
key={index}
|
||||||
|
title={tax.label}
|
||||||
|
value={tax.taxAmountFormatted}
|
||||||
|
borderStyle={TotalLineBorderStyle.None}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'invoice_form.label.total'} />}
|
title={'Total (USD)'}
|
||||||
value={formattedTotal}
|
value={formattedTotal}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
|
|||||||
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { PageFormBigNumber } from '@/components';
|
import { PageFormBigNumber } from '@/components';
|
||||||
import { useInvoiceTotal } from './utils';
|
import { useInvoiceSubtotal } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice form header section.
|
* Invoice form header section.
|
||||||
@@ -32,7 +32,7 @@ function InvoiceFormBigTotal() {
|
|||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// Calculate the total due amount of invoice entries.
|
// Calculate the total due amount of invoice entries.
|
||||||
const totalDueAmount = useInvoiceTotal();
|
const totalDueAmount = useInvoiceSubtotal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormBigNumber
|
<PageFormBigNumber
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
useEstimate,
|
useEstimate,
|
||||||
} from '@/hooks/query';
|
} from '@/hooks/query';
|
||||||
import { useProjects } from '@/containers/Projects/hooks';
|
import { useProjects } from '@/containers/Projects/hooks';
|
||||||
|
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||||
|
|
||||||
const InvoiceFormContext = createContext();
|
const InvoiceFormContext = createContext();
|
||||||
|
|
||||||
@@ -34,10 +35,14 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
const isBranchFeatureCan = featureCan(Features.Branches);
|
const isBranchFeatureCan = featureCan(Features.Branches);
|
||||||
const isProjectsFeatureCan = featureCan(Features.Projects);
|
const isProjectsFeatureCan = featureCan(Features.Projects);
|
||||||
|
|
||||||
|
// Fetch invoice data.
|
||||||
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
|
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
|
||||||
enabled: !!invoiceId,
|
enabled: !!invoiceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch tax rates.
|
||||||
|
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
|
||||||
|
|
||||||
// Fetch project list.
|
// Fetch project list.
|
||||||
const {
|
const {
|
||||||
data: { projects },
|
data: { projects },
|
||||||
@@ -113,6 +118,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
branches,
|
branches,
|
||||||
warehouses,
|
warehouses,
|
||||||
projects,
|
projects,
|
||||||
|
taxRates,
|
||||||
|
|
||||||
isInvoiceLoading,
|
isInvoiceLoading,
|
||||||
isItemsLoading,
|
isItemsLoading,
|
||||||
@@ -123,6 +129,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
|||||||
isFeatureLoading,
|
isFeatureLoading,
|
||||||
isBranchesSuccess,
|
isBranchesSuccess,
|
||||||
isWarehousesSuccess,
|
isWarehousesSuccess,
|
||||||
|
isTaxRatesLoading,
|
||||||
|
|
||||||
createInvoiceMutate,
|
createInvoiceMutate,
|
||||||
editInvoiceMutate,
|
editInvoiceMutate,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { FastField } from 'formik';
|
import { FastField } from 'formik';
|
||||||
import { CLASSES } from '@/constants/classes';
|
|
||||||
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||||
import { entriesFieldShouldUpdate } from './utils';
|
import { entriesFieldShouldUpdate } from './utils';
|
||||||
@@ -11,32 +9,33 @@ import { entriesFieldShouldUpdate } from './utils';
|
|||||||
* Invoice items entries editor field.
|
* Invoice items entries editor field.
|
||||||
*/
|
*/
|
||||||
export default function InvoiceItemsEntriesEditorField() {
|
export default function InvoiceItemsEntriesEditorField() {
|
||||||
const { items } = useInvoiceFormContext();
|
const { items, taxRates } = useInvoiceFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
<FastField
|
||||||
<FastField
|
name={'entries'}
|
||||||
name={'entries'}
|
items={items}
|
||||||
items={items}
|
taxRates={taxRates}
|
||||||
shouldUpdate={entriesFieldShouldUpdate}
|
shouldUpdate={entriesFieldShouldUpdate}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
form: { values, setFieldValue },
|
form: { values, setFieldValue },
|
||||||
field: { value },
|
field: { value },
|
||||||
meta: { error, touched },
|
meta: { error, touched },
|
||||||
}) => (
|
}) => (
|
||||||
<ItemsEntriesTable
|
<ItemsEntriesTable
|
||||||
entries={value}
|
value={value}
|
||||||
onUpdateData={(entries) => {
|
onChange={(entries) => {
|
||||||
setFieldValue('entries', entries);
|
setFieldValue('entries', entries);
|
||||||
}}
|
}}
|
||||||
items={items}
|
items={items}
|
||||||
errors={error}
|
taxRates={taxRates}
|
||||||
linesNumber={4}
|
errors={error}
|
||||||
currencyCode={values.currency_code}
|
linesNumber={4}
|
||||||
/>
|
currencyCode={values.currency_code}
|
||||||
)}
|
isInclusiveTax={values.inclusive_exclusive_tax === 'inclusive'}
|
||||||
</FastField>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</FastField>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const InclusiveButtonOptions = [
|
||||||
|
{ key: 'inclusive', label: 'Inclusive of Tax' },
|
||||||
|
{ key: 'exclusive', label: 'Exclusive of Tax' },
|
||||||
|
];
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { omit, first } from 'lodash';
|
import { omit, first, keyBy, sumBy, groupBy } from 'lodash';
|
||||||
import {
|
import { compose, transformToForm, repeatValue } from '@/utils';
|
||||||
compose,
|
|
||||||
transformToForm,
|
|
||||||
repeatValue,
|
|
||||||
transactionNumber,
|
|
||||||
} from '@/utils';
|
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
|
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
|
||||||
import { ERROR } from '@/constants/errors';
|
import { ERROR } from '@/constants/errors';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
import {
|
||||||
|
assignEntriesTaxAmount,
|
||||||
|
getEntriesTotal,
|
||||||
|
} from '@/containers/Entries/utils';
|
||||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||||
import {
|
import {
|
||||||
updateItemsEntriesTotal,
|
updateItemsEntriesTotal,
|
||||||
ensureEntriesHaveEmptyLine,
|
ensureEntriesHaveEmptyLine,
|
||||||
} from '@/containers/Entries/utils';
|
} from '@/containers/Entries/utils';
|
||||||
|
import { TaxType } from '@/interfaces/TaxRates';
|
||||||
|
|
||||||
export const MIN_LINES_NUMBER = 1;
|
export const MIN_LINES_NUMBER = 1;
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export const defaultInvoiceEntry = {
|
|||||||
quantity: '',
|
quantity: '',
|
||||||
description: '',
|
description: '',
|
||||||
amount: '',
|
amount: '',
|
||||||
|
tax_rate_id: '',
|
||||||
|
tax_rate: '',
|
||||||
|
tax_amount: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default invoice object.
|
// Default invoice object.
|
||||||
@@ -43,6 +46,7 @@ export const defaultInvoice = {
|
|||||||
due_date: moment().format('YYYY-MM-DD'),
|
due_date: moment().format('YYYY-MM-DD'),
|
||||||
delivered: '',
|
delivered: '',
|
||||||
invoice_no: '',
|
invoice_no: '',
|
||||||
|
inclusive_exclusive_tax: 'inclusive',
|
||||||
// Holds the invoice number that entered manually only.
|
// Holds the invoice number that entered manually only.
|
||||||
invoice_no_manually: '',
|
invoice_no_manually: '',
|
||||||
reference_no: '',
|
reference_no: '',
|
||||||
@@ -114,7 +118,7 @@ export const transformErrors = (errors, { setErrors }) => {
|
|||||||
*/
|
*/
|
||||||
export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
|
export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
|
||||||
return (
|
return (
|
||||||
newProps.shouldUpdateDeps.items !== oldProps.shouldUpdateDeps.items||
|
newProps.shouldUpdateDeps.items !== oldProps.shouldUpdateDeps.items ||
|
||||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -125,6 +129,7 @@ export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
|
|||||||
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
|
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
|
||||||
return (
|
return (
|
||||||
newProps.items !== oldProps.items ||
|
newProps.items !== oldProps.items ||
|
||||||
|
newProps.taxRates !== oldProps.taxRates ||
|
||||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -154,12 +159,17 @@ export function transformValueToRequest(values) {
|
|||||||
(item) => item.item_id && item.quantity,
|
(item) => item.item_id && item.quantity,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...omit(values, ['invoice_no', 'invoice_no_manually']),
|
...omit(values, [
|
||||||
|
'invoice_no',
|
||||||
|
'invoice_no_manually',
|
||||||
|
'inclusive_exclusive_tax',
|
||||||
|
]),
|
||||||
// The `invoice_no_manually` will be presented just if the auto-increment
|
// The `invoice_no_manually` will be presented just if the auto-increment
|
||||||
// is disable, always both attributes hold the same value in manual mode.
|
// is disable, always both attributes hold the same value in manual mode.
|
||||||
...(values.invoice_no_manually && {
|
...(values.invoice_no_manually && {
|
||||||
invoice_no: values.invoice_no,
|
invoice_no: values.invoice_no,
|
||||||
}),
|
}),
|
||||||
|
is_inclusive_tax: values.inclusive_exclusive_tax === 'inclusive',
|
||||||
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })),
|
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })),
|
||||||
delivered: false,
|
delivered: false,
|
||||||
};
|
};
|
||||||
@@ -196,7 +206,11 @@ export const useSetPrimaryBranchToForm = () => {
|
|||||||
}, [isBranchesSuccess, setFieldValue, branches]);
|
}, [isBranchesSuccess, setFieldValue, branches]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInvoiceTotal = () => {
|
/**
|
||||||
|
* Retrieves the invoice subtotal.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const useInvoiceSubtotal = () => {
|
||||||
const {
|
const {
|
||||||
values: { entries },
|
values: { entries },
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
@@ -216,10 +230,12 @@ export const useInvoiceTotals = () => {
|
|||||||
// Retrieves the invoice entries total.
|
// Retrieves the invoice entries total.
|
||||||
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
|
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
|
||||||
|
|
||||||
|
const total_ = useInvoiceTotal();
|
||||||
|
|
||||||
// Retrieves the formatted total money.
|
// Retrieves the formatted total money.
|
||||||
const formattedTotal = React.useMemo(
|
const formattedTotal = React.useMemo(
|
||||||
() => formattedAmount(total, currencyCode),
|
() => formattedAmount(total_, currencyCode),
|
||||||
[total, currencyCode],
|
[total_, currencyCode],
|
||||||
);
|
);
|
||||||
// Retrieves the formatted subtotal.
|
// Retrieves the formatted subtotal.
|
||||||
const formattedSubtotal = React.useMemo(
|
const formattedSubtotal = React.useMemo(
|
||||||
@@ -271,6 +287,9 @@ export const useInvoiceIsForeignCustomer = () => {
|
|||||||
return isForeignCustomer;
|
return isForeignCustomer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the form state to initial values
|
||||||
|
*/
|
||||||
export const resetFormState = ({ initialValues, values, resetForm }) => {
|
export const resetFormState = ({ initialValues, values, resetForm }) => {
|
||||||
resetForm({
|
resetForm({
|
||||||
values: {
|
values: {
|
||||||
@@ -281,3 +300,105 @@ export const resetFormState = ({ initialValues, values, resetForm }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-calcualte the entries tax amount when editing.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const composeEntriesOnEditInclusiveTax = (
|
||||||
|
inclusiveExclusiveTax: string,
|
||||||
|
entries,
|
||||||
|
) => {
|
||||||
|
return R.compose(
|
||||||
|
assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'),
|
||||||
|
)(entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives the invoice aggregated tax rates.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export const useInvoiceAggregatedTaxRates = () => {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
const { taxRates } = useInvoiceFormContext();
|
||||||
|
|
||||||
|
const taxRatesById = useMemo(() => keyBy(taxRates, 'id'), [taxRates]);
|
||||||
|
|
||||||
|
// Calculate the total tax amount of invoice entries.
|
||||||
|
return React.useMemo(() => {
|
||||||
|
const filteredEntries = values.entries.filter((e) => e.tax_rate_id);
|
||||||
|
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
|
||||||
|
|
||||||
|
return Object.keys(groupedTaxRates).map((taxRateId) => {
|
||||||
|
const taxRate = taxRatesById[taxRateId];
|
||||||
|
const taxRates = groupedTaxRates[taxRateId];
|
||||||
|
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
|
||||||
|
const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD');
|
||||||
|
|
||||||
|
return {
|
||||||
|
taxRateId,
|
||||||
|
taxRate: taxRate.rate,
|
||||||
|
label: `${taxRate.name} [${taxRate.rate}%]`,
|
||||||
|
taxAmount: totalTaxAmount,
|
||||||
|
taxAmountFormatted,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [values.entries]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives the invoice total tax amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const useInvoiceTotalTaxAmount = () => {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
return React.useMemo(() => {
|
||||||
|
const filteredEntries = values.entries.filter((entry) => entry.tax_amount);
|
||||||
|
return sumBy(filteredEntries, 'tax_amount');
|
||||||
|
}, [values.entries]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives the invoice total.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const useInvoiceTotal = () => {
|
||||||
|
const subtotal = useInvoiceSubtotal();
|
||||||
|
const totalTaxAmount = useInvoiceTotalTaxAmount();
|
||||||
|
const isExclusiveTax = useIsInvoiceTaxExclusive();
|
||||||
|
|
||||||
|
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))(
|
||||||
|
subtotal,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives the invoice due amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const useInvoiceDueAmount = () => {
|
||||||
|
const total = useInvoiceTotal();
|
||||||
|
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detrmines whether the tax is inclusive.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const useIsInvoiceTaxInclusive = () => {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
return values.inclusive_exclusive_tax === TaxType.Inclusive;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detrmines whether the tax is exclusive.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const useIsInvoiceTaxExclusive = () => {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
return values.inclusive_exclusive_tax === TaxType.Exclusive;
|
||||||
|
};
|
||||||
|
|||||||
22
packages/webapp/src/hooks/query/taxRates.ts
Normal file
22
packages/webapp/src/hooks/query/taxRates.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useRequestQuery } from '../useQueryRequest';
|
||||||
|
import QUERY_TYPES from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves tax rates.
|
||||||
|
* @param {number} customerId - Customer id.
|
||||||
|
*/
|
||||||
|
export function useTaxRates(props) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[QUERY_TYPES.TAX_RATES],
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
url: `tax-rates`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
defaultData: [],
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -224,6 +224,10 @@ const ORGANIZATION = {
|
|||||||
ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES: 'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES',
|
ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES: 'ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TAX_RATES = {
|
||||||
|
TAX_RATES: 'TAX_RATES',
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...Authentication,
|
...Authentication,
|
||||||
...ACCOUNTS,
|
...ACCOUNTS,
|
||||||
@@ -257,4 +261,5 @@ export default {
|
|||||||
...BRANCHES,
|
...BRANCHES,
|
||||||
...DASHBOARD,
|
...DASHBOARD,
|
||||||
...ORGANIZATION,
|
...ORGANIZATION,
|
||||||
|
...TAX_RATES
|
||||||
};
|
};
|
||||||
|
|||||||
36
packages/webapp/src/hooks/useUncontrolled.ts
Normal file
36
packages/webapp/src/hooks/useUncontrolled.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
interface UseUncontrolledInput<T> {
|
||||||
|
/** Value for controlled state */
|
||||||
|
value?: T;
|
||||||
|
|
||||||
|
/** Initial value for uncontrolled state */
|
||||||
|
initialValue?: T;
|
||||||
|
|
||||||
|
/** Final value for uncontrolled state when value and initialValue are not provided */
|
||||||
|
finalValue?: T;
|
||||||
|
|
||||||
|
/** Controlled state onChange handler */
|
||||||
|
onChange?(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUncontrolled<T>({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
finalValue,
|
||||||
|
onChange = () => {},
|
||||||
|
}: UseUncontrolledInput<T>) {
|
||||||
|
const [uncontrolledValue, setUncontrolledValue] = useState(
|
||||||
|
initialValue !== undefined ? initialValue : finalValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUncontrolledChange = (val: T) => {
|
||||||
|
setUncontrolledValue(val);
|
||||||
|
onChange?.(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value !== undefined) {
|
||||||
|
return [value as T, onChange, true];
|
||||||
|
}
|
||||||
|
return [uncontrolledValue as T, handleUncontrolledChange, false];
|
||||||
|
}
|
||||||
11
packages/webapp/src/interfaces/ItemEntries.ts
Normal file
11
packages/webapp/src/interfaces/ItemEntries.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ItemEntry {
|
||||||
|
index: number;
|
||||||
|
item_id: number;
|
||||||
|
description: string;
|
||||||
|
quantity: number;
|
||||||
|
rate: number;
|
||||||
|
discount: number;
|
||||||
|
tax_rate_id: number;
|
||||||
|
tax_rate: number;
|
||||||
|
tax_amount: number;
|
||||||
|
}
|
||||||
4
packages/webapp/src/interfaces/TaxRates.ts
Normal file
4
packages/webapp/src/interfaces/TaxRates.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum TaxType {
|
||||||
|
Inclusive = 'inclusive',
|
||||||
|
Exclusive = 'exclusive',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user