food-market/README.md
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

115 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

# Булочная
Десктопный учёт закупки и перепродажи булок. Вся база — один файл,
зашифрованный паролем, который раз в час уезжает в этот репозиторий.
## Что считает
Булки берутся в пекарне **в долг**: к дедлайну надо вернуть себестоимость
партии, иначе новых не напекут. Наценка остаётся тебе. Часть булок уходит
друзьям по себестоимости, часть съедается — и за них пекарне тоже платить.
Программа отвечает на главный вопрос: **успеваю ли я собрать себестоимость
конкретной партии до её срока**.
- **Закупки** — партии с составом, ценами и датой, до которой надо рассчитаться.
- **Продажи** — розница, «другу по себестоимости», «съел сам», подарок, списание.
Цена подставляется по типу.
- **Долги** — кто сколько должен, с частичными оплатами.
- **Товары** — номенклатура с историей изменения цен.
- **Журнал** — что менялось в базе, когда и с какой машины.
- **Сводка** — долг пекарне, дебиторка, прибыль, остатки, ближайший дедлайн.
Продажи разносятся по партиям методом FIFO. Цены у поставщика фиксированные,
поэтому на себестоимость это не влияет — FIFO нужен только чтобы понимать,
деньги за какую партию уже пришли.
Прибыль считается как наценка с проданного **минус** себестоимость съеденного
и подаренного: за них пекарне платить всё равно, и покрывается это из маржи.
## Как запустить
```bash
pip install -r requirements.txt
python run.py
```
Собрать один exe:
```bash
pip install -r requirements-dev.txt
pyinstaller food-market.spec
```
Готовый `dist/food-market.exe` (~50 МБ) положи в корень клона этого
репозитория. Базу он ищет в `data/vault.fmdb` рядом с собой, поэтому достаточно
склонировать репозиторий, положить exe в корень и запустить.
## Хранение и шифрование
`data/vault.fmdb` — единственный файл с данными:
```
JSON → gzip → AES-256-GCM (ключ: scrypt от пароля) → файл
```
Заголовок файла идёт в GCM как AAD, так что подменить соль или параметры KDF
незаметно нельзя. Запись атомарная, предыдущая версия остаётся в `.bak`.
**Забытый пароль восстановить невозможно.** В настройках есть кнопка выгрузки
открытого JSON — держи такую копию отдельно. В ней лежит и токен GitLab.
## Синхронизация
Раз в час (интервал настраивается) приложение сохраняет базу, коммитит
**только** `data/vault.fmdb` и пушит. Незакоммиченные правки исходников в
автокоммит не попадают.
Токен GitLab хранится внутри зашифрованной базы и подставляется в URL только на
время вызова — в `.git/config` он не пишется и в журнал не попадает.
### Один коммит на день
Дельта-сжатие на шифртексте не работает: каждый коммит хранит полную копию
файла. При часовых пушах это ~1800 коммитов и порядка 60 МБ в год. Поэтому в
течение суток приложение дописывает в ту же вершину через `--amend` — выходит
около 13 МБ в год.
Форс-пуш делается только с `--force-with-lease` и только когда все условия
выполнены: вершина имеет наш формат сообщения, датирована сегодня, трогает ровно
`data/vault.fmdb` и совпадает с тем, что на сервере. Иначе — обычный коммит без
переписывания истории.
Плата за схлопывание: откатиться через git можно на границу суток, а не на любой
час. Детализацию внутри дня даёт журнал.
### Работа с двух машин
Слить два зашифрованных блоба автоматически невозможно. Если версии разошлись,
автопуш останавливается, версия с сервера кладётся рядом как
`data/vault.remote.fmdb`, а выбор предлагается в настройках. Ничего не
затирается молча.
## Разработка
```bash
pytest -q
```
Модули:
| файл | зачем |
|---|---|
| `app/crypto.py` | формат файла, scrypt + AES-GCM |
| `app/models.py` | структура документа, деньги в Decimal |
| `app/storage.py` | загрузка, атомарная запись, миграции схемы, хеш «грязности» |
| `app/journal.py` | **единственный путь записи** в базу + аудит-лог |
| `app/ledger.py` | FIFO, покрытие партий, долги, остатки, сводка |
| `app/gitsync.py` | git, схлопывание коммитов, разрешение расхождений |
| `app/ui/` | экраны на PySide6 |
Главное архитектурное правило: **ни один экран не меняет документ напрямую**.
Всё идёт через функции `app/journal.py`, которые сами считают разницу и
дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать
изменение, и заметили бы это только когда история понадобится.