dodil/erp-brick-manufacturing

DODIL ERP brick: manufacturing

src/brick_manufacturing/domain/values.py

"""Value objects and the bounds every quantity and amount keeps (module.yaml invariants: bounded -> 422, never 500)."""
from decimal import ROUND_HALF_UP, Decimal
from enum import Enum

from brick_manufacturing.domain.errors import OutOfBounds

Q4 = Decimal("0.0001")                       # NUMERIC(19,4): quantities, rates, unit costs
Q2 = Decimal("0.01")                         # NUMERIC(18,2): money
MAX_QTY = Decimal("999999999999999.9999")    # 15 digits before the point, as NUMERIC(19,4) holds
MAX_MONEY = Decimal("9999999999999999.99")   # NUMERIC(18,2)
ZERO = Decimal("0")


class BomUsage(str, Enum):
    PRODUCTION = "production"
    SALE = "sale"


class BomState(str, Enum):
    DRAFT = "draft"
    RELEASED = "released"
    OBSOLETE = "obsolete"


class LineKind(str, Enum):
    COMPONENT = "component"
    BY_PRODUCT = "by_product"
    MODIFIER = "modifier"


class OrderState(str, Enum):
    CREATED = "created"
    RELEASED = "released"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    CLOSED = "closed"
    CANCELLED = "cancelled"


class OutboxState(str, Enum):
    PENDING = "pending"          # committed here, not yet taken by the neighbour (retried)
    SENT = "sent"                # the neighbour took it: final
    REJECTED = "rejected"        # the neighbour refused it (a 4xx): final, the movement it carried is void


def q4(v: Decimal) -> Decimal:
    return v.quantize(Q4, rounding=ROUND_HALF_UP)


def q2(v: Decimal) -> Decimal:
    return v.quantize(Q2, rounding=ROUND_HALF_UP)


def quantity(v, what: str = "a quantity") -> Decimal:
    """A positive exact quantity of at most four places that fits NUMERIC(19,4)."""
    if v is None or isinstance(v, (bool, float)):
        raise OutOfBounds(f"{what} is an exact number: give it as a string such as \"100.000\"")
    try:
        d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
    except Exception:
        raise OutOfBounds(f"{v!r} is not {what}") from None
    if not d.is_finite() or d.as_tuple().exponent < -4:
        raise OutOfBounds(f"{what} is a finite number with at most four decimal places")
    if not ZERO < d <= MAX_QTY:
        raise OutOfBounds(f"{what} is more than zero and below 1,000,000,000,000,000")
    return d


def non_negative(v, what: str) -> Decimal:
    """A zero-or-more rate or unit cost (NUMERIC 19,4)."""
    try:
        d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
    except Exception:
        raise OutOfBounds(f"{v!r} is not {what}") from None
    if not d.is_finite() or d.as_tuple().exponent < -4 or not ZERO <= d <= MAX_QTY:
        raise OutOfBounds(f"{what} is zero or more, with at most four decimal places")
    return d


def money(v: Decimal, what: str) -> Decimal:
    """A computed amount must fit NUMERIC(18,2) either way."""
    if abs(v) > MAX_MONEY:
        raise OutOfBounds(f"{what} {v} is above what a ledger amount can hold")
    return q2(v)