69 lines
2.7 KiB
Python
69 lines
2.7 KiB
Python
|
|
"""Деньги и количества.
|
|||
|
|
|
|||
|
|
Всё считаем в Decimal, а не в float: 0.1 + 0.2 в float даёт 0.30000000000004,
|
|||
|
|
и такие копейки накапливаются в отчётах по партиям. В JSON кладём строкой,
|
|||
|
|
чтобы точность не терялась при сохранении.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|||
|
|
|
|||
|
|
CENTS = Decimal("0.01")
|
|||
|
|
QTY_STEP = Decimal("0.001")
|
|||
|
|
ZERO = Decimal("0")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def money(value) -> Decimal:
|
|||
|
|
"""Привести что угодно к сумме денег, округлив до копеек."""
|
|||
|
|
return _to_decimal(value).quantize(CENTS, rounding=ROUND_HALF_UP)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def qty(value) -> Decimal:
|
|||
|
|
"""Привести что угодно к количеству.
|
|||
|
|
|
|||
|
|
До тысячных — булки штучные, но так же работают вес и объём.
|
|||
|
|
"""
|
|||
|
|
return _to_decimal(value).quantize(QTY_STEP, rounding=ROUND_HALF_UP)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _to_decimal(value) -> Decimal:
|
|||
|
|
if isinstance(value, Decimal):
|
|||
|
|
return value
|
|||
|
|
if value is None or value == "":
|
|||
|
|
return ZERO
|
|||
|
|
if isinstance(value, float):
|
|||
|
|
# Через str, иначе Decimal(0.1) даст 0.1000000000000000055511151231257827.
|
|||
|
|
value = repr(value)
|
|||
|
|
try:
|
|||
|
|
return Decimal(str(value).replace(",", ".").replace(" ", "").replace(" ", ""))
|
|||
|
|
except (InvalidOperation, ValueError) as exc:
|
|||
|
|
raise ValueError(f"Не похоже на число: {value!r}") from exc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def dumps(value: Decimal) -> str:
|
|||
|
|
"""Сериализовать сумму для JSON."""
|
|||
|
|
return format(value, "f")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fmt_money(value, currency: str = "₽") -> str:
|
|||
|
|
"""Отформатировать сумму для показа: 12 345,67 ₽."""
|
|||
|
|
value = money(value)
|
|||
|
|
sign = "-" if value < 0 else ""
|
|||
|
|
whole, _, frac = format(abs(value), "f").partition(".")
|
|||
|
|
groups = []
|
|||
|
|
while len(whole) > 3:
|
|||
|
|
groups.insert(0, whole[-3:])
|
|||
|
|
whole = whole[:-3]
|
|||
|
|
groups.insert(0, whole)
|
|||
|
|
text = f"{sign}{' '.join(groups)},{frac}"
|
|||
|
|
# Разделители — обычные пробелы, а не неразрывные: то, что видно на экране,
|
|||
|
|
# должно находиться поиском по журналу ровно так, как пользователь это набрал.
|
|||
|
|
return f"{text} {currency}" if currency else text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fmt_qty(value) -> str:
|
|||
|
|
"""Отформатировать количество, пряча незначащие нули: 5, 2,5."""
|
|||
|
|
text = format(qty(value), "f").rstrip("0").rstrip(".")
|
|||
|
|
return (text or "0").replace(".", ",")
|