Merge branch 'product_creater' into 'main'

Add pass flushing for move money

See merge request vato007/coster-rs!2
This commit is contained in:
Michael Pivato
2023-03-08 00:06:36 +00:00
5 changed files with 128 additions and 32 deletions

View File

@@ -14,10 +14,14 @@ pub use self::overhead_allocation::*;
mod create_products; mod create_products;
pub use self::create_products::*; pub use self::create_products::*;
mod shared_models;
pub use self::shared_models::*;
#[no_mangle] #[no_mangle]
pub extern "C" fn move_money_from_text( pub extern "C" fn move_money_from_text(
rules: *const c_char, rules: *const c_char,
lines: *const c_char, lines: *const c_char,
accounts: *const c_char,
use_numeric_accounts: bool, use_numeric_accounts: bool,
) -> *mut c_char { ) -> *mut c_char {
let mut output_writer = csv::Writer::from_writer(vec![]); let mut output_writer = csv::Writer::from_writer(vec![]);
@@ -29,11 +33,17 @@ pub extern "C" fn move_money_from_text(
assert!(!lines.is_null()); assert!(!lines.is_null());
CStr::from_ptr(lines) CStr::from_ptr(lines)
}; };
let safe_accounts = unsafe {
assert!(!lines.is_null());
CStr::from_ptr(accounts)
};
move_money( move_money(
csv::Reader::from_reader(safe_rules.to_bytes()), csv::Reader::from_reader(safe_rules.to_bytes()),
csv::Reader::from_reader(safe_lines.to_bytes()), csv::Reader::from_reader(safe_lines.to_bytes()),
csv::Reader::from_reader(safe_accounts.to_bytes()),
&mut output_writer, &mut output_writer,
use_numeric_accounts, use_numeric_accounts,
true,
); );
// TODO: Replace all these unwraps with something more elegant // TODO: Replace all these unwraps with something more elegant
let inner = output_writer.into_inner().unwrap(); let inner = output_writer.into_inner().unwrap();

View File

@@ -24,11 +24,17 @@ enum Commands {
#[arg(short = 'l', long, value_name = "FILE")] #[arg(short = 'l', long, value_name = "FILE")]
lines: PathBuf, lines: PathBuf,
#[arg(short = 'a', long, value_name = "FILE")]
accounts: PathBuf,
#[arg(short, long, value_name = "FILE")] #[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>, output: Option<PathBuf>,
#[arg(short, long)] #[arg(short, long)]
use_numeric_accounts: bool, use_numeric_accounts: bool,
#[arg(short, long)]
flush_pass: bool,
}, },
/// Combines rules to the minimum set required /// Combines rules to the minimum set required
smush_rules { smush_rules {
@@ -73,9 +79,18 @@ fn main() -> anyhow::Result<()> {
Commands::move_money { Commands::move_money {
rules, rules,
lines, lines,
accounts,
output, output,
use_numeric_accounts, use_numeric_accounts,
} => move_money(rules, lines, output, use_numeric_accounts), flush_pass,
} => move_money(
rules,
lines,
accounts,
output,
use_numeric_accounts,
flush_pass,
),
Commands::smush_rules { rules, output } => smush_rules(rules, output), Commands::smush_rules { rules, output } => smush_rules(rules, output),
Commands::allocate_overheads { Commands::allocate_overheads {
lines, lines,
@@ -102,14 +117,18 @@ fn main() -> anyhow::Result<()> {
fn move_money( fn move_money(
rules: PathBuf, rules: PathBuf,
lines: PathBuf, lines: PathBuf,
accounts: PathBuf,
output: Option<PathBuf>, output: Option<PathBuf>,
use_numeric_accounts: bool, use_numeric_accounts: bool,
flush_pass: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
coster_rs::move_money( coster_rs::move_money(
csv::Reader::from_path(rules)?, csv::Reader::from_path(rules)?,
csv::Reader::from_path(lines)?, csv::Reader::from_path(lines)?,
csv::Reader::from_path(accounts)?,
&mut csv::Writer::from_path(output.unwrap_or(PathBuf::from("output.csv")))?, &mut csv::Writer::from_path(output.unwrap_or(PathBuf::from("output.csv")))?,
use_numeric_accounts, use_numeric_accounts,
flush_pass,
) )
} }

View File

@@ -1,8 +1,10 @@
use std::collections::HashMap; use std::{collections::HashMap, thread::current};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use crate::CsvAccount;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct CsvMovementRule { struct CsvMovementRule {
#[serde(rename = "CostCentreSourceFrom", default)] #[serde(rename = "CostCentreSourceFrom", default)]
@@ -36,6 +38,8 @@ struct CsvMovementRule {
is_percent: String, is_percent: String,
#[serde(rename = "Apply", default)] #[serde(rename = "Apply", default)]
apply: String, apply: String,
#[serde(rename = "CostOutput")]
cost_output: Option<String>,
} }
#[derive(Default)] #[derive(Default)]
@@ -88,6 +92,7 @@ pub struct CsvCost {
pub department: String, pub department: String,
#[serde(serialize_with = "round_serialize")] #[serde(serialize_with = "round_serialize")]
pub value: f64, pub value: f64,
pub pass: Option<i32>,
} }
fn round_serialize<S>(x: &f64, s: S) -> Result<S::Ok, S::Error> fn round_serialize<S>(x: &f64, s: S) -> Result<S::Ok, S::Error>
@@ -97,15 +102,18 @@ where
s.serialize_f64((x * 100000.).round() / 100000.) s.serialize_f64((x * 100000.).round() / 100000.)
} }
pub fn move_money<R, L, O>( pub fn move_money<R, L, A, O>(
rules_reader: csv::Reader<R>, rules_reader: csv::Reader<R>,
lines_reader: csv::Reader<L>, lines_reader: csv::Reader<L>,
accounts_reader: csv::Reader<A>,
output: &mut csv::Writer<O>, output: &mut csv::Writer<O>,
use_numeric_accounts: bool, use_numeric_accounts: bool,
flush_pass: bool,
) -> anyhow::Result<()> ) -> anyhow::Result<()>
where where
R: std::io::Read, R: std::io::Read,
L: std::io::Read, L: std::io::Read,
A: std::io::Read,
O: std::io::Write, O: std::io::Write,
{ {
let mut lines_reader = lines_reader; let mut lines_reader = lines_reader;
@@ -146,6 +154,14 @@ where
) )
}) })
.collect(); .collect();
let mut accounts_reader = accounts_reader;
let all_accounts = accounts_reader
.deserialize()
.collect::<Result<Vec<CsvAccount>, csv::Error>>()?;
let account_mappings: HashMap<String, CsvAccount> = all_accounts
.into_iter()
.map(|account| (account.code.clone(), account))
.collect();
let all_accounts_sorted = if use_numeric_accounts { let all_accounts_sorted = if use_numeric_accounts {
lines lines
@@ -172,6 +188,7 @@ where
let mut rules_reader = rules_reader; let mut rules_reader = rules_reader;
let mut rules: Vec<MovementRule> = vec![]; let mut rules: Vec<MovementRule> = vec![];
for movement_rule in rules_reader.deserialize() { for movement_rule in rules_reader.deserialize() {
// TODO: Consider reclass rule group, how does that even work?
let movement_rule: CsvMovementRule = movement_rule?; let movement_rule: CsvMovementRule = movement_rule?;
let from_accounts = extract_range( let from_accounts = extract_range(
movement_rule.source_from_account, movement_rule.source_from_account,
@@ -179,12 +196,24 @@ where
false, false,
&all_accounts_sorted, &all_accounts_sorted,
); );
let to_accounts = extract_range( let to_accounts = if movement_rule.cost_output.is_some() {
account_mappings
.iter()
.filter(|(_, account)| {
account.cost_output.is_some()
&& account.cost_output.clone().unwrap()
== movement_rule.cost_output.clone().unwrap()
})
.map(|(code, _)| code.clone())
.collect()
} else {
extract_range(
movement_rule.dest_from_account, movement_rule.dest_from_account,
movement_rule.dest_to_account, movement_rule.dest_to_account,
false, false,
&all_accounts_sorted, &all_accounts_sorted,
); )
};
let from_departments = extract_range( let from_departments = extract_range(
movement_rule.source_from_department, movement_rule.source_from_department,
movement_rule.source_to_department, movement_rule.source_to_department,
@@ -218,16 +247,19 @@ where
} }
// Then run move_money // Then run move_money
let moved = move_money_2(lines, &rules); let moved = move_money_2(lines, &rules, flush_pass);
// Ouput the list moved moneys // Ouput the list moved moneys
for money in moved { for money in moved {
for (unit, value) in money.totals {
output.serialize(CsvCost { output.serialize(CsvCost {
account: money.0.account, account: unit.account,
department: money.0.department, department: unit.department,
value: money.1, value,
pass: if flush_pass { Some(money.pass) } else { None },
})?; })?;
} }
}
Ok(()) Ok(())
} }
@@ -267,6 +299,11 @@ fn extract_range(from: String, to: String, all: bool, options: &Vec<String>) ->
// Advantage of this approach is it can be easily extended to run on the gpu. // Advantage of this approach is it can be easily extended to run on the gpu.
pub fn move_money_1() {} pub fn move_money_1() {}
pub struct MoveMoneyResult {
pass: i32,
totals: HashMap<Unit, f64>,
}
// Approach 2: // Approach 2:
// Traditinoal/naive, total for each department is stored in an initial map (department -> total amount). // Traditinoal/naive, total for each department is stored in an initial map (department -> total amount).
// Another map is built up for each rule, and each rule is processed based on the amount in the current total // Another map is built up for each rule, and each rule is processed based on the amount in the current total
@@ -281,13 +318,34 @@ pub fn move_money_1() {}
pub fn move_money_2( pub fn move_money_2(
initial_totals: HashMap<Unit, f64>, initial_totals: HashMap<Unit, f64>,
rules: &Vec<MovementRule>, rules: &Vec<MovementRule>,
) -> HashMap<Unit, f64> { flush_pass: bool,
) -> Vec<MoveMoneyResult> {
// Note: It's potentially a bit more intensive to use cloned totals (rather than just update temp_total per rule), // Note: It's potentially a bit more intensive to use cloned totals (rather than just update temp_total per rule),
// but it's much simpler code and, and since we're only working line-by-line, it isn't really that much memory in practice // but it's much simpler code and, and since we're only working line-by-line, it isn't really that much memory in practice
let mut running_total = HashMap::from(initial_totals); let mut running_total = HashMap::from(initial_totals);
let mut temp_total = running_total.clone(); let mut temp_total = running_total.clone();
let mut moveMoneyResult: Vec<MoveMoneyResult> = vec![];
let mut current_pass = 0;
if flush_pass {
moveMoneyResult.push(MoveMoneyResult {
pass: current_pass,
totals: running_total.clone(),
})
}
for rule in rules { for rule in rules {
if rule.is_separator { if rule.is_separator {
if flush_pass {
// Flush the totals at the end of this pass (more specifically the change)
moveMoneyResult.push(MoveMoneyResult {
pass: current_pass,
totals: running_total
.iter()
.map(|(unit, value)| (unit.clone(), temp_total.get(unit).unwrap() - value))
.filter(|(_, value)| *value != 0.)
.collect(),
});
current_pass += 1;
}
running_total = temp_total.clone(); running_total = temp_total.clone();
} else { } else {
let mut sum_from = 0.; let mut sum_from = 0.;
@@ -323,7 +381,12 @@ pub fn move_money_2(
} }
} }
} }
temp_total moveMoneyResult.push(MoveMoneyResult {
pass: current_pass,
totals: temp_total,
});
moveMoneyResult
} }
#[cfg(test)] #[cfg(test)]
@@ -333,7 +396,9 @@ mod tests {
super::move_money( super::move_money(
csv::Reader::from_path("reclassrule.csv").unwrap(), csv::Reader::from_path("reclassrule.csv").unwrap(),
csv::Reader::from_path("line.csv").unwrap(), csv::Reader::from_path("line.csv").unwrap(),
csv::Reader::from_path("account.csv").unwrap(),
&mut csv::Writer::from_path("output.csv").unwrap(), &mut csv::Writer::from_path("output.csv").unwrap(),
false,
true, true,
); );
} }

