Implement overhead allocation for different account types, add it to binaries
This commit is contained in:
@@ -104,6 +104,9 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
||||
cost_centres: csv::Reader<CostCentre>,
|
||||
output: &mut csv::Writer<Output>,
|
||||
use_numeric_accounts: bool,
|
||||
exclude_negative_allocation_statistics: bool,
|
||||
any_limit_criteria: bool,
|
||||
account_type: String,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
Lines: Read,
|
||||
@@ -120,10 +123,12 @@ where
|
||||
|
||||
let mut accounts = accounts;
|
||||
|
||||
// TODO: Accounts need to come from actual account fiile
|
||||
let all_accounts_sorted: 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()
|
||||
@@ -132,6 +137,9 @@ where
|
||||
} 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()
|
||||
@@ -140,63 +148,69 @@ where
|
||||
|
||||
let mut allocation_statistics_reader = allocation_statistics;
|
||||
let allocation_statistics = allocation_statistics_reader
|
||||
.deserialize()
|
||||
.deserialize::<CsvAllocationStatistic>()
|
||||
.filter(|allocation_statistic| {
|
||||
allocation_statistic.as_ref().unwrap().account_type == account_type
|
||||
})
|
||||
.collect::<Result<Vec<CsvAllocationStatistic>, csv::Error>>()?;
|
||||
|
||||
let split_allocation_ranges: Vec<(String, Vec<AllocationStatisticAccountRange>)> =
|
||||
allocation_statistics
|
||||
.iter()
|
||||
.map(|allocation_statistic| {
|
||||
(
|
||||
allocation_statistic.name.clone(),
|
||||
split_allocation_statistic_range(allocation_statistic, &all_accounts_sorted),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// For each allocation statistic, sum the cost centres across accounts in the allocaiton statistic range
|
||||
// value is (cc, allocation_statistic, total)
|
||||
// TODO: This is super slow
|
||||
let flat_department_costs: HashMap<(String, String), f64> = allocation_statistics
|
||||
.iter()
|
||||
.map(|allocation_statistic| {
|
||||
(
|
||||
allocation_statistic,
|
||||
split_allocation_statistic_range(allocation_statistic, &all_accounts_sorted),
|
||||
)
|
||||
})
|
||||
.flat_map(|allocation_statistic| {
|
||||
let mut total_department_costs: HashMap<String, f64> = HashMap::new();
|
||||
lines
|
||||
let mut totals: HashMap<(String, String), f64> = HashMap::new();
|
||||
for line in lines.iter() {
|
||||
// TODO: Another optimisation potential here, puttinig the accounts into a map, although less important since there's usually <1k accounts
|
||||
let line_index = all_accounts_sorted
|
||||
.iter()
|
||||
.position(|account| account == &line.account);
|
||||
// Skip account as it doesn't exist (likely due to wrong account type)
|
||||
if line_index.is_none() {
|
||||
continue;
|
||||
}
|
||||
let line_index = line_index.unwrap();
|
||||
// Find the allocation statistics this line is in
|
||||
for (allocation_statistic, range) in split_allocation_ranges.iter() {
|
||||
if range
|
||||
.iter()
|
||||
.filter(|line| {
|
||||
let line_index = all_accounts_sorted
|
||||
.iter()
|
||||
.position(|account| account == &line.account)
|
||||
.unwrap();
|
||||
allocation_statistic
|
||||
.1
|
||||
.iter()
|
||||
.find(|range| line_index >= range.start && line_index <= range.end)
|
||||
.is_some()
|
||||
})
|
||||
.for_each(|line| {
|
||||
*total_department_costs
|
||||
.entry(line.department.clone())
|
||||
.or_insert(0.) += line.value;
|
||||
});
|
||||
total_department_costs
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
(
|
||||
(entry.0.clone(), allocation_statistic.0.name.clone()),
|
||||
*entry.1,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<((String, String), f64)>>()
|
||||
})
|
||||
.collect();
|
||||
// TODO: If ignore negative is used, then set values < 0 to 0
|
||||
.find(|range| line_index >= range.start && line_index <= range.end)
|
||||
.is_some()
|
||||
{
|
||||
*totals
|
||||
.entry((line.department.clone(), allocation_statistic.clone()))
|
||||
.or_insert(0.) += line.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut rollups: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
|
||||
// If ignore negative is used, then set values < 0 to 0
|
||||
if exclude_negative_allocation_statistics {
|
||||
for ((_, _), total) in totals.iter_mut() {
|
||||
if *total < 0. {
|
||||
*total = 0.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group ccs by area
|
||||
let mut area_ccs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut cost_centres = cost_centres;
|
||||
let headers = cost_centres.headers()?;
|
||||
headers
|
||||
// Group ccs by rollup, and group rollups into their slot
|
||||
let mut rollups: HashMap<String, HashMap<String, Vec<String>>> = headers
|
||||
.iter()
|
||||
.filter(|name| name.to_lowercase().starts_with("rollupslot:"))
|
||||
.for_each(|rollupslot| {
|
||||
rollups.insert(rollupslot.to_owned(), HashMap::new());
|
||||
});
|
||||
.map(|rollupslot| (rollupslot.to_owned(), HashMap::new()))
|
||||
.collect();
|
||||
for cost_centre in cost_centres.deserialize() {
|
||||
let cost_centre: HashMap<String, String> = cost_centre?;
|
||||
let name = cost_centre.get("Code").unwrap();
|
||||
@@ -224,7 +238,11 @@ where
|
||||
.map(|header| header["limitto:".len()..].to_owned())
|
||||
.collect();
|
||||
let mut overhead_other_total: Vec<(String, String, f64)> = Vec::new();
|
||||
|
||||
// Save overhead ccs, so we later know whether a to cc is overhead or operating
|
||||
let mut overhead_ccs: Vec<String> = Vec::new();
|
||||
// overhead department -> total (summed limit to costs)
|
||||
let mut overhead_cc_totals: HashMap<String, f64> = HashMap::new();
|
||||
// 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
|
||||
@@ -240,16 +258,25 @@ where
|
||||
if current_area_ccs.is_none() {
|
||||
continue;
|
||||
}
|
||||
// Also skip if allocation statistic isn't found
|
||||
// let allocation_statistic_found = allocation_statistics
|
||||
// .iter()
|
||||
// .find(|stat| stat.name == *allocation_statistic);
|
||||
// if allocation_statistic_found.is_none() {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
let mut current_area_ccs = current_area_ccs.unwrap().clone();
|
||||
|
||||
//TODO: This isn't bad, however if it's too slow then consider performance improvements, such as summing totals now
|
||||
// rather than calculating in the next step.
|
||||
if department_type == DepartmentType::Overhead {
|
||||
overhead_ccs.append(&mut current_area_ccs);
|
||||
let overhead_ccs = area_ccs.get(area_name).unwrap();
|
||||
// TODO: This depends on the area limit criteria. For now just assuming any limit criteria
|
||||
// TODO: This depends on the area limit criteria. For now just doing any limit criteria
|
||||
let mut limited_ccs: Vec<String> = Vec::new();
|
||||
for limit_to in limit_tos.iter() {
|
||||
// TODO: It is technically possible to have more than one limit to 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
|
||||
let limit_value = area.get(&("LimitTo:".to_owned() + limit_to)).unwrap();
|
||||
if limit_value.is_empty() {
|
||||
continue;
|
||||
@@ -277,19 +304,34 @@ where
|
||||
let mut totals: Vec<(String, String, f64)> = overhead_ccs
|
||||
.iter()
|
||||
.flat_map(|overhead_cc| {
|
||||
limited_ccs
|
||||
let limited = limited_ccs
|
||||
.iter()
|
||||
.filter(|other_cc| {
|
||||
totals.contains_key(&(
|
||||
// TODO: This looks terrible
|
||||
other_cc.clone().clone(),
|
||||
allocation_statistic.clone(),
|
||||
))
|
||||
})
|
||||
.map(|other_cc| {
|
||||
(
|
||||
overhead_cc.clone(),
|
||||
other_cc.clone(),
|
||||
flat_department_costs
|
||||
totals
|
||||
.get(&(other_cc.clone(), allocation_statistic.clone()))
|
||||
.map(|f| *f)
|
||||
.unwrap_or(0.),
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.filter(|(_, _, value)| *value != 0.)
|
||||
.filter(|(from_cc, to_cc, _)| from_cc != to_cc)
|
||||
.collect_vec();
|
||||
// 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();
|
||||
overhead_other_total.append(&mut totals);
|
||||
@@ -307,12 +349,7 @@ where
|
||||
|(from_overhead_department, to_department, percent)| OverheadAllocationRule {
|
||||
from_overhead_department: from_overhead_department.clone(),
|
||||
to_department: to_department.clone(),
|
||||
percent: percent
|
||||
/ overhead_other_total
|
||||
.iter()
|
||||
.filter(|cc| cc.1 == *from_overhead_department)
|
||||
.map(|cc| cc.2)
|
||||
.sum::<f64>(),
|
||||
percent: percent / overhead_cc_totals.get(from_overhead_department).unwrap(),
|
||||
to_department_type: if overhead_ccs.contains(&to_department) {
|
||||
DepartmentType::Overhead
|
||||
} else {
|
||||
@@ -324,13 +361,20 @@ where
|
||||
|
||||
let mut initial_account_costs: HashMap<String, Vec<TotalDepartmentCost>> = HashMap::new();
|
||||
for line in lines {
|
||||
initial_account_costs
|
||||
.entry(line.account)
|
||||
.or_insert(Vec::new())
|
||||
.push(TotalDepartmentCost {
|
||||
department: line.department,
|
||||
value: line.value,
|
||||
});
|
||||
// Only include accounts we've already filtered on (i.e. by account type)
|
||||
if all_accounts_sorted
|
||||
.iter()
|
||||
.find(|account| **account == line.account)
|
||||
.is_some()
|
||||
{
|
||||
initial_account_costs
|
||||
.entry(line.account)
|
||||
.or_insert(Vec::new())
|
||||
.push(TotalDepartmentCost {
|
||||
department: line.department,
|
||||
value: line.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let results = reciprocal_allocation_impl(
|
||||
@@ -390,6 +434,8 @@ fn split_allocation_statistic_range(
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Removes quotes and padding from accounts int he allocation statistic account range.
|
||||
// e.g. "'100' " becomes "100"
|
||||
fn remove_quote_and_padding(s: &str) -> String {
|
||||
s.trim()[1..s.trim().len() - 1].to_owned()
|
||||
}
|
||||
@@ -646,7 +692,10 @@ mod tests {
|
||||
csv::Reader::from_path("area.csv").unwrap(),
|
||||
csv::Reader::from_path("costcentre.csv").unwrap(),
|
||||
&mut csv::Writer::from_path("output_alloc_stat.csv").unwrap(),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"E".to_owned(),
|
||||
);
|
||||
assert!(result.is_ok())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user