Implement reconciling statement lines as transfer
This commit is contained in:
parent
82ed8d99b0
commit
7b9c8ebd55
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user