Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,8 @@ const CONST = {
UNREPORTED_REPORT_ID: '0',
TRASH_REPORT_ID: '-1',
SPLIT_REPORT_ID: '-2',
// "Auto report" destination: the backend picks each expense's report (its owner's latest draft, or a new one).
AUTOMATIC_REPORT_ID: '-3',
SECONDARY_ACTIONS: {
SUBMIT: 'submit',
APPROVE: 'approve',
Expand Down
27 changes: 23 additions & 4 deletions src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ import {
isDeletedTransaction,
isDistanceRequest,
isManagedCardTransaction,
isManualDistanceRequest,
isOdometerDistanceRequest,
isPending,
isPerDiemRequest,
isScanning,
Expand Down Expand Up @@ -2708,9 +2710,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
getReportOrDraftReport(transactionEntry.reportID, undefined, undefined, undefined, allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionEntry.reportID}`])?.ownerAccountID;
if (typeof ownerAccountID === 'number') {
ownerAccountIDs.add(ownerAccountID);
if (ownerAccountIDs.size > 1) {
break;
}
} else {
hasUnknownOwner = true;
}
Expand All @@ -2719,7 +2718,27 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {

const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport);

if (canAllTransactionsBeMoved && !hasMultipleOwners && !isExpenseReportType) {
// Across submitters the only destination the App can offer is "Auto report". Every other mixed-owner selection
// stays hidden as before, so there is no entry into a screen that could only offer one submitter's reports to
// everybody else's expenses. Requirements:
// - every owner resolved, or the count below cannot tell one cardholder's bulk selection from a mixed one
// - every expense on a managed card, because the backend resolves each destination through the card; one
// expense without a card fails the whole request with "404 Card not found"
// - nothing whose validity depends on the destination workspace, which the backend picks: per diem rates and
// the map/GPS rules on manual and odometer distance can only be checked against a known workspace
// An expense we cannot read fails all three, so it withholds the flow rather than risking a rejected move.
const canAutoReportAcrossSubmitters =
ownerAccountIDs.size > 1 &&
!hasUnknownOwner &&
selectedTransactionsKeys.every((id) => {
const transaction = selectedTransactions[id]?.transaction ?? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`];
if (!transaction || !isManagedCardTransaction(transaction)) {
return false;
}
return !(isPerDiemRequest(transaction) || isManualDistanceRequest(transaction) || isOdometerDistanceRequest(transaction));
});
Comment on lines +2721 to +2739

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto report reaches expenses on Processing reports - Is this expected?

canAutoReportAcrossSubmitters checks owners, managed cards and per diem/distance, but not report state.
canChangeReport is true for an admin on any outstanding report, including Processing, so submitted company-card expenses from two employees now reach Move to report → Auto report.
On main a mixed-owner selection never reaches the move flow.

  1. As a workspace admin, have members A and B each submit a report containing one company-card expense (Processing, awaiting first approval).
  2. Go to Search → Expenses and select both expenses.
  3. Open the bulk-action menu and tap Move to report.

Should Move to report be offered (with Auto report) or not?


