diff --git a/src/lib.rs b/src/lib.rs index 69c4477..2292aa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,14 @@ pub use self::overhead_allocation::*; mod create_products; pub use self::create_products::*; +mod shared_models; +pub use self::shared_models::*; + #[no_mangle] pub extern "C" fn move_money_from_text( rules: *const c_char, lines: *const c_char, + accounts: *const c_char, use_numeric_accounts: bool, ) -> *mut c_char { 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()); CStr::from_ptr(lines) }; + let safe_accounts = unsafe { + assert!(!lines.is_null()); + CStr::from_ptr(accounts) + }; move_money( csv::Reader::from_reader(safe_rules.to_bytes()), csv::Reader::from_reader(safe_lines.to_bytes()), + csv::Reader::from_reader(safe_accounts.to_bytes()), &mut output_writer, use_numeric_accounts, + true, ); // TODO: Replace all these unwraps with something more elegant let inner = output_writer.into_inner().unwrap(); diff --git a/src/main.rs b/src/main.rs index 53fc819..4337768 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,17 @@ enum Commands { #[arg(short = 'l', long, value_name = "FILE")] lines: PathBuf, + #[arg(short = 'a', long, value_name = "FILE")] + accounts: PathBuf, + #[arg(short, long, value_name = "FILE")] output: Option, #[arg(short, long)] use_numeric_accounts: bool, + + #[arg(short, long)] + flush_pass: bool, }, /// Combines rules to the minimum set required smush_rules { @@ -73,9 +79,18 @@ fn main() -> anyhow::Result<()> { Commands::move_money { rules, lines, + accounts, output, 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::allocate_overheads { lines, @@ -102,14 +117,18 @@ fn main() -> anyhow::Result<()> { fn move_money( rules: PathBuf, lines: PathBuf, + accounts: PathBuf, output: Option, use_numeric_accounts: bool, + flush_pass: bool, ) -> anyhow::Result<()> { coster_rs::move_money( csv::Reader::from_path(rules)?, csv::Reader::from_path(lines)?, + csv::Reader::from_path(accounts)?, &mut csv::Writer::from_path(output.unwrap_or(PathBuf::from("output.csv")))?, use_numeric_accounts, + flush_pass, ) } diff --git a/src/move_money.rs b/src/move_money.rs index d5e423e..a2e1bb4 100644 --- a/src/move_money.rs +++ b/src/move_money.rs @@ -1,8 +1,10 @@ -use std::collections::HashMap; +use std::{collections::HashMap, thread::current}; use itertools::Itertools; use serde::{Deserialize, Serialize, Serializer}; +use crate::CsvAccount; + #[derive(Debug, Deserialize)] struct CsvMovementRule { #[serde(rename = "CostCentreSourceFrom", default)] @@ -36,6 +38,8 @@ struct CsvMovementRule { is_percent: String, #[serde(rename = "Apply", default)] apply: String, + #[serde(rename = "CostOutput")] + cost_output: Option, } #[derive(Default)] @@ -88,6 +92,7 @@ pub struct CsvCost { pub department: String, #[serde(serialize_with = "round_serialize")] pub value: f64, + pub pass: Option, } fn round_serialize(x: &f64, s: S) -> Result @@ -97,15 +102,18 @@ where s.serialize_f64((x * 100000.).round() / 100000.) } -pub fn move_money( +pub fn move_money( rules_reader: csv::Reader, lines_reader: csv::Reader, + accounts_reader: csv::Reader, output: &mut csv::Writer, use_numeric_accounts: bool, + flush_pass: bool, ) -> anyhow::Result<()> where R: std::io::Read, L: std::io::Read, + A: std::io::Read, O: std::io::Write, { let mut lines_reader = lines_reader; @@ -146,6 +154,14 @@ where ) }) .collect(); + let mut accounts_reader = accounts_reader; + let all_accounts = accounts_reader + .deserialize() + .collect::, csv::Error>>()?; + let account_mappings: HashMap = all_accounts + .into_iter() + .map(|account| (account.code.clone(), account)) + .collect(); let all_accounts_sorted = if use_numeric_accounts { lines @@ -172,6 +188,7 @@ where let mut rules_reader = rules_reader; let mut rules: Vec = vec![]; for movement_rule in rules_reader.deserialize() { + // TODO: Consider reclass rule group, how does that even work? let movement_rule: CsvMovementRule = movement_rule?; let from_accounts = extract_range( movement_rule.source_from_account, @@ -179,12 +196,24 @@ where false, &all_accounts_sorted, ); - let to_accounts = extract_range( - movement_rule.dest_from_account, - movement_rule.dest_to_account, - false, - &all_accounts_sorted, - ); + 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_to_account, + false, + &all_accounts_sorted, + ) + }; let from_departments = extract_range( movement_rule.source_from_department, movement_rule.source_to_department, @@ -218,15 +247,18 @@ where } // 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 for money in moved { - output.serialize(CsvCost { - account: money.0.account, - department: money.0.department, - value: money.1, - })?; + for (unit, value) in money.totals { + output.serialize(CsvCost { + account: unit.account, + department: unit.department, + value, + pass: if flush_pass { Some(money.pass) } else { None }, + })?; + } } Ok(()) @@ -267,6 +299,11 @@ fn extract_range(from: String, to: String, all: bool, options: &Vec) -> // Advantage of this approach is it can be easily extended to run on the gpu. pub fn move_money_1() {} +pub struct MoveMoneyResult { + pass: i32, + totals: HashMap, +} + // Approach 2: // 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 @@ -281,13 +318,34 @@ pub fn move_money_1() {} pub fn move_money_2( initial_totals: HashMap, rules: &Vec, -) -> HashMap { + flush_pass: bool, +) -> Vec { // 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 let mut running_total = HashMap::from(initial_totals); let mut temp_total = running_total.clone(); + let mut moveMoneyResult: Vec = vec![]; + let mut current_pass = 0; + if flush_pass { + moveMoneyResult.push(MoveMoneyResult { + pass: current_pass, + totals: running_total.clone(), + }) + } for rule in rules { 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(); } else { 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)] @@ -333,7 +396,9 @@ mod tests { super::move_money( csv::Reader::from_path("reclassrule.csv").unwrap(), csv::Reader::from_path("line.csv").unwrap(), + csv::Reader::from_path("account.csv").unwrap(), &mut csv::Writer::from_path("output.csv").unwrap(), + false, true, ); } diff --git a/src/overhead_allocation.rs b/src/overhead_allocation.rs index ae3226b..0b3569e 100644 --- a/src/overhead_allocation.rs +++ b/src/overhead_allocation.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use nalgebra::{DMatrix, Dynamic, LU}; use serde::Deserialize; -use crate::CsvCost; +use crate::{CsvAccount, CsvCost}; #[derive(Debug, PartialEq, Eq)] pub enum DepartmentType { @@ -39,20 +39,6 @@ pub struct AllocationStatisticAccountRange { end: usize, } -#[derive(Deserialize)] -pub struct CsvAccount { - #[serde(rename = "Code")] - code: String, - #[serde(rename = "Description")] - description: Option, - #[serde(rename = "Type")] - account_type: String, - #[serde(rename = "CostOutput")] - cost_output: Option, - #[serde(rename = "PercentFixed")] - percent_fixed: f64, -} - type CsvCostCentre = HashMap; type CsvArea = HashMap; @@ -393,6 +379,7 @@ where account: cost.account.clone(), department: department.department, value: department.value, + pass: None, })?; } } diff --git a/src/shared_models.rs b/src/shared_models.rs new file mode 100644 index 0000000..d41d204 --- /dev/null +++ b/src/shared_models.rs @@ -0,0 +1,15 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct CsvAccount { + #[serde(rename = "Code")] + pub code: String, + #[serde(rename = "Description")] + pub description: Option, + #[serde(rename = "Type")] + pub account_type: String, + #[serde(rename = "CostOutput")] + pub cost_output: Option, + #[serde(rename = "PercentFixed")] + pub percent_fixed: f64, +}