From 8e81809db90be15ec5b6bea7da7fef6884fc63cb Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 10 Feb 2026 16:54:46 +0100 Subject: [PATCH 01/18] feat: enable duplicating distance expenses --- src/libs/ReportSecondaryActionUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index defb0037d1593..b2261940fbd02 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -800,8 +800,8 @@ function isDuplicateAction(report: Report, reportTransactions: Transaction[]): b const reportTransaction = reportTransactions.at(0); - // Per diem and distance requests will be handled separately in a follow-up - if (isPerDiemRequestTransactionUtils(reportTransaction) || isDistanceRequestTransactionUtils(reportTransaction)) { + // Per diem requests will be handled separately in a follow-up + if (isPerDiemRequestTransactionUtils(reportTransaction)) { return false; } From 27444571fb5ac278134299dca0b3f62836b9c489 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 10 Feb 2026 16:55:45 +0100 Subject: [PATCH 02/18] fix: offline shows merchant instead of distance & rate --- src/libs/actions/IOU/Duplicate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index fa273e8a2c657..adb940d330e71 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -613,6 +613,7 @@ function duplicateExpenseTransaction({ const distanceParams: CreateDistanceRequestInformation = { ...params, participants, + iouRequestType: getRequestType(transaction), existingTransaction: { ...(params.transactionParams ?? {}), comment: transaction.comment, From 9874cf8b159f4f02f069c61ec2c9f9451990aee4 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 10 Feb 2026 16:58:40 +0100 Subject: [PATCH 03/18] fix: remove unnecessary import --- src/libs/ReportSecondaryActionUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index b2261940fbd02..3c94d6582f56f 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -69,7 +69,6 @@ import { getOriginalTransactionWithSplitInfo, hasReceipt as hasReceiptTransactionUtils, hasSubmissionBlockingViolations, - isDistanceRequest as isDistanceRequestTransactionUtils, isDuplicate, isManagedCardTransaction as isManagedCardTransactionTransactionUtils, isOnHold as isOnHoldTransactionUtils, From 93a893ae4bf5075b931cdd63067a5879e7df468b Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 10 Feb 2026 17:34:52 +0100 Subject: [PATCH 04/18] revert: fix for offline shows merchant instead of distance & rate --- src/libs/actions/IOU/Duplicate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index adb940d330e71..fa273e8a2c657 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -613,7 +613,6 @@ function duplicateExpenseTransaction({ const distanceParams: CreateDistanceRequestInformation = { ...params, participants, - iouRequestType: getRequestType(transaction), existingTransaction: { ...(params.transactionParams ?? {}), comment: transaction.comment, From 4ea080943f195e0e1fba3f9ac8c0eb3a25e7e843 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Thu, 12 Feb 2026 12:18:33 +0100 Subject: [PATCH 05/18] fix: distance track expense offline shows merchant instead of distance & rate --- src/libs/actions/IOU/Duplicate.ts | 8 ++++++++ src/libs/actions/IOU/index.ts | 21 +++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index b39aa03729d37..82329185847d6 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -590,6 +590,14 @@ function duplicateExpenseTransaction({ ...(params.participantParams ?? {}), participant: {accountID: userAccountID, selected: true}, }, + existingTransaction: { + ...(params.transactionParams ?? {}), + comment: transaction.comment, + iouRequestType: getRequestType(transaction), + modifiedCreated: '', + reportID: '1', + transactionID: '1', + }, transactionParams: { ...(params.transactionParams ?? {}), validWaypoints: transactionDetails?.waypoints as WaypointCollection | undefined, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 400fe56a83c29..4ffa5f620c0ef 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -734,6 +734,7 @@ type CreateTrackExpenseParams = { participantParams: RequestMoneyParticipantParams; policyParams?: BasePolicyParams; transactionParams: TrackExpenseTransactionParams; + existingTransaction?: OnyxEntry; accountantParams?: TrackExpenseAccountantParams; isRetry?: boolean; shouldPlaySound?: boolean; @@ -778,6 +779,7 @@ type GetTrackExpenseInformationParticipantParams = { type GetTrackExpenseInformationParams = { parentChatReport: OnyxEntry; moneyRequestReportID?: string; + existingTransaction?: OnyxEntry; existingTransactionID?: string; participantParams: GetTrackExpenseInformationParticipantParams; policyParams: BasePolicyParams; @@ -3981,6 +3983,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T const { parentChatReport, moneyRequestReportID = '', + existingTransaction, existingTransactionID, participantParams, policyParams, @@ -4195,14 +4198,14 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // But we'll use the `shouldUseMoneyReport && iouReport` check further instead of `shouldUseMoneyReport` to avoid TS errors. // STEP 3: Build optimistic receipt and transaction - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - const isDistanceRequest = existingTransaction && isDistanceRequestTransactionUtils(existingTransaction); - const isManualDistanceRequest = existingTransaction && isManualDistanceRequestTransactionUtils(existingTransaction); - const isOdometerDistanceRequest = existingTransaction && isOdometerDistanceRequestTransactionUtils(existingTransaction); - const isGPSDistanceRequest = existingTransaction && isGPSDistanceRequestTransactionUtils(existingTransaction); + const existingTransactionData = existingTransaction ?? allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransactionData && isDistanceRequestTransactionUtils(existingTransactionData); + const isManualDistanceRequest = existingTransactionData && isManualDistanceRequestTransactionUtils(existingTransactionData); + const isOdometerDistanceRequest = existingTransactionData && isOdometerDistanceRequestTransactionUtils(existingTransactionData); + const isGPSDistanceRequest = existingTransactionData && isGPSDistanceRequestTransactionUtils(existingTransactionData); let optimisticTransaction = buildOptimisticTransaction({ existingTransactionID: optimisticTransactionID, - existingTransaction, + existingTransaction: existingTransactionData, policy, transactionParams: { amount: -amount, @@ -4220,7 +4223,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T billable, pendingFields: isDistanceRequest && !isManualDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, reimbursable, - filename: existingTransaction?.receipt?.filename, + filename: existingTransactionData?.receipt?.filename, attendees, odometerStart: isOdometerDistanceRequest ? odometerStart : undefined, odometerEnd: isOdometerDistanceRequest ? odometerEnd : undefined, @@ -4237,7 +4240,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. if (isDistanceRequest) { - optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); + optimisticTransaction = fastMerge(existingTransactionData, optimisticTransaction, false); } // STEP 4: Build optimistic reportActions. We need: @@ -6862,6 +6865,7 @@ function trackExpense(params: CreateTrackExpenseParams) { isDraftPolicy, participantParams, policyParams: policyData = {}, + existingTransaction, transactionParams: transactionData, accountantParams, shouldHandleNavigation = true, @@ -6966,6 +6970,7 @@ function trackExpense(params: CreateTrackExpenseParams) { } = getTrackExpenseInformation({ parentChatReport: currentChatReport, moneyRequestReportID, + existingTransaction, existingTransactionID: isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction) ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID From ba11b26f77755e1d3b1c1318c520f76745920b74 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Thu, 12 Feb 2026 14:17:58 +0100 Subject: [PATCH 06/18] fix: duplicating split distance expense creates a split expense with the original amount --- src/libs/actions/IOU/Duplicate.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 82329185847d6..5a349e22b8d44 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -592,11 +592,17 @@ function duplicateExpenseTransaction({ }, existingTransaction: { ...(params.transactionParams ?? {}), - comment: transaction.comment, + comment: { + ...transaction.comment, + originalTransactionID: undefined, + source: undefined, + }, iouRequestType: getRequestType(transaction), modifiedCreated: '', reportID: '1', transactionID: '1', + originalTransactionID: undefined, + source: undefined, }, transactionParams: { ...(params.transactionParams ?? {}), @@ -630,7 +636,11 @@ function duplicateExpenseTransaction({ participants, existingTransaction: { ...(params.transactionParams ?? {}), - comment: transaction.comment, + comment: { + ...transaction.comment, + originalTransactionID: undefined, + source: undefined, + }, iouRequestType: getRequestType(transaction), modifiedCreated: '', reportID: '1', From 25ffb3cd96297e3cb185afcd08d5c124e6dfe04c Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Thu, 12 Feb 2026 14:32:16 +0100 Subject: [PATCH 07/18] fix: remove unnecessary params --- src/libs/actions/IOU/Duplicate.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 5a349e22b8d44..40206b7dce1bc 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -601,8 +601,6 @@ function duplicateExpenseTransaction({ modifiedCreated: '', reportID: '1', transactionID: '1', - originalTransactionID: undefined, - source: undefined, }, transactionParams: { ...(params.transactionParams ?? {}), From 4a1ae7a90fb93c4c04edb7393e05f73fc503bc8a Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 2 Mar 2026 15:59:59 +0100 Subject: [PATCH 08/18] fix: prevent duplicating unsupported distance expenses --- src/components/MoneyReportHeader.tsx | 18 ++++++++++++++++++ src/components/MoneyRequestHeader.tsx | 13 +++++++++++++ src/languages/en.ts | 1 + 3 files changed, 32 insertions(+) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index cb27a42652c77..ddc0e786a3105 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -89,6 +89,7 @@ import { isOpenExpenseReport, isProcessingReport, isReportOwner, + isSelfDM, navigateOnDeleteExpense, navigateToDetailsPage, rejectMoneyRequestReason, @@ -100,6 +101,7 @@ import { getOriginalTransactionWithSplitInfo, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, + isDistanceRequest, isDuplicate, isExpensifyCardTransaction, isPayAtEndExpense as isPayAtEndExpenseTransactionUtils, @@ -427,7 +429,9 @@ function MoneyReportHeader({ ); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); + const isDistanceExpenseUnspportedForDuplicating = isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || isDM(chatReport) || isSelfDM(chatReport)); + const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); @@ -1445,6 +1449,11 @@ function MoneyReportHeader({ return; } + if (isDistanceExpenseUnspportedForDuplicating) { + setDuplicateDistanceErrorModalVisible(true); + return; + } + if (isPerDiemRequestOnNonDefaultWorkspace) { setDuplicatePerDiemErrorModalVisible(true); return; @@ -1949,6 +1958,15 @@ function MoneyReportHeader({ prompt={translate('iou.correctRateError')} shouldShowCancelButton={false} /> + setDuplicateDistanceErrorModalVisible(false)} + onCancel={() => setDuplicateDistanceErrorModalVisible(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('iou.cannotDuplicateDistanceExpense')} + shouldShowCancelButton={false} + /> `${fragments} via workspace rules`, }, duplicateNonDefaultWorkspacePerDiemError: "You can't duplicate per diem expenses across workspaces because the rates may differ between workspaces.", + cannotDuplicateDistanceExpense: "You can't duplicate distance expenses across workspaces because the rates may differ between workspaces.", }, transactionMerge: { listPage: { From 6f69824e6f32cfc27ad94fa7ddae76205579d719 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 2 Mar 2026 16:13:45 +0100 Subject: [PATCH 09/18] fix: spelling --- src/components/MoneyReportHeader.tsx | 4 ++-- src/components/MoneyRequestHeader.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index ddc0e786a3105..6e3c219276546 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -429,7 +429,7 @@ function MoneyReportHeader({ ); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); - const isDistanceExpenseUnspportedForDuplicating = isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || isDM(chatReport) || isSelfDM(chatReport)); + const isDistanceExpenseUnsupportedForDuplicating = isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || isDM(chatReport) || isSelfDM(chatReport)); const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); @@ -1449,7 +1449,7 @@ function MoneyReportHeader({ return; } - if (isDistanceExpenseUnspportedForDuplicating) { + if (isDistanceExpenseUnsupportedForDuplicating) { setDuplicateDistanceErrorModalVisible(true); return; } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index b05ca30c827a4..fd962d0d3e62e 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -193,7 +193,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // If the parent report is a selfDM, it should always be opened in the Inbox tab const shouldOpenParentReportInCurrentTab = !isSelfDM(parentReport); - const isDistanceExpenseUnspportedForDuplicating = isDistanceRequest(transaction) && (isParentReportArchived || isSelfDM(parentReport) || isParentChatReportDM); + const isDistanceExpenseUnsupportedForDuplicating = isDistanceRequest(transaction) && (isParentReportArchived || isSelfDM(parentReport) || isParentChatReportDM); const {wideRHPRouteKeys} = useWideRHPState(); const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector}); @@ -503,7 +503,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre return; } - if (isDistanceExpenseUnspportedForDuplicating) { + if (isDistanceExpenseUnsupportedForDuplicating) { showConfirmModal({ title: translate('common.duplicateExpense'), prompt: translate('iou.cannotDuplicateDistanceExpense'), From df96e2153ed01286851c9e66e2c7f8ee6e3cceac Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 2 Mar 2026 16:15:55 +0100 Subject: [PATCH 10/18] feat: add Spanish translation --- src/languages/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index a4c45e1aebc34..03445c73e75de 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1425,6 +1425,7 @@ const translations: TranslationDeepObject = { formatPolicyRules: (fragments: string, route: string) => `${fragments} vía reglas del espacio de trabajo`, }, duplicateNonDefaultWorkspacePerDiemError: 'No puedes duplicar gastos de viáticos entre espacios de trabajo porque las tarifas pueden variar entre ellos.', + cannotDuplicateDistanceExpense: 'No puedes duplicar gastos de distancia entre espacios de trabajo porque las tasas pueden diferir entre espacios de trabajo.', }, transactionMerge: { listPage: { From a6b912fb361c1013a522a347c1fde2e9edc29ad4 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 2 Mar 2026 16:31:21 +0100 Subject: [PATCH 11/18] fix: prevent navigation when duplicating per diem expense --- src/libs/actions/IOU/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 5c5b2a767fad4..ae8f40a5d4d86 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -533,6 +533,7 @@ type PerDiemExpenseInformation = { policyRecentlyUsedCurrencies: string[]; betas: OnyxEntry; customUnitPolicyID?: string; + shouldHandleNavigation?: boolean; }; type PerDiemExpenseInformationParams = { @@ -6873,6 +6874,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf policyRecentlyUsedCurrencies, betas, customUnitPolicyID, + shouldHandleNavigation = true, } = submitPerDiemExpenseInformation; const {payeeAccountID} = participantParams; const {currency, comment = '', category, tag, created, customUnit, attendees, isFromGlobalCreate} = transactionParams; @@ -6964,7 +6966,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate}); + handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate, shouldHandleNavigation}); if (activeReportID) { notifyNewAction(activeReportID, undefined, payeeAccountID === currentUserAccountIDParam); From b46e5bc96420503bf5e06a6e474f6aba0d5928a7 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 2 Mar 2026 16:59:48 +0100 Subject: [PATCH 12/18] fix: allow duplicating DMs if there is no workspace --- src/components/MoneyReportHeader.tsx | 3 ++- src/components/MoneyRequestHeader.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6e3c219276546..68e38c04c8ab9 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -429,7 +429,8 @@ function MoneyReportHeader({ ); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); - const isDistanceExpenseUnsupportedForDuplicating = isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || isDM(chatReport) || isSelfDM(chatReport)); + const isDistanceExpenseUnsupportedForDuplicating = + isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport)))); const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index fd962d0d3e62e..41e2fdaafff50 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -193,7 +193,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // If the parent report is a selfDM, it should always be opened in the Inbox tab const shouldOpenParentReportInCurrentTab = !isSelfDM(parentReport); - const isDistanceExpenseUnsupportedForDuplicating = isDistanceRequest(transaction) && (isParentReportArchived || isSelfDM(parentReport) || isParentChatReportDM); + const isDistanceExpenseUnsupportedForDuplicating = + isDistanceRequest(transaction) && (isParentReportArchived || (activePolicyExpenseChat && (isSelfDM(parentReport) || isParentChatReportDM))); const {wideRHPRouteKeys} = useWideRHPState(); const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector}); From 55e34a9a0eb6f77c815ca3e721fa7ec7b23d3509 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 3 Mar 2026 10:10:32 +0100 Subject: [PATCH 13/18] fix: close modal if we're going to show an error --- src/components/MoneyReportHeader.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 68e38c04c8ab9..f4ce03770a01f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1468,7 +1468,7 @@ function MoneyReportHeader({ duplicateExpenseTransaction([transaction]); }, - shouldCloseModalOnSelect: isPerDiemRequestOnNonDefaultWorkspace || hasCustomUnitOutOfPolicyViolation || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, + shouldCloseModalOnSelect: isDistanceExpenseUnsupportedForDuplicating || isPerDiemRequestOnNonDefaultWorkspace || hasCustomUnitOutOfPolicyViolation || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, }, [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT]: { text: translate('common.duplicateReport'), diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 41e2fdaafff50..326db32f5177d 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -532,7 +532,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre duplicateTransaction([transaction]); }, - shouldCloseModalOnSelect: hasCustomUnitOutOfPolicyViolation || isPerDiemRequestOnNonDefaultWorkspace, + shouldCloseModalOnSelect: isDistanceExpenseUnsupportedForDuplicating || hasCustomUnitOutOfPolicyViolation || isPerDiemRequestOnNonDefaultWorkspace, }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS]: { value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS, From 983c6f295404b85eb64f49b48468a4cd2a7127b3 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 3 Mar 2026 10:25:49 +0100 Subject: [PATCH 14/18] fix: prettier --- src/components/MoneyReportHeader.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f4ce03770a01f..f835f90c68b8f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1468,7 +1468,11 @@ function MoneyReportHeader({ duplicateExpenseTransaction([transaction]); }, - shouldCloseModalOnSelect: isDistanceExpenseUnsupportedForDuplicating || isPerDiemRequestOnNonDefaultWorkspace || hasCustomUnitOutOfPolicyViolation || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, + shouldCloseModalOnSelect: + isDistanceExpenseUnsupportedForDuplicating || + isPerDiemRequestOnNonDefaultWorkspace || + hasCustomUnitOutOfPolicyViolation || + activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, }, [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT]: { text: translate('common.duplicateReport'), From 00e81a8f3e6eb97a5c5aa473e872c95cc3e0ba9c Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 3 Mar 2026 10:26:35 +0100 Subject: [PATCH 15/18] fix: lint --- src/components/MoneyReportHeader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f835f90c68b8f..d1dc955f31b55 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -429,8 +429,10 @@ function MoneyReportHeader({ ); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); - const isDistanceExpenseUnsupportedForDuplicating = - isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport)))); + const isDistanceExpenseUnsupportedForDuplicating = !!( + isDistanceRequest(transaction) && + (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport)))) + ); const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); From 4b600ca84844e0161690901d3140ef397eb16751 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 3 Mar 2026 10:40:09 +0100 Subject: [PATCH 16/18] fix: lint --- src/components/MoneyRequestHeader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 326db32f5177d..95bd71e8f5ad2 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -193,8 +193,10 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // If the parent report is a selfDM, it should always be opened in the Inbox tab const shouldOpenParentReportInCurrentTab = !isSelfDM(parentReport); - const isDistanceExpenseUnsupportedForDuplicating = - isDistanceRequest(transaction) && (isParentReportArchived || (activePolicyExpenseChat && (isSelfDM(parentReport) || isParentChatReportDM))); + const isDistanceExpenseUnsupportedForDuplicating = !!( + isDistanceRequest(transaction) && + (isParentReportArchived || (activePolicyExpenseChat && (isSelfDM(parentReport) || isParentChatReportDM))) + ); const {wideRHPRouteKeys} = useWideRHPState(); const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector}); From de586590361f4f1ad8ae6f51f1d189f749ae8c6e Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 3 Mar 2026 13:28:50 +0100 Subject: [PATCH 17/18] fix: strip waypoints when duplicating split distance expenses --- src/libs/actions/IOU/Duplicate.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index d85dbf05837b5..f4f14a0a496ec 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -18,7 +18,7 @@ import { buildTransactionThread, getTransactionDetails, } from '@libs/ReportUtils'; -import {getRequestType, getTransactionType} from '@libs/TransactionUtils'; +import {getRequestType, getTransactionType, isDistanceRequest, isExpenseSplit} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -545,6 +545,9 @@ function duplicateExpenseTransaction({ // to try to create a transaction thread report with an ID that already exists. const {linkedTrackedExpenseReportAction, ...transactionWithoutLinkedAction} = transaction; + // We remove waypoints for split distance expenses in order to preserve the split's amount and distance. + const waypoints = !isExpenseSplit(transaction) ? (transactionDetails?.waypoints as WaypointCollection) : undefined; + const params: RequestMoneyInformation = { report: targetReport, optimisticChatReportID, @@ -571,7 +574,7 @@ function duplicateExpenseTransaction({ originalTransactionID: undefined, receipt: undefined, source: undefined, - waypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + waypoints, type: transaction?.comment?.type, count: transaction?.comment?.units?.count, rate: transaction?.comment?.units?.rate, @@ -592,6 +595,11 @@ function duplicateExpenseTransaction({ personalDetails, }; + // Since we remove waypoints for split distance expenses, we need to re-add the distance param here + if (isExpenseSplit(transaction) && isDistanceRequest(transaction)) { + params.transactionParams.distance = transaction.comment?.customUnit?.quantity ?? undefined; + } + // If no workspace is provided the expense should be unreported if (!targetPolicy) { const trackExpenseParams: CreateTrackExpenseParams = { @@ -614,7 +622,7 @@ function duplicateExpenseTransaction({ }, transactionParams: { ...(params.transactionParams ?? {}), - validWaypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + validWaypoints: waypoints, }, report: undefined, isDraftPolicy: false, @@ -657,7 +665,7 @@ function duplicateExpenseTransaction({ transactionParams: { ...(params.transactionParams ?? {}), comment: Parser.htmlToMarkdown(transactionDetails?.comment ?? ''), - validWaypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + validWaypoints: waypoints, }, policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], quickAction, From 56f90eab10482757b911659ca9704b9524538287 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Wed, 4 Mar 2026 15:59:47 +0100 Subject: [PATCH 18/18] chore: add translations --- src/languages/de.ts | 3 ++- src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 1 + 8 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 032bb690faa02..e9e25d8e75c72 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1600,6 +1600,8 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `Der Bericht konnte nicht übermittelt werden. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `Genehmigung über Workspace-Regeln fehlgeschlagen. ${reason}`, failedToApproveViaDEW: (reason: string) => `Genehmigung fehlgeschlagen. ${reason}`, + cannotDuplicateDistanceExpense: + 'Sie können Entfernungsausgaben nicht über mehrere Arbeitsbereiche hinweg duplizieren, da sich die Sätze zwischen den Arbeitsbereichen unterscheiden können.', }, transactionMerge: { listPage: { @@ -8596,7 +8598,6 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, addMember: 'Dieses Mitglied kann nicht hinzugefügt werden. Bitte versuche es erneut.', vacationDelegate: 'Dieser Benutzer kann nicht als Urlaubsvertretung festgelegt werden. Bitte versuche es erneut.', }, - reportSuspiciousActivityPrompt: (email: string) => `Bist du sicher? Dadurch wird das Konto von ${email} gesperrt.

