Go to file
Claude bc868f5af1 Git-операции всегда над папкой данных, а не над корнем приложения
Приложение создавало GitSync от paths.app_root(). Пока база лежала внутри
репозитория с кодом, это работало; после разделения репозиториев — нет.
exe лежит в корне клона с исходниками, поэтому приложение распоряжалось
репозиторием кода: переписало ему origin на food-records и попыталось
отправить туда исходники. Пуш спас только отказ по fast-forward.

Теперь GitSync создаётся в одном месте — MainWindow._git() — и всегда
от paths.data_dir(). Тест проверяет и цель, и то, что других мест
создания GitSync в файле не осталось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:30:42 +03:00
.vscode Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
app Git-операции всегда над папкой данных, а не над корнем приложения 2026-08-22 22:30:42 +03:00
tests Git-операции всегда над папкой данных, а не над корнем приложения 2026-08-22 22:30:42 +03:00
.gitattributes Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
.gitignore Отдельный репозиторий для базы, фасовки товара, ввод истории 2026-08-22 21:25:39 +03:00
food-market.code-workspace Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
food-market.spec Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
pytest.ini Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
README.md Дробные количества: пол-литра сока, 1,125 кг сыра 2026-08-22 22:20:29 +03:00
requirements-dev.txt Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
requirements.txt Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
run.py Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00

Булочная

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

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

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

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

Что считает

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

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

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

Дробные количества

Количества хранятся с точностью до тысячных, поэтому 0,5 л сока, 1,125 кг сыра или полбулки учитываются как есть. Единица измерения у товара произвольная — шт, л, кг.

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

Общее количество остатка по всем товарам сразу нигде не показывается — складывать литры с килограммами бессмысленно. В сводке остаток выражен деньгами и числом позиций, а разбивка по товарам есть в таблице «Остатки».

Фасовки

Пачка печенья 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, которые сами считают разницу и дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать изменение, и заметили бы это только когда история понадобится.