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"))