mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat: integrate tax rates to bills (#260)
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import AccountDrawer from '@/containers/Drawers/AccountDrawer';
|
||||
import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer';
|
||||
import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer';
|
||||
|
||||
@@ -26,6 +26,10 @@ const SelectButton = styled(Button)`
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
|
||||
&.bp4-small{
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
|
||||
color: #5c7080;
|
||||
}
|
||||
|
||||
7
packages/webapp/src/constants/InclusiveTaxOptions.ts
Normal file
7
packages/webapp/src/constants/InclusiveTaxOptions.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
|
||||
export const InclusiveTaxOptions = [
|
||||
{ key: TaxType.Inclusive, label: 'Inclusive of Tax' },
|
||||
{ key: TaxType.Exclusive, label: 'Exclusive of Tax' },
|
||||
];
|
||||
@@ -30,7 +30,7 @@ export default function BillDetailHeader() {
|
||||
<CommercialDocTopHeader>
|
||||
<DetailsMenu>
|
||||
<AmountDetailItem label={intl.get('amount')}>
|
||||
<h3 class="big-number">{bill.formatted_amount}</h3>
|
||||
<h3 class="big-number">{bill.total_formatted}</h3>
|
||||
</AmountDetailItem>
|
||||
<StatusDetailItem>
|
||||
<BillDetailsStatus bill={bill} />
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
TotalLineBorderStyle,
|
||||
TotalLineTextStyle,
|
||||
FormatNumber,
|
||||
T,
|
||||
TotalLines,
|
||||
TotalLine,
|
||||
@@ -23,12 +20,20 @@ export function BillDetailTableFooter() {
|
||||
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||
<TotalLine
|
||||
title={<T id={'bill.details.subtotal'} />}
|
||||
value={<FormatNumber value={bill.amount} />}
|
||||
value={bill.subtotal_formatted}
|
||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||
/>
|
||||
{bill.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={'bill.details.total'} />}
|
||||
value={bill.formatted_amount}
|
||||
value={bill.total_formatted}
|
||||
borderStyle={TotalLineBorderStyle.DoubleDark}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
@@ -39,6 +44,7 @@ export function BillDetailTableFooter() {
|
||||
<TotalLine
|
||||
title={<T id={'bill.details.due_amount'} />}
|
||||
value={bill.formatted_due_amount}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
</BillTotalLines>
|
||||
</BillDetailsFooterRoot>
|
||||
|
||||
16
packages/webapp/src/containers/Entries/EntriesActionBar.tsx
Normal file
16
packages/webapp/src/containers/Entries/EntriesActionBar.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Box } from '@/components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const EntriesActionsBar = styled(Box)`
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
|
||||
.bp4-form-group {
|
||||
margin-bottom: 0;
|
||||
|
||||
label.bp4-label {
|
||||
opacity: 0.6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,8 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, isEmpty, last, keyBy } from 'lodash';
|
||||
|
||||
import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash';
|
||||
import { useItem } from '@/hooks/query';
|
||||
import {
|
||||
toSafeNumber,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
updateAutoAddNewLine,
|
||||
orderingLinesIndexes,
|
||||
updateTableRow,
|
||||
formattedAmount,
|
||||
} from '@/utils';
|
||||
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
||||
|
||||
@@ -116,6 +116,11 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
||||
? item.purchase_description
|
||||
: item.sell_description;
|
||||
|
||||
const taxRateId =
|
||||
itemType === ITEM_TYPE.PURCHASABLE
|
||||
? item.purchase_tax_rate_id
|
||||
: item.sell_tax_rate_id;
|
||||
|
||||
// Detarmines whether the landed cost checkbox should be disabled.
|
||||
const landedCostDisabled = isLandedCostDisabled(item);
|
||||
|
||||
@@ -130,6 +135,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
||||
landed_cost_disabled: landedCostDisabled,
|
||||
}
|
||||
: {}),
|
||||
taxRateId,
|
||||
};
|
||||
setItemRow(null);
|
||||
saveInvoke(notifyNewRow, newRow, rowIndex);
|
||||
@@ -266,3 +272,29 @@ export const useComposeRowsOnRemoveTableRow = () => {
|
||||
[minLinesNumber, defaultEntry, localValue],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the aggregate tax rates from the given item entries.
|
||||
*/
|
||||
export const aggregateItemEntriesTaxRates = R.curry((taxRates, entries) => {
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
// Calculate the total tax amount of invoice entries.
|
||||
const filteredEntries = 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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
handleErrors,
|
||||
} from './utils';
|
||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||
import { BillFormEntriesActions } from './BillFormEntriesActions';
|
||||
|
||||
/**
|
||||
* Bill form.
|
||||
@@ -126,7 +127,10 @@ function BillForm({
|
||||
<Form>
|
||||
<BillFormTopBar />
|
||||
<BillFormHeader />
|
||||
<BillItemsEntriesEditor />
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<BillFormEntriesActions />
|
||||
<BillItemsEntriesEditor />
|
||||
</div>
|
||||
<BillFormFooter />
|
||||
<BillFloatingActions />
|
||||
</Form>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { FFormGroup, FSelect } from '@/components';
|
||||
import { InclusiveTaxOptions } from '@/constants/InclusiveTaxOptions';
|
||||
|
||||
import { composeEntriesOnEditInclusiveTax } from './utils';
|
||||
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
|
||||
|
||||
export function BillFormEntriesActions() {
|
||||
return (
|
||||
<EntriesActionsBar>
|
||||
<BillExclusiveInclusiveSelect />
|
||||
</EntriesActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill exclusive/inclusive select.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
export function BillExclusiveInclusiveSelect(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}
|
||||
>
|
||||
<FSelect
|
||||
name={'inclusive_exclusive_tax'}
|
||||
items={InclusiveTaxOptions}
|
||||
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-left: auto;
|
||||
`;
|
||||
@@ -1,15 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
T,
|
||||
TotalLines,
|
||||
TotalLine,
|
||||
TotalLineBorderStyle,
|
||||
TotalLineTextStyle,
|
||||
} from '@/components';
|
||||
import { useBillTotals } from './utils';
|
||||
import { useBillAggregatedTaxRates, useBillTotals } from './utils';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
|
||||
export function BillFormFooterRight() {
|
||||
const {
|
||||
@@ -19,26 +18,46 @@ export function BillFormFooterRight() {
|
||||
formattedPaymentTotal,
|
||||
} = useBillTotals();
|
||||
|
||||
const {
|
||||
values: { inclusive_exclusive_tax, currency_code },
|
||||
} = useFormikContext();
|
||||
|
||||
const taxEntries = useBillAggregatedTaxRates();
|
||||
|
||||
return (
|
||||
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||
<TotalLine
|
||||
title={<T id={'bill_form.label.subtotal'} />}
|
||||
title={
|
||||
<>
|
||||
{inclusive_exclusive_tax === TaxType.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={'bill_form.label.total'} />}
|
||||
title={`Total (${currency_code})`}
|
||||
value={formattedTotal}
|
||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
<TotalLine
|
||||
title={<T id={'bill_form.label.total'} />}
|
||||
title={'Paid Amount'}
|
||||
value={formattedPaymentTotal}
|
||||
borderStyle={TotalLineBorderStyle.None}
|
||||
/>
|
||||
<TotalLine
|
||||
title={<T id={'bill_form.label.total'} />}
|
||||
title={'Due Amount'}
|
||||
value={formattedDueTotal}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useCreateBill,
|
||||
useEditBill,
|
||||
} from '@/hooks/query';
|
||||
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||
|
||||
const BillFormContext = createContext();
|
||||
|
||||
@@ -83,6 +84,9 @@ function BillFormProvider({ billId, ...props }) {
|
||||
isSuccess: isBranchesSuccess,
|
||||
} = useBranches({}, { enabled: isBranchFeatureCan });
|
||||
|
||||
// Fetch tax rates.
|
||||
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
|
||||
|
||||
// Fetches the projects list.
|
||||
const {
|
||||
data: { projects },
|
||||
@@ -103,7 +107,10 @@ function BillFormProvider({ billId, ...props }) {
|
||||
|
||||
// Determines whether the warehouse and branches are loading.
|
||||
const isFeatureLoading =
|
||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
|
||||
isWarehouesLoading ||
|
||||
isBranchesLoading ||
|
||||
isProjectsLoading ||
|
||||
isTaxRatesLoading;
|
||||
|
||||
const provider = {
|
||||
accounts,
|
||||
@@ -113,6 +120,7 @@ function BillFormProvider({ billId, ...props }) {
|
||||
warehouses,
|
||||
branches,
|
||||
projects,
|
||||
taxRates,
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
|
||||
@@ -124,6 +132,7 @@ function BillFormProvider({ billId, ...props }) {
|
||||
isFeatureLoading,
|
||||
isBranchesSuccess,
|
||||
isWarehousesSuccess,
|
||||
isTaxRatesLoading,
|
||||
|
||||
createBillMutate,
|
||||
editBillMutate,
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
||||
import { FastField } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
import { entriesFieldShouldUpdate } from './utils';
|
||||
import { ITEM_TYPE } from '@/containers/Entries/utils';
|
||||
|
||||
/**
|
||||
* Bill form body.
|
||||
* Bill form body.
|
||||
*/
|
||||
export default function BillFormBody({ defaultBill }) {
|
||||
const { items } = useBillFormContext();
|
||||
const { items, taxRates } = useBillFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<FastField
|
||||
name={'entries'}
|
||||
items={items}
|
||||
shouldUpdate={entriesFieldShouldUpdate}
|
||||
>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<ItemsEntriesTable
|
||||
value={value}
|
||||
onChange={(entries) => {
|
||||
setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
currencyCode={values.currency_code}
|
||||
itemType={ITEM_TYPE.PURCHASABLE}
|
||||
landedCost={true}
|
||||
enableTaxRates={false}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
<FastField
|
||||
name={'entries'}
|
||||
items={items}
|
||||
shouldUpdate={entriesFieldShouldUpdate}
|
||||
>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<ItemsEntriesTable
|
||||
value={value}
|
||||
onChange={(entries) => {
|
||||
setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
currencyCode={values.currency_code}
|
||||
itemType={ITEM_TYPE.PURCHASABLE}
|
||||
taxRates={taxRates}
|
||||
landedCost={true}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
import { first } from 'lodash';
|
||||
import { first, chain } from 'lodash';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { AppToaster } from '@/components';
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import {
|
||||
updateItemsEntriesTotal,
|
||||
ensureEntriesHaveEmptyLine,
|
||||
assignEntriesTaxAmount,
|
||||
aggregateItemEntriesTaxRates,
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import {
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
getEntriesTotal,
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -37,6 +40,9 @@ export const defaultBillEntry = {
|
||||
description: '',
|
||||
amount: '',
|
||||
landed_cost: false,
|
||||
tax_rate_id: '',
|
||||
tax_rate: '',
|
||||
tax_amount: '',
|
||||
};
|
||||
|
||||
// Default bill.
|
||||
@@ -46,6 +52,7 @@ export const defaultBill = {
|
||||
bill_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
inclusive_exclusive_tax: TaxType.Inclusive,
|
||||
note: '',
|
||||
open: '',
|
||||
branch_id: '',
|
||||
@@ -82,6 +89,9 @@ export const transformToEditForm = (bill) => {
|
||||
|
||||
return {
|
||||
...transformToForm(bill, defaultBill),
|
||||
inclusive_exclusive_tax: bill.is_inclusive_tax
|
||||
? TaxType.Inclusive
|
||||
: TaxType.Exclusive,
|
||||
entries,
|
||||
};
|
||||
};
|
||||
@@ -228,11 +238,12 @@ export const useSetPrimaryWarehouseToForm = () => {
|
||||
*/
|
||||
export const useBillTotals = () => {
|
||||
const {
|
||||
values: { entries, currency_code: currencyCode },
|
||||
values: { currency_code: currencyCode },
|
||||
} = useFormikContext();
|
||||
|
||||
// Retrieves the bili entries total.
|
||||
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
|
||||
// Retrieves the bill subtotal.
|
||||
const subtotal = useBillSubtotal();
|
||||
const total = useBillTotal();
|
||||
|
||||
// Retrieves the formatted total money.
|
||||
const formattedTotal = React.useMemo(
|
||||
@@ -241,8 +252,8 @@ export const useBillTotals = () => {
|
||||
);
|
||||
// Retrieves the formatted subtotal.
|
||||
const formattedSubtotal = React.useMemo(
|
||||
() => formattedAmount(total, currencyCode, { money: false }),
|
||||
[total, currencyCode],
|
||||
() => formattedAmount(subtotal, currencyCode, { money: false }),
|
||||
[subtotal, currencyCode],
|
||||
);
|
||||
// Retrieves the payment total.
|
||||
const paymentTotal = React.useMemo(() => 0, []);
|
||||
@@ -288,3 +299,86 @@ export const useBillIsForeignCustomer = () => {
|
||||
);
|
||||
return isForeignCustomer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-calculates the entries tax amount when editing.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const composeEntriesOnEditInclusiveTax = (
|
||||
inclusiveExclusiveTax: string,
|
||||
entries,
|
||||
) => {
|
||||
return R.compose(
|
||||
assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'),
|
||||
)(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the bill aggregated tax rates.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export const useBillAggregatedTaxRates = () => {
|
||||
const { values } = useFormikContext();
|
||||
const { taxRates } = useBillFormContext();
|
||||
|
||||
const aggregateTaxRates = React.useMemo(
|
||||
() => aggregateItemEntriesTaxRates(taxRates),
|
||||
[taxRates],
|
||||
);
|
||||
// Calculate the total tax amount of bill entries.
|
||||
return React.useMemo(() => {
|
||||
return aggregateTaxRates(values.entries);
|
||||
}, [aggregateTaxRates, values.entries]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the bill subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
export const useBillSubtotal = () => {
|
||||
const {
|
||||
values: { entries },
|
||||
} = useFormikContext();
|
||||
|
||||
// Calculate the total due amount of bill entries.
|
||||
return React.useMemo(() => getEntriesTotal(entries), [entries]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the bill total tax amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
export const useBillTotalTaxAmount = () => {
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return React.useMemo(() => {
|
||||
return chain(values.entries)
|
||||
.filter((entry) => entry.tax_amount)
|
||||
.sumBy('tax_amount')
|
||||
.value();
|
||||
}, [values.entries]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the tax is exclusive.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const useIsBillTaxExclusive = () => {
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return values.inclusive_exclusive_tax === TaxType.Exclusive;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the bill total.
|
||||
* @returns {number}
|
||||
*/
|
||||
export const useBillTotal = () => {
|
||||
const subtotal = useBillSubtotal();
|
||||
const totalTaxAmount = useBillTotalTaxAmount();
|
||||
const isExclusiveTax = useIsBillTaxExclusive();
|
||||
|
||||
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))(
|
||||
subtotal,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,20 +9,18 @@ import {
|
||||
Tag,
|
||||
ProgressBar,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import clsx from 'classnames';
|
||||
import {
|
||||
FormatDateCell,
|
||||
FormattedMessage as T,
|
||||
Icon,
|
||||
If,
|
||||
Choose,
|
||||
Money,
|
||||
Can,
|
||||
} from '@/components';
|
||||
import {
|
||||
formattedAmount,
|
||||
safeCallback,
|
||||
isBlank,
|
||||
calculateStatus,
|
||||
} from '@/utils';
|
||||
import {
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
PaymentMadeAction,
|
||||
AbilitySubject,
|
||||
} from '@/constants/abilityOption';
|
||||
import { CLASSES } from '@/constants';
|
||||
|
||||
/**
|
||||
* Actions menu.
|
||||
@@ -101,17 +100,6 @@ export function ActionsMenu({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount accessor.
|
||||
*/
|
||||
export function AmountAccessor(bill) {
|
||||
return !isBlank(bill.amount) ? (
|
||||
<Money amount={bill.amount} currency={bill.currency_code} />
|
||||
) : (
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Status accessor.
|
||||
*/
|
||||
@@ -198,11 +186,11 @@ export function useBillsTableColumns() {
|
||||
{
|
||||
id: 'amount',
|
||||
Header: intl.get('amount'),
|
||||
accessor: AmountAccessor,
|
||||
accessor: 'total_formatted',
|
||||
width: 120,
|
||||
className: 'amount',
|
||||
align: 'right',
|
||||
clickable: true,
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
|
||||
@@ -3,7 +3,8 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { InclusiveButtonOptions } from './constants';
|
||||
import { Box, FFormGroup, FSelect } from '@/components';
|
||||
import { FFormGroup, FSelect } from '@/components';
|
||||
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
|
||||
import { composeEntriesOnEditInclusiveTax } from './utils';
|
||||
|
||||
/**
|
||||
@@ -12,9 +13,9 @@ import { composeEntriesOnEditInclusiveTax } from './utils';
|
||||
*/
|
||||
export function InvoiceFormActions() {
|
||||
return (
|
||||
<InvoiceFormActionsRoot>
|
||||
<EntriesActionsBar>
|
||||
<InvoiceExclusiveInclusiveSelect />
|
||||
</InvoiceFormActionsRoot>
|
||||
</EntriesActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ export function InvoiceExclusiveInclusiveSelect(props) {
|
||||
label={'Amounts are'}
|
||||
inline={true}
|
||||
>
|
||||
<InclusiveSelect
|
||||
<FSelect
|
||||
name={'inclusive_exclusive_tax'}
|
||||
items={InclusiveButtonOptions}
|
||||
textAccessor={'label'}
|
||||
@@ -57,23 +58,5 @@ export function InvoiceExclusiveInclusiveSelect(props) {
|
||||
}
|
||||
|
||||
const InclusiveFormGroup = styled(FFormGroup)`
|
||||
margin-bottom: 0;
|
||||
margin-left: auto;
|
||||
|
||||
&.bp4-form-group.bp4-inline label.bp4-label {
|
||||
line-height: 1.25;
|
||||
opacity: 0.6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const InclusiveSelect = styled(FSelect)`
|
||||
.bp4-button {
|
||||
padding-right: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const InvoiceFormActionsRoot = styled(Box)`
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TotalLineTextStyle,
|
||||
} from '@/components';
|
||||
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
|
||||
import { TaxType } from '@/interfaces/TaxRates';
|
||||
|
||||
export function InvoiceFormFooterRight() {
|
||||
// Calculate the total due amount of invoice entries.
|
||||
@@ -32,7 +33,7 @@ export function InvoiceFormFooterRight() {
|
||||
<TotalLine
|
||||
title={
|
||||
<>
|
||||
{inclusive_exclusive_tax === 'inclusive'
|
||||
{inclusive_exclusive_tax === TaxType.Inclusive
|
||||
? 'Subtotal (Tax Inclusive)'
|
||||
: 'Subtotal'}
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import intl from 'react-intl-universal';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { omit, first, keyBy, sumBy, groupBy } from 'lodash';
|
||||
import { compose, transformToForm, repeatValue } from '@/utils';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
|
||||
import { omit, first, sumBy } from 'lodash';
|
||||
import {
|
||||
compose,
|
||||
transformToForm,
|
||||
repeatValue,
|
||||
formattedAmount,
|
||||
defaultFastFieldShouldUpdate,
|
||||
} from '@/utils';
|
||||
import { ERROR } from '@/constants/errors';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import {
|
||||
aggregateItemEntriesTaxRates,
|
||||
assignEntriesTaxAmount,
|
||||
getEntriesTotal,
|
||||
} from '@/containers/Entries/utils';
|
||||
@@ -327,28 +332,14 @@ export const useInvoiceAggregatedTaxRates = () => {
|
||||
const { values } = useFormikContext();
|
||||
const { taxRates } = useInvoiceFormContext();
|
||||
|
||||
const taxRatesById = useMemo(() => keyBy(taxRates, 'id'), [taxRates]);
|
||||
|
||||
const aggregateTaxRates = React.useMemo(
|
||||
() => aggregateItemEntriesTaxRates(taxRates),
|
||||
[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]);
|
||||
return aggregateTaxRates(values.entries);
|
||||
}, [aggregateTaxRates, values.entries]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,12 +21,9 @@ label.bp4-label {
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bp4-form-group.bp4-inline & {
|
||||
margin: 0 10px 0 0;
|
||||
line-height: 1.6;
|
||||
padding-top: calc(0.3rem + 1px);
|
||||
padding-bottom: calc(0.3rem + 1px);
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user