Implement IncomeStatement step

This commit is contained in:
RunasSudo 2025-05-27 00:21:30 +10:00
parent b8b2547aab
commit 541467a2da
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
2 changed files with 214 additions and 28 deletions

View File

@ -103,25 +103,25 @@ impl DynamicReport {
for entry in self.entries.iter() {
match entry.try_borrow() {
Ok(entry) => match &*entry {
DynamicReportEntry::Section(section) => {
if let Some(i) = &section.id {
if i == id {
DynamicReportEntry::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(e);
}
}
if let Some(e) = section.by_id(id) {
return Some(e);
}
}
DynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id {
if i == id {
DynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id {
if i == id {
return Some(entry.clone());
}
}
}
}
DynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (),
DynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (),
},
Err(err) => panic!(
"Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}",
@ -143,6 +143,16 @@ impl DynamicReport {
}
}
// Return the quantities for the [LiteralRow] with the given id
pub fn quantity_for_id(&self, id: &str) -> Vec<QuantityInt> {
let entry = self.by_id(id).expect("Invalid id");
if let DynamicReportEntry::LiteralRow(row) = entry {
row.quantity
} else {
panic!("Called quantity_for_id on non-LiteralRow");
}
}
/// Serialise the report (as JSON) using serde
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap()
@ -256,25 +266,25 @@ impl Section {
for entry in self.entries.iter() {
match entry.try_borrow() {
Ok(entry) => match &*entry {
DynamicReportEntry::Section(section) => {
if let Some(i) = &section.id {
if i == id {
DynamicReportEntry::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(e);
}
}
if let Some(e) = section.by_id(id) {
return Some(e);
}
}
DynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id {
if i == id {
DynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id {
if i == id {
return Some(entry.clone());
}
}
}
}
DynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (),
DynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (),
},
Err(err) => panic!(
"Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}",

View File

@ -37,8 +37,9 @@ use super::dynamic_report::{
};
use super::executor::ReportingExecutionError;
use super::types::{
BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProductKind,
ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs,
BalancesBetween, DateArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, ReportingContext,
ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId,
VoidArgs,
};
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
@ -50,6 +51,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) {
CombineOrdinaryTransactions::register_lookup_fn(context);
CurrentYearEarningsToEquity::register_lookup_fn(context);
DBBalances::register_lookup_fn(context);
IncomeStatement::register_lookup_fn(context);
PostUnreconciledStatementLines::register_lookup_fn(context);
RetainedEarningsToEquity::register_lookup_fn(context);
}
@ -867,6 +869,180 @@ impl ReportingStep for DBBalances {
}
}
/// Generates an income statement [DynamicReport]
#[derive(Debug)]
pub struct IncomeStatement {
pub args: MultipleDateStartDateEndArgs,
}
impl IncomeStatement {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"IncomeStatement",
&[ReportingProductKind::Generic],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<MultipleDateStartDateEndArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(IncomeStatement {
args: *args.downcast().unwrap(),
})
}
}
impl Display for IncomeStatement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
impl ReportingStep for IncomeStatement {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "IncomeStatement",
product_kinds: &[ReportingProductKind::Generic],
args: Box::new(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> {
let mut result = Vec::new();
// IncomeStatement depends on AllTransactionsExceptEarningsToEquity in each requested period
for date_args in self.args.dates.iter() {
result.push(ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity",
kind: ReportingProductKind::BalancesBetween,
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: "AllTransactionsExceptEarningsToEquity",
kind: ReportingProductKind::BalancesBetween,
args: Box::new(date_args.clone()),
})?;
balances.push(&product.downcast_ref::<BalancesBetween>().unwrap().balances);
}
// Get names of all income statement accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations());
// Init report
let mut report = DynamicReport::new(
"Income statement".to_string(),
self.args
.dates
.iter()
.map(|d| d.date_end.to_string())
.collect(),
vec![
DynamicReportEntry::Section(Section::new(
"Income".to_string(),
Some("income".to_string()),
true,
false,
{
let mut entries =
entries_for_kind("drcr.income", true, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total income".to_string(),
quantity: report.subtotal_for_id("income"),
id: Some("total_income".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}));
entries
},
)),
DynamicReportEntry::Spacer,
DynamicReportEntry::Section(Section::new(
"Expenses".to_string(),
Some("expenses".to_string()),
true,
false,
{
let mut entries =
entries_for_kind("drcr.expense", false, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Total expenses".to_string(),
quantity: report.subtotal_for_id("expenses"),
id: Some("total_expenses".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}));
entries
},
)),
DynamicReportEntry::Spacer,
DynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow {
text: "Net surplus (deficit)".to_string(),
quantity: report
.quantity_for_id("total_income") // Get total income row
.iter()
.zip(report.quantity_for_id("total_expenses").iter()) // Zip with total expenses row
.map(|(i, e)| i - e) // Compute net surplus
.collect(),
id: Some("net_surplus".to_string()),
visible: true,
auto_hide: false,
link: None,
heading: true,
bordered: true,
},
}),
],
);
report.calculate();
report.auto_hide();
// Store the result
products.insert(
ReportingProductId {
name: "IncomeStatement",
kind: ReportingProductKind::Generic,
args: Box::new(self.args.clone()),
},
Box::new(report),
);
Ok(())
}
}
/// Generate transactions for unreconciled statement lines
#[derive(Debug)]
pub struct PostUnreconciledStatementLines {