Trial balance using libdrcr

This commit is contained in:
RunasSudo 2025-05-27 22:20:33 +10:00
parent c3a407b048
commit 51a40e5ed9
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 117 additions and 168 deletions

View File

@ -92,6 +92,7 @@ pub fn run() {
set_open_filename,
libdrcr_bridge::get_balance_sheet,
libdrcr_bridge::get_income_statement,
libdrcr_bridge::get_trial_balance,
sql::sql_transaction_begin,
sql::sql_transaction_execute,
sql::sql_transaction_select,

View File

@ -33,10 +33,9 @@ use tokio::sync::Mutex;
use crate::AppState;
#[tauri::command]
pub(crate) async fn get_balance_sheet(
async fn get_dynamic_report(
state: State<'_, Mutex<AppState>>,
dates: Vec<String>,
target: ReportingProductId,
) -> Result<String, ()> {
let state = state.lock().await;
let db_filename = state.db_filename.clone().unwrap();
@ -51,19 +50,37 @@ pub(crate) async fn get_balance_sheet(
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
// Get balance sheet
let mut date_args = Vec::new();
for date in dates.iter() {
date_args.push(DateArgs {
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
})
}
// Get dynamic report
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
},
target.clone(),
];
let products = generate_report(targets, Arc::new(context)).await.unwrap();
let result = products.get_or_err(&target).unwrap();
let dynamic_report = result.downcast_ref::<DynamicReport>().unwrap().to_json();
Ok(dynamic_report)
}
#[tauri::command]
pub(crate) async fn get_balance_sheet(
state: State<'_, Mutex<AppState>>,
dates: Vec<String>,
) -> Result<String, ()> {
let mut date_args = Vec::new();
for date in dates.iter() {
date_args.push(DateArgs {
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").expect("Invalid date"),
})
}
get_dynamic_report(
state,
ReportingProductId {
name: "BalanceSheet",
kind: ReportingProductKind::Generic,
@ -71,21 +88,8 @@ pub(crate) async fn get_balance_sheet(
dates: date_args.clone(),
}),
},
];
// Run report
let products = generate_report(targets, Arc::new(context)).await.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "BalanceSheet",
kind: ReportingProductKind::Generic,
args: Box::new(MultipleDateArgs { dates: date_args }),
})
.unwrap();
let balance_sheet = result.downcast_ref::<DynamicReport>().unwrap().to_json();
Ok(balance_sheet)
)
.await
}
#[tauri::command]
@ -93,33 +97,16 @@ pub(crate) async fn get_income_statement(
state: State<'_, Mutex<AppState>>,
dates: Vec<(String, String)>,
) -> Result<String, ()> {
let state = state.lock().await;
let db_filename = state.db_filename.clone().unwrap();
// Connect to database
let db_connection =
DbConnection::new(format!("sqlite:{}", db_filename.as_str()).as_str()).await;
// Initialise ReportingContext
let eofy_date = db_connection.metadata().eofy_date;
let mut context = ReportingContext::new(db_connection, eofy_date, "$".to_string());
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
// Get income statement
let mut date_args = Vec::new();
for (date_start, date_end) in dates.iter() {
date_args.push(DateStartDateEndArgs {
date_start: NaiveDate::parse_from_str(date_start, "%Y-%m-%d").unwrap(),
date_end: NaiveDate::parse_from_str(date_end, "%Y-%m-%d").unwrap(),
date_start: NaiveDate::parse_from_str(date_start, "%Y-%m-%d").expect("Invalid date"),
date_end: NaiveDate::parse_from_str(date_end, "%Y-%m-%d").expect("Invalid date"),
})
}
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
},
get_dynamic_report(
state,
ReportingProductId {
name: "IncomeStatement",
kind: ReportingProductKind::Generic,
@ -127,19 +114,24 @@ pub(crate) async fn get_income_statement(
dates: date_args.clone(),
}),
},
];
// Run report
let products = generate_report(targets, Arc::new(context)).await.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "IncomeStatement",
kind: ReportingProductKind::Generic,
args: Box::new(MultipleDateStartDateEndArgs { dates: date_args }),
})
.unwrap();
let income_statement = result.downcast_ref::<DynamicReport>().unwrap().to_json();
Ok(income_statement)
)
.await
}
#[tauri::command]
pub(crate) async fn get_trial_balance(
state: State<'_, Mutex<AppState>>,
date: String,
) -> Result<String, ()> {
let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date");
get_dynamic_report(
state,
ReportingProductId {
name: "TrialBalance",
kind: ReportingProductKind::Generic,
args: Box::new(DateArgs { date }),
},
)
.await
}