Unser Team wird das Konto anschließend überprüfen und unbefugten Zugriff entfernen. Um den Zugriff wiederherzustellen, muss die Person mit Concierge zusammenarbeiten.`, reportSuspiciousActivityConfirmationPrompt: 'Wir überprüfen das Konto, um sicherzustellen, dass es sicher entsperrt werden kann, und melden uns bei Fragen über Concierge.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4e3309f1fbf39..fa75cf0325823 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1606,6 +1606,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `impossible d’approuver via les règles de l’espace de travail. ${reason}`, failedToApproveViaDEW: (reason: string) => `échec de l’approbation. ${reason}`, + cannotDuplicateDistanceExpense: 'Vous ne pouvez pas dupliquer des dépenses de distance entre espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.', }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 21557b0403610..9e78b24f5005b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1597,6 +1597,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `approvazione non riuscita tramite le regole dello spazio di lavoro. ${reason}`, failedToApproveViaDEW: (reason: string) => `approvazione non riuscita. ${reason}`, + cannotDuplicateDistanceExpense: 'Non puoi duplicare le spese chilometriche tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse.', }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 90d56d212fe61..f7f87c719fe4f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1586,6 +1586,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `レポートの送信に失敗しました。${reason}`, failedToAutoApproveViaDEW: (reason: string) => `ワークスペースルールで承認に失敗しました。${reason}`, failedToApproveViaDEW: (reason: string) => `承認に失敗しました。${reason}`, + cannotDuplicateDistanceExpense: '距離精算はワークスペースごとにレートが異なる可能性があるため、ワークスペース間で複製することはできません。', }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5b0bf0eee75a6..7de585afa8938 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1594,6 +1594,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `het is niet gelukt om het rapport in te dienen. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `goedkeuren via werkruimte­regels is mislukt. ${reason}`, failedToApproveViaDEW: (reason: string) => `goedkeuren mislukt. ${reason}`, + cannotDuplicateDistanceExpense: 'Je kunt afstandsvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.', }, transactionMerge: { listPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 20148b5e53e78..af0b92efdd988 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1595,6 +1595,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `nie udało się zatwierdzić przez zasady w przestrzeni roboczej. ${reason}`, failedToApproveViaDEW: (reason: string) => `nie udało się zaakceptować. ${reason}`, + cannotDuplicateDistanceExpense: 'Nie możesz duplikować wydatków za przejazdy między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.', }, transactionMerge: { listPage: { @@ -8546,7 +8547,6 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`, vacationDelegate: 'Nie można ustawić tego użytkownika jako zastępującego na czas nieobecności. Spróbuj ponownie.', }, cannotSetVacationDelegateForMember: (email: string) => `Nie możesz ustawić zastępstwa urlopowego dla ${email}, ponieważ jest on/ona obecnie zastępcą dla następujących członków:`, - reportSuspiciousActivityPrompt: (email: string) => `Czy na pewno? To zablokuje konto użytkownika ${email}.

Nasz zespół następnie przejrzy konto i usunie wszelki nieautoryzowany dostęp. Aby odzyskać dostęp, będą musieli współpracować z Concierge.`, reportSuspiciousActivityConfirmationPrompt: 'Przejrzymy konto, aby potwierdzić, że bezpiecznie je odblokować, i skontaktujemy się przez Concierge w razie pytań.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ba6fc6953beec..f6b5054dd567d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1591,6 +1591,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `falha ao enviar o relatório. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `falha ao aprovar pelas regras do workspace. ${reason}`, failedToApproveViaDEW: (reason: string) => `falha ao aprovar. ${reason}`, + cannotDuplicateDistanceExpense: 'Você não pode duplicar despesas de distância entre espaços de trabalho porque as tarifas podem ser diferentes entre eles.', }, transactionMerge: { listPage: { @@ -8551,7 +8552,6 @@ Aqui está um *comprovante de teste* para mostrar como funciona:`, vacationDelegate: 'Não foi possível definir este usuário como delegado de férias. Tente novamente.', }, cannotSetVacationDelegateForMember: (email: string) => `Você não pode definir um procurador de férias para ${email} porque esta pessoa já é procuradora dos seguintes membros:`, - reportSuspiciousActivityPrompt: (email: string) => `Tem certeza? Isso irá bloquear a conta de ${email}.

Nossa equipe irá então analisar a conta e remover qualquer acesso não autorizado. Para recuperar o acesso, será necessário que trabalhem com a Concierge.`, reportSuspiciousActivityConfirmationPrompt: 'Vamos revisar a conta para verificar se é seguro desbloqueá-la e entraremos em contato via Concierge caso haja dúvidas.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2b4d9d8da7e78..479231ccb2d7e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1563,6 +1563,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `报表提交失败。${reason}`, failedToAutoApproveViaDEW: (reason: string) => `未能通过工作区规则批准。${reason}`, failedToApproveViaDEW: (reason: string) => `批准失败。${reason}`, + cannotDuplicateDistanceExpense: '你无法在不同工作区之间复制里程报销,因为各个工作区的费率可能不同。', }, transactionMerge: { listPage: {