From 06ee20c2d28fafaceb0cc81b1968d3f4377c8a36 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 5 Oct 2023 18:35:04 +0200 Subject: [PATCH] feat(webapp): assign tax rates to bill entries --- .../webapp/src/containers/Entries/utils.tsx | 36 +++++++- .../Bills/BillForm/BillFormFooterRight.tsx | 35 ++++++-- .../Bills/BillForm/BillFormProvider.tsx | 11 ++- .../Bills/BillForm/BillItemsEntriesEditor.tsx | 3 +- .../Purchases/Bills/BillForm/utils.tsx | 86 +++++++++++++++++-- .../InvoiceForm/InvoiceFormFooterRight.tsx | 3 +- .../Sales/Invoices/InvoiceForm/utils.tsx | 43 ++++------ 7 files changed, 171 insertions(+), 46 deletions(-) diff --git a/packages/webapp/src/containers/Entries/utils.tsx b/packages/webapp/src/containers/Entries/utils.tsx index 51ff04412..bfc62138e 100644 --- a/packages/webapp/src/containers/Entries/utils.tsx +++ b/packages/webapp/src/containers/Entries/utils.tsx @@ -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, + }; + }); +}); diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx index 825cb8eff..0657299d6 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx @@ -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 ( } + title={ + <> + {inclusive_exclusive_tax === TaxType.Inclusive + ? 'Subtotal (Tax Inclusive)' + : 'Subtotal'} + + } value={formattedSubtotal} borderStyle={TotalLineBorderStyle.None} /> + {taxEntries.map((tax, index) => ( + + ))} } + title={`Total (${currency_code})`} value={formattedTotal} borderStyle={TotalLineBorderStyle.SingleDark} textStyle={TotalLineTextStyle.Bold} /> } + title={'Paid Amount'} value={formattedPaymentTotal} borderStyle={TotalLineBorderStyle.None} /> } + title={'Due Amount'} value={formattedDueTotal} textStyle={TotalLineTextStyle.Bold} /> diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx index 7c717a9b1..d960891f0 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx @@ -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, diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx index c2a022ae2..a07e43fcb 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx @@ -9,7 +9,7 @@ import { ITEM_TYPE } from '@/containers/Entries/utils'; * Bill form body. */ export default function BillFormBody({ defaultBill }) { - const { items } = useBillFormContext(); + const { items, taxRates } = useBillFormContext(); return ( )} diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx index ff56831fd..9bc771198 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx @@ -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'; @@ -18,6 +18,7 @@ import { updateItemsEntriesTotal, ensureEntriesHaveEmptyLine, assignEntriesTaxAmount, + aggregateItemEntriesTaxRates, } from '@/containers/Entries/utils'; import { useCurrentOrganization } from '@/hooks/state'; import { @@ -85,7 +86,7 @@ export const transformToEditForm = (bill) => { return { ...transformToForm(bill, defaultBill), - inclusive_exclusive_tax: invoice.is_inclusive_tax + inclusive_exclusive_tax: bill.is_inclusive_tax ? TaxType.Inclusive : TaxType.Exclusive, entries, @@ -234,11 +235,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( @@ -247,8 +249,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, []); @@ -307,3 +309,73 @@ export const composeEntriesOnEditInclusiveTax = ( 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, + ); +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx index a82db600e..5c152e26b 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx @@ -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() { - {inclusive_exclusive_tax === 'inclusive' + {inclusive_exclusive_tax === TaxType.Inclusive ? 'Subtotal (Tax Inclusive)' : 'Subtotal'} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index 0e6e5dcba..4186cbc63 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -1,18 +1,23 @@ // @ts-nocheck -import React, { 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]); }; /**