Add progress on overhead allocation conversion from ppm format
This commit is contained in:
@@ -211,7 +211,6 @@ where
|
|||||||
|
|
||||||
// Then run move_money
|
// Then run move_money
|
||||||
let moved = move_money_2(lines, &rules);
|
let moved = move_money_2(lines, &rules);
|
||||||
let mut output = output;
|
|
||||||
|
|
||||||
// Ouput the list moved moneys
|
// Ouput the list moved moneys
|
||||||
for money in moved {
|
for money in moved {
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ pub enum DepartmentType {
|
|||||||
Overhead,
|
Overhead,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DepartmentType {
|
||||||
|
pub fn from(s: &str) -> DepartmentType {
|
||||||
|
if s == "P" {
|
||||||
|
DepartmentType::Operating
|
||||||
|
} else {
|
||||||
|
DepartmentType::Overhead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CsvAllocationStatistic {
|
pub struct CsvAllocationStatistic {
|
||||||
#[serde(rename = "Name")]
|
#[serde(rename = "Name")]
|
||||||
@@ -92,7 +102,7 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
|||||||
allocation_statistics: csv::Reader<AllocationStatistic>,
|
allocation_statistics: csv::Reader<AllocationStatistic>,
|
||||||
areas: csv::Reader<Area>,
|
areas: csv::Reader<Area>,
|
||||||
cost_centres: csv::Reader<CostCentre>,
|
cost_centres: csv::Reader<CostCentre>,
|
||||||
output: csv::Writer<Output>,
|
output: &mut csv::Writer<Output>,
|
||||||
use_numeric_accounts: bool,
|
use_numeric_accounts: bool,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
@@ -108,18 +118,21 @@ where
|
|||||||
.deserialize()
|
.deserialize()
|
||||||
.collect::<Result<Vec<CsvCost>, csv::Error>>()?;
|
.collect::<Result<Vec<CsvCost>, csv::Error>>()?;
|
||||||
|
|
||||||
|
let mut accounts = accounts;
|
||||||
|
|
||||||
|
// TODO: Accounts need to come from actual account fiile
|
||||||
let all_accounts_sorted: Vec<String> = if use_numeric_accounts {
|
let all_accounts_sorted: Vec<String> = if use_numeric_accounts {
|
||||||
lines
|
accounts
|
||||||
.iter()
|
.deserialize::<CsvAccount>()
|
||||||
.map(|line| line.account.clone().parse::<i32>().unwrap())
|
.map(|line| line.unwrap().code.clone().parse::<i32>().unwrap())
|
||||||
.unique()
|
.unique()
|
||||||
.sorted()
|
.sorted()
|
||||||
.map(|account| account.to_string())
|
.map(|account| account.to_string())
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
lines
|
accounts
|
||||||
.iter()
|
.deserialize::<CsvAccount>()
|
||||||
.map(|line| line.account.clone())
|
.map(|line| line.unwrap().code.clone())
|
||||||
.unique()
|
.unique()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect()
|
.collect()
|
||||||
@@ -131,7 +144,9 @@ where
|
|||||||
.collect::<Result<Vec<CsvAllocationStatistic>, csv::Error>>()?;
|
.collect::<Result<Vec<CsvAllocationStatistic>, csv::Error>>()?;
|
||||||
|
|
||||||
// For each allocation statistic, sum the cost centres across accounts in the allocaiton statistic range
|
// 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
|
// value is (cc, allocation_statistic, total)
|
||||||
|
// TODO: This is super slow
|
||||||
|
let flat_department_costs: HashMap<(String, String), f64> = allocation_statistics
|
||||||
.iter()
|
.iter()
|
||||||
.map(|allocation_statistic| {
|
.map(|allocation_statistic| {
|
||||||
(
|
(
|
||||||
@@ -163,52 +178,181 @@ where
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
(
|
(
|
||||||
entry.0.clone(),
|
(entry.0.clone(), allocation_statistic.0.name.clone()),
|
||||||
allocation_statistic.0.name.clone(),
|
|
||||||
*entry.1,
|
*entry.1,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<(String, String, f64)>>()
|
.collect::<Vec<((String, String), f64)>>()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// TODO: If ignore negative is used, then set values < 0 to 0
|
// TODO: If ignore negative is used, then set values < 0 to 0
|
||||||
|
|
||||||
let mut rollups: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
|
let mut rollups: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
|
||||||
|
let mut area_ccs: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
let mut cost_centres = cost_centres;
|
let mut cost_centres = cost_centres;
|
||||||
for cost_centre in cost_centres.records() {
|
let headers = cost_centres.headers()?;
|
||||||
let cost_centre = cost_centre?;
|
headers
|
||||||
// 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
|
.iter()
|
||||||
// I think map of rollup -> cc would be better, although this would need to be for each rollup slot... so a map of maps?
|
.filter(|name| name.to_lowercase().starts_with("rollupslot:"))
|
||||||
|
.for_each(|rollupslot| {
|
||||||
|
rollups.insert(rollupslot.to_owned(), HashMap::new());
|
||||||
|
});
|
||||||
|
for cost_centre in cost_centres.deserialize() {
|
||||||
|
let cost_centre: HashMap<String, String> = cost_centre?;
|
||||||
|
let name = cost_centre.get("Code").unwrap();
|
||||||
|
let area = cost_centre.get("Area").unwrap();
|
||||||
|
area_ccs
|
||||||
|
.entry(area.clone())
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.push(name.clone());
|
||||||
|
|
||||||
|
for rollupslot in rollups.iter_mut() {
|
||||||
|
let rollup_name = cost_centre.get(rollupslot.0).unwrap();
|
||||||
|
rollupslot
|
||||||
|
.1
|
||||||
|
.entry(rollup_name.clone())
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.push(name.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut areas = areas;
|
let mut areas = areas;
|
||||||
let area_name_index = areas
|
let headers = areas.headers()?;
|
||||||
.headers()?
|
let limit_tos: Vec<String> = headers
|
||||||
.iter()
|
.iter()
|
||||||
.position(|header| header == "Name")
|
.filter(|header| header.to_lowercase().starts_with("limitto:"))
|
||||||
.unwrap();
|
.map(|header| header["limitto:".len()..].to_owned())
|
||||||
let allocation_statistic_index = areas
|
.collect();
|
||||||
.headers()?
|
let mut overhead_other_total: Vec<(String, String, f64)> = Vec::new();
|
||||||
.iter()
|
let mut overhead_ccs: Vec<String> = Vec::new();
|
||||||
.position(|header| header == "AllocationStatistic")
|
|
||||||
.unwrap();
|
|
||||||
// For each overhead area, get the cost centres in the area, and get all cost centres
|
// 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).
|
// 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
|
// 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
|
// allocation statistic matches the allocation statistic for this area
|
||||||
for area in areas.records() {
|
for area in areas.deserialize() {
|
||||||
let area = area?;
|
let area: HashMap<String, String> = area?;
|
||||||
// Check for limitTos, should probably somehow build out the list of allocation rules from this point.
|
// 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 area_name = area.get("Name").unwrap();
|
||||||
let allocation_statistic = area.get(allocation_statistic_index).unwrap();
|
let allocation_statistic = area.get("AllocationStatistic").unwrap();
|
||||||
|
let department_type: DepartmentType = DepartmentType::from(area.get("Type").unwrap());
|
||||||
|
let current_area_ccs = area_ccs.get(area_name);
|
||||||
|
|
||||||
|
if current_area_ccs.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_area_ccs = current_area_ccs.unwrap().clone();
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
let limit_value = area.get(&("LimitTo:".to_owned() + limit_to)).unwrap();
|
||||||
|
if limit_value.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if limit_to.eq_ignore_ascii_case("costcentre") {
|
||||||
|
limited_ccs.push(limit_value.clone());
|
||||||
|
} else {
|
||||||
|
let mut found_ccs = rollups
|
||||||
|
.get(&("RollupSlot:".to_owned() + limit_to))
|
||||||
|
.map(|rollups| rollups.get(limit_value))
|
||||||
|
.flatten()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
limited_ccs.append(&mut found_ccs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if limited_ccs.is_empty() {
|
||||||
|
let mut other_ccs: Vec<String> = area_ccs
|
||||||
|
.values()
|
||||||
|
.flat_map(|ccs| ccs.iter().map(|cc| cc.clone()))
|
||||||
|
.collect();
|
||||||
|
// No limit criteria, use all ccs
|
||||||
|
limited_ccs.append(&mut other_ccs);
|
||||||
|
}
|
||||||
|
let mut totals: Vec<(String, String, f64)> = overhead_ccs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|overhead_cc| {
|
||||||
|
limited_ccs
|
||||||
|
.iter()
|
||||||
|
.map(|other_cc| {
|
||||||
|
(
|
||||||
|
overhead_cc.clone(),
|
||||||
|
other_cc.clone(),
|
||||||
|
flat_department_costs
|
||||||
|
.get(&(other_cc.clone(), allocation_statistic.clone()))
|
||||||
|
.map(|f| *f)
|
||||||
|
.unwrap_or(0.),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|(_, _, value)| *value != 0.)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
overhead_other_total.append(&mut totals);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// divide the other cc by this summed amount (thus getting the relative cost)
|
||||||
|
|
||||||
// do reciprocal allocation (only for variable portion of accounts), for each account
|
// 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
|
||||||
|
|
||||||
// Copy across fixed stuff (if necessary, not sure it is)... don't think it's necessary, initial totals handle this
|
let allocation_rules: Vec<OverheadAllocationRule> = overhead_other_total
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|(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>(),
|
||||||
|
to_department_type: if overhead_ccs.contains(&to_department) {
|
||||||
|
DepartmentType::Overhead
|
||||||
|
} else {
|
||||||
|
DepartmentType::Operating
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = reciprocal_allocation_impl(
|
||||||
|
allocation_rules,
|
||||||
|
initial_account_costs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(account, total_cost)| AccountCost {
|
||||||
|
account: account,
|
||||||
|
summed_department_costs: total_cost,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for cost in results {
|
||||||
|
for department in cost.summed_department_costs {
|
||||||
|
output.serialize(CsvCost {
|
||||||
|
account: cost.account.clone(),
|
||||||
|
department: department.department,
|
||||||
|
value: department.value,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,9 +365,10 @@ fn split_allocation_statistic_range(
|
|||||||
split
|
split
|
||||||
.map(|split| {
|
.map(|split| {
|
||||||
let range_split = split.split('-').collect::<Vec<_>>();
|
let range_split = split.split('-').collect::<Vec<_>>();
|
||||||
|
let start = remove_quote_and_padding(range_split[0]);
|
||||||
let start_index = accounts_sorted
|
let start_index = accounts_sorted
|
||||||
.iter()
|
.iter()
|
||||||
.position(|account| *account == range_split[0].to_owned())
|
.position(|account| *account == start)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if range_split.len() == 1 {
|
if range_split.len() == 1 {
|
||||||
AllocationStatisticAccountRange {
|
AllocationStatisticAccountRange {
|
||||||
@@ -231,9 +376,10 @@ fn split_allocation_statistic_range(
|
|||||||
end: start_index,
|
end: start_index,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let end = remove_quote_and_padding(range_split[1]);
|
||||||
let end_index = accounts_sorted
|
let end_index = accounts_sorted
|
||||||
.iter()
|
.iter()
|
||||||
.position(|account| *account == range_split[1].to_owned())
|
.position(|account| *account == end)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
AllocationStatisticAccountRange {
|
AllocationStatisticAccountRange {
|
||||||
start: start_index,
|
start: start_index,
|
||||||
@@ -244,6 +390,10 @@ fn split_allocation_statistic_range(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_quote_and_padding(s: &str) -> String {
|
||||||
|
s.trim()[1..s.trim().len() - 1].to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
|
// 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
|
// to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when
|
||||||
// matrix is singular
|
// matrix is singular
|
||||||
@@ -267,7 +417,6 @@ fn reciprocal_allocation_impl(
|
|||||||
let to_index = overhead_department_mappings
|
let to_index = overhead_department_mappings
|
||||||
.get(&allocation.to_department)
|
.get(&allocation.to_department)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// TODO: Check if dmatrix is row or column major order, would need to flip if column major
|
|
||||||
slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
|
slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
|
||||||
allocation.percent * -1.;
|
allocation.percent * -1.;
|
||||||
}
|
}
|
||||||
@@ -401,6 +550,7 @@ fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::reciprocal_allocation;
|
||||||
use crate::AccountCost;
|
use crate::AccountCost;
|
||||||
use crate::DepartmentType;
|
use crate::DepartmentType;
|
||||||
use crate::OverheadAllocationRule;
|
use crate::OverheadAllocationRule;
|
||||||
@@ -486,4 +636,18 @@ mod tests {
|
|||||||
let result = reciprocal_allocation_impl(allocation_rules, initial_totals).unwrap();
|
let result = reciprocal_allocation_impl(allocation_rules, initial_totals).unwrap();
|
||||||
assert_eq!(expected_final_allocations, result);
|
assert_eq!(expected_final_allocations, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_real() {
|
||||||
|
let result = reciprocal_allocation(
|
||||||
|
csv::Reader::from_path("output.csv").unwrap(),
|
||||||
|
csv::Reader::from_path("account.csv").unwrap(),
|
||||||
|
csv::Reader::from_path("allocstat.csv").unwrap(),
|
||||||
|
csv::Reader::from_path("area.csv").unwrap(),
|
||||||
|
csv::Reader::from_path("costcentre.csv").unwrap(),
|
||||||
|
&mut csv::Writer::from_path("output_alloc_stat.csv").unwrap(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user