Functional Weave
Code in Python

inventory.valuation-weighted-average@1.0.0

impl/python.py

2,963 bytes · the Python 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.

from typing import List, Optional, Sequence

from .dates_add_days import epoch_day_from_iso  ← from dates.add-days ^1.0.0 · built alongside by fune
from .inventory_valuation_weighted_average_types import AverageCostValuation, StockMovement
from .math_round_div import RoundingMode, round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune


def weighted_average_valuation(
    movements: Sequence[StockMovement], currency: str, mode: RoundingMode
) -> AverageCostValuation:
    """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.
    """
    zero = money(0, currency)
    round_div(0, 1, mode)  # an unknown mode fails even on a ledger with no issues
    quantity = 0
    value = 0
    cost_of_sales = 0
    issue_costs: List[Money] = []
    previous_day: Optional[int] = None
    previous_date = ""
    for line in movements:
        day = epoch_day_from_iso(line.date)
        if previous_day is not None and day < previous_day:
            raise ValueError("movements must be in date order: %s comes after %s" % (line.date, previous_date))
        previous_day = day
        previous_date = line.date
        if isinstance(line.quantity, bool) or not isinstance(line.quantity, int) or line.quantity == 0:
            raise ValueError(
                "quantity must be a non-zero whole number, received %r on %s" % (line.quantity, line.date)
            )
        if line.quantity > 0:
            if line.unit_cost is None:
                raise ValueError("a receipt needs a unitCost: %d on %s" % (line.quantity, line.date))
            assert_same_currency(zero, line.unit_cost)
            if line.unit_cost.minor < 0:
                raise ValueError(
                    "unitCost must not be negative, received %d on %s" % (line.unit_cost.minor, line.date)
                )
            quantity += line.quantity
            value += line.quantity * line.unit_cost.minor
            continue
        if line.unit_cost is not None:
            raise ValueError(
                "an issue takes its cost from stock, so its unitCost must be null: %d on %s"
                % (line.quantity, line.date)
            )
        issued = -line.quantity
        if issued > quantity:
            raise ValueError(
                "insufficient stock: an issue of %d on %s exceeds the %d on hand" % (issued, line.date, quantity)
            )
        cost = round_div(value * issued, quantity, mode)
        issue_costs.append(money(cost, currency))
        cost_of_sales += cost
        value -= cost
        quantity -= issued
    return AverageCostValuation(
        closing_quantity=quantity,
        closing_value=money(value, currency),
        cost_of_sales=money(cost_of_sales, currency),
        issue_costs=issue_costs,
        average_unit_cost=money(0 if quantity == 0 else round_div(value, quantity, mode), currency),
    )