View File

@ -1,6 +1,6 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
Copyright (C) 2022-2025 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
@ -43,7 +43,7 @@ async function initApp() {
{ path: '/statement-lines', name: 'statement-lines', component: () => import('./pages/StatementLinesView.vue') },
{ path: '/statement-lines/import', name: 'import-statement', component: () => import('./pages/ImportStatementView.vue') },
{ path: '/transactions/:account', name: 'transactions', component: () => import('./pages/TransactionsView.vue') },
{ path: '/trial-balance', name: 'trial-balance', component: () => import('./pages/TrialBalanceView.vue') },
{ path: '/trial-balance', name: 'trial-balance', component: () => import('./reports/TrialBalanceReport.vue') },
];
const router = createRouter({
history: createWebHistory(),

View File

@ -1,79 +0,0 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 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>
<h1 class="page-heading mb-4">
Trial balance
</h1>
<table class="min-w-full" v-if="report">
<thead>
<tr class="border-b border-gray-300">
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">Account</th>
<th class="py-0.5 px-1 text-gray-900 font-semibold text-end">Dr</th>
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-end">Cr</th>
</tr>
</thead>
<tbody>
<tr v-for="[account, quantity] in report.balances.entries()">
<td class="py-0.5 pr-1 text-gray-900"><RouterLink :to="{ name: 'transactions', params: { account: account } }" class="hover:text-blue-700 hover:underline">{{ account }}</RouterLink></td>
<td class="py-0.5 px-1 text-gray-900 text-end">
<template v-if="quantity >= 0">{{ pp(quantity) }}</template>
</td>
<td class="py-0.5 pl-1 text-gray-900 text-end">
<template v-if="quantity < 0">{{ pp(-quantity) }}</template>
</td>
</tr>
<tr>
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">Total</th>
<th class="py-0.5 px-1 text-gray-900 text-end">{{ pp(total_dr!) }}</th>
<th class="py-0.5 pl-1 text-gray-900 text-end">{{ pp(-total_cr!) }}</th>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { db } from '../db.ts';
import { pp } from '../display.ts';
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
import TrialBalanceReport from '../reports/TrialBalanceReport.ts';
const report = ref(null as TrialBalanceReport | null);
// WebKit: Iterator.reduce not supported - https://bugs.webkit.org/show_bug.cgi?id=248650
const total_dr = computed(() => report.value ?
[...report.value.balances.values()].reduce((acc, x) => x > 0 ? acc + x : acc, 0)
: 0
);
const total_cr = computed(() => report.value ?
[...report.value.balances.values()].reduce((acc, x) => x < 0 ? acc + x : acc, 0)
: 0
);
async function load() {
const session = await db.load();
const reportingWorkflow = new ReportingWorkflow();
await reportingWorkflow.generate(session);
report.value = reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, TrialBalanceReport) as TrialBalanceReport;
}
load();
</script>

View File

@ -1,25 +0,0 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 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>
) {}
}

View File

@ -0,0 +1,60 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 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>
<DynamicReportComponent :report="report">
<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">
</div>
</div>
</DynamicReportComponent>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { invoke } from '@tauri-apps/api/core';
import { ref, watch } from 'vue';
import { DynamicReport } from './base.ts';
import { db } from '../db.ts';
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
const report = ref(null as DynamicReport | null);
const dt = ref(null as string | null);
async function load() {
await db.load();
dt.value = db.metadata.eofy_date;
await updateReport();
// Update report when dates etc. changed
// We initialise the watcher here only after dt is initialised above
watch([dt], updateReport);
}
load();
async function updateReport() {
const reportDate = dayjs(dt.value!).format('YYYY-MM-DD');
report.value = JSON.parse(await invoke('get_trial_balance', { date: reportDate }));
}
</script>