Fixes overhead allocation to consider lines that don't participate in overhead allocation, and fix from_cost_centre department
This commit is contained in:
@@ -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(),
|
overhead_cc.clone(),
|
||||||
other_cc.clone(),
|
combined_stat.0.clone(),
|
||||||
totals
|
totals.get(&combined_stat).map(|f| *f).unwrap(),
|
||||||
.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() {
|
||||||
if !overhead_ccs.contains(&line.department) {
|
// TODO: Should we still output accounts that aren't in the accounts file anyway?
|
||||||
|
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>>()
|
||||||
|
|||||||
Reference in New Issue
Block a user