Skip to content
Draft
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 @@ -1984,6 +1984,8 @@ const CONST = {
IN_APP: 'in-app',
},
EXPORT_TEMPLATE: 'exportTemplate',
// Marker present in a template column formula (e.g. {expense:category:glcode}) that requires a workspace policyID to resolve GL codes
EXPORT_TEMPLATE_GL_CODE_COLUMN_MARKER: ':glcode',
NEXT_STEP: {
MESSAGE_KEY: {
WAITING_TO_ADD_TRANSACTIONS: 'waitingToAddTransactions',
Expand Down
10 changes: 8 additions & 2 deletions src/hooks/useExportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {PopoverMenuItem} from '@components/PopoverMenu';
import {useSearchSelectionActions} from '@components/Search/SearchContext';
import {openOldDotLink} from '@libs/actions/Link';
import {exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report';
import {getExportTemplates, queueExportSearchWithTemplate} from '@libs/actions/Search';
import {doesExportTemplateRequireWorkspacePolicy, getExportTemplates, queueExportSearchWithTemplate} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getConnectedIntegration, getValidConnectedIntegration} from '@libs/PolicyUtils';
import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils';
Expand Down Expand Up @@ -212,7 +212,13 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
value: template.templateName,
description: template.description,
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE,
onSelected: () => beginExportWithTemplate(template.templateName, template.type, transactionIDs, template.policyID),
onSelected: () =>
beginExportWithTemplate(
template.templateName,
template.type,
transactionIDs,
template.policyID ?? (doesExportTemplateRequireWorkspacePolicy(template) ? moneyRequestReport?.policyID : undefined),
),
};
}

Expand Down
37 changes: 32 additions & 5 deletions src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransacti
import {deleteAppReport, exportReportToPDF, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
import {
approveMoneyRequestOnSearch,
doesExportTemplateRequireWorkspacePolicy,
exportSearchItemsToCSV,
exportToIntegrationOnSearch,
getExportTemplates,
Expand Down Expand Up @@ -572,7 +573,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionsKeys);

const beginExportWithTemplate = useCallback(
(templateName: string, templateType: string, policyID: string | undefined) => {
(templateName: string, templateType: string, policyID: string | undefined, requiresWorkspacePolicy: boolean) => {
const emptyReports =
selectedReports?.filter((selectedReport) => {
if (!selectedReport) {
Expand All @@ -593,6 +594,22 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
return;
}
const serializedQuery = queryJSON ? serializeQueryJSONForBackend(queryJSON) : JSON.stringify(queryJSON);

// Only resolve a workspace policyID for templates that reference GL codes. Account-level templates with plain
// {expense:category}/{expense:tag} columns must keep policyID undefined, otherwise the backend can't generate the
// file (see #93088). When resolving, only fall back to the selection's workspace when every selected transaction
// belongs to a single real workspace, so GL codes aren't resolved against the wrong workspace.
const resolveFallbackPolicyID = () => {
if (!requiresWorkspacePolicy) {
return undefined;
}
if (areAllMatchingItemsSelected) {
return queryJSON?.policyID?.length === 1 ? queryJSON.policyID.at(0) : undefined;
}
const allSelectedHavePolicy = Object.values(selectedTransactions).every((transaction) => !!transaction.policyID);
return allSelectedHavePolicy && selectedPolicyIDs.length === 1 ? selectedPolicyIDs.at(0) : undefined;
};
const exportPolicyID = policyID ?? resolveFallbackPolicyID();
let exportID: string;

if (areAllMatchingItemsSelected) {
Expand All @@ -603,7 +620,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
jsonQuery: serializedQuery,
reportIDList: [],
transactionIDList: [],
policyID,
policyID: exportPolicyID,
},
true,
);
Expand All @@ -616,14 +633,24 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
jsonQuery: isGroupExport ? serializeQueryJSONForBackend(addSelectedGroupsFilter(queryJSON, selectedTransactions, currentSearchResults?.data)) : '{}',
reportIDList: isGroupExport ? [] : selectedTransactionReportIDs,
transactionIDList: isGroupExport ? [] : selectedTransactionsKeys,
policyID,
policyID: exportPolicyID,
},
true,
);
}
setActiveExportID(exportID);
},
[selectedReports, selectedTransactions, isOffline, areAllMatchingItemsSelected, currentSearchResults?.data, queryJSON, selectedTransactionReportIDs, selectedTransactionsKeys],
[
selectedReports,
selectedTransactions,
isOffline,
areAllMatchingItemsSelected,
currentSearchResults?.data,
queryJSON,
selectedTransactionReportIDs,
selectedTransactionsKeys,
selectedPolicyIDs,
],
);

const policyIDsWithVBBA = useMemo(() => {
Expand Down Expand Up @@ -1509,7 +1536,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
icon: isStandardTemplate ? expensifyIcons.Table : expensifyIcons.TablePencil,
description: template.description,
onSelected: () => {
beginExportWithTemplate(template.templateName, template.type, template.policyID);
beginExportWithTemplate(template.templateName, template.type, template.policyID, doesExportTemplateRequireWorkspacePolicy(template));
},
shouldCloseModalOnSelect: true,
shouldCallAfterModalHide: true,
Expand Down
11 changes: 11 additions & 0 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,16 @@ function getExportTemplates(
return [...exportTemplates, ...integrationsTemplates, ...accountInAppTemplates, ...policyInAppTemplates];
}

/**
* Account-level export templates have no policyID by design. Only templates that reference GL codes
* (e.g. {expense:category:glcode}) need a workspace policyID for the backend to resolve them. Plain
* {expense:category}/{expense:tag} templates must keep policyID undefined — sending a workspace policyID
* for them prevents the backend from generating the file for account-level templates (see #93088).
*/
function doesExportTemplateRequireWorkspacePolicy(template: ExportTemplate): boolean {
return (template.columns ?? []).some((column) => column.includes(CONST.EXPORT_TEMPLATE_GL_CODE_COLUMN_MARKER));
}

/**
* Updates the form values for the advanced filters search form.
*/
Expand Down Expand Up @@ -1698,6 +1708,7 @@ export {
handleBulkPayItemSelected,
isCurrencySupportWalletBulkPay,
getExportTemplates,
doesExportTemplateRequireWorkspacePolicy,
getReportType,
getTotalFormattedAmount,
setOptimisticDataForTransactionThreadPreview,
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/ExportTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type ExportTemplate = OnyxCommon.OnyxValueWithOfflineFeedback<{

/** Description of the template */
description: string;

/** Column formulas the template exports (e.g. {expense:category:glcode}); present for in-app/CSV layout templates */
columns?: string[];
}>;

export default ExportTemplate;
Loading