Basic implementation of tax summary report

This commit is contained in:
RunasSudo 2025-05-30 21:49:31 +10:00
parent 49dc6bc078
commit 315ff158c3
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
11 changed files with 797 additions and 136 deletions

661
libdrcr/src/austax/mod.rs Normal file
View File

@ -0,0 +1,661 @@
/*
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/>.
*/
//! Implements Australian individual income tax calculations
// TODO: Ideally this would be separated into its own plugin
use std::collections::HashMap;
use std::fmt::Display;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::account_config::kinds_for_account;
use crate::reporting::calculator::ReportingGraphDependencies;
use crate::reporting::dynamic_report::{
entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry,
CalculatableSection, CalculatedRow, DynamicReport, LiteralRow,
};
use crate::reporting::executor::ReportingExecutionError;
use crate::reporting::steps::AllTransactionsExceptEarningsToEquityBalances;
use crate::reporting::types::{
BalancesBetween, DateStartDateEndArgs, ReportingContext, ReportingProductId,
ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId,
Transactions, VoidArgs,
};
use crate::util::sofy_from_eofy;
use crate::QuantityInt;
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
pub fn register_lookup_fns(context: &mut ReportingContext) {
CalculateIncomeTax::register_lookup_fn(context);
}
/// Calculates income tax
///
/// [Transactions] product represents income tax charge for the year.
/// [DynamicReport] product represents the tax summary report.
#[derive(Debug)]
pub struct CalculateIncomeTax {}
impl CalculateIncomeTax {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CalculateIncomeTax",
&[ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<VoidArgs>()
}
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(CalculateIncomeTax {})
}
}
impl Display for CalculateIncomeTax {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for CalculateIncomeTax {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CalculateIncomeTax",
product_kinds: &[
ReportingProductKind::DynamicReport,
ReportingProductKind::Transactions,
],
args: Box::new(VoidArgs {}),
}
}
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
// CalculateIncomeTax depends on CombineOrdinaryTransactions
vec![ReportingProductId {
name: "CombineOrdinaryTransactions",
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date),
date_end: context.eofy_date.clone(),
}),
}]
}
fn after_init_graph(
&self,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
for other in steps {
if let Some(other) =
other.downcast_ref::<AllTransactionsExceptEarningsToEquityBalances>()
{
// AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
dependencies.add_dependency(
other.id(),
ReportingProductId {
name: self.id().name,
kind: other.product_kinds[0],
args: if other.product_kinds[0] == ReportingProductKind::Transactions {
Box::new(VoidArgs {})
} else {
other.id().args
},
},
);
}
}
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get balances for current year
let balances = &products
.get_or_err(&ReportingProductId {
name: "CombineOrdinaryTransactions",
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date),
date_end: context.eofy_date.clone(),
}),
})?
.downcast_ref::<BalancesBetween>()
.unwrap()
.balances;
// Get taxable income and deduction accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await);
// Generate tax summary report
let report = CalculatableDynamicReport::new(
"Tax summary".to_string(),
vec!["$".to_string()],
vec![
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Salary or wages (1)".to_string(),
Some("income1".to_string()),
true,
true,
{
let mut entries = entries_for_kind_floor(
"austax.income1",
true,
&vec![balances],
&kinds_for_account,
100,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 1".to_string(),
quantity: report.subtotal_for_id("income1").unwrap(),
id: Some("total_income1".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
// Add spacer as child of the Section so it is hidden if the Section is hidden
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Australian Government allowances and payments (5)".to_string(),
Some("income5".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.income5",
true,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 5".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("income5").unwrap(),
100,
),
id: Some("total_income5".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Gross interest (10)".to_string(),
Some("income10".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.income10",
true,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 10".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("income10").unwrap(),
100,
),
id: Some("total_income10".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Partnerships and trusts (13)".to_string(),
Some("income13".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.income13",
true,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 13".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("income13").unwrap(),
100,
),
id: Some("total_income13".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Foreign source income and foreign assets or property (20)".to_string(),
Some("income20".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.income20",
true,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 20".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("income20").unwrap(),
100,
),
id: Some("total_income20".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Other income (24)".to_string(),
Some("income24".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.income24",
true,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item 24".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("income24").unwrap(),
100,
),
id: Some("total_income24".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total assessable income".to_string(),
quantity: vec![
report
.quantity_for_id("total_income1")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_income5")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_income10")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_income13")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_income20")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_income24")
.map(|v| v[0])
.unwrap_or(0),
],
id: Some("total_income".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}),
CalculatableDynamicReportEntry::Spacer,
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Work-related travel expenses (D2)".to_string(),
Some("d2".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.d2",
false,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item D2".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("d2").unwrap(),
100,
),
id: Some("total_d2".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Work-related self-education expenses (D4)".to_string(),
Some("d4".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.d4",
false,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item D4".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("d4").unwrap(),
100,
),
id: Some("total_d4".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Other work-related expenses (D5)".to_string(),
Some("d5".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.d5",
false,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item D5".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("d5").unwrap(),
100,
),
id: Some("total_d5".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Gifts or donations (D9)".to_string(),
Some("d9".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.d9",
false,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item D9".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("d9").unwrap(),
100,
),
id: Some("total_d9".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Other deductions (D15)".to_string(),
Some("d15".to_string()),
true,
true,
{
let mut entries = entries_for_kind(
"austax.d15",
false,
&vec![balances],
&kinds_for_account,
);
entries.push(CalculatableDynamicReportEntry::CalculatedRow(
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total item D15".to_string(),
quantity: floor_quantity(
report.subtotal_for_id("d15").unwrap(),
100,
),
id: Some("total_d15".to_string()),
visible: true,
auto_hide: true,
link: None,
heading: true,
bordered: true,
},
},
));
entries.push(CalculatableDynamicReportEntry::Spacer);
entries
},
)),
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total deductions".to_string(),
quantity: vec![
report
.quantity_for_id("total_d2")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_d4")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_d5")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_d9")
.map(|v| v[0])
.unwrap_or(0) + report
.quantity_for_id("total_d15")
.map(|v| v[0])
.unwrap_or(0),
],
id: Some("total_deductions".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}),
CalculatableDynamicReportEntry::Spacer,
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Net taxable income".to_string(),
quantity: vec![
report.quantity_for_id("total_income").unwrap()[0]
- report.quantity_for_id("total_deductions").unwrap()[0],
],
id: Some("net_taxable".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}),
],
);
let mut report: DynamicReport = report.calculate();
report.auto_hide();
// Generate income tax transaction
let transactions = Transactions {
transactions: Vec::new(), // FIXME
};
// Store products
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
},
Box::new(transactions),
);
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}),
},
Box::new(report),
);
Ok(result)
}
}
/// Call [entries_for_kind] then round results down to next multiple of `floor`
fn entries_for_kind_floor(
kind: &str,
invert: bool,
balances: &Vec<&HashMap<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>,
floor: QuantityInt,
) -> Vec<CalculatableDynamicReportEntry> {
let mut entries_for_kind = entries_for_kind(kind, invert, balances, kinds_for_account);
entries_for_kind.iter_mut().for_each(|e| match e {
CalculatableDynamicReportEntry::LiteralRow(row) => row
.quantity
.iter_mut()
.for_each(|v| *v = (*v / floor) * floor),
_ => unreachable!(),
});
entries_for_kind
}
fn floor_quantity(mut quantity: Vec<QuantityInt>, floor: QuantityInt) -> Vec<QuantityInt> {
quantity.iter_mut().for_each(|v| *v = (*v / floor) * floor);
quantity
}

