Implement CSV importer
This commit is contained in:
parent
156a89e2ba
commit
51cf3661b4
@ -17,6 +17,7 @@
|
|||||||
"@tauri-apps/plugin-sql": "~2",
|
"@tauri-apps/plugin-sql": "~2",
|
||||||
"@tauri-apps/plugin-store": "~2",
|
"@tauri-apps/plugin-store": "~2",
|
||||||
"clusterize.js": "^1.0.0",
|
"clusterize.js": "^1.0.0",
|
||||||
|
"csv-parse": "^5.6.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "4"
|
"vue-router": "4"
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ importers:
|
|||||||
clusterize.js:
|
clusterize.js:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
csv-parse:
|
||||||
|
specifier: ^5.6.0
|
||||||
|
version: 5.6.0
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.11.13
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
@ -606,6 +609,9 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
csv-parse@5.6.0:
|
||||||
|
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||||
|
|
||||||
dayjs@1.11.13:
|
dayjs@1.11.13:
|
||||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
@ -1474,6 +1480,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
csv-parse@5.6.0: {}
|
||||||
|
|
||||||
dayjs@1.11.13: {}
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
|
68
src/importers/csv.ts
Normal file
68
src/importers/csv.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
DrCr: Web-based double-entry bookkeeping framework
|
||||||
|
Copyright (C) 2022–2025 Lee Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse } from 'csv-parse/browser/esm/sync';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { DT_FORMAT, StatementLine, db } from '../db.ts';
|
||||||
|
|
||||||
|
export default function importCsv(sourceAccount: string, content: string): StatementLine[] {
|
||||||
|
const records = parse(content, {
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate column layout
|
||||||
|
if (records.length === 0) {
|
||||||
|
throw new Error('Empty CSV file');
|
||||||
|
}
|
||||||
|
if (records[0][0] !== 'Date') {
|
||||||
|
throw new Error('Unexpected column 1, expected "Date"');
|
||||||
|
}
|
||||||
|
if (records[0][1] !== 'Description') {
|
||||||
|
throw new Error('Unexpected column 1, expected "Description"');
|
||||||
|
}
|
||||||
|
if (records[0][2] !== 'Amount') {
|
||||||
|
throw new Error('Unexpected column 1, expected "Amount"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statementLines: StatementLine[] = [];
|
||||||
|
|
||||||
|
// Parse records
|
||||||
|
for (let i = 1; i < records.length; i++) {
|
||||||
|
const record = records[i];
|
||||||
|
|
||||||
|
const date = dayjs(record[0], 'YYYY-MM-DD').format(DT_FORMAT);
|
||||||
|
const description = record[1];
|
||||||
|
const amount = record[2];
|
||||||
|
|
||||||
|
const quantity = Math.round(parseFloat(amount) * Math.pow(10, db.metadata.dps));
|
||||||
|
if (!Number.isSafeInteger(quantity)) { throw new Error('Quantity not representable by safe integer'); }
|
||||||
|
|
||||||
|
statementLines.push({
|
||||||
|
id: null,
|
||||||
|
source_account: sourceAccount,
|
||||||
|
dt: date,
|
||||||
|
description: description,
|
||||||
|
quantity: quantity,
|
||||||
|
balance: null,
|
||||||
|
commodity: db.metadata.reporting_commodity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return statementLines;
|
||||||
|
}
|
@ -26,6 +26,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<select class="bordered-field" id="format" v-model="format">
|
<select class="bordered-field" id="format" v-model="format">
|
||||||
<option value="ofx">OFX (1.x/2.x)</option>
|
<option value="ofx">OFX (1.x/2.x)</option>
|
||||||
|
<option value="csv">CSV</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="account" class="block text-gray-900 pr-4">Source account</label>
|
<label for="account" class="block text-gray-900 pr-4">Source account</label>
|
||||||
@ -85,6 +86,7 @@
|
|||||||
import ComboBoxAccounts from '../components/ComboBoxAccounts.vue';
|
import ComboBoxAccounts from '../components/ComboBoxAccounts.vue';
|
||||||
import { ppWithCommodity } from '../display.ts';
|
import { ppWithCommodity } from '../display.ts';
|
||||||
|
|
||||||
|
import importCsv from '../importers/csv.ts';
|
||||||
import importOfxAutodetectVersion from '../importers/ofx.ts';
|
import importOfxAutodetectVersion from '../importers/ofx.ts';
|
||||||
|
|
||||||
const fileInput = useTemplateRef('file');
|
const fileInput = useTemplateRef('file');
|
||||||
@ -113,7 +115,9 @@
|
|||||||
|
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
|
|
||||||
if (format.value === 'ofx') {
|
if (format.value === 'csv') {
|
||||||
|
statementLines.value = importCsv(sourceAccount.value, content);
|
||||||
|
} else if (format.value === 'ofx') {
|
||||||
statementLines.value = importOfxAutodetectVersion(sourceAccount.value, content);
|
statementLines.value = importOfxAutodetectVersion(sourceAccount.value, content);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected import format');
|
throw new Error('Unexpected import format');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user