if (canAllTransactionsBeMoved && !isExpenseReportType && (!hasMultipleOwners || canAutoReportAcrossSubmitters)) {
options.push({
text: translate('iou.moveExpenses'),
icon: expensifyIcons.DocumentMerge,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Der Bericht konnte nicht abgelehnt werden. Bitte versuchen Sie es erneut.',
},
moveExpenses: 'Zum Bericht verschieben',
autoReport: 'Automatischer Bericht',
autoReportDescription: 'Im Namen des Mitarbeiters zu Berichtsentwürfen hinzufügen',
moveExpensesMaxTransactionsError: `Berichte sind auf ${CONST.REPORT.MAX_TRANSACTIONS} Ausgaben begrenzt. Bitte verschiebe einige in einen anderen Bericht.`,
moveExpensesError: 'Sie können Pauschalspesen nicht in Berichte anderer Arbeitsbereiche verschieben, da die Pauschalsätze je nach Arbeitsbereich unterschiedlich sein können.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Η αναφορά δεν μπόρεσε να απορριφθεί. Παρακαλούμε προσπαθήστε ξανά.',
},
moveExpenses: 'Μετακίνηση στην αναφορά',
autoReport: 'Αυτόματη αναφορά',
autoReportDescription: 'Προσθήκη σε προσχέδια αναφορών εκ μέρους του εργαζομένου',
moveExpensesMaxTransactionsError: `Οι αναφορές περιορίζονται σε ${CONST.REPORT.MAX_TRANSACTIONS} έξοδα. Μετακινήστε ορισμένα σε άλλη αναφορά.`,
moveExpensesError:
'Δεν μπορείτε να μετακινήσετε έξοδα ημερήσιας αποζημίωσης σε αναφορές άλλων χώρων εργασίας, επειδή οι τιμές ημερήσιας αποζημίωσης μπορεί να διαφέρουν μεταξύ των χώρων εργασίας.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,8 @@ const translations = {
couldNotReject: 'The report could not be rejected. Please try again.',
},
moveExpenses: 'Move to report',
autoReport: 'Auto report',
autoReportDescription: 'Add to draft reports on behalf of the employee',
moveExpensesError: "You can't move per diem expenses to reports on other workspaces, because the per diem rates may differ between workspaces.",
moveExpensesMaxTransactionsError: `Reports are limited to ${CONST.REPORT.MAX_TRANSACTIONS} expenses. Please move some to another report.`,
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'No se pudo rechazar el informe. Por favor, inténtalo de nuevo.',
},
moveExpenses: 'Mover a informe',
autoReport: 'Informe automático',
autoReportDescription: 'Añadir a los informes borrador en nombre del empleado',
moveExpensesMaxTransactionsError: `Los informes están limitados a ${CONST.REPORT.MAX_TRANSACTIONS} gastos. Por favor, mueve algunos a otro informe.`,
moveExpensesError: 'No puedes mover gastos per diem a informes de otros espacios de trabajo, porque las tarifas de dietas pueden diferir entre espacios de trabajo.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'La note de frais n’a pas pu être rejetée. Veuillez réessayer.',
},
moveExpenses: 'Déplacer vers le rapport',
autoReport: 'Rapport automatique',
autoReportDescription: 'Ajouter aux brouillons de rapports au nom de l’employé',
moveExpensesMaxTransactionsError: `Les rapports sont limités à ${CONST.REPORT.MAX_TRANSACTIONS} dépenses. Veuillez en déplacer certaines vers un autre rapport.`,
moveExpensesError:
'Vous ne pouvez pas déplacer des frais de per diem vers des notes de frais d’autres espaces de travail, car les taux de per diem peuvent varier d’un espace de travail à l’autre.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Non è stato possibile rifiutare il report. Riprova.',
},
moveExpenses: 'Sposta nel report',
autoReport: 'Report automatico',
autoReportDescription: 'Aggiungi alle bozze dei report a nome del dipendente',
moveExpensesMaxTransactionsError: `I report sono limitati a ${CONST.REPORT.MAX_TRANSACTIONS} spese. Spostane alcune in un altro report.`,
moveExpensesError: 'Non puoi spostare le spese di diaria nei report di altri spazi di lavoro, perché le tariffe di diaria possono variare tra gli spazi di lavoro.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'レポートを拒否できませんでした。もう一度お試しください。',
},
moveExpenses: 'レポートに移動',
autoReport: '自動レポート',
autoReportDescription: '従業員に代わって下書きレポートに追加',
moveExpensesMaxTransactionsError: `レポートは${CONST.REPORT.MAX_TRANSACTIONS}件の経費までに制限されています。一部を別のレポートに移動してください。`,
moveExpensesError: '日当経費は、ワークスペースごとに日当レートが異なる場合があるため、他のワークスペースのレポートに移動することはできません。',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Het rapport kon niet worden afgewezen. Probeer het opnieuw.',
},
moveExpenses: 'Verplaatsen naar rapport',
autoReport: 'Automatisch rapport',
autoReportDescription: 'Toevoegen aan conceptrapporten namens de werknemer',
moveExpensesMaxTransactionsError: `Rapporten zijn beperkt tot ${CONST.REPORT.MAX_TRANSACTIONS} uitgaven. Verplaats er enkele naar een ander rapport.`,
moveExpensesError: 'Je kunt dagvergoedingdeclaraties niet naar rapporten in andere werkruimten verplaatsen, omdat de dagvergoedingsbedragen per werkruimte kunnen verschillen.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1806,6 +1806,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Nie udało się odrzucić raportu. Spróbuj ponownie.',
},
moveExpenses: 'Przenieś do raportu',
autoReport: 'Raport automatyczny',
autoReportDescription: 'Dodaj do szkiców raportów w imieniu pracownika',
moveExpensesMaxTransactionsError: `Raporty są ograniczone do ${CONST.REPORT.MAX_TRANSACTIONS} wydatków. Przenieś część z nich do innego raportu.`,
moveExpensesError: 'Nie możesz przenosić diet do raportów w innych przestrzeniach roboczych, ponieważ stawki diet mogą się różnić między przestrzeniami roboczymi.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: 'Não foi possível rejeitar o relatório. Tente novamente.',
},
moveExpenses: 'Mover para relatório',
autoReport: 'Relatório automático',
autoReportDescription: 'Adicionar aos relatórios de rascunho em nome do funcionário',
moveExpensesMaxTransactionsError: `Os relatórios são limitados a ${CONST.REPORT.MAX_TRANSACTIONS} despesas. Mova algumas para outro relatório.`,
moveExpensesError: 'Você não pode mover despesas de diária para relatórios em outros workspaces, porque as tarifas de diária podem ser diferentes entre os workspaces.',
submitReportTo: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,8 @@ const translations: TranslationDeepObject<typeof en> = {
couldNotReject: '无法拒绝该报表。请重试。',
},
moveExpenses: '移动到报告',
autoReport: '自动报告',
autoReportDescription: '代表员工添加到草稿报告',
moveExpensesMaxTransactionsError: `每个报告最多可包含 ${CONST.REPORT.MAX_TRANSACTIONS} 笔支出。请将部分支出移动到其他报告。`,
moveExpensesError: '您无法将每日津贴报销移动到其他工作区的报表中,因为不同工作区的每日津贴标准可能不同。',
submitReportTo: {
Expand Down
35 changes: 35 additions & 0 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import DateUtils from '@libs/DateUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils';
import {toLocaleDigit} from '@libs/LocaleDigitUtils';
import {translateLocal} from '@libs/Localize';
import {buildOptimisticNextStep} from '@libs/NextStepUtils';
Expand Down Expand Up @@ -2038,6 +2039,39 @@ function changeTransactionsReport(props: ChangeTransactionsReportProps) {
});
}

