Количества и раньше хранились в Decimal с тремя знаками, но до документа они не доходили: поля ввода отвергали точку. Интерфейс русский, локаль ждёт запятую, а на цифровой клавиатуре клавиша даёт точку — символ молча не появлялся, и выглядело это так, будто дробные вводить нельзя вообще. Теперь оба разделителя принимаются, пробелы-разделители разрядов тоже. Заодно исправлена ошибка, которую дробные вскрыли: в сводке складывалось общее количество остатка по всем товарам, то есть литры с килограммами и штуками. Теперь остаток выражен деньгами и числом позиций, а разбивка по единицам осталась в таблице «Остатки». Фасовки тоже могут быть дробными: бутылка 1,5 л. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
Булочная
Десктопный учёт закупки и перепродажи булок. Вся база — один файл, зашифрованный паролем, который раз в час уезжает в отдельный репозиторий.
Два репозитория, и это принципиально:
| репозиторий | что внутри |
|---|---|
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, которые сами считают разницу и
дописывают запись в журнал. Иначе новый экран однажды забыл бы залогировать
изменение, и заметили бы это только когда история понадобится.