inventory.valuation-weighted-average@1.0.0
impl/rust.rs
4,400 bytes · the Rust implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::epoch_day_from_iso; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// Perpetual weighted average cost. The stock's value is held exactly and each
/// issue takes value x issued / on hand, rounded once, so the last issue takes
/// exactly what is left and an empty item is worth nothing.
///
/// # Panics
/// Panics on a ledger out of date order, a zero quantity, a receipt without a
/// unit cost or with a negative one, an issue with a unit cost, a cost in
/// another currency, an issue larger than the stock on hand, or an unknown mode.
pub fn weighted_average_valuation(movements: &[StockMovement], currency: &str, mode: &str) -> AverageCostValuation {
let zero = money(0, currency);
round_div(0, 1, mode); // an unknown mode fails even on a ledger with no issues
let mut quantity: i64 = 0;
let mut value: i64 = 0;
let mut cost_of_sales: i64 = 0;
let mut issue_costs = Vec::new();
let mut previous: Option<(i64, &str)> = None;
for line in movements {
let day = epoch_day_from_iso(&line.date);
if let Some((previous_day, previous_date)) = previous {
if day < previous_day {
panic!("movements must be in date order: {} comes after {}", line.date, previous_date);
}
}
previous = Some((day, &line.date));
if line.quantity == 0 {
panic!("quantity must be a non-zero whole number, received 0 on {}", line.date);
}
if line.quantity > 0 {
let unit_cost = match &line.unit_cost {
Some(c) => c,
None => panic!("a receipt needs a unitCost: {} on {}", line.quantity, line.date),
};
assert_same_currency(&zero, unit_cost);
if unit_cost.minor < 0 {
panic!("unitCost must not be negative, received {} on {}", unit_cost.minor, line.date);
}
quantity += line.quantity;
value += line.quantity * unit_cost.minor;
continue;
}
if line.unit_cost.is_some() {
panic!(
"an issue takes its cost from stock, so its unitCost must be null: {} on {}",
line.quantity, line.date
);
}
let issued = -line.quantity;
if issued > quantity {
panic!(
"insufficient stock: an issue of {} on {} exceeds the {} on hand",
issued, line.date, quantity
);
}
let cost = round_div(value * issued, quantity, mode);
issue_costs.push(money(cost, currency));
cost_of_sales += cost;
value -= cost;
quantity -= issued;
}
AverageCostValuation {
closing_quantity: quantity,
closing_value: money(value, currency),
cost_of_sales: money(cost_of_sales, currency),
issue_costs,
average_unit_cost: money(if quantity == 0 { 0 } else { round_div(value, quantity, mode) }, currency),
}
}
pub fn stock_movement_from_value(v: &Value) -> StockMovement {
let cost = v.get("unitCost");
StockMovement {
date: v.get("date").as_str().to_string(),
quantity: v.get("quantity").as_i64(),
unit_cost: if cost.is_null() { None } else { Some(money_from_value(cost)) },
}
}
pub fn average_cost_valuation_to_value(result: &AverageCostValuation) -> Value {
Value::obj(vec![
("closingQuantity", Value::Int(result.closing_quantity)),
("closingValue", money_to_value(&result.closing_value)),
("costOfSales", money_to_value(&result.cost_of_sales)),
("issueCosts", Value::Arr(result.issue_costs.iter().map(money_to_value).collect())),
("averageUnitCost", money_to_value(&result.average_unit_cost)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let movements: Vec<StockMovement> = args[0]
.as_arr()
.iter()
.map(|v| {
if let Value::Float(f) = v.get("quantity") {
panic!("quantity must be a non-zero whole number, received {} on {}", f, v.get("date").as_str());
}
stock_movement_from_value(v)
})
.collect();
average_cost_valuation_to_value(&weighted_average_valuation(&movements, args[1].as_str(), args[2].as_str()))
}