Implement reconciling statement lines as transfer

This commit is contained in:
RunasSudo 2024-12-17 15:34:26 +11:00
parent 82ed8d99b0
commit 7b9c8ebd55
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A

View File

@ -23,9 +23,9 @@
<div class="my-2 py-2 flex bg-white sticky top-0"> <div class="my-2 py-2 flex bg-white sticky top-0">
<div class="grow flex gap-x-2 items-baseline"> <div class="grow flex gap-x-2 items-baseline">
<!--<button class="btn-secondary text-emerald-700 ring-emerald-600"> <button @click="reconcileAsTransfer" class="btn-secondary text-emerald-700 ring-emerald-600">
Reconcile selected as transfer Reconcile selected as transfer
</button>--> </button>
<RouterLink :to="{ name: 'import-statement' }" class="btn-secondary"> <RouterLink :to="{ name: 'import-statement' }" class="btn-secondary">
Import statement Import statement
</RouterLink> </RouterLink>
@ -172,6 +172,11 @@
const statementLine = statementLines.value.find((l) => l.id === lineId)!; const statementLine = statementLines.value.find((l) => l.id === lineId)!;
if (statementLine.posting_accounts.length !== 0) {
await alert('Cannot reconcile already reconciled statement line');
return;
}
// Insert transaction and statement line reconciliation atomically // Insert transaction and statement line reconciliation atomically
const session = await db.load(); const session = await db.load();
const dbTransaction = await session.begin(); const dbTransaction = await session.begin();
@ -226,22 +231,109 @@
await load(); await load();
} }
async function reconcileAsTransfer() {
const selectedCheckboxes = document.querySelectorAll('.statement-line-checkbox:checked');
if (selectedCheckboxes.length !== 2) {
await alert('Must select exactly 2 statement lines');
return;
}
const selectedLineIds = [...selectedCheckboxes].map((el) => parseInt(el.closest('tr')?.dataset.lineId!));
const line1 = statementLines.value.find((l) => l.id === selectedLineIds[0])!;
const line2 = statementLines.value.find((l) => l.id === selectedLineIds[1])!;
// Sanity checks
if (line1.quantity + line2.quantity !== 0 || line1.commodity !== line2.commodity) {
await alert('Selected statement line debits/credits must equal');
return;
}
if (line1.posting_accounts.length !== 0 || line2.posting_accounts.length !== 0) {
await alert('Cannot reconcile already reconciled statement lines');
return;
}
// Insert transaction and statement line reconciliation atomically
const session = await db.load();
const dbTransaction = await session.begin();
// Insert transaction
const transactionResult = await dbTransaction.execute(
`INSERT INTO transactions (dt, description)
VALUES ($1, $2)`,
[line1.dt, line1.description]
);
const transactionId = transactionResult.lastInsertId;
// Insert posting for line1
const postingResult1 = await dbTransaction.execute(
`INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance)
VALUES ($1, $2, $3, $4, $5, NULL)`,
[transactionId, line1.description, line1.source_account, line1.quantity, line1.commodity]
);
const postingId1 = postingResult1.lastInsertId;
// Insert statement line reconciliation
await dbTransaction.execute(
`INSERT INTO statement_line_reconciliations (statement_line_id, posting_id)
VALUES ($1, $2)`,
[line1.id, postingId1]
);
// Insert posting for line2
const postingResult2 = await dbTransaction.execute(
`INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance)
VALUES ($1, $2, $3, $4, $5, NULL)`,
[transactionId, line2.description, line2.source_account, line2.quantity, line2.commodity]
);
const postingId2 = postingResult2.lastInsertId;
// Insert statement line reconciliation
await dbTransaction.execute(
`INSERT INTO statement_line_reconciliations (statement_line_id, posting_id)
VALUES ($1, $2)`,
[line2.id, postingId2]
);
// Invalidate running balances
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 IN ($2, $3)
) p
WHERE postings.id = p.id`,
[line1.dt, line1.source_account, line2.source_account]
);
dbTransaction.commit();
// Reload transactions and re-render the table
await load();
}
function renderTable() { function renderTable() {
const PencilIconHTML = renderComponent(PencilIcon, { 'class': 'w-4 h-4 inline align-middle -mt-0.5' }); // Pre-render the pencil icon const PencilIconHTML = renderComponent(PencilIcon, { 'class': 'w-4 h-4 inline align-middle -mt-0.5' }); // Pre-render the pencil icon
const rows = []; const rows = [];
for (const line of statementLines.value) { for (const line of statementLines.value) {
let reconciliationCell; let reconciliationCell, checkboxCell;
if (line.posting_accounts.length === 0) { if (line.posting_accounts.length === 0) {
// Unreconciled // Unreconciled
reconciliationCell = reconciliationCell =
`<a href="#" class="classify-link text-red-500 hover:text-red-600 hover:underline" onclick="return showClassifyLinePanel(this);">Unclassified</a>`; `<a href="#" class="classify-link text-red-500 hover:text-red-600 hover:underline" onclick="return showClassifyLinePanel(this);">Unclassified</a>`;
checkboxCell = `<input class="checkbox-primary statement-line-checkbox" type="checkbox">`; // Only show checkbox for unreconciled lines
} else if (line.posting_accounts.length === 2) { } else if (line.posting_accounts.length === 2) {
// Simple reconciliation // Simple reconciliation
const otherAccount = line.posting_accounts.find((a) => a !== line.source_account); const otherAccount = line.posting_accounts.find((a) => a !== line.source_account);
reconciliationCell = reconciliationCell =
`<span>${ otherAccount }</span> `<span>${ otherAccount }</span>
<a href="/journal/edit/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`; <a href="/journal/edit/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`;
checkboxCell = '';
if (showOnlyUnclassified.value) { continue; } if (showOnlyUnclassified.value) { continue; }
} else { } else {
@ -249,13 +341,14 @@
reconciliationCell = reconciliationCell =
`<i>(Complex)</i> `<i>(Complex)</i>
<a href="/journal/edit/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`; <a href="/journal/edit/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`;
checkboxCell = '';
if (showOnlyUnclassified.value) { continue; } if (showOnlyUnclassified.value) { continue; }
} }
rows.push( rows.push(
`<tr data-line-id="${ line.id }"> `<tr data-line-id="${ line.id }">
<td class="py-0.5 pr-1 align-baseline"><input class="checkbox-primary" type="checkbox" name="sel-line-id" value="${ line.id }"></td> <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="#" 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 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="py-0.5 px-1 align-baseline text-gray-900">${ line.description }</td>