From 9ac77af077fa3fed8b7e1a097dda955745f474f4 Mon Sep 17 00:00:00 2001 From: Stefan Wilkes <Horizon0156@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:16:33 +0200 Subject: [PATCH] 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: youngcw <calebyoung94@gmail.com> --- .../components/modals/ImportTransactions.jsx | 54 +++++++++++++++---- .../src/server/accounts/parse-file.ts | 8 ++- packages/loot-core/src/types/prefs.d.ts | 1 + upcoming-release-notes/3234.md | 6 +++ 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 upcoming-release-notes/3234.md diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx index 430779264..88ffb4788 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx @@ -186,20 +186,14 @@ function getInitialMappings(transactions) { return entry ? entry[0] : null; } - function isString(value) { - return typeof value === 'string' || value instanceof String; - } - const dateField = key( fields.find(([name]) => name.toLowerCase().includes('date')) || - fields.find( - ([, value]) => isString(value) && value.match(/^\d+[-/]\d+[-/]\d+$/), - ), + fields.find(([, value]) => String(value)?.match(/^\d+[-/]\d+[-/]\d+$/)), ); const amountField = key( 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( @@ -880,6 +874,9 @@ export function ImportTransactions({ options }) { prefs[`csv-delimiter-${accountId}`] || (filename.endsWith('.tsv') ? '\t' : ','), ); + const [skipLines, setSkipLines] = useState( + prefs[`csv-skip-lines-${accountId}`] ?? 0, + ); const [hasHeaderRow, setHasHeaderRow] = useState( prefs[`csv-has-header-${accountId}`] ?? true, ); @@ -988,6 +985,7 @@ export function ImportTransactions({ options }) { const parseOptions = getParseOptions(fileType, { delimiter, hasHeaderRow, + skipLines, fallbackMissingPayeeToMemo, }); @@ -1040,6 +1038,7 @@ export function ImportTransactions({ options }) { const parseOptions = getParseOptions(fileType, { delimiter, hasHeaderRow, + skipLines, fallbackMissingPayeeToMemo, }); @@ -1196,6 +1195,8 @@ export function ImportTransactions({ options }) { [`csv-mappings-${accountId}`]: JSON.stringify(fieldMappings), }); savePrefs({ [`csv-delimiter-${accountId}`]: delimiter }); + savePrefs({ [`csv-has-header-${accountId}`]: hasHeaderRow }); + savePrefs({ [`csv-skip-lines-${accountId}`]: skipLines }); } if (filetype === 'csv' || filetype === 'qif') { @@ -1282,6 +1283,10 @@ export function ImportTransactions({ options }) { ); break; } + if (trans.payee == null || !(trans.payee instanceof String)) { + console.log(`Unable·to·parse·payee·${trans.payee || '(empty)'}`); + break; + } const { amount } = parseAmountFields( trans, @@ -1575,6 +1580,34 @@ export function ImportTransactions({ options }) { getParseOptions('csv', { delimiter: value, 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 }) { getParseOptions('csv', { delimiter, hasHeaderRow: !hasHeaderRow, + skipLines, }), ); }} @@ -1711,8 +1745,8 @@ export function ImportTransactions({ options }) { function getParseOptions(fileType, options = {}) { if (fileType === 'csv') { - const { delimiter, hasHeaderRow } = options; - return { delimiter, hasHeaderRow }; + const { delimiter, hasHeaderRow, skipLines } = options; + return { delimiter, hasHeaderRow, skipLines }; } else if (isOfxFile(fileType)) { const { fallbackMissingPayeeToMemo } = options; return { fallbackMissingPayeeToMemo }; diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/accounts/parse-file.ts index 0dd086086..fd6178411 100644 --- a/packages/loot-core/src/server/accounts/parse-file.ts +++ b/packages/loot-core/src/server/accounts/parse-file.ts @@ -18,6 +18,7 @@ type ParseFileOptions = { hasHeaderRow?: boolean; delimiter?: string; fallbackMissingPayeeToMemo?: boolean; + skipLines?: number; }; export async function parseFile( @@ -57,7 +58,12 @@ async function parseCSV( options: ParseFileOptions, ): Promise<ParseFileResult> { 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; try { diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 3d3664a87..a348eca04 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -30,6 +30,7 @@ export type SyncedPrefs = Partial< [key: `parse-date-${string}-${'csv' | 'qif'}`]: string; [key: `csv-mappings-${string}`]: string; [key: `csv-delimiter-${string}`]: ',' | ';' | '\t'; + [key: `csv-skip-lines-${string}`]: number; [key: `csv-has-header-${string}`]: boolean; [key: `ofx-fallback-missing-payee-${string}`]: boolean; [key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean; diff --git a/upcoming-release-notes/3234.md b/upcoming-release-notes/3234.md new file mode 100644 index 000000000..12dafb981 --- /dev/null +++ b/upcoming-release-notes/3234.md @@ -0,0 +1,6 @@ +--- +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 -- GitLab