food-market/app/money.py
Claude c8b9f5048b Учёт закупки и перепродажи булок
Десктопное приложение на PySide6. Вся база — один JSON-документ,
зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит
в data/vault.fmdb и раз в час уезжает в этот репозиторий.

Основное:
- партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж
  по партиям и прогресс покрытия к сроку;
- типы выбытия: розница, другу по себестоимости, съел сам, подарок,
  списание — съеденное вычитается из прибыли, за него платить всё равно;
- долги контрагентов с частичными оплатами;
- номенклатура с историей изменения цен;
- журнал изменений внутри базы: git хранит непрозрачные снимки,
  поэтому настоящая история ведётся здесь.

Синхронизация коммитит только путь базы и схлопывает часовые пуши
в один коммит на день через amend + force-with-lease.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:29:53 +03:00

69 lines
2.7 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Деньги и количества.
Всё считаем в 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(".", ",")