Compare commits

...

5 Commits

8 changed files with 86 additions and 24 deletions

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -108,13 +108,14 @@
import { ref } from 'vue';
import { DT_FORMAT, Transaction, db, deserialiseAmount } from '../db.ts';
import { DT_FORMAT, Posting, Transaction, db, deserialiseAmount } from '../db.ts';
import ComboBoxAccounts from './ComboBoxAccounts.vue';
interface EditingPosting {
id: number | null,
description: string | null,
account: string,
originalAccount: string | null,
sign: string, // Keep track of Dr/Cr status so this can be independently changed in the UI
amount_abs: string,
}
@ -135,6 +136,7 @@
id: null,
description: null,
account: '',
originalAccount: null,
sign: posting.sign, // Create the new posting with the same sign as the entry clicked on
amount_abs: ''
});
@ -158,9 +160,10 @@
id: posting.id,
description: posting.description,
account: posting.account,
originalAccount: posting.originalAccount,
quantity: posting.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity,
commodity: amount_abs.commodity
});
} as Posting);
}
// Validate transaction
@ -278,6 +281,23 @@
WHERE postings.id = p.id`,
[newTransaction.dt, posting.account]
);
// Must also invalidate running balance of original account, if the account has changed
const originalAccount = (posting as unknown as EditingPosting).originalAccount;
if (originalAccount && originalAccount !== posting.account) {
await dbTransaction.execute(
`UPDATE postings
SET running_balance = NULL
FROM (
SELECT postings.id
FROM transactions
JOIN postings ON transactions.id = postings.transaction_id
WHERE DATE(dt) >= DATE($1) AND account = $2
) p
WHERE postings.id = p.id`,
[newTransaction.dt, (posting as unknown as EditingPosting).originalAccount]
);
}
}
await dbTransaction.commit();

31
src/importers/ofx.ts Normal file
View File

@ -0,0 +1,31 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222025 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 importOfx1 from './ofx1.ts';
import importOfx2 from './ofx2.ts';
import { StatementLine } from '../db.ts';
export default function importOfxAutodetectVersion(sourceAccount: string, content: string): StatementLine[] {
if (content.startsWith('<?')) {
// XML-style: OFX2
return importOfx2(sourceAccount, content);
} else {
// Assume SGML style: OFX1
return importOfx1(sourceAccount, content);
}
}

View File

@ -1,6 +1,6 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -20,7 +20,7 @@ import dayjs from 'dayjs';
import { DT_FORMAT, StatementLine, db } from '../db.ts';
export default function import_ofx1(sourceAccount: string, content: string): StatementLine[] {
export default function importOfx1(sourceAccount: string, content: string): StatementLine[] {
// Import an OFX1 SGML file
// Strip OFX header and parse

View File

@ -1,6 +1,6 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -20,7 +20,7 @@ import dayjs from 'dayjs';
import { DT_FORMAT, StatementLine, db } from '../db.ts';
export default function import_ofx2(sourceAccount: string, content: string): StatementLine[] {
export default function importOfx2(sourceAccount: string, content: string): StatementLine[] {
// Import an OFX2 XML file
// Convert OFX header to XML and parse

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -61,8 +61,9 @@
// Format dt
rawTransaction.dt = dayjs(rawTransaction.dt).format('YYYY-MM-DD');
// Initialise sign and amount_abs
// Initialise originalAccount, sign and amount_abs
for (const posting of rawTransaction.postings) {
posting.originalAccount = posting.account;
posting.sign = posting.quantity >= 0 ? 'dr' : 'cr';
posting.amount_abs = serialiseAmount(Math.abs(posting.quantity), posting.commodity);
}

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -25,8 +25,7 @@
<label for="format" class="block text-gray-900 pr-4">File type</label>
<div>
<select class="bordered-field" id="format" v-model="format">
<option value="ofx2">OFX 2.x</option>
<option value="ofx1">OFX 1.x</option>
<option value="ofx">OFX (1.x/2.x)</option>
</select>
</div>
<label for="account" class="block text-gray-900 pr-4">Source account</label>
@ -86,12 +85,11 @@
import ComboBoxAccounts from '../components/ComboBoxAccounts.vue';
import { ppWithCommodity } from '../display.ts';
import import_ofx1 from '../importers/ofx1.ts';
import import_ofx2 from '../importers/ofx2.ts';
import importOfxAutodetectVersion from '../importers/ofx.ts';
const fileInput = useTemplateRef('file');
const format = ref('ofx2');
const format = ref('ofx');
const selectedFilename = ref('');
const sourceAccount = ref('');
@ -115,10 +113,8 @@
const content = await file.text();
if (format.value === 'ofx2') {
statementLines.value = import_ofx2(sourceAccount.value, content);
} else if (format.value === 'ofx1') {
statementLines.value = import_ofx1(sourceAccount.value, content);
if (format.value === 'ofx') {
statementLines.value = importOfxAutodetectVersion(sourceAccount.value, content);
} else {
throw new Error('Unexpected import format');
}

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -42,6 +42,7 @@
id: null,
description: null,
account: '',
originalAccount: null,
sign: 'dr',
amount_abs: '',
},
@ -49,6 +50,7 @@
id: null,
description: null,
account: '',
originalAccount: null,
sign: 'cr',
amount_abs: '',
}

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 20222025 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
@ -135,7 +135,7 @@
td.className = 'relative'; // CSS trickery so as to not expand the height of the tr
td.innerHTML =
`<div class="flex items-stretch absolute top-[-4px]">
<input type="text" class="bordered-field">
<input type="text" class="bordered-field min-w-[8em]">
<button type="button" class="relative -ml-px inline-flex items-center gap-x-1.5 px-3 py-1 text-gray-800 shadow-sm ring-1 ring-inset ring-gray-400 bg-white hover:bg-gray-50">${ CheckIconHTML }</button>
</div>`;
@ -177,8 +177,20 @@
return;
}
// Insert transaction and statement line reconciliation atomically
// Check if account exists
const session = await db.load();
const countResult = await session.select('SELECT COUNT(*) FROM postings WHERE account = $1', [chargeAccount]) as any[];
const doesAccountExist = countResult[0]['COUNT(*)'] > 0;
if (!doesAccountExist) {
// Prompt for confirmation
if (!await confirm('Account "' + chargeAccount + '" does not exist. Continue to reconcile this transaction and create a new account?')) {
td.querySelector('input')!.disabled = false;
td.querySelector('button')!.disabled = false;
return;
}
}
// Insert transaction and statement line reconciliation atomically
const dbTransaction = await session.begin();
// Insert transaction
@ -349,7 +361,7 @@
rows.push(
`<tr data-line-id="${ line.id }">
<td class="py-0.5 pr-1 align-baseline">${ checkboxCell }</td>
<td class="py-0.5 px-1 align-baseline text-gray-900"><a href="#" class="hover:text-blue-700 hover:underline">${ line.source_account }</a></td>
<td class="py-0.5 px-1 align-baseline text-gray-900"><a href="/transactions/${ encodeURIComponent(line.source_account) }" class="hover:text-blue-700 hover:underline">${ line.source_account }</a></td>
<td class="py-0.5 px-1 align-baseline text-gray-900 lg:w-[12ex]">${ dayjs(line.dt).format('YYYY-MM-DD') }</td>
<td class="py-0.5 px-1 align-baseline text-gray-900">${ line.description }</td>
<td class="charge-account py-0.5 px-1 align-baseline text-gray-900">${ reconciliationCell }</td>