Go to file
Claude 582f041dba Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.

Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.

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

По съеденному и подаренному чаевых не бывает — поле отключается.

Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 00:56:26 +03:00
.vscode Учёт закупки и перепродажи булок 2026-08-22 17:29:53 +03:00
app Чаевые 2026-08-23 00:56:26 +03:00
tests Чаевые 2026-08-23 00:56:26 +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 Чаевые 2026-08-23 00:56:26 +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. Исходники её полностью игнорируют.

Что считает

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

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

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

Чаевые

Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела бы закрытой, когда за булки ещё должны.

Если вводишь оплату больше суммы — булка 70, дали 100 — программа сама предложит записать разницу в чаевые. То же самое при приёме оплаты по долгу: заплатили больше, чем оставалось, — излишек становится чаевыми, а не теряется.

В расчётах чаевые:

  • идут в покрытие партии — это живые деньги, ими так же рассчитываются с пекарней. Если продажа списалась с нескольких партий, чаевые делятся между ними пропорционально выручке;
  • добавляются в прибыль целиком — себестоимости за ними нет;
  • не входят в выручку и не создают долга — за них никто ничего не должен.

По съеденному, подаренному и списанному чаевых не бывает — поле отключается.

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

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