dodil/erp-brick-inventory

DODIL ERP brick: inventory

src/brick_inventory/domain/measures.py

"""Weights and volumes of a document, summed exactly through the units' conversions (no float anywhere)."""
from decimal import Decimal
from fractions import Fraction
from typing import Optional

from brick_inventory.domain.errors import RuleBroken
from brick_inventory.domain.values import fraction, round_half_up

# size in the reference unit of each dimension (KGM; MTQ). unit_conversion (GRM, LBR, ...) is exact.
WEIGHT = {"KGM": Fraction(1), "GRM": Fraction(1, 1000), "TNE": Fraction(1000), "LBR": Fraction(45359237, 100000000),
          "ONZ": Fraction(28349523125, 1000000000000)}
VOLUME = {"MTQ": Fraction(1), "LTR": Fraction(1, 1000), "MLT": Fraction(1, 1000000), "CMQ": Fraction(1, 1000000)}


def to_kgm(value: Decimal, unit_code: str) -> Fraction:
    if unit_code not in WEIGHT:
        raise RuleBroken(f"cannot convert a weight in {unit_code}: use one of {', '.join(WEIGHT)}")
    return fraction(value) * WEIGHT[unit_code]


def to_mtq(value: Decimal, unit_code: str) -> Fraction:
    if unit_code not in VOLUME:
        raise RuleBroken(f"cannot convert a volume in {unit_code}: use one of {', '.join(VOLUME)}")
    return fraction(value) * VOLUME[unit_code]


def shown(value: Fraction, places: int = 3) -> str:
    d = round_half_up(value, places)
    s = format(d, "f")
    return s.rstrip("0").rstrip(".") if "." in s else s


def shipping(lines: list) -> dict:
    """lines: [{quantity_done, gross_weight, net_weight, weight_unit_code, volume, volume_unit_code}] per one of the line's unit.
    A line with an unknown measure is named, never counted as 0."""
    gross = net = vol = Fraction(0)
    unknown_w, unknown_v = [], []
    for i, l in enumerate(lines, start=1):
        qty = fraction(l["quantity_done"] if l["quantity_done"] is not None else 0)
        if l.get("gross_weight") is None and l.get("net_weight") is None:
            unknown_w.append(l.get("label") or i)
        else:
            if l.get("gross_weight") is not None:
                gross += qty * to_kgm(l["gross_weight"], l["weight_unit_code"] or "KGM")
            if l.get("net_weight") is not None:
                net += qty * to_kgm(l["net_weight"], l["weight_unit_code"] or "KGM")
        if l.get("volume") is None:
            unknown_v.append(l.get("label") or i)
        else:
            vol += qty * to_mtq(l["volume"], l["volume_unit_code"] or "MTQ")
    n = len(lines)
    return {"gross_weight": shown(gross), "net_weight": shown(net), "weight_unit_code": "KGM", "volume": shown(vol),
            "volume_unit_code": "MTQ",
            "weight_unknown": f"weight unknown for {len(unknown_w)} of {n} lines" if unknown_w else None,
            "volume_unknown": f"volume unknown for {len(unknown_v)} of {n} lines" if unknown_v else None,
            "unknown_weight_lines": unknown_w, "unknown_volume_lines": unknown_v}


def kgm_to_unit(value: Fraction, unit_code: str) -> Optional[Fraction]:
    return value / WEIGHT[unit_code] if unit_code in WEIGHT else None