Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c82f269
Fix unresponsive create report in move to report
ShridharGoel Oct 23, 2026
d3045f6
Remove useMemo and useCallback hooks
ShridharGoel Oct 24, 2026
5599b64
Gate moving expenses for approve-only members
ShridharGoel Oct 24, 2026
f81f0fa
Fix move permission when policy is unblocked
ShridharGoel Oct 24, 2026
bbcc94a
Revert changes in DynamicIOURequestStepUpgrade
ShridharGoel Oct 24, 2026
6d292fc
Simplify create report gating in edit report
ShridharGoel Oct 24, 2026
90e94eb
Hoist isPerDiemRequest check out of loops
ShridharGoel Oct 24, 2026
da4f625
Deduplicate report counting in move permission
ShridharGoel Oct 24, 2026
abfdc2a
Remove unnecessary fallback for reportOwnerLogin
ShridharGoel Oct 24, 2026
7a979ad
Deduplicate create report check in edit report
ShridharGoel Oct 24, 2026
0937397
Address review feedback on move eligibility and test
ShridharGoel Oct 24, 2026
fc6dd4c
Allow admins to move expenses of approve-only members
ShridharGoel Oct 24, 2026
c26ac11
Scope move eligibility gate to members who cannot create expenses
ShridharGoel Oct 25, 2026
3aa6afa
Fix shadowed policy params in move eligibility gate
ShridharGoel Oct 25, 2026
54b69fb
Merge branch 'main' of github.com:Expensify/App into fix/create-repor…
ShridharGoel Oct 25, 2026
1b81f78
Collapse move-destination checks into a single policy loop
ShridharGoel Oct 27, 2026
730c027
Remove unreachable guard from createReport
ShridharGoel Oct 27, 2026
df24d63
Use suggested move-destination check
ShridharGoel Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ import {
canMemberWrite as canMemberWritePolicyUtils,
canCreateExpensesOnPolicy,
canSendInvoiceFromWorkspace,
canSubmitPerDiemExpenseFromWorkspace,
getActivePolicies,
getConnectedIntegration,
getForwardsToAccount,
getGroupPoliciesWhereReportCanBeCreated,
getManagerAccountEmail,
getManagerAccountID,
getPerDiemCustomUnit,
Expand Down Expand Up @@ -6217,6 +6219,17 @@ function canEditFieldOfMoneyRequest({
return false;
}

// 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 getGroupPoliciesWhereReportCanBeCreated(allPolicies, deprecatedCurrentUserEmail).some(
(destinationPolicy) => destinationPolicy.id !== moneyRequestReport.policyID && (!isPerDiem || canSubmitPerDiemExpenseFromWorkspace(destinationPolicy)),
);
}

Comment thread
ShridharGoel marked this conversation as resolved.
// Check the cheaper condition first
if ((isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) {
return true;
Expand Down
7 changes: 3 additions & 4 deletions src/pages/iou/request/step/DynamicIOURequestEditReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps)
shouldBypassConfirmation: true,
});

const canCreateReport = !!policyForMovingExpensesID || shouldSelectPolicy || hasUnreportedManagedCardTransactions;

const createReport = () => {
const restrictionPolicy = policyForMovingExpenses;
if (
Expand All @@ -226,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;
Expand All @@ -247,7 +246,7 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps)
selectReport={selectReport}
removeFromReport={removeFromReport}
isEditing={action === CONST.IOU.ACTION.EDIT}
createReport={createReport}
createReport={canCreateReport ? createReport : undefined}
isPerDiemRequest={hasPerDiemTransactions}
isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions}
transactionPolicyID={targetExpensePolicyID}
Expand Down
79 changes: 79 additions & 0 deletions tests/unit/canEditFieldOfMoneyRequestTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,85 @@ 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 () => {
Comment thread
ShridharGoel marked this conversation as resolved.
// Given a policy where the current user only has approveOnly role and no other workspaces
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);

// When checking if the user 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 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', () => {
Expand Down
Loading