diff --git a/libdrcr/src/austax/mod.rs b/libdrcr/src/austax/mod.rs
new file mode 100644
index 0000000..4a7ed87
--- /dev/null
+++ b/libdrcr/src/austax/mod.rs
@@ -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 .
+*/
+
+//! 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) -> bool {
+ args.is::()
+ }
+
+ fn from_args(_args: Box) -> Box {
+ 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 {
+ // 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>,
+ dependencies: &mut ReportingGraphDependencies,
+ _context: &ReportingContext,
+ ) {
+ for other in steps {
+ if let Some(other) =
+ other.downcast_ref::()
+ {
+ // 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>,
+ _dependencies: &ReportingGraphDependencies,
+ products: &RwLock,
+ ) -> Result {
+ 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::()
+ .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>,
+ kinds_for_account: &HashMap>,
+ floor: QuantityInt,
+) -> Vec {
+ 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, floor: QuantityInt) -> Vec {
+ quantity.iter_mut().for_each(|v| *v = (*v / floor) * floor);
+ quantity
+}
diff --git a/libdrcr/src/lib.rs b/libdrcr/src/lib.rs
index 300942e..2a06dfb 100644
--- a/libdrcr/src/lib.rs
+++ b/libdrcr/src/lib.rs
@@ -1,4 +1,5 @@
pub mod account_config;
+pub mod austax;
pub mod db;
pub mod model;
pub mod reporting;
diff --git a/libdrcr/src/main.rs b/libdrcr/src/main.rs
index fa25ff3..63b7e6a 100644
--- a/libdrcr/src/main.rs
+++ b/libdrcr/src/main.rs
@@ -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",
diff --git a/libdrcr/src/reporting/dynamic_report.rs b/libdrcr/src/reporting/dynamic_report.rs
index d2149c8..0291e33 100644
--- a/libdrcr/src/reporting/dynamic_report.rs
+++ b/libdrcr/src/reporting/dynamic_report.rs
@@ -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 {
- 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> {
+ 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 {
- 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> {
+ 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);
+ }
}
}
diff --git a/libdrcr/src/reporting/steps.rs b/libdrcr/src/reporting/steps.rs
index 2c4d9c5..ec3c7bb 100644
--- a/libdrcr/src/reporting/steps.rs
+++ b/libdrcr/src/reporting/steps.rs
@@ -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) -> bool {
- args.is::()
- }
-
- fn from_args(_args: Box) -> Box {
- 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 {
- // 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>,
- dependencies: &mut ReportingGraphDependencies,
- _context: &ReportingContext,
- ) {
- for other in steps {
- if let Some(other) =
- other.downcast_ref::()
- {
- // 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>,
- _dependencies: &ReportingGraphDependencies,
- _products: &RwLock,
- ) -> Result {
- 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()),
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 9ef1a19..8c70573 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -16,6 +16,7 @@
along with this program. If not, see .
*/
+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,
diff --git a/src-tauri/src/libdrcr_austax.rs b/src-tauri/src/libdrcr_austax.rs
new file mode 100644
index 0000000..8dfb6d2
--- /dev/null
+++ b/src-tauri/src/libdrcr_austax.rs
@@ -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 .
+*/
+
+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>) -> Result {
+ Ok(get_report(
+ state,
+ &ReportingProductId {
+ name: "CalculateIncomeTax",
+ kind: ReportingProductKind::DynamicReport,
+ args: Box::new(VoidArgs {}),
+ },
+ )
+ .await
+ .downcast_ref::()
+ .unwrap()
+ .to_json())
+}
diff --git a/src-tauri/src/libdrcr_bridge.rs b/src-tauri/src/libdrcr_bridge.rs
index 035db3b..8550373 100644
--- a/src-tauri/src/libdrcr_bridge.rs
+++ b/src-tauri/src/libdrcr_bridge.rs
@@ -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>,
target: &ReportingProductId,
) -> Box {
@@ -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 {
diff --git a/src/austax/TaxSummaryReport.vue b/src/austax/TaxSummaryReport.vue
new file mode 100644
index 0000000..44a9411
--- /dev/null
+++ b/src/austax/TaxSummaryReport.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/src/main.ts b/src/main.ts
index 1e64a3f..1ddedf2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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(),
diff --git a/src/pages/HomeView.vue b/src/pages/HomeView.vue
index 96aa095..b8453e2 100644
--- a/src/pages/HomeView.vue
+++ b/src/pages/HomeView.vue
@@ -1,6 +1,6 @@
+
+ Tax summary