From 5595ab2f7c2ed1d4f7db85e15ed19059cba04034 Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Wed, 8 Feb 2023 18:32:29 +1030 Subject: [PATCH] Start adding overhead allocation load from file --- src/move_money.rs | 6 +- src/overhead_allocation.rs | 143 +++++++++++++++++++++++++++++++++---- 2 files changed, 132 insertions(+), 17 deletions(-) diff --git a/src/move_money.rs b/src/move_money.rs index 8b2cf71..943a1a1 100644 --- a/src/move_money.rs +++ b/src/move_money.rs @@ -83,10 +83,10 @@ pub struct Unit { #[derive(Debug, Serialize, Deserialize)] pub struct CsvCost { #[serde(rename = "ACCOUNT")] - account: String, + pub account: String, #[serde(rename = "COSTCENTRE")] - department: String, - value: f64, + pub department: String, + pub value: f64, } pub fn move_money( diff --git a/src/overhead_allocation.rs b/src/overhead_allocation.rs index 058d7b9..0f98479 100644 --- a/src/overhead_allocation.rs +++ b/src/overhead_allocation.rs @@ -4,6 +4,8 @@ use itertools::Itertools; use nalgebra::{DMatrix, Dynamic, LU}; use serde::Deserialize; +use crate::CsvCost; + #[derive(Debug, PartialEq, Eq)] pub enum DepartmentType { Operating, @@ -22,6 +24,11 @@ pub struct CsvAllocationStatistic { account_ranges: String, } +pub struct AllocationStatisticAccountRange { + start: String, + end: String, +} + #[derive(Deserialize)] pub struct CsvAccount { #[serde(rename = "Code")] @@ -96,30 +103,138 @@ where CostCentre: Read, Output: std::io::Write, { - let mut accounts_reader = accounts; - let all_accounts_sorted: Result, csv::Error> = - accounts_reader.deserialize::().collect(); - let mut accounts_sorted = all_accounts_sorted?; + let mut lines_reader = lines; + let lines = lines_reader + .deserialize() + .collect::, csv::Error>>()?; + // Sort the accounts, as allocation statistics use account ranges - if use_numeric_accounts { - accounts_sorted.sort_by(|a, b| { - a.code - .parse::() - .unwrap() - .cmp(&b.code.parse::().unwrap()) - }) + let all_accounts_sorted: Vec = if use_numeric_accounts { + lines + .iter() + .map(|line| line.account.clone().parse::().unwrap()) + .unique() + .sorted() + .map(|account| account.to_string()) + .collect() } else { - accounts_sorted.sort_by(|a, b| a.code.cmp(&b.code)) + lines + .iter() + .map(|line| line.account.clone()) + .unique() + .sorted() + .collect() }; - // Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers') + let mut allocation_statistics_reader = allocation_statistics; + let allocation_statistics = allocation_statistics_reader + .deserialize() + .collect::, csv::Error>>()?; + + // For each allocation statistic, sum the cost centres across accounts in the allocaiton statistic range + let flat_department_costs: Vec<(String, String, f64)> = allocation_statistics + .iter() + .map(|allocation_statistic| { + ( + allocation_statistic, + split_allocation_statistic_range(allocation_statistic), + ) + }) + .flat_map(|allocation_statistic| { + let mut total_department_costs: HashMap = HashMap::new(); + let cc_costs = lines + .iter() + .filter(|line| { + let line_index = all_accounts_sorted + .iter() + .position(|account| account == &line.account); + allocation_statistic + .1 + .iter() + .find(|range| { + let start_index = all_accounts_sorted + .iter() + .position(|account| account == range.0); + let end_index = all_accounts_sorted + .iter() + .position(|account| account == range.1); + line_index >= start_index && line_index <= end_index + }) + .is_some() + }) + .for_each(|line| { + *total_department_costs.entry(line.department).or_insert(0.) += line.value; + }); + total_department_costs + .iter() + .map(|entry| (entry.0, allocation_statistic.0.name, entry.1)) + .collect() + }) + .collect(); + // TODO: If ignore negative is used, then set values < 0 to 0 + + let mut rollups: HashMap>> = HashMap::new(); + let mut cost_centres = cost_centres; + for cost_centre in cost_centres.records() { + let cost_centre = cost_centre?; + // Extract rollups, used later with the areas... I could do a map of rollups -> cc's, or just a list of rollups on each cc struct + // I think map of rollup -> cc would be better, although this would need to be for each rollup slot... so a map of maps? + } + + let mut areas = areas; + let area_name_index = areas + .headers()? + .iter() + .position(|header| header == "Name") + .unwrap(); + let allocation_statistic_index = areas + .headers()? + .iter() + .position(|header| header == "AllocationStatistic") + .unwrap(); + // For each overhead area, get the cost centres in the area, and get all cost centres + // that fit the limit to criteria for the area (skip any cases of overhead cc = other cc). + // Then get the totals for the other ccs, by looking in the flat_department_costs, where the + // allocation statistic matches the allocation statistic for this area + for area in areas.records() { + let area = area?; + // Check for limitTos, should probably somehow build out the list of allocation rules from this point. + let area_name = area.get(area_name_index).unwrap(); + let allocation_statistic = area.get(allocation_statistic_index).unwrap(); + } + + // Finally, for each cc match total produced previously, sum the overhead cc where overhead cc appears in other cc, then + // divide the other cc by this summed amount // do reciprocal allocation (only for variable portion of accounts), for each account - // Copy across fixed stuff (if necessary, not sure it is) + // Copy across fixed stuff (if necessary, not sure it is)... don't think it's necessary, initial totals handle this Ok(()) } +fn split_allocation_statistic_range( + allocation_statistic: &CsvAllocationStatistic, +) -> Vec { + // TODO: This split needs to be more comprehensive so that we don't split between quotes + let split = allocation_statistic.account_ranges.split(";"); + split + .map(|split| { + let range_split = split.split('-').collect::>(); + if range_split.len() == 1 { + AllocationStatisticAccountRange { + start: range_split[0].to_owned(), + end: range_split[0].to_owned(), + } + } else { + AllocationStatisticAccountRange { + start: range_split[0].to_owned(), + end: range_split[1].to_owned(), + } + } + }) + .collect() +} + // Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs // to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when // matrix is singular