Implement income statement report
This commit is contained in:
parent
071d7c8988
commit
879fac9999
47
src/components/DynamicReportComponent.vue
Normal file
47
src/components/DynamicReportComponent.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<template v-if="report !== null">
|
||||
<h1 class="page-heading">
|
||||
{{ report.title }}
|
||||
</h1>
|
||||
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-300">
|
||||
<th></th>
|
||||
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-end">{{ db.metadata.reporting_commodity }} </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<DynamicReportEntry :entry="entry" v-for="entry of report.entries" />
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
import { db } from '../db.ts';
|
||||
import { DynamicReport } from '../reports/base.ts';
|
||||
import DynamicReportEntry from './DynamicReportEntry.vue';
|
||||
|
||||
const { report } = defineProps<{ report: DynamicReport | null }>();
|
||||
</script>
|
49
src/components/DynamicReportEntry.vue
Normal file
49
src/components/DynamicReportEntry.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<template v-if="entry instanceof Entry">
|
||||
<!-- NB: Subtotal and Calculated are subclasses of Entry -->
|
||||
<tr :class="entry.bordered ? 'border-y border-gray-300' : null">
|
||||
<component :is="entry.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': entry.heading }">
|
||||
<a :href="entry.link" class="hover:text-blue-700 hover:underline" v-if="entry.link !== null">{{ entry.text }}</a>
|
||||
<template v-if="entry.link === null">{{ entry.text }}</template>
|
||||
</component>
|
||||
<component :is="entry.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': entry.heading }" v-html="ppBracketed(entry.quantity, entry.link ?? undefined)" />
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="entry instanceof Section">
|
||||
<tr v-if="entry.title !== null">
|
||||
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">{{ entry.title }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<DynamicReportEntry :entry="child" v-for="child of entry.entries" />
|
||||
</template>
|
||||
<template v-if="entry instanceof Spacer">
|
||||
<tr><td colspan="2" class="py-0.5"> </td></tr>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
import { ppBracketed } from '../display.ts';
|
||||
import { DynamicReportNode, Entry, Section, Spacer } from '../reports/base.ts';
|
||||
|
||||
const { entry } = defineProps<{ entry: DynamicReportNode }>();
|
||||
</script>
|
12
src/db.ts
12
src/db.ts
@ -180,6 +180,18 @@ export function joinedToTransactions(joinedTransactionPostings: JoinedTransactio
|
||||
return transactions;
|
||||
}
|
||||
|
||||
export async function getAccountsForKind(session: ExtendedDatabase, kind: string): Promise<string[]> {
|
||||
const rawAccountsForKind: {account: string}[] = await session.select(
|
||||
`SELECT account
|
||||
FROM account_configurations
|
||||
WHERE kind = $1
|
||||
ORDER BY account`,
|
||||
[kind]
|
||||
);
|
||||
const accountsForKind = rawAccountsForKind.map((a) => a.account);
|
||||
return accountsForKind;
|
||||
}
|
||||
|
||||
export function serialiseAmount(quantity: number, commodity: string): string {
|
||||
// Pretty print the amount for an editable input
|
||||
if (quantity < 0) {
|
||||
|
@ -39,3 +39,22 @@ export function ppWithCommodity(quantity: number, commodity: string): string {
|
||||
return pp(quantity) + ' ' + commodity;
|
||||
}
|
||||
}
|
||||
|
||||
export function ppBracketed(quantity: number, link?: string): string {
|
||||
// Pretty print the quantity with brackets for negative numbers
|
||||
let text, space;
|
||||
if (quantity >= 0) {
|
||||
text = pp(quantity);
|
||||
space = ' ';
|
||||
} else {
|
||||
text = '(' + pp(-quantity) + ')';
|
||||
space = '';
|
||||
}
|
||||
|
||||
if (link) {
|
||||
// Put the space outside of the hyperlink so it is not underlined
|
||||
return '<a href="' + encodeURIComponent(link) + '">' + text + '</a>' + space;
|
||||
} else {
|
||||
return text + space;
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ async function initApp() {
|
||||
{ path: '/balance-assertions/new', name: 'balance-assertions-new', component: () => import('./pages/NewBalanceAssertionView.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: '/income-statement', name: 'income-statement', component: () => import('./reports/IncomeStatementReport.vue') },
|
||||
{ path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') },
|
||||
{ path: '/journal/edit/:id', name: 'journal-edit-transaction', component: () => import('./pages/EditTransactionView.vue') },
|
||||
{ path: '/journal/new', name: 'journal-new-transaction', component: () => import('./pages/NewTransactionView.vue') },
|
||||
|
@ -83,7 +83,7 @@
|
||||
const reportingWorkflow = new ReportingWorkflow();
|
||||
await reportingWorkflow.generate(session);
|
||||
|
||||
transactions.value = reportingWorkflow.getTransactionsAtStage(ReportingStage.OrdinaryAPITransactions);
|
||||
transactions.value = reportingWorkflow.getTransactionsAtStage(ReportingStage.FINAL_STAGE);
|
||||
|
||||
// Display transactions in reverse chronological order
|
||||
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
||||
|
@ -34,7 +34,7 @@
|
||||
<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><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Balance sheet</a></li>-->
|
||||
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</a></li>-->
|
||||
<li><RouterLink :to="{ name: 'income-statement' }" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</RouterLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pl-4">
|
||||
|
@ -58,14 +58,20 @@
|
||||
const reportingWorkflow = new ReportingWorkflow();
|
||||
await reportingWorkflow.generate(session); // This also ensures running balances are up to date
|
||||
|
||||
const transactionsRaw = reportingWorkflow.getTransactionsAtStage(ReportingStage.OrdinaryAPITransactions);
|
||||
const transactionsRaw = reportingWorkflow.getTransactionsAtStage(ReportingStage.FINAL_STAGE);
|
||||
|
||||
// Filter only transactions affecting this account
|
||||
transactions.value = transactionsRaw.filter((t) => t.postings.some((p) => p.account === route.params.account));
|
||||
const filteredTransactions = transactionsRaw.filter((t) => t.postings.some((p) => p.account === route.params.account));
|
||||
|
||||
// Display transactions in reverse chronological order
|
||||
// In order to correctly sort API transactions, we need to remember their indexes
|
||||
const filteredTxnsWithIndexes = filteredTransactions.map((t, index) => [t, index] as [Transaction, number]);
|
||||
|
||||
// Sort transactions in reverse chronological order
|
||||
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
||||
transactions.value.sort((a, b) => (b.dt.localeCompare(a.dt)) || ((b.id ?? 0) - (a.id ?? 0)));
|
||||
// Use Number.MAX_SAFE_INTEGER as ID for API transactions
|
||||
filteredTxnsWithIndexes.sort(([t1, i1], [t2, i2]) => (t2.dt.localeCompare(t1.dt)) || ((t2.id ?? Number.MAX_SAFE_INTEGER) - (t1.id ?? Number.MAX_SAFE_INTEGER) || (i2 - i1)));
|
||||
|
||||
transactions.value = filteredTxnsWithIndexes.map(([t, _idx]) => t);
|
||||
}
|
||||
load();
|
||||
</script>
|
||||
|
@ -53,7 +53,8 @@
|
||||
|
||||
import { db } from '../db.ts';
|
||||
import { pp } from '../display.ts';
|
||||
import { ReportingStage, ReportingWorkflow, TrialBalanceReport } from '../reporting.ts';
|
||||
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
|
||||
import TrialBalanceReport from '../reports/TrialBalanceReport.ts';
|
||||
|
||||
const report = ref(null as TrialBalanceReport | null);
|
||||
|
||||
@ -72,7 +73,7 @@
|
||||
const reportingWorkflow = new ReportingWorkflow();
|
||||
await reportingWorkflow.generate(session);
|
||||
|
||||
report.value = reportingWorkflow.getReportAtStage(ReportingStage.OrdinaryAPITransactions, TrialBalanceReport) as TrialBalanceReport;
|
||||
report.value = reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, TrialBalanceReport) as TrialBalanceReport;
|
||||
}
|
||||
load();
|
||||
</script>
|
||||
|
100
src/reporting.ts
100
src/reporting.ts
@ -16,21 +16,44 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { asCost } from './amounts.ts';
|
||||
import { JoinedTransactionPosting, StatementLine, Transaction, 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 { DrcrReport } from './reports/base.ts';
|
||||
import TrialBalanceReport from './reports/TrialBalanceReport.ts';
|
||||
import { IncomeStatementReport } from './reports/IncomeStatementReport.vue';
|
||||
|
||||
export enum ReportingStage {
|
||||
// Load transactions from database
|
||||
TransactionsFromDatabase = 100,
|
||||
|
||||
// Load unreconciled statement lines and other ordinary API transactions
|
||||
OrdinaryAPITransactions = 200,
|
||||
|
||||
// Recognise accumulated surplus as equity
|
||||
AccumulatedSurplusToEquity = 300,
|
||||
|
||||
// Interim income statement considering only DB and ordinary API transactions
|
||||
InterimIncomeStatement = 400,
|
||||
|
||||
// Income tax estimation
|
||||
//Tax = 500,
|
||||
|
||||
// Final income statement
|
||||
//IncomeStatement = 600,
|
||||
|
||||
// Final balance sheet
|
||||
//BalanceSheet = 700,
|
||||
|
||||
FINAL_STAGE = InterimIncomeStatement
|
||||
}
|
||||
|
||||
export class ReportingWorkflow {
|
||||
transactionsForStage: Map<ReportingStage, Transaction[]> = new Map();
|
||||
reportsForStage: Map<ReportingStage, Report[]> = new Map();
|
||||
reportsForStage: Map<ReportingStage, DrcrReport[]> = new Map();
|
||||
|
||||
async generate(session: ExtendedDatabase, dt?: string) {
|
||||
// ------------------------
|
||||
@ -123,9 +146,71 @@ export class ReportingWorkflow {
|
||||
balances = applyTransactionsToBalances(balances, transactions);
|
||||
this.reportsForStage.set(ReportingStage.OrdinaryAPITransactions, [new TrialBalanceReport(balances)]);
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// AccumulatedSurplusToEquity
|
||||
|
||||
{
|
||||
// Compute balances at end of last financial year
|
||||
const last_eofy_date = dayjs(db.metadata.eofy_date).subtract(1, 'year');
|
||||
const balancesLastEofy = await totalBalancesAtDate(session, last_eofy_date.format('YYYY-MM-DD'));
|
||||
|
||||
// Get income and expense accounts
|
||||
const incomeAccounts = await getAccountsForKind(session, 'drcr.income');
|
||||
const expenseAccounts = await getAccountsForKind(session, 'drcr.expense');
|
||||
const pandlAccounts = [...incomeAccounts, ...expenseAccounts];
|
||||
pandlAccounts.sort();
|
||||
|
||||
// Prepare transactions
|
||||
const transactions = [];
|
||||
for (const account of pandlAccounts) {
|
||||
if (balancesLastEofy.has(account)) {
|
||||
const balanceLastEofy = balancesLastEofy.get(account)!;
|
||||
if (balanceLastEofy === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
transactions.push(new Transaction(
|
||||
null,
|
||||
last_eofy_date.format(DT_FORMAT),
|
||||
'Accumulated surplus/deficit',
|
||||
[
|
||||
{
|
||||
id: null,
|
||||
description: null,
|
||||
account: account,
|
||||
quantity: -balanceLastEofy,
|
||||
commodity: db.metadata.reporting_commodity
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
description: null,
|
||||
account: 'Accumulated surplus (deficit)',
|
||||
quantity: balanceLastEofy,
|
||||
commodity: db.metadata.reporting_commodity
|
||||
},
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
this.transactionsForStage.set(ReportingStage.AccumulatedSurplusToEquity, transactions);
|
||||
|
||||
// Recompute balances
|
||||
balances = applyTransactionsToBalances(balances, transactions);
|
||||
this.reportsForStage.set(ReportingStage.AccumulatedSurplusToEquity, [new TrialBalanceReport(balances)]);
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// InterimIncomeStatement
|
||||
|
||||
{
|
||||
const incomeStatementReport = new IncomeStatementReport();
|
||||
await incomeStatementReport.generate(balances);
|
||||
this.reportsForStage.set(ReportingStage.InterimIncomeStatement, [incomeStatementReport, new TrialBalanceReport(balances)]);
|
||||
}
|
||||
}
|
||||
|
||||
getReportAtStage(stage: ReportingStage, reportType: any): Report {
|
||||
getReportAtStage(stage: ReportingStage, reportType: any): DrcrReport {
|
||||
// TODO: This function needs generics
|
||||
const reportsForTheStage = this.reportsForStage.get(stage);
|
||||
if (!reportsForTheStage) {
|
||||
@ -151,15 +236,6 @@ export class ReportingWorkflow {
|
||||
}
|
||||
}
|
||||
|
||||
interface Report {
|
||||
}
|
||||
|
||||
export class TrialBalanceReport implements Report {
|
||||
constructor(
|
||||
public balances: Map<string, number>
|
||||
) {}
|
||||
}
|
||||
|
||||
function applyTransactionsToBalances(balances: Map<string, number>, transactions: Transaction[]): Map<string, number> {
|
||||
// Initialise new balances
|
||||
const newBalances: Map<string, number> = new Map([...balances.entries()]);
|
||||
|
81
src/reports/IncomeStatementReport.vue
Normal file
81
src/reports/IncomeStatementReport.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<!--
|
||||
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 IncomeStatementReport extends DynamicReport {
|
||||
constructor() {
|
||||
super('Income statement');
|
||||
}
|
||||
|
||||
async generate(balances: Map<string, number>) {
|
||||
const report = this;
|
||||
this.entries = [
|
||||
new Section(
|
||||
'Income',
|
||||
[
|
||||
...await DynamicReport.entriesForKind(balances, 'drcr.income', true),
|
||||
new Subtotal('Total income', 'total_income', true /* visible */, true /* bordered */)
|
||||
]
|
||||
),
|
||||
new Spacer(),
|
||||
new Section(
|
||||
'Expenses',
|
||||
[
|
||||
...await DynamicReport.entriesForKind(balances, 'drcr.expense'),
|
||||
new Subtotal('Total expenses', 'total_expenses', true /* visible */, true /* bordered */)
|
||||
]
|
||||
),
|
||||
new Spacer(),
|
||||
new Computed(
|
||||
'Net surplus (deficit)',
|
||||
() => (report.byId('total_income') as Subtotal).quantity - (report.byId('total_expenses') as Subtotal).quantity,
|
||||
'net_surplus',
|
||||
true /* visible */, false /* autoHide */, null /* link */, true /* heading */, true /* bordered */
|
||||
)
|
||||
];
|
||||
|
||||
this.calculate();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Report display -->
|
||||
|
||||
<template>
|
||||
<DynamicReportComponent :report="report" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Computed, DynamicReport, Section, Spacer, Subtotal } from './base.ts';
|
||||
import { db } from '../db.ts';
|
||||
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
|
||||
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
|
||||
|
||||
const report = ref(null as IncomeStatementReport | null);
|
||||
|
||||
async function load() {
|
||||
const session = await db.load();
|
||||
const reportingWorkflow = new ReportingWorkflow();
|
||||
await reportingWorkflow.generate(session);
|
||||
|
||||
report.value = reportingWorkflow.getReportAtStage(ReportingStage.InterimIncomeStatement, IncomeStatementReport) as IncomeStatementReport;
|
||||
}
|
||||
load();
|
||||
</script>
|
25
src/reports/TrialBalanceReport.ts
Normal file
25
src/reports/TrialBalanceReport.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { DrcrReport } from './base.ts';
|
||||
|
||||
export default class TrialBalanceReport implements DrcrReport {
|
||||
constructor(
|
||||
public balances: Map<string, number>
|
||||
) {}
|
||||
}
|
179
src/reports/base.ts
Normal file
179
src/reports/base.ts
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { db, getAccountsForKind } from '../db.ts';
|
||||
|
||||
export interface DrcrReport {
|
||||
}
|
||||
|
||||
export interface DynamicReportNode {
|
||||
id: string | null;
|
||||
calculate(parent: DynamicReport | DynamicReportNode): void;
|
||||
}
|
||||
|
||||
export class DynamicReport implements DrcrReport {
|
||||
constructor(
|
||||
public title: string,
|
||||
public entries: DynamicReportNode[] = [],
|
||||
) {}
|
||||
|
||||
byId(id: string): DynamicReportNode | null {
|
||||
// Get the DynamicReportNode with the given ID
|
||||
for (const entry of this.entries) {
|
||||
if (entry.id === id) {
|
||||
return entry;
|
||||
}
|
||||
if (entry instanceof Section) {
|
||||
const result = entry.byId(id);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
calculate() {
|
||||
// Compute all subtotals
|
||||
for (const entry of this.entries) {
|
||||
entry.calculate(this);
|
||||
}
|
||||
}
|
||||
|
||||
static async entriesForKind(balances: Map<string, number>, kind: string, negate = false) {
|
||||
// Get accounts associated with this kind
|
||||
const accountsForKind = await getAccountsForKind(await db.load(), kind);
|
||||
|
||||
// Return one entry for each such account
|
||||
const entries = [];
|
||||
for (const account of accountsForKind) {
|
||||
if (balances.has(account)) {
|
||||
const quantity = balances.get(account)!;
|
||||
if (quantity === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(new Entry(
|
||||
account,
|
||||
negate ? -quantity : quantity,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
export class Entry implements DynamicReportNode {
|
||||
constructor(
|
||||
public text: string,
|
||||
public quantity: number,
|
||||
public id: string | null = null,
|
||||
public visible = true,
|
||||
public autoHide = false,
|
||||
public link: string | null = null,
|
||||
public heading = false,
|
||||
public bordered = false,
|
||||
) {}
|
||||
|
||||
calculate(_parent: DynamicReport | DynamicReportNode) {}
|
||||
}
|
||||
|
||||
export class Computed extends Entry {
|
||||
constructor(
|
||||
public text: string,
|
||||
public calc: Function,
|
||||
public id: string | null = null,
|
||||
public visible = true,
|
||||
public autoHide = false,
|
||||
public link: string | null = null,
|
||||
public heading = false,
|
||||
public bordered = false,
|
||||
) {
|
||||
super(text, null!, id, visible, autoHide, link, heading, bordered);
|
||||
}
|
||||
|
||||
calculate(_parent: DynamicReport | DynamicReportNode) {
|
||||
// Calculate the value of this entry
|
||||
this.quantity = this.calc();
|
||||
}
|
||||
}
|
||||
|
||||
export class Section implements DynamicReportNode {
|
||||
constructor(
|
||||
public title: string | null,
|
||||
public entries: DynamicReportNode[] = [],
|
||||
public id: string | null = null,
|
||||
public visible = true,
|
||||
public autoHide = false,
|
||||
) {}
|
||||
|
||||
calculate(_parent: DynamicReport | DynamicReportNode) {
|
||||
for (const entry of this.entries) {
|
||||
entry.calculate(this);
|
||||
}
|
||||
}
|
||||
|
||||
byId(id: string): DynamicReportNode | null {
|
||||
// Get the DynamicReportNode with the given ID
|
||||
for (const entry of this.entries) {
|
||||
if (entry.id === id) {
|
||||
return entry;
|
||||
}
|
||||
if (entry instanceof Section) {
|
||||
const result = entry.byId(id);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class Spacer implements DynamicReportNode {
|
||||
id = null;
|
||||
|
||||
calculate(_parent: DynamicReport | DynamicReportNode) {}
|
||||
}
|
||||
|
||||
export class Subtotal extends Entry {
|
||||
constructor(
|
||||
public text: string,
|
||||
public id: string | null = null,
|
||||
public visible = true,
|
||||
public bordered = false,
|
||||
public floor = 0,
|
||||
) {
|
||||
super(text, null!, id, visible, false /* autoHide */, null /* link */, true /* heading */, bordered);
|
||||
}
|
||||
|
||||
calculate(parent: DynamicReport | DynamicReportNode) {
|
||||
// Calculate total amount
|
||||
if (!(parent instanceof Section)) {
|
||||
throw new Error('Attempt to calculate Subtotal not in Section');
|
||||
}
|
||||
|
||||
this.quantity = 0;
|
||||
for (const entry of parent.entries) {
|
||||
if (entry instanceof Entry) {
|
||||
this.quantity += entry.quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user