use super::funejson::Value; use super::manufacturing_bom_explode::{bom_line_from_value, explode_bom, BomLine}; use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer, Rational}; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value}; /// Standard cost of one unit, by cost element, rolled up through the BOM. /// Every element is an exact fraction of a minor unit until it is rounded once. /// /// # Panics /// Panics on anything `explode_bom` refuses, a missing, duplicate or negative /// material cost, a cost for a made item, negative operation time or rates, /// or a cost or rate in another currency. pub fn bom_cost( lines: &[BomLine], item: &str, material_costs: &[ItemCost], operations: &[RoutingOperation], currency: &str, mode: &str, ) -> CostRollup { let zero = money(0, currency); let mut needs: Vec<(String, Rational, bool)> = vec![(item.to_string(), rational(1, 1), false)]; for r in explode_bom(lines, item, 1) { needs.push((r.item, r.quantity, r.leaf)); } let mut costs: Vec<(&str, i64)> = Vec::new(); for c in material_costs { assert_same_currency(&zero, &c.unit_cost); if costs.iter().any(|(n, _)| *n == c.item) { panic!("duplicate material cost for \"{}\"", c.item); } if c.unit_cost.minor < 0 { panic!("unitCost of \"{}\" must not be negative, received {}", c.item, c.unit_cost.minor); } costs.push((&c.item, c.unit_cost.minor)); } let mut material = rational(0, 1); for (name, quantity, leaf) in &needs { let cost = costs.iter().find(|(n, _)| n == name).map(|(_, c)| *c); if !leaf { if cost.is_some() { panic!( "\"{}\" is made from its own bill of materials, so its cost is rolled up; remove it from materialCosts", name ); } continue; } let cost = match cost { Some(c) => c, None => panic!("no material cost for \"{}\"", name), }; material = add_rational(&material, &multiply_rational(quantity, &rational(cost, 1))); } let mut labour = rational(0, 1); let mut overhead = rational(0, 1); for op in operations { if op.seconds < 0 || op.seconds > (1i64 << 53) - 1 { panic!( "seconds of an operation on \"{}\" must be a whole number, not negative, received {}", op.item, op.seconds ); } assert_same_currency(&zero, &op.labour_rate); assert_same_currency(&zero, &op.overhead_rate); if op.labour_rate.minor < 0 || op.overhead_rate.minor < 0 { panic!("the rates of an operation on \"{}\" must not be negative", op.item); } let need = match needs.iter().find(|(n, _, _)| *n == op.item) { Some((_, q, _)) => *q, None => continue, }; let hours = multiply_rational(&need, &rational(op.seconds, 3600)); labour = add_rational(&labour, &multiply_rational(&hours, &rational(op.labour_rate.minor, 1))); overhead = add_rational(&overhead, &multiply_rational(&hours, &rational(op.overhead_rate.minor, 1))); } let m = rational_to_integer(&material, mode); let l = rational_to_integer(&labour, mode); let o = rational_to_integer(&overhead, mode); CostRollup { material: money(m, currency), labour: money(l, currency), overhead: money(o, currency), total: money(m + l + o, currency), } } pub fn cost_rollup_to_value(c: &CostRollup) -> Value { Value::obj(vec![ ("material", money_to_value(&c.material)), ("labour", money_to_value(&c.labour)), ("overhead", money_to_value(&c.overhead)), ("total", money_to_value(&c.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let lines: Vec = args[0].as_arr().iter().map(bom_line_from_value).collect(); let costs: Vec = args[2] .as_arr() .iter() .map(|v| ItemCost { item: v.get("item").as_str().to_string(), unit_cost: money_from_value(v.get("unitCost")), }) .collect(); let operations: Vec = args[3] .as_arr() .iter() .map(|v| { if let Value::Float(f) = v.get("seconds") { panic!( "seconds of an operation on \"{}\" must be a whole number, not negative, received {}", v.get("item").as_str(), f ); } RoutingOperation { item: v.get("item").as_str().to_string(), seconds: v.get("seconds").as_i64(), labour_rate: money_from_value(v.get("labourRate")), overhead_rate: money_from_value(v.get("overheadRate")), } }) .collect(); cost_rollup_to_value(&bom_cost(&lines, args[1].as_str(), &costs, &operations, args[4].as_str(), args[5].as_str())) }