diff --git a/src/components/TransactionEditor.vue b/src/components/TransactionEditor.vue
index ac421fa..3279a1a 100644
--- a/src/components/TransactionEditor.vue
+++ b/src/components/TransactionEditor.vue
@@ -111,7 +111,7 @@
import { ref } from 'vue';
- import { Transaction, db, deserialiseAmount } from '../db.ts';
+ import { DT_FORMAT, Transaction, db, deserialiseAmount } from '../db.ts';
interface EditingPosting {
id: number | null,
@@ -148,7 +148,7 @@
// Prepare transaction for save
const newTransaction = new Transaction(
transaction.id,
- dayjs(transaction.dt).format('YYYY-MM-DD HH:mm:ss.SSS000'),
+ dayjs(transaction.dt).format(DT_FORMAT),
transaction.description,
[]
);
diff --git a/src/db.ts b/src/db.ts
index e40419a..a426f0e 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -25,6 +25,8 @@ import { reactive } from 'vue';
import { asCost, Balance } from './amounts.ts';
import { ExtendedDatabase } from './dbutil.ts';
+export const DT_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS000';
+
export const db = reactive({
filename: null as (string | null),
@@ -243,3 +245,13 @@ export interface JoinedTransactionPosting {
commodity: string,
running_balance?: number
}
+
+export interface StatementLine {
+ id: number | null,
+ source_account: string,
+ dt: string,
+ description: string,
+ quantity: number,
+ balance: number | null,
+ commodity: string
+}
diff --git a/src/importers/ofx2.ts b/src/importers/ofx2.ts
new file mode 100644
index 0000000..fca3926
--- /dev/null
+++ b/src/importers/ofx2.ts
@@ -0,0 +1,60 @@
+/*
+ DrCr: Web-based double-entry bookkeeping framework
+ Copyright (C) 2022–2024 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 .
+*/
+
+import dayjs from 'dayjs';
+
+import { DT_FORMAT, StatementLine, db } from '../db.ts';
+
+export default function import_ofx2(sourceAccount: string, content: string): StatementLine[] {
+ // Import an OFX2 XML file
+
+ // Convert OFX header to XML and parse
+ const xml_header = '';
+ const raw_payload = content.substring(content.indexOf('?>') + 2).replaceAll('&', '&');
+ const tree = new DOMParser().parseFromString(xml_header + raw_payload, 'application/xml');
+
+ // Read transactions
+ const statementLines: StatementLine[] = [];
+
+ for (const transaction of tree.querySelectorAll('BANKMSGSRSV1 STMTTRNRS STMTRS BANKTRANLIST STMTTRN')) {
+ const dateRaw = transaction.querySelector('DTPOSTED')!.textContent;
+ const date = dayjs(dateRaw, 'YYYYMMDDHHmmss').hour(0).minute(0).second(0).millisecond(0).format(DT_FORMAT);
+ const description = transaction.querySelector('NAME')!.textContent;
+ const amount = transaction.querySelector('TRNAMT')!.textContent;
+
+ if (amount === '0') {
+ // Continuation line
+ statementLines.at(-1)!.description += '\n' + description;
+ } else {
+ 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;
+}
diff --git a/src/main.ts b/src/main.ts
index 09d2319..fed8c5b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -35,6 +35,7 @@ async function initApp() {
{ path: '/journal/edit-transaction/:id', name: 'journal-edit-transaction', component: () => import('./pages/EditTransactionView.vue') },
{ path: '/journal/new-transaction', name: 'journal-new-transaction', component: () => import('./pages/NewTransactionView.vue') },
{ path: '/statement-lines', name: 'statement-lines', component: () => import('./pages/StatementLinesView.vue') },
+ { path: '/statement-lines/import', name: 'import-statement', component: () => import('./pages/ImportStatementView.vue') },
{ path: '/transactions/:account', name: 'transactions', component: () => import('./pages/TransactionsView.vue') },
{ path: '/trial-balance', name: 'trial-balance', component: () => import('./pages/TrialBalanceView.vue') },
];
diff --git a/src/pages/ImportStatementView.vue b/src/pages/ImportStatementView.vue
new file mode 100644
index 0000000..a71835e
--- /dev/null
+++ b/src/pages/ImportStatementView.vue
@@ -0,0 +1,138 @@
+
+
+
+
+ Import statement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import statement preview
+
+
+
+
+
+ Date |
+ Description |
+ Dr |
+ Cr |
+ Balance |
+
+
+
+
+ {{ dayjs(line.dt).format('YYYY-MM-DD') }} |
+ {{ line.description }} |
+ {{ line.quantity >= 0 ? ppWithCommodity(line.quantity, line.commodity) : '' }} |
+ {{ line.quantity < 0 ? ppWithCommodity(-line.quantity, line.commodity) : '' }} |
+ {{ line.balance ?? '' }} |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/StatementLinesView.vue b/src/pages/StatementLinesView.vue
index 0e852d9..e36b1af 100644
--- a/src/pages/StatementLinesView.vue
+++ b/src/pages/StatementLinesView.vue
@@ -25,11 +25,11 @@
+
Import statement
-
-
+
+