/* 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 . */ // FIXME: Tidy up this file use std::cell::RefCell; use std::collections::HashMap; use serde::{Deserialize, Serialize}; 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) -> Vec { let entry = self.by_id(id).expect("Invalid id"); if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry { section.subtotal(&self) } else { panic!("Called subtotal_for_id on non-Section"); } } // 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 } else { panic!("Called quantity_for_id on non-LiteralRow"); } } } /// Represents a dynamically generated report composed of [DynamicReportEntry], with no [CalculatedRow]s #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DynamicReport { pub title: String, pub columns: Vec, pub entries: Vec, } impl DynamicReport { pub fn new(title: String, columns: Vec, entries: Vec) -> Self { Self { title, columns, entries, } } /// 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() } } 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), Spacer, } #[derive(Clone, Debug)] pub struct CalculatableSection { pub text: String, pub id: Option, pub visible: bool, pub auto_hide: bool, pub entries: Vec>, } 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 => (), } } 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 } } #[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::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 { // 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 { DynamicReportEntry::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(e); } } DynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry.clone()); } } } DynamicReportEntry::Spacer => (), } } None } /// Calculate the subtotals for this [Section] pub fn subtotal(&self, report: &CalculatableDynamicReport) -> 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::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: &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 { // 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::>(); // Some exceptions for the link let link; if account == crate::CURRENT_YEAR_EARNINGS { link = Some("/income-statement".to_string()); } else if account == crate::RETAINED_EARNINGS { link = None } else { link = Some(format!("/transactions/{}", account)); } let entry = LiteralRow { text: account.to_string(), quantity: quantities, id: None, visible: true, auto_hide: true, link, heading: false, bordered: false, }; entries.push(CalculatableDynamicReportEntry::LiteralRow(entry)); } entries }