/**
* Reports expenses without naming a destination: the backend puts each one in its owner's latest draft report, or a new
* report when they have none. Needed when a selection spans submitters, since any report the App picked would belong to
* just one of them. Carries no optimistic data because every update the normal move builds hangs off a destination only
* the backend knows.
*/
function autoReportTransactions(transactionIDs: string[]) {
if (transactionIDs.length === 0) {
return;
}

// There is no optimistic move to roll back, but a failure still has to reach the admin: the screen has closed and
// the selection is gone by then, so the error is surfaced on the expenses themselves via their red brick road.
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION>> = transactionIDs.map((transactionID) => ({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {errors: null},
}));
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION>> = transactionIDs.map((transactionID) => ({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage')},
}));
Comment on lines +2060 to +2064

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto report failures don't show in Search

failureData writes errors onto each transaction, but getTransactionsSections sets errors: undefined on every Search row (src/libs/SearchUIUtils.ts:2450, :3461), and report-level red brick roads don't read transaction.errors.

  1. As a workspace admin, go to Search → Expenses and select unreported card expenses from two cardholders.
  2. Tap Move to report → Auto report.
  3. Make the ChangeTransactionsReport request fail.

Expected: The affected expenses show an error in the list.
Actual: Nothing appears in Search; the expenses just stay unreported.


const parameters: ChangeTransactionsReportParams = {
transactionList: transactionIDs.join(','),
reportID: CONST.REPORT.AUTOMATIC_REPORT_ID,
transactionIDToReportActionAndThreadData: '{}',
};

API.write(WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT, parameters, {successData, failureData});
}

function getDefaultP2PMileageRate() {
API.read(READ_COMMANDS.GET_DEFAULT_P2P_MILEAGE_RATE, null);
}
Expand All @@ -2059,6 +2093,7 @@ function getDuplicateTransactionDetails(transactionID?: string) {
}