View File

@ -1,4 +1,5 @@
pub mod account_config;
pub mod austax;
pub mod db;
pub mod model;
pub mod reporting;

View File

@ -20,11 +20,9 @@ use std::sync::Arc;
use chrono::NaiveDate;
use libdrcr::db::DbConnection;
use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets};
use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::register_lookup_fns;
use libdrcr::reporting::types::{
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs,
@ -43,8 +41,9 @@ async fn main() {
NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
"$".to_string(),
);
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
libdrcr::reporting::steps::register_lookup_fns(&mut context);
libdrcr::reporting::builders::register_dynamic_builders(&mut context);
libdrcr::austax::register_lookup_fns(&mut context);
let context = Arc::new(context);
@ -109,6 +108,18 @@ async fn main() {
let products = generate_report(targets, Arc::clone(&context))
.await
.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}),
})
.unwrap();
println!("Tax summary:");
println!("{:?}", result);
let result = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",

View File

@ -155,22 +155,28 @@ impl CalculatableDynamicReport {
}
/// Calculate the subtotals for the [Section] with the given id
pub fn subtotal_for_id(&self, id: &str) -> Vec<QuantityInt> {
let entry = self.by_id(id).expect("Invalid id");
if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry {
section.subtotal(&self)
pub fn subtotal_for_id(&self, id: &str) -> Option<Vec<QuantityInt>> {
if let Some(entry) = self.by_id(id) {
if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry {
Some(section.subtotal(&self))
} else {
panic!("Called subtotal_for_id on non-Section");
}
} else {
panic!("Called subtotal_for_id on non-Section");
None
}
}
// Return the quantities for the [LiteralRow] with the given id
pub fn quantity_for_id(&self, id: &str) -> Vec<QuantityInt> {
let entry = self.by_id(id).expect("Invalid id");
if let CalculatableDynamicReportEntry::LiteralRow(row) = entry {
row.quantity
pub fn quantity_for_id(&self, id: &str) -> Option<Vec<QuantityInt>> {
if let Some(entry) = self.by_id(id) {
if let CalculatableDynamicReportEntry::LiteralRow(row) = entry {
Some(row.quantity)
} else {
panic!("Called quantity_for_id on non-LiteralRow");
}
} else {
panic!("Called quantity_for_id on non-LiteralRow");
None
}
}
}
@ -296,7 +302,9 @@ impl CalculatableSection {
calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row));
}
CalculatableDynamicReportEntry::Spacer => (),
CalculatableDynamicReportEntry::Spacer => {
calculated_entries.push(DynamicReportEntry::Spacer);
}
}
}

