/* 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 std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::QuantityInt; use super::types::{GenericReportingProduct, ReportingProduct}; /// Represents a dynamically generated report composed of [DynamicReportEntry] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DynamicReport { pub title: String, pub columns: Vec, pub entries: Vec, } 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::CalculatedRow(_) => true, DynamicReportEntry::Spacer => true, }); } /// Recursively calculate all [CalculatedRow] entries pub fn calculate(&mut self) { // FIXME: This is for the borrow checker - can it be avoided? let report_cloned = self.clone(); for entry in self.entries.iter_mut() { match entry { DynamicReportEntry::Section(section) => section.calculate(&report_cloned), DynamicReportEntry::LiteralRow(_) => (), DynamicReportEntry::CalculatedRow(row) => { *entry = DynamicReportEntry::LiteralRow(row.calculate(&report_cloned)); } DynamicReportEntry::Spacer => (), } } } /// Look up [DynamicReportEntry] by id pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { for entry in self.entries.iter() { match entry { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry); } } if let Some(e) = section.by_id(id) { return Some(e); } } DynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry); } } } DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), } } None } /// 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 DynamicReportEntry::Section(section) = entry { section.subtotal(&self) } else { panic!("Called subtotal_for_id on non-Section"); } } /// Serialise the report (as JSON) using serde pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap() } } impl GenericReportingProduct for DynamicReport {} impl ReportingProduct for DynamicReport {} #[derive(Clone, Debug, Deserialize, Serialize)] pub enum DynamicReportEntry { Section(Section), LiteralRow(LiteralRow), #[serde(skip)] CalculatedRow(CalculatedRow), Spacer, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Section { pub text: String, 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::CalculatedRow(_) => 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::CalculatedRow(_) => false, DynamicReportEntry::Spacer => true, }) } /// Recursively calculate all [CalculatedRow] entries pub fn calculate(&mut self, report: &DynamicReport) { for entry in self.entries.iter_mut() { match entry { DynamicReportEntry::Section(section) => section.calculate(report), DynamicReportEntry::LiteralRow(_) => (), DynamicReportEntry::CalculatedRow(row) => { *entry = DynamicReportEntry::LiteralRow(row.calculate(report)) } DynamicReportEntry::Spacer => (), } } } /// Look up [DynamicReportEntry] by id pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { for entry in self.entries.iter() { match entry { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry); } } if let Some(e) = section.by_id(id) { return Some(e); } } DynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry); } } } DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), } } None } /// Calculate the subtotals for this [Section] pub fn subtotal(&self, report: &DynamicReport) -> Vec { let mut subtotals = vec![0; report.columns.len()]; for entry in self.entries.iter() { match entry { DynamicReportEntry::Section(section) => { for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { subtotals[col_idx] += subtotal; } } DynamicReportEntry::LiteralRow(row) => { for (col_idx, subtotal) in row.quantity.iter().enumerate() { subtotals[col_idx] += subtotal; } } DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), } } subtotals } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LiteralRow { 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: &DynamicReport) -> 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: &DynamicReport) -> LiteralRow { (self.calculate_fn)(report) } } pub fn entries_for_kind( kind: &str, invert: bool, balances: &Vec<&HashMap>, kinds_for_account: &HashMap>, ) -> Vec { // Get accounts of specified kind let mut accounts = kinds_for_account .iter() .filter_map(|(a, k)| { if k.iter().any(|k| k == kind) { Some(a) } else { None } }) .collect::>(); accounts.sort(); let mut entries = Vec::new(); for account in accounts { let quantities = balances .iter() .map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 }) .collect::>(); let entry = LiteralRow { text: account.to_string(), quantity: quantities, id: None, visible: true, auto_hide: true, link: None, heading: false, bordered: false, }; entries.push(DynamicReportEntry::LiteralRow(entry)); } entries }