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
This commit is contained in:
RunasSudo 2025-05-31 13:37:54 +10:00
parent 4f845eaaea
commit aefe5a351c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 441 additions and 1185 deletions

View File

@ -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<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>,
floor: QuantityInt,
) -> Vec<CalculatableDynamicReportEntry> {
) -> Vec<DynamicReportEntry> {
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),

View File

@ -16,9 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// 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<String>,
// This must use RefCell as, during calculation, we iterate while mutating the report
pub entries: Vec<RefCell<CalculatableDynamicReportEntry>>,
}
impl CalculatableDynamicReport {
pub fn new(
title: String,
columns: Vec<String>,
entries: Vec<CalculatableDynamicReportEntry>,
) -> 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<CalculatableDynamicReportEntry> {
// 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) = &section.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) = &section.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<Vec<QuantityInt>> {
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<Vec<QuantityInt>> {
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<DynamicReportEntry> {
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) = &section.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<Vec<QuantityInt>> {
pub fn quantity_for_id(&self, id: &str) -> Option<&Vec<QuantityInt>> {
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<String>,
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<RefCell<CalculatableDynamicReportEntry>>,
impl From<Section> for DynamicReportEntry {
fn from(value: Section) -> Self {
DynamicReportEntry::Section(value)
}
}
impl CalculatableSection {
pub fn new(
text: String,
id: Option<String>,
visible: bool,
auto_hide: bool,
entries: Vec<CalculatableDynamicReportEntry>,
) -> 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<CalculatableDynamicReportEntry> {
// 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) = &section.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<QuantityInt> {
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<Row> for DynamicReportEntry {
fn from(value: Row) -> Self {
DynamicReportEntry::Row(value)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Section {
pub text: String,
pub text: Option<String>,
pub id: Option<String>,
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<DynamicReportEntry>,
}
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<DynamicReportEntry> {
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) = &section.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<QuantityInt> {
pub fn subtotal(&self, report: &DynamicReport) -> Vec<QuantityInt> {
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<QuantityInt>,
pub id: Option<String>,
pub visible: bool,
pub auto_hide: bool,
pub link: Option<String>,
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<String>,
//pub visible: bool,
//pub auto_hide: bool,
//pub link: Option<String>,
//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<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>,
) -> Vec<CalculatableDynamicReportEntry> {
) -> Vec<DynamicReportEntry> {
// 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::<Vec<_>>();
// 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

View File

@ -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::<Vec<_>>();
accounts.sort();
// Get total debits and credits
let total_dr = balances.values().filter(|b| **b >= 0).sum::<i64>();
let total_cr = -balances.values().filter(|b| **b < 0).sum::<i64>();
// 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(

View File

@ -18,14 +18,14 @@
-->
<template>
<template v-if="literalRow">
<template v-if="literalRow.visible">
<tr :class="literalRow.bordered ? 'border-y border-gray-300' : null">
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': literalRow.heading }">
<a :href="literalRow.link as string" class="hover:text-blue-700 hover:underline" v-if="literalRow.link !== null">{{ literalRow.text }}</a>
<template v-if="literalRow.link === null">{{ literalRow.text }}</template>
<template v-if="row">
<template v-if="row.visible">
<tr :class="row.bordered ? 'border-y border-gray-300' : null">
<component :is="row.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': row.heading }">
<a :href="row.link as string" class="hover:text-blue-700 hover:underline" v-if="row.link !== null">{{ row.text }}</a>
<template v-if="row.link === null">{{ row.text }}</template>
</component>
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': literalRow.heading }" v-html="(cell !== 0 || literalRow.heading) ? ppBracketed(cell, literalRow.link ?? undefined) : ''" v-for="cell of literalRow.quantity">
<component :is="row.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': row.heading }" v-html="(cell !== 0 || row.heading) ? ppBracketed(cell, row.link ?? undefined) : ''" v-for="cell of row.quantity">
</component>
</tr>
</template>
@ -48,12 +48,12 @@
import { computed } from 'vue';
import { ppBracketed } from '../display.ts';
import { DynamicReportEntry, LiteralRow, Section } from '../reports/base.ts';
import { DynamicReportEntry, Row, Section } from '../reports/base.ts';
const { entry } = defineProps<{ entry: DynamicReportEntry }>();
const literalRow = computed(function() {
return (entry as { LiteralRow: LiteralRow }).LiteralRow;
const row = computed(function() {
return (entry as { Row: Row }).Row;
});
const section = computed(function() {
return (entry as { Section: Section }).Section;

View File

@ -54,7 +54,7 @@
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid';
import { DynamicReport, LiteralRow, reportEntryById } from './base.ts';
import { DynamicReport, Row, reportEntryById } from './base.ts';
import { db } from '../db.ts';
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
@ -118,9 +118,9 @@
return true;
}
const totalAssets = (reportEntryById(report.value, 'total_assets') as { LiteralRow: LiteralRow }).LiteralRow.quantity;
const totalLiabilities = (reportEntryById(report.value, 'total_liabilities') as { LiteralRow: LiteralRow }).LiteralRow.quantity;
const totalEquity = (reportEntryById(report.value, 'total_equity') as { LiteralRow: LiteralRow }).LiteralRow.quantity;
const totalAssets = (reportEntryById(report.value, 'total_assets') as { Row: Row }).Row.quantity;
const totalLiabilities = (reportEntryById(report.value, 'total_liabilities') as { Row: Row }).Row.quantity;
const totalEquity = (reportEntryById(report.value, 'total_equity') as { Row: Row }).Row.quantity;
let doesBalance = true;
for (let column = 0; column < report.value.columns.length; column++) {

View File

@ -24,7 +24,7 @@ export interface DynamicReport {
}
// serde_json serialises an enum like this
export type DynamicReportEntry = { Section: Section } | { LiteralRow: LiteralRow } | 'Spacer';
export type DynamicReportEntry = { Section: Section } | { Row: Row } | 'Spacer';
export interface Section {
text: string;
@ -34,7 +34,7 @@ export interface Section {
entries: DynamicReportEntry[];
}
export interface LiteralRow {
export interface Row {
text: string;
quantity: number[];
id: string;
@ -55,8 +55,8 @@ export function reportEntryById(report: DynamicReport | Section, id: string): Dy
if (result !== null) {
return result;
}
} else if ((entry as { LiteralRow: LiteralRow }).LiteralRow) {
if ((entry as { LiteralRow: LiteralRow }).LiteralRow.id === id) {
} else if ((entry as { Row: Row }).Row) {
if ((entry as { Row: Row }).Row.id === id) {
return entry;
}
}