Десктопное приложение на PySide6. Вся база — один JSON-документ, зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит в data/vault.fmdb и раз в час уезжает в этот репозиторий. Основное: - партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж по партиям и прогресс покрытия к сроку; - типы выбытия: розница, другу по себестоимости, съел сам, подарок, списание — съеденное вычитается из прибыли, за него платить всё равно; - долги контрагентов с частичными оплатами; - номенклатура с историей изменения цен; - журнал изменений внутри базы: git хранит непрозрачные снимки, поэтому настоящая история ведётся здесь. Синхронизация коммитит только путь базы и схлопывает часовые пуши в один коммит на день через amend + force-with-lease. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
6.8 KiB
Markdown
115 lines
6.8 KiB
Markdown
# Булочная
|
||
|
||
Десктопный учёт закупки и перепродажи булок. Вся база — один файл,
|
||
зашифрованный паролем, который раз в час уезжает в этот репозиторий.
|
||
|
||
## Что считает
|
||
|
||
Булки берутся в пекарне **в долг**: к дедлайну надо вернуть себестоимость
|
||
партии, иначе новых не напекут. Наценка остаётся тебе. Часть булок уходит
|
||
друзьям по себестоимости, часть съедается — и за них пекарне тоже платить.
|
||
|
||
Программа отвечает на главный вопрос: **успеваю ли я собрать себестоимость
|
||
конкретной партии до её срока**.
|
||
|
||
- **Закупки** — партии с составом, ценами и датой, до которой надо рассчитаться.
|
||
- **Продажи** — розница, «другу по себестоимости», «съел сам», подарок, списание.
|
||
Цена подставляется по типу.
|
||
- **Долги** — кто сколько должен, с частичными оплатами.
|
||
- **Товары** — номенклатура с историей изменения цен.
|
||
- **Журнал** — что менялось в базе, когда и с какой машины.
|
||
- **Сводка** — долг пекарне, дебиторка, прибыль, остатки, ближайший дедлайн.
|
||
|
||
Продажи разносятся по партиям методом 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`, которые сами считают разницу и
|
||
дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать
|
||
изменение, и заметили бы это только когда история понадобится.
|