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