Skip to content

Commit d101b53

Browse files
MelvinBotdangrous
andcommitted
Add receipt requirement columns to category spreadsheet import
Support "Require receipts over" and "Require itemized receipts over" columns in the category CSV import flow for Control policies. Values are parsed using the same logic as OD: default, required/always_required, not_required, or a numeric threshold. Normalization ensures consistent states (e.g. receipts not_required forces itemized receipts to not_required). Co-authored-by: Daniel Gale-Rosen <dangrous@users.noreply.github.com>
1 parent 96365e1 commit d101b53

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8482,6 +8482,8 @@ const CONST = {
84828482
ORIGINAL_CURRENCY: 'originalCurrency',
84838483
UNIQUE_ID: 'uniqueID',
84848484
EXTERNAL_ID: 'externalID',
8485+
MAX_AMOUNT_NO_RECEIPT: 'maxAmountNoReceipt',
8486+
MAX_AMOUNT_NO_ITEMIZED_RECEIPT: 'maxAmountNoItemizedReceipt',
84858487
},
84868488

84878489
IMPORT_SPREADSHEET: {

src/libs/actions/Policy/Category.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,12 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
933933
const existing = policyCategories[name];
934934
if (!existing) {
935935
acc.added++;
936-
} else if (existing.enabled !== category.enabled || (existing['GL Code'] ?? '') !== (category['GL Code'] ?? '')) {
936+
} else if (
937+
existing.enabled !== category.enabled ||
938+
(existing['GL Code'] ?? '') !== (category['GL Code'] ?? '') ||
939+
('maxAmountNoReceipt' in category && existing.maxAmountNoReceipt !== category.maxAmountNoReceipt) ||
940+
('maxAmountNoItemizedReceipt' in category && existing.maxAmountNoItemizedReceipt !== category.maxAmountNoItemizedReceipt)
941+
) {
937942
acc.updated++;
938943
}
939944

@@ -952,6 +957,8 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
952957
enabled: category.enabled,
953958
// eslint-disable-next-line @typescript-eslint/naming-convention
954959
'GL Code': String(category['GL Code']),
960+
...('maxAmountNoReceipt' in category && {maxAmountNoReceipt: category.maxAmountNoReceipt}),
961+
...('maxAmountNoItemizedReceipt' in category && {maxAmountNoItemizedReceipt: category.maxAmountNoItemizedReceipt}),
955962
})),
956963
),
957964
};

src/pages/workspace/categories/ImportedCategoriesPage.tsx

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ import SCREENS from '@src/SCREENS';
2323
import type {Errors} from '@src/types/onyx/OnyxCommon';
2424
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
2525

26+
/**
27+
* Parses a CSV cell value for receipt requirement columns.
28+
* Mirrors the OD import logic: "default" → null, "required"/"always_required" → 0,
29+
* "not_required" → DISABLED_MAX_EXPENSE_VALUE, numeric string → number.
30+
*/
31+
function parseCsvReceiptValue(raw: string | undefined): number | null | undefined {
32+
if (raw === undefined) {
33+
return undefined;
34+
}
35+
const trimmed = raw.trim().toLowerCase();
36+
if (!trimmed || trimmed === 'default') {
37+
return null;
38+
}
39+
if (trimmed === 'required' || trimmed === 'always_required') {
40+
return 0;
41+
}
42+
if (trimmed === 'not_required') {
43+
return CONST.DISABLED_MAX_EXPENSE_VALUE;
44+
}
45+
const num = Number(trimmed);
46+
if (Number.isFinite(num) && num >= 0) {
47+
return num;
48+
}
49+
return undefined;
50+
}
51+
2652
type ImportedCategoriesPageProps = {
2753
route: RouteProp<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.DYNAMIC_CATEGORIES_IMPORTED | typeof SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORTED>;
2854
};
@@ -52,7 +78,11 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
5278
);
5379

5480
if (isControlPolicy(policy)) {
55-
roles.push({text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE});
81+
roles.push(
82+
{text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE},
83+
{text: translate('workspace.rules.categoryRules.requireReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT},
84+
{text: translate('workspace.rules.categoryRules.requireItemizedReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT},
85+
);
5686
}
5787

5888
return roles;
@@ -99,17 +129,39 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
99129
const categoriesNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME);
100130
const categoriesGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE);
101131
const categoriesEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED);
132+
const categoriesMaxAmountNoReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT);
133+
const categoriesMaxAmountNoItemizedReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT);
102134
const categoriesNames = spreadsheet?.data[categoriesNamesColumn].map((name) => name);
103135
const categoriesEnabled = categoriesEnabledColumn !== -1 ? spreadsheet?.data[categoriesEnabledColumn].map((enabled) => enabled) : [];
104136
const categoriesGLCode = categoriesGLCodeColumn !== -1 ? spreadsheet?.data[categoriesGLCodeColumn].map((glCode) => glCode) : [];
137+
const categoriesMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoReceiptColumn] : [];
138+
const categoriesMaxAmountNoItemizedReceipt = categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoItemizedReceiptColumn] : [];
105139
const categories = categoriesNames?.slice(containsHeader ? 1 : 0).map((name, index) => {
106140
const categoryAlreadyExists = policyCategories?.[name];
107141
const existingGLCodeOrDefault = categoryAlreadyExists?.['GL Code'] ?? '';
142+
const dataIndex = containsHeader ? index + 1 : index;
143+
144+
const parsedMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoReceipt?.[dataIndex]?.toString()) : undefined;
145+
const parsedMaxAmountNoItemizedReceipt =
146+
categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoItemizedReceipt?.[dataIndex]?.toString()) : undefined;
147+
148+
// Apply normalization: if itemized receipts required but receipts not required, force both to required
149+
let normalizedMaxAmountNoReceipt = parsedMaxAmountNoReceipt;
150+
let normalizedMaxAmountNoItemizedReceipt = parsedMaxAmountNoItemizedReceipt;
151+
if (normalizedMaxAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE && normalizedMaxAmountNoItemizedReceipt !== undefined) {
152+
normalizedMaxAmountNoItemizedReceipt = CONST.DISABLED_MAX_EXPENSE_VALUE;
153+
}
154+
if (normalizedMaxAmountNoItemizedReceipt === 0 && normalizedMaxAmountNoReceipt !== undefined) {
155+
normalizedMaxAmountNoReceipt = 0;
156+
}
157+
108158
return {
109159
name,
110-
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[containsHeader ? index + 1 : index]?.toString().toLowerCase() ?? '') : true,
160+
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[dataIndex]?.toString().toLowerCase() ?? '') : true,
111161
// eslint-disable-next-line @typescript-eslint/naming-convention
112-
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[containsHeader ? index + 1 : index] ?? '') : existingGLCodeOrDefault,
162+
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[dataIndex] ?? '') : existingGLCodeOrDefault,
163+
...(normalizedMaxAmountNoReceipt !== undefined && {maxAmountNoReceipt: normalizedMaxAmountNoReceipt}),
164+
...(normalizedMaxAmountNoItemizedReceipt !== undefined && {maxAmountNoItemizedReceipt: normalizedMaxAmountNoItemizedReceipt}),
113165
};
114166
});
115167

0 commit comments

Comments
 (0)