From c82f2697347efccd64ce700cce8fe2b3920ca5d7 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:19:15 +0530 Subject: [PATCH 01/17] Fix unresponsive create report in move to report Route to upgrade flow when user has no workspace to move expenses into. --- .../step/DynamicIOURequestEditReport.tsx | 29 +++++++++++++++++-- .../step/DynamicIOURequestStepReport.tsx | 21 ++++++++++++-- .../step/DynamicIOURequestStepUpgrade.tsx | 29 ++++++++++++++----- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 4cdc6d5e84c3..795117fc0710 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -96,7 +96,12 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) const isOwnedByOther = selectedReport?.ownerAccountID !== currentUserPersonalDetails.accountID; const isOwnedByOtherOrHasPerDiem = isOwnedByOther || hasPerDiemTransactions; const targetExpensePolicyID = isOwnedByOtherOrHasPerDiem ? selectedReport?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, targetExpensePolicyID, hasUnreportedManagedCardTransactions); + const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath, arePoliciesLoaded} = usePolicyForMovingExpenses( + hasPerDiemTransactions, + undefined, + targetExpensePolicyID, + hasUnreportedManagedCardTransactions, + ); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, currentUserPersonalDetails.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails.email ?? ''); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; @@ -226,7 +231,27 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) handleCreateReport(); return; } - if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!arePoliciesLoaded) { + return; + } + if (shouldNavigateToUpgradePath && !hasUnreportedManagedCardTransactions) { + const firstTransactionID = transactionIDs.at(0); + if (firstTransactionID) { + Navigation.navigate( + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ + action: CONST.IOU.ACTION.EDIT, + iouType: CONST.IOU.TYPE.SUBMIT, + transactionID: firstTransactionID, + reportID: reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID, + upgradePath: CONST.UPGRADE_PATHS.REPORTS, + }), + ), + ); + } + return; + } + if (!hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { diff --git a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx index 256ca7dd1297..c3d391cb3f57 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx @@ -106,7 +106,7 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses( + const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath, arePoliciesLoaded} = usePolicyForMovingExpenses( isPerDiemTransaction, isTimeRequestUtil(transaction), targetExpensePolicyID, @@ -210,7 +210,24 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep handleCreateReport(); return; } - if (!isPerDiemTransaction && !isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!arePoliciesLoaded) { + return; + } + if (shouldNavigateToUpgradePath && !isUnreportedManagedCardTransaction) { + Navigation.navigate( + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ + action, + iouType, + transactionID, + reportID: reportIDFromRoute ?? CONST.REPORT.UNREPORTED_REPORT_ID, + upgradePath: CONST.UPGRADE_PATHS.REPORTS, + }), + ), + ); + return; + } + if (!isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { diff --git a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx index e19d22939ae8..ca67ea7df5db 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx @@ -24,6 +24,7 @@ import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import type CreateWorkspaceParams from '@libs/API/parameters/CreateWorkspaceParams'; @@ -98,10 +99,15 @@ function DynamicIOURequestStepUpgrade({ const createReportForCurrentUser = useCreateNewReport(); // Hooks for bulk move functionality - const {selectedTransactions, areAllMatchingItemsSelected, excludedTransactions} = useSearchSelectionContext(); + const {selectedTransactions, selectedTransactionIDs, areAllMatchingItemsSelected, excludedTransactions} = useSearchSelectionContext(); const {clearSelectedTransactions} = useSearchSelectionActions(); const {currentSearchQueryJSON} = useSearchQueryContext(); + const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]); + const effectiveSelectedTransactionsKeys = useMemo( + () => (selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs), + [selectedTransactionsKeys, selectedTransactionIDs], + ); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -115,9 +121,15 @@ function DynamicIOURequestStepUpgrade({ const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions(); // Search-selected transactions are not in COLLECTION.TRANSACTION — extract from `selectedTransactions` directly. - const transactions = Object.values(selectedTransactions) - .map((transactionItem) => transactionItem.transaction) - .filter((item): item is Transaction => !!item); + const transactions = useMemo( + () => + selectedTransactionsKeys.length + ? Object.values(selectedTransactions) + .map((transactionItem) => transactionItem.transaction) + .filter((item): item is Transaction => !!item) + : selectedTransactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((item): item is Transaction => !!item), + [selectedTransactionsKeys, selectedTransactions, selectedTransactionIDs, allTransactions], + ); const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); @@ -145,7 +157,7 @@ function DynamicIOURequestStepUpgrade({ const policyID = policyDataRef.current?.policyID; // Bulk move expenses - if (upgradePath === CONST.UPGRADE_PATHS.REPORTS && policyID && selectedTransactionsKeys.includes(transactionID)) { + if (upgradePath === CONST.UPGRADE_PATHS.REPORTS && policyID && effectiveSelectedTransactionsKeys.includes(transactionID)) { const newPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const optimisticReport = createNewReport(ownerPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, newPolicy, isTrackIntentUser, getCurrencyDecimals, rules); @@ -163,7 +175,7 @@ function DynamicIOURequestStepUpgrade({ // Move ALL selected transactions to the new report changeTransactionsReport({ isVendorMatchingBetaEnabled, - transactionIDs: selectedTransactionsKeys, + transactionIDs: effectiveSelectedTransactionsKeys, isASAPSubmitBetaEnabled, accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, email: session?.email ?? '', @@ -186,7 +198,8 @@ function DynamicIOURequestStepUpgrade({ ...allMatchingQueryParams, }); - clearSelectedTransactions(); + turnOffMobileSelectionMode(); + clearSelectedTransactions(true); Navigation.dismissModal(); return; @@ -278,7 +291,7 @@ function DynamicIOURequestStepUpgrade({ shouldSubmitExpense, transactionID, upgradePath, - selectedTransactionsKeys, + effectiveSelectedTransactionsKeys, clearSelectedTransactions, hasViolations, isASAPSubmitBetaEnabled, From d3045f66394f2a16975ff80a1d0a264eb24dff9b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:58:52 +0530 Subject: [PATCH 02/17] Remove useMemo and useCallback hooks --- .../step/DynamicIOURequestEditReport.tsx | 7 +- .../step/DynamicIOURequestStepUpgrade.tsx | 66 ++++--------------- 2 files changed, 13 insertions(+), 60 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 795117fc0710..eb2a554df78c 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -42,7 +42,7 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; -import React, {useMemo} from 'react'; +import React from 'react'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -80,10 +80,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) const [allPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`); const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const personalDetails = usePersonalDetails(); - const ownerPersonalDetails = useMemo( - () => getPersonalDetailsForAccountID(selectedReport?.ownerAccountID, personalDetails) as PersonalDetails, - [personalDetails, selectedReport?.ownerAccountID], - ); + const ownerPersonalDetails = getPersonalDetailsForAccountID(selectedReport?.ownerAccountID, personalDetails) as PersonalDetails; const [transactions] = useTransactionsByID(transactionIDs); const managedCardTransactionID = transactions.find((transaction) => isUnreportedManagedCardTransaction(transaction))?.transactionID; const hasUnreportedManagedCardTransactions = !!managedCardTransactionID; diff --git a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx index ca67ea7df5db..5f9e84d29237 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx @@ -54,7 +54,7 @@ import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Transaction} from '@src/types/onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; type DynamicIOURequestStepUpgradeProps = PlatformStackScreenProps; @@ -103,11 +103,8 @@ function DynamicIOURequestStepUpgrade({ const {clearSelectedTransactions} = useSearchSelectionActions(); const {currentSearchQueryJSON} = useSearchQueryContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]); - const effectiveSelectedTransactionsKeys = useMemo( - () => (selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs), - [selectedTransactionsKeys, selectedTransactionIDs], - ); + const selectedTransactionsKeys = Object.keys(selectedTransactions); + const effectiveSelectedTransactionsKeys = selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs; const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -121,15 +118,11 @@ function DynamicIOURequestStepUpgrade({ const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions(); // Search-selected transactions are not in COLLECTION.TRANSACTION — extract from `selectedTransactions` directly. - const transactions = useMemo( - () => - selectedTransactionsKeys.length - ? Object.values(selectedTransactions) - .map((transactionItem) => transactionItem.transaction) - .filter((item): item is Transaction => !!item) - : selectedTransactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((item): item is Transaction => !!item), - [selectedTransactionsKeys, selectedTransactions, selectedTransactionIDs, allTransactions], - ); + const transactions = selectedTransactionsKeys.length + ? Object.values(selectedTransactions) + .map((transactionItem) => transactionItem.transaction) + .filter((item): item is Transaction => !!item) + : selectedTransactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((item): item is Transaction => !!item); const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); @@ -138,7 +131,7 @@ function DynamicIOURequestStepUpgrade({ const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); const ownerAccountID = selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID; - const ownerPersonalDetails = useMemo(() => getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails, [personalDetails, ownerAccountID]); + const ownerPersonalDetails = getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails; const feature = Object.values(CONST.UPGRADE_FEATURE_INTRO_MAPPING) .filter((value) => value.id !== CONST.UPGRADE_FEATURE_INTRO_MAPPING.policyPreventMemberChangingTitle.id) @@ -152,7 +145,7 @@ function DynamicIOURequestStepUpgrade({ } }; - const afterUpgradeAcknowledged = useCallback(() => { + const afterUpgradeAcknowledged = () => { const expenseReportID = policyDataRef.current?.expenseChatReportID ?? reportID; const policyID = policyDataRef.current?.policyID; @@ -281,44 +274,7 @@ function DynamicIOURequestStepUpgrade({ default: Navigation.goBack(); } - }, [ - isVendorMatchingBetaEnabled, - action, - upgradeBackTo, - shouldReturnToConfirmation, - navigateWithMicrotask, - reportID, - shouldSubmitExpense, - transactionID, - upgradePath, - effectiveSelectedTransactionsKeys, - clearSelectedTransactions, - hasViolations, - isASAPSubmitBetaEnabled, - allPolicies, - allPolicyCategories, - session?.accountID, - session?.email, - ownerPersonalDetails, - transactions, - iouType, - isTrack, - allPolicyTags, - createReportForCurrentUser, - transactionViolations, - reports, - selfDMReportActions, - isTrackIntentUser, - delegateAccountID, - getCurrencyDecimals, - getCurrencySymbol, - rules, - cardList, - areAllMatchingItemsSelected, - currentSearchQueryJSON, - excludedTransactions, - isOffline, - ]); + }; const participant = transaction?.participants?.[0]; const adminParticipant = From 5599b64a4635472c5280418caac11fb20ab14846 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:11:18 +0530 Subject: [PATCH 03/17] Gate moving expenses for approve-only members --- src/libs/ReportUtils.ts | 14 ++++++++- .../step/DynamicIOURequestEditReport.tsx | 21 ++----------- .../step/DynamicIOURequestStepReport.tsx | 16 +--------- tests/unit/canEditFieldOfMoneyRequestTest.ts | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 069dfd8c66a9..dbf15aeed1bd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -145,9 +145,11 @@ import { canMemberWrite as canMemberWritePolicyUtils, canCreateExpensesOnPolicy, canSendInvoiceFromWorkspace, + canSubmitPerDiemExpenseFromWorkspace, getActivePolicies, getConnectedIntegration, getForwardsToAccount, + getGroupPoliciesWhereReportCanBeCreated, getManagerAccountEmail, getManagerAccountID, getPerDiemCustomUnit, @@ -6217,14 +6219,24 @@ function canEditFieldOfMoneyRequest({ return false; } + const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()) ?? deprecatedCurrentUserEmail; + const eligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail); + const canCreateReport = isPerDiemRequest(transaction) ? eligiblePolicies.some(canSubmitPerDiemExpenseFromWorkspace) : eligiblePolicies.length > 0; + // Check the cheaper condition first - if ((isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) { + if (canCreateReport && (isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) { return true; } // Check if there are multiple outstanding reports across policies let outstandingReportsCount = 0; for (const currentPolicy of policiesArray) { + if (!canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { + continue; + } + if (isPerDiemRequest(transaction) && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { + continue; + } const reports = getOutstandingReportsForUser( currentPolicy.id, moneyRequestReport?.ownerAccountID, diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index eb2a554df78c..1fb7f11e3fd5 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -93,7 +93,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) const isOwnedByOther = selectedReport?.ownerAccountID !== currentUserPersonalDetails.accountID; const isOwnedByOtherOrHasPerDiem = isOwnedByOther || hasPerDiemTransactions; const targetExpensePolicyID = isOwnedByOtherOrHasPerDiem ? selectedReport?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath, arePoliciesLoaded} = usePolicyForMovingExpenses( + const {policyForMovingExpensesID, shouldSelectPolicy, arePoliciesLoaded} = usePolicyForMovingExpenses( hasPerDiemTransactions, undefined, targetExpensePolicyID, @@ -231,23 +231,6 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) if (!arePoliciesLoaded) { return; } - if (shouldNavigateToUpgradePath && !hasUnreportedManagedCardTransactions) { - const firstTransactionID = transactionIDs.at(0); - if (firstTransactionID) { - Navigation.navigate( - createDynamicRoute( - DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ - action: CONST.IOU.ACTION.EDIT, - iouType: CONST.IOU.TYPE.SUBMIT, - transactionID: firstTransactionID, - reportID: reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID, - upgradePath: CONST.UPGRADE_PATHS.REPORTS, - }), - ), - ); - } - return; - } if (!hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } @@ -269,7 +252,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) selectReport={selectReport} removeFromReport={removeFromReport} isEditing={action === CONST.IOU.ACTION.EDIT} - createReport={createReport} + createReport={policyForMovingExpensesID || shouldSelectPolicy || hasPerDiemTransactions || hasUnreportedManagedCardTransactions ? createReport : undefined} isPerDiemRequest={hasPerDiemTransactions} isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions} transactionPolicyID={targetExpensePolicyID} diff --git a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx index c3d391cb3f57..f273cffa1fb5 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx @@ -106,7 +106,7 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath, arePoliciesLoaded} = usePolicyForMovingExpenses( + const {policyForMovingExpensesID, shouldSelectPolicy, arePoliciesLoaded} = usePolicyForMovingExpenses( isPerDiemTransaction, isTimeRequestUtil(transaction), targetExpensePolicyID, @@ -213,20 +213,6 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep if (!arePoliciesLoaded) { return; } - if (shouldNavigateToUpgradePath && !isUnreportedManagedCardTransaction) { - Navigation.navigate( - createDynamicRoute( - DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ - action, - iouType, - transactionID, - reportID: reportIDFromRoute ?? CONST.REPORT.UNREPORTED_REPORT_ID, - upgradePath: CONST.UPGRADE_PATHS.REPORTS, - }), - ), - ); - return; - } if (!isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index 38f0114012e4..b4625bea13b0 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -453,6 +453,36 @@ describe('canEditFieldOfMoneyRequest', () => { // Then they should be able to move the expense since there are multiple outstanding expense reports expect(canEditReportField).toBe(false); }); + + it('should return false when the user has approveOnly role on the policy and no other workspaces', async () => { + const approveOnlyPolicy = { + ...expensePolicy, + role: CONST.POLICY.ROLE.APPROVE_ONLY, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, approveOnlyPolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${IOUReportID}`, expenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_1_ID}`, outstandingExpenseReport1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_2_ID}`, outstandingExpenseReport2); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [currentUserAccountID]: { + accountID: currentUserAccountID, + login: currentUserEmail, + }, + }); + await waitForBatchedUpdates(); + const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + + const canEditReportField = canEditFieldOfMoneyRequest({ + rules: undefined, + reportAction, + fieldToEdit: CONST.EDIT_REQUEST_FIELD.REPORT, + outstandingReportsByPolicyID, + transaction: moneyRequestTransaction, + reportNameValuePairs: undefined, + }); + + expect(canEditReportField).toBe(false); + }); }); describe('Policy has Dynamic External Workflow', () => { From f81f0fa6025a967d1b7ad29ab1f04a33f754bde9 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:29:07 +0530 Subject: [PATCH 04/17] Fix move permission when policy is unblocked --- src/libs/ReportUtils.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index dbf15aeed1bd..8f2a63be4708 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6220,11 +6220,41 @@ function canEditFieldOfMoneyRequest({ } const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()) ?? deprecatedCurrentUserEmail; - const eligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail); - const canCreateReport = isPerDiemRequest(transaction) ? eligiblePolicies.some(canSubmitPerDiemExpenseFromWorkspace) : eligiblePolicies.length > 0; + const isBlockedOnCurrentPolicy = !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, reportOwnerLogin); + + if (isBlockedOnCurrentPolicy) { + const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some((p) => p.id !== moneyRequestReport?.policyID); + if (!hasOtherEligiblePolicies) { + let otherOutstandingReportsCount = 0; + for (const currentPolicy of policiesArray) { + if (currentPolicy.id === moneyRequestReport?.policyID || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { + continue; + } + if (isPerDiemRequest(transaction) && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { + continue; + } + const reports = getOutstandingReportsForUser( + currentPolicy.id, + moneyRequestReport?.ownerAccountID, + rules, + reportNameValuePairs, + outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, + ); + otherOutstandingReportsCount += reports.length; + + if (otherOutstandingReportsCount > 0) { + break; + } + } + + if (otherOutstandingReportsCount === 0) { + return false; + } + } + } // Check the cheaper condition first - if (canCreateReport && (isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) { + if ((isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) { return true; } From bbcc94a7e8ca6e44bdbd46479287f6bf7e61b5c8 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:45:53 +0530 Subject: [PATCH 05/17] Revert changes in DynamicIOURequestStepUpgrade --- .../step/DynamicIOURequestStepUpgrade.tsx | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx index 5f9e84d29237..e19d22939ae8 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx @@ -24,7 +24,6 @@ import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import type CreateWorkspaceParams from '@libs/API/parameters/CreateWorkspaceParams'; @@ -54,7 +53,7 @@ import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Transaction} from '@src/types/onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; -import React, {useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; type DynamicIOURequestStepUpgradeProps = PlatformStackScreenProps; @@ -99,12 +98,10 @@ function DynamicIOURequestStepUpgrade({ const createReportForCurrentUser = useCreateNewReport(); // Hooks for bulk move functionality - const {selectedTransactions, selectedTransactionIDs, areAllMatchingItemsSelected, excludedTransactions} = useSearchSelectionContext(); + const {selectedTransactions, areAllMatchingItemsSelected, excludedTransactions} = useSearchSelectionContext(); const {clearSelectedTransactions} = useSearchSelectionActions(); const {currentSearchQueryJSON} = useSearchQueryContext(); - const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const selectedTransactionsKeys = Object.keys(selectedTransactions); - const effectiveSelectedTransactionsKeys = selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs; + const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -118,11 +115,9 @@ function DynamicIOURequestStepUpgrade({ const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions(); // Search-selected transactions are not in COLLECTION.TRANSACTION — extract from `selectedTransactions` directly. - const transactions = selectedTransactionsKeys.length - ? Object.values(selectedTransactions) - .map((transactionItem) => transactionItem.transaction) - .filter((item): item is Transaction => !!item) - : selectedTransactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((item): item is Transaction => !!item); + const transactions = Object.values(selectedTransactions) + .map((transactionItem) => transactionItem.transaction) + .filter((item): item is Transaction => !!item); const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); @@ -131,7 +126,7 @@ function DynamicIOURequestStepUpgrade({ const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); const ownerAccountID = selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID; - const ownerPersonalDetails = getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails; + const ownerPersonalDetails = useMemo(() => getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails, [personalDetails, ownerAccountID]); const feature = Object.values(CONST.UPGRADE_FEATURE_INTRO_MAPPING) .filter((value) => value.id !== CONST.UPGRADE_FEATURE_INTRO_MAPPING.policyPreventMemberChangingTitle.id) @@ -145,12 +140,12 @@ function DynamicIOURequestStepUpgrade({ } }; - const afterUpgradeAcknowledged = () => { + const afterUpgradeAcknowledged = useCallback(() => { const expenseReportID = policyDataRef.current?.expenseChatReportID ?? reportID; const policyID = policyDataRef.current?.policyID; // Bulk move expenses - if (upgradePath === CONST.UPGRADE_PATHS.REPORTS && policyID && effectiveSelectedTransactionsKeys.includes(transactionID)) { + if (upgradePath === CONST.UPGRADE_PATHS.REPORTS && policyID && selectedTransactionsKeys.includes(transactionID)) { const newPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const optimisticReport = createNewReport(ownerPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, newPolicy, isTrackIntentUser, getCurrencyDecimals, rules); @@ -168,7 +163,7 @@ function DynamicIOURequestStepUpgrade({ // Move ALL selected transactions to the new report changeTransactionsReport({ isVendorMatchingBetaEnabled, - transactionIDs: effectiveSelectedTransactionsKeys, + transactionIDs: selectedTransactionsKeys, isASAPSubmitBetaEnabled, accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, email: session?.email ?? '', @@ -191,8 +186,7 @@ function DynamicIOURequestStepUpgrade({ ...allMatchingQueryParams, }); - turnOffMobileSelectionMode(); - clearSelectedTransactions(true); + clearSelectedTransactions(); Navigation.dismissModal(); return; @@ -274,7 +268,44 @@ function DynamicIOURequestStepUpgrade({ default: Navigation.goBack(); } - }; + }, [ + isVendorMatchingBetaEnabled, + action, + upgradeBackTo, + shouldReturnToConfirmation, + navigateWithMicrotask, + reportID, + shouldSubmitExpense, + transactionID, + upgradePath, + selectedTransactionsKeys, + clearSelectedTransactions, + hasViolations, + isASAPSubmitBetaEnabled, + allPolicies, + allPolicyCategories, + session?.accountID, + session?.email, + ownerPersonalDetails, + transactions, + iouType, + isTrack, + allPolicyTags, + createReportForCurrentUser, + transactionViolations, + reports, + selfDMReportActions, + isTrackIntentUser, + delegateAccountID, + getCurrencyDecimals, + getCurrencySymbol, + rules, + cardList, + areAllMatchingItemsSelected, + currentSearchQueryJSON, + excludedTransactions, + isOffline, + ]); const participant = transaction?.participants?.[0]; const adminParticipant = From 6d292fc72b76ab281d3a29031150232e595ed819 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:49:10 +0530 Subject: [PATCH 06/17] Simplify create report gating in edit report --- .../step/DynamicIOURequestEditReport.tsx | 19 +++++++------------ .../step/DynamicIOURequestStepReport.tsx | 7 ++----- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 1fb7f11e3fd5..79901ecc0fd1 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -42,7 +42,7 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; -import React from 'react'; +import React, {useMemo} from 'react'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -80,7 +80,10 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) const [allPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`); const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const personalDetails = usePersonalDetails(); - const ownerPersonalDetails = getPersonalDetailsForAccountID(selectedReport?.ownerAccountID, personalDetails) as PersonalDetails; + const ownerPersonalDetails = useMemo( + () => getPersonalDetailsForAccountID(selectedReport?.ownerAccountID, personalDetails) as PersonalDetails, + [personalDetails, selectedReport?.ownerAccountID], + ); const [transactions] = useTransactionsByID(transactionIDs); const managedCardTransactionID = transactions.find((transaction) => isUnreportedManagedCardTransaction(transaction))?.transactionID; const hasUnreportedManagedCardTransactions = !!managedCardTransactionID; @@ -93,12 +96,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) const isOwnedByOther = selectedReport?.ownerAccountID !== currentUserPersonalDetails.accountID; const isOwnedByOtherOrHasPerDiem = isOwnedByOther || hasPerDiemTransactions; const targetExpensePolicyID = isOwnedByOtherOrHasPerDiem ? selectedReport?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy, arePoliciesLoaded} = usePolicyForMovingExpenses( - hasPerDiemTransactions, - undefined, - targetExpensePolicyID, - hasUnreportedManagedCardTransactions, - ); + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, targetExpensePolicyID, hasUnreportedManagedCardTransactions); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, currentUserPersonalDetails.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails.email ?? ''); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; @@ -228,10 +226,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) handleCreateReport(); return; } - if (!arePoliciesLoaded) { - return; - } - if (!hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { diff --git a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx index f273cffa1fb5..256ca7dd1297 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepReport.tsx @@ -106,7 +106,7 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpensesID, shouldSelectPolicy, arePoliciesLoaded} = usePolicyForMovingExpenses( + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses( isPerDiemTransaction, isTimeRequestUtil(transaction), targetExpensePolicyID, @@ -210,10 +210,7 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep handleCreateReport(); return; } - if (!arePoliciesLoaded) { - return; - } - if (!isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!isPerDiemTransaction && !isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { From 90e94eb26c10f7a74deb6e4a5926da747b3f06e0 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:01:13 +0530 Subject: [PATCH 07/17] Hoist isPerDiemRequest check out of loops --- src/libs/ReportUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8f2a63be4708..e41a22231d89 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6221,6 +6221,7 @@ function canEditFieldOfMoneyRequest({ const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()) ?? deprecatedCurrentUserEmail; const isBlockedOnCurrentPolicy = !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, reportOwnerLogin); + const isPerDiem = isPerDiemRequest(transaction); if (isBlockedOnCurrentPolicy) { const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some((p) => p.id !== moneyRequestReport?.policyID); @@ -6230,7 +6231,7 @@ function canEditFieldOfMoneyRequest({ if (currentPolicy.id === moneyRequestReport?.policyID || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { continue; } - if (isPerDiemRequest(transaction) && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { + if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { continue; } const reports = getOutstandingReportsForUser( @@ -6264,7 +6265,7 @@ function canEditFieldOfMoneyRequest({ if (!canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { continue; } - if (isPerDiemRequest(transaction) && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { + if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { continue; } const reports = getOutstandingReportsForUser( From da4f6252fad380f205399d24d161e75c1ea6e35d Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:14:06 +0530 Subject: [PATCH 08/17] Deduplicate report counting in move permission --- src/libs/ReportUtils.ts | 77 ++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e41a22231d89..f72ed15c991c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6223,35 +6223,37 @@ function canEditFieldOfMoneyRequest({ const isBlockedOnCurrentPolicy = !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, reportOwnerLogin); const isPerDiem = isPerDiemRequest(transaction); - if (isBlockedOnCurrentPolicy) { - const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some((p) => p.id !== moneyRequestReport?.policyID); - if (!hasOtherEligiblePolicies) { - let otherOutstandingReportsCount = 0; - for (const currentPolicy of policiesArray) { - if (currentPolicy.id === moneyRequestReport?.policyID || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { - continue; - } - if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { - continue; - } - const reports = getOutstandingReportsForUser( - currentPolicy.id, - moneyRequestReport?.ownerAccountID, - rules, - reportNameValuePairs, - outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, - ); - otherOutstandingReportsCount += reports.length; - - if (otherOutstandingReportsCount > 0) { - break; - } + const countOutstandingReports = (excludePolicyID?: string, stopAt = 1): number => { + let count = 0; + for (const currentPolicy of policiesArray) { + if ((excludePolicyID && currentPolicy.id === excludePolicyID) || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { + continue; } - - if (otherOutstandingReportsCount === 0) { - return false; + if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { + continue; + } + const reports = getOutstandingReportsForUser( + currentPolicy.id, + moneyRequestReport?.ownerAccountID, + rules, + reportNameValuePairs, + outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, + ); + count += reports.length; + if (count > stopAt) { + break; } } + return count; + }; + + // A member who cannot create expenses on this policy (e.g. approveOnly role) can only move the expense if + // there is another eligible workspace or an existing outstanding report to move it to. Otherwise disallow it. + if (isBlockedOnCurrentPolicy) { + const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some((p) => p.id !== moneyRequestReport?.policyID); + if (!hasOtherEligiblePolicies && countOutstandingReports(moneyRequestReport?.policyID, 0) === 0) { + return false; + } } // Check the cheaper condition first @@ -6260,27 +6262,8 @@ function canEditFieldOfMoneyRequest({ } // Check if there are multiple outstanding reports across policies - let outstandingReportsCount = 0; - for (const currentPolicy of policiesArray) { - if (!canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { - continue; - } - if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { - continue; - } - const reports = getOutstandingReportsForUser( - currentPolicy.id, - moneyRequestReport?.ownerAccountID, - rules, - reportNameValuePairs, - outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, - ); - outstandingReportsCount += reports.length; - - // Short-circuit once we find more than 1 - if (outstandingReportsCount > 1) { - return true; - } + if (countOutstandingReports(undefined, 1) > 1) { + return true; } return false; From abfdc2a93a86b25ab617cd01cd5a3f7a50eb468a Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:19:08 +0530 Subject: [PATCH 09/17] Remove unnecessary fallback for reportOwnerLogin --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f72ed15c991c..6c81c7e69102 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6219,7 +6219,7 @@ function canEditFieldOfMoneyRequest({ return false; } - const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()) ?? deprecatedCurrentUserEmail; + const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()); const isBlockedOnCurrentPolicy = !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, reportOwnerLogin); const isPerDiem = isPerDiemRequest(transaction); From 7a979ada990aed8a388a620c4c1df0c164ac18c9 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:25:05 +0530 Subject: [PATCH 10/17] Deduplicate create report check in edit report --- src/pages/iou/request/step/DynamicIOURequestEditReport.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 79901ecc0fd1..a83d0f9c4b8e 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -213,6 +213,8 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) shouldBypassConfirmation: true, }); + const canCreateReport = !!policyForMovingExpensesID || shouldSelectPolicy || hasPerDiemTransactions || hasUnreportedManagedCardTransactions; + const createReport = () => { const restrictionPolicy = policyForMovingExpenses; if ( @@ -226,7 +228,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) handleCreateReport(); return; } - if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!canCreateReport) { return; } if (shouldSelectPolicy) { @@ -247,7 +249,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) selectReport={selectReport} removeFromReport={removeFromReport} isEditing={action === CONST.IOU.ACTION.EDIT} - createReport={policyForMovingExpensesID || shouldSelectPolicy || hasPerDiemTransactions || hasUnreportedManagedCardTransactions ? createReport : undefined} + createReport={canCreateReport ? createReport : undefined} isPerDiemRequest={hasPerDiemTransactions} isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions} transactionPolicyID={targetExpensePolicyID} From 0937397eaa3ed20443cdec562dbcaac7d6b0a9a3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:39:58 +0530 Subject: [PATCH 11/17] Address review feedback on move eligibility and test --- src/libs/ReportUtils.ts | 6 ++++-- .../iou/request/step/DynamicIOURequestEditReport.tsx | 8 ++++---- tests/unit/canEditFieldOfMoneyRequestTest.ts | 3 +++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6c81c7e69102..a50477e64a21 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6226,7 +6226,7 @@ function canEditFieldOfMoneyRequest({ const countOutstandingReports = (excludePolicyID?: string, stopAt = 1): number => { let count = 0; for (const currentPolicy of policiesArray) { - if ((excludePolicyID && currentPolicy.id === excludePolicyID) || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin)) { + if ((excludePolicyID && currentPolicy.id === excludePolicyID) || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin) || isTeachersUnitePolicyID(currentPolicy.id)) { continue; } if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { @@ -6250,7 +6250,9 @@ function canEditFieldOfMoneyRequest({ // A member who cannot create expenses on this policy (e.g. approveOnly role) can only move the expense if // there is another eligible workspace or an existing outstanding report to move it to. Otherwise disallow it. if (isBlockedOnCurrentPolicy) { - const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some((p) => p.id !== moneyRequestReport?.policyID); + const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some( + (p) => p.id !== moneyRequestReport?.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(p)), + ); if (!hasOtherEligiblePolicies && countOutstandingReports(moneyRequestReport?.policyID, 0) === 0) { return false; } diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index a83d0f9c4b8e..66c29e12696c 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -213,7 +213,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) shouldBypassConfirmation: true, }); - const canCreateReport = !!policyForMovingExpensesID || shouldSelectPolicy || hasPerDiemTransactions || hasUnreportedManagedCardTransactions; + const canCreateReport = !!policyForMovingExpensesID || shouldSelectPolicy || hasUnreportedManagedCardTransactions; const createReport = () => { const restrictionPolicy = policyForMovingExpenses; @@ -224,11 +224,11 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(restrictionPolicy.id)); return; } - if (hasPerDiemTransactions) { - handleCreateReport(); + if (!canCreateReport) { return; } - if (!canCreateReport) { + if (hasPerDiemTransactions) { + handleCreateReport(); return; } if (shouldSelectPolicy) { diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index b4625bea13b0..d869c53c59a6 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -455,6 +455,7 @@ describe('canEditFieldOfMoneyRequest', () => { }); it('should return false when the user has approveOnly role on the policy and no other workspaces', async () => { + // Given a policy where the current user only has approveOnly role and no other workspaces const approveOnlyPolicy = { ...expensePolicy, role: CONST.POLICY.ROLE.APPROVE_ONLY, @@ -472,6 +473,7 @@ describe('canEditFieldOfMoneyRequest', () => { await waitForBatchedUpdates(); const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + // When checking if the user can move the expense const canEditReportField = canEditFieldOfMoneyRequest({ rules: undefined, reportAction, @@ -481,6 +483,7 @@ describe('canEditFieldOfMoneyRequest', () => { reportNameValuePairs: undefined, }); + // Then the move action should be disallowed expect(canEditReportField).toBe(false); }); }); From fc6dd4cb1b603f28d4bf03c1f761e32ab0114a2c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:32:09 +0530 Subject: [PATCH 12/17] Allow admins to move expenses of approve-only members --- src/libs/ReportUtils.ts | 4 +- tests/unit/canEditFieldOfMoneyRequestTest.ts | 46 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a50477e64a21..cbc3fb6a481b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6220,9 +6220,11 @@ function canEditFieldOfMoneyRequest({ } const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()); - const isBlockedOnCurrentPolicy = !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, reportOwnerLogin); + const isBlockedOnCurrentPolicy = !isAdmin && !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, deprecatedCurrentUserEmail); const isPerDiem = isPerDiemRequest(transaction); + // Counts outstanding reports the owner could move this expense to, stopping early once `count` exceeds + // `stopAt` since callers only need to know whether the count is 0 or greater than 1, not the exact total. const countOutstandingReports = (excludePolicyID?: string, stopAt = 1): number => { let count = 0; for (const currentPolicy of policiesArray) { diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index d869c53c59a6..da5cdecc63ba 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -486,6 +486,52 @@ describe('canEditFieldOfMoneyRequest', () => { // Then the move action should be disallowed expect(canEditReportField).toBe(false); }); + + it('should return true when an admin moves an expense owned by an approveOnly member', async () => { + // Given an admin viewing an expense owned by an approve-only member + const employeeAccountID = 12345; + const employeeEmail = 'employee@test.com'; + const adminPolicy = { + ...expensePolicy, + role: CONST.POLICY.ROLE.ADMIN, + employeeList: { + [employeeEmail]: { + role: CONST.POLICY.ROLE.APPROVE_ONLY, + }, + }, + }; + const employeeExpenseReport = { + ...expenseReport, + ownerAccountID: employeeAccountID, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, adminPolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${IOUReportID}`, employeeExpenseReport); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [employeeAccountID]: { + accountID: employeeAccountID, + login: employeeEmail, + }, + [currentUserAccountID]: { + accountID: currentUserAccountID, + login: currentUserEmail, + }, + }); + await waitForBatchedUpdates(); + const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + + // When checking if the admin can move the expense + const canEditReportField = canEditFieldOfMoneyRequest({ + rules: undefined, + reportAction, + fieldToEdit: CONST.EDIT_REQUEST_FIELD.REPORT, + outstandingReportsByPolicyID, + transaction: moneyRequestTransaction, + reportNameValuePairs: undefined, + }); + + // Then the move action should be allowed for the admin + expect(canEditReportField).toBe(true); + }); }); describe('Policy has Dynamic External Workflow', () => { From c26ac11f8f3beb7364f984159fda1ee52366d17e Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:40:17 +0530 Subject: [PATCH 13/17] Scope move eligibility gate to members who cannot create expenses --- src/libs/ReportUtils.ts | 70 ++++++++----------- .../step/DynamicIOURequestEditReport.tsx | 6 +- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cbc3fb6a481b..ad47fb21a72e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6219,45 +6219,24 @@ function canEditFieldOfMoneyRequest({ return false; } - const reportOwnerLogin = getLoginByAccountID(moneyRequestReport?.ownerAccountID, getAllPersonalDetails()); - const isBlockedOnCurrentPolicy = !isAdmin && !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, deprecatedCurrentUserEmail); - const isPerDiem = isPerDiemRequest(transaction); - - // Counts outstanding reports the owner could move this expense to, stopping early once `count` exceeds - // `stopAt` since callers only need to know whether the count is 0 or greater than 1, not the exact total. - const countOutstandingReports = (excludePolicyID?: string, stopAt = 1): number => { - let count = 0; - for (const currentPolicy of policiesArray) { - if ((excludePolicyID && currentPolicy.id === excludePolicyID) || !canCreateExpensesOnPolicy(currentPolicy, reportOwnerLogin) || isTeachersUnitePolicyID(currentPolicy.id)) { - continue; - } - if (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(currentPolicy)) { - continue; - } - const reports = getOutstandingReportsForUser( - currentPolicy.id, - moneyRequestReport?.ownerAccountID, - rules, - reportNameValuePairs, - outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, - ); - count += reports.length; - if (count > stopAt) { - break; - } - } - return count; - }; - - // A member who cannot create expenses on this policy (e.g. approveOnly role) can only move the expense if - // there is another eligible workspace or an existing outstanding report to move it to. Otherwise disallow it. - if (isBlockedOnCurrentPolicy) { - const hasOtherEligiblePolicies = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some( - (p) => p.id !== moneyRequestReport?.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(p)), - ); - if (!hasOtherEligiblePolicies && countOutstandingReports(moneyRequestReport?.policyID, 0) === 0) { + // A member who cannot create expenses on the report's policy can only move their own expense, + // and only to another workspace where it can be reported, or to an existing outstanding report there. + if (!isAdmin && !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, deprecatedCurrentUserEmail)) { + if (!isOwner) { return false; } + const isPerDiem = isPerDiemRequest(transaction); + const isEligibleDestination = (policy: Policy) => policy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(policy)); + const canCreateReportElsewhere = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some(isEligibleDestination); + return ( + canCreateReportElsewhere || + policiesArray.some( + (policy) => + isEligibleDestination(policy) && + canCreateExpensesOnPolicy(policy, deprecatedCurrentUserEmail) && + getOutstandingReportsForUser(policy.id, moneyRequestReport.ownerAccountID, rules, reportNameValuePairs, outstandingReportsByPolicyID?.[policy.id] ?? {}).length > 0, + ) + ); } // Check the cheaper condition first @@ -6266,8 +6245,21 @@ function canEditFieldOfMoneyRequest({ } // Check if there are multiple outstanding reports across policies - if (countOutstandingReports(undefined, 1) > 1) { - return true; + let outstandingReportsCount = 0; + for (const currentPolicy of policiesArray) { + const reports = getOutstandingReportsForUser( + currentPolicy.id, + moneyRequestReport?.ownerAccountID, + rules, + reportNameValuePairs, + outstandingReportsByPolicyID?.[currentPolicy?.id] ?? {}, + ); + outstandingReportsCount += reports.length; + + // Short-circuit once we find more than 1 + if (outstandingReportsCount > 1) { + return true; + } } return false; diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 66c29e12696c..a571e897c56a 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -224,13 +224,13 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(restrictionPolicy.id)); return; } - if (!canCreateReport) { - return; - } if (hasPerDiemTransactions) { handleCreateReport(); return; } + if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { + return; + } if (shouldSelectPolicy) { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute(true))); return; From 3aa6afabcd82be4083678311760b34e13ab1715e Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:47:02 +0530 Subject: [PATCH 14/17] Fix shadowed policy params in move eligibility gate --- src/libs/ReportUtils.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ad47fb21a72e..6433e8519710 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6226,15 +6226,22 @@ function canEditFieldOfMoneyRequest({ return false; } const isPerDiem = isPerDiemRequest(transaction); - const isEligibleDestination = (policy: Policy) => policy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(policy)); + const isEligibleDestination = (destinationPolicy: Policy) => + destinationPolicy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(destinationPolicy)); const canCreateReportElsewhere = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some(isEligibleDestination); return ( canCreateReportElsewhere || policiesArray.some( - (policy) => - isEligibleDestination(policy) && - canCreateExpensesOnPolicy(policy, deprecatedCurrentUserEmail) && - getOutstandingReportsForUser(policy.id, moneyRequestReport.ownerAccountID, rules, reportNameValuePairs, outstandingReportsByPolicyID?.[policy.id] ?? {}).length > 0, + (destinationPolicy) => + isEligibleDestination(destinationPolicy) && + canCreateExpensesOnPolicy(destinationPolicy, deprecatedCurrentUserEmail) && + getOutstandingReportsForUser( + destinationPolicy.id, + moneyRequestReport.ownerAccountID, + rules, + reportNameValuePairs, + outstandingReportsByPolicyID?.[destinationPolicy.id] ?? {}, + ).length > 0, ) ); } From 1b81f78f34b90caaa294790d2d4dd6090d2c6d8b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:22:52 +0530 Subject: [PATCH 15/17] Collapse move-destination checks into a single policy loop --- src/libs/ReportUtils.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e530da739e26..c7e57036a4d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -144,12 +144,12 @@ import { arePaymentsEnabled, canMemberWrite as canMemberWritePolicyUtils, canCreateExpensesOnPolicy, + canCreateReportOnPolicy, canSendInvoiceFromWorkspace, canSubmitPerDiemExpenseFromWorkspace, getActivePolicies, getConnectedIntegration, getForwardsToAccount, - getGroupPoliciesWhereReportCanBeCreated, getManagerAccountEmail, getManagerAccountID, getPerDiemCustomUnit, @@ -6226,24 +6226,24 @@ function canEditFieldOfMoneyRequest({ return false; } const isPerDiem = isPerDiemRequest(transaction); - const isEligibleDestination = (destinationPolicy: Policy) => - destinationPolicy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(destinationPolicy)); - const canCreateReportElsewhere = getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some(isEligibleDestination); - return ( - canCreateReportElsewhere || - policiesArray.some( - (destinationPolicy) => - isEligibleDestination(destinationPolicy) && - canCreateExpensesOnPolicy(destinationPolicy, deprecatedCurrentUserEmail) && + return policiesArray.some((destinationPolicy) => { + if (destinationPolicy.id === moneyRequestReport.policyID || (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(destinationPolicy))) { + return false; + } + const destinationPolicyID = destinationPolicy.id; + const canCreateReportElsewhere = canCreateReportOnPolicy(destinationPolicy, deprecatedCurrentUserEmail); + return ( + canCreateReportElsewhere || + (canCreateExpensesOnPolicy(destinationPolicy, deprecatedCurrentUserEmail) && getOutstandingReportsForUser( - destinationPolicy.id, + destinationPolicyID, moneyRequestReport.ownerAccountID, rules, reportNameValuePairs, - outstandingReportsByPolicyID?.[destinationPolicy.id] ?? {}, - ).length > 0, - ) - ); + outstandingReportsByPolicyID?.[destinationPolicyID] ?? {}, + ).length > 0) + ); + }); } // Check the cheaper condition first From 730c0276b65fb86ea71bbae9e2a145c64381b509 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:35:34 +0530 Subject: [PATCH 16/17] Remove unreachable guard from createReport --- src/pages/iou/request/step/DynamicIOURequestEditReport.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index a571e897c56a..0cb2c781a0f8 100644 --- a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx @@ -228,9 +228,6 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps) handleCreateReport(); return; } - if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { - return; - } if (shouldSelectPolicy) { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute(true))); return; From df24d631a17dfeebbedc4f008a3b2bcd04797f1a Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:47:51 +0530 Subject: [PATCH 17/17] Use suggested move-destination check --- src/libs/ReportUtils.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c7e57036a4d1..7823d29e3783 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -144,12 +144,12 @@ import { arePaymentsEnabled, canMemberWrite as canMemberWritePolicyUtils, canCreateExpensesOnPolicy, - canCreateReportOnPolicy, canSendInvoiceFromWorkspace, canSubmitPerDiemExpenseFromWorkspace, getActivePolicies, getConnectedIntegration, getForwardsToAccount, + getGroupPoliciesWhereReportCanBeCreated, getManagerAccountEmail, getManagerAccountID, getPerDiemCustomUnit, @@ -6219,31 +6219,15 @@ function canEditFieldOfMoneyRequest({ return false; } - // A member who cannot create expenses on the report's policy can only move their own expense, - // and only to another workspace where it can be reported, or to an existing outstanding report there. + // A member who cannot create expenses on the report's policy can only move their own expense to another workspace where they can create a report. if (!isAdmin && !!reportPolicy && !canCreateExpensesOnPolicy(reportPolicy, deprecatedCurrentUserEmail)) { if (!isOwner) { return false; } const isPerDiem = isPerDiemRequest(transaction); - return policiesArray.some((destinationPolicy) => { - if (destinationPolicy.id === moneyRequestReport.policyID || (isPerDiem && !canSubmitPerDiemExpenseFromWorkspace(destinationPolicy))) { - return false; - } - const destinationPolicyID = destinationPolicy.id; - const canCreateReportElsewhere = canCreateReportOnPolicy(destinationPolicy, deprecatedCurrentUserEmail); - return ( - canCreateReportElsewhere || - (canCreateExpensesOnPolicy(destinationPolicy, deprecatedCurrentUserEmail) && - getOutstandingReportsForUser( - destinationPolicyID, - moneyRequestReport.ownerAccountID, - rules, - reportNameValuePairs, - outstandingReportsByPolicyID?.[destinationPolicyID] ?? {}, - ).length > 0) - ); - }); + return getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some( + (destinationPolicy) => destinationPolicy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(destinationPolicy)), + ); } // Check the cheaper condition first