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", "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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.17.0" version = "3.17.0"
@ -278,18 +288,6 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" 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]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@ -305,6 +303,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "etcetera" name = "etcetera"
version = "0.8.0" version = "0.8.0"
@ -691,15 +699,24 @@ dependencies = [
"chrono", "chrono",
"downcast-rs", "downcast-rs",
"dyn-clone", "dyn-clone",
"dyn-eq",
"dyn-hash",
"indexmap", "indexmap",
"mlua",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tokio", "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]] [[package]]
name = "libm" name = "libm"
version = "0.2.15" version = "0.2.15"
@ -739,6 +756,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 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]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -775,6 +801,37 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -837,6 +894,15 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 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]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -1021,6 +1087,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.20" version = "1.0.20"
@ -1048,6 +1120,16 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.219"
@ -1527,6 +1609,12 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"

View File

@ -8,9 +8,8 @@ async-trait = "0.1.88"
chrono = "0.4.41" chrono = "0.4.41"
downcast-rs = "2.0.1" downcast-rs = "2.0.1"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
dyn-eq = "0.1.3"
dyn-hash = "0.2.2"
indexmap = "2.9.0" indexmap = "2.9.0"
mlua = { version = "0.10", features = ["luau", "serialize"] }
serde = "1.0.219" serde = "1.0.219"
serde_json = "1.0.140" serde_json = "1.0.140"
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } 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::model::transaction::{Posting, Transaction, TransactionWithPostings};
use crate::reporting::calculator::ReportingGraphDependencies; use crate::reporting::calculator::ReportingGraphDependencies;
use crate::reporting::dynamic_report::{ use crate::reporting::dynamic_report::{
entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, entries_for_kind, DynamicReport, DynamicReportEntry, Row, Section,
CalculatableSection, CalculatedRow, DynamicReport, LiteralRow,
}; };
use crate::reporting::executor::ReportingExecutionError; use crate::reporting::executor::ReportingExecutionError;
use crate::reporting::steps::AllTransactionsExceptEarningsToEquityBalances; use crate::reporting::steps::AllTransactionsExceptEarningsToEquityBalances;
@ -44,6 +43,53 @@ use crate::util::sofy_from_eofy;
use crate::{QuantityInt, INCOME_TAX, INCOME_TAX_CONTROL}; use crate::{QuantityInt, INCOME_TAX, INCOME_TAX_CONTROL};
// Constants and tax calculations // 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 { fn get_grossedup_rfb(taxable_value: QuantityInt) -> QuantityInt {
// FIXME: May vary from year to year // FIXME: May vary from year to year
((taxable_value as f64) * 2.0802) as QuantityInt ((taxable_value as f64) * 2.0802) as QuantityInt
@ -90,8 +136,8 @@ pub struct CalculateIncomeTax {}
impl CalculateIncomeTax { impl CalculateIncomeTax {
fn register_lookup_fn(context: &mut ReportingContext) { fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn( context.register_lookup_fn(
"CalculateIncomeTax", "CalculateIncomeTax".to_string(),
&[ReportingProductKind::Transactions], vec![ReportingProductKind::Transactions],
Self::takes_args, Self::takes_args,
Self::from_args, Self::from_args,
); );
@ -116,8 +162,8 @@ impl Display for CalculateIncomeTax {
impl ReportingStep for CalculateIncomeTax { impl ReportingStep for CalculateIncomeTax {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
name: "CalculateIncomeTax", name: "CalculateIncomeTax".to_string(),
product_kinds: &[ product_kinds: vec![
ReportingProductKind::DynamicReport, ReportingProductKind::DynamicReport,
ReportingProductKind::Transactions, ReportingProductKind::Transactions,
], ],
@ -128,7 +174,7 @@ impl ReportingStep for CalculateIncomeTax {
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> { fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
// CalculateIncomeTax depends on CombineOrdinaryTransactions // CalculateIncomeTax depends on CombineOrdinaryTransactions
vec![ReportingProductId { vec![ReportingProductId {
name: "CombineOrdinaryTransactions", name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs { args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date), date_start: sofy_from_eofy(context.eofy_date),
@ -152,8 +198,8 @@ impl ReportingStep for CalculateIncomeTax {
other.id(), other.id(),
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: other.product_kinds[0], kind: other.product_kind,
args: if other.product_kinds[0] == ReportingProductKind::Transactions { args: if other.product_kind == ReportingProductKind::Transactions {
Box::new(VoidArgs {}) Box::new(VoidArgs {})
} else { } else {
other.id().args other.id().args
@ -176,7 +222,7 @@ impl ReportingStep for CalculateIncomeTax {
// Get balances for current year // Get balances for current year
let balances = &products let balances = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "CombineOrdinaryTransactions", name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs { args: Box::new(DateStartDateEndArgs {
date_start: sofy_from_eofy(context.eofy_date), date_start: sofy_from_eofy(context.eofy_date),
@ -191,7 +237,156 @@ impl ReportingStep for CalculateIncomeTax {
let kinds_for_account = let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await); 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 let rfb_taxable = balances
.iter() .iter()
.filter(|(acc, _)| { .filter(|(acc, _)| {
@ -202,543 +397,37 @@ impl ReportingStep for CalculateIncomeTax {
}) })
.map(|(_, bal)| *bal) .map(|(_, bal)| *bal)
.sum(); .sum();
let _rfb_grossedup = get_grossedup_rfb(rfb_taxable);
// Generate tax summary report // Base income tax row
let report = CalculatableDynamicReport::new( let tax_base = get_base_income_tax(net_taxable);
"Tax summary".to_string(), report.entries.push(
vec!["$".to_string()], Row {
vec![ text: "Base income tax".to_string(),
CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( quantity: vec![tax_base],
"Salary or wages (1)".to_string(), id: Some("tax_base".to_string()),
Some("income1".to_string()), visible: true,
true, link: None,
true, heading: false,
{ bordered: false,
let mut entries = entries_for_kind_floor( }
"austax.income1", .into(),
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,
},
}),
],
); );
let mut report: DynamicReport = report.calculate(); // Total income tax row
report.auto_hide(); let tax_total = tax_base;
report.entries.push(
let total_tax = report.quantity_for_id("total_tax").unwrap()[0]; 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 // Generate income tax transaction
let transactions = Transactions { let transactions = Transactions {
@ -759,18 +448,18 @@ impl ReportingStep for CalculateIncomeTax {
transaction_id: None, transaction_id: None,
description: None, description: None,
account: INCOME_TAX.to_string(), account: INCOME_TAX.to_string(),
quantity: total_tax, quantity: tax_total,
commodity: context.db_connection.metadata().reporting_commodity.clone(), commodity: context.db_connection.metadata().reporting_commodity.clone(),
quantity_ascost: Some(total_tax), quantity_ascost: Some(tax_total),
}, },
Posting { Posting {
id: None, id: None,
transaction_id: None, transaction_id: None,
description: None, description: None,
account: INCOME_TAX_CONTROL.to_string(), account: INCOME_TAX_CONTROL.to_string(),
quantity: -total_tax, quantity: -tax_total,
commodity: context.db_connection.metadata().reporting_commodity.clone(), 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>>, balances: &Vec<&HashMap<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>, kinds_for_account: &HashMap<String, Vec<String>>,
floor: QuantityInt, floor: QuantityInt,
) -> Vec<CalculatableDynamicReportEntry> { ) -> Vec<DynamicReportEntry> {
let mut entries_for_kind = entries_for_kind(kind, invert, balances, kinds_for_account); let mut entries_for_kind = entries_for_kind(kind, invert, balances, kinds_for_account);
entries_for_kind.iter_mut().for_each(|e| match e { entries_for_kind.iter_mut().for_each(|e| match e {
CalculatableDynamicReportEntry::LiteralRow(row) => row DynamicReportEntry::Row(row) => row
.quantity .quantity
.iter_mut() .iter_mut()
.for_each(|v| *v = (*v / floor) * floor), .for_each(|v| *v = (*v / floor) * floor),

View File

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

View File

@ -25,7 +25,7 @@ use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report; use libdrcr::reporting::generate_report;
use libdrcr::reporting::types::{ use libdrcr::reporting::types::{
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingStepArgs,
}; };
#[tokio::main] #[tokio::main]
@ -38,12 +38,14 @@ async fn main() {
// Initialise ReportingContext // Initialise ReportingContext
let mut context = ReportingContext::new( let mut context = ReportingContext::new(
db_connection, db_connection,
"plugins".to_string(),
vec!["austax.austax".to_string()],
NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
"$".to_string(), "$".to_string(),
); );
libdrcr::plugin::register_lookup_fns(&mut context);
libdrcr::reporting::steps::register_lookup_fns(&mut context); libdrcr::reporting::steps::register_lookup_fns(&mut context);
libdrcr::reporting::builders::register_dynamic_builders(&mut context); libdrcr::reporting::builders::register_dynamic_builders(&mut context);
libdrcr::austax::register_lookup_fns(&mut context);
let context = Arc::new(context); let context = Arc::new(context);
@ -51,9 +53,9 @@ async fn main() {
let targets = vec![ let targets = vec![
ReportingProductId { ReportingProductId {
name: "CalculateIncomeTax", name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
// ReportingProductId { // ReportingProductId {
// name: "AllTransactionsExceptEarningsToEquity", // name: "AllTransactionsExceptEarningsToEquity",
@ -63,18 +65,18 @@ async fn main() {
// }), // }),
// }, // },
ReportingProductId { ReportingProductId {
name: "BalanceSheet", name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs { args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs { dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}], }],
}), }),
}, },
ReportingProductId { ReportingProductId {
name: "IncomeStatement", name: "IncomeStatement".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateStartDateEndArgs { args: ReportingStepArgs::MultipleDateStartDateEndArgs(MultipleDateStartDateEndArgs {
dates: vec![DateStartDateEndArgs { dates: vec![DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
@ -91,14 +93,14 @@ async fn main() {
let targets = vec![ let targets = vec![
ReportingProductId { ReportingProductId {
name: "CalculateIncomeTax", name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
ReportingProductId { ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity", name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs { args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}), }),
@ -109,22 +111,22 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
let result = products // let result = products
.get_or_err(&ReportingProductId { // .get_or_err(&ReportingProductId {
name: "CalculateIncomeTax", // name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::DynamicReport, // kind: ReportingProductKind::DynamicReport,
args: Box::new(VoidArgs {}), // args: ReportingStepArgs::VoidArgs,
}) // })
.unwrap(); // .unwrap();
println!("Tax summary:"); // println!("Tax summary:");
println!("{:?}", result); // println!("{:?}", result);
let result = products let result = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity", name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs { args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}), }),
@ -138,14 +140,14 @@ async fn main() {
let targets = vec![ let targets = vec![
ReportingProductId { ReportingProductId {
name: "CalculateIncomeTax", name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
ReportingProductId { ReportingProductId {
name: "BalanceSheet", name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs { args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs { dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}], }],
@ -158,9 +160,9 @@ async fn main() {
.unwrap(); .unwrap();
let result = products let result = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "BalanceSheet", name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(MultipleDateArgs { args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs {
dates: vec![DateArgs { dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}], }],
@ -178,14 +180,14 @@ async fn main() {
let targets = vec![ let targets = vec![
ReportingProductId { ReportingProductId {
name: "CalculateIncomeTax", name: "CalculateIncomeTax".to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
ReportingProductId { ReportingProductId {
name: "TrialBalance", name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
}), }),
}, },
@ -196,9 +198,9 @@ async fn main() {
.unwrap(); .unwrap();
let result = products let result = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "TrialBalance", name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport, kind: ReportingProductKind::DynamicReport,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), 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::{ use super::types::{
BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, 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 /// 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 /// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates
#[derive(Debug)] #[derive(Debug)]
pub struct BalancesAtToBalancesBetween { pub struct BalancesAtToBalancesBetween {
step_name: &'static str, step_name: String,
args: DateStartDateEndArgs, args: DateStartDateEndArgs,
} }
@ -65,55 +65,53 @@ impl BalancesAtToBalancesBetween {
} }
fn can_build( fn can_build(
name: &'static str, name: &str,
kind: ReportingProductKind, kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>, args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
) -> bool { ) -> bool {
// Check for BalancesAt, BalancesAt -> BalancesBetween // Check for BalancesAt, BalancesAt -> BalancesBetween
if kind == ReportingProductKind::BalancesBetween { if kind == ReportingProductKind::BalancesBetween {
if !args.is::<DateStartDateEndArgs>() { if let ReportingStepArgs::DateStartDateEndArgs(args) = args {
return false; match has_step_or_can_build(
} &ReportingProductId {
name: name.to_string(),
let args = args.downcast_ref::<DateStartDateEndArgs>().unwrap(); kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
match has_step_or_can_build( date: args.date_start.clone(),
&ReportingProductId { }),
name, },
kind: ReportingProductKind::BalancesAt, steps,
args: Box::new(DateArgs { dependencies,
date: args.date_start.clone(), context,
}), ) {
}, HasStepOrCanBuild::HasStep(_)
steps, | HasStepOrCanBuild::CanLookup(_)
dependencies, | HasStepOrCanBuild::CanBuild(_) => {
context, return true;
) { }
HasStepOrCanBuild::HasStep(_) HasStepOrCanBuild::None => {}
| HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_) => {
return true;
} }
HasStepOrCanBuild::None => {} } else {
return false;
} }
} }
return false; return false;
} }
fn build( fn build(
name: &'static str, name: String,
_kind: ReportingProductKind, _kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>, args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
_context: &ReportingContext, _context: &ReportingContext,
) -> Box<dyn ReportingStep> { ) -> Box<dyn ReportingStep> {
Box::new(BalancesAtToBalancesBetween { Box::new(BalancesAtToBalancesBetween {
step_name: name, step_name: name,
args: *args.downcast().unwrap(), args: args.into(),
}) })
} }
} }
@ -131,9 +129,9 @@ impl Display for BalancesAtToBalancesBetween {
impl ReportingStep for BalancesAtToBalancesBetween { impl ReportingStep for BalancesAtToBalancesBetween {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
name: self.step_name, name: self.step_name.clone(),
product_kinds: &[ReportingProductKind::BalancesBetween], product_kinds: vec![ReportingProductKind::BalancesBetween],
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
} }
} }
@ -141,16 +139,16 @@ impl ReportingStep for BalancesAtToBalancesBetween {
// BalancesAtToBalancesBetween depends on BalancesAt at both time points // BalancesAtToBalancesBetween depends on BalancesAt at both time points
vec![ vec![
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, 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 date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day
}), }),
}, },
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end, date: self.args.date_end,
}), }),
}, },
@ -169,9 +167,9 @@ impl ReportingStep for BalancesAtToBalancesBetween {
// Get balances at dates // Get balances at dates
let balances_start = &products let balances_start = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, 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 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 let balances_end = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date_end, date: self.args.date_end,
}), }),
})? })?
@ -207,7 +205,7 @@ impl ReportingStep for BalancesAtToBalancesBetween {
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}, },
Box::new(balances), 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]) /// 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)] #[derive(Debug)]
pub struct GenerateBalances { pub struct GenerateBalances {
step_name: &'static str, step_name: String,
args: DateArgs, args: DateArgs,
} }
@ -232,9 +230,9 @@ impl GenerateBalances {
} }
fn can_build( fn can_build(
name: &'static str, name: &str,
kind: ReportingProductKind, kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>, args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
@ -244,7 +242,7 @@ impl GenerateBalances {
// Try DateArgs // Try DateArgs
match has_step_or_can_build( match has_step_or_can_build(
&ReportingProductId { &ReportingProductId {
name, name: name.to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: args.clone(), args: args.clone(),
}, },
@ -260,7 +258,7 @@ impl GenerateBalances {
} }
HasStepOrCanBuild::CanLookup(lookup_fn) => { HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions // Check for () -> Transactions
let step = lookup_fn(args.clone()); let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 { if step.requires(context).len() == 0 {
return true; return true;
} }
@ -271,9 +269,9 @@ impl GenerateBalances {
// Try VoidArgs // Try VoidArgs
match has_step_or_can_build( match has_step_or_can_build(
&ReportingProductId { &ReportingProductId {
name, name: name.to_string(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
steps, steps,
dependencies, dependencies,
@ -287,7 +285,7 @@ impl GenerateBalances {
} }
HasStepOrCanBuild::CanLookup(lookup_fn) => { HasStepOrCanBuild::CanLookup(lookup_fn) => {
// Check for () -> Transactions // Check for () -> Transactions
let step = lookup_fn(args.clone()); let step = lookup_fn(name, args.clone(), context);
if step.requires(context).len() == 0 { if step.requires(context).len() == 0 {
return true; return true;
} }
@ -299,16 +297,16 @@ impl GenerateBalances {
} }
fn build( fn build(
name: &'static str, name: String,
_kind: ReportingProductKind, _kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>, args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
_context: &ReportingContext, _context: &ReportingContext,
) -> Box<dyn ReportingStep> { ) -> Box<dyn ReportingStep> {
Box::new(GenerateBalances { Box::new(GenerateBalances {
step_name: name, step_name: name,
args: *args.downcast().unwrap(), args: args.into(),
}) })
} }
} }
@ -323,9 +321,9 @@ impl Display for GenerateBalances {
impl ReportingStep for GenerateBalances { impl ReportingStep for GenerateBalances {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
name: self.step_name, name: self.step_name.clone(),
product_kinds: &[ReportingProductKind::BalancesAt], product_kinds: vec![ReportingProductKind::BalancesAt],
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
} }
} }
@ -341,9 +339,9 @@ impl ReportingStep for GenerateBalances {
// Try DateArgs // Try DateArgs
match has_step_or_can_build( match has_step_or_can_build(
&ReportingProductId { &ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
}, },
steps, steps,
dependencies, dependencies,
@ -355,9 +353,9 @@ impl ReportingStep for GenerateBalances {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
}, },
); );
return; return;
@ -369,9 +367,9 @@ impl ReportingStep for GenerateBalances {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}), args: ReportingStepArgs::VoidArgs,
}, },
); );
} }
@ -408,9 +406,9 @@ impl ReportingStep for GenerateBalances {
let mut result = ReportingProducts::new(); let mut result = ReportingProducts::new();
result.insert( result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
}, },
Box::new(balances), 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 /// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available
#[derive(Debug)] #[derive(Debug)]
pub struct UpdateBalancesAt { pub struct UpdateBalancesAt {
step_name: &'static str, step_name: String,
args: DateArgs, args: DateArgs,
} }
@ -439,75 +437,74 @@ impl UpdateBalancesAt {
} }
fn can_build( fn can_build(
name: &'static str, name: &str,
kind: ReportingProductKind, kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>, args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
) -> bool { ) -> bool {
if !args.is::<DateArgs>() { if let ReportingStepArgs::DateArgs(args) = args {
return false; // 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 // Check if BalancesBetween -> Transactions and BalancesAt is available
if kind == ReportingProductKind::BalancesAt { if dependencies_for_step.len() == 1
// Initially no need to check args && dependencies_for_step[0].product.kind
if let Some(step) = steps.iter().find(|s| { == ReportingProductKind::BalancesBetween
s.id().name == name {
&& s.id() match has_step_or_can_build(
.product_kinds &ReportingProductId {
.contains(&ReportingProductKind::Transactions) name: dependencies_for_step[0].product.name.clone(),
}) { kind: ReportingProductKind::BalancesAt,
// Check for BalancesAt -> Transactions args: ReportingStepArgs::DateArgs(DateArgs { date: args.date }),
let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); },
if dependencies_for_step.len() == 1 steps,
&& dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt dependencies,
{ context,
return true; ) {
} HasStepOrCanBuild::HasStep(_)
| HasStepOrCanBuild::CanLookup(_)
// Check if BalancesBetween -> Transactions and BalancesAt is available | HasStepOrCanBuild::CanBuild(_) => {
if dependencies_for_step.len() == 1 return true;
&& dependencies_for_step[0].product.kind }
== ReportingProductKind::BalancesBetween HasStepOrCanBuild::None => {}
{
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;
} }
HasStepOrCanBuild::None => {}
} }
} }
} }
return false;
} else {
return false;
} }
return false;
} }
fn build( fn build(
name: &'static str, name: String,
_kind: ReportingProductKind, _kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>, args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
_context: &ReportingContext, _context: &ReportingContext,
) -> Box<dyn ReportingStep> { ) -> Box<dyn ReportingStep> {
Box::new(UpdateBalancesAt { Box::new(UpdateBalancesAt {
step_name: name, step_name: name,
args: *args.downcast().unwrap(), args: args.into(),
}) })
} }
} }
@ -522,9 +519,9 @@ impl Display for UpdateBalancesAt {
impl ReportingStep for UpdateBalancesAt { impl ReportingStep for UpdateBalancesAt {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
name: self.step_name, name: self.step_name.clone(),
product_kinds: &[ReportingProductKind::BalancesAt], product_kinds: vec![ReportingProductKind::BalancesAt],
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
} }
} }
@ -549,7 +546,7 @@ impl ReportingStep for UpdateBalancesAt {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: parent_step.id().args.clone(), args: parent_step.id().args.clone(),
}, },
@ -567,9 +564,9 @@ impl ReportingStep for UpdateBalancesAt {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: dependency.name, name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date, date: self.args.date,
}), }),
}, },
@ -600,7 +597,7 @@ impl ReportingStep for UpdateBalancesAt {
// Get transactions // Get transactions
let transactions = &products let transactions = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: parent_step.id().args, 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 // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available
opening_balances_at = products opening_balances_at = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: dependency.name, name: dependency.name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { args: ReportingStepArgs::DateArgs(DateArgs {
date: self.args.date, date: self.args.date,
}), }),
})? })?
@ -649,9 +646,9 @@ impl ReportingStep for UpdateBalancesAt {
let mut result = ReportingProducts::new(); let mut result = ReportingProducts::new();
result.insert( result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateArgs(self.args.clone()),
}, },
Box::new(balances), 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] /// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween]
#[derive(Debug)] #[derive(Debug)]
pub struct UpdateBalancesBetween { pub struct UpdateBalancesBetween {
step_name: &'static str, step_name: String,
args: DateStartDateEndArgs, args: DateStartDateEndArgs,
} }
@ -676,9 +673,9 @@ impl UpdateBalancesBetween {
} }
fn can_build( fn can_build(
name: &'static str, name: &str,
kind: ReportingProductKind, kind: ReportingProductKind,
_args: &Box<dyn ReportingStepArgs>, _args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
_context: &ReportingContext, _context: &ReportingContext,
@ -706,16 +703,16 @@ impl UpdateBalancesBetween {
} }
fn build( fn build(
name: &'static str, name: String,
_kind: ReportingProductKind, _kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>, args: ReportingStepArgs,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
_context: &ReportingContext, _context: &ReportingContext,
) -> Box<dyn ReportingStep> { ) -> Box<dyn ReportingStep> {
Box::new(UpdateBalancesBetween { Box::new(UpdateBalancesBetween {
step_name: name, step_name: name,
args: *args.downcast().unwrap(), args: args.into(),
}) })
} }
} }
@ -730,9 +727,9 @@ impl Display for UpdateBalancesBetween {
impl ReportingStep for UpdateBalancesBetween { impl ReportingStep for UpdateBalancesBetween {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
name: self.step_name, name: self.step_name.clone(),
product_kinds: &[ReportingProductKind::BalancesBetween], product_kinds: vec![ReportingProductKind::BalancesBetween],
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
} }
} }
@ -757,7 +754,7 @@ impl ReportingStep for UpdateBalancesBetween {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: parent_step.id().args, 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 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 let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
if *balances_between_product if matches!(
.args balances_between_product.args,
.downcast_ref::<DateStartDateEndArgs>() ReportingStepArgs::DateStartDateEndArgs(_)
.unwrap() == self.args ) {
{
// Directly depends on BalanceBetween -> Transaction with appropriate date // Directly depends on BalanceBetween -> Transaction with appropriate date
// Do not need to add extra dependencies // Do not need to add extra dependencies
} else { } else {
@ -779,9 +775,9 @@ impl ReportingStep for UpdateBalancesBetween {
dependencies.add_dependency( dependencies.add_dependency(
self.id(), self.id(),
ReportingProductId { ReportingProductId {
name: balances_between_product.name, name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween, 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 // Get transactions
let transactions = &products let transactions = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: parent_step.id().args, args: parent_step.id().args,
})? })?
@ -825,9 +821,9 @@ impl ReportingStep for UpdateBalancesBetween {
// Get opening balances // Get opening balances
let opening_balances = &products let opening_balances = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: balances_between_product.name, name: balances_between_product.name.clone(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
})? })?
.downcast_ref::<BalancesBetween>() .downcast_ref::<BalancesBetween>()
.unwrap() .unwrap()
@ -849,9 +845,9 @@ impl ReportingStep for UpdateBalancesBetween {
let mut result = ReportingProducts::new(); let mut result = ReportingProducts::new();
result.insert( result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name.clone(),
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
args: Box::new(self.args.clone()), args: ReportingStepArgs::DateStartDateEndArgs(self.args.clone()),
}, },
Box::new(balances), 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)) .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(); 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); 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 // No explicit step for product - try builders
for builder in context.step_dynamic_builders.iter() { for builder in context.step_dynamic_builders.iter() {
if (builder.can_build)( if (builder.can_build)(
product.name, &product.name,
product.kind, product.kind,
&product.args, &product.args,
steps, steps,
@ -133,7 +133,7 @@ fn build_step_for_product(
panic!("Attempted to call build_step_for_product for already existing step") panic!("Attempted to call build_step_for_product for already existing step")
} }
HasStepOrCanBuild::CanLookup(from_args_fn) => { 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 // Check new step meets the dependency
if new_step.id().name != product.name { if new_step.id().name != product.name {
@ -162,7 +162,7 @@ fn build_step_for_product(
} }
HasStepOrCanBuild::CanBuild(builder) => { HasStepOrCanBuild::CanBuild(builder) => {
new_step = (builder.build)( new_step = (builder.build)(
product.name, product.name.clone(),
product.kind, product.kind,
product.args.clone(), product.args.clone(),
&steps, &steps,

View File

@ -16,9 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. 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 std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -27,161 +24,7 @@ use crate::QuantityInt;
use super::types::ReportingProduct; use super::types::ReportingProduct;
/// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] /// Represents a dynamically generated report composed of [DynamicReportEntry]
#[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
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DynamicReport { pub struct DynamicReport {
pub title: String, 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 /// Serialise the report (as JSON) using serde
pub fn to_json(&self) -> String { pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap() serde_json::to_string(self).unwrap()
} }
/// Look up [DynamicReportEntry] by id /// Look up [DynamicReportEntry] by id
/// pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> {
/// 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> {
// Manually iterate over self.entries rather than self.entries() // Manually iterate over self.entries rather than self.entries()
// To catch the situation where entry is already mutably borrowed // To catch the situation where entry is already mutably borrowed
for entry in self.entries.iter() { for entry in self.entries.iter() {
@ -236,25 +55,17 @@ impl DynamicReport {
DynamicReportEntry::Section(section) => { DynamicReportEntry::Section(section) => {
if let Some(i) = &section.id { if let Some(i) = &section.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry);
} }
} }
if let Some(e) = section.by_id(id) { if let Some(e) = section.by_id(id) {
return Some(match e { return Some(e);
DynamicReportEntry::Section(section) => {
DynamicReportEntry::Section(section.clone())
}
DynamicReportEntry::LiteralRow(row) => {
DynamicReportEntry::LiteralRow(row.clone())
}
DynamicReportEntry::Spacer => DynamicReportEntry::Spacer,
});
} }
} }
DynamicReportEntry::LiteralRow(row) => { DynamicReportEntry::Row(row) => {
if let Some(i) = &row.id { if let Some(i) = &row.id {
if i == 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 // 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 Some(entry) = self.by_id(id) {
if let DynamicReportEntry::LiteralRow(row) = entry { if let DynamicReportEntry::Row(row) = entry {
Some(row.quantity) Some(&row.quantity)
} else { } else {
panic!("Called quantity_for_id on non-LiteralRow"); panic!("Called quantity_for_id on non-LiteralRow");
} }
@ -281,207 +92,36 @@ impl DynamicReport {
impl ReportingProduct for 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)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub enum DynamicReportEntry { pub enum DynamicReportEntry {
Section(Section), Section(Section),
LiteralRow(LiteralRow), Row(Row),
Spacer, Spacer,
} }
#[derive(Clone, Debug)] impl From<Section> for DynamicReportEntry {
pub struct CalculatableSection { fn from(value: Section) -> Self {
pub text: String, DynamicReportEntry::Section(value)
pub id: Option<String>, }
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<RefCell<CalculatableDynamicReportEntry>>,
} }
impl CalculatableSection { impl From<Row> for DynamicReportEntry {
pub fn new( fn from(value: Row) -> Self {
text: String, DynamicReportEntry::Row(value)
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
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Section { pub struct Section {
pub text: String, pub text: Option<String>,
pub id: Option<String>, pub id: Option<String>,
pub visible: bool, pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<DynamicReportEntry>, pub entries: Vec<DynamicReportEntry>,
} }
impl Section { 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 /// Look up [DynamicReportEntry] by id
/// pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> {
/// Returns a cloned copy of the [DynamicReportEntry].
pub fn by_id(&self, id: &str) -> Option<DynamicReportEntry> {
// Manually iterate over self.entries rather than self.entries() // Manually iterate over self.entries rather than self.entries()
// To catch the situation where entry is already mutably borrowed // To catch the situation where entry is already mutably borrowed
for entry in self.entries.iter() { for entry in self.entries.iter() {
@ -489,17 +129,17 @@ impl Section {
DynamicReportEntry::Section(section) => { DynamicReportEntry::Section(section) => {
if let Some(i) = &section.id { if let Some(i) = &section.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry);
} }
} }
if let Some(e) = section.by_id(id) { if let Some(e) = section.by_id(id) {
return Some(e); return Some(e);
} }
} }
DynamicReportEntry::LiteralRow(row) => { DynamicReportEntry::Row(row) => {
if let Some(i) = &row.id { if let Some(i) = &row.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry);
} }
} }
} }
@ -511,7 +151,7 @@ impl Section {
} }
/// Calculate the subtotals for this [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()]; let mut subtotals = vec![0; report.columns.len()];
for entry in self.entries.iter() { for entry in self.entries.iter() {
match entry { match entry {
@ -520,7 +160,7 @@ impl Section {
subtotals[col_idx] += subtotal; subtotals[col_idx] += subtotal;
} }
} }
DynamicReportEntry::LiteralRow(row) => { DynamicReportEntry::Row(row) => {
for (col_idx, subtotal) in row.quantity.iter().enumerate() { for (col_idx, subtotal) in row.quantity.iter().enumerate() {
subtotals[col_idx] += subtotal; subtotals[col_idx] += subtotal;
} }
@ -533,48 +173,22 @@ impl Section {
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LiteralRow { pub struct Row {
pub text: String, pub text: String,
pub quantity: Vec<QuantityInt>, pub quantity: Vec<QuantityInt>,
pub id: Option<String>, pub id: Option<String>,
pub visible: bool, pub visible: bool,
pub auto_hide: bool,
pub link: Option<String>, pub link: Option<String>,
pub heading: bool, pub heading: bool,
pub bordered: 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( pub fn entries_for_kind(
kind: &str, kind: &str,
invert: bool, invert: bool,
balances: &Vec<&HashMap<String, QuantityInt>>, balances: &Vec<&HashMap<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>, kinds_for_account: &HashMap<String, Vec<String>>,
) -> Vec<CalculatableDynamicReportEntry> { ) -> Vec<DynamicReportEntry> {
// Get accounts of specified kind // Get accounts of specified kind
let mut accounts = kinds_for_account let mut accounts = kinds_for_account
.iter() .iter()
@ -596,6 +210,11 @@ pub fn entries_for_kind(
.map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 }) .map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Do not show if all quantities are zero
if quantities.iter().all(|q| *q == 0) {
continue;
}
// Some exceptions for the link // Some exceptions for the link
let link; let link;
if account == crate::CURRENT_YEAR_EARNINGS { if account == crate::CURRENT_YEAR_EARNINGS {
@ -606,17 +225,16 @@ pub fn entries_for_kind(
link = Some(format!("/transactions/{}", account)); link = Some(format!("/transactions/{}", account));
} }
let entry = LiteralRow { let entry = Row {
text: account.to_string(), text: account.to_string(),
quantity: quantities, quantity: quantities,
id: None, id: None,
visible: true, visible: true,
auto_hide: true,
link, link,
heading: false, heading: false,
bordered: false, bordered: false,
}; };
entries.push(CalculatableDynamicReportEntry::LiteralRow(entry)); entries.push(entry.into());
} }
entries 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 chrono::NaiveDate;
use downcast_rs::Downcast; use downcast_rs::Downcast;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use dyn_eq::DynEq;
use dyn_hash::DynHash;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::db::DbConnection; use crate::db::DbConnection;
use crate::model::transaction::TransactionWithPostings; use crate::model::transaction::TransactionWithPostings;
use crate::plugin::PluginSpec;
use crate::QuantityInt; use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies; use super::calculator::ReportingGraphDependencies;
@ -44,30 +43,38 @@ use super::executor::ReportingExecutionError;
pub struct ReportingContext { pub struct ReportingContext {
// Configuration // Configuration
pub db_connection: DbConnection, pub db_connection: DbConnection,
pub plugin_dir: String,
pub plugin_names: Vec<String>,
pub eofy_date: NaiveDate, pub eofy_date: NaiveDate,
pub reporting_commodity: String, pub reporting_commodity: String,
// State // State
pub(crate) step_lookup_fn: HashMap< pub(crate) step_lookup_fn: HashMap<
(&'static str, &'static [ReportingProductKind]), (String, Vec<ReportingProductKind>),
(ReportingStepTakesArgsFn, ReportingStepFromArgsFn), (ReportingStepTakesArgsFn, ReportingStepFromArgsFn),
>, >,
pub(crate) step_dynamic_builders: Vec<ReportingStepDynamicBuilder>, pub(crate) step_dynamic_builders: Vec<ReportingStepDynamicBuilder>,
pub(crate) plugin_specs: HashMap<String, PluginSpec>,
} }
impl ReportingContext { impl ReportingContext {
/// Initialise a new [ReportingContext] /// Initialise a new [ReportingContext]
pub fn new( pub fn new(
db_connection: DbConnection, db_connection: DbConnection,
plugin_dir: String,
plugin_names: Vec<String>,
eofy_date: NaiveDate, eofy_date: NaiveDate,
reporting_commodity: String, reporting_commodity: String,
) -> Self { ) -> Self {
Self { Self {
db_connection, db_connection,
plugin_dir,
plugin_names,
eofy_date, eofy_date,
reporting_commodity, reporting_commodity,
step_lookup_fn: HashMap::new(), step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::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]. /// A lookup function generates concrete [ReportingStep]s from a [ReportingStepId].
pub fn register_lookup_fn( pub fn register_lookup_fn(
&mut self, &mut self,
name: &'static str, name: String,
product_kinds: &'static [ReportingProductKind], product_kinds: Vec<ReportingProductKind>,
takes_args_fn: ReportingStepTakesArgsFn, takes_args_fn: ReportingStepTakesArgsFn,
from_args_fn: ReportingStepFromArgsFn, from_args_fn: ReportingStepFromArgsFn,
) { ) {
@ -102,12 +109,14 @@ impl ReportingContext {
/// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep] /// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep]
/// ///
/// See [ReportingContext::register_lookup_fn]. /// 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] /// Function which builds a concrete [ReportingStep] from the given [ReportingStepArgs]
/// ///
/// See [ReportingContext::register_lookup_fn]. /// 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 // REPORTING STEP DYNAMIC BUILDERS
@ -118,17 +127,17 @@ pub type ReportingStepFromArgsFn = fn(args: Box<dyn ReportingStepArgs>) -> Box<d
pub struct ReportingStepDynamicBuilder { pub struct ReportingStepDynamicBuilder {
pub name: &'static str, pub name: &'static str,
pub can_build: fn( pub can_build: fn(
name: &'static str, name: &str,
kind: ReportingProductKind, kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>, args: &ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
) -> bool, ) -> bool,
pub build: fn( pub build: fn(
name: &'static str, name: String,
kind: ReportingProductKind, kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>, args: ReportingStepArgs,
steps: &Vec<Box<dyn ReportingStep>>, steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
@ -139,11 +148,11 @@ pub struct ReportingStepDynamicBuilder {
// REPORTING PRODUCTS // REPORTING PRODUCTS
/// Identifies a [ReportingProduct] /// Identifies a [ReportingProduct]
#[derive(Clone, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ReportingProductId { pub struct ReportingProductId {
pub name: &'static str, pub name: String,
pub kind: ReportingProductKind, pub kind: ReportingProductKind,
pub args: Box<dyn ReportingStepArgs>, pub args: ReportingStepArgs,
} }
impl Display for ReportingProductId { impl Display for ReportingProductId {
@ -155,7 +164,7 @@ impl Display for ReportingProductId {
/// Identifies a type of [Box]ed [ReportingProduct] /// Identifies a type of [Box]ed [ReportingProduct]
/// ///
/// See [Box::downcast]. /// See [Box::downcast].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum ReportingProductKind { pub enum ReportingProductKind {
/// The [Box]ed [ReportingProduct] is a [Transactions] /// The [Box]ed [ReportingProduct] is a [Transactions]
Transactions, Transactions,
@ -276,9 +285,9 @@ impl Display for ReportingProducts {
/// Identifies a [ReportingStep] /// Identifies a [ReportingStep]
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReportingStepId { pub struct ReportingStepId {
pub name: &'static str, pub name: String,
pub product_kinds: &'static [ReportingProductKind], pub product_kinds: Vec<ReportingProductKind>,
pub args: Box<dyn ReportingStepArgs>, pub args: ReportingStepArgs,
} }
impl Display for ReportingStepId { impl Display for ReportingStepId {
@ -345,50 +354,68 @@ downcast_rs::impl_downcast!(ReportingStep);
// REPORTING STEP ARGUMENTS // REPORTING STEP ARGUMENTS
/// Represents arguments to a [ReportingStep] /// Represents arguments to a [ReportingStep]
pub trait ReportingStepArgs: #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
Debug + Display + Downcast + DynClone + DynEq + DynHash + Send + Sync 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); impl Display for 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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DateArgs { pub struct DateArgs {
#[serde(with = "crate::serde::naivedate_to_js")]
pub date: NaiveDate, pub date: NaiveDate,
} }
impl ReportingStepArgs for DateArgs {}
impl Display for DateArgs { impl Display for DateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.date)) f.write_fmt(format_args!("{}", self.date))
} }
} }
/// [ReportingStepArgs] implementation which takes a date range impl Into<DateArgs> for ReportingStepArgs {
#[derive(Clone, Debug, Eq, Hash, PartialEq)] fn into(self) -> DateArgs {
pub struct DateStartDateEndArgs { if let ReportingStepArgs::DateArgs(args) = self {
pub date_start: NaiveDate, args
pub date_end: NaiveDate, } 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 { impl Display for DateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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] impl Into<DateStartDateEndArgs> for ReportingStepArgs {
#[derive(Clone, Debug, Eq, Hash, PartialEq)] 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 struct MultipleDateArgs {
pub dates: Vec<DateArgs>, pub dates: Vec<DateArgs>,
} }
impl ReportingStepArgs for MultipleDateArgs {}
impl Display for MultipleDateArgs { impl Display for MultipleDateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!( f.write_fmt(format_args!(
@ -417,14 +451,21 @@ impl Display for MultipleDateArgs {
} }
} }
/// [ReportingStepArgs] implementation which takes multiple [DateStartDateEndArgs] impl Into<MultipleDateArgs> for ReportingStepArgs {
#[derive(Clone, Debug, Eq, Hash, PartialEq)] 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 struct MultipleDateStartDateEndArgs {
pub dates: Vec<DateStartDateEndArgs>, pub dates: Vec<DateStartDateEndArgs>,
} }
impl ReportingStepArgs for MultipleDateStartDateEndArgs {}
impl Display for MultipleDateStartDateEndArgs { impl Display for MultipleDateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!( 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/>. 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 /// Serialises [chrono::NaiveDateTime] in database format
/// ///
/// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc. /// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc.
@ -40,7 +85,7 @@ pub mod naivedatetime_to_js {
type Value = NaiveDateTime; type Value = NaiveDateTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 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> 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", "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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -878,7 +888,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading", "libloading 0.8.8",
] ]
[[package]] [[package]]
@ -977,18 +987,6 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" 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]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -2165,7 +2163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
dependencies = [ dependencies = [
"gtk-sys", "gtk-sys",
"libloading", "libloading 0.7.4",
"once_cell", "once_cell",
] ]
@ -2183,9 +2181,8 @@ dependencies = [
"chrono", "chrono",
"downcast-rs 2.0.1", "downcast-rs 2.0.1",
"dyn-clone", "dyn-clone",
"dyn-eq",
"dyn-hash",
"indexmap 2.9.0", "indexmap 2.9.0",
"mlua",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
@ -2202,6 +2199,16 @@ dependencies = [
"winapi", "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]] [[package]]
name = "libm" name = "libm"
version = "0.2.11" version = "0.2.11"
@ -2257,6 +2264,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 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]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@ -2351,6 +2367,37 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "muda" name = "muda"
version = "0.15.3" version = "0.15.3"
@ -2769,6 +2816,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 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]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@ -3424,6 +3480,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -3446,6 +3508,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -3549,6 +3617,16 @@ dependencies = [
"typeid", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.219"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/* /*
DrCr: Web-based double-entry bookkeeping framework 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 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 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[] { export function joinedToTransactions(joinedTransactionPostings: JoinedTransactionPosting[]): Transaction[] {
// Group postings into transactions // Group postings into transactions
const transactions: Transaction[] = []; const transactions: Transaction[] = [];
@ -143,18 +95,6 @@ export function joinedToTransactions(joinedTransactionPostings: JoinedTransactio
return transactions; 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 { export function serialiseAmount(quantity: number, commodity: string): string {
// Pretty print the amount for an editable input // Pretty print the amount for an editable input
if (quantity < 0) { if (quantity < 0) {

View File

@ -54,7 +54,7 @@
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid'; 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 { db } from '../db.ts';
import DynamicReportComponent from '../components/DynamicReportComponent.vue'; import DynamicReportComponent from '../components/DynamicReportComponent.vue';
@ -118,9 +118,9 @@
return true; return true;
} }
const totalAssets = (reportEntryById(report.value, 'total_assets') 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 { LiteralRow: LiteralRow }).LiteralRow.quantity; const totalLiabilities = (reportEntryById(report.value, 'total_liabilities') as { Row: Row }).Row.quantity;
const totalEquity = (reportEntryById(report.value, 'total_equity') as { LiteralRow: LiteralRow }).LiteralRow.quantity; const totalEquity = (reportEntryById(report.value, 'total_equity') as { Row: Row }).Row.quantity;
let doesBalance = true; let doesBalance = true;
for (let column = 0; column < report.value.columns.length; column++) { 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 // 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 { export interface Section {
text: string; text: string;
@ -34,7 +34,7 @@ export interface Section {
entries: DynamicReportEntry[]; entries: DynamicReportEntry[];
} }
export interface LiteralRow { export interface Row {
text: string; text: string;
quantity: number[]; quantity: number[];
id: string; id: string;
@ -55,8 +55,8 @@ export function reportEntryById(report: DynamicReport | Section, id: string): Dy
if (result !== null) { if (result !== null) {
return result; return result;
} }
} else if ((entry as { LiteralRow: LiteralRow }).LiteralRow) { } else if ((entry as { Row: Row }).Row) {
if ((entry as { LiteralRow: LiteralRow }).LiteralRow.id === id) { if ((entry as { Row: Row }).Row.id === id) {
return entry; return entry;
} }
} }