Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0c1d38c
add new cols for exchange rate
JS00001 Dec 16, 2025
d1148be
add exchange rate and en translations
JS00001 Dec 16, 2025
2f12abf
apply diff for langs
JS00001 Dec 16, 2025
7ef0200
add header
JS00001 Dec 16, 2025
4f3d144
also add text cell here
JS00001 Dec 16, 2025
201d7b2
start exchange rate
JS00001 Dec 16, 2025
823d68e
add exchange rate logic
JS00001 Dec 16, 2025
4674cf0
conversion cleanup
JS00001 Dec 16, 2025
dfd4688
abstract the method
JS00001 Dec 16, 2025
8bc5510
add sorting for exchange rate
JS00001 Dec 16, 2025
8c552e7
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 16, 2025
c9ef9fa
fix lint
JS00001 Dec 16, 2025
2359624
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 17, 2025
5d07543
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 17, 2025
ea01fad
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 17, 2025
7fa5a0e
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 17, 2025
641ef51
add new groupExchangeRate
JS00001 Dec 17, 2025
7ddd265
add a message for exchange rate
JS00001 Dec 17, 2025
92b49d2
create helper so sorting works
JS00001 Dec 17, 2025
5ace60b
handle more exchange rate data
JS00001 Dec 17, 2025
6fb73c8
fix tests
JS00001 Dec 17, 2025
2f10a4a
fix eslint
JS00001 Dec 17, 2025
bfa55d5
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 18, 2025
b05d112
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 18, 2025
2fcfd89
Merge branch 'main' of github.com:Expensify/App into jsenyitko-releas…
JS00001 Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
},
Expand Down
4 changes: 4 additions & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 11 additions & 2 deletions src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {isExpenseReport, isSettled} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {
getDescription,
getExchangeRate,
getMerchant,
getOriginalAmount,
getOriginalCurrency,
Expand Down Expand Up @@ -243,6 +244,8 @@ function TransactionItemRow({
}
}, [transactionItem, translate, report]);

const exchangeRateMessage = getExchangeRate(transactionItem);

const columnComponent = useMemo(
() => ({
[CONST.SEARCH.TABLE_COLUMNS.TYPE]: (
Expand Down Expand Up @@ -478,6 +481,11 @@ function TransactionItemRow({
/>
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE]: (
<View style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE)]}>
<TextCell text={exchangeRateMessage} />
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: (
<View
key={CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT}
Expand Down Expand Up @@ -556,7 +564,6 @@ function TransactionItemRow({
),
}),
[
translate,
StyleUtils,
transactionItem,
shouldShowTooltip,
Expand All @@ -572,17 +579,19 @@ function TransactionItemRow({
report?.total,
isApprovedColumnWide,
isPostedColumnWide,
translate,
isExportedColumnWide,
isReportItemChild,
onButtonPress,
isActionLoading,
merchant,
description,
isInSingleTransactionReport,
exchangeRateMessage,
isAmountColumnWide,
formattedTaxRate,
isTaxAmountColumnWide,
isLargeScreenWidth,
formattedTaxRate,
],
);
const shouldRenderChatBubbleCell = useMemo(() => {
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: 'Aktion erforderlich',
duplicate: 'Duplizieren',
duplicated: 'Dupliziert',
exchangeRate: 'Wechselkurs',
reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag',
nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag',
originalAmount: 'Ursprünglicher Betrag',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ const translations = {
actionRequired: 'Action required',
duplicate: 'Duplicate',
duplicated: 'Duplicated',
exchangeRate: 'Exchange rate',
reimbursableTotal: 'Reimbursable total',
nonReimbursableTotal: 'Non-reimbursable total',
},
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: 'Acción requerida',
duplicate: 'Duplicar',
duplicated: 'Duplicado',
exchangeRate: 'Tipo de cambio',
reimbursableTotal: 'Total reembolsable',
nonReimbursableTotal: 'Total no reembolsable',
},
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: 'Action requise',
duplicate: 'Dupliquer',
duplicated: 'Dupliqué',
exchangeRate: 'Taux de change',
reimbursableTotal: 'Total remboursable',
nonReimbursableTotal: 'Total non remboursable',
originalAmount: 'Montant d’origine',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: 'Azione richiesta',
duplicate: 'Duplica',
duplicated: 'Duplicato',
exchangeRate: 'Tasso di cambio',
reimbursableTotal: 'Totale rimborsabile',
nonReimbursableTotal: 'Totale non rimborsabile',
originalAmount: 'Importo originale',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: '対応が必要',
duplicate: '複製',
duplicated: '重複',
exchangeRate: '為替レート',
reimbursableTotal: '経費精算対象の合計',
nonReimbursableTotal: '非払い戻し合計',
originalAmount: '元の金額',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: 'Actie vereist',
duplicate: 'Dupliceren',
duplicated: 'Gedupliceerd',
exchangeRate: 'Wisselkoers',
reimbursableTotal: 'Totaal te vergoeden',
nonReimbursableTotal: 'Niet-vergoedbaar totaal',
originalAmount: 'Oorspronkelijk bedrag',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ const translations: TranslationDeepObject<typeof en> = {
actionRequired: '需要操作',
duplicate: '复制',
duplicated: '已重复',
exchangeRate: '汇率',
reimbursableTotal: '可报销总额',
nonReimbursableTotal: '不可报销总额',
originalAmount: '原始金额',
Expand Down
2 changes: 2 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
},
Expand Down
12 changes: 12 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {getIOUPayerAndReceiver} from './TransactionPreviewUtils';
import {
getCategory,
getDescription,
getExchangeRate,
getTag,
getTaxAmount,
getAmount as getTransactionAmount,
Expand Down Expand Up @@ -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 ?? ''})`;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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';
Expand Down Expand Up @@ -124,7 +125,7 @@
};

let deprecatedAllReports: OnyxCollection<Report> = {};
Onyx.connect({

Check warning on line 128 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -133,7 +134,7 @@
});

let deprecatedAllTransactionViolations: OnyxCollection<TransactionViolations> = {};
Onyx.connect({

Check warning on line 137 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => (deprecatedAllTransactionViolations = value),
Expand Down Expand Up @@ -1047,6 +1048,24 @@
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.
Expand Down Expand Up @@ -2407,6 +2426,7 @@
mergeProhibitedViolations,
getOriginalAttendees,
getReportOwnerAsAttendee,
getExchangeRate,
shouldReuseInitialTransaction,
};

Expand Down
1 change: 1 addition & 0 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading