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::money_amount::{assert_same_currency, money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// Value stock first in, first out, and cost each issue from the oldest layers.
///
/// Every cost is a whole quantity times a unit cost, so nothing is rounded and
/// closing value plus cost of sales always equals what was received.
///
/// # 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, or an issue larger than the stock on hand.
pub fn fifo_valuation(movements: &[StockMovement], currency: &str) -> FifoValuation {
let zero = money(0, currency);
let mut layers: Vec<StockLayer> = Vec::new();
let mut issue_costs = Vec::new();
let mut cost_of_sales: i64 = 0;
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);
}
layers.push(StockLayer {
date: line.date.clone(),
quantity: line.quantity,
unit_cost: unit_cost.clone(),
});
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 mut wanted = -line.quantity;
let on_hand: i64 = layers.iter().map(|l| l.quantity).sum();
if wanted > on_hand {
panic!(
"insufficient stock: an issue of {} on {} exceeds the {} on hand",
wanted, line.date, on_hand
);
}
let mut cost: i64 = 0;
while wanted > 0 {
let oldest = &mut layers[0];
let used = wanted.min(oldest.quantity);
cost += used * oldest.unit_cost.minor;
oldest.quantity -= used;
wanted -= used;
if oldest.quantity == 0 {
layers.remove(0);
}
}
issue_costs.push(money(cost, currency));
cost_of_sales += cost;
}
let closing_quantity: i64 = layers.iter().map(|l| l.quantity).sum();
let closing_value: i64 = layers.iter().map(|l| l.quantity * l.unit_cost.minor).sum();
FifoValuation {
closing_quantity,
closing_value: money(closing_value, currency),
cost_of_sales: money(cost_of_sales, currency),
issue_costs,
layers,
}
}
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 fifo_valuation_to_value(result: &FifoValuation) -> 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())),
(
"layers",
Value::Arr(
result
.layers
.iter()
.map(|l| {
Value::obj(vec![
("date", Value::str(&l.date)),
("quantity", Value::Int(l.quantity)),
("unitCost", money_to_value(&l.unit_cost)),
])
})
.collect(),
),
),
])
}
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();
fifo_valuation_to_value(&fifo_valuation(&movements, args[1].as_str()))
}