Compare commits
5 Commits
b8223aef05
...
064f50e668
Author | SHA1 | Date | |
---|---|---|---|
064f50e668 | |||
93e0466dd1 | |||
277535dc53 | |||
bea8b76e57 | |||
b8845e9b77 |
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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
31
src/importers/ofx.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 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);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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');
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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: '',
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
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
|
||||
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user