Repository navigation
[Due for payment 2026-10-15] Extend admin bulk edit to expenses on approved and paid reports #101253
Description
Proposal
Extend admin bulk edit to expenses on approved and paid reports.
Background
- Expensify imports the chart of accounts from a connected accounting system and turns each GL account into a workspace category. A category is identified by its name. When someone codes an expense to a category, the expense records that name, and the name is what we use at export to work out which GL account the expense belongs to. Tags work the same way.
- When an accountant renames a GL account in their accounting system, the next sync renames the matching Expensify category in place. The old name disappears from the category list and the new name takes its place, still pointing at the same underlying account. Expenses already coded to the old name keep the old name, get an Out of policy violation, and the mismatched name prevents them from exporting.
- Today, admins can edit an expense individually in all states, including after a report is approved or paid. Admins can only bulk edit coding on draft or outstanding reports, to prevent accidental changes to records that should normally stay as they are.
Problem
When a workspace syncs categories from a connected accounting system and expenses on approved or paid reports are coded to those categories, if the accountant renames one of those GL accounts, then every affected expense is blocked from exporting. The spend never reaches the general ledger and the books stay incomplete.
Fixing this today means editing every affected expense one at a time. For a customer who just hit this, that is thousands of expenses.
Solution
Allow admins to bulk edit expenses on approved and paid reports.
Admins can already make this change one expense at a time, so this grants no additional access they don't have today. It only removes an inconsistency between the individual and bulk flows.
To prevent unintentional bulk changes, when any selected expense is on an approved or paid report, show a warning modal before the change commits:
3 of the 12 expenses you selected are on approved or paid reports. You're about to edit finalized expenses. Are you sure?
Notes
- This is already possible in Expensify Classic, so New Expensify should match.
- The warning modal should only appear when the selection actually includes an approved or paid expense, and should name the count of finalized expenses in the selection.
Reported in Slack.
Activity
heyjennahay commentedon Sep 15, 2026
@MelvinBot can you please outline what is required to implement this?
MelvinBot commentedon Sep 15, 2026
Proposal
What is the root cause of that problem?
One guard.
src/libs/ReportUtils.ts:5273-5277makescanEditMultipleTransactionsreturnfalseas soon as any selected expense sits on an approved or settled report, so the Edit multiple option disappears from the selection menu.Nothing below that guard needs new permission logic:
canEditFieldOfMoneyRequestblocks the restricted fields (amount, currency, merchant, date, receipt, distance, reimbursable, report, billable) and allows the coding fields — category, tag, description, tax, attendees. That is exactly the renamed-GL-account case.canEditMoneyRequestreturnsfalseon approved/settled/closed reports, so removing the guard does not widen access beyond admins and managers. No new admin check is needed.UpdateMoneyRequestper expense atsrc/libs/actions/IOU/BulkEdit.ts:175, so no new endpoint is expected.What changes do you think we should make in order to solve the problem?
1. Drop the guard — remove the
isApproved || isSettledearly return incanEditMultipleTransactions. The loop's existing per-transaction editability check takes over. Both call sites pick it up for free: search bulk actions atuseSearchBulkActions.ts:2253and the report page atuseSelectedTransactionsActions.ts:290.2. Add the warning modal — a
ConfirmModalinsave()atSearchEditMultiplePage.tsx:162. Count finalized expenses from the existingselectedTransactionContextsusingisReportApproved/isSettled, show the modal only when that count is above zero, and runupdateMultipleMoneyRequestson confirm. Add the copy as a count-taking key next tosearch.bulkActions.editMultiple*inen.ts:9679, plus the other 10 locale files, with singular and plural forms.3. Disable Billable and Reimbursable when the selection is mixed — this is the one real gap. Both are restricted fields, but their rows at
SearchEditMultiplePage.tsx:323-339carry nodisabledflag — they are gated only on policy config. Once approved expenses can be selected, an admin could set Billable and have it silently dropped for the finalized ones by the per-field filter atBulkEdit.ts:281-289. Adddisabled: isFieldDisabledForAnyTransaction(...)to both, the same way amount, merchant, date and tax already do it at lines 115-122.4. Tests —
tests/unit/canEditMultipleTransactionsTest.ts:258asserts today's behavior and must be inverted. Add cases for a paid report, for a non-admin (stillfalse), for the modal count, and for the two newly disabled rows.Two things to confirm before building
UpdateMoneyRequestper expense. The customer case in this issue is thousands of expenses — worth a backend check that the queue handles that shape before shipping.CONST.BETAS.BULK_EDIT, so this ships to beta users first.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/101253#issuecomment-5688699701)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Sep 16, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @daledah (
External)melvin-bot commentedon Sep 16, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
daledah commentedon Sep 18, 2026
working on this issue now
daledah commentedon Sep 18, 2026
@MelvinBot, the direction is right and the root cause is correct. We can discuss some improvements in the PR.
@MelvinBot implement this
MelvinBot commentedon Sep 18, 2026
🤖 Draft PR created: #101553
Please mark it as "Ready for review" when it is ready for review.
Implemented all four parts of the approved proposal:
isApproved || isSettledearly return incanEditMultipleTransactions, so the loop's existing per-transaction editability check decides instead. Both entry points pick it up unchanged.en.tsplus all 10 other locales with singular and plural forms.isFieldDisabledForAnyTransactioncheck amount, merchant, date and tax already use.Two deviations from the proposal, both minor:
ConfirmModal. That component is deprecated andlint-changedrejects it, so the modal uses theuseConfirmModalhook instead. Same UI, no local modal state.{count, total}inline rather than a new type inparams.ts, matching the existingcommon.durationDaysprecedent.Verification
Typecheck,
lint-changed,spell-changed, and the React Compiler compliance check all pass. Unit tests: 21/21 incanEditMultipleTransactionsTest, 1373/1373 inReportUtilsTest, 1847/1847 across the Search and bulk-action hook suites, plus the translate and bulk-edit utils suites.Not verified: the UI itself. The browser session I was handed rendered blank after CI's own onboarding replay failed, and reloading isn't a permitted recovery step — the dev server was healthy, so this was session infrastructure, not the build. The confirmation dialog and the disabled Billable/Reimbursable rows still need a human pass, and the
Tests/QAsections are left for you.Also still open from the proposal: bulk edit enqueues one
UpdateMoneyRequestper expense, and the customer case here ran to thousands. Worth a backend check on that shape before shipping. Both entry points remain behind thebulkEditbeta.I don't have push access to your fork, so the PR is from an upstream branch — instructions for taking it over are on the PR. Happy to discuss the improvements you had in mind there.
48 remaining items
Eskalifer1 commentedon Oct 8, 2026
Not sure why @francoisl was unassigned...
Just assigning myself since i reviewed this pr: #102720
francoisl commentedon Oct 8, 2026
Because it needs an internal employee for the final review and to merge the PR, and so we also assign the employee to the linked issue
Eskalifer1 commentedon Oct 8, 2026
yea, What I mean is, when I assigned myself, you were unassigned, but I assigned you again, so no problem!
Eskalifer1 commentedon Oct 13, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: N/A - follow-up to Allow admins to bulk edit expenses on approved and paid reports #101553
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: N/A
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
Test:
bulkEditbeta and sign in as an admin of a Collect or Control workspace with Billable enabled.Edit finalized expenses?modal, and verify both expenses update.Do we agree 👍 or 👎
Eskalifer1 commentedon Oct 13, 2026
Note about payment
As far as I understand, two PRs were created as part of this task:
The second PR was a follow-up to the first.
Since I only reviewed the second PR, I should receive the standard payment ($175). (I'm getting paid via Upwork)
@daledah, could you please describe the work you completed so Matt can understand what the payment amount should be? Thank you!
melvin-bot commentedon Oct 14, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)daledah commentedon Oct 15, 2026
@Eskalifer1 agreed on the split, you reviewed #102720, so $175 for that review is right. Here's my side.
Price increase request
Mandatory
Proposed amount: $350 - $175 as C+ on #101553 + $175 as the author of #102720.
Why the work exceeded the issue: I held two different roles, but the payment summary only counts one. The $175 listed covers my C+ review of #101553. It does not cover #102720, where I was the contributor, not the reviewer.
#102720 wasn't in the original scope. After #101553 deployed, @trjExpensify comment that
Billableis editable on an approved report one expense at a time but missing fromEdit multiple- a separate root cause that predates #101553 (BILLABLEwas already inrestrictedFields)Regressions caused by this PR: None. No deploy blocker and no regression issue against either PR.
Multiple PRs: #101553 (C+) and #102720 (author).
cc @mallenexpensify @trjExpensify
mallenexpensify commentedon Oct 16, 2026
Payment Summary
Contributor: @Eskalifer1 due $175 via Upwork
Contributor+: @daledah due $350 via NewDot
@Eskalifer1 can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022111160965840563145
@daledah your request makes sense to me, cc @trjExpensify @francoisl in case you disagree.
Eskalifer1 commentedon Oct 16, 2026
Hi @mallenexpensify, i have accepted, thank you!