View File

@@ -4,7 +4,7 @@ use itertools::Itertools;
use nalgebra::{DMatrix, Dynamic, LU}; use nalgebra::{DMatrix, Dynamic, LU};
use serde::Deserialize; use serde::Deserialize;
use crate::CsvCost; use crate::{CsvAccount, CsvCost};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum DepartmentType { pub enum DepartmentType {
@@ -39,20 +39,6 @@ pub struct AllocationStatisticAccountRange {
end: usize, end: usize,
} }
#[derive(Deserialize)]
pub struct CsvAccount {
#[serde(rename = "Code")]
code: String,
#[serde(rename = "Description")]
description: Option<String>,
#[serde(rename = "Type")]
account_type: String,
#[serde(rename = "CostOutput")]
cost_output: Option<String>,
#[serde(rename = "PercentFixed")]
percent_fixed: f64,
}
type CsvCostCentre = HashMap<String, String>; type CsvCostCentre = HashMap<String, String>;
type CsvArea = HashMap<String, String>; type CsvArea = HashMap<String, String>;
@@ -393,6 +379,7 @@ where
account: cost.account.clone(), account: cost.account.clone(),
department: department.department, department: department.department,
value: department.value, value: department.value,
pass: None,
})?; })?;
} }
} }

15
src/shared_models.rs Normal file
View File

@@ -0,0 +1,15 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CsvAccount {
#[serde(rename = "Code")]
pub code: String,
#[serde(rename = "Description")]
pub description: Option<String>,
#[serde(rename = "Type")]
pub account_type: String,
#[serde(rename = "CostOutput")]
pub cost_output: Option<String>,
#[serde(rename = "PercentFixed")]
pub percent_fixed: f64,
}