Implement comparative balance sheet report

This commit is contained in:
RunasSudo 2025-02-23 00:07:47 +11:00
parent 2c0e936db6
commit 4a9a4078ad
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A

View File

@ -1,6 +1,6 @@
<!-- <!--
DrCr: Web-based double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo) Copyright (C) 20222025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -61,7 +61,23 @@
<!-- Report display --> <!-- Report display -->
<template> <template>
<DynamicReportComponent :report="report"> <ComparativeDynamicReportComponent :reports="reports" :labels="reportLabels">
<div class="my-2 py-2 flex">
<div class="grow flex gap-x-2 items-baseline">
<span class="whitespace-nowrap">As at</span>
<input type="date" class="bordered-field" v-model.lazy="dt">
<span>Compare</span>
<div class="relative flex flex-grow items-stretch shadow-sm">
<input type="number" min="1" class="bordered-field w-[9.5em] pr-[6em]" v-model.lazy="comparePeriods">
<div class="absolute inset-y-0 right-0 flex items-center z-10">
<select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-emerald-600" v-model="compareUnit">
<option value="years">years</option>
<option value="months">months</option>
</select>
</div>
</div>
</div>
</div>
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="!doesBalance"> <div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="!doesBalance">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
@ -72,38 +88,105 @@
</div> </div>
</div> </div>
</div> </div>
</DynamicReportComponent> </ComparativeDynamicReportComponent>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid'; import { ExclamationCircleIcon } from '@heroicons/vue/20/solid';
import { Computed, DynamicReport, Entry, Section, Spacer, Subtotal } from './base.ts'; import { Computed, DynamicReport, Entry, Section, Spacer, Subtotal } from './base.ts';
import { IncomeStatementReport} from './IncomeStatementReport.vue'; import { IncomeStatementReport} from './IncomeStatementReport.vue';
import { db } from '../db.ts'; import { db } from '../db.ts';
import DynamicReportComponent from '../components/DynamicReportComponent.vue'; import { ExtendedDatabase } from '../dbutil.ts';
import ComparativeDynamicReportComponent from '../components/ComparativeDynamicReportComponent.vue';
import { ReportingStage, ReportingWorkflow } from '../reporting.ts'; import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
const report = ref(null as BalanceSheetReport | null); const reports = ref([] as BalanceSheetReport[]);
const reportLabels = ref([] as string[]);
const dt = ref(null as string | null);
const comparePeriods = ref(1);
const compareUnit = ref('years');
async function load() { async function load() {
const session = await db.load(); const session = await db.load();
const reportingWorkflow = new ReportingWorkflow();
await reportingWorkflow.generate(session);
report.value = reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, BalanceSheetReport) as BalanceSheetReport; dt.value = db.metadata.eofy_date;
await updateReport(session);
// Update report when dates etc. changed
// We initialise the watcher here only after dt and dtStart are initialised above
watch([dt, comparePeriods, compareUnit], async () => {
const session = await db.load();
await updateReport(session);
});
} }
load(); load();
const doesBalance = computed(function() { async function updateReport(session: ExtendedDatabase) {
if (report.value === null) { const newReportPromises = [];
return true; const newReportLabels = [];
for (let i = 0; i < comparePeriods.value; i++) {
let thisReportDt, thisReportLabel;
// Get period end date
if (compareUnit.value === 'years') {
thisReportDt = dayjs(dt.value!).subtract(i, 'year');
thisReportLabel = dayjs(dt.value!).subtract(i, 'year').format('YYYY');
} else if (compareUnit.value === 'months') {
if (dayjs(dt.value!).add(1, 'day').isSame(dayjs(dt.value!).set('date', 1).add(1, 'month'))) {
// If dt is the end of a calendar month, then fix each prior dt to be the end of the calendar month
thisReportDt = dayjs(dt.value!).subtract(i, 'month').set('date', 1).add(1, 'month').subtract(1, 'day');
} else {
thisReportDt = dayjs(dt.value!).subtract(i, 'month');
} }
const totalAssets = (report.value.byId('total_assets') as Computed).quantity; thisReportLabel = dayjs(dt.value!).subtract(i, 'month').format('YYYY-MM');
const totalLiabilities = (report.value.byId('total_liabilities') as Computed).quantity; } else {
const totalEquity = (report.value.byId('total_equity') as Computed).quantity; throw new Error('Unexpected compareUnit');
return totalAssets === totalLiabilities + totalEquity; }
// Get start of financial year date
let sofyDayjs = dayjs(db.metadata.eofy_date).subtract(1, 'year').add(1, 'day');
let thisReportDtStart = thisReportDt.set('date', sofyDayjs.get('date')).set('month', sofyDayjs.get('month'));
if (thisReportDtStart.isAfter(thisReportDt)) {
thisReportDtStart = thisReportDtStart.subtract(1, 'year');
}
console.log([thisReportDt, thisReportDtStart]);
// Generate reports asynchronously
newReportPromises.push((async () => {
const reportingWorkflow = new ReportingWorkflow();
await reportingWorkflow.generate(session, thisReportDt.format('YYYY-MM-DD'), thisReportDtStart.format('YYYY-MM-DD'));
return reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, BalanceSheetReport) as BalanceSheetReport;
})());
if (comparePeriods.value === 1) {
// If only 1 report, the heading is simply "$"
newReportLabels.push(db.metadata.reporting_commodity);
} else {
newReportLabels.push(thisReportLabel);
}
}
reports.value = await Promise.all(newReportPromises);
reportLabels.value = newReportLabels;
}
const doesBalance = computed(function() {
let doesBalance = true;
for (const report of reports.value) {
const totalAssets = (report.byId('total_assets') as Computed).quantity;
const totalLiabilities = (report.byId('total_liabilities') as Computed).quantity;
const totalEquity = (report.byId('total_equity') as Computed).quantity;
if (totalAssets !== totalLiabilities + totalEquity) {
doesBalance = false;
}
}
return doesBalance;
}); });
</script> </script>