Skip to content

Commit 69cb8b6

Browse files
authored
Merge pull request #88962 from Expensify/claude-addReceiptColumnsToSpreadsheetImport
2 parents de90b2c + 0023995 commit 69cb8b6

4 files changed

Lines changed: 70 additions & 6 deletions

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8585,6 +8585,8 @@ const CONST = {
85858585
ORIGINAL_CURRENCY: 'originalCurrency',
85868586
UNIQUE_ID: 'uniqueID',
85878587
EXTERNAL_ID: 'externalID',
8588+
MAX_AMOUNT_NO_RECEIPT: 'maxAmountNoReceipt',
8589+
MAX_AMOUNT_NO_ITEMIZED_RECEIPT: 'maxAmountNoItemizedReceipt',
85888590
},
85898591

85908592
IMPORT_SPREADSHEET: {

src/components/ImportColumn.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {Str} from 'expensify-common';
21
import React, {useEffect, useRef} from 'react';
32
import {View} from 'react-native';
43
import useLocalize from '@hooks/useLocalize';
@@ -14,7 +13,7 @@ import Text from './Text';
1413
// cspell:disable
1514
function findColumnName(header: string, columnRoles?: ColumnRole[]): string {
1615
let attribute = '';
17-
const formattedHeader = Str.removeSpaces(String(header).toLowerCase().trim());
16+
const formattedHeader = String(header).toLowerCase().trim().replaceAll(' ', '');
1817
switch (formattedHeader) {
1918
case 'email':
2019
case 'emailaddress':
@@ -142,6 +141,19 @@ function findColumnName(header: string, columnRoles?: ColumnRole[]): string {
142141
attribute = CONST.CSV_IMPORT_COLUMNS.ENABLED;
143142
break;
144143

144+
case 'receiptsrequired':
145+
case 'requirereceiptsover':
146+
case 'maxamountnoreceipt':
147+
attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT;
148+
break;
149+
150+
case 'itemisedreceiptrequirement':
151+
case 'itemizedreceiptrequirement':
152+
case 'requireitemizedreceiptsover':
153+
case 'maxamountnoitemizedreceipt':
154+
attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT;
155+
break;
156+
145157
default:
146158
break;
147159
}

src/libs/actions/Policy/Category.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,12 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
936936
const existing = policyCategories[name];
937937
if (!existing) {
938938
acc.added++;
939-
} else if (existing.enabled !== category.enabled || (existing['GL Code'] ?? '') !== (category['GL Code'] ?? '')) {
939+
} else if (
940+
existing.enabled !== category.enabled ||
941+
(existing['GL Code'] ?? '') !== (category['GL Code'] ?? '') ||
942+
('maxAmountNoReceipt' in category && existing.maxAmountNoReceipt !== category.maxAmountNoReceipt) ||
943+
('maxAmountNoItemizedReceipt' in category && existing.maxAmountNoItemizedReceipt !== category.maxAmountNoItemizedReceipt)
944+
) {
940945
acc.updated++;
941946
}
942947

@@ -955,6 +960,8 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[],
955960
enabled: category.enabled,
956961
// eslint-disable-next-line @typescript-eslint/naming-convention
957962
'GL Code': String(category['GL Code']),
963+
...('maxAmountNoReceipt' in category && {maxAmountNoReceipt: category.maxAmountNoReceipt}),
964+
...('maxAmountNoItemizedReceipt' in category && {maxAmountNoItemizedReceipt: category.maxAmountNoItemizedReceipt}),
958965
})),
959966
),
960967
};

src/pages/workspace/categories/ImportedCategoriesPage.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ 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: "required"/"always_required" → 0,
29+
* "not_required" → DISABLED_MAX_EXPENSE_VALUE, numeric string → number.
30+
* Returns undefined for unmapped columns, empty/default values, or invalid input.
31+
*/
32+
function parseCsvReceiptValue(raw: string | undefined): number | undefined {
33+
if (raw === undefined) {
34+
return undefined;
35+
}
36+
const trimmed = raw.trim().toLowerCase();
37+
if (!trimmed || trimmed === 'default') {
38+
return undefined;
39+
}
40+
if (trimmed === 'required' || trimmed === 'always_required') {
41+
return 0;
42+
}
43+
if (trimmed === 'not_required') {
44+
return CONST.DISABLED_MAX_EXPENSE_VALUE;
45+
}
46+
const num = Number(trimmed);
47+
if (Number.isFinite(num) && num >= 0) {
48+
return num;
49+
}
50+
return undefined;
51+
}
52+
2653
type ImportedCategoriesPageProps = {
2754
route: RouteProp<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.DYNAMIC_CATEGORIES_IMPORTED | typeof SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_IMPORTED>;
2855
};
@@ -52,7 +79,11 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
5279
);
5380

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

5889
return roles;
@@ -99,17 +130,29 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
99130
const categoriesNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME);
100131
const categoriesGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE);
101132
const categoriesEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED);
133+
const categoriesMaxAmountNoReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT);
134+
const categoriesMaxAmountNoItemizedReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT);
102135
const categoriesNames = spreadsheet?.data[categoriesNamesColumn].map((name) => name);
103136
const categoriesEnabled = categoriesEnabledColumn !== -1 ? spreadsheet?.data[categoriesEnabledColumn].map((enabled) => enabled) : [];
104137
const categoriesGLCode = categoriesGLCodeColumn !== -1 ? spreadsheet?.data[categoriesGLCodeColumn].map((glCode) => glCode) : [];
138+
const categoriesMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoReceiptColumn] : [];
139+
const categoriesMaxAmountNoItemizedReceipt = categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoItemizedReceiptColumn] : [];
105140
const categories = categoriesNames?.slice(containsHeader ? 1 : 0).map((name, index) => {
106141
const categoryAlreadyExists = policyCategories?.[name];
107142
const existingGLCodeOrDefault = categoryAlreadyExists?.['GL Code'] ?? '';
143+
const dataIndex = containsHeader ? index + 1 : index;
144+
145+
const parsedMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoReceipt?.[dataIndex]?.toString()) : undefined;
146+
const parsedMaxAmountNoItemizedReceipt =
147+
categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoItemizedReceipt?.[dataIndex]?.toString()) : undefined;
148+
108149
return {
109150
name,
110-
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[containsHeader ? index + 1 : index]?.toString().trim().toLowerCase() ?? '') : true,
151+
enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[dataIndex]?.toString().trim().toLowerCase() ?? '') : true,
111152
// eslint-disable-next-line @typescript-eslint/naming-convention
112-
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[containsHeader ? index + 1 : index] ?? '') : existingGLCodeOrDefault,
153+
'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[dataIndex] ?? '') : existingGLCodeOrDefault,
154+
...(parsedMaxAmountNoReceipt !== undefined && {maxAmountNoReceipt: parsedMaxAmountNoReceipt}),
155+
...(parsedMaxAmountNoItemizedReceipt !== undefined && {maxAmountNoItemizedReceipt: parsedMaxAmountNoItemizedReceipt}),
113156
};
114157
});
115158

0 commit comments

Comments
 (0)