diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b1d299082bb8..7823d29e3783 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,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)), + ); + } + // Check the cheaper condition first if ((isOwner || isAdmin || isManager) && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID, rules, moneyRequestReportNameValuePair)) { return true; diff --git a/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx b/src/pages/iou/request/step/DynamicIOURequestEditReport.tsx index 4cdc6d5e84c3..0cb2c781a0f8 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 || hasUnreportedManagedCardTransactions; + const createReport = () => { const restrictionPolicy = policyForMovingExpenses; if ( @@ -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; @@ -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} diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index 38f0114012e4..da5cdecc63ba 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -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 () => { + // 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', () => {