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

@@ -23,22 +23,30 @@ export function InvoiceDetailTableFooter() {
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'invoice.details.subtotal'} />}
value={<FormatNumber value={invoice.balance} />}
value={<FormatNumber value={invoice.subtotal_formatted} />}
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
title={<T id={'invoice.details.total'} />}
value={invoice.formatted_amount}
value={invoice.total_formatted}
borderStyle={TotalLineBorderStyle.DoubleDark}
textStyle={TotalLineTextStyle.Bold}
/>
<TotalLine
title={<T id={'invoice.details.payment_amount'} />}
value={invoice.formatted_payment_amount}
value={invoice.payment_amount_formatted}
/>
<TotalLine
title={<T id={'invoice.details.due_amount'} />}
value={invoice.formatted_due_amount}
value={invoice.due_amount_formatted}
textStyle={TotalLineTextStyle.Bold}
/>
</InvoiceTotalLines>

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

View File

@@ -4,6 +4,7 @@ import moment from 'moment';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
import { isBlank } from '@/utils';
import { TaxType } from '@/interfaces/TaxRates';
const getSchema = () =>
Yup.object().shape({
@@ -35,6 +36,10 @@ const getSchema = () =>
.max(DATATYPES_LENGTH.TEXT)
.label(intl.get('note')),
exchange_rate: Yup.number(),
inclusive_exclusive_tax: Yup.string().oneOf([
TaxType.Inclusive,
TaxType.Exclusive,
]),
branch_id: Yup.string(),
warehouse_id: Yup.string(),
project_id: Yup.string(),

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
@@ -26,6 +26,7 @@ import withCurrentOrganization from '@/containers/Organization/withCurrentOrgani
import { AppToaster } from '@/components';
import { compose, orderingLinesIndexes, transactionNumber } from '@/utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import { InvoiceFormActions } from './InvoiceFormActions';
import {
transformToEditForm,
defaultInvoice,
@@ -71,7 +72,7 @@ function InvoiceForm({
? { ...transformToEditForm(invoice) }
: {
...defaultInvoice,
// If the auto-increment mode is enabled, take the next invoice
// If the auto-increment mode is enabled, take the next invoice
// number from the settings.
...(invoiceAutoIncrementMode && {
invoice_no: invoiceNumber,
@@ -166,7 +167,11 @@ function InvoiceForm({
<Form>
<InvoiceFormTopBar />
<InvoiceFormHeader />
<InvoiceItemsEntriesEditorField />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<InvoiceFormActions />
<InvoiceItemsEntriesEditorField />
</div>
<InvoiceFormFooter />
<InvoiceFloatingActions />

View File

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

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import {
T,
@@ -9,7 +10,7 @@ import {
TotalLineBorderStyle,
TotalLineTextStyle,
} from '@/components';
import { useInvoiceTotals } from './utils';
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
export function InvoiceFormFooterRight() {
// Calculate the total due amount of invoice entries.
@@ -20,15 +21,34 @@ export function InvoiceFormFooterRight() {
formattedPaymentTotal,
} = useInvoiceTotals();
const {
values: { inclusive_exclusive_tax },
} = useFormikContext();
const taxEntries = useInvoiceAggregatedTaxRates();
return (
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'invoice_form.label.subtotal'} />}
title={
<>
{inclusive_exclusive_tax === 'inclusive'
? 'Subtotal (Tax Inclusive)'
: 'Subtotal'}
</>
}
value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None}
/>
{taxEntries.map((tax, index) => (
<TotalLine
key={index}
title={tax.label}
value={tax.taxAmountFormatted}
borderStyle={TotalLineBorderStyle.None}
/>
))}
<TotalLine
title={<T id={'invoice_form.label.total'} />}
title={'Total (USD)'}
value={formattedTotal}
borderStyle={TotalLineBorderStyle.SingleDark}
textStyle={TotalLineTextStyle.Bold}

View File

@@ -8,7 +8,7 @@ import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
import { CLASSES } from '@/constants/classes';
import { PageFormBigNumber } from '@/components';
import { useInvoiceTotal } from './utils';
import { useInvoiceSubtotal } from './utils';
/**
* Invoice form header section.
@@ -32,7 +32,7 @@ function InvoiceFormBigTotal() {
} = useFormikContext();
// Calculate the total due amount of invoice entries.
const totalDueAmount = useInvoiceTotal();
const totalDueAmount = useInvoiceSubtotal();
return (
<PageFormBigNumber

View File

@@ -18,6 +18,7 @@ import {
useEstimate,
} from '@/hooks/query';
import { useProjects } from '@/containers/Projects/hooks';
import { useTaxRates } from '@/hooks/query/taxRates';
const InvoiceFormContext = createContext();
@@ -34,10 +35,14 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
const isBranchFeatureCan = featureCan(Features.Branches);
const isProjectsFeatureCan = featureCan(Features.Projects);
// Fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
enabled: !!invoiceId,
});
// Fetch tax rates.
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
// Fetch project list.
const {
data: { projects },
@@ -113,6 +118,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
branches,
warehouses,
projects,
taxRates,
isInvoiceLoading,
isItemsLoading,
@@ -123,6 +129,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
isFeatureLoading,
isBranchesSuccess,
isWarehousesSuccess,
isTaxRatesLoading,
createInvoiceMutate,
editInvoiceMutate,

View File

@@ -1,8 +1,6 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { FastField } from 'formik';
import { CLASSES } from '@/constants/classes';
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import { entriesFieldShouldUpdate } from './utils';
@@ -11,32 +9,33 @@ import { entriesFieldShouldUpdate } from './utils';
* Invoice items entries editor field.
*/
export default function InvoiceItemsEntriesEditorField() {
const { items } = useInvoiceFormContext();
const { items, taxRates } = useInvoiceFormContext();
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField
name={'entries'}
items={items}
shouldUpdate={entriesFieldShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<ItemsEntriesTable
entries={value}
onUpdateData={(entries) => {
setFieldValue('entries', entries);
}}
items={items}
errors={error}
linesNumber={4}
currencyCode={values.currency_code}
/>
)}
</FastField>
</div>
<FastField
name={'entries'}
items={items}
taxRates={taxRates}
shouldUpdate={entriesFieldShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<ItemsEntriesTable
value={value}
onChange={(entries) => {
setFieldValue('entries', entries);
}}
items={items}
taxRates={taxRates}
errors={error}
linesNumber={4}
currencyCode={values.currency_code}
isInclusiveTax={values.inclusive_exclusive_tax === 'inclusive'}
/>
)}
</FastField>
);
}

View File

@@ -0,0 +1,6 @@
export const InclusiveButtonOptions = [
{ key: 'inclusive', label: 'Inclusive of Tax' },
{ key: 'exclusive', label: 'Exclusive of Tax' },
];

View File

@@ -1,27 +1,27 @@
// @ts-nocheck
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import intl from 'react-intl-universal';
import moment from 'moment';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
import { omit, first } from 'lodash';
import {
compose,
transformToForm,
repeatValue,
transactionNumber,
} from '@/utils';
import { omit, first, keyBy, sumBy, groupBy } from 'lodash';
import { compose, transformToForm, repeatValue } from '@/utils';
import { useFormikContext } from 'formik';
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
import { ERROR } from '@/constants/errors';
import { AppToaster } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import { getEntriesTotal } from '@/containers/Entries/utils';
import {
assignEntriesTaxAmount,
getEntriesTotal,
} from '@/containers/Entries/utils';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import {
updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine,
} from '@/containers/Entries/utils';
import { TaxType } from '@/interfaces/TaxRates';
export const MIN_LINES_NUMBER = 1;
@@ -34,6 +34,9 @@ export const defaultInvoiceEntry = {
quantity: '',
description: '',
amount: '',
tax_rate_id: '',
tax_rate: '',
tax_amount: '',
};
// Default invoice object.
@@ -43,6 +46,7 @@ export const defaultInvoice = {
due_date: moment().format('YYYY-MM-DD'),
delivered: '',
invoice_no: '',
inclusive_exclusive_tax: 'inclusive',
// Holds the invoice number that entered manually only.
invoice_no_manually: '',
reference_no: '',
@@ -114,7 +118,7 @@ export const transformErrors = (errors, { setErrors }) => {
*/
export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.shouldUpdateDeps.items !== oldProps.shouldUpdateDeps.items||
newProps.shouldUpdateDeps.items !== oldProps.shouldUpdateDeps.items ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
@@ -125,6 +129,7 @@ export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.items !== oldProps.items ||
newProps.taxRates !== oldProps.taxRates ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
@@ -154,12 +159,17 @@ export function transformValueToRequest(values) {
(item) => item.item_id && item.quantity,
);
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
// is disable, always both attributes hold the same value in manual mode.
...(values.invoice_no_manually && {
invoice_no: values.invoice_no,
}),
is_inclusive_tax: values.inclusive_exclusive_tax === 'inclusive',
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })),
delivered: false,
};
@@ -196,7 +206,11 @@ export const useSetPrimaryBranchToForm = () => {
}, [isBranchesSuccess, setFieldValue, branches]);
};
export const useInvoiceTotal = () => {
/**
* Retrieves the invoice subtotal.
* @returns {number}
*/
export const useInvoiceSubtotal = () => {
const {
values: { entries },
} = useFormikContext();
@@ -216,10 +230,12 @@ export const useInvoiceTotals = () => {
// Retrieves the invoice entries total.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
const total_ = useInvoiceTotal();
// Retrieves the formatted total money.
const formattedTotal = React.useMemo(
() => formattedAmount(total, currencyCode),
[total, currencyCode],
() => formattedAmount(total_, currencyCode),
[total_, currencyCode],
);
// Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo(
@@ -271,6 +287,9 @@ export const useInvoiceIsForeignCustomer = () => {
return isForeignCustomer;
};
/**
* Resets the form state to initial values
*/
export const resetFormState = ({ initialValues, values, resetForm }) => {
resetForm({
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;
};