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 @@ -8487,6 +8487,8 @@ const CONST = {
ORIGINAL_CURRENCY: 'originalCurrency',
UNIQUE_ID: 'uniqueID',
EXTERNAL_ID: 'externalID',
MAX_AMOUNT_NO_RECEIPT: 'maxAmountNoReceipt',
MAX_AMOUNT_NO_ITEMIZED_RECEIPT: 'maxAmountNoItemizedReceipt',
},

IMPORT_SPREADSHEET: {
Expand Down
13 changes: 13 additions & 0 deletions src/components/ImportColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ function findColumnName(header: string, columnRoles?: ColumnRole[]): string {
attribute = CONST.CSV_IMPORT_COLUMNS.ENABLED;
break;

case 'receiptsrequired':
case 'requirereceiptsover':
case 'maxamountnoreceipt':
attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT;
break;

case 'itemisedreceiptrequirement':
case 'itemizedreceiptrequirement':
case 'requireitemizedreceiptsover':
case 'maxamountnoitemizedreceipt':
attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT;
break;

default:
break;
}
Expand Down
9 changes: 8 additions & 1 deletion src/libs/actions/Policy/Category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,12 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
const existing = policyCategories[name];
if (!existing) {
acc.added++;
} else if (existing.enabled !== category.enabled || (existing['GL Code'] ?? '') !== (category['GL Code'] ?? '')) {
} else if (
existing.enabled !== category.enabled ||
(existing['GL Code'] ?? '') !== (category['GL Code'] ?? '') ||
('maxAmountNoReceipt' in category && existing.maxAmountNoReceipt !== category.maxAmountNoReceipt) ||
('maxAmountNoItemizedReceipt' in category && existing.maxAmountNoItemizedReceipt !== category.maxAmountNoItemizedReceipt)
) {
acc.updated++;
}

Expand All @@ -952,6 +957,8 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
enabled: category.enabled,
// eslint-disable-next-line @typescript-eslint/naming-convention
'GL Code': String(category['GL Code']),
...('maxAmountNoReceipt' in category && {maxAmountNoReceipt: category.maxAmountNoReceipt}),
...('maxAmountNoItemizedReceipt' in category && {maxAmountNoItemizedReceipt: category.maxAmountNoItemizedReceipt}),
})),
),
};
Expand Down
58 changes: 55 additions & 3 deletions src/pages/workspace/categories/ImportedCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ import SCREENS from '@src/SCREENS';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';

/**
* Parses a CSV cell value for receipt requirement columns.
* Mirrors the OD import logic: "default" → null, "required"/"always_required" → 0,
* "not_required" → DISABLED_MAX_EXPENSE_VALUE, numeric string → number.
*/
function parseCsvReceiptValue(raw: string | undefined): number | null | undefined {
if (raw === undefined) {
return undefined;
}
const trimmed = raw.trim().toLowerCase();
if (!trimmed || trimmed === 'default') {
return null;
}
if (trimmed === 'required' || trimmed === 'always_required') {
return 0;
}
if (trimmed === 'not_required') {
return CONST.DISABLED_MAX_EXPENSE_VALUE;
}
const num = Number(trimmed);
if (Number.isFinite(num) && num >= 0) {
return num;
}
return undefined;
}

type ImportedCategoriesPageProps = {
route: RouteProp<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.DYNAMIC_CATEGORIES_IMPORTED | typeof SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORTED>;
};
Expand Down Expand Up @@ -52,7 +78,11 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
);

if (isControlPolicy(policy)) {
roles.push({text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE});
roles.push(
{text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE},
{text: translate('workspace.rules.categoryRules.requireReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT},
{text: translate('workspace.rules.categoryRules.requireItemizedReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT},
);
}

return roles;
Expand Down Expand Up @@ -99,17 +129,39 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
const categoriesNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME);
const categoriesGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE);
const categoriesEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED);
const categoriesMaxAmountNoReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT);
const categoriesMaxAmountNoItemizedReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT);
const categoriesNames = spreadsheet?.data[categoriesNamesColumn].map((name) => name);
const categoriesEnabled = categoriesEnabledColumn !== -1 ? spreadsheet?.data[categoriesEnabledColumn].map((enabled) => enabled) : [];
const categoriesGLCode = categoriesGLCodeColumn !== -1 ? spreadsheet?.data[categoriesGLCodeColumn].map((glCode) => glCode) : [];
const categoriesMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoReceiptColumn] : [];
const categoriesMaxAmountNoItemizedReceipt = categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoItemizedReceiptColumn] : [];
const categories = categoriesNames?.slice(containsHeader ? 1 : 0).map((name, index) => {
const categoryAlreadyExists = policyCategories?.[name];
const existingGLCodeOrDefault = categoryAlreadyExists?.['GL Code'] ?? '';
const dataIndex = containsHeader ? index + 1 : index;

const parsedMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoReceipt?.[dataIndex]?.toString()) : undefined;
const parsedMaxAmountNoItemizedReceipt =
categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoItemizedReceipt?.[dataIndex]?.toString()) : undefined;

// Apply normalization: if itemized receipts required but receipts not required, force both to required
let normalizedMaxAmountNoReceipt = parsedMaxAmountNoReceipt;
let normalizedMaxAmountNoItemizedReceipt = parsedMaxAmountNoItemizedReceipt;
if (normalizedMaxAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE && normalizedMaxAmountNoItemizedReceipt !== undefined) {
normalizedMaxAmountNoItemizedReceipt = CONST.DISABLED_MAX_EXPENSE_VALUE;
}
if (normalizedMaxAmountNoItemizedReceipt === 0 && normalizedMaxAmountNoReceipt !== undefined) {
normalizedMaxAmountNoReceipt = 0;
Comment on lines +154 to +155
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize paired receipt rules when only one CSV column is mapped

The new normalization logic only runs when both parsed values are defined, so imports that map just one of the new columns can leave category rules in a state that diverges from the manual UI behavior. For example, mapping only maxAmountNoItemizedReceipt to always_required (0) will not force maxAmountNoReceipt to 0 because normalizedMaxAmountNoReceipt is undefined; similarly, mapping only maxAmountNoReceipt to not_required will not force itemized to not_required. This means the import path does not consistently apply the intended coupled-rule normalization for existing categories.

Useful? React with 👍 / 👎.

}

return {
name,
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[containsHeader ? index + 1 : index]?.toString().trim().toLowerCase() ?? '') : true,
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[dataIndex]?.toString().trim().toLowerCase() ?? '') : true,
// eslint-disable-next-line @typescript-eslint/naming-convention
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[containsHeader ? index + 1 : index] ?? '') : existingGLCodeOrDefault,
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[dataIndex] ?? '') : existingGLCodeOrDefault,
...(normalizedMaxAmountNoReceipt !== undefined && {maxAmountNoReceipt: normalizedMaxAmountNoReceipt}),
...(normalizedMaxAmountNoItemizedReceipt !== undefined && {maxAmountNoItemizedReceipt: normalizedMaxAmountNoItemizedReceipt}),
};
});

Expand Down
Loading