dodil/erp-brick-pos
DODIL ERP brick: point of sale
src/brick_pos/domain/money.py
"""Money is an exact Decimal carried to the currency's minor unit (never more than two places: NUMERIC(18,2)); a unit price
is NUMERIC(19,4). Never a float, never summed across currencies."""
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from typing import Any
from erp_platform.money import minor_unit
from brick_pos.domain.errors import OutOfBounds
LIMIT = Decimal("1e16") # NUMERIC(18,2): sixteen digits before the point
ZERO = Decimal("0.00")
def places(currency: str) -> int:
"""Decimals this brick stores for a currency: the minor unit, at most two (the column's scale)."""
return min(minor_unit(currency), 2)
def quantum(currency: str) -> Decimal:
return Decimal(1).scaleb(-places(currency))
def round_half_up(value: Decimal, currency: str) -> Decimal:
return value.quantize(quantum(currency), rounding=ROUND_HALF_UP)
def amount(value: Any, currency: str, what: str, *, positive: bool = False) -> Decimal:
"""An amount the caller sent, checked against what the books can hold. `positive` means more than zero; otherwise zero
or more. Floats and booleans are refused (a float is already a rounded guess); more places than the currency has are
refused, never rounded away."""
if isinstance(value, (bool, float)) or value is None or value == "":
raise OutOfBounds(f"{what} is an exact amount: send it as a string such as \"10.00\"")
try:
d = value if isinstance(value, Decimal) else Decimal(str(value).strip())
except (InvalidOperation, ValueError):
raise OutOfBounds(f"{what} {str(value)[:40]!r} is not an amount") from None
if not d.is_finite():
raise OutOfBounds(f"{what} is a finite number")
if d.as_tuple().exponent < -places(currency):
raise OutOfBounds(f"{what} has more decimal places than {currency} has ({places(currency)})")
if abs(d) >= LIMIT:
raise OutOfBounds(f"{what} is below 10,000,000,000,000,000")
if d < 0 or (positive and d == 0):
raise OutOfBounds(f"{what} is {'more than zero' if positive else 'zero or more'}")
return d.quantize(quantum(currency))
def quantity(value: Any, what: str = "quantity") -> Decimal:
"""A quantity sold: more than zero, at most three decimals, below a million."""
if isinstance(value, (bool, float)) or value is None or value == "":
raise OutOfBounds(f"{what} is an exact number: send it as a string such as \"2\"")
try:
d = value if isinstance(value, Decimal) else Decimal(str(value).strip())
except (InvalidOperation, ValueError):
raise OutOfBounds(f"{what} {str(value)[:40]!r} is not a number") from None
if not d.is_finite() or d.as_tuple().exponent < -3 or not (Decimal(0) < d < Decimal("1000000")):
raise OutOfBounds(f"{what} is more than zero, below 1,000,000, with at most three decimals")
return Decimal(format(d.normalize(), "f"))