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)