Fixes overhead allocation to consider lines that don't participate in overhead allocation, and fix from_cost_centre department

This commit is contained in:
Michael Pivato
2024-02-23 19:51:58 +10:30
parent 19e08f9ca7
commit e88d2a6319

View File

@@ -3,6 +3,7 @@ use std::{
io::Read, io::Read,
}; };
use csv::Reader;
use itertools::Itertools; use itertools::Itertools;
use nalgebra::{DMatrix, Dynamic, LU}; use nalgebra::{DMatrix, Dynamic, LU};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
@@ -93,9 +94,6 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
allocation_statistics: &mut csv::Reader<AllocationStatistic>, allocation_statistics: &mut csv::Reader<AllocationStatistic>,
areas: &mut csv::Reader<Area>, areas: &mut csv::Reader<Area>,
cost_centres: &mut csv::Reader<CostCentre>, cost_centres: &mut csv::Reader<CostCentre>,
// TODO: Receiver method rather than this writer that can accept
// the raw float results, so we can write in an alternate format
// that more accurately represents the values on disk
output: &mut impl RecordSerializer, output: &mut impl RecordSerializer,
use_numeric_accounts: bool, use_numeric_accounts: bool,
exclude_negative_allocation_statistics: bool, exclude_negative_allocation_statistics: bool,
@@ -115,28 +113,8 @@ where
.deserialize() .deserialize()
.collect::<Result<Vec<CsvCost>, csv::Error>>()?; .collect::<Result<Vec<CsvCost>, csv::Error>>()?;
let all_accounts_sorted: Vec<String> = if use_numeric_accounts { let all_accounts_sorted: Vec<String> =
accounts get_accounts_sorted(use_numeric_accounts, &account_type, accounts);
.deserialize::<CsvAccount>()
.filter(|account| {
account.is_ok() && account.as_ref().unwrap().account_type == account_type
})
.map(|line| line.unwrap().code.clone().parse::<i32>().unwrap())
.unique()
.sorted()
.map(|account| account.to_string())
.collect()
} else {
accounts
.deserialize::<CsvAccount>()
.filter(|account| {
account.is_ok() && account.as_ref().unwrap().account_type == account_type
})
.map(|line| line.unwrap().code.clone())
.unique()
.sorted()
.collect()
};
let allocation_statistics = allocation_statistics let allocation_statistics = allocation_statistics
.deserialize::<CsvAllocationStatistic>() .deserialize::<CsvAllocationStatistic>()
@@ -266,6 +244,7 @@ where
let mut limited_ccs: Vec<String> = Vec::new(); let mut limited_ccs: Vec<String> = Vec::new();
for limit_to in limit_tos.iter() { for limit_to in limit_tos.iter() {
// TODO: It is technically possible to have more than one limit to (I think?) for a slot, so consider eventually splitting this and doing a foreach // TODO: It is technically possible to have more than one limit to (I think?) for a slot, so consider eventually splitting this and doing a foreach
// Also there's an exclude criteria that needs to be considered, which can exclude a rollup that would normally get included
let limit_value = area.get(&(format!("LimitTo:{}", limit_to))).unwrap(); let limit_value = area.get(&(format!("LimitTo:{}", limit_to))).unwrap();
if limit_value.is_empty() { if limit_value.is_empty() {
continue; continue;
@@ -293,35 +272,24 @@ where
let mut totals: Vec<(String, String, f64)> = overhead_ccs let mut totals: Vec<(String, String, f64)> = overhead_ccs
.par_iter() .par_iter()
.flat_map(|overhead_cc| { .flat_map(|overhead_cc| {
let limited = limited_ccs limited_ccs
.iter() .iter()
.filter(|other_cc| { .map(|other_cc| (other_cc.clone(), allocation_statistic.clone()))
totals.contains_key(&( .filter_map(|(other_cc, allocation_statistic)| {
// TODO: This looks terrible let combined_stat = (other_cc, allocation_statistic);
other_cc.clone().clone(), if !totals.contains_key(&combined_stat) {
allocation_statistic.clone(), None
)) } else {
}) Some((
.map(|other_cc| { overhead_cc.clone(),
( combined_stat.0.clone(),
overhead_cc.clone(), totals.get(&combined_stat).map(|f| *f).unwrap(),
other_cc.clone(), ))
totals }
.get(&(other_cc.clone(), allocation_statistic.clone()))
.map(|f| *f)
.unwrap(),
)
}) })
.filter(|(_, _, value)| *value != 0.) .filter(|(_, _, value)| *value != 0.)
.filter(|(from_cc, to_cc, _)| from_cc != to_cc) .filter(|(from_cc, to_cc, _)| from_cc != to_cc)
.collect_vec(); .collect_vec()
// TODO: Put me back if rayon proves problematic
// Insert is safe, since an overhead cc can only be a part of one area
// overhead_cc_totals.insert(
// overhead_cc.clone(),
// limited.iter().map(|(_, _, value)| value).sum(),
// );
limited
}) })
.collect(); .collect();
overhead_other_total.append(&mut totals); overhead_other_total.append(&mut totals);
@@ -355,24 +323,41 @@ where
} }
// Export initial totals for operating departments // Export initial totals for operating departments
if show_from { for line in lines.iter() {
for line in lines.iter() { // TODO: Should we still output accounts that aren't in the accounts file anyway?
if !overhead_ccs.contains(&line.department) { if all_accounts_sorted
.iter()
.find(|account| **account == line.account)
.is_some()
&& !overhead_ccs.contains(&line.department)
&& (show_from
// When we write out the final amounts rather than changes,
// ensure we still output departments that won't be receiving
// any costs.
|| !overhead_other_total
.iter()
.any(|(_, to_department, _)| *to_department == line.department))
{
if show_from {
output.serialize(MovedAmount { output.serialize(MovedAmount {
account: line.account.clone(), account: line.account.clone(),
cost_centre: line.department.clone(), cost_centre: line.department.clone(),
value: line.value, value: line.value,
from_cost_centre: line.department.clone(), from_cost_centre: line.department.clone(),
})?; })?;
} else {
output.serialize(CsvCost {
account: line.account.clone(),
department: line.department.clone(),
value: line.value,
})?;
} }
} }
} }
// Finally, for each cc match total produced previously, sum the overhead cc where overhead cc appears in other cc, then // 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 (thus getting the relative cost) // divide the other cc by this summed amount (thus getting the relative cost)
// At this point we convert to our format that's actually used in overhead allocation
// At this point we convert to our format that's actually used, need to somehow recover the to_cc_type... could build that out from the areas
let allocation_rules: Vec<OverheadAllocationRule> = overhead_other_total let allocation_rules: Vec<OverheadAllocationRule> = overhead_other_total
.iter() .iter()
.map( .map(
@@ -389,6 +374,8 @@ where
) )
.collect(); .collect();
// TODO: THIS CAN BE WRONG WHEN USING A FILE WITH ALL PASSES, for now ensure the input movement
// file only contains the final pass/outputs.
let mut initial_account_costs: HashMap<String, Vec<TotalDepartmentCost>> = HashMap::new(); let mut initial_account_costs: HashMap<String, Vec<TotalDepartmentCost>> = HashMap::new();
for line in lines { for line in lines {
// Only include accounts we've already filtered on (i.e. by account type) // Only include accounts we've already filtered on (i.e. by account type)
@@ -430,7 +417,7 @@ where
for cost in results { for cost in results {
for department in cost.summed_department_costs { for department in cost.summed_department_costs {
// Any consumers should assume missing cc/account value was 0 (we already ignore overhead, as they all 0 out) // Any consumers should assume missing cc/account value was 0 (we already ignore overhead, as they all 0 out)
if department.value > 0.00001 || department.value < -0.00001 { if department.value != 0_f64 {
output.serialize(CsvCost { output.serialize(CsvCost {
account: cost.account.clone(), account: cost.account.clone(),
department: department.department, department: department.department,
@@ -443,6 +430,35 @@ where
Ok(()) Ok(())
} }
fn get_accounts_sorted(
use_numeric_accounts: bool,
account_type: &String,
accounts: &mut Reader<impl Read>,
) -> Vec<String> {
if use_numeric_accounts {
accounts
.deserialize::<CsvAccount>()
.filter(|account| {
account.is_ok() && account.as_ref().unwrap().account_type == *account_type
})
.map(|line| line.unwrap().code.clone().parse::<i32>().unwrap())
.unique()
.sorted()
.map(|account| account.to_string())
.collect()
} else {
accounts
.deserialize::<CsvAccount>()
.filter(|account| {
account.is_ok() && account.as_ref().unwrap().account_type == *account_type
})
.map(|line| line.unwrap().code.clone())
.unique()
.sorted()
.collect()
}
}
fn split_allocation_statistic_range( fn split_allocation_statistic_range(
allocation_statistic: &CsvAllocationStatistic, allocation_statistic: &CsvAllocationStatistic,
accounts_sorted: &Vec<String>, accounts_sorted: &Vec<String>,
@@ -661,7 +677,7 @@ fn solve_reciprocal_no_from(
&operating_slice_costs, &operating_slice_costs,
); );
// // Borrow so we don't move between loops // Borrow so we don't move between loops
let operating_overhead_mappings = &operating_overhead_mappings_mat; let operating_overhead_mappings = &operating_overhead_mappings_mat;
let calculated_overheads = &calculated_overheads; let calculated_overheads = &calculated_overheads;
@@ -682,7 +698,6 @@ fn solve_reciprocal_no_from(
// Redistribute floating point errors (only for ccs we actually allocated from/to) // Redistribute floating point errors (only for ccs we actually allocated from/to)
// Considered removing this since redistribution should be done in cost driver calculations, however since that usually // Considered removing this since redistribution should be done in cost driver calculations, however since that usually
// does nothing, we may as well keep this just in case. // does nothing, we may as well keep this just in case.
// TODO: Not sure we actually need this, would probably be better to have a better storage format than // TODO: Not sure we actually need this, would probably be better to have a better storage format than
// csv/string conversions // csv/string conversions
// let initial_cost: f64 = total_costs // let initial_cost: f64 = total_costs
@@ -696,7 +711,6 @@ fn solve_reciprocal_no_from(
// .sum(); // .sum();
// let new_cost: f64 = converted_result.iter().map(|cost| cost.value).sum(); // let new_cost: f64 = converted_result.iter().map(|cost| cost.value).sum();
// let diff = initial_cost - new_cost; // let diff = initial_cost - new_cost;
AccountCost { AccountCost {
account: total_costs.account.clone(), account: total_costs.account.clone(),
summed_department_costs: converted_result summed_department_costs: converted_result
@@ -753,7 +767,7 @@ fn solve_reciprocal_with_from<T: ReciprocalAllocationSolver + Sync + Send>(
account: total_costs.account.clone(), account: total_costs.account.clone(),
cost_centre: department.clone(), cost_centre: department.clone(),
value, value,
from_cost_centre: department.clone(), from_cost_centre: overhead_department_cost.department.clone(),
}) })
.filter(|cost| cost.value != 0_f64) .filter(|cost| cost.value != 0_f64)
.collect::<Vec<MovedAmount>>() .collect::<Vec<MovedAmount>>()