2026-08-22 21:29:53 +07:00
|
|
|
|
"""Продажи и прочее выбытие: розница, друзья по себестоимости, съел сам."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
from PySide6.QtCore import Qt
|
2026-08-22 21:29:53 +07:00
|
|
|
|
from PySide6.QtWidgets import (
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
QButtonGroup,
|
2026-08-22 21:29:53 +07:00
|
|
|
|
QComboBox,
|
|
|
|
|
|
QDialog,
|
|
|
|
|
|
QFormLayout,
|
|
|
|
|
|
QLineEdit,
|
|
|
|
|
|
QMessageBox,
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
QRadioButton,
|
2026-08-22 21:29:53 +07:00
|
|
|
|
QVBoxLayout,
|
|
|
|
|
|
QWidget,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from .. import journal
|
|
|
|
|
|
from .. import money as m
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
from .. import salesfilter as sf
|
2026-08-22 21:29:53 +07:00
|
|
|
|
from ..models import (
|
|
|
|
|
|
CONSUMPTION_KINDS,
|
|
|
|
|
|
KIND_FRIEND,
|
|
|
|
|
|
KIND_RETAIL,
|
|
|
|
|
|
SALE_KIND_LABELS,
|
|
|
|
|
|
SALE_KINDS,
|
|
|
|
|
|
)
|
|
|
|
|
|
from . import theme
|
|
|
|
|
|
from . import widgets as w
|
|
|
|
|
|
|
|
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
# --- как рассчитались за продажу -----------------------------------------
|
|
|
|
|
|
#
|
|
|
|
|
|
# Раньше это был просто денежный ввод, заполненный полной суммой. Долг из него
|
|
|
|
|
|
# получался, если догадаться стереть число и вписать ноль, — а потом любая
|
|
|
|
|
|
# правка строк молча возвращала полную оплату обратно. Явный выбор режима
|
|
|
|
|
|
# делает «взял в долг» видимым вариантом, а не побочным эффектом.
|
|
|
|
|
|
|
|
|
|
|
|
PAY_FULL = "full"
|
|
|
|
|
|
PAY_DEBT = "debt"
|
|
|
|
|
|
PAY_PART = "part"
|
|
|
|
|
|
|
|
|
|
|
|
PAY_LABELS = {
|
|
|
|
|
|
PAY_FULL: "Заплатил полностью",
|
|
|
|
|
|
PAY_DEBT: "Взял в долг целиком",
|
|
|
|
|
|
PAY_PART: "Заплатил часть",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pay_mode_for(total, paid):
|
|
|
|
|
|
"""Каким режимом описывается эта пара сумм.
|
|
|
|
|
|
|
|
|
|
|
|
Нужно, чтобы переключатель отражал набранное вручную: вписал ноль —
|
|
|
|
|
|
загорается «взял в долг», вписал всю сумму — «заплатил полностью».
|
|
|
|
|
|
"""
|
|
|
|
|
|
if paid <= 0:
|
|
|
|
|
|
return PAY_DEBT
|
|
|
|
|
|
if paid >= total:
|
|
|
|
|
|
return PAY_FULL
|
|
|
|
|
|
return PAY_PART
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
def price_source_for(kind: str):
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
"""Откуда берётся цена по умолчанию для этого типа выбытия и фасовки.
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
Ради этого типы и заведены: друг платит себестоимость, съеденное не стоит
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
ничего, розница идёт по цене продажи. Цена всегда за одну штуку выбранной
|
|
|
|
|
|
фасовки — пачка стоит своих денег, а не десяти поштучных цен.
|
2026-08-22 21:29:53 +07:00
|
|
|
|
"""
|
|
|
|
|
|
if kind == KIND_FRIEND:
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
return lambda product, uom: product.cost_for(uom)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
if kind in CONSUMPTION_KINDS:
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
return lambda product, uom: m.ZERO
|
|
|
|
|
|
return lambda product, uom: product.retail_for(uom)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SaleDialog(QDialog):
|
|
|
|
|
|
def __init__(self, ctx, sale=None, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
self.sale = sale
|
|
|
|
|
|
self.setWindowTitle("Продажа" if sale else "Новая продажа")
|
|
|
|
|
|
self.setMinimumSize(640, 520)
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 16, 20, 16)
|
|
|
|
|
|
layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
|
|
form = QFormLayout()
|
|
|
|
|
|
form.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
self.date = w.DateInput(sale.date if sale else None)
|
|
|
|
|
|
form.addRow("Дата", self.date)
|
|
|
|
|
|
|
|
|
|
|
|
self.kind = QComboBox()
|
|
|
|
|
|
for kind in SALE_KINDS:
|
|
|
|
|
|
self.kind.addItem(SALE_KIND_LABELS[kind], kind)
|
|
|
|
|
|
self.kind.setCurrentIndex(SALE_KINDS.index(sale.kind) if sale else 0)
|
|
|
|
|
|
self.kind.currentIndexChanged.connect(self._on_kind_changed)
|
|
|
|
|
|
form.addRow("Тип", self.kind)
|
|
|
|
|
|
|
|
|
|
|
|
self.counterparty = QComboBox()
|
|
|
|
|
|
self.counterparty.addItem("— не указан —", None)
|
|
|
|
|
|
for cp in sorted(ctx.vault.doc.counterparties, key=lambda c: c.name.lower()):
|
|
|
|
|
|
self.counterparty.addItem(cp.name, cp.id)
|
|
|
|
|
|
if sale and sale.counterparty_id:
|
|
|
|
|
|
index = self.counterparty.findData(sale.counterparty_id)
|
|
|
|
|
|
if index >= 0:
|
|
|
|
|
|
self.counterparty.setCurrentIndex(index)
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
# Выбор человека снимает предупреждение о долге без имени.
|
|
|
|
|
|
self.counterparty.currentIndexChanged.connect(lambda _: self._update_payment_hint())
|
2026-08-22 21:29:53 +07:00
|
|
|
|
form.addRow("Кому", w.row(self.counterparty, w.button("+ Новый", on_click=self._add_counterparty), stretch_at=0))
|
|
|
|
|
|
|
|
|
|
|
|
self.note = QLineEdit(sale.note if sale else "")
|
|
|
|
|
|
form.addRow("Заметка", self.note)
|
|
|
|
|
|
layout.addLayout(form)
|
|
|
|
|
|
|
Починен ввод цены и выделение в таблицах
Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы
из вводимого текста, чтобы принимались разделители разрядов, — но у поля
цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс,
Qt отвергало символ, и в поле оставалась только первая цифра: набираешь
«25,50», получаешь 2.
Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как
есть, и только если Qt отверг именно из-за точки — повторяем с запятой.
Второе: форма продажи подключала сигнал редактора строк раньше, чем
создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо
падения оплата просто молча не подставлялась.
Третье: выделение в таблицах держалось за номер строки. Таблицы
отсортированы, и после перерисовки под тем же номером оказывался другой
объект — можно было удалить не тот товар. Теперь выделение
восстанавливается по идентификатору.
Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(),
которые этот баг пропустили, и перехват sys.excepthook, чтобы
проглоченные Qt исключения считались провалом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 03:13:45 +07:00
|
|
|
|
# Поля оплаты создаются ниже, но редактор строк начинает слать сигналы
|
|
|
|
|
|
# сразу при заполнении. Без этих заглушек первый же сигнал приходит
|
|
|
|
|
|
# в обработчик, для которого self.paid ещё не существует, — Qt съедает
|
|
|
|
|
|
# исключение, и подстановка оплаты молча не работает.
|
|
|
|
|
|
self.paid = None
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
self.tip = None
|
Починен ввод цены и выделение в таблицах
Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы
из вводимого текста, чтобы принимались разделители разрядов, — но у поля
цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс,
Qt отвергало символ, и в поле оставалась только первая цифра: набираешь
«25,50», получаешь 2.
Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как
есть, и только если Qt отверг именно из-за точки — повторяем с запятой.
Второе: форма продажи подключала сигнал редактора строк раньше, чем
создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо
падения оплата просто молча не подставлялась.
Третье: выделение в таблицах держалось за номер строки. Таблицы
отсортированы, и после перерисовки под тем же номером оказывался другой
объект — можно было удалить не тот товар. Теперь выделение
восстанавливается по идентификатору.
Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(),
которые этот баг пропустили, и перехват sys.excepthook, чтобы
проглоченные Qt исключения считались провалом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 03:13:45 +07:00
|
|
|
|
self.paid_hint = None
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
self.pay_mode = None
|
|
|
|
|
|
self.pay_buttons = {}
|
Починен ввод цены и выделение в таблицах
Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы
из вводимого текста, чтобы принимались разделители разрядов, — но у поля
цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс,
Qt отвергало символ, и в поле оставалась только первая цифра: набираешь
«25,50», получаешь 2.
Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как
есть, и только если Qt отверг именно из-за точки — повторяем с запятой.
Второе: форма продажи подключала сигнал редактора строк раньше, чем
создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо
падения оплата просто молча не подставлялась.
Третье: выделение в таблицах держалось за номер строки. Таблицы
отсортированы, и после перерисовки под тем же номером оказывался другой
объект — можно было удалить не тот товар. Теперь выделение
восстанавливается по идентификатору.
Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(),
которые этот баг пропустили, и перехват sys.excepthook, чтобы
проглоченные Qt исключения считались провалом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 03:13:45 +07:00
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
layout.addWidget(w.label("Что ушло", "h2"))
|
|
|
|
|
|
products = [p for p in ctx.vault.doc.products if not p.archived]
|
|
|
|
|
|
self.lines = w.LinesEditor(
|
|
|
|
|
|
products, "Цена", price_source_for(self.current_kind()), ctx.currency
|
|
|
|
|
|
)
|
|
|
|
|
|
self.lines.changed.connect(self._sync_payment_default)
|
|
|
|
|
|
if sale:
|
|
|
|
|
|
self.lines.set_lines(sale.lines, "unit_price")
|
|
|
|
|
|
elif products:
|
|
|
|
|
|
self.lines.add_line()
|
|
|
|
|
|
layout.addWidget(self.lines, 1)
|
|
|
|
|
|
|
|
|
|
|
|
# Оплату задаём только при создании: у существующей продажи платежи
|
|
|
|
|
|
# живут своей жизнью и правятся на экране долгов.
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
pay_form = QFormLayout()
|
2026-08-22 21:29:53 +07:00
|
|
|
|
if sale is None:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
pay_form.addRow("Как рассчитались", self._pay_mode_row())
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
self.paid = w.MoneySpin(ctx.currency)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
# Переплату разбираем, когда человек закончил вводить сумму,
|
|
|
|
|
|
# а не на каждом нажатии: иначе поле дёргалось бы посреди набора.
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
self.paid.editingFinished.connect(self._on_paid_edited)
|
|
|
|
|
|
# Без этого поле можно было обнулить, а подсказка продолжала
|
|
|
|
|
|
# уверять, что оплачено полностью.
|
|
|
|
|
|
self.paid.valueChanged.connect(lambda _: self._update_payment_hint())
|
|
|
|
|
|
pay_form.addRow("Получено сразу", self.paid)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
|
|
|
|
|
|
# Чаевые правятся и у записанной продажи: сдачу могли оставить
|
|
|
|
|
|
# и после того, как продажу занесли.
|
|
|
|
|
|
self.tip = w.MoneySpin(ctx.currency)
|
|
|
|
|
|
if sale is not None:
|
|
|
|
|
|
self.tip.set_decimal(sale.tip)
|
|
|
|
|
|
self.tip.valueChanged.connect(lambda _: self._update_payment_hint())
|
|
|
|
|
|
pay_form.addRow("Чаевые", self.tip)
|
|
|
|
|
|
layout.addLayout(pay_form)
|
|
|
|
|
|
|
|
|
|
|
|
self.paid_hint = w.label("", "dim")
|
|
|
|
|
|
self.paid_hint.setWordWrap(True)
|
|
|
|
|
|
layout.addWidget(self.paid_hint)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(None, w.button("Отмена", on_click=self.reject), w.button("Сохранить", "primary", self.accept))
|
|
|
|
|
|
)
|
|
|
|
|
|
self._on_kind_changed()
|
|
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
# --- как рассчитались ---
|
|
|
|
|
|
|
|
|
|
|
|
def _pay_mode_row(self) -> QWidget:
|
|
|
|
|
|
self.pay_mode = PAY_FULL
|
|
|
|
|
|
self.pay_group = QButtonGroup(self)
|
|
|
|
|
|
buttons = []
|
|
|
|
|
|
for mode in (PAY_FULL, PAY_DEBT, PAY_PART):
|
|
|
|
|
|
button = QRadioButton(PAY_LABELS[mode])
|
|
|
|
|
|
button.setChecked(mode == PAY_FULL)
|
|
|
|
|
|
button.toggled.connect(
|
|
|
|
|
|
lambda checked, value=mode: checked and self._set_pay_mode(value)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.pay_group.addButton(button)
|
|
|
|
|
|
self.pay_buttons[mode] = button
|
|
|
|
|
|
buttons.append(button)
|
|
|
|
|
|
return w.row(*buttons, None)
|
|
|
|
|
|
|
|
|
|
|
|
def _set_pay_mode(self, mode: str) -> None:
|
|
|
|
|
|
"""Переключили режим руками — подставить сумму под него."""
|
|
|
|
|
|
self.pay_mode = mode
|
|
|
|
|
|
if self.paid is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if mode == PAY_FULL:
|
|
|
|
|
|
self.paid.set_decimal(self.lines.total())
|
|
|
|
|
|
elif mode == PAY_DEBT:
|
|
|
|
|
|
self.paid.set_decimal(0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.paid.setFocus()
|
|
|
|
|
|
self.paid.selectAll()
|
|
|
|
|
|
self._update_payment_hint()
|
|
|
|
|
|
|
|
|
|
|
|
def _show_pay_mode(self, mode: str) -> None:
|
|
|
|
|
|
"""Отразить режим в переключателе, не трогая суммы."""
|
|
|
|
|
|
self.pay_mode = mode
|
|
|
|
|
|
button = self.pay_buttons.get(mode)
|
|
|
|
|
|
if button is None or button.isChecked():
|
|
|
|
|
|
return
|
|
|
|
|
|
button.blockSignals(True)
|
|
|
|
|
|
button.setChecked(True)
|
|
|
|
|
|
button.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_paid_edited(self) -> None:
|
|
|
|
|
|
"""Сумму вписали руками: разобрать переплату и показать, что вышло."""
|
|
|
|
|
|
overpaid = self._absorb_overpayment()
|
|
|
|
|
|
if self.paid is not None:
|
|
|
|
|
|
self._show_pay_mode(pay_mode_for(self.lines.total(), self.paid.value_decimal()))
|
|
|
|
|
|
self._update_payment_hint(overpaid)
|
|
|
|
|
|
|
|
|
|
|
|
def problem(self) -> str | None:
|
|
|
|
|
|
"""Что мешает записать продажу. None — всё в порядке.
|
|
|
|
|
|
|
|
|
|
|
|
Отдельным методом, а не прямо в accept: проверка — это правило, и его
|
|
|
|
|
|
должно быть видно тесту, а не только человеку в модальном окне.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.current_debt() > 0 and not self.counterparty.currentData():
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"В долг уходит {m.fmt_money(self.current_debt(), self.ctx.currency)}, "
|
|
|
|
|
|
"но не указано кому.\n\nВыбери человека в поле «Кому» или заведи "
|
|
|
|
|
|
"нового кнопкой рядом — иначе долг будет не за кем."
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def accept(self) -> None:
|
|
|
|
|
|
"""Не выпускать долг без имени.
|
|
|
|
|
|
|
|
|
|
|
|
Такая запись невосстановима: на экране долгов она попадает в кучу
|
|
|
|
|
|
«без контрагента», и с кого спрашивать деньги — уже не узнать.
|
|
|
|
|
|
"""
|
|
|
|
|
|
problem = self.problem()
|
|
|
|
|
|
if problem:
|
|
|
|
|
|
QMessageBox.warning(self, "Кому в долг?", problem)
|
|
|
|
|
|
self.counterparty.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
super().accept()
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
def current_kind(self) -> str:
|
|
|
|
|
|
return self.kind.currentData()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_kind_changed(self) -> None:
|
|
|
|
|
|
kind = self.current_kind()
|
|
|
|
|
|
self.lines.price_source = price_source_for(kind)
|
|
|
|
|
|
self.lines.refresh_prices()
|
|
|
|
|
|
|
|
|
|
|
|
consumption = kind in CONSUMPTION_KINDS
|
|
|
|
|
|
if self.paid is not None:
|
|
|
|
|
|
self.paid.setEnabled(not consumption)
|
|
|
|
|
|
if consumption:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
# Съеденное и подаренное денег не приносит: долга тут не бывает,
|
|
|
|
|
|
# поэтому и выбирать нечего.
|
|
|
|
|
|
self._show_pay_mode(PAY_FULL)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
self.paid.set_decimal(0)
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
for button in self.pay_buttons.values():
|
|
|
|
|
|
button.setEnabled(not consumption)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
if self.tip is not None:
|
|
|
|
|
|
self.tip.setEnabled(not consumption)
|
|
|
|
|
|
if consumption:
|
|
|
|
|
|
self.tip.set_decimal(0)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
self._sync_payment_default()
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_payment_default(self) -> None:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
"""Пересчитать оплату вслед за строками.
|
|
|
|
|
|
|
|
|
|
|
|
Только в режиме «заплатил полностью». Иначе правка количества затирала
|
|
|
|
|
|
бы выставленный долг: вписал ноль, поправил строку — и продажа снова
|
|
|
|
|
|
оплачена целиком, причём молча.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if (
|
|
|
|
|
|
self.paid is not None
|
|
|
|
|
|
and self.pay_mode == PAY_FULL
|
|
|
|
|
|
and self.current_kind() not in CONSUMPTION_KINDS
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
# Пока поле в фокусе, не перебиваем то, что человек набирает.
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
and not self.paid.hasFocus()
|
|
|
|
|
|
):
|
|
|
|
|
|
self.paid.set_decimal(self.lines.total())
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
self._update_payment_hint()
|
|
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
def _absorb_overpayment(self):
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
"""Дали больше суммы — разница уходит в чаевые.
|
|
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
Булка стоит 70, дали 100, сдачи не надо. Оставлять 100 в «получено»
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
нельзя: столько за товар никто не должен, и продажа выглядела бы
|
|
|
|
|
|
переплаченной вместо того, чтобы показать чаевые.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.paid is None or self.tip is None:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
return m.ZERO
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
total = self.lines.total()
|
|
|
|
|
|
extra = m.money(self.paid.value_decimal() - total)
|
|
|
|
|
|
if extra <= 0:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
return m.ZERO
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
|
|
|
|
|
|
self.paid.set_decimal(total)
|
|
|
|
|
|
self.tip.set_decimal(self.tip.value_decimal() + extra)
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
return extra
|
|
|
|
|
|
|
|
|
|
|
|
def current_debt(self):
|
|
|
|
|
|
if self.paid is None:
|
|
|
|
|
|
return m.ZERO
|
|
|
|
|
|
return max(m.ZERO, m.money(self.lines.total() - self.paid.value_decimal()))
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
def _update_payment_hint(self, overpaid=None) -> None:
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
if self.paid_hint is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if self.current_kind() in CONSUMPTION_KINDS:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
self._say("Денег нет по определению, но себестоимость пекарне вернуть надо.")
|
2026-08-22 21:29:53 +07:00
|
|
|
|
return
|
|
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
currency = self.ctx.currency
|
|
|
|
|
|
tip = self.tip.value_decimal() if self.tip is not None else m.ZERO
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
debt = self.current_debt()
|
|
|
|
|
|
nameless = debt > 0 and not self.counterparty.currentData()
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
parts = []
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
if overpaid:
|
|
|
|
|
|
parts.append(f"Переплата {m.fmt_money(overpaid, currency)} записана в чаевые.")
|
|
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
if self.paid is not None:
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
if debt > 0:
|
|
|
|
|
|
# Долг без имени взыскать не с кого: на экране долгов он
|
|
|
|
|
|
# окажется в куче «без контрагента» и там и останется.
|
|
|
|
|
|
parts.append(
|
|
|
|
|
|
f"В долг уйдёт {m.fmt_money(debt, currency)} — "
|
|
|
|
|
|
"укажи, кому, иначе будет непонятно, с кого спрашивать."
|
|
|
|
|
|
if nameless
|
|
|
|
|
|
else f"В долг уйдёт {m.fmt_money(debt, currency)}. "
|
|
|
|
|
|
"Долг появится на экране «Долги»."
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
parts.append("Оплачено полностью.")
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
if tip > 0:
|
|
|
|
|
|
parts.append(f"Чаевые {m.fmt_money(tip, currency)} идут в покрытие партии и в прибыль.")
|
Долг как явный выбор, история цен у фасовок
Долг было непонятно как записать, и не зря. Оплата была просто денежным
полем с полной суммой: долг получался, если догадаться стереть число
и вписать ноль. Дальше — хуже. Подсказка при этом продолжала уверять, что
оплачено полностью, а любая правка строк подставляла полную сумму обратно
и молча стирала выставленный долг.
Теперь выбор явный: заплатил полностью / взял в долг целиком / заплатил
часть. Пересчёт работает только в первом режиме. Поле суммы осталось
живым — вписанное руками число само зажигает подходящий переключатель.
Долг без имени записать нельзя: на экране долгов такая запись попадает
в кучу «без контрагента», и с кого спрашивать — уже не узнать. В быстром
вводе это предупреждение, а не запрет: внося историю, можно и правда не
помнить, кто это был.
На экране долгов человека можно выбрать целиком и рассчитаться сразу за
всё: деньги приходят одной суммой, а гасятся долги по очереди, со старых.
Цены фасовок переехали в «Цены и история». Они меняются одним решением
поставщика вместе с ценой товара, а история была только у товара —
у коробки цена уезжала молча. Заодно закрыт второй вход: в карточке
товара цены фасовок теперь только показываются, правится там состав.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 04:25:49 +07:00
|
|
|
|
|
|
|
|
|
|
self._say(" ".join(parts), theme.WARN if nameless else "")
|
|
|
|
|
|
|
|
|
|
|
|
def _say(self, text: str, color: str = "") -> None:
|
|
|
|
|
|
self.paid_hint.setText(text)
|
|
|
|
|
|
self.paid_hint.setStyleSheet(f"color: {color};" if color else "")
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
def _add_counterparty(self) -> None:
|
|
|
|
|
|
from PySide6.QtWidgets import QInputDialog
|
|
|
|
|
|
|
|
|
|
|
|
name, ok = QInputDialog.getText(self, "Новый контрагент", "Имя")
|
|
|
|
|
|
if not ok or not name.strip():
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
cp = journal.create_counterparty(self.ctx.vault, name)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.counterparty.addItem(cp.name, cp.id)
|
|
|
|
|
|
self.counterparty.setCurrentIndex(self.counterparty.count() - 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SalesPage(QWidget):
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
COL_DATE, COL_KIND, COL_WHO, COL_WHAT, COL_TOTAL, COL_PAID, COL_TIP, COL_DEBT = range(8)
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
def __init__(self, ctx, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 18, 20, 18)
|
|
|
|
|
|
layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(w.heading("Продажи"))
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(
|
|
|
|
|
|
w.button("+ Продажа", "primary", self.create),
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
w.button("Быстрый ввод за период", on_click=self.quick_entry),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
w.button("Изменить", on_click=self.edit),
|
|
|
|
|
|
w.button("Принять оплату", on_click=self.take_payment),
|
|
|
|
|
|
w.button("Удалить", "danger", self.delete),
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
layout.addWidget(self._filter_bar())
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
self.table = w.table(
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
["Дата", "Тип", "Кому", "Что", "Сумма", "Оплачено", "Чаевые", "Долг"],
|
|
|
|
|
|
stretch_column=3,
|
2026-08-22 21:29:53 +07:00
|
|
|
|
)
|
|
|
|
|
|
self.table.doubleClicked.connect(self.edit)
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
# Свежие продажи сверху. Индикатор в заголовке при этом соответствует
|
|
|
|
|
|
# тому, что видно: без него сортировка «по умолчанию» ничем не помечена,
|
|
|
|
|
|
# и первый клик по «Дате» кажется ничего не делающим.
|
|
|
|
|
|
self.table.sortByColumn(self.COL_DATE, Qt.DescendingOrder)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
layout.addWidget(self.table, 1)
|
|
|
|
|
|
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
self.summary = w.label("", "dim")
|
|
|
|
|
|
layout.addWidget(self.summary)
|
|
|
|
|
|
|
|
|
|
|
|
# --- фильтры ---
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_bar(self) -> QWidget:
|
|
|
|
|
|
"""Панель отбора. Всё, что её меняет, ведёт в один refresh."""
|
|
|
|
|
|
self.search = QLineEdit()
|
|
|
|
|
|
self.search.setPlaceholderText("Поиск: товар, имя, заметка")
|
|
|
|
|
|
self.search.setClearButtonEnabled(True)
|
|
|
|
|
|
self.search.textChanged.connect(self.refresh)
|
|
|
|
|
|
|
|
|
|
|
|
self.period = QComboBox()
|
|
|
|
|
|
for key in sf.PERIODS:
|
|
|
|
|
|
self.period.addItem(sf.PERIOD_LABELS[key], key)
|
|
|
|
|
|
self.period.currentIndexChanged.connect(self._on_period_changed)
|
|
|
|
|
|
|
|
|
|
|
|
# Свои даты нужны редко, а места занимают много — показываем их только
|
|
|
|
|
|
# когда выбран собственный диапазон.
|
|
|
|
|
|
self.since = w.DateInput()
|
|
|
|
|
|
self.until = w.DateInput()
|
|
|
|
|
|
for field in (self.since, self.until):
|
|
|
|
|
|
field.dateChanged.connect(lambda _: self.refresh())
|
|
|
|
|
|
field.setVisible(False)
|
|
|
|
|
|
|
|
|
|
|
|
self.kind = QComboBox()
|
|
|
|
|
|
self.kind.addItem("Все типы", None)
|
|
|
|
|
|
for kind in SALE_KINDS:
|
|
|
|
|
|
self.kind.addItem(SALE_KIND_LABELS[kind], kind)
|
|
|
|
|
|
self.kind.currentIndexChanged.connect(self.refresh)
|
|
|
|
|
|
|
|
|
|
|
|
self.who = QComboBox()
|
|
|
|
|
|
self.who.currentIndexChanged.connect(self.refresh)
|
|
|
|
|
|
self._fill_counterparties()
|
|
|
|
|
|
|
|
|
|
|
|
self.money = QComboBox()
|
|
|
|
|
|
for state in sf.MONEY_STATES:
|
|
|
|
|
|
self.money.addItem(sf.MONEY_LABELS[state], state)
|
|
|
|
|
|
self.money.currentIndexChanged.connect(self.refresh)
|
|
|
|
|
|
|
|
|
|
|
|
self.reset_button = w.button("Сбросить", on_click=self.reset_filters)
|
|
|
|
|
|
|
|
|
|
|
|
return w.row(
|
|
|
|
|
|
self.search,
|
|
|
|
|
|
self.period,
|
|
|
|
|
|
self.since,
|
|
|
|
|
|
self.until,
|
|
|
|
|
|
self.kind,
|
|
|
|
|
|
self.who,
|
|
|
|
|
|
self.money,
|
|
|
|
|
|
self.reset_button,
|
|
|
|
|
|
stretch_at=0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _fill_counterparties(self) -> None:
|
|
|
|
|
|
"""Пересобрать список людей, сохранив выбранного.
|
|
|
|
|
|
|
|
|
|
|
|
Контрагенты заводятся прямо из формы продажи, поэтому список живой.
|
|
|
|
|
|
Восстанавливаем по идентификатору: под тем же номером после сортировки
|
|
|
|
|
|
по алфавиту оказывается уже другой человек.
|
|
|
|
|
|
"""
|
|
|
|
|
|
previously = self.who.currentData()
|
|
|
|
|
|
self.who.blockSignals(True)
|
|
|
|
|
|
self.who.clear()
|
|
|
|
|
|
self.who.addItem("Все люди", None)
|
|
|
|
|
|
self.who.addItem("Без контрагента", sf.NO_COUNTERPARTY)
|
|
|
|
|
|
for cp in sorted(self.ctx.vault.doc.counterparties, key=lambda c: c.name.lower()):
|
|
|
|
|
|
self.who.addItem(cp.name, cp.id)
|
|
|
|
|
|
index = self.who.findData(previously) if previously else 0
|
|
|
|
|
|
self.who.setCurrentIndex(max(0, index))
|
|
|
|
|
|
self.who.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_period_changed(self) -> None:
|
|
|
|
|
|
custom = self.period.currentData() == sf.PERIOD_CUSTOM
|
|
|
|
|
|
self.since.setVisible(custom)
|
|
|
|
|
|
self.until.setVisible(custom)
|
|
|
|
|
|
self.refresh()
|
|
|
|
|
|
|
|
|
|
|
|
def current_filter(self) -> sf.SalesFilter:
|
|
|
|
|
|
return sf.SalesFilter(
|
|
|
|
|
|
text=self.search.text(),
|
|
|
|
|
|
kind=self.kind.currentData(),
|
|
|
|
|
|
counterparty_id=self.who.currentData(),
|
|
|
|
|
|
period=self.period.currentData() or sf.PERIOD_ALL,
|
|
|
|
|
|
since=self.since.get_date(),
|
|
|
|
|
|
until=self.until.get_date(),
|
|
|
|
|
|
money=self.money.currentData() or sf.MONEY_ANY,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def reset_filters(self) -> None:
|
|
|
|
|
|
for widget in (self.period, self.kind, self.who, self.money):
|
|
|
|
|
|
widget.blockSignals(True)
|
|
|
|
|
|
widget.setCurrentIndex(0)
|
|
|
|
|
|
widget.blockSignals(False)
|
|
|
|
|
|
self.search.blockSignals(True)
|
|
|
|
|
|
self.search.clear()
|
|
|
|
|
|
self.search.blockSignals(False)
|
|
|
|
|
|
self._on_period_changed()
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
# --- отрисовка ---
|
|
|
|
|
|
|
|
|
|
|
|
def refresh(self) -> None:
|
|
|
|
|
|
doc = self.ctx.vault.doc
|
|
|
|
|
|
currency = self.ctx.currency
|
|
|
|
|
|
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
self._fill_counterparties()
|
|
|
|
|
|
active = self.current_filter()
|
|
|
|
|
|
shown = sf.apply(
|
|
|
|
|
|
doc,
|
|
|
|
|
|
sorted(doc.sales, key=lambda s: s.date, reverse=True),
|
|
|
|
|
|
active,
|
|
|
|
|
|
self.ctx.report.today,
|
|
|
|
|
|
)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
rows, keys = [], []
|
|
|
|
|
|
for sale in shown:
|
2026-08-22 21:29:53 +07:00
|
|
|
|
what = ", ".join(
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
f"{doc.product_name(line.product_id)} × {m.fmt_qty(line.qty)} "
|
|
|
|
|
|
f"{line.unit_name(doc.product(line.product_id))}"
|
2026-08-22 21:29:53 +07:00
|
|
|
|
for line in sale.lines
|
|
|
|
|
|
)
|
|
|
|
|
|
debt = sale.debt
|
|
|
|
|
|
rows.append(
|
|
|
|
|
|
[
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
w.date_item(sale.date),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
w.text_item(
|
|
|
|
|
|
SALE_KIND_LABELS.get(sale.kind, sale.kind),
|
|
|
|
|
|
theme.MUTED if sale.is_consumption else "",
|
|
|
|
|
|
),
|
|
|
|
|
|
w.text_item(doc.counterparty_name(sale.counterparty_id)),
|
|
|
|
|
|
w.text_item(what),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(sale.total, currency), sale.total),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(sale.paid, currency), sale.paid),
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
w.sortable_num_item(
|
|
|
|
|
|
m.fmt_money(sale.tip, currency) if sale.tip > 0 else "",
|
|
|
|
|
|
sale.tip,
|
|
|
|
|
|
theme.OK if sale.tip > 0 else "",
|
|
|
|
|
|
),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
w.sortable_num_item(
|
|
|
|
|
|
m.fmt_money(debt, currency) if debt > 0 else "",
|
|
|
|
|
|
debt,
|
|
|
|
|
|
theme.WARN if debt > 0 else "",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
keys.append(sale.id)
|
|
|
|
|
|
|
|
|
|
|
|
w.fill(self.table, rows, keys)
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
self.summary.setText(
|
|
|
|
|
|
sf.summary_text(sf.totals(shown), len(doc.sales), currency)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.reset_button.setEnabled(not active.is_empty)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
# --- действия ---
|
|
|
|
|
|
|
|
|
|
|
|
def _selected(self):
|
|
|
|
|
|
sale_id = w.selected_key(self.table)
|
|
|
|
|
|
sale = self.ctx.vault.doc.sale(sale_id) if sale_id else None
|
|
|
|
|
|
if sale is None:
|
|
|
|
|
|
QMessageBox.information(self, "Не выбрано", "Выбери продажу в списке.")
|
|
|
|
|
|
return sale
|
|
|
|
|
|
|
|
|
|
|
|
def create(self) -> None:
|
|
|
|
|
|
if not [p for p in self.ctx.vault.doc.products if not p.archived]:
|
|
|
|
|
|
QMessageBox.information(self, "Нет товаров", "Сначала заведи булки на вкладке «Товары».")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
dialog = SaleDialog(self.ctx, parent=self)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.create_sale(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
dialog.current_kind(),
|
|
|
|
|
|
dialog.lines.lines("unit_price"),
|
|
|
|
|
|
dialog.counterparty.currentData(),
|
|
|
|
|
|
dialog.note.text(),
|
|
|
|
|
|
dialog.paid.value_decimal(),
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
dialog.tip.value_decimal(),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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-23 01:25:39 +07:00
|
|
|
|
def quick_entry(self) -> None:
|
|
|
|
|
|
"""Внести пачку продаж за прошедший период."""
|
|
|
|
|
|
if not [p for p in self.ctx.vault.doc.products if not p.archived]:
|
|
|
|
|
|
QMessageBox.information(self, "Нет товаров", "Сначала заведи булки на вкладке «Товары».")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
from .quick_sales import QuickSalesDialog
|
|
|
|
|
|
|
|
|
|
|
|
dialog = QuickSalesDialog(self.ctx, parent=self)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
QMessageBox.information(self, "Готово", f"Записано продаж: {dialog.created}.")
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
def edit(self) -> None:
|
|
|
|
|
|
sale = self._selected()
|
|
|
|
|
|
if sale is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
dialog = SaleDialog(self.ctx, sale, parent=self)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.update_sale(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
sale.id,
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
dialog.current_kind(),
|
|
|
|
|
|
dialog.lines.lines("unit_price"),
|
|
|
|
|
|
dialog.counterparty.currentData() or "",
|
|
|
|
|
|
dialog.note.text(),
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip=dialog.tip.value_decimal(),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def take_payment(self) -> None:
|
|
|
|
|
|
sale = self._selected()
|
|
|
|
|
|
if sale is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if sale.debt <= 0:
|
|
|
|
|
|
QMessageBox.information(self, "Долга нет", "Эта продажа уже оплачена.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
from .page_debts import ask_payment
|
|
|
|
|
|
|
|
|
|
|
|
amount = ask_payment(self, sale.debt, self.ctx.currency)
|
|
|
|
|
|
if amount is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.add_sale_payment(self.ctx.vault, sale.id, amount)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def delete(self) -> None:
|
|
|
|
|
|
sale = self._selected()
|
|
|
|
|
|
if sale is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
confirm = QMessageBox.question(
|
|
|
|
|
|
self, "Удалить продажу?", "Товар вернётся в остатки, деньги — исчезнут из учёта."
|
|
|
|
|
|
)
|
|
|
|
|
|
if confirm != QMessageBox.Yes:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.delete_sale(self.ctx.vault, sale.id)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|