Skip to content
Snippets Groups Projects
Unverified Commit 9ac77af0 authored by Stefan Wilkes's avatar Stefan Wilkes Committed by GitHub
Browse files

Added configuration to CSV importer that allows to skip lines (#3234)


* Added configuration to CSV importer that allows to skip lines

* Fixed several type / parser check when import is executed with a different CSV structure on accounts with transactions

* Fixed linter warning

* Reverted changes on sync.ts as initial error is not occuring anymore.
This will also fix the tests again

---------

Co-authored-by: default avataryoungcw <calebyoung94@gmail.com>
parent 3e07d18a
No related branches found
No related tags found
No related merge requests found
...@@ -186,20 +186,14 @@ function getInitialMappings(transactions) { ...@@ -186,20 +186,14 @@ function getInitialMappings(transactions) {
return entry ? entry[0] : null; return entry ? entry[0] : null;
} }
function isString(value) {
return typeof value === 'string' || value instanceof String;
}
const dateField = key( const dateField = key(
fields.find(([name]) => name.toLowerCase().includes('date')) || fields.find(([name]) => name.toLowerCase().includes('date')) ||
fields.find( fields.find(([, value]) => String(value)?.match(/^\d+[-/]\d+[-/]\d+$/)),
([, value]) => isString(value) && value.match(/^\d+[-/]\d+[-/]\d+$/),
),
); );
const amountField = key( const amountField = key(
fields.find(([name]) => name.toLowerCase().includes('amount')) || fields.find(([name]) => name.toLowerCase().includes('amount')) ||
fields.find(([, value]) => isString(value) && value.match(/^-?[.,\d]+$/)), fields.find(([, value]) => String(value)?.match(/^-?[.,\d]+$/)),
); );
const categoryField = key( const categoryField = key(
...@@ -880,6 +874,9 @@ export function ImportTransactions({ options }) { ...@@ -880,6 +874,9 @@ export function ImportTransactions({ options }) {
prefs[`csv-delimiter-${accountId}`] || prefs[`csv-delimiter-${accountId}`] ||
(filename.endsWith('.tsv') ? '\t' : ','), (filename.endsWith('.tsv') ? '\t' : ','),
); );
const [skipLines, setSkipLines] = useState(
prefs[`csv-skip-lines-${accountId}`] ?? 0,
);
const [hasHeaderRow, setHasHeaderRow] = useState( const [hasHeaderRow, setHasHeaderRow] = useState(
prefs[`csv-has-header-${accountId}`] ?? true, prefs[`csv-has-header-${accountId}`] ?? true,
); );
...@@ -988,6 +985,7 @@ export function ImportTransactions({ options }) { ...@@ -988,6 +985,7 @@ export function ImportTransactions({ options }) {
const parseOptions = getParseOptions(fileType, { const parseOptions = getParseOptions(fileType, {
delimiter, delimiter,
hasHeaderRow, hasHeaderRow,
skipLines,
fallbackMissingPayeeToMemo, fallbackMissingPayeeToMemo,
}); });
...@@ -1040,6 +1038,7 @@ export function ImportTransactions({ options }) { ...@@ -1040,6 +1038,7 @@ export function ImportTransactions({ options }) {
const parseOptions = getParseOptions(fileType, { const parseOptions = getParseOptions(fileType, {
delimiter, delimiter,
hasHeaderRow, hasHeaderRow,
skipLines,
fallbackMissingPayeeToMemo, fallbackMissingPayeeToMemo,
}); });
...@@ -1196,6 +1195,8 @@ export function ImportTransactions({ options }) { ...@@ -1196,6 +1195,8 @@ export function ImportTransactions({ options }) {
[`csv-mappings-${accountId}`]: JSON.stringify(fieldMappings), [`csv-mappings-${accountId}`]: JSON.stringify(fieldMappings),
}); });
savePrefs({ [`csv-delimiter-${accountId}`]: delimiter }); savePrefs({ [`csv-delimiter-${accountId}`]: delimiter });
savePrefs({ [`csv-has-header-${accountId}`]: hasHeaderRow });
savePrefs({ [`csv-skip-lines-${accountId}`]: skipLines });
} }
if (filetype === 'csv' || filetype === 'qif') { if (filetype === 'csv' || filetype === 'qif') {
...@@ -1282,6 +1283,10 @@ export function ImportTransactions({ options }) { ...@@ -1282,6 +1283,10 @@ export function ImportTransactions({ options }) {
); );
break; break;
} }
if (trans.payee == null || !(trans.payee instanceof String)) {
console.log(`Unable·to·parse·payee·${trans.payee || '(empty)'}`);
break;
}
const { amount } = parseAmountFields( const { amount } = parseAmountFields(
trans, trans,
...@@ -1575,6 +1580,34 @@ export function ImportTransactions({ options }) { ...@@ -1575,6 +1580,34 @@ export function ImportTransactions({ options }) {
getParseOptions('csv', { getParseOptions('csv', {
delimiter: value, delimiter: value,
hasHeaderRow, hasHeaderRow,
skipLines,
}),
);
}}
style={{ width: 50 }}
/>
</label>
<label
style={{
display: 'flex',
flexDirection: 'row',
gap: 5,
alignItems: 'baseline',
}}
>
Skip lines:
<Input
type="number"
value={skipLines}
min="0"
onChangeValue={value => {
setSkipLines(+value);
parse(
filename,
getParseOptions('csv', {
delimiter,
hasHeaderRow,
skipLines: +value,
}), }),
); );
}} }}
...@@ -1591,6 +1624,7 @@ export function ImportTransactions({ options }) { ...@@ -1591,6 +1624,7 @@ export function ImportTransactions({ options }) {
getParseOptions('csv', { getParseOptions('csv', {
delimiter, delimiter,
hasHeaderRow: !hasHeaderRow, hasHeaderRow: !hasHeaderRow,
skipLines,
}), }),
); );
}} }}
...@@ -1711,8 +1745,8 @@ export function ImportTransactions({ options }) { ...@@ -1711,8 +1745,8 @@ export function ImportTransactions({ options }) {
function getParseOptions(fileType, options = {}) { function getParseOptions(fileType, options = {}) {
if (fileType === 'csv') { if (fileType === 'csv') {
const { delimiter, hasHeaderRow } = options; const { delimiter, hasHeaderRow, skipLines } = options;
return { delimiter, hasHeaderRow }; return { delimiter, hasHeaderRow, skipLines };
} else if (isOfxFile(fileType)) { } else if (isOfxFile(fileType)) {
const { fallbackMissingPayeeToMemo } = options; const { fallbackMissingPayeeToMemo } = options;
return { fallbackMissingPayeeToMemo }; return { fallbackMissingPayeeToMemo };
......
...@@ -18,6 +18,7 @@ type ParseFileOptions = { ...@@ -18,6 +18,7 @@ type ParseFileOptions = {
hasHeaderRow?: boolean; hasHeaderRow?: boolean;
delimiter?: string; delimiter?: string;
fallbackMissingPayeeToMemo?: boolean; fallbackMissingPayeeToMemo?: boolean;
skipLines?: number;
}; };
export async function parseFile( export async function parseFile(
...@@ -57,7 +58,12 @@ async function parseCSV( ...@@ -57,7 +58,12 @@ async function parseCSV(
options: ParseFileOptions, options: ParseFileOptions,
): Promise<ParseFileResult> { ): Promise<ParseFileResult> {
const errors = Array<ParseError>(); const errors = Array<ParseError>();
const contents = await fs.readFile(filepath); let contents = await fs.readFile(filepath);
if (options.skipLines > 0) {
const lines = contents.split(/\r?\n/);
contents = lines.slice(options.skipLines).join('\r\n');
}
let data; let data;
try { try {
......
...@@ -30,6 +30,7 @@ export type SyncedPrefs = Partial< ...@@ -30,6 +30,7 @@ export type SyncedPrefs = Partial<
[key: `parse-date-${string}-${'csv' | 'qif'}`]: string; [key: `parse-date-${string}-${'csv' | 'qif'}`]: string;
[key: `csv-mappings-${string}`]: string; [key: `csv-mappings-${string}`]: string;
[key: `csv-delimiter-${string}`]: ',' | ';' | '\t'; [key: `csv-delimiter-${string}`]: ',' | ';' | '\t';
[key: `csv-skip-lines-${string}`]: number;
[key: `csv-has-header-${string}`]: boolean; [key: `csv-has-header-${string}`]: boolean;
[key: `ofx-fallback-missing-payee-${string}`]: boolean; [key: `ofx-fallback-missing-payee-${string}`]: boolean;
[key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean; [key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean;
......
---
category: Features
authors: [Horizon0156]
---
Added an optional configuration value to skip one or more heading lines (added by some banks, like ING) during the CSV transactions import.
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment