From aefe5a351c90435ba1f2d81f9b87a27bddbc7ccd Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 31 May 2025 13:37:54 +1000 Subject: [PATCH] Refactor dynamic reporting API Change from a declarative style to an imperative style Previous declarative style was not good fit for Rust, as the borrow checker does not understand the flat data structure --- libdrcr/src/austax/mod.rs | 745 +++++------------- libdrcr/src/reporting/dynamic_report.rs | 448 +---------- libdrcr/src/reporting/steps.rs | 397 +++++----- .../DynamicReportEntryComponent.vue | 20 +- src/reports/BalanceSheetReport.vue | 8 +- src/reports/base.ts | 8 +- 6 files changed, 441 insertions(+), 1185 deletions(-) diff --git a/libdrcr/src/austax/mod.rs b/libdrcr/src/austax/mod.rs index f839e2e..4391e41 100644 --- a/libdrcr/src/austax/mod.rs +++ b/libdrcr/src/austax/mod.rs @@ -30,8 +30,7 @@ use crate::account_config::kinds_for_account; use crate::model::transaction::{Posting, Transaction, TransactionWithPostings}; use crate::reporting::calculator::ReportingGraphDependencies; use crate::reporting::dynamic_report::{ - entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, - CalculatableSection, CalculatedRow, DynamicReport, LiteralRow, + entries_for_kind, DynamicReport, DynamicReportEntry, Row, Section, }; use crate::reporting::executor::ReportingExecutionError; use crate::reporting::steps::AllTransactionsExceptEarningsToEquityBalances; @@ -44,6 +43,23 @@ use crate::util::sofy_from_eofy; use crate::{QuantityInt, INCOME_TAX, INCOME_TAX_CONTROL}; // Constants and tax calculations +#[rustfmt::skip] +const INCOME_TYPES: &[(&str, &str, &str)] = &[ + ("income1", "Salary or wages", "1"), + ("income5", "Australian Government allowances and payments", "5"), + ("income10", "Gross interest", "10"), + ("income13", "Partnerships and trusts", "13"), + ("income24", "Other income", "24"), +]; + +#[rustfmt::skip] +const DEDUCTION_TYPES: &[(&str, &str, &str)] = &[ + ("d2", "Work-related travel expenses", "D2"), + ("d4", "Work-related self-education expenses", "D4"), + ("d5", "Other work-related expenses", "D5"), + ("d9", "Gifts or donations", "D9"), +]; + fn get_grossedup_rfb(taxable_value: QuantityInt) -> QuantityInt { // FIXME: May vary from year to year ((taxable_value as f64) * 2.0802) as QuantityInt @@ -191,7 +207,156 @@ impl ReportingStep for CalculateIncomeTax { let kinds_for_account = kinds_for_account(context.db_connection.get_account_configurations().await); - // Pre-compute taxable value of reportable fringe benefits (required for MLS) + // Generate tax summary report + let mut report = DynamicReport { + title: "Tax summary".to_string(), + columns: vec!["$".to_string()], + entries: Vec::new(), + }; + + // Add income entries + let mut total_income: QuantityInt = 0; + + for (code, label, number) in INCOME_TYPES { + let entries; + if *code == "income1" { + // Special case for salary or wages - round each separately + entries = entries_for_kind_floor( + &format!("austax.{}", code), + true, + &vec![balances], + &kinds_for_account, + 100, + ); + } else { + entries = entries_for_kind( + &format!("austax.{}", code), + true, + &vec![balances], + &kinds_for_account, + ); + } + + if entries.is_empty() { + continue; + } + + let mut section = Section { + text: Some(format!("{} ({})", label, number)), + id: None, + visible: true, + entries, + }; + + // Add subtotal row + let subtotal = floor_quantity(section.subtotal(&report), 100); + total_income += subtotal[0]; + + section.entries.push( + Row { + text: format!("Total item {}", number), + quantity: subtotal, + id: Some(format!("total_{}", code)), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(section.into()); + report.entries.push(DynamicReportEntry::Spacer); + } + + // Total assessable income + report.entries.push( + Row { + text: "Total assessable income".to_string(), + quantity: vec![total_income], + id: Some("total_income".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(DynamicReportEntry::Spacer); + + // Add deduction entries + let mut total_deductions: QuantityInt = 0; + + for (code, label, number) in DEDUCTION_TYPES { + let entries = entries_for_kind( + &format!("austax.{}", code), + false, + &vec![balances], + &kinds_for_account, + ); + + if entries.is_empty() { + continue; + } + + let mut section = Section { + text: Some(format!("{} ({})", label, number)), + id: None, + visible: true, + entries, + }; + + // Add subtotal row + let subtotal = floor_quantity(section.subtotal(&report), 100); + total_deductions += subtotal[0]; + + section.entries.push( + Row { + text: format!("Total item {}", number), + quantity: subtotal, + id: Some(format!("total_{}", code)), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(section.into()); + report.entries.push(DynamicReportEntry::Spacer); + } + + // Total deductions + report.entries.push( + Row { + text: "Total deductions".to_string(), + quantity: vec![total_deductions], + id: Some("total_deductions".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(DynamicReportEntry::Spacer); + + // Net taxable income + let net_taxable = total_income - total_deductions; + report.entries.push( + Row { + text: "Net taxable income".to_string(), + quantity: vec![net_taxable], + id: Some("net_taxable".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(DynamicReportEntry::Spacer); + + // Precompute RFB amount as this is required for MLS let rfb_taxable = balances .iter() .filter(|(acc, _)| { @@ -202,543 +367,37 @@ impl ReportingStep for CalculateIncomeTax { }) .map(|(_, bal)| *bal) .sum(); + let _rfb_grossedup = get_grossedup_rfb(rfb_taxable); - // 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, - }, - }), - // Precompute RFB amount as this is required for MLS - CalculatableDynamicReportEntry::LiteralRow(LiteralRow { - text: "Taxable value of reportable fringe benefits".to_string(), - quantity: vec![rfb_taxable], - id: Some("rfb_taxable".to_string()), - visible: false, - auto_hide: false, - link: None, - heading: false, - bordered: false, - }), - CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Grossed-up value".to_string(), - quantity: vec![get_grossedup_rfb( - report.quantity_for_id("rfb_taxable").unwrap()[0], - )], - id: Some("rfb_grossedup".to_string()), - visible: false, - auto_hide: false, - link: None, - heading: false, - bordered: false, - }, - }), - CalculatableDynamicReportEntry::Spacer, - CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Base income tax".to_string(), - quantity: vec![get_base_income_tax( - report.quantity_for_id("net_taxable").unwrap()[0], - )], - id: Some("tax_base".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: false, - bordered: false, - }, - }), - // CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - // calculate_fn: |report| LiteralRow { - // text: "Medicare levy".to_string(), - // quantity: vec![get_medicare_levy( - // report.quantity_for_id("net_taxable").unwrap()[0], - // )], - // id: Some("tax_ml".to_string()), - // visible: true, - // auto_hide: true, - // link: None, - // heading: false, - // bordered: false, - // }, - // }), - // CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - // calculate_fn: |report| LiteralRow { - // text: "Medicare levy".to_string(), - // quantity: vec![get_medicare_levy_surcharge( - // report.quantity_for_id("net_taxable").unwrap()[0], - // report.quantity_for_id("rfb_grossedup").unwrap()[0], - // )], - // id: Some("tax_mls".to_string()), - // visible: true, - // auto_hide: true, - // link: None, - // heading: false, - // bordered: false, - // }, - // }), - CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total income tax".to_string(), - quantity: vec![ - report.quantity_for_id("tax_base").unwrap()[0], // + report.quantity_for_id("tax_ml").map(|v| v[0]).unwrap_or(0) - // + report.quantity_for_id("tax_mls").map(|v| v[0]).unwrap_or(0), - ], - id: Some("total_tax".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }), - ], + // Base income tax row + let tax_base = get_base_income_tax(net_taxable); + report.entries.push( + Row { + text: "Base income tax".to_string(), + quantity: vec![tax_base], + id: Some("tax_base".to_string()), + visible: true, + link: None, + heading: false, + bordered: false, + } + .into(), ); - let mut report: DynamicReport = report.calculate(); - report.auto_hide(); - - let total_tax = report.quantity_for_id("total_tax").unwrap()[0]; + // Total income tax row + let tax_total = tax_base; + report.entries.push( + Row { + text: "Total income tax".to_string(), + quantity: vec![tax_total], + id: Some("tax_total".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); // Generate income tax transaction let transactions = Transactions { @@ -759,18 +418,18 @@ impl ReportingStep for CalculateIncomeTax { transaction_id: None, description: None, account: INCOME_TAX.to_string(), - quantity: total_tax, + quantity: tax_total, commodity: context.db_connection.metadata().reporting_commodity.clone(), - quantity_ascost: Some(total_tax), + quantity_ascost: Some(tax_total), }, Posting { id: None, transaction_id: None, description: None, account: INCOME_TAX_CONTROL.to_string(), - quantity: -total_tax, + quantity: -tax_total, commodity: context.db_connection.metadata().reporting_commodity.clone(), - quantity_ascost: Some(total_tax), + quantity_ascost: Some(tax_total), }, ], }], @@ -805,10 +464,10 @@ fn entries_for_kind_floor( balances: &Vec<&HashMap>, kinds_for_account: &HashMap>, floor: QuantityInt, -) -> Vec { +) -> 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 + DynamicReportEntry::Row(row) => row .quantity .iter_mut() .for_each(|v| *v = (*v / floor) * floor), diff --git a/libdrcr/src/reporting/dynamic_report.rs b/libdrcr/src/reporting/dynamic_report.rs index fb955ff..689cc81 100644 --- a/libdrcr/src/reporting/dynamic_report.rs +++ b/libdrcr/src/reporting/dynamic_report.rs @@ -16,9 +16,6 @@ along with this program. If not, see . */ -// FIXME: Tidy up this file - -use std::cell::RefCell; use std::collections::HashMap; use serde::{Deserialize, Serialize}; @@ -27,161 +24,7 @@ use crate::QuantityInt; use super::types::ReportingProduct; -/// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] -#[derive(Clone, Debug)] -pub struct CalculatableDynamicReport { - pub title: String, - pub columns: Vec, - // This must use RefCell as, during calculation, we iterate while mutating the report - pub entries: Vec>, -} - -impl CalculatableDynamicReport { - pub fn new( - title: String, - columns: Vec, - entries: Vec, - ) -> Self { - Self { - title, - columns, - entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), - } - } - - /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(self) -> DynamicReport { - let mut calculated_entries = Vec::new(); - - for (entry_idx, entry) in self.entries.iter().enumerate() { - let entry_ref = entry.borrow(); - - match &*entry_ref { - CalculatableDynamicReportEntry::CalculatableSection(section) => { - // Clone first, in case calculation needs to take reference to the section - let updated_section = section.clone().calculate(&self); - - drop(entry_ref); // Drop entry_ref so we can borrow mutably - let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); - - calculated_entries.push(DynamicReportEntry::Section(updated_section)); - } - CalculatableDynamicReportEntry::Section(section) => { - calculated_entries.push(DynamicReportEntry::Section(section.clone())); - } - CalculatableDynamicReportEntry::LiteralRow(row) => { - calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); - } - CalculatableDynamicReportEntry::CalculatedRow(row) => { - let updated_row = row.calculate(&self); - - drop(entry_ref); // Drop entry_ref so we can borrow mutably - let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); - - calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); - } - CalculatableDynamicReportEntry::Spacer => { - calculated_entries.push(DynamicReportEntry::Spacer); - } - } - } - - DynamicReport { - title: self.title, - columns: self.columns, - entries: calculated_entries, - } - } - - /// Look up [CalculatableDynamicReportEntry] by id - /// - /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. - pub fn by_id(&self, id: &str) -> Option { - // Manually iterate over self.entries rather than self.entries() - // To catch the situation where entry is already mutably borrowed - for entry in self.entries.iter() { - match entry.try_borrow() { - Ok(entry) => match &*entry { - CalculatableDynamicReportEntry::CalculatableSection(section) => { - if let Some(i) = §ion.id { - if i == id { - return Some(entry.clone()); - } - } - if let Some(e) = section.by_id(id) { - return Some(e); - } - } - CalculatableDynamicReportEntry::Section(section) => { - if let Some(i) = §ion.id { - if i == id { - return Some(entry.clone()); - } - } - if let Some(e) = section.by_id(id) { - return Some(match e { - DynamicReportEntry::Section(section) => { - CalculatableDynamicReportEntry::Section(section.clone()) - } - DynamicReportEntry::LiteralRow(row) => { - CalculatableDynamicReportEntry::LiteralRow(row.clone()) - } - DynamicReportEntry::Spacer => { - CalculatableDynamicReportEntry::Spacer - } - }); - } - } - CalculatableDynamicReportEntry::LiteralRow(row) => { - if let Some(i) = &row.id { - if i == id { - return Some(entry.clone()); - } - } - } - CalculatableDynamicReportEntry::CalculatedRow(_) => (), - CalculatableDynamicReportEntry::Spacer => (), - }, - Err(err) => panic!( - "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", - err - ), - } - } - - None - } - - /// Calculate the subtotals for the [Section] with the given id - 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 { - None - } - } - - // Return the quantities for the [LiteralRow] with the given id - 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 { - None - } - } -} - -/// Represents a dynamically generated report composed of [DynamicReportEntry], with no [CalculatedRow]s +/// Represents a dynamically generated report composed of [DynamicReportEntry] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DynamicReport { pub title: String, @@ -198,37 +41,13 @@ impl DynamicReport { } } - /// Remove all entries from the report where auto_hide is enabled and quantity is zero - pub fn auto_hide(&mut self) { - self.entries.retain_mut(|e| match e { - DynamicReportEntry::Section(section) => { - section.auto_hide_children(); - if section.can_auto_hide_self() { - false - } else { - true - } - } - DynamicReportEntry::LiteralRow(row) => { - if row.can_auto_hide() { - false - } else { - true - } - } - DynamicReportEntry::Spacer => true, - }); - } - /// Serialise the report (as JSON) using serde pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap() } /// Look up [DynamicReportEntry] by id - /// - /// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. - pub fn by_id(&self, id: &str) -> Option { + pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { @@ -236,25 +55,17 @@ impl DynamicReport { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { - return Some(entry.clone()); + return Some(entry); } } if let Some(e) = section.by_id(id) { - return Some(match e { - DynamicReportEntry::Section(section) => { - DynamicReportEntry::Section(section.clone()) - } - DynamicReportEntry::LiteralRow(row) => { - DynamicReportEntry::LiteralRow(row.clone()) - } - DynamicReportEntry::Spacer => DynamicReportEntry::Spacer, - }); + return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + DynamicReportEntry::Row(row) => { if let Some(i) = &row.id { if i == id { - return Some(entry.clone()); + return Some(entry); } } } @@ -266,10 +77,10 @@ impl DynamicReport { } // Return the quantities for the [LiteralRow] with the given id - pub fn quantity_for_id(&self, id: &str) -> Option> { + pub fn quantity_for_id(&self, id: &str) -> Option<&Vec> { if let Some(entry) = self.by_id(id) { - if let DynamicReportEntry::LiteralRow(row) = entry { - Some(row.quantity) + if let DynamicReportEntry::Row(row) = entry { + Some(&row.quantity) } else { panic!("Called quantity_for_id on non-LiteralRow"); } @@ -281,207 +92,36 @@ impl DynamicReport { impl ReportingProduct for DynamicReport {} -#[derive(Clone, Debug)] -pub enum CalculatableDynamicReportEntry { - CalculatableSection(CalculatableSection), - Section(Section), - LiteralRow(LiteralRow), - CalculatedRow(CalculatedRow), - Spacer, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub enum DynamicReportEntry { Section(Section), - LiteralRow(LiteralRow), + Row(Row), Spacer, } -#[derive(Clone, Debug)] -pub struct CalculatableSection { - pub text: String, - pub id: Option, - pub visible: bool, - pub auto_hide: bool, - pub entries: Vec>, +impl From
for DynamicReportEntry { + fn from(value: Section) -> Self { + DynamicReportEntry::Section(value) + } } -impl CalculatableSection { - pub fn new( - text: String, - id: Option, - visible: bool, - auto_hide: bool, - entries: Vec, - ) -> Self { - Self { - text, - id, - visible, - auto_hide, - entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), - } - } - - /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(&mut self, report: &CalculatableDynamicReport) -> Section { - let mut calculated_entries = Vec::new(); - - for (entry_idx, entry) in self.entries.iter().enumerate() { - let entry_ref = entry.borrow(); - - match &*entry_ref { - CalculatableDynamicReportEntry::CalculatableSection(section) => { - let updated_section = section.clone().calculate(&report); - - drop(entry_ref); // Drop entry_ref so we can borrow mutably - let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); - - calculated_entries.push(DynamicReportEntry::Section(updated_section)); - } - CalculatableDynamicReportEntry::Section(section) => { - calculated_entries.push(DynamicReportEntry::Section(section.clone())); - } - CalculatableDynamicReportEntry::LiteralRow(row) => { - calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); - } - CalculatableDynamicReportEntry::CalculatedRow(row) => { - let updated_row = row.calculate(&report); - - drop(entry_ref); // Drop entry_ref so we can borrow mutably - let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); - - calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); - } - CalculatableDynamicReportEntry::Spacer => { - calculated_entries.push(DynamicReportEntry::Spacer); - } - } - } - - Section { - text: self.text.clone(), - id: self.id.clone(), - visible: self.visible, - auto_hide: self.auto_hide, - entries: calculated_entries, - } - } - - /// Look up [CalculatableDynamicReportEntry] by id - /// - /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. - pub fn by_id(&self, id: &str) -> Option { - // Manually iterate over self.entries rather than self.entries() - // To catch the situation where entry is already mutably borrowed - for entry in self.entries.iter() { - match entry.try_borrow() { - Ok(entry) => match &*entry { - CalculatableDynamicReportEntry::CalculatableSection(section) => { - if let Some(i) = §ion.id { - if i == id { - return Some(entry.clone()); - } - } - if let Some(e) = section.by_id(id) { - return Some(e); - } - } - CalculatableDynamicReportEntry::Section(_) => todo!(), - CalculatableDynamicReportEntry::LiteralRow(row) => { - if let Some(i) = &row.id { - if i == id { - return Some(entry.clone()); - } - } - } - CalculatableDynamicReportEntry::CalculatedRow(_) => (), - CalculatableDynamicReportEntry::Spacer => (), - }, - Err(err) => panic!( - "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", - err - ), - } - } - - None - } - - /// Calculate the subtotals for this [CalculatableSection] - pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { - let mut subtotals = vec![0; report.columns.len()]; - for entry in self.entries.iter() { - match &*entry.borrow() { - CalculatableDynamicReportEntry::CalculatableSection(section) => { - for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { - subtotals[col_idx] += subtotal; - } - } - CalculatableDynamicReportEntry::Section(section) => { - for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { - subtotals[col_idx] += subtotal; - } - } - CalculatableDynamicReportEntry::LiteralRow(row) => { - for (col_idx, subtotal) in row.quantity.iter().enumerate() { - subtotals[col_idx] += subtotal; - } - } - CalculatableDynamicReportEntry::CalculatedRow(_) => (), - CalculatableDynamicReportEntry::Spacer => (), - } - } - subtotals +impl From for DynamicReportEntry { + fn from(value: Row) -> Self { + DynamicReportEntry::Row(value) } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Section { - pub text: String, + pub text: Option, pub id: Option, pub visible: bool, - pub auto_hide: bool, pub entries: Vec, } impl Section { - fn auto_hide_children(&mut self) { - self.entries.retain_mut(|e| match e { - DynamicReportEntry::Section(section) => { - section.auto_hide_children(); - if section.can_auto_hide_self() { - false - } else { - true - } - } - DynamicReportEntry::LiteralRow(row) => { - if row.can_auto_hide() { - false - } else { - true - } - } - DynamicReportEntry::Spacer => true, - }); - } - - fn can_auto_hide_self(&self) -> bool { - self.auto_hide - && self.entries.iter().all(|e| match e { - DynamicReportEntry::Section(section) => section.can_auto_hide_self(), - DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), - DynamicReportEntry::Spacer => true, - }) - } - /// Look up [DynamicReportEntry] by id - /// - /// Returns a cloned copy of the [DynamicReportEntry]. - pub fn by_id(&self, id: &str) -> Option { + pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { @@ -489,17 +129,17 @@ impl Section { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { - return Some(entry.clone()); + return Some(entry); } } if let Some(e) = section.by_id(id) { return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + DynamicReportEntry::Row(row) => { if let Some(i) = &row.id { if i == id { - return Some(entry.clone()); + return Some(entry); } } } @@ -511,7 +151,7 @@ impl Section { } /// Calculate the subtotals for this [Section] - pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { + pub fn subtotal(&self, report: &DynamicReport) -> Vec { let mut subtotals = vec![0; report.columns.len()]; for entry in self.entries.iter() { match entry { @@ -520,7 +160,7 @@ impl Section { subtotals[col_idx] += subtotal; } } - DynamicReportEntry::LiteralRow(row) => { + DynamicReportEntry::Row(row) => { for (col_idx, subtotal) in row.quantity.iter().enumerate() { subtotals[col_idx] += subtotal; } @@ -533,48 +173,22 @@ impl Section { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct LiteralRow { +pub struct Row { pub text: String, pub quantity: Vec, pub id: Option, pub visible: bool, - pub auto_hide: bool, pub link: Option, pub heading: bool, pub bordered: bool, } -impl LiteralRow { - /// Returns whether the row has auto_hide enabled and all quantities are zero - fn can_auto_hide(&self) -> bool { - self.auto_hide && self.quantity.iter().all(|q| *q == 0) - } -} - -#[derive(Clone, Debug)] -pub struct CalculatedRow { - //pub text: String, - pub calculate_fn: fn(report: &CalculatableDynamicReport) -> LiteralRow, - //pub id: Option, - //pub visible: bool, - //pub auto_hide: bool, - //pub link: Option, - //pub heading: bool, - //pub bordered: bool, -} - -impl CalculatedRow { - fn calculate(&self, report: &CalculatableDynamicReport) -> LiteralRow { - (self.calculate_fn)(report) - } -} - pub fn entries_for_kind( kind: &str, invert: bool, balances: &Vec<&HashMap>, kinds_for_account: &HashMap>, -) -> Vec { +) -> Vec { // Get accounts of specified kind let mut accounts = kinds_for_account .iter() @@ -596,6 +210,11 @@ pub fn entries_for_kind( .map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 }) .collect::>(); + // Do not show if all quantities are zero + if quantities.iter().all(|q| *q == 0) { + continue; + } + // Some exceptions for the link let link; if account == crate::CURRENT_YEAR_EARNINGS { @@ -606,17 +225,16 @@ pub fn entries_for_kind( link = Some(format!("/transactions/{}", account)); } - let entry = LiteralRow { + let entry = Row { text: account.to_string(), quantity: quantities, id: None, visible: true, - auto_hide: true, link, heading: false, bordered: false, }; - entries.push(CalculatableDynamicReportEntry::LiteralRow(entry)); + entries.push(entry.into()); } entries diff --git a/libdrcr/src/reporting/steps.rs b/libdrcr/src/reporting/steps.rs index ec3c7bb..bfa69ec 100644 --- a/libdrcr/src/reporting/steps.rs +++ b/libdrcr/src/reporting/steps.rs @@ -35,8 +35,7 @@ use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; use super::dynamic_report::{ - entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, - CalculatableSection, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, + entries_for_kind, DynamicReport, DynamicReportEntry, Row, Section, }; use super::executor::ReportingExecutionError; use super::types::{ @@ -457,92 +456,79 @@ impl ReportingStep for BalanceSheet { kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let report = CalculatableDynamicReport::new( + let mut report = DynamicReport::new( "Balance sheet".to_string(), self.args.dates.iter().map(|d| d.date.to_string()).collect(), - vec![ - CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( - "Assets".to_string(), - Some("assets".to_string()), - true, - false, - { - let mut entries = - entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); - entries.push(CalculatableDynamicReportEntry::CalculatedRow( - CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total assets".to_string(), - quantity: report.subtotal_for_id("assets").unwrap(), - id: Some("total_assets".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }, - )); - entries - }, - )), - CalculatableDynamicReportEntry::Spacer, - CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( - "Liabilities".to_string(), - Some("liabilities".to_string()), - true, - false, - { - let mut entries = - entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); - entries.push(CalculatableDynamicReportEntry::CalculatedRow( - CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total liabilities".to_string(), - quantity: report.subtotal_for_id("liabilities").unwrap(), - id: Some("total_liabilities".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }, - )); - entries - }, - )), - CalculatableDynamicReportEntry::Spacer, - CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( - "Equity".to_string(), - Some("equity".to_string()), - true, - false, - { - let mut entries = - entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); - entries.push(CalculatableDynamicReportEntry::CalculatedRow( - CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total equity".to_string(), - quantity: report.subtotal_for_id("equity").unwrap(), - id: Some("total_equity".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }, - )); - entries - }, - )), - ], + Vec::new(), ); - let mut report = report.calculate(); - report.auto_hide(); + // Add assets section + let mut assets = Section { + text: Some("Assets".to_string()), + id: None, + visible: true, + entries: entries_for_kind("drcr.asset", false, &balances, &kinds_for_account), + }; + let total_assets = assets.subtotal(&report); + assets.entries.push( + Row { + text: "Total assets".to_string(), + quantity: total_assets, + id: Some("total_assets".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(assets.into()); + report.entries.push(DynamicReportEntry::Spacer); + + // Add liabilities section + let mut liabilities = Section { + text: Some("Liabilities".to_string()), + id: None, + visible: true, + entries: entries_for_kind("drcr.liability", true, &balances, &kinds_for_account), + }; + let total_liabilities = liabilities.subtotal(&report); + liabilities.entries.push( + Row { + text: "Total liabilities".to_string(), + quantity: total_liabilities, + id: Some("total_liabilities".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(liabilities.into()); + report.entries.push(DynamicReportEntry::Spacer); + + // Add equity section + let mut equity = Section { + text: Some("Equity".to_string()), + id: None, + visible: true, + entries: entries_for_kind("drcr.equity", true, &balances, &kinds_for_account), + }; + let total_equity = equity.subtotal(&report); + equity.entries.push( + Row { + text: "Total equity".to_string(), + quantity: total_equity, + id: Some("total_equity".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(equity.into()); // Store the result let mut result = ReportingProducts::new(); @@ -1089,89 +1075,80 @@ impl ReportingStep for IncomeStatement { kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let report = CalculatableDynamicReport::new( + let mut report = DynamicReport::new( "Income statement".to_string(), self.args .dates .iter() .map(|d| d.date_end.to_string()) .collect(), - vec![ - CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( - "Income".to_string(), - Some("income".to_string()), - true, - false, - { - let mut entries = - entries_for_kind("drcr.income", true, &balances, &kinds_for_account); - entries.push(CalculatableDynamicReportEntry::CalculatedRow( - CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total income".to_string(), - quantity: report.subtotal_for_id("income").unwrap(), - id: Some("total_income".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }, - )); - entries - }, - )), - CalculatableDynamicReportEntry::Spacer, - CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( - "Expenses".to_string(), - Some("expenses".to_string()), - true, - false, - { - let mut entries = - entries_for_kind("drcr.expense", false, &balances, &kinds_for_account); - entries.push(CalculatableDynamicReportEntry::CalculatedRow( - CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total expenses".to_string(), - quantity: report.subtotal_for_id("expenses").unwrap(), - id: Some("total_expenses".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }, - )); - entries - }, - )), - CalculatableDynamicReportEntry::Spacer, - CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Net surplus (deficit)".to_string(), - quantity: report - .quantity_for_id("total_income") - .unwrap() // Get total income row - .iter() - .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()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - }, - }), - ], + Vec::new(), ); - let mut report = report.calculate(); - report.auto_hide(); + // Add income section + let mut income = Section { + text: Some("Income".to_string()), + id: None, + visible: true, + entries: entries_for_kind("drcr.income", true, &balances, &kinds_for_account), + }; + let total_income = income.subtotal(&report); + income.entries.push( + Row { + text: "Total income".to_string(), + quantity: total_income.clone(), + id: Some("total_income".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(income.into()); + report.entries.push(DynamicReportEntry::Spacer); + + // Add expenses section + let mut expenses = Section { + text: Some("Expenses".to_string()), + id: None, + visible: true, + entries: entries_for_kind("drcr.expense", false, &balances, &kinds_for_account), + }; + let total_expenses = expenses.subtotal(&report); + expenses.entries.push( + Row { + text: "Total expenses".to_string(), + quantity: total_expenses.clone(), + id: Some("total_expenses".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); + report.entries.push(expenses.into()); + report.entries.push(DynamicReportEntry::Spacer); + + // Add net surplus (deficit) row + let net_surplus = total_income + .into_iter() + .zip(total_expenses.into_iter()) + .map(|(i, e)| i - e) + .collect(); + report.entries.push( + Row { + text: "Net surplus (deficit)".to_string(), + quantity: net_surplus, + id: Some("net_surplus".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), + ); // Store the result let mut result = ReportingProducts::new(); @@ -1512,62 +1489,64 @@ impl ReportingStep for TrialBalance { let mut accounts = balances.keys().collect::>(); accounts.sort(); - // Get total debits and credits - let total_dr = balances.values().filter(|b| **b >= 0).sum::(); - let total_cr = -balances.values().filter(|b| **b < 0).sum::(); - // Init report - let mut report = DynamicReport::new( - "Trial balance".to_string(), - vec!["Dr".to_string(), "Cr".to_string()], - { - let mut entries = Vec::new(); + let mut report = DynamicReport { + title: "Trial balance".to_string(), + columns: vec!["Dr".to_string(), "Cr".to_string()], + entries: Vec::new(), + }; - // Entry for each account - for account in accounts { - entries.push(DynamicReportEntry::LiteralRow(LiteralRow { - text: account.clone(), - quantity: vec![ - // Dr cell - if balances[account] >= 0 { - balances[account] - } else { - 0 - }, - // Cr cell - if balances[account] < 0 { - -balances[account] - } else { - 0 - }, - ], - id: None, - visible: true, - auto_hide: true, - link: Some(format!("/transactions/{}", account)), - heading: false, - bordered: false, - })); - } - - // Total row - entries.push(DynamicReportEntry::LiteralRow(LiteralRow { - text: "Totals".to_string(), - quantity: vec![total_dr, total_cr], - id: Some("totals".to_string()), + // Add entry for each account + let mut section = Section { + text: None, + id: None, + visible: true, + entries: Vec::new(), + }; + for account in accounts { + section.entries.push( + Row { + text: account.clone(), + quantity: vec![ + // Dr cell + if balances[account] >= 0 { + balances[account] + } else { + 0 + }, + // Cr cell + if balances[account] < 0 { + -balances[account] + } else { + 0 + }, + ], + id: None, visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, - })); + link: Some(format!("/transactions/{}", account)), + heading: false, + bordered: false, + } + .into(), + ); + } + let totals_row = section.subtotal(&report); + report.entries.push(section.into()); - entries - }, + // Add total row + report.entries.push( + Row { + text: "Totals".to_string(), + quantity: totals_row, + id: Some("totals".to_string()), + visible: true, + link: None, + heading: true, + bordered: true, + } + .into(), ); - report.auto_hide(); - // Store result let mut result = ReportingProducts::new(); result.insert( diff --git a/src/components/DynamicReportEntryComponent.vue b/src/components/DynamicReportEntryComponent.vue index 3d5d4a6..321ea4f 100644 --- a/src/components/DynamicReportEntryComponent.vue +++ b/src/components/DynamicReportEntryComponent.vue @@ -18,14 +18,14 @@ -->