Implement formal BalanceSheet report
This commit is contained in:
parent
eb13bd5a87
commit
737ed5bfb2
16
src/db.rs
16
src/db.rs
@ -93,7 +93,7 @@ impl DbConnection {
|
|||||||
async fn get_account_configurations_async(&self) -> Vec<AccountConfiguration> {
|
async fn get_account_configurations_async(&self) -> Vec<AccountConfiguration> {
|
||||||
let mut connection = self.sqlx_connection.borrow_mut();
|
let mut connection = self.sqlx_connection.borrow_mut();
|
||||||
|
|
||||||
let account_configurations =
|
let mut account_configurations =
|
||||||
sqlx::query("SELECT id, account, kind, data FROM account_configurations")
|
sqlx::query("SELECT id, account, kind, data FROM account_configurations")
|
||||||
.map(|r: SqliteRow| AccountConfiguration {
|
.map(|r: SqliteRow| AccountConfiguration {
|
||||||
id: r.get("id"),
|
id: r.get("id"),
|
||||||
@ -105,6 +105,20 @@ impl DbConnection {
|
|||||||
.await
|
.await
|
||||||
.expect("SQL error");
|
.expect("SQL error");
|
||||||
|
|
||||||
|
// System accounts
|
||||||
|
account_configurations.push(AccountConfiguration {
|
||||||
|
id: None,
|
||||||
|
account: "Current Year Earnings".to_string(),
|
||||||
|
kind: "drcr.equity".to_string(),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
account_configurations.push(AccountConfiguration {
|
||||||
|
id: None,
|
||||||
|
account: "Retained Earnings".to_string(),
|
||||||
|
kind: "drcr.equity".to_string(),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
|
||||||
account_configurations
|
account_configurations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
src/main.rs
34
src/main.rs
@ -23,8 +23,8 @@ use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets};
|
|||||||
use libdrcr::reporting::generate_report;
|
use libdrcr::reporting::generate_report;
|
||||||
use libdrcr::reporting::steps::register_lookup_fns;
|
use libdrcr::reporting::steps::register_lookup_fns;
|
||||||
use libdrcr::reporting::types::{
|
use libdrcr::reporting::types::{
|
||||||
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind,
|
DateArgs, DateStartDateEndArgs, MultipleDateArgs, ReportingContext, ReportingProductId,
|
||||||
VoidArgs,
|
ReportingProductKind, VoidArgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -50,10 +50,12 @@ fn main() {
|
|||||||
args: Box::new(VoidArgs {}),
|
args: Box::new(VoidArgs {}),
|
||||||
},
|
},
|
||||||
ReportingProductId {
|
ReportingProductId {
|
||||||
name: "AllTransactionsIncludingEarningsToEquity",
|
name: "BalanceSheet",
|
||||||
kind: ReportingProductKind::BalancesAt,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(DateArgs {
|
args: Box::new(MultipleDateArgs {
|
||||||
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
dates: vec![DateArgs {
|
||||||
|
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
||||||
|
}],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -104,10 +106,12 @@ fn main() {
|
|||||||
args: Box::new(VoidArgs {}),
|
args: Box::new(VoidArgs {}),
|
||||||
},
|
},
|
||||||
ReportingProductId {
|
ReportingProductId {
|
||||||
name: "AllTransactionsIncludingEarningsToEquity",
|
name: "BalanceSheet",
|
||||||
kind: ReportingProductKind::BalancesAt,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(DateArgs {
|
args: Box::new(MultipleDateArgs {
|
||||||
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
dates: vec![DateArgs {
|
||||||
|
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
||||||
|
}],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -115,10 +119,12 @@ fn main() {
|
|||||||
let products = generate_report(targets, &context).unwrap();
|
let products = generate_report(targets, &context).unwrap();
|
||||||
let result = products
|
let result = products
|
||||||
.get_or_err(&ReportingProductId {
|
.get_or_err(&ReportingProductId {
|
||||||
name: "AllTransactionsIncludingEarningsToEquity",
|
name: "BalanceSheet",
|
||||||
kind: ReportingProductKind::BalancesAt,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(DateArgs {
|
args: Box::new(MultipleDateArgs {
|
||||||
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
dates: vec![DateArgs {
|
||||||
|
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
||||||
|
}],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
311
src/reporting/dynamic_report.rs
Normal file
311
src/reporting/dynamic_report.rs
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
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::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::QuantityInt;
|
||||||
|
|
||||||
|
use super::types::{GenericReportingProduct, ReportingProduct};
|
||||||
|
|
||||||
|
/// Represents a dynamically generated report composed of [DynamicReportEntry]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DynamicReport {
|
||||||
|
pub title: String,
|
||||||
|
pub columns: Vec<String>,
|
||||||
|
pub entries: Vec<DynamicReportEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicReport {
|
||||||
|
/// Remove all entries from the report where auto_hide is enabled and quantity is zero
|
||||||
|
pub fn auto_hide(&mut self) {
|
||||||
|
self.entries.retain_mut(|e| match e {
|
||||||
|
DynamicReportEntry::Section(section) => {
|
||||||
|
section.auto_hide_children();
|
||||||
|
if section.can_auto_hide_self() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::LiteralRow(row) => {
|
||||||
|
if row.can_auto_hide() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => true,
|
||||||
|
DynamicReportEntry::Spacer => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively calculate all [CalculatedRow] entries
|
||||||
|
pub fn calculate(&mut self) {
|
||||||
|
// FIXME: This is for the borrow checker - can it be avoided?
|
||||||
|
let report_cloned = self.clone();
|
||||||
|
|
||||||
|
for entry in self.entries.iter_mut() {
|
||||||
|
match entry {
|
||||||
|
DynamicReportEntry::Section(section) => section.calculate(&report_cloned),
|
||||||
|
DynamicReportEntry::LiteralRow(_) => (),
|
||||||
|
DynamicReportEntry::CalculatedRow(row) => {
|
||||||
|
*entry = DynamicReportEntry::LiteralRow(row.calculate(&report_cloned));
|
||||||
|
}
|
||||||
|
DynamicReportEntry::Spacer => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up [DynamicReportEntry] by id
|
||||||
|
pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> {
|
||||||
|
for entry in self.entries.iter() {
|
||||||
|
match entry {
|
||||||
|
DynamicReportEntry::Section(section) => {
|
||||||
|
if let Some(i) = §ion.id {
|
||||||
|
if i == id {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(e) = section.by_id(id) {
|
||||||
|
return Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::LiteralRow(row) => {
|
||||||
|
if let Some(i) = &row.id {
|
||||||
|
if i == id {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => (),
|
||||||
|
DynamicReportEntry::Spacer => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the subtotals for the [Section] with the given id
|
||||||
|
pub fn subtotal_for_id(&self, id: &str) -> Vec<QuantityInt> {
|
||||||
|
let entry = self.by_id(id).expect("Invalid id");
|
||||||
|
if let DynamicReportEntry::Section(section) = entry {
|
||||||
|
section.subtotal(&self)
|
||||||
|
} else {
|
||||||
|
panic!("Called subtotal_for_id on non-Section");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericReportingProduct for DynamicReport {}
|
||||||
|
impl ReportingProduct for DynamicReport {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum DynamicReportEntry {
|
||||||
|
Section(Section),
|
||||||
|
LiteralRow(LiteralRow),
|
||||||
|
CalculatedRow(CalculatedRow),
|
||||||
|
Spacer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Section {
|
||||||
|
pub text: String,
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub auto_hide: bool,
|
||||||
|
pub entries: Vec<DynamicReportEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Section {
|
||||||
|
fn auto_hide_children(&mut self) {
|
||||||
|
self.entries.retain_mut(|e| match e {
|
||||||
|
DynamicReportEntry::Section(section) => {
|
||||||
|
section.auto_hide_children();
|
||||||
|
if section.can_auto_hide_self() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::LiteralRow(row) => {
|
||||||
|
if row.can_auto_hide() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => true,
|
||||||
|
DynamicReportEntry::Spacer => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_auto_hide_self(&self) -> bool {
|
||||||
|
self.auto_hide
|
||||||
|
&& self.entries.iter().all(|e| match e {
|
||||||
|
DynamicReportEntry::Section(section) => section.can_auto_hide_self(),
|
||||||
|
DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(),
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => false,
|
||||||
|
DynamicReportEntry::Spacer => true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively calculate all [CalculatedRow] entries
|
||||||
|
pub fn calculate(&mut self, report: &DynamicReport) {
|
||||||
|
for entry in self.entries.iter_mut() {
|
||||||
|
match entry {
|
||||||
|
DynamicReportEntry::Section(section) => section.calculate(report),
|
||||||
|
DynamicReportEntry::LiteralRow(_) => (),
|
||||||
|
DynamicReportEntry::CalculatedRow(row) => {
|
||||||
|
*entry = DynamicReportEntry::LiteralRow(row.calculate(report))
|
||||||
|
}
|
||||||
|
DynamicReportEntry::Spacer => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up [DynamicReportEntry] by id
|
||||||
|
pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> {
|
||||||
|
for entry in self.entries.iter() {
|
||||||
|
match entry {
|
||||||
|
DynamicReportEntry::Section(section) => {
|
||||||
|
if let Some(i) = §ion.id {
|
||||||
|
if i == id {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(e) = section.by_id(id) {
|
||||||
|
return Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::LiteralRow(row) => {
|
||||||
|
if let Some(i) = &row.id {
|
||||||
|
if i == id {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => (),
|
||||||
|
DynamicReportEntry::Spacer => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the subtotals for this [Section]
|
||||||
|
pub fn subtotal(&self, report: &DynamicReport) -> Vec<QuantityInt> {
|
||||||
|
let mut subtotals = vec![0; report.columns.len()];
|
||||||
|
for entry in self.entries.iter() {
|
||||||
|
match entry {
|
||||||
|
DynamicReportEntry::Section(section) => {
|
||||||
|
for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() {
|
||||||
|
subtotals[col_idx] += subtotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::LiteralRow(row) => {
|
||||||
|
for (col_idx, subtotal) in row.quantity.iter().enumerate() {
|
||||||
|
subtotals[col_idx] += subtotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicReportEntry::CalculatedRow(_) => (),
|
||||||
|
DynamicReportEntry::Spacer => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subtotals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LiteralRow {
|
||||||
|
pub text: String,
|
||||||
|
pub quantity: Vec<QuantityInt>,
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub auto_hide: bool,
|
||||||
|
pub link: Option<String>,
|
||||||
|
pub heading: bool,
|
||||||
|
pub bordered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LiteralRow {
|
||||||
|
/// Returns whether the row has auto_hide enabled and all quantities are zero
|
||||||
|
fn can_auto_hide(&self) -> bool {
|
||||||
|
self.auto_hide && self.quantity.iter().all(|q| *q == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CalculatedRow {
|
||||||
|
//pub text: String,
|
||||||
|
pub calculate_fn: fn(report: &DynamicReport) -> 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: &DynamicReport) -> LiteralRow {
|
||||||
|
(self.calculate_fn)(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries_for_kind(
|
||||||
|
kind: &str,
|
||||||
|
invert: bool,
|
||||||
|
balances: &Vec<&HashMap<String, QuantityInt>>,
|
||||||
|
kinds_for_account: &HashMap<String, Vec<String>>,
|
||||||
|
) -> Vec<DynamicReportEntry> {
|
||||||
|
// Get accounts of specified kind
|
||||||
|
let mut accounts = kinds_for_account
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(a, k)| {
|
||||||
|
if k.iter().any(|k| k == kind) {
|
||||||
|
Some(a)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
accounts.sort();
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for account in accounts {
|
||||||
|
let quantities = balances
|
||||||
|
.iter()
|
||||||
|
.map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let entry = LiteralRow {
|
||||||
|
text: account.to_string(),
|
||||||
|
quantity: quantities,
|
||||||
|
id: None,
|
||||||
|
visible: true,
|
||||||
|
auto_hide: true,
|
||||||
|
link: None,
|
||||||
|
heading: false,
|
||||||
|
bordered: false,
|
||||||
|
};
|
||||||
|
entries.push(DynamicReportEntry::LiteralRow(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
@ -22,6 +22,7 @@ use types::{ReportingContext, ReportingProductId, ReportingProducts};
|
|||||||
|
|
||||||
pub mod builders;
|
pub mod builders;
|
||||||
pub mod calculator;
|
pub mod calculator;
|
||||||
|
pub mod dynamic_report;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod steps;
|
pub mod steps;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
@ -29,18 +29,24 @@ use crate::transaction::{
|
|||||||
update_balances_from_transactions, Posting, Transaction, TransactionWithPostings,
|
update_balances_from_transactions, Posting, Transaction, TransactionWithPostings,
|
||||||
};
|
};
|
||||||
use crate::util::sofy_from_eofy;
|
use crate::util::sofy_from_eofy;
|
||||||
|
use crate::QuantityInt;
|
||||||
|
|
||||||
use super::calculator::ReportingGraphDependencies;
|
use super::calculator::ReportingGraphDependencies;
|
||||||
|
use super::dynamic_report::{
|
||||||
|
entries_for_kind, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, Section,
|
||||||
|
};
|
||||||
use super::executor::ReportingExecutionError;
|
use super::executor::ReportingExecutionError;
|
||||||
use super::types::{
|
use super::types::{
|
||||||
BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind,
|
BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProduct,
|
||||||
ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs,
|
ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId,
|
||||||
|
VoidArgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
|
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
|
||||||
pub fn register_lookup_fns(context: &mut ReportingContext) {
|
pub fn register_lookup_fns(context: &mut ReportingContext) {
|
||||||
AllTransactionsExceptEarningsToEquity::register_lookup_fn(context);
|
AllTransactionsExceptEarningsToEquity::register_lookup_fn(context);
|
||||||
AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context);
|
AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context);
|
||||||
|
BalanceSheet::register_lookup_fn(context);
|
||||||
CalculateIncomeTax::register_lookup_fn(context);
|
CalculateIncomeTax::register_lookup_fn(context);
|
||||||
CombineOrdinaryTransactions::register_lookup_fn(context);
|
CombineOrdinaryTransactions::register_lookup_fn(context);
|
||||||
CurrentYearEarningsToEquity::register_lookup_fn(context);
|
CurrentYearEarningsToEquity::register_lookup_fn(context);
|
||||||
@ -296,6 +302,182 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a balance sheet [DynamicReport]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BalanceSheet {
|
||||||
|
pub args: MultipleDateArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BalanceSheet {
|
||||||
|
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||||
|
context.register_lookup_fn(
|
||||||
|
"BalanceSheet",
|
||||||
|
&[ReportingProductKind::Generic],
|
||||||
|
Self::takes_args,
|
||||||
|
Self::from_args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||||
|
args.is::<MultipleDateArgs>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
||||||
|
Box::new(BalanceSheet {
|
||||||
|
args: *args.downcast().unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BalanceSheet {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!("{}", self.id()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportingStep for BalanceSheet {
|
||||||
|
fn id(&self) -> ReportingStepId {
|
||||||
|
ReportingStepId {
|
||||||
|
name: "BalanceSheet",
|
||||||
|
product_kinds: &[ReportingProductKind::Generic],
|
||||||
|
args: Box::new(self.args.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
// BalanceSheet depends on AllTransactionsIncludingEarningsToEquity in each requested period
|
||||||
|
for date_args in self.args.dates.iter() {
|
||||||
|
result.push(ReportingProductId {
|
||||||
|
name: "AllTransactionsIncludingEarningsToEquity",
|
||||||
|
kind: ReportingProductKind::BalancesAt,
|
||||||
|
args: Box::new(date_args.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&self,
|
||||||
|
context: &ReportingContext,
|
||||||
|
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
_dependencies: &ReportingGraphDependencies,
|
||||||
|
products: &mut ReportingProducts,
|
||||||
|
) -> Result<(), ReportingExecutionError> {
|
||||||
|
// Get balances for each period
|
||||||
|
let mut balances: Vec<&HashMap<String, QuantityInt>> = Vec::new();
|
||||||
|
for date_args in self.args.dates.iter() {
|
||||||
|
let product = products.get_or_err(&ReportingProductId {
|
||||||
|
name: "AllTransactionsIncludingEarningsToEquity",
|
||||||
|
kind: ReportingProductKind::BalancesAt,
|
||||||
|
args: Box::new(date_args.clone()),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
balances.push(&product.downcast_ref::<BalancesAt>().unwrap().balances);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get names of all balance sheet accounts
|
||||||
|
let kinds_for_account =
|
||||||
|
kinds_for_account(context.db_connection.get_account_configurations());
|
||||||
|
|
||||||
|
// Init report
|
||||||
|
let mut report = DynamicReport {
|
||||||
|
title: "Balance sheet".to_string(),
|
||||||
|
columns: self.args.dates.iter().map(|d| d.date.to_string()).collect(),
|
||||||
|
entries: vec![
|
||||||
|
DynamicReportEntry::Section(Section {
|
||||||
|
text: "Assets".to_string(),
|
||||||
|
id: Some("assets".to_string()),
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
entries: {
|
||||||
|
let mut entries =
|
||||||
|
entries_for_kind("drcr.asset", false, &balances, &kinds_for_account);
|
||||||
|
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||||
|
calculate_fn: |report| LiteralRow {
|
||||||
|
text: "Total assets".to_string(),
|
||||||
|
quantity: report.subtotal_for_id("assets"),
|
||||||
|
id: None,
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
link: None,
|
||||||
|
heading: true,
|
||||||
|
bordered: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
entries
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DynamicReportEntry::Spacer,
|
||||||
|
DynamicReportEntry::Section(Section {
|
||||||
|
text: "Liabilities".to_string(),
|
||||||
|
id: Some("liabilities".to_string()),
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
entries: {
|
||||||
|
let mut entries =
|
||||||
|
entries_for_kind("drcr.liability", true, &balances, &kinds_for_account);
|
||||||
|
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||||
|
calculate_fn: |report| LiteralRow {
|
||||||
|
text: "Total liabilities".to_string(),
|
||||||
|
quantity: report.subtotal_for_id("liabilities"),
|
||||||
|
id: None,
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
link: None,
|
||||||
|
heading: true,
|
||||||
|
bordered: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
entries
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DynamicReportEntry::Spacer,
|
||||||
|
DynamicReportEntry::Section(Section {
|
||||||
|
text: "Equity".to_string(),
|
||||||
|
id: Some("equity".to_string()),
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
entries: {
|
||||||
|
let mut entries =
|
||||||
|
entries_for_kind("drcr.equity", true, &balances, &kinds_for_account);
|
||||||
|
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||||
|
calculate_fn: |report| LiteralRow {
|
||||||
|
text: "Total equity".to_string(),
|
||||||
|
quantity: report.subtotal_for_id("equity"),
|
||||||
|
id: None,
|
||||||
|
visible: true,
|
||||||
|
auto_hide: false,
|
||||||
|
link: None,
|
||||||
|
heading: true,
|
||||||
|
bordered: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
entries
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
report.calculate();
|
||||||
|
report.auto_hide();
|
||||||
|
|
||||||
|
// Store the result
|
||||||
|
products.insert(
|
||||||
|
ReportingProductId {
|
||||||
|
name: "BalanceSheet",
|
||||||
|
kind: ReportingProductKind::Generic,
|
||||||
|
args: Box::new(self.args.clone()),
|
||||||
|
},
|
||||||
|
Box::new(report),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculates income tax
|
/// Calculates income tax
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CalculateIncomeTax {}
|
pub struct CalculateIncomeTax {}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use downcast_rs::Downcast;
|
use downcast_rs::Downcast;
|
||||||
@ -356,3 +357,45 @@ impl Display for DateStartDateEndArgs {
|
|||||||
f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end))
|
f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [ReportingStepArgs] implementation which takes multiple [DateArgs]
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct MultipleDateArgs {
|
||||||
|
pub dates: Vec<DateArgs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportingStepArgs for MultipleDateArgs {}
|
||||||
|
|
||||||
|
impl Display for MultipleDateArgs {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{}",
|
||||||
|
self.dates
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [ReportingStepArgs] implementation which takes multiple [DateStartDateEndArgs]
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct MultipleDateStartDateEndArgs {
|
||||||
|
pub dates: Vec<DateStartDateEndArgs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportingStepArgs for MultipleDateStartDateEndArgs {}
|
||||||
|
|
||||||
|
impl Display for MultipleDateStartDateEndArgs {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{}",
|
||||||
|
self.dates
|
||||||
|
.iter()
|
||||||
|
.map(|a| format!("({})", a))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user