diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3b98cf83f3cc..ba4f5f522c23 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6715,6 +6715,7 @@ const CONST = { CARD: this.TABLE_COLUMNS.CARD, CATEGORY: this.TABLE_COLUMNS.CATEGORY, TAG: this.TABLE_COLUMNS.TAG, + EXCHANGE_RATE: this.TABLE_COLUMNS.EXCHANGE_RATE, ORIGINAL_AMOUNT: this.TABLE_COLUMNS.ORIGINAL_AMOUNT, REPORT_ID: this.TABLE_COLUMNS.REPORT_ID, BASE_62_REPORT_ID: this.TABLE_COLUMNS.BASE_62_REPORT_ID, @@ -6848,6 +6849,7 @@ const CONST = { REPORT_ID: 'reportID', BASE_62_REPORT_ID: 'base62ReportID', TAX: 'tax', + EXCHANGE_RATE: 'exchangeRate', REIMBURSABLE_TOTAL: 'reimbursableTotal', NON_REIMBURSABLE_TOTAL: 'nonReimbursableTotal', }, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ac038e7f3435..de5625b95190 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -126,6 +126,7 @@ function mapTransactionItemToSelectedEntry( ), action: item.action, groupCurrency: item.groupCurrency, + groupExchangeRate: item.groupExchangeRate, reportID: item.reportID, policyID: item.report?.policyID, amount: item.modifiedAmount ?? item.amount, @@ -223,6 +224,7 @@ function prepareTransactionsList( amount: Math.abs(item.modifiedAmount || item.amount), groupAmount: item.groupAmount, groupCurrency: item.groupCurrency, + groupExchangeRate: item.groupExchangeRate, currency: item.currency, isFromOneTransactionReport: isOneTransactionReport(item.report), ownerAccountID: item.reportAction?.actorAccountID, @@ -570,6 +572,7 @@ function Search({ amount: transactionItem.modifiedAmount ?? transactionItem.amount, groupAmount: transactionItem.groupAmount, groupCurrency: transactionItem.groupCurrency, + groupExchangeRate: transactionItem.groupExchangeRate, currency: transactionItem.currency, ownerAccountID: transactionItem.reportAction?.actorAccountID, }; @@ -622,6 +625,7 @@ function Search({ amount: transactionItem.modifiedAmount ?? transactionItem.amount, groupAmount: transactionItem.groupAmount, groupCurrency: transactionItem.groupCurrency, + groupExchangeRate: transactionItem.groupExchangeRate, currency: transactionItem.currency, ownerAccountID: transactionItem.reportAction?.actorAccountID, }; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index e0997ba00990..74ad8c14fc44 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -56,6 +56,9 @@ type SelectedTransactionInfo = { /** The group currency if the transaction is grouped. Defaults to the active policy currency if group has no target currency */ groupCurrency?: string; + /** The exchange rate of the transaction if the transaction is grouped. Defaults to the exchange rate against the active policy currency if group has no target currency */ + groupExchangeRate?: number; + /** Whether it is the only expense of the parent expense report */ isFromOneTransactionReport?: boolean; diff --git a/src/components/SelectionListWithSections/SearchTableHeader.tsx b/src/components/SelectionListWithSections/SearchTableHeader.tsx index b3546a2637a6..27ec88f85a60 100644 --- a/src/components/SelectionListWithSections/SearchTableHeader.tsx +++ b/src/components/SelectionListWithSections/SearchTableHeader.tsx @@ -114,6 +114,10 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ translationKey: 'common.tax', canBeMissing: true, }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE, + translationKey: 'common.exchangeRate', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT, translationKey: 'common.originalAmount', diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index c5f3add0e7b5..a495ee95300b 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -27,6 +27,7 @@ import {isExpenseReport, isSettled} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import { getDescription, + getExchangeRate, getMerchant, getOriginalAmount, getOriginalCurrency, @@ -243,6 +244,8 @@ function TransactionItemRow({ } }, [transactionItem, translate, report]); + const exchangeRateMessage = getExchangeRate(transactionItem); + const columnComponent = useMemo( () => ({ [CONST.SEARCH.TABLE_COLUMNS.TYPE]: ( @@ -478,6 +481,11 @@ function TransactionItemRow({ /> ), + [CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE]: ( + + + + ), [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: ( { diff --git a/src/languages/de.ts b/src/languages/de.ts index d7b4707af93b..80dcd2164157 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -691,6 +691,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Aktion erforderlich', duplicate: 'Duplizieren', duplicated: 'Dupliziert', + exchangeRate: 'Wechselkurs', reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag', nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag', originalAmount: 'Ursprünglicher Betrag', diff --git a/src/languages/en.ts b/src/languages/en.ts index d56abd3489b8..d4d66599c6c0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -682,6 +682,7 @@ const translations = { actionRequired: 'Action required', duplicate: 'Duplicate', duplicated: 'Duplicated', + exchangeRate: 'Exchange rate', reimbursableTotal: 'Reimbursable total', nonReimbursableTotal: 'Non-reimbursable total', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index dedde21e08e7..d75bcacdb7f0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -378,6 +378,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Acción requerida', duplicate: 'Duplicar', duplicated: 'Duplicado', + exchangeRate: 'Tipo de cambio', reimbursableTotal: 'Total reembolsable', nonReimbursableTotal: 'Total no reembolsable', }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 9a4a60d46bac..a98d3b427516 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -693,6 +693,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Action requise', duplicate: 'Dupliquer', duplicated: 'Dupliqué', + exchangeRate: 'Taux de change', reimbursableTotal: 'Total remboursable', nonReimbursableTotal: 'Total non remboursable', originalAmount: 'Montant d’origine', diff --git a/src/languages/it.ts b/src/languages/it.ts index ba77bd5b59ed..9a761598cfd9 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -692,6 +692,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Azione richiesta', duplicate: 'Duplica', duplicated: 'Duplicato', + exchangeRate: 'Tasso di cambio', reimbursableTotal: 'Totale rimborsabile', nonReimbursableTotal: 'Totale non rimborsabile', originalAmount: 'Importo originale', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f4d6eebd9981..28d56c69a6e5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -691,6 +691,7 @@ const translations: TranslationDeepObject = { actionRequired: '対応が必要', duplicate: '複製', duplicated: '重複', + exchangeRate: '為替レート', reimbursableTotal: '経費精算対象の合計', nonReimbursableTotal: '非払い戻し合計', originalAmount: '元の金額', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 74558ae305a5..919cf4d608f4 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -692,6 +692,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Actie vereist', duplicate: 'Dupliceren', duplicated: 'Gedupliceerd', + exchangeRate: 'Wisselkoers', reimbursableTotal: 'Totaal te vergoeden', nonReimbursableTotal: 'Niet-vergoedbaar totaal', originalAmount: 'Oorspronkelijk bedrag', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 1376bedd0c99..e8de68789f50 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -692,6 +692,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Wymagane działanie', duplicate: 'Duplikat', duplicated: 'Zduplikowano', + exchangeRate: 'Kurs wymiany', reimbursableTotal: 'Łączna kwota podlegająca zwrotowi', nonReimbursableTotal: 'Suma niepodlegająca zwrotowi', originalAmount: 'Kwota pierwotna', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 4f314137f33a..03a5a38d879a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -691,6 +691,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Ação necessária', duplicate: 'Duplicar', duplicated: 'Duplicado', + exchangeRate: 'Taxa de câmbio', reimbursableTotal: 'Total reembolsável', nonReimbursableTotal: 'Total não reembolsável', originalAmount: 'Valor original', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4570d036f1f4..6f730225bab4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -688,6 +688,7 @@ const translations: TranslationDeepObject = { actionRequired: '需要操作', duplicate: '复制', duplicated: '已重复', + exchangeRate: '汇率', reimbursableTotal: '可报销总额', nonReimbursableTotal: '不可报销总额', originalAmount: '原始金额', diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 7cc3666de656..0469e4a43d4e 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -986,6 +986,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) case 'originalAmount': case 'convertedAmount': case 'groupAmount': + case 'groupExchangeRate': return validateNumber(value); case 'iouRequestType': return validateConstantEnum(value, CONST.IOU.REQUEST_TYPE); @@ -1107,6 +1108,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) taxValue: CONST.RED_BRICK_ROAD_PENDING_ACTION, groupAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, groupCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION, + groupExchangeRate: CONST.RED_BRICK_ROAD_PENDING_ACTION, splitsStartDate: CONST.RED_BRICK_ROAD_PENDING_ACTION, splitsEndDate: CONST.RED_BRICK_ROAD_PENDING_ACTION, }, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 276d88f7b9b4..cfe15dfb4d2b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -118,6 +118,7 @@ import {getIOUPayerAndReceiver} from './TransactionPreviewUtils'; import { getCategory, getDescription, + getExchangeRate, getTag, getTaxAmount, getAmount as getTransactionAmount, @@ -2221,6 +2222,14 @@ function getSortedTransactionData( }); } + if (sortBy === CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE) { + return data.sort((a, b) => { + const aExchangeRate = getExchangeRate(a); + const bExchangeRate = getExchangeRate(b); + return compareValues(aExchangeRate, bExchangeRate, sortOrder, sortBy, localeCompare, true); + }); + } + if (sortBy === CONST.SEARCH.TABLE_COLUMNS.TAX_RATE) { return data.sort((a, b) => { const aValue = `${a.policy?.taxRates?.taxes?.[a.taxCode ?? '']?.name ?? ''} (${a.policy?.taxRates?.taxes?.[a.taxCode ?? '']?.value ?? ''})`; @@ -2546,6 +2555,8 @@ function getSearchColumnTranslationKey(columnId: SearchCustomColumnIds): Transla return 'common.title'; case CONST.SEARCH.TABLE_COLUMNS.STATUS: return 'common.status'; + case CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE: + return 'common.exchangeRate'; case CONST.SEARCH.TABLE_COLUMNS.POLICY_NAME: return 'workspace.common.workspace'; case CONST.SEARCH.TABLE_COLUMNS.CARD: @@ -3069,6 +3080,7 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAX_RATE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: false, + [CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE]: false, [CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT]: false, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: true, [CONST.SEARCH.TABLE_COLUMNS.BASE_62_REPORT_ID]: false, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 982460203e3c..b52233516379 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -9,6 +9,7 @@ import type {ValueOf} from 'type-fest'; import type {Coordinate} from '@components/MapView/MapViewTypes'; import utils from '@components/MapView/utils'; import type {UnreportedExpenseListItemType} from '@components/SelectionListWithSections/types'; +import type {TransactionWithOptionalSearchFields} from '@components/TransactionItemRow'; import {getPolicyTagsData} from '@libs/actions/Policy/Tag'; import type {MergeDuplicatesParams} from '@libs/API/parameters'; import {getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; @@ -1047,6 +1048,24 @@ function getTagArrayFromName(tagName: string): string[] { return newMatches; } +/** + * Returns the exchange rate for a transaction, based on its group + */ +function getExchangeRate(transaction: TransactionWithOptionalSearchFields) { + const fromCurrency = getCurrency(transaction); + const toCurrency = transaction.groupCurrency ?? fromCurrency; + + if (!transaction.groupExchangeRate) { + return ''; + } + + if (Number(transaction.groupExchangeRate) === 1) { + return ''; + } + + return transaction.groupExchangeRate ? `${transaction.groupExchangeRate} ${fromCurrency}/${toCurrency}` : ''; +} + /** * Return the tag from the transaction. When the tagIndex is passed, return the tag based on the index. * This "tag" field has no "modified" complement. @@ -2407,6 +2426,7 @@ export { mergeProhibitedViolations, getOriginalAttendees, getReportOwnerAsAttendee, + getExchangeRate, shouldReuseInitialTransaction, }; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 54d8507c09f4..53469413b5fb 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1811,6 +1811,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.TITLE: case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: case CONST.SEARCH.TABLE_COLUMNS.IN: + case CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE: default: columnWidth = styles.flex1; } diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index b4c1b566c49c..03817227b3c4 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -170,6 +170,9 @@ type SearchTransaction = { /** The group currency if the transaction is grouped. Defaults to the active policy currency if group has no target currency */ groupCurrency?: string; + /** The exchange rate of the transaction if the transaction is grouped. Defaults to the exchange rate against the active policy currency if group has no target currency */ + groupExchangeRate?: number; + /** Reimbursable status of the transaction */ reimbursable?: boolean; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 715f46d494e9..3ff93f59613b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -451,6 +451,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** The group currency if the transaction is grouped. Defaults to the active policy currency if group has no target currency */ groupCurrency?: string; + /** The exchange rate of the transaction if the transaction is grouped. Defaults to the exchange rate against the active policy currency if group has no target currency */ + groupExchangeRate?: number; + /** Used during the creation flow before the transaction is saved to the server */ iouRequestType?: IOURequestType;