export {
autoReportTransactions,
saveWaypoint,
removeWaypoint,
getRoute,
Expand Down
37 changes: 36 additions & 1 deletion src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import usePersonalPolicy from '@hooks/usePersonalPolicy';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';

import {createNewReport} from '@libs/actions/Report';
import {changeTransactionsReport} from '@libs/actions/Transaction';
import {autoReportTransactions, changeTransactionsReport} from '@libs/actions/Transaction';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue';
Expand All @@ -23,6 +23,7 @@ import {generateReportID, getPersonalDetailsForAccountID, getReportOrDraftReport
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {
isDistanceRequest as isDistanceRequestUtil,
isManagedCardTransaction,
isManualDistanceRequest as isManualDistanceRequestUtil,
isOdometerDistanceRequest as isOdometerDistanceRequestUtil,
isUnreportedManagedCardTransaction,
Expand Down Expand Up @@ -119,6 +120,29 @@ function SearchTransactionsChangeReport() {
return report?.ownerAccountID;
}, [selectedTransactions, selectedTransactionsKeys, allReports]);
const targetOwnerPersonalDetails = useMemo(() => getPersonalDetailsForAccountID(targetOwnerAccountID, personalDetails) as PersonalDetails, [personalDetails, targetOwnerAccountID]);
// Kept separate from `targetOwnerAccountID`, which stops at the first owner it finds. Counting needs them all.
// Only distinct resolved owners count. An owner we cannot resolve must not stand in for a second submitter: for an
// unreported expense the report lookup can never resolve one (its reportID is `0`), so a search snapshot missing
// the money-request action would otherwise file one cardholder's bulk selection as mixed and strip its report list.
// "Auto report" has the backend resolve each destination through the expense's card, so one expense without a card
// fails the whole request with "404 Card not found".
Comment on lines +123 to +128

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please simplify all inline comments which are too long.

const areAllManagedCardTransactions = selectedTransactionsKeys.length > 0 && transactions.length === selectedTransactionsKeys.length && transactions.every(isManagedCardTransaction);
const hasMultipleSubmitters = useMemo(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One cardholder's selection with unresolved owners gets no Move to report - expected?

This now ignores unresolved owners, but the entry gate at src/hooks/useSearchBulkActions.ts:2717 still counts them as a second owner, so the selection never reaches the fixed screen. Behavior is unchanged from main.

  1. As a workspace admin, go to Search → Expenses and select two unreported card expenses from one cardholder whose rows have no money-request action.
  2. Open the bulk-action menu.

Should Move to report be offered (with the cardholder's reports and Create report) or not?

const ownerAccountIDs = new Set<number>();

for (const transactionKey of selectedTransactionsKeys) {
const selection = selectedTransactions[transactionKey];
const reportID = selection?.reportID;
const ownerAccountID =
selection?.ownerAccountID ?? getReportOrDraftReport(reportID, undefined, undefined, undefined, allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`])?.ownerAccountID;

if (typeof ownerAccountID === 'number') {
ownerAccountIDs.add(ownerAccountID);
}
}

return ownerAccountIDs.size > 1;
}, [selectedTransactions, selectedTransactionsKeys, allReports]);

useHydrateReportsFromSnapshot(currentSearchResults, allReports);

Expand Down Expand Up @@ -258,6 +282,14 @@ function SearchTransactionsChangeReport() {
Navigation.goBack(undefined, {afterTransition: clearSelectedTransactions});
};

const autoReport = () => {
if (selectedTransactionsKeys.length === 0) {
return;
}
autoReportTransactions(selectedTransactionsKeys);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce billing restrictions before auto-reporting

When a multi-submitter selection includes expenses from a workspace whose owner is past due, this path sends ChangeTransactionsReport without the shouldRestrictUserBillableActions check used by explicit report selection and report creation. Because Auto report can move expenses into or create draft reports, it either bypasses the billing restriction or is rejected server-side after the RHP closes and the selection is cleared; check every involved policy and route to the restricted-action screen before invoking the write.

Useful? React with 👍 / 👎.

Navigation.goBack(undefined, {afterTransition: clearSelectedTransactions});
};

const removeFromReport = () => {
if (selectedTransactionsKeys.length === 0) {
return;
Expand Down Expand Up @@ -302,6 +334,9 @@ function SearchTransactionsChangeReport() {
transactionPolicyID={selectedReportPolicyID}
isPerDiemRequest={hasPerDiemTransactions}
isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions}
hasMultipleSubmitters={hasMultipleSubmitters}
areAllManagedCardTransactions={areAllManagedCardTransactions}
autoReport={autoReport}
/>
);
}
Expand Down
Loading
Loading