Compare commits

..

7 Commits

Author SHA1 Message Date
25924d2a0a
Stub implementation of Lua plugins 2025-06-01 02:24:13 +10:00
97644042a3
Change ReportingStepArgs to enum not trait
Preparation for plugins
2025-06-01 02:10:56 +10:00
c422b53f16
Pass step name and context to reporting step
Preparation for plugins
2025-06-01 00:53:45 +10:00
d147f1a569
Don't assume reporting step names/kinds are 'static
Preparation for plugins
2025-05-31 23:15:56 +10:00
5d573ac421
Remove functions from db.ts now implemented in libdrcr 2025-05-31 15:35:54 +10:00
4e4baf0320
austax: Implement remaining income and deduction types 2025-05-31 13:38:15 +10:00
aefe5a351c
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
2025-05-31 13:37:54 +10:00
22 changed files with 1631 additions and 1762 deletions

116
libdrcr/Cargo.lock generated
View File

@ -109,6 +109,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
@ -278,18 +288,6 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "dyn-eq"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388"
[[package]]
name = "dyn-hash"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88"
[[package]]
name = "either"
version = "1.15.0"
@ -305,6 +303,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "etcetera"
version = "0.8.0"
@ -691,15 +699,24 @@ dependencies = [
"chrono",
"downcast-rs",
"dyn-clone",
"dyn-eq",
"dyn-hash",
"indexmap",
"mlua",
"serde",
"serde_json",
"sqlx",
"tokio",
]
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]]
name = "libm"
version = "0.2.15"
@ -739,6 +756,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "luau0-src"
version = "0.12.3+luau663"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ae337c644bbf86a8d8e9ce3ee023311833d41741baf5e51acc31b37843aba1"
dependencies = [
"cc",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -775,6 +801,37 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mlua"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0"
dependencies = [
"bstr",
"either",
"erased-serde",
"libloading",
"mlua-sys",
"num-traits",
"parking_lot",
"rustc-hash",
"rustversion",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
dependencies = [
"cc",
"cfg-if",
"luau0-src",
"pkg-config",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@ -837,6 +894,15 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -1021,6 +1087,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustversion"
version = "1.0.20"
@ -1048,6 +1120,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
@ -1527,6 +1609,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.18.0"

View File

@ -8,9 +8,8 @@ async-trait = "0.1.88"
chrono = "0.4.41"
downcast-rs = "2.0.1"
dyn-clone = "1.0.19"
dyn-eq = "0.1.3"
dyn-hash = "0.2.2"
indexmap = "2.9.0"
mlua = { version = "0.10", features = ["luau", "serialize"] }
serde = "1.0.219"
serde_json = "1.0.140"
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] }

View File

@ -0,0 +1,43 @@
--!strict
-- 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 <https://www.gnu.org/licenses/>.
local libdrcr = require("../libdrcr")
function requires(step_name, product_kinds, args)
return {}
end
function execute(step_name, product_kinds, args)
print("Stub: Lua plugin execute")
return {}
end
local plugin: libdrcr.Plugin = {
spec = {
name = "austax",
reporting_steps = {
{
name = "CalculateIncomeTax",
product_kinds = {"DynamicReport", "Transactions"}
}
},
},
requires = requires,
execute = execute,
}
return plugin

View File

@ -0,0 +1,57 @@
--!strict
-- 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 <https://www.gnu.org/licenses/>.
------------------------
-- Plugin specific types
-- Represents a libdrcr plugin specification and implementation
export type Plugin = {
spec: PluginSpec,
requires: (string, {ReportingProductKind}, ReportingStepArgs) -> {ReportingProductId},
execute: (string, {ReportingProductKind}, ReportingStepArgs) -> {ReportingProduct},
}
-- Represents a libdrcr plugin specification
export type PluginSpec = {
name: string,
reporting_steps: {ReportingStepSpec},
}
-- Specifies a ReportingStep provided by the plugin
export type ReportingStepSpec = {
name: string,
product_kinds: {ReportingProductKind}
}
-------------------------
-- libdrcr internal types
export type ReportingProduct = any
export type ReportingProductId = {
name: string,
kind: ReportingProductKind,
args: ReportingStepArgs,
}
export type ReportingProductKind = string
-- TODO: Currently only VoidArgs is supported
export type ReportingStepArgs = string
local libdrcr = {}
return libdrcr

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,53 @@ 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"),
("income2", "Allowances, earnings, tips, director's fees etc.", "2"),
("income3", "Employer lump sum payments", "3"),
("income4", "Employment termination payments", "4"),
("income5", "Australian Government allowances and payments", "5"),
("income6", "Australian Government pensions and allowances", "6"),
("income7", "Australian annuities and superannuation income streams", "7"),
("income8", "Australian superannuation lump sum payments", "8"),
("income9", "Attributed personal services income", "9"),
("income10", "Gross interest", "10"),
("income11", "Dividends", "11"),
("income12", "Employee share schemes", "12"),
("income13", "Partnerships and trusts", "13"),
("income14", "Personal services income", "14"),
("income15", "Net income or loss from business", "15"),
("income16", "Deferred non-commercial business losses", "16"),
("income17", "Net farm management deposits or repayments", "17"),
("income18", "Capital gains", "18"),
("income19", "Foreign entities", "19"),
("income20", "Foreign source income and foreign assets or property", "20"),
("income21", "Rent", "21"),
("income22", "Bonuses from life insurance companies and friendly societies", "22"),
("income23", "Forestry managed investment scheme income", "23"),
("income24", "Other income", "24"),
];
#[rustfmt::skip]
const DEDUCTION_TYPES: &[(&str, &str, &str)] = &[
("d1", "Work-related car expenses", "D1"),
("d2", "Work-related travel expenses", "D2"),
("d3", "Work-related clothing, laundry and dry cleaning expenses", "D3"),
("d4", "Work-related self-education expenses", "D4"),
("d5", "Other work-related expenses", "D5"),
("d6", "Low value pool deduction", "D6"),
("d7", "Interest deductions", "D7"),
("d8", "Dividend deductions", "D8"),
("d9", "Gifts or donations", "D9"),
("d10", "Cost of managing tax affairs", "D10"),
("d11", "Deductible amount of undeducted purchase price of a foreign pension or annuity", "D11"),
("d12", "Personal superannuation contributions", "D12"),
("d13", "Deduction for project pool", "D13"),
("d14", "Forestry managed investment scheme deduction", "D14"),
("d15", "Other deductions", "D15"),
];
fn get_grossedup_rfb(taxable_value: QuantityInt) -> QuantityInt {
// FIXME: May vary from year to year
((taxable_value as f64) * 2.0802) as QuantityInt
@ -90,8 +136,8 @@ pub struct CalculateIncomeTax {}
impl CalculateIncomeTax {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CalculateIncomeTax",
&[ReportingProductKind::Transactions],
"CalculateIncomeTax".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
@ -116,8 +162,8 @@ impl Display for CalculateIncomeTax {
impl ReportingStep for CalculateIncomeTax {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CalculateIncomeTax",
product_kinds: &[
name: "CalculateIncomeTax".to_string(),
product_kinds: vec![
ReportingProductKind::DynamicReport,
ReportingProductKind::Transactions,
],
@ -128,7 +174,7 @@ impl ReportingStep for CalculateIncomeTax {
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
// CalculateIncomeTax depends on CombineOrdinaryTransactions
vec![ReportingProductId {
name: "CombineOrdinaryTransactions",
name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date),
@ -152,8 +198,8 @@ impl ReportingStep for CalculateIncomeTax {
other.id(),
ReportingProductId {
name: self.id().name,
kind: other.product_kinds[0],
args: if other.product_kinds[0] == ReportingProductKind::Transactions {
kind: other.product_kind,
args: if other.product_kind == ReportingProductKind::Transactions {
Box::new(VoidArgs {})
} else {
other.id().args
@ -176,7 +222,7 @@ impl ReportingStep for CalculateIncomeTax {
// Get balances for current year
let balances = &products
.get_or_err(&ReportingProductId {
name: "CombineOrdinaryTransactions",
name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date),
@ -191,7 +237,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 +397,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 +448,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 +494,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

@ -1,7 +1,8 @@
pub mod account_config;
pub mod austax;
//pub mod austax;
pub mod db;
pub mod model;
pub mod plugin;
pub mod reporting;
pub mod serde;
pub mod util;

View File

@ -25,7 +25,7 @@ use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::types::{
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs,
ReportingContext, ReportingProductId, ReportingProductKind, ReportingStepArgs,
};
#[tokio::main]
@ -38,12 +38,14 @@ async fn main() {
// Initialise ReportingContext
let mut context = ReportingContext::new(
db_connection,
"plugins".to_string(),
vec!["austax.austax".to_string()],
NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
"$".to_string(),
);
libdrcr::plugin::register_lookup_fns(&mut context);
libdrcr::reporting::steps::register_lookup_fns(&mut context);
libdrcr::reporting::builders::register_dynamic_builders(&mut context);
libdrcr::austax::register_lookup_fns(&mut context);
let context = Arc::new(context);
@ -51,9 +53,9 @@ async fn main() {
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
// ReportingProductId {
// name: "AllTransactionsExceptEarningsToEquity",
@ -63,18 +65,18 @@ async fn main() {
// }),
// },
ReportingProductId {
name: "BalanceSheet",
name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs {
args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}],
}),
},
ReportingProductId {
name: "IncomeStatement",
name: "IncomeStatement".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateStartDateEndArgs {
args: ReportingStepArgs::MultipleDateStartDateEndArgs(MultipleDateStartDateEndArgs {
dates: vec![DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
@ -91,14 +93,14 @@ async fn main() {
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}),
@ -109,22 +111,22 @@ async fn main() {
.await
.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}),
})
.unwrap();
// let result = products
// .get_or_err(&ReportingProductId {
// name: "CalculateIncomeTax".to_string(),
// kind: ReportingProductKind::DynamicReport,
// args: ReportingStepArgs::VoidArgs,
// })
// .unwrap();
println!("Tax summary:");
println!("{:?}", result);
// println!("Tax summary:");
// println!("{:?}", result);
let result = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}),
@ -138,14 +140,14 @@ async fn main() {
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
ReportingProductId {
name: "BalanceSheet",
name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs {
args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}],
@ -158,9 +160,9 @@ async fn main() {
.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "BalanceSheet",
name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs {
args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}],
@ -178,14 +180,14 @@ async fn main() {
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
ReportingProductId {
name: "TrialBalance",
name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}),
},
@ -196,9 +198,9 @@ async fn main() {
.unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "TrialBalance",
name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}),
})

216
libdrcr/src/plugin.rs Normal file
View File

@ -0,0 +1,216 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
use std::fmt::Display;
use async_trait::async_trait;
use mlua::{Function, Lua, LuaSerdeExt, Table, Value};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::reporting::calculator::ReportingGraphDependencies;
use crate::reporting::executor::ReportingExecutionError;
use crate::reporting::types::{
ReportingContext, ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep,
ReportingStepArgs, ReportingStepId,
};
fn load_plugin(plugin_dir: &str, plugin_name: &str) -> (Lua, Plugin) {
let lua = Lua::new();
// Init Lua environment
let package = lua.globals().get::<Table>("package").unwrap();
package
.set("path", format!("{}/?.luau", plugin_dir))
.unwrap();
// Require and call the plugin
let require = lua.load("require").eval::<Function>().unwrap();
let plugin_table = require.call::<Table>(plugin_name).expect("Lua error");
// Convert plugin to Rust struct
let plugin = Plugin {
spec: lua
.from_value(
plugin_table
.get("spec")
.expect("Error parsing Plugin definition"),
)
.unwrap(),
requires: plugin_table
.get("requires")
.expect("Error parsing Plugin definition"),
execute: plugin_table
.get("execute")
.expect("Error parsing Plugin definition"),
};
(lua, plugin)
}
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
pub fn register_lookup_fns(context: &mut ReportingContext) {
for plugin_path in context.plugin_names.clone().iter() {
let (_, plugin) = load_plugin(&context.plugin_dir, plugin_path);
for reporting_step in plugin.spec.reporting_steps.iter() {
context.register_lookup_fn(
reporting_step.name.clone(),
reporting_step.product_kinds.clone(),
PluginReportingStep::takes_args,
PluginReportingStep::from_args,
);
}
context
.plugin_specs
.insert(plugin_path.clone(), plugin.spec);
}
}
#[derive(Debug)]
pub struct Plugin {
spec: PluginSpec,
requires: Function,
execute: Function,
}
/// Represents a libdrcr plugin
#[derive(Debug, Deserialize, Serialize)]
pub struct PluginSpec {
name: String,
reporting_steps: Vec<ReportingStepSpec>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ReportingStepSpec {
name: String,
product_kinds: Vec<ReportingProductKind>,
}
/// Generic reporting step which is implemented by a plugin
#[derive(Debug)]
pub struct PluginReportingStep {
pub plugin_path: String,
pub step_name: String,
pub product_kinds: Vec<ReportingProductKind>,
pub args: ReportingStepArgs, // Currently only VoidArgs is supported
}
impl PluginReportingStep {
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
*args == ReportingStepArgs::VoidArgs
}
fn from_args(
name: &str,
args: ReportingStepArgs,
context: &ReportingContext,
) -> Box<dyn ReportingStep> {
// Look up plugin
for (plugin_path, plugin_spec) in context.plugin_specs.iter() {
if let Some(reporting_step_spec) =
plugin_spec.reporting_steps.iter().find(|s| s.name == name)
{
return Box::new(Self {
plugin_path: plugin_path.to_string(),
step_name: name.to_string(),
product_kinds: reporting_step_spec.product_kinds.clone(),
args,
});
}
}
panic!("No plugin provides step {}", name);
}
}
impl Display for PluginReportingStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{} {{PluginReportingStep}}", self.id()))
}
}
#[async_trait]
impl ReportingStep for PluginReportingStep {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name.clone(),
product_kinds: self.product_kinds.clone(),
args: self.args.clone(),
}
}
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
// Call to plugin
let (lua, plugin) = load_plugin(&context.plugin_dir, &self.plugin_path);
let result_table = plugin
.requires
.call::<Table>((
lua.to_value(&self.step_name).unwrap(),
lua.to_value(&self.product_kinds).unwrap(),
lua.to_value(&self.args).unwrap(),
))
.expect("Lua error");
// Convert result to Rust
let result = result_table
.sequence_values()
.map(|s| s.expect("Lua error"))
.map(|v| lua.from_value(v).expect("Deserialise error"))
.collect::<Vec<ReportingProductId>>();
result
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> {
// Call to plugin
let (lua, plugin) = load_plugin(&context.plugin_dir, &self.plugin_path);
let _result_table = plugin
.execute
.call::<Table>((
lua.to_value(&self.step_name).unwrap(),
lua.to_value(&self.product_kinds).unwrap(),
lua.to_value(&self.args).unwrap(),
))
.expect("Lua error");
eprintln!("Stub: Lua plugin execute");
Ok(ReportingProducts::new())
}
}
/// Format the [Table] as a string
fn _dbg_table(table: Table) -> String {
format!(
"{{{}}}",
table
.pairs::<Value, Value>()
.map(|p| p.expect("Lua error"))
.map(|(k, v)| format!("{:?}: {:?}", k, v))
.collect::<Vec<_>>()
.join(", ")
)
}

View File

@ -33,7 +33,7 @@ use super::executor::ReportingExecutionError;
use super::types::{
BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs,
ReportingStepDynamicBuilder, ReportingStepId, Transactions, VoidArgs,
ReportingStepDynamicBuilder, ReportingStepId, Transactions,
};
/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module
@ -49,7 +49,7 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) {
/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates
#[derive(Debug)]
pub struct BalancesAtToBalancesBetween {
step_name: &'static str,
step_name: String,
args: DateStartDateEndArgs,
}
@ -65,55 +65,53 @@ impl BalancesAtToBalancesBetween {
}
fn can_build(
name: &'static str,
name: &str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool {
// Check for BalancesAt, BalancesAt -> BalancesBetween
if kind == ReportingProductKind::BalancesBetween {
if !args.is::<DateStartDateEndArgs>() {
return false;
}
let args = args.downcast_ref::<DateStartDateEndArgs>().unwrap();
match has_step_or_can_build(
&ReportingProductId {
name,
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
date: args.date_start.clone(),
}),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
if let ReportingStepArgs::DateStartDateEndArgs(args) = args {
match has_step_or_can_build(
&ReportingProductId {
name: name.to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: args.date_start.clone(),
}),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
}
HasStepOrCanBuild::None => {}
}
HasStepOrCanBuild::None => {}
} else {
return false;
}
}
return false;
}
fn build(
name: &'static str,
name: String,
_kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box<dyn ReportingStep> {
Box::new(BalancesAtToBalancesBetween {
step_name: name,
args: *args.downcast().unwrap(),
args: args.into(),
})
}
}
@ -131,9 +129,9 @@ impl Display for BalancesAtToBalancesBetween {
impl ReportingStep for BalancesAtToBalancesBetween {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name,
product_kinds: &[ReportingProductKind::BalancesBetween],
args: Box::new(self.args.clone()),
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesBetween],
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}
}
@ -141,16 +139,16 @@ impl ReportingStep for BalancesAtToBalancesBetween {
// BalancesAtToBalancesBetween depends on BalancesAt at both time points
vec![
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day
}),
},
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end,
}),
},
@ -169,9 +167,9 @@ impl ReportingStep for BalancesAtToBalancesBetween {
// Get balances at dates
let balances_start = &products
.get_or_err(&ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day
}),
})?
@ -181,9 +179,9 @@ impl ReportingStep for BalancesAtToBalancesBetween {
let balances_end = &products
.get_or_err(&ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end,
}),
})?
@ -207,7 +205,7 @@ impl ReportingStep for BalancesAtToBalancesBetween {
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
Box::new(balances),
);
@ -218,7 +216,7 @@ impl ReportingStep for BalancesAtToBalancesBetween {
/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [PostUnreconciledStatementLines][super::steps::PostUnreconciledStatementLines])
#[derive(Debug)]
pub struct GenerateBalances {
step_name: &'static str,
step_name: String,
args: DateArgs,
}
@ -232,9 +230,9 @@ impl GenerateBalances {
}
fn can_build(
name: &'static str,
name: &str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
@ -244,7 +242,7 @@ impl GenerateBalances {
// Try DateArgs
match has_step_or_can_build(
&ReportingProductId {
name,
name: name.to_string(),
kind: ReportingProductKind::Transactions,
args: args.clone(),
},
@ -260,7 +258,7 @@ impl GenerateBalances {
}
HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions
let step = lookup_fn(args.clone());
let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 {
return true;
}
@ -271,9 +269,9 @@ impl GenerateBalances {
// Try VoidArgs
match has_step_or_can_build(
&ReportingProductId {
name,
name: name.to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
steps,
dependencies,
@ -287,7 +285,7 @@ impl GenerateBalances {
}
HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions
let step = lookup_fn(args.clone());
let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 {
return true;
}
@ -299,16 +297,16 @@ impl GenerateBalances {
}
fn build(
name: &'static str,
name: String,
_kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box<dyn ReportingStep> {
Box::new(GenerateBalances {
step_name: name,
args: *args.downcast().unwrap(),
args: args.into(),
})
}
}
@ -323,9 +321,9 @@ impl Display for GenerateBalances {
impl ReportingStep for GenerateBalances {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name,
product_kinds: &[ReportingProductKind::BalancesAt],
args: Box::new(self.args.clone()),
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
@ -341,9 +339,9 @@ impl ReportingStep for GenerateBalances {
// Try DateArgs
match has_step_or_can_build(
&ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
steps,
dependencies,
@ -355,9 +353,9 @@ impl ReportingStep for GenerateBalances {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
);
return;
@ -369,9 +367,9 @@ impl ReportingStep for GenerateBalances {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
);
}
@ -408,9 +406,9 @@ impl ReportingStep for GenerateBalances {
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
@ -423,7 +421,7 @@ impl ReportingStep for GenerateBalances {
/// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available
#[derive(Debug)]
pub struct UpdateBalancesAt {
step_name: &'static str,
step_name: String,
args: DateArgs,
}
@ -439,75 +437,74 @@ impl UpdateBalancesAt {
}
fn can_build(
name: &'static str,
name: &str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool {
if !args.is::<DateArgs>() {
return false;
}
if let ReportingStepArgs::DateArgs(args) = args {
// Check for Transactions -> BalancesAt
if kind == ReportingProductKind::BalancesAt {
// Initially no need to check args
if let Some(step) = steps.iter().find(|s| {
s.id().name == name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
}) {
// Check for BalancesAt -> Transactions
let dependencies_for_step = dependencies.dependencies_for_step(&step.id());
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt
{
return true;
}
// Check for Transactions -> BalancesAt
if kind == ReportingProductKind::BalancesAt {
// Initially no need to check args
if let Some(step) = steps.iter().find(|s| {
s.id().name == name
&& s.id()
.product_kinds
.contains(&ReportingProductKind::Transactions)
}) {
// Check for BalancesAt -> Transactions
let dependencies_for_step = dependencies.dependencies_for_step(&step.id());
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt
{
return true;
}
// Check if BalancesBetween -> Transactions and BalancesAt is available
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind
== ReportingProductKind::BalancesBetween
{
match has_step_or_can_build(
&ReportingProductId {
name: dependencies_for_step[0].product.name,
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
date: args.downcast_ref::<DateArgs>().unwrap().date,
}),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
// Check if BalancesBetween -> Transactions and BalancesAt is available
if dependencies_for_step.len() == 1
&& dependencies_for_step[0].product.kind
== ReportingProductKind::BalancesBetween
{
match has_step_or_can_build(
&ReportingProductId {
name: dependencies_for_step[0].product.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs { date: args.date }),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
}
HasStepOrCanBuild::None => {}
}
HasStepOrCanBuild::None => {}
}
}
}
return false;
} else {
return false;
}
return false;
}
fn build(
name: &'static str,
name: String,
_kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box<dyn ReportingStep> {
Box::new(UpdateBalancesAt {
step_name: name,
args: *args.downcast().unwrap(),
args: args.into(),
})
}
}
@ -522,9 +519,9 @@ impl Display for UpdateBalancesAt {
impl ReportingStep for UpdateBalancesAt {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name,
product_kinds: &[ReportingProductKind::BalancesAt],
args: Box::new(self.args.clone()),
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
@ -549,7 +546,7 @@ impl ReportingStep for UpdateBalancesAt {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args.clone(),
},
@ -567,9 +564,9 @@ impl ReportingStep for UpdateBalancesAt {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: dependency.name,
name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date,
}),
},
@ -600,7 +597,7 @@ impl ReportingStep for UpdateBalancesAt {
// Get transactions
let transactions = &products
.get_or_err(&ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
})?
@ -624,9 +621,9 @@ impl ReportingStep for UpdateBalancesAt {
// As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available
opening_balances_at = products
.get_or_err(&ReportingProductId {
name: dependency.name,
name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date,
}),
})?
@ -649,9 +646,9 @@ impl ReportingStep for UpdateBalancesAt {
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
@ -662,7 +659,7 @@ impl ReportingStep for UpdateBalancesAt {
/// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween]
#[derive(Debug)]
pub struct UpdateBalancesBetween {
step_name: &'static str,
step_name: String,
args: DateStartDateEndArgs,
}
@ -676,9 +673,9 @@ impl UpdateBalancesBetween {
}
fn can_build(
name: &'static str,
name: &str,
kind: ReportingProductKind,
_args: &Box<dyn ReportingStepArgs>,
_args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
@ -706,16 +703,16 @@ impl UpdateBalancesBetween {
}
fn build(
name: &'static str,
name: String,
_kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box<dyn ReportingStep> {
Box::new(UpdateBalancesBetween {
step_name: name,
args: *args.downcast().unwrap(),
args: args.into(),
})
}
}
@ -730,9 +727,9 @@ impl Display for UpdateBalancesBetween {
impl ReportingStep for UpdateBalancesBetween {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: self.step_name,
product_kinds: &[ReportingProductKind::BalancesBetween],
args: Box::new(self.args.clone()),
name: self.step_name.clone(),
product_kinds: vec![ReportingProductKind::BalancesBetween],
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}
}
@ -757,7 +754,7 @@ impl ReportingStep for UpdateBalancesBetween {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
},
@ -767,11 +764,10 @@ impl ReportingStep for UpdateBalancesBetween {
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
if *balances_between_product
.args
.downcast_ref::<DateStartDateEndArgs>()
.unwrap() == self.args
{
if matches!(
balances_between_product.args,
ReportingStepArgs::DateStartDateEndArgs(_)
) {
// Directly depends on BalanceBetween -> Transaction with appropriate date
// Do not need to add extra dependencies
} else {
@ -779,9 +775,9 @@ impl ReportingStep for UpdateBalancesBetween {
dependencies.add_dependency(
self.id(),
ReportingProductId {
name: balances_between_product.name,
name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
);
}
@ -810,7 +806,7 @@ impl ReportingStep for UpdateBalancesBetween {
// Get transactions
let transactions = &products
.get_or_err(&ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::Transactions,
args: parent_step.id().args,
})?
@ -825,9 +821,9 @@ impl ReportingStep for UpdateBalancesBetween {
// Get opening balances
let opening_balances = &products
.get_or_err(&ReportingProductId {
name: balances_between_product.name,
name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
})?
.downcast_ref::<BalancesBetween>()
.unwrap()
@ -849,9 +845,9 @@ impl ReportingStep for UpdateBalancesBetween {
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.step_name,
name: self.step_name.clone(),
kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()),
args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
},
Box::new(balances),
);

View File

@ -96,7 +96,7 @@ pub fn has_step_or_can_build<'a, 'b>(
.find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind))
{
let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap();
if takes_args_fn(&product.args) {
if takes_args_fn(&product.name, &product.args, context) {
return HasStepOrCanBuild::CanLookup(*from_args_fn);
}
}
@ -104,7 +104,7 @@ pub fn has_step_or_can_build<'a, 'b>(
// No explicit step for product - try builders
for builder in context.step_dynamic_builders.iter() {
if (builder.can_build)(
product.name,
&product.name,
product.kind,
&product.args,
steps,
@ -133,7 +133,7 @@ fn build_step_for_product(
panic!("Attempted to call build_step_for_product for already existing step")
}
HasStepOrCanBuild::CanLookup(from_args_fn) => {
new_step = from_args_fn(product.args.clone());
new_step = from_args_fn(&product.name, product.args.clone(), context);
// Check new step meets the dependency
if new_step.id().name != product.name {
@ -162,7 +162,7 @@ fn build_step_for_product(
}
HasStepOrCanBuild::CanBuild(builder) => {
new_step = (builder.build)(
product.name,
product.name.clone(),
product.kind,
product.args.clone(),
&steps,

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

File diff suppressed because it is too large Load Diff

View File

@ -24,14 +24,13 @@ use async_trait::async_trait;
use chrono::NaiveDate;
use downcast_rs::Downcast;
use dyn_clone::DynClone;
use dyn_eq::DynEq;
use dyn_hash::DynHash;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::db::DbConnection;
use crate::model::transaction::TransactionWithPostings;
use crate::plugin::PluginSpec;
use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies;
@ -44,30 +43,38 @@ use super::executor::ReportingExecutionError;
pub struct ReportingContext {
// Configuration
pub db_connection: DbConnection,
pub plugin_dir: String,
pub plugin_names: Vec<String>,
pub eofy_date: NaiveDate,
pub reporting_commodity: String,
// State
pub(crate) step_lookup_fn: HashMap<
(&'static str, &'static [ReportingProductKind]),
(String, Vec<ReportingProductKind>),
(ReportingStepTakesArgsFn, ReportingStepFromArgsFn),
>,
pub(crate) step_dynamic_builders: Vec<ReportingStepDynamicBuilder>,
pub(crate) plugin_specs: HashMap<String, PluginSpec>,
}
impl ReportingContext {
/// Initialise a new [ReportingContext]
pub fn new(
db_connection: DbConnection,
plugin_dir: String,
plugin_names: Vec<String>,
eofy_date: NaiveDate,
reporting_commodity: String,
) -> Self {
Self {
db_connection,
plugin_dir,
plugin_names,
eofy_date,
reporting_commodity,
step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::new(),
plugin_specs: HashMap::new(),
}
}
@ -76,8 +83,8 @@ impl ReportingContext {
/// A lookup function generates concrete [ReportingStep]s from a [ReportingStepId].
pub fn register_lookup_fn(
&mut self,
name: &'static str,
product_kinds: &'static [ReportingProductKind],
name: String,
product_kinds: Vec<ReportingProductKind>,
takes_args_fn: ReportingStepTakesArgsFn,
from_args_fn: ReportingStepFromArgsFn,
) {
@ -102,12 +109,14 @@ impl ReportingContext {
/// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep]
///
/// See [ReportingContext::register_lookup_fn].
pub type ReportingStepTakesArgsFn = fn(args: &Box<dyn ReportingStepArgs>) -> bool;
pub type ReportingStepTakesArgsFn =
fn(name: &str, args: &ReportingStepArgs, context: &ReportingContext) -> bool;
/// Function which builds a concrete [ReportingStep] from the given [ReportingStepArgs]
///
/// See [ReportingContext::register_lookup_fn].
pub type ReportingStepFromArgsFn = fn(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep>;
pub type ReportingStepFromArgsFn =
fn(name: &str, args: ReportingStepArgs, context: &ReportingContext) -> Box<dyn ReportingStep>;
// -------------------------------
// REPORTING STEP DYNAMIC BUILDERS
@ -118,17 +127,17 @@ pub type ReportingStepFromArgsFn = fn(args: Box<dyn ReportingStepArgs>) -> Box<d
pub struct ReportingStepDynamicBuilder {
pub name: &'static str,
pub can_build: fn(
name: &'static str,
name: &str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool,
pub build: fn(
name: &'static str,
name: String,
kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
args: ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
@ -139,11 +148,11 @@ pub struct ReportingStepDynamicBuilder {
// REPORTING PRODUCTS
/// Identifies a [ReportingProduct]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ReportingProductId {
pub name: &'static str,
pub name: String,
pub kind: ReportingProductKind,
pub args: Box<dyn ReportingStepArgs>,
pub args: ReportingStepArgs,
}
impl Display for ReportingProductId {
@ -155,7 +164,7 @@ impl Display for ReportingProductId {
/// Identifies a type of [Box]ed [ReportingProduct]
///
/// See [Box::downcast].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum ReportingProductKind {
/// The [Box]ed [ReportingProduct] is a [Transactions]
Transactions,
@ -276,9 +285,9 @@ impl Display for ReportingProducts {
/// Identifies a [ReportingStep]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReportingStepId {
pub name: &'static str,
pub product_kinds: &'static [ReportingProductKind],
pub args: Box<dyn ReportingStepArgs>,
pub name: String,
pub product_kinds: Vec<ReportingProductKind>,
pub args: ReportingStepArgs,
}
impl Display for ReportingStepId {
@ -345,50 +354,68 @@ downcast_rs::impl_downcast!(ReportingStep);
// REPORTING STEP ARGUMENTS
/// Represents arguments to a [ReportingStep]
pub trait ReportingStepArgs:
Debug + Display + Downcast + DynClone + DynEq + DynHash + Send + Sync
{
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum ReportingStepArgs {
// This is an enum not a trait, to simply conversion to and from Lua
/// [ReportingStepArgs] implementation which takes no arguments
VoidArgs,
/// [ReportingStepArgs] implementation which takes a single date
DateArgs(DateArgs),
/// [ReportingStepArgs] implementation which takes a date range
DateStartDateEndArgs(DateStartDateEndArgs),
/// [ReportingStepArgs] implementation which takes multiple [DateArgs]
MultipleDateArgs(MultipleDateArgs),
/// [ReportingStepArgs] implementation which takes multiple [DateStartDateEndArgs]
MultipleDateStartDateEndArgs(MultipleDateStartDateEndArgs),
}
downcast_rs::impl_downcast!(ReportingStepArgs);
dyn_clone::clone_trait_object!(ReportingStepArgs);
dyn_eq::eq_trait_object!(ReportingStepArgs);
dyn_hash::hash_trait_object!(ReportingStepArgs);
/// [ReportingStepArgs] implementation which takes no arguments
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct VoidArgs {}
impl ReportingStepArgs for VoidArgs {}
impl Display for VoidArgs {
impl Display for ReportingStepArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(""))
match self {
ReportingStepArgs::VoidArgs => f.write_str("void"),
ReportingStepArgs::DateArgs(args) => f.write_fmt(format_args!("{}", args)),
ReportingStepArgs::DateStartDateEndArgs(args) => f.write_fmt(format_args!("{}", args)),
ReportingStepArgs::MultipleDateArgs(args) => f.write_fmt(format_args!("{}", args)),
ReportingStepArgs::MultipleDateStartDateEndArgs(args) => {
f.write_fmt(format_args!("{}", args))
}
}
}
}
/// [ReportingStepArgs] implementation which takes a single date
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct DateArgs {
#[serde(with = "crate::serde::naivedate_to_js")]
pub date: NaiveDate,
}
impl ReportingStepArgs for DateArgs {}
impl Display for DateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.date))
}
}
/// [ReportingStepArgs] implementation which takes a date range
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DateStartDateEndArgs {
pub date_start: NaiveDate,
pub date_end: NaiveDate,
impl Into<DateArgs> for ReportingStepArgs {
fn into(self) -> DateArgs {
if let ReportingStepArgs::DateArgs(args) = self {
args
} else {
panic!("Expected DateArgs")
}
}
}
impl ReportingStepArgs for DateStartDateEndArgs {}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct DateStartDateEndArgs {
#[serde(with = "crate::serde::naivedate_to_js")]
pub date_start: NaiveDate,
#[serde(with = "crate::serde::naivedate_to_js")]
pub date_end: NaiveDate,
}
impl Display for DateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -396,14 +423,21 @@ impl Display for DateStartDateEndArgs {
}
}
/// [ReportingStepArgs] implementation which takes multiple [DateArgs]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
impl Into<DateStartDateEndArgs> for ReportingStepArgs {
fn into(self) -> DateStartDateEndArgs {
if let ReportingStepArgs::DateStartDateEndArgs(args) = self {
args
} else {
panic!("Expected DateStartDateEndArgs")
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct MultipleDateArgs {
pub dates: Vec<DateArgs>,
}
impl ReportingStepArgs for MultipleDateArgs {}
impl Display for MultipleDateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
@ -417,14 +451,21 @@ impl Display for MultipleDateArgs {
}
}
/// [ReportingStepArgs] implementation which takes multiple [DateStartDateEndArgs]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
impl Into<MultipleDateArgs> for ReportingStepArgs {
fn into(self) -> MultipleDateArgs {
if let ReportingStepArgs::MultipleDateArgs(args) = self {
args
} else {
panic!("Expected MultipleDateArgs")
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct MultipleDateStartDateEndArgs {
pub dates: Vec<DateStartDateEndArgs>,
}
impl ReportingStepArgs for MultipleDateStartDateEndArgs {}
impl Display for MultipleDateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
@ -437,3 +478,13 @@ impl Display for MultipleDateStartDateEndArgs {
))
}
}
impl Into<MultipleDateStartDateEndArgs> for ReportingStepArgs {
fn into(self) -> MultipleDateStartDateEndArgs {
if let ReportingStepArgs::MultipleDateStartDateEndArgs(args) = self {
args
} else {
panic!("Expected MultipleDateStartDateEndArgs")
}
}
}

View File

@ -16,6 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/// Serialises [chrono::NaiveDate] in database format
///
/// Use as `#[serde(with = "crate::serde::naivedate_to_js")]`, etc.
pub mod naivedate_to_js {
use std::fmt;
use chrono::NaiveDate;
use serde::{
de::{self, Unexpected, Visitor},
Deserializer, Serializer,
};
pub(crate) fn serialize<S: Serializer>(
dt: &NaiveDate,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&dt.format("%Y-%m-%d").to_string())
}
struct DateVisitor;
impl<'de> Visitor<'de> for DateVisitor {
type Value = NaiveDate;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a date string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match NaiveDate::parse_from_str(s, "%Y-%m-%d") {
Ok(dt) => Ok(dt),
Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)),
}
}
}
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<NaiveDate, D::Error> {
deserializer.deserialize_str(DateVisitor)
}
}
/// Serialises [chrono::NaiveDateTime] in database format
///
/// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc.
@ -40,7 +85,7 @@ pub mod naivedatetime_to_js {
type Value = NaiveDateTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a date string")
write!(formatter, "a datetime string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>

110
src-tauri/Cargo.lock generated
View File

@ -370,6 +370,16 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -878,7 +888,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
"libloading 0.8.8",
]
[[package]]
@ -977,18 +987,6 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "dyn-eq"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388"
[[package]]
name = "dyn-hash"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88"
[[package]]
name = "either"
version = "1.13.0"
@ -2165,7 +2163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
dependencies = [
"gtk-sys",
"libloading",
"libloading 0.7.4",
"once_cell",
]
@ -2183,9 +2181,8 @@ dependencies = [
"chrono",
"downcast-rs 2.0.1",
"dyn-clone",
"dyn-eq",
"dyn-hash",
"indexmap 2.9.0",
"mlua",
"serde",
"serde_json",
"sqlx",
@ -2202,6 +2199,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]]
name = "libm"
version = "0.2.11"
@ -2257,6 +2264,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "luau0-src"
version = "0.12.3+luau663"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ae337c644bbf86a8d8e9ce3ee023311833d41741baf5e51acc31b37843aba1"
dependencies = [
"cc",
]
[[package]]
name = "mac"
version = "0.1.1"
@ -2351,6 +2367,37 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mlua"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0"
dependencies = [
"bstr",
"either",
"erased-serde",
"libloading 0.8.8",
"mlua-sys",
"num-traits",
"parking_lot",
"rustc-hash",
"rustversion",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
dependencies = [
"cc",
"cfg-if",
"luau0-src",
"pkg-config",
]
[[package]]
name = "muda"
version = "0.15.3"
@ -2769,6 +2816,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -3424,6 +3480,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -3446,6 +3508,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.18"
@ -3549,6 +3617,16 @@ dependencies = [
"typeid",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.219"

View File

@ -17,21 +17,25 @@
*/
use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::types::{ReportingProductId, ReportingProductKind, VoidArgs};
use tauri::State;
use libdrcr::reporting::types::{ReportingProductId, ReportingProductKind, ReportingStepArgs};
use tauri::{AppHandle, State};
use tokio::sync::Mutex;
use crate::libdrcr_bridge::get_report;
use crate::AppState;
#[tauri::command]
pub(crate) async fn get_tax_summary(state: State<'_, Mutex<AppState>>) -> Result<String, ()> {
pub(crate) async fn get_tax_summary(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
) -> Result<String, ()> {
Ok(get_report(
app,
state,
&ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
)
.await

View File

@ -26,22 +26,29 @@ use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::types::{
BalancesAt, DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
ReportingContext, ReportingProduct, ReportingProductId, ReportingProductKind, Transactions,
VoidArgs,
ReportingContext, ReportingProduct, ReportingProductId, ReportingProductKind,
ReportingStepArgs, Transactions,
};
use serde::{Deserialize, Serialize};
use tauri::State;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, State};
use tokio::sync::Mutex;
use crate::AppState;
fn prepare_reporting_context(context: &mut ReportingContext) {
libdrcr::austax::register_lookup_fns(context);
libdrcr::reporting::steps::register_lookup_fns(context);
libdrcr::reporting::builders::register_dynamic_builders(context);
libdrcr::plugin::register_lookup_fns(context);
}
fn get_plugins() -> Vec<String> {
// FIXME: Dynamically get this
vec!["austax.austax".to_string()]
}
pub(crate) async fn get_report(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
target: &ReportingProductId,
) -> Box<dyn ReportingProduct> {
@ -54,16 +61,27 @@ pub(crate) async fn get_report(
// Initialise ReportingContext
let eofy_date = db_connection.metadata().eofy_date;
let mut context = ReportingContext::new(db_connection, eofy_date, "$".to_string());
let mut context = ReportingContext::new(
db_connection,
app.path()
.resolve("plugins", BaseDirectory::Resource)
.unwrap()
.to_str()
.unwrap()
.to_string(),
get_plugins(),
eofy_date,
"$".to_string(),
);
prepare_reporting_context(&mut context);
// Get dynamic report
let targets = vec![
// FIXME: Make this configurable
ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
},
target.clone(),
];
@ -75,14 +93,16 @@ pub(crate) async fn get_report(
#[tauri::command]
pub(crate) async fn get_all_transactions_except_earnings_to_equity(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
) -> Result<String, ()> {
let transactions = get_report(
app,
state,
&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(),
}),
},
@ -97,15 +117,17 @@ pub(crate) async fn get_all_transactions_except_earnings_to_equity(
#[tauri::command]
pub(crate) async fn get_all_transactions_except_earnings_to_equity_for_account(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
account: String,
) -> Result<String, ()> {
let transactions = get_report(
app,
state,
&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(),
}),
},
@ -126,6 +148,7 @@ pub(crate) async fn get_all_transactions_except_earnings_to_equity_for_account(
#[tauri::command]
pub(crate) async fn get_balance_sheet(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
dates: Vec<String>,
) -> Result<String, ()> {
@ -137,11 +160,12 @@ pub(crate) async fn get_balance_sheet(
}
Ok(get_report(
app,
state,
&ReportingProductId {
name: "BalanceSheet",
name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs {
args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: date_args.clone(),
}),
},
@ -154,6 +178,7 @@ pub(crate) async fn get_balance_sheet(
#[tauri::command]
pub(crate) async fn get_income_statement(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
dates: Vec<(String, String)>,
) -> Result<String, ()> {
@ -166,11 +191,12 @@ pub(crate) async fn get_income_statement(
}
Ok(get_report(
app,
state,
&ReportingProductId {
name: "IncomeStatement",
name: "IncomeStatement".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateStartDateEndArgs {
args: ReportingStepArgs::MultipleDateStartDateEndArgs(MultipleDateStartDateEndArgs {
dates: date_args.clone(),
}),
},
@ -183,17 +209,19 @@ pub(crate) async fn get_income_statement(
#[tauri::command]
pub(crate) async fn get_trial_balance(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
date: String,
) -> Result<String, ()> {
let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date");
Ok(get_report(
app,
state,
&ReportingProductId {
name: "TrialBalance",
name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport,
args: Box::new(DateArgs { date }),
args: ReportingStepArgs::DateArgs(DateArgs { date }),
},
)
.await
@ -211,6 +239,7 @@ struct ValidatedBalanceAssertion {
#[tauri::command]
pub(crate) async fn get_validated_balance_assertions(
app: AppHandle,
state: State<'_, Mutex<AppState>>,
) -> Result<String, ()> {
let state = state.lock().await;
@ -233,21 +262,32 @@ pub(crate) async fn get_validated_balance_assertions(
// Initialise ReportingContext
let eofy_date = db_connection.metadata().eofy_date;
let mut context = ReportingContext::new(db_connection, eofy_date, "$".to_string());
let mut context = ReportingContext::new(
db_connection,
app.path()
.resolve("plugins", BaseDirectory::Resource)
.unwrap()
.to_str()
.unwrap()
.to_string(),
get_plugins(),
eofy_date,
"$".to_string(),
);
prepare_reporting_context(&mut context);
// Get report targets
let mut targets = vec![ReportingProductId {
name: "CalculateIncomeTax",
name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
args: ReportingStepArgs::VoidArgs,
}];
for dt in dates {
// Request ordinary transaction balances at each balance assertion date
targets.push(ReportingProductId {
name: "CombineOrdinaryTransactions",
name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { date: dt.date() }),
args: ReportingStepArgs::DateArgs(DateArgs { date: dt.date() }),
});
}
@ -259,9 +299,9 @@ pub(crate) async fn get_validated_balance_assertions(
for balance_assertion in balance_assertions {
let balances_at_date = products
.get_or_err(&ReportingProductId {
name: "CombineOrdinaryTransactions",
name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
args: ReportingStepArgs::DateArgs(DateArgs {
date: balance_assertion.dt.date(),
}),
})

View File

@ -26,6 +26,9 @@
"targets": "all",
"icon": [
"icons/icon.png"
]
],
"resources": {
"../libdrcr/plugins/": "plugins/"
}
}
}

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

@ -1,6 +1,6 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222025 Lee Yingtong Li (RunasSudo)
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
@ -67,54 +67,6 @@ export const db = reactive({
},
});
export async function totalBalances(session: ExtendedDatabase): Promise<Map<string, number>> {
const resultsRaw: {account: string, quantity: number}[] = await session.select(
`-- Get last transaction for each account
WITH max_dt_by_account AS (
SELECT account, max(dt) AS max_dt
FROM joined_transactions
GROUP BY account
),
max_tid_by_account AS (
SELECT max_dt_by_account.account, max(transaction_id) AS max_tid
FROM max_dt_by_account
JOIN joined_transactions ON max_dt_by_account.account = joined_transactions.account AND max_dt_by_account.max_dt = joined_transactions.dt
GROUP BY max_dt_by_account.account
)
-- Get running balance at last transaction for each account
SELECT max_tid_by_account.account, running_balance AS quantity
FROM max_tid_by_account
JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account`
);
return new Map(resultsRaw.map((x) => [x.account, x.quantity]));
}
export async function totalBalancesAtDate(session: ExtendedDatabase, dt: string): Promise<Map<string, number>> {
const resultsRaw: {account: string, quantity: number}[] = await session.select(
`-- Get last transaction for each account
WITH max_dt_by_account AS (
SELECT account, max(dt) AS max_dt
FROM joined_transactions
WHERE DATE(dt) <= DATE($1)
GROUP BY account
),
max_tid_by_account AS (
SELECT max_dt_by_account.account, max(transaction_id) AS max_tid
FROM max_dt_by_account
JOIN joined_transactions ON max_dt_by_account.account = joined_transactions.account AND max_dt_by_account.max_dt = joined_transactions.dt
GROUP BY max_dt_by_account.account
)
-- Get running balance at last transaction for each account
SELECT max_tid_by_account.account, running_balance AS quantity
FROM max_tid_by_account
JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account`,
[dt]
);
return new Map(resultsRaw.map((x) => [x.account, x.quantity]));
}
export function joinedToTransactions(joinedTransactionPostings: JoinedTransactionPosting[]): Transaction[] {
// Group postings into transactions
const transactions: Transaction[] = [];
@ -143,18 +95,6 @@ export function joinedToTransactions(joinedTransactionPostings: JoinedTransactio
return transactions;
}
export async function getAccountsForKind(session: ExtendedDatabase, kind: string): Promise<string[]> {
const rawAccountsForKind: {account: string}[] = await session.select(
`SELECT account
FROM account_configurations
WHERE kind = $1
ORDER BY account`,
[kind]
);
const accountsForKind = rawAccountsForKind.map((a) => a.account);
return accountsForKind;
}
export function serialiseAmount(quantity: number, commodity: string): string {
// Pretty print the amount for an editable input
if (quantity < 0) {

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;
}
}