Implement reconciling statement lines
This commit is contained in:
parent
331d7d8b7c
commit
04c22c798f
@ -64,13 +64,13 @@
|
|||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { PencilIcon } from '@heroicons/vue/24/outline';
|
import { CheckIcon, PencilIcon } from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
import { onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { db } from '../db.ts';
|
import { db } from '../db.ts';
|
||||||
import { renderComponent } from '../webutil.ts';
|
import { renderComponent } from '../webutil.ts';
|
||||||
import { ppWithCommodity } from '../display.ts';
|
import { ppWithCommodity } from '../display.ts';
|
||||||
|
|
||||||
interface StatementLine {
|
interface StatementLine {
|
||||||
id: number,
|
id: number,
|
||||||
@ -125,6 +125,91 @@ import { ppWithCommodity } from '../display.ts';
|
|||||||
statementLines.value = newStatementLines;
|
statementLines.value = newStatementLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Could probably avoid polluting global scope by using clusterize clusterChanged callback
|
||||||
|
(window as any).showClassifyLinePanel = function(el: HTMLAnchorElement) {
|
||||||
|
const CheckIconHTML = renderComponent(CheckIcon, { 'class': 'w-5 h-5' });
|
||||||
|
|
||||||
|
const td = el.closest('td')!;
|
||||||
|
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">
|
||||||
|
<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>`;
|
||||||
|
|
||||||
|
td.querySelector('input')!.addEventListener('keydown', async function(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
await onLineClassified(event);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
td.querySelector('button')!.addEventListener('click', onLineClassified);
|
||||||
|
|
||||||
|
td.querySelector('input')!.focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onLineClassified(event: Event) {
|
||||||
|
// Callback when clicking OK or pressing enter to classify a statement line
|
||||||
|
if ((event.target as HTMLInputElement).disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const td = (event.target as Element).closest('td')!;
|
||||||
|
const tr = td.closest('tr')!;
|
||||||
|
const lineId = parseInt(tr.dataset.lineId!);
|
||||||
|
const chargeAccount = (td.querySelector('input')! as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (!chargeAccount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable further submissions
|
||||||
|
td.querySelector('input')!.disabled = true;
|
||||||
|
td.querySelector('button')!.disabled = true;
|
||||||
|
|
||||||
|
const statementLine = statementLines.value.find((l) => l.id === lineId)!;
|
||||||
|
|
||||||
|
// 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)`,
|
||||||
|
[statementLine.dt, statementLine.description]
|
||||||
|
);
|
||||||
|
const transactionId = transactionResult.lastInsertId;
|
||||||
|
|
||||||
|
// Insert posting for this account
|
||||||
|
const accountPostingResult = await dbTransaction.execute(
|
||||||
|
`INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance)
|
||||||
|
VALUES ($1, NULL, $2, $3, $4, NULL)`,
|
||||||
|
[transactionId, statementLine.source_account, statementLine.quantity, statementLine.commodity]
|
||||||
|
);
|
||||||
|
const accountPostingId = accountPostingResult.lastInsertId;
|
||||||
|
|
||||||
|
// Insert posting for the charge account - no need to remember this ID
|
||||||
|
await dbTransaction.execute(
|
||||||
|
`INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance)
|
||||||
|
VALUES ($1, NULL, $2, $3, $4, NULL)`,
|
||||||
|
[transactionId, chargeAccount, -statementLine.quantity, statementLine.commodity]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert statement line reconciliation
|
||||||
|
await dbTransaction.execute(
|
||||||
|
`INSERT INTO statement_line_reconciliations (statement_line_id, posting_id)
|
||||||
|
VALUES ($1, $2)`,
|
||||||
|
[statementLine.id, accountPostingId]
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = [];
|
||||||
@ -134,13 +219,12 @@ import { ppWithCommodity } from '../display.ts';
|
|||||||
if (line.posting_accounts.length === 0) {
|
if (line.posting_accounts.length === 0) {
|
||||||
// Unreconciled
|
// Unreconciled
|
||||||
reconciliationCell =
|
reconciliationCell =
|
||||||
`<a href="#" class="text-red-500 hover:text-red-600 hover:underline" onclick="return classifyLine(this);">Unclassified</a>
|
`<a href="#" class="classify-link text-red-500 hover:text-red-600 hover:underline" onclick="return showClassifyLinePanel(this);">Unclassified</a>`;
|
||||||
<a href="/journal/edit-transaction/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`;
|
|
||||||
} 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 =
|
||||||
`<a href="#" class="hover:text-blue-700 hover:underline" onclick="return classifyLine(this);">${ otherAccount }</a>
|
`<span>${ otherAccount }</span>
|
||||||
<a href="/journal/edit-transaction/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`;
|
<a href="/journal/edit-transaction/${ line.transaction_id }" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">${ PencilIconHTML }</a>`;
|
||||||
} else {
|
} else {
|
||||||
// Complex reconciliation
|
// Complex reconciliation
|
||||||
@ -150,7 +234,7 @@ import { ppWithCommodity } from '../display.ts';
|
|||||||
}
|
}
|
||||||
|
|
||||||
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"><input class="checkbox-primary" type="checkbox" name="sel-line-id" value="${ line.id }"></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>
|
||||||
|
Loading…
Reference in New Issue
Block a user