diff --git a/src/CONST.ts b/src/CONST.ts index 2b3cc7c09708..3b660997dbb7 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1827,6 +1827,8 @@ const CONST = { RECEIPT: 'receipt', DISTANCE: 'distance', TAG: 'tag', + TAX_RATE: 'taxRate', + TAX_AMOUNT: 'taxAmount', }, FOOTER: { EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`, diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index afc0c7b703dc..546b2885e24f 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -10,6 +10,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -21,6 +22,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; @@ -204,6 +206,11 @@ const defaultProps = { isPolicyExpenseChat: false, }; +const getTaxAmount = (transaction, defaultTaxValue) => { + const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || ''; + return TransactionUtils.calculateTaxAmount(percentage, transaction.amount); +}; + function MoneyTemporaryForRefactorRequestConfirmationList({ bankAccountRoute, canModifyParticipants, @@ -277,7 +284,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy && lodashGet(policy, 'tax.trackingEnabled', policy.isTaxTrackingEnabled); + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy); // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); @@ -292,9 +299,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); - const defaultTaxKey = taxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName; + const taxRateTitle = TransactionUtils.getDefaultTaxName(taxRates, transaction); + + const previousTransactionAmount = usePrevious(transaction.amount); const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -362,6 +369,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency); }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); + // Calculate and set tax amount in transaction draft + useEffect(() => { + const taxAmount = getTaxAmount(transaction, taxRates.defaultValue); + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); + + if (transaction.taxAmount && previousTransactionAmount === transaction.amount) { + return IOU.setMoneyRequestTaxAmount(transaction.transactionID, transaction.taxAmount, true); + } + + IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true); + }, [taxRates.defaultValue, transaction, previousTransactionAmount]); + /** * Returns the participants with amount * @param {Array} participants @@ -855,7 +874,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ key={`${taxRates.name}${formattedTaxAmount}`} shouldShowRightIcon={!isReadOnly} title={formattedTaxAmount} - description={taxRates.name} + description={translate('iou.taxAmount')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 074baa586dba..c052843fefe2 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -23,6 +23,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -100,6 +101,8 @@ function MoneyRequestView({ const { created: transactionDate, amount: transactionAmount, + taxAmount: transactionTaxAmount, + taxCode: transactionTaxCode, currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, @@ -119,6 +122,15 @@ function MoneyRequestView({ const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : ''; const isApproved = ReportUtils.isReportApproved(moneyRequestReport); + const taxRates = policy?.taxRates; + const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : ''; + + const taxRatesDescription = taxRates?.name; + const taxRateTitle = + taxRates && + (transactionTaxCode === taxRates?.defaultExternalID + ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction) + : transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode)); // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); @@ -147,6 +159,9 @@ function MoneyRequestView({ const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); + // A flag for showing tax rate + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount; + const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback( (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, @@ -423,6 +438,31 @@ function MoneyRequestView({ /> )} + {shouldShowTax && ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_RATE))} + /> + + )} + + {shouldShowTax && ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT))} + /> + + )} {shouldShowBillable && ( diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 936bd23b530d..4980025024ed 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -1,22 +1,31 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useMemo, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; -import type {TaxRatesWithDefault} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; import SelectionList from './SelectionList'; import RadioListItem from './SelectionList/RadioListItem'; import type {ListItem} from './SelectionList/types'; -type TaxPickerProps = { - /** Collection of tax rates attached to a policy */ - taxRates?: TaxRatesWithDefault; +type TaxPickerOnyxProps = { + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; +}; +type TaxPickerProps = TaxPickerOnyxProps & { /** The selected tax rate of an expense */ selectedTaxRate?: string; + /** ID of the policy */ + // eslint-disable-next-line react/no-unused-prop-types + policyID?: string; + /** * Safe area insets required for reflecting the portion of the view, * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. @@ -27,18 +36,17 @@ type TaxPickerProps = { onSubmit: (tax: ListItem) => void; }; -function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPickerProps) { +function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); + const taxRates = policy?.taxRates; const taxRatesCount = TransactionUtils.getEnabledTaxRateCount(taxRates?.taxes ?? {}); const isTaxRatesCountBelowThreshold = taxRatesCount < CONST.TAX_RATES_LIST_THRESHOLD; const shouldShowTextInput = !isTaxRatesCountBelowThreshold; - const getTaxName = useCallback((key: string) => taxRates?.taxes[key]?.name, [taxRates?.taxes]); - const selectedOptions = useMemo(() => { if (!selectedTaxRate) { return []; @@ -46,36 +54,39 @@ function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPicker return [ { - name: getTaxName(selectedTaxRate), + name: selectedTaxRate, enabled: true, accountID: null, }, ]; - }, [selectedTaxRate, getTaxName]); + }, [selectedTaxRate]); - const sections = useMemo( - () => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue, selectedTaxRate), - [taxRates, searchValue, selectedOptions, selectedTaxRate], - ); + const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue); + const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); + return ( ); } TaxPicker.displayName = 'TaxPicker'; -export default TaxPicker; +export default withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, +})(TaxPicker); diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fd84e65c028e..9e6d4a6a5ecf 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -143,6 +143,8 @@ const WRITE_COMMANDS = { UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable', UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant', UPDATE_MONEY_REQUEST_TAG: 'UpdateMoneyRequestTag', + UPDATE_MONEY_REQUEST_TAX_AMOUNT: 'UpdateMoneyRequestTaxAmount', + UPDATE_MONEY_REQUEST_TAX_RATE: 'UpdateMoneyRequestTaxRate', UPDATE_MONEY_REQUEST_DISTANCE: 'UpdateMoneyRequestDistance', UPDATE_MONEY_REQUEST_CATEGORY: 'UpdateMoneyRequestCategory', UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription', @@ -329,6 +331,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG]: Parameters.UpdateMoneyRequestParams; + [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_AMOUNT]: Parameters.UpdateMoneyRequestParams; + [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_RATE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams; diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 8f1cb89d695b..0d961ea27115 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -55,6 +55,14 @@ function buildMessageFragmentForValue( } } +/** + * Get the absolute value for a tax amount. + */ +function getTaxAmountAbsValue(taxAmount: number): number { + // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value + return Math.abs(taxAmount ?? 0); +} + /** * Get the message line for a modified expense. */ @@ -116,6 +124,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr 'currency' in reportActionOriginalMessage; const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; + if (hasModifiedAmount) { const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? ''; const oldAmountValue = reportActionOriginalMessage?.oldAmount ?? 0; @@ -216,6 +225,29 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr }); } + const hasModifiedTaxAmount = reportActionOriginalMessage && 'oldTaxAmount' in reportActionOriginalMessage && 'taxAmount' in reportActionOriginalMessage; + if (hasModifiedTaxAmount) { + const currency = reportActionOriginalMessage?.currency; + + const taxAmount = CurrencyUtils.convertToDisplayString(getTaxAmountAbsValue(reportActionOriginalMessage?.taxAmount ?? 0), currency); + const oldTaxAmountValue = getTaxAmountAbsValue(reportActionOriginalMessage?.oldTaxAmount ?? 0); + const oldTaxAmount = oldTaxAmountValue > 0 ? CurrencyUtils.convertToDisplayString(oldTaxAmountValue, currency) : ''; + buildMessageFragmentForValue(taxAmount, oldTaxAmount, Localize.translateLocal('iou.taxAmount'), false, setFragments, removalFragments, changeFragments); + } + + const hasModifiedTaxRate = reportActionOriginalMessage && 'oldTaxRate' in reportActionOriginalMessage && 'taxRate' in reportActionOriginalMessage; + if (hasModifiedTaxRate) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.taxRate ?? '', + reportActionOriginalMessage?.oldTaxRate ?? '', + Localize.translateLocal('iou.taxRate'), + false, + setFragments, + removalFragments, + changeFragments, + ); + } + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { buildMessageFragmentForValue( diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7e4082bff481..ca44931e7e8e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1191,8 +1191,8 @@ function hasEnabledTags(policyTagList: Array * @param taxRates - The original tax rates object. * @returns The transformed tax rates object.g */ -function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record { - const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID; +function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record { + const defaultTaxKey = taxRates?.defaultExternalID; const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; @@ -1212,7 +1212,7 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] { function getTaxRatesOptions(taxRates: Array>): Option[] { return taxRates.map((taxRate) => ({ text: taxRate.modifiedName, - keyForList: taxRate.code, + keyForList: taxRate.modifiedName, searchText: taxRate.modifiedName, tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, @@ -1223,10 +1223,10 @@ function getTaxRatesOptions(taxRates: Array>): Option[] { /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] { const policyRatesSections = []; - const taxes = transformedTaxRates(taxRates, defaultTaxKey); + const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 0a8437a5afaf..d393f8e64fba 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -244,6 +244,10 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE; } +function isTaxPolicyEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry): boolean { + return (isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled)) ?? false; +} + /** * Checks if policy's scheduled submit / auto reporting frequency is "instant". * Note: Free policies have "instant" submit always enabled. @@ -328,6 +332,7 @@ export { isInstantSubmitEnabled, isFreeGroupPolicy, isPolicyAdmin, + isTaxPolicyEnabled, isSubmitAndClose, getMemberAccountIDsForWorkspace, getIneligibleInvitees, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a2bf892b96b4..8296e38411be 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -99,6 +99,10 @@ type ExpenseOriginalMessage = { oldTag?: string; billable?: string; oldBillable?: string; + oldTaxAmount?: number; + taxAmount?: number; + taxRate?: string; + oldTaxRate?: string; }; type SpendBreakdown = { @@ -327,6 +331,8 @@ type OptimisticTaskReport = Pick< type TransactionDetails = { created: string; amount: number; + taxAmount?: number; + taxCode?: string; currency: string; merchant: string; waypoints?: WaypointCollection | string; @@ -2290,6 +2296,8 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), + taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), + taxCode: TransactionUtils.getTaxCode(transaction), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), @@ -2714,7 +2722,12 @@ function getReportPreviewMessage( * * At the moment, we only allow changing one transaction field at a time. */ -function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: TransactionChanges, isFromExpenseReport: boolean): ExpenseOriginalMessage { +function getModifiedExpenseOriginalMessage( + oldTransaction: OnyxEntry, + transactionChanges: TransactionChanges, + isFromExpenseReport: boolean, + policy: OnyxEntry, +): ExpenseOriginalMessage { const originalMessage: ExpenseOriginalMessage = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created @@ -2750,6 +2763,16 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: TransactionChanges, isFromExpenseReport: boolean, + policy: OnyxEntry, ): OptimisticModifiedExpenseReportAction { - const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); + const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport, policy); return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, actorAccountID: currentUserAccountID, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index cdd23aac1596..430100e84b2f 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,11 +4,12 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; +import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; import {getCleanedTagName} from './PolicyUtils'; @@ -207,6 +208,16 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra shouldStopSmartscan = true; } + if (Object.hasOwn(transactionChanges, 'taxAmount') && typeof transactionChanges.taxAmount === 'number') { + updatedTransaction.taxAmount = isFromExpenseReport ? -transactionChanges.taxAmount : transactionChanges.taxAmount; + shouldStopSmartscan = true; + } + + if (Object.hasOwn(transactionChanges, 'taxCode') && typeof transactionChanges.taxCode === 'string') { + updatedTransaction.taxCode = transactionChanges.taxCode; + shouldStopSmartscan = true; + } + if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') { updatedTransaction.billable = transactionChanges.billable; } @@ -240,6 +251,8 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra ...(Object.hasOwn(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }; return updatedTransaction; @@ -280,6 +293,27 @@ function getAmount(transaction: OnyxEntry, isFromExpenseReport = fa return amount ? -amount : 0; } +/** + * Return the tax amount field from the transaction. + */ +function getTaxAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { + // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value + if (!isFromExpenseReport) { + return Math.abs(transaction?.taxAmount ?? 0); + } + + // To avoid -0 being shown, lets only change the sign if the value is other than 0. + const amount = transaction?.taxAmount ?? 0; + return amount ? -amount : 0; +} + +/** + * Return the tax code from the transaction. + */ +function getTaxCode(transaction: OnyxEntry): string { + return transaction?.taxCode ?? ''; +} + /** * Return the currency field from the transaction, return the modifiedCurrency if present. */ @@ -586,9 +620,29 @@ function getEnabledTaxRateCount(options: TaxRates) { return Object.values(options).filter((option: TaxRate) => !option.isDisabled).length; } +/** + * Gets the default tax name + */ +function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction: Transaction) { + const defaultTaxKey = taxRates.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${Localize.translateLocal('common.default')}`) || ''; + return transaction?.taxRate?.text ?? defaultTaxName; +} + +/** + * Gets the tax name + */ +function getTaxName(taxes: TaxRates, transactionTaxCode: string) { + const taxName = `${taxes[transactionTaxCode].name}`; + const taxValue = `${taxes[transactionTaxCode].value}`; + return transactionTaxCode ? `${taxName} (${taxValue})` : ''; +} + export { buildOptimisticTransaction, calculateTaxAmount, + getTaxName, + getDefaultTaxName, getEnabledTaxRateCount, getUpdatedTransaction, getDescription, @@ -597,6 +651,8 @@ export { isManualRequest, isScanRequest, getAmount, + getTaxAmount, + getTaxCode, getCurrency, getDistance, getCardID, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 92e05996fe3d..761ae02ea4c6 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1533,7 +1533,7 @@ function getUpdateMoneyRequestParams( // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport, policy); if (!hasPendingWaypoints) { params.reportActionID = updatedReportAction.reportActionID; @@ -1743,6 +1743,7 @@ function getUpdateTrackExpenseParams( transactionThreadReportID: string, transactionChanges: TransactionChanges, onlyIncludeChangedFields: boolean, + policy: OnyxEntry, ): UpdateMoneyRequestData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; @@ -1809,7 +1810,7 @@ function getUpdateTrackExpenseParams( // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false); + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false, policy); if (!hasPendingWaypoints) { params.reportActionID = updatedReportAction.reportActionID; @@ -1922,7 +1923,7 @@ function updateMoneyRequestDate( const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; let data: UpdateMoneyRequestData; if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { - data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); } @@ -1961,7 +1962,7 @@ function updateMoneyRequestMerchant( const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; let data: UpdateMoneyRequestData; if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { - data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); } @@ -1985,6 +1986,38 @@ function updateMoneyRequestTag( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG, params, onyxData); } +/** Updates the created tax amount of a money request */ +function updateMoneyRequestTaxAmount( + transactionID: string, + optimisticReportActionID: string, + taxAmount: number, + policy: OnyxEntry, + policyTagList: OnyxEntry, + policyCategories: OnyxEntry, +) { + const transactionChanges = { + taxAmount, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true); + API.write('UpdateMoneyRequestTaxAmount', params, onyxData); +} + +/** Updates the created tax rate of a money request */ +function updateMoneyRequestTaxRate( + transactionID: string, + optimisticReportActionID: string, + taxCode: string, + policy: OnyxEntry, + policyTagList: OnyxEntry, + policyCategories: OnyxEntry, +) { + const transactionChanges = { + taxCode, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true); + API.write('UpdateMoneyRequestTaxRate', params, onyxData); +} + /** Updates the waypoints of a distance money request */ function updateMoneyRequestDistance( transactionID: string, @@ -2000,7 +2033,7 @@ function updateMoneyRequestDistance( const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; let data: UpdateMoneyRequestData; if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { - data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); } @@ -2039,7 +2072,7 @@ function updateMoneyRequestDescription( const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; let data: UpdateMoneyRequestData; if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { - data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); } @@ -3330,7 +3363,7 @@ function editRegularMoneyRequest( const isFromExpenseReport = ReportUtils.isExpenseReport(iouReport); // STEP 2: Build new modified expense report action. - const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport, policy); const updatedTransaction = transaction ? TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) : null; // STEP 3: Compute the IOU total and update the report preview message so LHN amount owed is correct @@ -5067,8 +5100,8 @@ function setMoneyRequestTaxRate(transactionID: string, taxRate: TaxRate) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxRate}); } -function setMoneyRequestTaxAmount(transactionID: string, taxAmount: number) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxAmount}); +function setMoneyRequestTaxAmount(transactionID: string, taxAmount: number, isDraft: boolean) { + Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {taxAmount}); } function setMoneyRequestBillable(billable: boolean) { @@ -5280,6 +5313,8 @@ export { updateMoneyRequestBillable, updateMoneyRequestMerchant, updateMoneyRequestTag, + updateMoneyRequestTaxAmount, + updateMoneyRequestTaxRate, updateMoneyRequestDistance, updateMoneyRequestCategory, updateMoneyRequestAmountAndCurrency, diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index d3941dca044e..ff2c2c5ce6ea 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -8,10 +8,12 @@ import ScreenWrapper from '@components/ScreenWrapper'; import tagPropTypes from '@components/tagPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import compose from '@libs/compose'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; @@ -19,6 +21,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; +import EditRequestTaxAmountPage from './EditRequestTaxAmountPage'; +import EditRequestTaxRatePage from './EditRequestTaxRatePage'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import reportPropTypes from './reportPropTypes'; import {policyPropTypes} from './workspace/withPolicy'; @@ -68,10 +72,17 @@ const defaultProps = { transaction: {}, }; +const getTaxAmount = (transactionAmount, transactionTaxCode, taxRates) => { + const percentage = (transactionTaxCode ? taxRates.taxes[transactionTaxCode].value : taxRates.defaultValue) || ''; + return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transactionAmount))); +}; + function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); - const {tag: transactionTag} = ReportUtils.getTransactionDetails(transaction); + const {taxAmount: transactionTaxAmount, taxCode: transactionTaxCode, currency: transactionCurrency, tag: transactionTag} = ReportUtils.getTransactionDetails(transaction); + + const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); @@ -80,12 +91,23 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + const taxRates = lodashGet(policy, 'taxRates', {}); + + const taxRateTitle = + taxRates && + (transactionTaxCode === taxRates.defaultExternalID + ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction) + : transactionTaxCode && TransactionUtils.getTaxName(taxRates.taxes, transactionTaxCode)); + // A flag for verifying that the current report is a sub-report of a workspace chat const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); // A flag for showing the tags page const shouldShowTags = useMemo(() => isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)), [isPolicyExpenseChat, policyTagLists, transactionTag]); + // A flag for showing tax rate + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy); + // Decides whether to allow or disallow editing a money request useEffect(() => { // Do not dismiss the modal, when a current user can edit this property of the money request. @@ -99,6 +121,35 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p }); }, [parentReportAction, fieldToEdit]); + const updateTaxAmount = useCallback( + (transactionChanges) => { + const newTaxAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(transactionChanges.amount)); + + if (newTaxAmount === TransactionUtils.getTaxAmount(transaction)) { + Navigation.dismissModal(); + return; + } + IOU.updateMoneyRequestTaxAmount(transaction.transactionID, report.reportID, newTaxAmount, policy, policyTags, policyCategories); + Navigation.dismissModal(report.reportID); + }, + [transaction, report, policy, policyTags, policyCategories], + ); + + const updateTaxRate = useCallback( + (transactionChanges) => { + const newTaxCode = transactionChanges.data.code; + + if (newTaxCode === undefined || newTaxCode === TransactionUtils.getTaxCode(transaction)) { + Navigation.dismissModal(); + return; + } + + IOU.updateMoneyRequestTaxRate(transaction.transactionID, report.reportID, newTaxCode, policy, policyTags, policyCategories); + Navigation.dismissModal(report.reportID); + }, + [transaction, report, policy, policyTags, policyCategories], + ); + const saveTag = useCallback( ({tag: newTag}) => { let updatedTag = newTag; @@ -131,6 +182,27 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p ); } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT && shouldShowTax) { + return ( + + ); + } + + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_RATE && shouldShowTax) { + return ( + + ); + } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { return ( void; +}; + +function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onSubmit}: EditRequestTaxAmountPageProps) { + const {translate} = useLocalize(); + const textInput = useRef(null); + + const focusTimeoutRef = useRef(null); + + useFocusEffect( + useCallback(() => { + focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + }; + }, []), + ); + + return ( + + + + + ); +} + +EditRequestTaxAmountPage.displayName = 'EditRequestTaxAmountPage'; + +export default EditRequestTaxAmountPage; diff --git a/src/pages/EditRequestTaxRatePage.tsx b/src/pages/EditRequestTaxRatePage.tsx new file mode 100644 index 000000000000..099851e92209 --- /dev/null +++ b/src/pages/EditRequestTaxRatePage.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TaxPicker from '@components/TaxPicker'; +import useLocalize from '@hooks/useLocalize'; + +type EditRequestTaxRatePageProps = { + /** Transaction default tax Rate value */ + defaultTaxRate: string; + + /** The policyID we are getting categories for */ + policyID: string; + + /** Callback to fire when the Save button is pressed */ + onSubmit: () => void; +}; + +function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditRequestTaxRatePageProps) { + const {translate} = useLocalize(); + + return ( + + {({insets}) => ( + <> + + + + )} + + ); +} + +EditRequestTaxRatePage.displayName = 'EditRequestTaxRatePage'; + +export default EditRequestTaxRatePage; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 9465c7e3edae..1c3cdefd4392 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -1,10 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import lodashIsEmpty from 'lodash/isEmpty'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; -import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import * as TransactionEdit from '@libs/actions/TransactionEdit'; @@ -41,24 +39,6 @@ const propTypes = { /** The draft transaction object being modified in Onyx */ draftTransaction: transactionPropTypes, - - /** The policy of the report */ - policy: PropTypes.shape({ - /** - * Whether or not the policy has tax tracking enabled - * - * @deprecated - use tax.trackingEnabled instead - */ - isTaxTrackingEnabled: PropTypes.bool, - - /** Whether or not the policy has tax tracking enabled */ - tax: PropTypes.shape({ - trackingEnabled: PropTypes.bool, - }), - - /** Collection of tax rates attached to a policy */ - taxRates: taxPropTypes, - }), }; const defaultProps = { @@ -66,12 +46,6 @@ const defaultProps = { transaction: {}, splitDraftTransaction: {}, draftTransaction: {}, - policy: {}, -}; - -const getTaxAmount = (transaction, defaultTaxValue, amount) => { - const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || ''; - return TransactionUtils.calculateTaxAmount(percentage, amount); }; function IOURequestStepAmount({ @@ -82,7 +56,6 @@ function IOURequestStepAmount({ transaction, splitDraftTransaction, draftTransaction, - policy, }) { const {translate} = useLocalize(); const textInput = useRef(null); @@ -96,10 +69,6 @@ function IOURequestStepAmount({ const {amount: transactionAmount} = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction); const {currency} = ReportUtils.getTransactionDetails(isEditing ? draftTransaction : transaction); - const taxRates = lodashGet(policy, 'taxRates', {}); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)); - const isTaxTrackingEnabled = isPolicyExpenseChat && lodashGet(policy, 'tax.trackingEnabled', policy.isTaxTrackingEnabled); - useFocusEffect( useCallback(() => { focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); @@ -156,12 +125,6 @@ function IOURequestStepAmount({ isSaveButtonPressed.current = true; const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); - if ((iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL || backTo) && isTaxTrackingEnabled) { - const taxAmount = getTaxAmount(transaction, taxRates.defaultValue, amountInSmallestCurrencyUnits); - const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits); - } - IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, true); if (backTo) { @@ -242,9 +205,6 @@ export default compose( return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; }, }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, draftTransaction: { key: ({route}) => { const transactionID = lodashGet(route, 'params.transactionID', 0); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 61ae0d2b67b7..0df9a7333e7a 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -90,7 +90,8 @@ function IOURequestStepConfirmation({ const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); const receiptType = lodashGet(transaction, 'receipt.type'); - const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList; + const foreignTaxDefault = lodashGet(policy, 'taxRates.foreignTaxDefault'); + const transactionTaxCode = transaction.taxRate ? transaction.taxRate.data.code : foreignTaxDefault; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); const headerTitle = useMemo(() => { diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index fb984cb801c1..3d5ddcc1a47f 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -2,18 +2,12 @@ import {useFocusEffect} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestAmountForm from '@pages/iou/steps/MoneyRequestAmountForm'; @@ -23,6 +17,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -47,8 +42,8 @@ const propTypes = { const defaultProps = { report: {}, - transaction: {}, policy: {}, + transaction: {}, }; const getTaxAmount = (transaction, defaultTaxValue) => { @@ -66,7 +61,6 @@ function IOURequestStepTaxAmountPage({ policy, }) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const textInput = useRef(null); const isEditing = Navigation.getActiveRoute().includes('taxAmount'); @@ -120,7 +114,7 @@ function IOURequestStepTaxAmountPage({ const updateTaxAmount = (currentAmount) => { isSaveButtonPressed.current = true; const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount.amount)); - IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits); + IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); IOU.setMoneyRequestCurrency_temporaryForRefactor(transactionID, currency || CONST.CURRENCY.USD, true); @@ -144,37 +138,24 @@ function IOURequestStepTaxAmountPage({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); }; - const content = ( - (textInput.current = e)} - onCurrencyButtonPress={navigateToCurrencySelectionPage} - onSubmitButtonPress={updateTaxAmount} - /> - ); - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - - {content} - - - )} - + (textInput.current = e)} + onCurrencyButtonPress={navigateToCurrencySelectionPage} + onSubmitButtonPress={updateTaxAmount} + /> + ); } diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js index 335964adf309..d4a2c10d24b0 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js @@ -3,8 +3,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; import TaxPicker from '@components/TaxPicker'; import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -14,9 +12,11 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -27,6 +27,9 @@ const propTypes = { /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, + /** The report attached to the transaction */ + report: reportPropTypes, + /* Onyx Props */ /** The policy of the report */ policy: PropTypes.shape({ @@ -36,6 +39,7 @@ const propTypes = { }; const defaultProps = { + report: {}, policy: {}, transaction: {}, }; @@ -51,46 +55,40 @@ function IOURequestStepTaxRatePage({ }, policy, transaction, + report, }) { const {translate} = useLocalize(); + const taxRates = lodashGet(policy, 'taxRates', {}); + const navigateBack = () => { Navigation.goBack(backTo); }; - const taxRates = lodashGet(policy, 'taxRates', {}); - const defaultTaxKey = taxRates.defaultExternalID; - const selectedTaxRate = (transaction.taxRate && transaction.taxRate.keyForList) || defaultTaxKey; + + const selectedTaxRate = TransactionUtils.getDefaultTaxName(taxRates, transaction); const updateTaxRates = (taxes) => { const taxAmount = getTaxAmount(taxRates, taxes.text, transaction.amount); const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); IOU.setMoneyRequestTaxRate(transaction.transactionID, taxes); - IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits); + IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true); Navigation.goBack(backTo); }; return ( - - {({insets}) => ( - <> - navigateBack()} - /> - - - )} - + + ); } diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index a010e13ff496..00970455fb8a 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -34,8 +34,11 @@ type MoneyRequestAmountFormProps = { /** Whether the amount is being edited or not */ isEditing?: boolean; + /** Whether the currency symbol is pressable */ + isCurrencyPressable?: boolean; + /** Fired when back button pressed, navigates to currency selection page */ - onCurrencyButtonPress: () => void; + onCurrencyButtonPress?: () => void; /** Fired when submit button pressed, saves the given amount and navigates to the next page */ onSubmitButtonPress: ({amount, currency}: {amount: string; currency: string}) => void; @@ -59,7 +62,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01; const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) => - isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(taxAmount); + isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(Math.abs(taxAmount)); const AMOUNT_VIEW_ID = 'amountView'; const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; @@ -70,6 +73,7 @@ function MoneyRequestAmountForm( amount = 0, taxAmount = 0, currency = CONST.CURRENCY.USD, + isCurrencyPressable = true, isEditing = false, onCurrencyButtonPress, onSubmitButtonPress, @@ -98,7 +102,7 @@ function MoneyRequestAmountForm( const forwardDeletePressedRef = useRef(false); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(taxAmount, currency); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(Math.abs(taxAmount), currency); /** * Event occurs when a user presses a mouse button over an DOM element. @@ -301,7 +305,7 @@ function MoneyRequestAmountForm( setSelection({start, end}); }} onKeyPress={textInputKeyPress} - isCurrencyPressable + isCurrencyPressable={isCurrencyPressable} /> {!!formError && ( { setForeignCurrencyDefault(policyID, keyForList ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); @@ -55,8 +58,7 @@ function WorkspaceTaxesSettingsForeignCurrency({ diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index 2fe2985daa22..c6de23069837 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -10,6 +10,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceCurrencyDefault} from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as TransactionUtils from '@libs/TransactionUtils'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; @@ -31,6 +32,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ const {translate} = useLocalize(); const styles = useThemeStyles(); + const selectedTaxRate = TransactionUtils.getTaxName(policy?.taxRates?.taxes ?? {}, policy?.taxRates?.foreignTaxDefault ?? ''); const submit = ({keyForList}: ListItem) => { setWorkspaceCurrencyDefault(policyID, keyForList ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); @@ -55,8 +57,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 196267dc28cc..2e24fe00539a 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -268,6 +268,10 @@ type OriginalMessageModifiedExpense = { category?: string; oldTag?: string; tag?: string; + oldTaxAmount?: number; + taxAmount?: number; + oldTaxRate?: string; + taxRate?: string; oldBillable?: string; billable?: string; }; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ddb0c33c2f0c..247eb64f48e9 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -39,7 +39,7 @@ type TaxRate = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Name of the a tax rate. */ name: string; - /** The value of the tax rate as percentage. */ + /** The value of the tax rate. */ value: string; /** The code associated with the tax rate. If a tax is created in old dot, code field is undefined */ diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 4c1d154fc1ad..1750fa61e514 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -102,6 +102,12 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** The original transaction amount */ amount: number; + /** The transaction tax amount */ + taxAmount?: number; + + /** The transaction tax code */ + taxCode?: string; + /** Whether the request is billable */ billable?: boolean; @@ -177,9 +183,6 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** The transaction tax rate */ taxRate?: TaxRate; - /** Tax amount */ - taxAmount?: number; - /** Card Transactions */ /** The parent transaction id */ diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index d89c81f58262..49ad848fe466 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -2301,7 +2301,7 @@ describe('OptionsListUtils', () => { // Adds 'Default' title to default tax. // Adds value to tax name for more description. text: 'Tax exempt 1 (0%) • Default', - keyForList: 'CODE1', + keyForList: 'Tax exempt 1 (0%) • Default', searchText: 'Tax exempt 1 (0%) • Default', tooltipText: 'Tax exempt 1 (0%) • Default', isDisabled: undefined, @@ -2315,7 +2315,7 @@ describe('OptionsListUtils', () => { }, { text: 'Tax option 3 (5%)', - keyForList: 'CODE3', + keyForList: 'Tax option 3 (5%)', searchText: 'Tax option 3 (5%)', tooltipText: 'Tax option 3 (5%)', isDisabled: undefined, @@ -2328,7 +2328,7 @@ describe('OptionsListUtils', () => { }, { text: 'Tax rate 2 (3%)', - keyForList: 'CODE2', + keyForList: 'Tax rate 2 (3%)', searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, @@ -2351,7 +2351,7 @@ describe('OptionsListUtils', () => { data: [ { text: 'Tax rate 2 (3%)', - keyForList: 'CODE2', + keyForList: 'Tax rate 2 (3%)', searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined,