Compare commits
No commits in common. "be6af2fd27130a23f18a388daf97b3fb3ef8625a" and "40945def3ee762fea4f3d892a830f0ebe42c1b69" have entirely different histories.
be6af2fd27
...
40945def3e
@ -27,7 +27,10 @@
|
|||||||
<input type="text" class="bordered-field" id="description" v-model="assertion.description" placeholder=" ">
|
<input type="text" class="bordered-field" id="description" v-model="assertion.description" placeholder=" ">
|
||||||
</div>
|
</div>
|
||||||
<label for="account" class="block text-gray-900 pr-4">Account</label>
|
<label for="account" class="block text-gray-900 pr-4">Account</label>
|
||||||
<ComboBoxAccounts v-model="assertion.account" />
|
<div class="relative combobox">
|
||||||
|
<input type="text" class="bordered-field peer" id="account" v-model="assertion.account" placeholder=" " autocomplete="off">
|
||||||
|
<!--{% include 'components/accounts_combobox_inner.html' %}-->
|
||||||
|
</div>
|
||||||
<label for="amount" class="block text-gray-900 pr-4">Balance</label>
|
<label for="amount" class="block text-gray-900 pr-4">Balance</label>
|
||||||
<div class="relative shadow-sm">
|
<div class="relative shadow-sm">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
@ -58,7 +61,6 @@
|
|||||||
import { defineProps } from 'vue';
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
||||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
|
||||||
|
|
||||||
export interface EditingAssertion {
|
export interface EditingAssertion {
|
||||||
id: number | null,
|
id: number | null,
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
||||||
<div class="relative">
|
|
||||||
<!-- WebKit bug: Does not align baseline correctly unless some text or placeholder is present -->
|
|
||||||
<input type="text" class="bordered-field peer" :class="inputClass" id="account" v-model="selectedValue" placeholder=" " autocomplete="off" ref="inputField">
|
|
||||||
<button type="button" class="absolute inset-y-0 right-0 flex items-center px-2 focus:outline-none" @click="inputField!.focus()">
|
|
||||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
<ul class="hidden peer-focus:block absolute z-20 mt-1 max-h-60 w-full overflow-auto bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" v-if="values.length > 0">
|
|
||||||
<li
|
|
||||||
v-for="value in values"
|
|
||||||
v-show="value.toLowerCase().startsWith(selectedValue.toLowerCase())"
|
|
||||||
class="group relative cursor-default select-none py-1 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-emerald-600 wk-aa"
|
|
||||||
:data-selected="value === selectedValue ? 'selected': ''"
|
|
||||||
@mousedown="selectedValue = value"
|
|
||||||
>
|
|
||||||
<span class="block truncate group-data-[selected=selected]:font-semibold">{{ value }}</span>
|
|
||||||
<span class="hidden group-data-[selected=selected]:flex absolute inset-y-0 right-0 items-center pr-4 text-emerald-600 group-hover:text-white">
|
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
|
||||||
import { defineModel, defineProps, useTemplateRef } from 'vue';
|
|
||||||
|
|
||||||
const { values, inputClass } = defineProps<{ values: string[], inputClass?: string }>();
|
|
||||||
const inputField = useTemplateRef('inputField');
|
|
||||||
|
|
||||||
const selectedValue = defineModel({ default: '' });
|
|
||||||
</script>
|
|
@ -1,46 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
||||||
<ComboBox :values="accounts" :inputClass="inputClass" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { defineProps, ref } from 'vue';
|
|
||||||
|
|
||||||
import { db } from '../db.ts';
|
|
||||||
import ComboBox from './ComboBox.vue';
|
|
||||||
|
|
||||||
const { inputClass } = defineProps<{ inputClass?: string }>();
|
|
||||||
|
|
||||||
const accounts = ref([] as string[]);
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
// Load account names
|
|
||||||
const session = await db.load();
|
|
||||||
|
|
||||||
const rawAccounts: {account: string}[] = await session.select(
|
|
||||||
`SELECT DISTINCT account
|
|
||||||
FROM postings
|
|
||||||
ORDER BY account`
|
|
||||||
);
|
|
||||||
|
|
||||||
accounts.value = rawAccounts.map((a) => a.account);
|
|
||||||
}
|
|
||||||
load();
|
|
||||||
</script>
|
|
@ -1,49 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
<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>
|
|
@ -1,49 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -29,21 +29,15 @@
|
|||||||
|
|
||||||
<!-- Menu items -->
|
<!-- Menu items -->
|
||||||
<div v-if="db.filename !== null" class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-4">
|
<div v-if="db.filename !== null" class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-4">
|
||||||
<RouterLink :to="{ name: 'journal' }" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
<!--<a href="#" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
||||||
Journal
|
Journal
|
||||||
</RouterLink>
|
</a>-->
|
||||||
<RouterLink :to="{ name: 'statement-lines' }" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
<!--<a href="#" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
||||||
Statement lines
|
Statement lines
|
||||||
</RouterLink>
|
</a>-->
|
||||||
<RouterLink :to="{ name: 'trial-balance'}" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
<RouterLink to="/trial-balance" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
||||||
Trial balance
|
Trial balance
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink :to="{ name: 'balance-sheet'}" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
|
||||||
Balance sheet
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink :to="{ name: 'income-statement'}" class="border-transparent text-gray-700 hover:border-emerald-500 hover:text-emerald-700 inline-flex items-center border-b-2 px-1 pt-1 text-sm">
|
|
||||||
Income statement
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +49,10 @@
|
|||||||
<option value="cr">Cr</option>
|
<option value="cr">Cr</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<ComboBoxAccounts v-model="posting.account" class="w-full" inputClass="pl-16" />
|
<div class="relative combobox w-full">
|
||||||
|
<input type="text" class="bordered-field pl-16 peer" v-model="posting.account">
|
||||||
|
<!-- TODO: Accounts combobox -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="relative -ml-px px-2 py-2 text-gray-500 hover:text-gray-700" @click="addPosting(posting)">
|
<button class="relative -ml-px px-2 py-2 text-gray-500 hover:text-gray-700" @click="addPosting(posting)">
|
||||||
<PlusIcon class="w-4 h-4" />
|
<PlusIcon class="w-4 h-4" />
|
||||||
@ -109,7 +112,6 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { DT_FORMAT, Transaction, db, deserialiseAmount } from '../db.ts';
|
import { DT_FORMAT, Transaction, db, deserialiseAmount } from '../db.ts';
|
||||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
|
||||||
|
|
||||||
interface EditingPosting {
|
interface EditingPosting {
|
||||||
id: number | null,
|
id: number | null,
|
||||||
|
12
src/db.ts
12
src/db.ts
@ -180,18 +180,6 @@ export function joinedToTransactions(joinedTransactionPostings: JoinedTransactio
|
|||||||
return transactions;
|
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 {
|
export function serialiseAmount(quantity: number, commodity: string): string {
|
||||||
// Pretty print the amount for an editable input
|
// Pretty print the amount for an editable input
|
||||||
if (quantity < 0) {
|
if (quantity < 0) {
|
||||||
|
@ -39,22 +39,3 @@ export function ppWithCommodity(quantity: number, commodity: string): string {
|
|||||||
return pp(quantity) + ' ' + commodity;
|
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) + '" class="hover:text-blue-700 hover:underline">' + text + '</a>' + space;
|
|
||||||
} else {
|
|
||||||
return text + space;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -33,10 +33,8 @@ 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: '/journal', name: 'journal', component: () => import('./pages/JournalView.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/edit/:id', name: 'journal-edit-transaction', component: () => import('./pages/EditTransactionView.vue') },
|
||||||
{ path: '/journal/new', name: 'journal-new-transaction', component: () => import('./pages/NewTransactionView.vue') },
|
{ path: '/journal/new', name: 'journal-new-transaction', component: () => import('./pages/NewTransactionView.vue') },
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
const reportingWorkflow = new ReportingWorkflow();
|
const reportingWorkflow = new ReportingWorkflow();
|
||||||
await reportingWorkflow.generate(session);
|
await reportingWorkflow.generate(session);
|
||||||
|
|
||||||
transactions.value = reportingWorkflow.getTransactionsAtStage(ReportingStage.FINAL_STAGE);
|
transactions.value = reportingWorkflow.getTransactionsAtStage(ReportingStage.OrdinaryAPITransactions);
|
||||||
|
|
||||||
// Display transactions in reverse chronological order
|
// Display transactions in reverse chronological order
|
||||||
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
||||||
|
@ -33,8 +33,8 @@
|
|||||||
<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><RouterLink :to="{ name: 'balance-sheet' }" class="text-gray-900 hover:text-blue-700 hover:underline">Balance sheet</RouterLink></li>
|
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Balance sheet</a></li>-->
|
||||||
<li><RouterLink :to="{ name: 'income-statement' }" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</RouterLink></li>
|
<!--<li><a href="#" class="text-gray-900 hover:text-blue-700 hover:underline">Income statement</a></li>-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
|
@ -30,7 +30,11 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="account" class="block text-gray-900 pr-4">Source account</label>
|
<label for="account" class="block text-gray-900 pr-4">Source account</label>
|
||||||
<ComboBoxAccounts v-model="sourceAccount" />
|
<div class="relative combobox">
|
||||||
|
<!-- WebKit bug: Does not align baseline correctly unless some text or placeholder is present -->
|
||||||
|
<input type="text" class="bordered-field peer" id="account" placeholder=" " autocomplete="off" v-model="sourceAccount">
|
||||||
|
<!--{% include 'components/accounts_combobox_inner.html' %}-->
|
||||||
|
</div>
|
||||||
<label for="file" class="block text-gray-900 pr-4">File</label>
|
<label for="file" class="block text-gray-900 pr-4">File</label>
|
||||||
<div class="flex grow">
|
<div class="flex grow">
|
||||||
<!-- WebKit: file:hidden hides the filename as well so we have a dummy text input -->
|
<!-- WebKit: file:hidden hides the filename as well so we have a dummy text input -->
|
||||||
@ -83,7 +87,6 @@
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { StatementLine, db } from '../db.ts';
|
import { StatementLine, db } from '../db.ts';
|
||||||
import ComboBoxAccounts from '../components/ComboBoxAccounts.vue';
|
|
||||||
import { ppWithCommodity } from '../display.ts';
|
import { ppWithCommodity } from '../display.ts';
|
||||||
import import_ofx2 from '../importers/ofx2.ts';
|
import import_ofx2 from '../importers/ofx2.ts';
|
||||||
|
|
||||||
|
@ -58,20 +58,14 @@
|
|||||||
const reportingWorkflow = new ReportingWorkflow();
|
const reportingWorkflow = new ReportingWorkflow();
|
||||||
await reportingWorkflow.generate(session); // This also ensures running balances are up to date
|
await reportingWorkflow.generate(session); // This also ensures running balances are up to date
|
||||||
|
|
||||||
const transactionsRaw = reportingWorkflow.getTransactionsAtStage(ReportingStage.FINAL_STAGE);
|
const transactionsRaw = reportingWorkflow.getTransactionsAtStage(ReportingStage.OrdinaryAPITransactions);
|
||||||
|
|
||||||
// Filter only transactions affecting this account
|
// Filter only transactions affecting this account
|
||||||
const filteredTransactions = transactionsRaw.filter((t) => t.postings.some((p) => p.account === route.params.account));
|
transactions.value = transactionsRaw.filter((t) => t.postings.some((p) => p.account === route.params.account));
|
||||||
|
|
||||||
// In order to correctly sort API transactions, we need to remember their indexes
|
// Display transactions in reverse chronological order
|
||||||
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
|
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
||||||
// Use Number.MAX_SAFE_INTEGER as ID for API transactions
|
transactions.value.sort((a, b) => (b.dt.localeCompare(a.dt)) || ((b.id ?? 0) - (a.id ?? 0)));
|
||||||
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();
|
load();
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,8 +53,7 @@
|
|||||||
|
|
||||||
import { db } from '../db.ts';
|
import { db } from '../db.ts';
|
||||||
import { pp } from '../display.ts';
|
import { pp } from '../display.ts';
|
||||||
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
|
import { ReportingStage, ReportingWorkflow, TrialBalanceReport } from '../reporting.ts';
|
||||||
import TrialBalanceReport from '../reports/TrialBalanceReport.ts';
|
|
||||||
|
|
||||||
const report = ref(null as TrialBalanceReport | null);
|
const report = ref(null as TrialBalanceReport | null);
|
||||||
|
|
||||||
@ -73,7 +72,7 @@
|
|||||||
const reportingWorkflow = new ReportingWorkflow();
|
const reportingWorkflow = new ReportingWorkflow();
|
||||||
await reportingWorkflow.generate(session);
|
await reportingWorkflow.generate(session);
|
||||||
|
|
||||||
report.value = reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, TrialBalanceReport) as TrialBalanceReport;
|
report.value = reportingWorkflow.getReportAtStage(ReportingStage.OrdinaryAPITransactions, TrialBalanceReport) as TrialBalanceReport;
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
|
130
src/reporting.ts
130
src/reporting.ts
@ -16,45 +16,21 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { asCost } from './amounts.ts';
|
import { asCost } from './amounts.ts';
|
||||||
import { DT_FORMAT, JoinedTransactionPosting, StatementLine, Transaction, db, getAccountsForKind, joinedToTransactions, totalBalances, totalBalancesAtDate } from './db.ts';
|
import { JoinedTransactionPosting, StatementLine, Transaction, 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 TrialBalanceReport from './reports/TrialBalanceReport.ts';
|
|
||||||
import { IncomeStatementReport } from './reports/IncomeStatementReport.vue';
|
|
||||||
|
|
||||||
export enum ReportingStage {
|
export enum ReportingStage {
|
||||||
// Load transactions from database
|
// Load transactions from database
|
||||||
TransactionsFromDatabase = 100,
|
TransactionsFromDatabase = 100,
|
||||||
|
|
||||||
// Load unreconciled statement lines and other ordinary API transactions
|
// Load unreconciled statement lines and other ordinary API transactions
|
||||||
OrdinaryAPITransactions = 200,
|
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 = BalanceSheet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReportingWorkflow {
|
export class ReportingWorkflow {
|
||||||
transactionsForStage: Map<ReportingStage, Transaction[]> = new Map();
|
transactionsForStage: Map<ReportingStage, Transaction[]> = new Map();
|
||||||
reportsForStage: Map<ReportingStage, DrcrReport[]> = new Map();
|
reportsForStage: Map<ReportingStage, Report[]> = new Map();
|
||||||
|
|
||||||
async generate(session: ExtendedDatabase, dt?: string) {
|
async generate(session: ExtendedDatabase, dt?: string) {
|
||||||
// ------------------------
|
// ------------------------
|
||||||
@ -147,81 +123,9 @@ export class ReportingWorkflow {
|
|||||||
balances = applyTransactionsToBalances(balances, transactions);
|
balances = applyTransactionsToBalances(balances, transactions);
|
||||||
this.reportsForStage.set(ReportingStage.OrdinaryAPITransactions, [new TrialBalanceReport(balances)]);
|
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
|
|
||||||
|
|
||||||
let incomeStatementReport;
|
|
||||||
{
|
|
||||||
incomeStatementReport = new IncomeStatementReport();
|
|
||||||
await incomeStatementReport.generate(balances);
|
|
||||||
this.reportsForStage.set(ReportingStage.InterimIncomeStatement, [incomeStatementReport]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------
|
|
||||||
// BalanceSheet
|
|
||||||
|
|
||||||
{
|
|
||||||
const balanceSheetReport = new BalanceSheetReport();
|
|
||||||
await balanceSheetReport.generate(balances, incomeStatementReport);
|
|
||||||
this.reportsForStage.set(ReportingStage.BalanceSheet, [balanceSheetReport]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getReportAtStage(stage: ReportingStage, reportType: any): DrcrReport {
|
getReportAtStage(stage: ReportingStage, reportType: any): Report {
|
||||||
// TODO: This function needs generics
|
// TODO: This function needs generics
|
||||||
const reportsForTheStage = this.reportsForStage.get(stage);
|
const reportsForTheStage = this.reportsForStage.get(stage);
|
||||||
if (!reportsForTheStage) {
|
if (!reportsForTheStage) {
|
||||||
@ -229,24 +133,11 @@ export class ReportingWorkflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const report = reportsForTheStage.find((r) => r instanceof reportType);
|
const report = reportsForTheStage.find((r) => r instanceof reportType);
|
||||||
if (report) {
|
if (!report) {
|
||||||
return report;
|
throw new Error('Report does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse earlier stages
|
return report;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Report does not exist at requested stage or any earlier stage');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactionsAtStage(stage: ReportingStage): Transaction[] {
|
getTransactionsAtStage(stage: ReportingStage): Transaction[] {
|
||||||
@ -260,6 +151,15 @@ 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> {
|
function applyTransactionsToBalances(balances: Map<string, number>, transactions: Transaction[]): Map<string, number> {
|
||||||
// Initialise new balances
|
// Initialise new balances
|
||||||
const newBalances: Map<string, number> = new Map([...balances.entries()]);
|
const newBalances: Map<string, number> = new Map([...balances.entries()]);
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,81 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,25 +0,0 @@
|
|||||||
/*
|
|
||||||
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>
|
|
||||||
) {}
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
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,
|
|
||||||
null /* id */,
|
|
||||||
true /* visible */,
|
|
||||||
false /* autoHide */,
|
|
||||||
'/transactions/' + account
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && entry !== this) {
|
|
||||||
this.quantity += entry.quantity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user