# Булочная Десктопный учёт закупки и перепродажи булок. Вся база — один файл, зашифрованный паролем, который раз в час уезжает в этот репозиторий. ## Что считает Булки берутся в пекарне **в долг**: к дедлайну надо вернуть себестоимость партии, иначе новых не напекут. Наценка остаётся тебе. Часть булок уходит друзьям по себестоимости, часть съедается — и за них пекарне тоже платить. Программа отвечает на главный вопрос: **успеваю ли я собрать себестоимость конкретной партии до её срока**. - **Закупки** — партии с составом, ценами и датой, до которой надо рассчитаться. - **Продажи** — розница, «другу по себестоимости», «съел сам», подарок, списание. Цена подставляется по типу. - **Долги** — кто сколько должен, с частичными оплатами. - **Товары** — номенклатура с историей изменения цен. - **Журнал** — что менялось в базе, когда и с какой машины. - **Сводка** — долг пекарне, дебиторка, прибыль, остатки, ближайший дедлайн. Продажи разносятся по партиям методом 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`, которые сами считают разницу и дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать изменение, и заметили бы это только когда история понадобится.