food-market/README.md
Claude efce552e2b Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.

1. База и код разъехались по разным репозиториям. Автокоммит и раньше
   трогал только vault.fmdb, но лежал он в той же ветке, что и исходники,
   поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный
   клон food-records со своим .git, а исходники его игнорируют целиком.
   При первом запуске приложение встаёт на уже существующую историю
   сервера, а не заводит параллельную.

2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно
   за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO
   считаются в базовых единицах, поэтому поштучные продажи вычитаются из
   купленных пачек. Размер фасовки хранится в документе слепком.
   Миграция схемы 1→2 не меняет поведение уже заведённых данных.

3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы —
   отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя
   заводится как контрагент, повтор периода подсвечивается.

Тесты больше не имеют настроенного remote, чтобы не ходить в сеть.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:25:39 +03:00

10 KiB
Raw Blame History

Булочная

Десктопный учёт закупки и перепродажи булок. Вся база — один файл, зашифрованный паролем, который раз в час уезжает в отдельный репозиторий.

Два репозитория, и это принципиально:

репозиторий что внутри
gt.ser.gay/kizya/food-market исходники программы (этот)
gt.ser.gay/kizya/food-records только vault.fmdb — зашифрованная база

Смешивать их нельзя: git push отправляет ветку целиком, поэтому в общем репозитории история данных неизбежно тащила бы за собой историю программы. На диске это папка data/ рядом с exe — самостоятельный клон репозитория с данными, со своим .git. Исходники её полностью игнорируют.

Что считает

Булки берутся в пекарне в долг: к дедлайну надо вернуть себестоимость партии, иначе новых не напекут. Наценка остаётся тебе. Часть булок уходит друзьям по себестоимости, часть съедается — и за них пекарне тоже платить.

Программа отвечает на главный вопрос: успеваю ли я собрать себестоимость конкретной партии до её срока.

  • Закупки — партии с составом, ценами и датой, до которой надо рассчитаться.
  • Продажи — розница, «другу по себестоимости», «съел сам», подарок, списание. Цена подставляется по типу. Есть быстрый ввод пачки продаж за прошедший период.
  • Долги — кто сколько должен, с частичными оплатами.
  • Товары — номенклатура с фасовками и историей изменения цен.
  • Журнал — что менялось в базе, когда и с какой машины.
  • Сводка — долг пекарне, дебиторка, прибыль, остатки, ближайший дедлайн.

Фасовки

Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — это один товар с двумя фасовками, а не два разных товара. У товара есть базовая единица (штука) и любое число фасовок со своим размером и своей ценой.

Остатки, себестоимость и FIFO всегда считаются в базовых единицах, поэтому поштучные продажи корректно вычитаются из купленных пачек. Количество и цена при этом вводятся в той фасовке, которую выбрал ты: «2 пачки по 200 ₽» так и остаётся в документе. Размер фасовки сохраняется слепком — переопределишь пачку с 10 на 12 штук, и уже записанные документы не поедут.

Ввод истории

Кнопка «Быстрый ввод за период» на экране продаж: задаёшь диапазон дат, каждая строка таблицы — отдельная продажа. Дата новой строки наследуется от предыдущей, Enter добавляет строку, Ctrl+D дублирует, Ctrl+Enter записывает всё разом. Незнакомое имя в колонке «Кому» заводится как новый контрагент. Программа подсказывает, сколько продаж за выбранный период уже записано — чтобы не внести один и тот же месяц дважды.

Продажи разносятся по партиям методом FIFO. Цены у поставщика фиксированные, поэтому на себестоимость это не влияет — FIFO нужен только чтобы понимать, деньги за какую партию уже пришли.

Прибыль считается как наценка с проданного минус себестоимость съеденного и подаренного: за них пекарне платить всё равно, и покрывается это из маржи.

Как запустить

pip install -r requirements.txt
python run.py

Собрать один exe:

pip install -r requirements-dev.txt
pyinstaller food-market.spec

Готовый dist/food-market.exe (~50 МБ) можно положить куда угодно — базу он ищет в папке data/ рядом с собой и при первом запуске сам заведёт там клон репозитория с данными.

На новой машине быстрее склонировать данные сразу:

git clone https://gt.ser.gay/kizya/food-records.git data

Хранение и шифрование

data/vault.fmdb — единственный файл с данными:

JSON → gzip → AES-256-GCM (ключ: scrypt от пароля) → файл

Заголовок файла идёт в GCM как AAD, так что подменить соль или параметры KDF незаметно нельзя. Запись атомарная, предыдущая версия остаётся в .bak.

Забытый пароль восстановить невозможно. В настройках есть кнопка выгрузки открытого JSON — держи такую копию отдельно. В ней лежит и токен GitLab.

Синхронизация

Раз в час (интервал настраивается) приложение сохраняет базу, коммитит только vault.fmdb и пушит — в репозиторий данных, не в этот. Что бы ни оказалось в папке рядом с базой, в коммит оно не попадёт: все команды идут с явным списком путей.

Токен GitLab хранится внутри зашифрованной базы и подставляется в URL только на время вызова — в .git/config он не пишется и в журнал не попадает.

Один коммит на день

Дельта-сжатие на шифртексте не работает: каждый коммит хранит полную копию файла. При часовых пушах это ~1800 коммитов и порядка 60 МБ в год. Поэтому в течение суток приложение дописывает в ту же вершину через --amend — выходит около 13 МБ в год.

Форс-пуш делается только с --force-with-lease и только когда все условия выполнены: вершина имеет наш формат сообщения, датирована сегодня, трогает ровно data/vault.fmdb и совпадает с тем, что на сервере. Иначе — обычный коммит без переписывания истории.

Плата за схлопывание: откатиться через git можно на границу суток, а не на любой час. Детализацию внутри дня даёт журнал.

Работа с двух машин

Слить два зашифрованных блоба автоматически невозможно. Если версии разошлись, автопуш останавливается, версия с сервера кладётся рядом как data/vault.remote.fmdb, а выбор предлагается в настройках. Ничего не затирается молча.

Разработка

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/paths.py где лежат данные и почему отдельно от кода
app/ui/quick_sales.py быстрый ввод продаж за период
app/ui/ остальные экраны на PySide6

Главное архитектурное правило: ни один экран не меняет документ напрямую. Всё идёт через функции app/journal.py, которые сами считают разницу и дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать изменение, и заметили бы это только когда история понадобится.