View File

@ -51,7 +51,6 @@ pub fn register_lookup_fns(context: &mut ReportingContext) {
AllTransactionsExceptEarningsToEquityBalances::register_lookup_fn(context);
AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context);
BalanceSheet::register_lookup_fn(context);
CalculateIncomeTax::register_lookup_fn(context);
CombineOrdinaryTransactions::register_lookup_fn(context);
CombineOrdinaryTransactionsBalances::register_lookup_fn(context);
CurrentYearEarningsToEquity::register_lookup_fn(context);
@ -474,7 +473,7 @@ impl ReportingStep for BalanceSheet {
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total assets".to_string(),
quantity: report.subtotal_for_id("assets"),
quantity: report.subtotal_for_id("assets").unwrap(),
id: Some("total_assets".to_string()),
visible: true,
auto_hide: false,
@ -500,7 +499,7 @@ impl ReportingStep for BalanceSheet {
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total liabilities".to_string(),
quantity: report.subtotal_for_id("liabilities"),
quantity: report.subtotal_for_id("liabilities").unwrap(),
id: Some("total_liabilities".to_string()),
visible: true,
auto_hide: false,
@ -526,7 +525,7 @@ impl ReportingStep for BalanceSheet {
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total equity".to_string(),
quantity: report.subtotal_for_id("equity"),
quantity: report.subtotal_for_id("equity").unwrap(),
id: Some("total_equity".to_string()),
visible: true,
auto_hide: false,
@ -559,110 +558,6 @@ impl ReportingStep for BalanceSheet {
}
}
/// Calculates income tax
#[derive(Debug)]
pub struct CalculateIncomeTax {}
impl CalculateIncomeTax {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CalculateIncomeTax",
&[ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<VoidArgs>()
}
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(CalculateIncomeTax {})
}
}
impl Display for CalculateIncomeTax {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for CalculateIncomeTax {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CalculateIncomeTax",
product_kinds: &[ReportingProductKind::Transactions],
args: Box::new(VoidArgs {}),
}
}
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
// CalculateIncomeTax depends on CombineOrdinaryTransactions
vec![ReportingProductId {
name: "CombineOrdinaryTransactions",
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date),
date_end: context.eofy_date.clone(),
}),
}]
}
fn after_init_graph(
&self,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
for other in steps {
if let Some(other) =
other.downcast_ref::<AllTransactionsExceptEarningsToEquityBalances>()
{
// AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
dependencies.add_dependency(
other.id(),
ReportingProductId {
name: self.id().name,
kind: other.product_kinds[0],
args: if other.product_kinds[0] == ReportingProductKind::Transactions {
Box::new(VoidArgs {})
} else {
other.id().args
},
},
);
}
}
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> {
eprintln!("Stub: CalculateIncomeTax.execute");
let transactions = Transactions {
transactions: Vec::new(),
};
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
},
Box::new(transactions),
);
Ok(result)
}
}
/// Combines all steps producing ordinary transactions (returns transaction list)
///
/// By default, these are [DBTransactions] and [PostUnreconciledStatementLines].
@ -1214,7 +1109,7 @@ impl ReportingStep for IncomeStatement {
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total income".to_string(),
quantity: report.subtotal_for_id("income"),
quantity: report.subtotal_for_id("income").unwrap(),
id: Some("total_income".to_string()),
visible: true,
auto_hide: false,
@ -1240,7 +1135,7 @@ impl ReportingStep for IncomeStatement {
CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total expenses".to_string(),
quantity: report.subtotal_for_id("expenses"),
quantity: report.subtotal_for_id("expenses").unwrap(),
id: Some("total_expenses".to_string()),
visible: true,
auto_hide: false,
@ -1258,9 +1153,10 @@ impl ReportingStep for IncomeStatement {
calculate_fn: |report| LiteralRow {
text: "Net surplus (deficit)".to_string(),
quantity: report
.quantity_for_id("total_income") // Get total income row
.quantity_for_id("total_income")
.unwrap() // Get total income row
.iter()
.zip(report.quantity_for_id("total_expenses").iter()) // Zip with total expenses row
.zip(report.quantity_for_id("total_expenses").unwrap().iter()) // Zip with total expenses row
.map(|(i, e)| i - e) // Compute net surplus
.collect(),
id: Some("net_surplus".to_string()),

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
mod libdrcr_austax;
mod libdrcr_bridge;
mod sql;
@ -90,6 +91,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
get_open_filename,
set_open_filename,
libdrcr_austax::get_tax_summary,
libdrcr_bridge::get_all_transactions_except_earnings_to_equity,
libdrcr_bridge::get_all_transactions_except_earnings_to_equity_for_account,
libdrcr_bridge::get_balance_sheet,

View File

@ -0,0 +1,41 @@
/*
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/>.
*/
use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::types::{ReportingProductId, ReportingProductKind, VoidArgs};
use tauri::State;
use tokio::sync::Mutex;
use crate::libdrcr_bridge::get_report;
use crate::AppState;
#[tauri::command]
pub(crate) async fn get_tax_summary(state: State<'_, Mutex<AppState>>) -> Result<String, ()> {
Ok(get_report(
state,
&ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}),
},
)
.await
.downcast_ref::<DynamicReport>()
.unwrap()
.to_json())
}

View File

@ -22,10 +22,8 @@ use std::sync::Arc;
use chrono::NaiveDate;
use libdrcr::db::DbConnection;
use libdrcr::model::assertions::BalanceAssertion;
use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::register_lookup_fns;
use libdrcr::reporting::types::{
BalancesAt, DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
ReportingContext, ReportingProduct, ReportingProductId, ReportingProductKind, Transactions,
@ -37,7 +35,13 @@ use tokio::sync::Mutex;
use crate::AppState;
async fn get_report(
fn prepare_reporting_context(context: &mut ReportingContext) {
libdrcr::austax::register_lookup_fns(context);
libdrcr::reporting::steps::register_lookup_fns(context);
libdrcr::reporting::builders::register_dynamic_builders(context);
}
pub(crate) async fn get_report(
state: State<'_, Mutex<AppState>>,
target: &ReportingProductId,
) -> Box<dyn ReportingProduct> {
@ -51,11 +55,11 @@ async fn get_report(
// 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);
prepare_reporting_context(&mut context);
// Get dynamic report
let targets = vec![
// FIXME: Make this configurable
ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::Transactions,
@ -230,8 +234,7 @@ pub(crate) async fn get_validated_balance_assertions(
// 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);
prepare_reporting_context(&mut context);
// Get report targets
let mut targets = vec![ReportingProductId {

View File

@ -0,0 +1,36 @@
<!--
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" />
</template>
<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { ref } from 'vue';
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
import { DynamicReport } from '../reports/base.ts';
const report = ref(null as DynamicReport | null);
async function load() {
report.value = JSON.parse(await invoke('get_tax_summary'));
}
load();
</script>

View File

@ -44,6 +44,7 @@ async function initApp() {
{ 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('./reports/TrialBalanceReport.vue') },
{ path: '/austax/tax-summary', name: 'tax-summary', component: () => import('./austax/TaxSummaryReport.vue') },
];
const router = createRouter({
history: createWebHistory(),

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
@ -40,7 +40,8 @@
<div class="pl-4">
<h2 class="font-medium text-gray-700 mb-2">Advanced reports</h2>
<ul class="list-disc ml-6">
<!-- TODO: Plugin reports -->
<!-- TODO: Generate this list dynamically -->
<li><RouterLink :to="{ name: 'tax-summary' }" class="text-gray-900 hover:text-blue-700 hover:underline">Tax summary</RouterLink></li>
</ul>
</div>
</div>