Implement balance sheet report
This commit is contained in:
parent
879fac9999
commit
ae2b9e4016
@ -22,6 +22,8 @@
|
|||||||
{{ report.title }}
|
{{ report.title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-300">
|
<tr class="border-b border-gray-300">
|
||||||
|
@ -53,7 +53,7 @@ export function ppBracketed(quantity: number, link?: string): string {
|
|||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
// Put the space outside of the hyperlink so it is not underlined
|
// Put the space outside of the hyperlink so it is not underlined
|
||||||
return '<a href="' + encodeURIComponent(link) + '">' + text + '</a>' + space;
|
return '<a href="' + encodeURIComponent(link) + '" class="hover:text-blue-700 hover:underline">' + text + '</a>' + space;
|
||||||
} else {
|
} else {
|
||||||
return text + space;
|
return text + space;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ async function initApp() {
|
|||||||
{ path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') },
|
{ path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') },
|
||||||
{ path: '/balance-assertions/edit/:id', name: 'balance-assertions-edit', component: () => import('./pages/EditBalanceAssertionView.vue') },
|
{ path: '/balance-assertions/edit/:id', name: 'balance-assertions-edit', component: () => import('./pages/EditBalanceAssertionView.vue') },
|
||||||
{ path: '/balance-assertions/new', name: 'balance-assertions-new', component: () => import('./pages/NewBalanceAssertionView.vue') },
|
{ path: '/balance-assertions/new', name: 'balance-assertions-new', component: () => import('./pages/NewBalanceAssertionView.vue') },
|
||||||
|
{ path: '/balance-sheet', name: 'balance-sheet', component: () => import('./reports/BalanceSheetReport.vue') },
|
||||||
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
|
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
|
||||||
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
|
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
|
||||||
{ path: '/income-statement', name: 'income-statement', component: () => import('./reports/IncomeStatementReport.vue') },
|
{ path: '/income-statement', name: 'income-statement', component: () => import('./reports/IncomeStatementReport.vue') },
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<ul class="list-disc ml-6">
|
<ul class="list-disc ml-6">
|
||||||
<li><RouterLink :to="{ name: 'general-ledger' }" class="text-gray-900 hover:text-blue-700 hover:underline">General ledger</RouterLink></li>
|
<li><RouterLink :to="{ name: 'general-ledger' }" class="text-gray-900 hover:text-blue-700 hover:underline">General ledger</RouterLink></li>
|
||||||
<li><RouterLink :to="{ name: 'trial-balance' }" class="text-gray-900 hover:text-blue-700 hover:underline">Trial balance</RouterLink></li>
|
<li><RouterLink :to="{ name: 'trial-balance' }" class="text-gray-900 hover:text-blue-700 hover:underline">Trial balance</RouterLink></li>
|
||||||
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Balance sheet</a></li>-->
|
<li><RouterLink :to="{ name: 'balance-sheet' }" class="text-gray-900 hover:text-blue-700 hover:underline">Balance sheet</RouterLink></li>
|
||||||
<li><RouterLink :to="{ name: 'income-statement' }" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</RouterLink></li>
|
<li><RouterLink :to="{ name: 'income-statement' }" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</RouterLink></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@ import { asCost } from './amounts.ts';
|
|||||||
import { DT_FORMAT, JoinedTransactionPosting, StatementLine, Transaction, db, getAccountsForKind, joinedToTransactions, totalBalances, totalBalancesAtDate } from './db.ts';
|
import { DT_FORMAT, JoinedTransactionPosting, StatementLine, Transaction, db, getAccountsForKind, joinedToTransactions, totalBalances, totalBalancesAtDate } from './db.ts';
|
||||||
import { ExtendedDatabase } from './dbutil.ts';
|
import { ExtendedDatabase } from './dbutil.ts';
|
||||||
|
|
||||||
|
import { BalanceSheetReport } from './reports/BalanceSheetReport.vue';
|
||||||
import { DrcrReport } from './reports/base.ts';
|
import { DrcrReport } from './reports/base.ts';
|
||||||
import TrialBalanceReport from './reports/TrialBalanceReport.ts';
|
import TrialBalanceReport from './reports/TrialBalanceReport.ts';
|
||||||
import { IncomeStatementReport } from './reports/IncomeStatementReport.vue';
|
import { IncomeStatementReport } from './reports/IncomeStatementReport.vue';
|
||||||
@ -46,9 +47,9 @@ export enum ReportingStage {
|
|||||||
//IncomeStatement = 600,
|
//IncomeStatement = 600,
|
||||||
|
|
||||||
// Final balance sheet
|
// Final balance sheet
|
||||||
//BalanceSheet = 700,
|
BalanceSheet = 700,
|
||||||
|
|
||||||
FINAL_STAGE = InterimIncomeStatement
|
FINAL_STAGE = BalanceSheet
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReportingWorkflow {
|
export class ReportingWorkflow {
|
||||||
@ -203,10 +204,20 @@ export class ReportingWorkflow {
|
|||||||
// ---------------
|
// ---------------
|
||||||
// InterimIncomeStatement
|
// InterimIncomeStatement
|
||||||
|
|
||||||
|
let incomeStatementReport;
|
||||||
{
|
{
|
||||||
const incomeStatementReport = new IncomeStatementReport();
|
incomeStatementReport = new IncomeStatementReport();
|
||||||
await incomeStatementReport.generate(balances);
|
await incomeStatementReport.generate(balances);
|
||||||
this.reportsForStage.set(ReportingStage.InterimIncomeStatement, [incomeStatementReport, new TrialBalanceReport(balances)]);
|
this.reportsForStage.set(ReportingStage.InterimIncomeStatement, [incomeStatementReport]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------
|
||||||
|
// BalanceSheet
|
||||||
|
|
||||||
|
{
|
||||||
|
const balanceSheetReport = new BalanceSheetReport();
|
||||||
|
await balanceSheetReport.generate(balances, incomeStatementReport);
|
||||||
|
this.reportsForStage.set(ReportingStage.BalanceSheet, [balanceSheetReport]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,12 +229,25 @@ export class ReportingWorkflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const report = reportsForTheStage.find((r) => r instanceof reportType);
|
const report = reportsForTheStage.find((r) => r instanceof reportType);
|
||||||
if (!report) {
|
if (report) {
|
||||||
throw new Error('Report does not exist');
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recurse earlier stages
|
||||||
|
const stages = [...this.reportsForStage.keys()];
|
||||||
|
stages.reverse();
|
||||||
|
for (const earlierStage of stages) {
|
||||||
|
if (earlierStage >= stage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const report = this.reportsForStage.get(earlierStage)!.find((r) => r instanceof reportType);
|
||||||
|
if (report) {
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Report does not exist at requested stage or any earlier stage');
|
||||||
|
}
|
||||||
|
|
||||||
getTransactionsAtStage(stage: ReportingStage): Transaction[] {
|
getTransactionsAtStage(stage: ReportingStage): Transaction[] {
|
||||||
const transactions: Transaction[] = [];
|
const transactions: Transaction[] = [];
|
||||||
|
109
src/reports/BalanceSheetReport.vue
Normal file
109
src/reports/BalanceSheetReport.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<!--
|
||||||
|
DrCr: Web-based double-entry bookkeeping framework
|
||||||
|
Copyright (C) 2022–2024 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export class BalanceSheetReport extends DynamicReport {
|
||||||
|
constructor() {
|
||||||
|
super('Balance sheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate(balances: Map<string, number>, incomeStatementReport: IncomeStatementReport) {
|
||||||
|
this.entries = [
|
||||||
|
new Section(
|
||||||
|
'Assets',
|
||||||
|
[
|
||||||
|
...await DynamicReport.entriesForKind(balances, 'drcr.asset'),
|
||||||
|
new Subtotal('Total assets', 'total_assets', true /* visible */, true /* bordered */)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new Spacer(),
|
||||||
|
new Section(
|
||||||
|
'Liabilities',
|
||||||
|
[
|
||||||
|
...await DynamicReport.entriesForKind(balances, 'drcr.liability', true),
|
||||||
|
new Subtotal('Total liabilities', 'total_liabilities', true /* visible */, true /* bordered */)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new Spacer(),
|
||||||
|
new Section(
|
||||||
|
'Equity',
|
||||||
|
[
|
||||||
|
...await DynamicReport.entriesForKind(balances, 'drcr.equity', true),
|
||||||
|
new Entry('Current year surplus (deficit)', (incomeStatementReport.byId('net_surplus') as Computed).quantity, null /* id */, true /* visible */, false /* autoHide */, '/income-statement'),
|
||||||
|
new Entry('Accumulated surplus (deficit)', -(balances.get('Accumulated surplus (deficit)') ?? 0), null /* id */, true /* visible */, false /* autoHide */, '/transactions/Accumulated surplus (deficit)'),
|
||||||
|
new Subtotal('Total equity', 'total_equity', true /* visible */, true /* bordered */)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
this.calculate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Report display -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DynamicReportComponent :report="report">
|
||||||
|
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="!doesBalance">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<ExclamationCircleIcon class="h-5 w-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<p class="text-sm text-red-700">Total assets do not equal total liabilities and equity. This may occur if not all accounts have been classified in the chart of accounts.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicReportComponent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
|
|
||||||
|
import { Computed, DynamicReport, Entry, Section, Spacer, Subtotal } from './base.ts';
|
||||||
|
import { IncomeStatementReport} from './IncomeStatementReport.vue';
|
||||||
|
import { db } from '../db.ts';
|
||||||
|
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
|
||||||
|
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
|
||||||
|
|
||||||
|
const report = ref(null as BalanceSheetReport | null);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const session = await db.load();
|
||||||
|
const reportingWorkflow = new ReportingWorkflow();
|
||||||
|
await reportingWorkflow.generate(session);
|
||||||
|
|
||||||
|
report.value = reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, BalanceSheetReport) as BalanceSheetReport;
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
|
||||||
|
const doesBalance = computed(function() {
|
||||||
|
if (report.value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const totalAssets = (report.value.byId('total_assets') as Computed).quantity;
|
||||||
|
const totalLiabilities = (report.value.byId('total_liabilities') as Computed).quantity;
|
||||||
|
const totalEquity = (report.value.byId('total_equity') as Computed).quantity;
|
||||||
|
return totalAssets === totalLiabilities + totalEquity;
|
||||||
|
});
|
||||||
|
</script>
|
@ -71,6 +71,10 @@ export class DynamicReport implements DrcrReport {
|
|||||||
entries.push(new Entry(
|
entries.push(new Entry(
|
||||||
account,
|
account,
|
||||||
negate ? -quantity : quantity,
|
negate ? -quantity : quantity,
|
||||||
|
null /* id */,
|
||||||
|
true /* visible */,
|
||||||
|
false /* autoHide */,
|
||||||
|
'/transactions/' + account
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,7 +175,7 @@ export class Subtotal extends Entry {
|
|||||||
|
|
||||||
this.quantity = 0;
|
this.quantity = 0;
|
||||||
for (const entry of parent.entries) {
|
for (const entry of parent.entries) {
|
||||||
if (entry instanceof Entry) {
|
if (entry instanceof Entry && entry !== this) {
|
||||||
this.quantity += entry.quantity;
|
this.quantity += entry.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user