Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
"""Быстрый ввод продаж за период.
|
|
|
|
|
|
|
|
|
|
|
|
Обычная форма продажи хороша, когда записываешь одну сделку. Когда надо внести
|
|
|
|
|
|
историю за прошедший период, она превращается в пытку: на каждую продажу —
|
|
|
|
|
|
открыть, заполнить, закрыть.
|
|
|
|
|
|
|
|
|
|
|
|
Здесь каждая строка таблицы — отдельная продажа. Период задаётся сверху, дата
|
|
|
|
|
|
новой строки наследуется от предыдущей, всё вводится с клавиатуры, а запись
|
|
|
|
|
|
происходит одним действием в конце.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import date, timedelta
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
|
|
from PySide6.QtCore import Qt
|
|
|
|
|
|
from PySide6.QtGui import QColor, QKeySequence, QShortcut
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QComboBox,
|
|
|
|
|
|
QDialog,
|
|
|
|
|
|
QHeaderView,
|
|
|
|
|
|
QMessageBox,
|
|
|
|
|
|
QPushButton,
|
|
|
|
|
|
QTableWidget,
|
|
|
|
|
|
QVBoxLayout,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from .. import journal
|
|
|
|
|
|
from .. import money as m
|
|
|
|
|
|
from ..models import CONSUMPTION_KINDS, SALE_KIND_LABELS, SALE_KINDS
|
|
|
|
|
|
from . import theme
|
|
|
|
|
|
from . import widgets as w
|
|
|
|
|
|
from .page_sales import price_source_for
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_PERIOD_DAYS = 30
|
|
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
(
|
|
|
|
|
|
COL_DATE, COL_KIND, COL_WHO, COL_PRODUCT, COL_QTY,
|
|
|
|
|
|
COL_UOM, COL_PRICE, COL_PAID, COL_TIP, COL_TOTAL,
|
|
|
|
|
|
) = range(10)
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
HEADERS = [
|
|
|
|
|
|
"Дата", "Тип", "Кому", "Товар", "Кол-во",
|
|
|
|
|
|
"Фасовка", "Цена", "Оплачено", "Чаевые", "Сумма",
|
|
|
|
|
|
]
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
|
|
|
|
|
|
# Пользователь поправил оплату руками — больше не подставляем её автоматически.
|
|
|
|
|
|
TOUCHED = "paid_touched"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QuickSalesDialog(QDialog):
|
|
|
|
|
|
def __init__(self, ctx, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
self.products = [p for p in ctx.vault.doc.products if not p.archived]
|
|
|
|
|
|
|
|
|
|
|
|
self.setWindowTitle("Быстрый ввод продаж")
|
|
|
|
|
|
self.setMinimumSize(1120, 620)
|
|
|
|
|
|
self.resize(1320, 700)
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 16, 20, 16)
|
|
|
|
|
|
layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(w.heading("Быстрый ввод продаж"))
|
|
|
|
|
|
layout.addLayout(self._period_row())
|
|
|
|
|
|
|
|
|
|
|
|
self.hint = w.label("", "dim")
|
|
|
|
|
|
self.hint.setWordWrap(True)
|
|
|
|
|
|
layout.addWidget(self.hint)
|
|
|
|
|
|
|
|
|
|
|
|
self.table = QTableWidget(0, len(HEADERS))
|
|
|
|
|
|
self.table.setHorizontalHeaderLabels(HEADERS)
|
|
|
|
|
|
self.table.verticalHeader().setVisible(False)
|
|
|
|
|
|
# Строки целиком из полей ввода: по содержимому Qt делает их слишком
|
|
|
|
|
|
# низкими, и текст обрезается.
|
|
|
|
|
|
self.table.verticalHeader().setDefaultSectionSize(36)
|
|
|
|
|
|
|
|
|
|
|
|
header = self.table.horizontalHeader()
|
|
|
|
|
|
header.setSectionResizeMode(COL_PRODUCT, QHeaderView.Stretch)
|
|
|
|
|
|
# Ширины заданы руками: ResizeToContents схлопывает пустой выпадающий
|
|
|
|
|
|
# список до нечитаемого огрызка.
|
|
|
|
|
|
for column, width in (
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
(COL_DATE, 110), (COL_KIND, 195), (COL_WHO, 130), (COL_QTY, 80),
|
|
|
|
|
|
(COL_UOM, 95), (COL_PRICE, 110), (COL_PAID, 110), (COL_TIP, 105),
|
|
|
|
|
|
(COL_TOTAL, 100),
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
):
|
|
|
|
|
|
header.setSectionResizeMode(column, QHeaderView.Interactive)
|
|
|
|
|
|
self.table.setColumnWidth(column, width)
|
|
|
|
|
|
layout.addWidget(self.table, 1)
|
|
|
|
|
|
|
|
|
|
|
|
self.total_label = w.label("", "h2")
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(
|
|
|
|
|
|
w.button("+ Строка (Enter)", on_click=self.add_row),
|
|
|
|
|
|
w.button("Дублировать (Ctrl+D)", on_click=self.duplicate_row),
|
|
|
|
|
|
w.button("Удалить строку", role="danger", on_click=self.remove_row),
|
|
|
|
|
|
None,
|
|
|
|
|
|
self.total_label,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(
|
|
|
|
|
|
w.label("Записать всё — Ctrl+Enter", "dim"),
|
|
|
|
|
|
None,
|
|
|
|
|
|
w.button("Отмена", on_click=self.reject),
|
|
|
|
|
|
w.button("Записать всё", "primary", self.accept),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Enter в диалоге по умолчанию нажимает кнопку — здесь он нужен для
|
|
|
|
|
|
# добавления строки, поэтому автонажатие отключаем у всех кнопок.
|
|
|
|
|
|
for widget in self.findChildren(QPushButton):
|
|
|
|
|
|
widget.setAutoDefault(False)
|
|
|
|
|
|
widget.setDefault(False)
|
|
|
|
|
|
|
|
|
|
|
|
QShortcut(QKeySequence(Qt.Key_Return), self, activated=self.add_row)
|
|
|
|
|
|
QShortcut(QKeySequence(Qt.Key_Enter), self, activated=self.add_row)
|
|
|
|
|
|
QShortcut(QKeySequence("Ctrl+D"), self, activated=self.duplicate_row)
|
|
|
|
|
|
QShortcut(QKeySequence("Ctrl+Return"), self, activated=self.accept)
|
|
|
|
|
|
QShortcut(QKeySequence("Ctrl+Enter"), self, activated=self.accept)
|
|
|
|
|
|
|
|
|
|
|
|
self._update_hint()
|
|
|
|
|
|
if self.products:
|
|
|
|
|
|
self.add_row()
|
|
|
|
|
|
|
|
|
|
|
|
# --- период ---
|
|
|
|
|
|
|
|
|
|
|
|
def _period_row(self):
|
|
|
|
|
|
today = date.today()
|
|
|
|
|
|
self.period_from = w.DateInput(today - timedelta(days=DEFAULT_PERIOD_DAYS))
|
|
|
|
|
|
self.period_to = w.DateInput(today)
|
|
|
|
|
|
self.period_from.dateChanged.connect(self._update_hint)
|
|
|
|
|
|
self.period_to.dateChanged.connect(self._update_hint)
|
|
|
|
|
|
|
|
|
|
|
|
holder = QVBoxLayout()
|
|
|
|
|
|
holder.addWidget(
|
|
|
|
|
|
w.row(
|
|
|
|
|
|
w.label("Вношу данные за период:"),
|
|
|
|
|
|
self.period_from,
|
|
|
|
|
|
w.label("—"),
|
|
|
|
|
|
self.period_to,
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return holder
|
|
|
|
|
|
|
|
|
|
|
|
def _update_hint(self) -> None:
|
|
|
|
|
|
start, end = self.period_from.get_date(), self.period_to.get_date()
|
|
|
|
|
|
if end < start:
|
|
|
|
|
|
self.hint.setText("Конец периода раньше начала — поправь даты.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Показываем, что за период уже записано: дешёвая страховка от того,
|
|
|
|
|
|
# чтобы не внести один и тот же месяц дважды.
|
|
|
|
|
|
existing = [s for s in self.ctx.vault.doc.sales if start <= s.date <= end]
|
|
|
|
|
|
already = (
|
|
|
|
|
|
f"За этот период уже записано продаж: {len(existing)}."
|
|
|
|
|
|
if existing
|
|
|
|
|
|
else "За этот период продаж ещё нет."
|
|
|
|
|
|
)
|
|
|
|
|
|
self.hint.setText(
|
|
|
|
|
|
f"{already} Дата новой строки наследуется от предыдущей — "
|
|
|
|
|
|
"меняй её, когда переходишь к следующему дню."
|
|
|
|
|
|
)
|
|
|
|
|
|
self._recalc()
|
|
|
|
|
|
|
|
|
|
|
|
# --- строки ---
|
|
|
|
|
|
|
|
|
|
|
|
def _last_row_state(self) -> dict:
|
|
|
|
|
|
r = self.table.rowCount() - 1
|
|
|
|
|
|
if r < 0:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"date": self.period_from.get_date(),
|
|
|
|
|
|
"kind": SALE_KINDS[0],
|
|
|
|
|
|
"who": "",
|
|
|
|
|
|
"product_id": None,
|
|
|
|
|
|
"qty": 1,
|
|
|
|
|
|
"uom": "",
|
|
|
|
|
|
"price": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
"date": self.table.cellWidget(r, COL_DATE).get_date(),
|
|
|
|
|
|
"kind": self.table.cellWidget(r, COL_KIND).currentData(),
|
|
|
|
|
|
"who": self.table.cellWidget(r, COL_WHO).currentText(),
|
|
|
|
|
|
"product_id": None,
|
|
|
|
|
|
"qty": 1,
|
|
|
|
|
|
"uom": "",
|
|
|
|
|
|
"price": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def add_row(self, state: dict | None = None) -> None:
|
|
|
|
|
|
if not self.products:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
state = state or self._last_row_state()
|
|
|
|
|
|
r = self.table.rowCount()
|
|
|
|
|
|
self.table.insertRow(r)
|
|
|
|
|
|
|
|
|
|
|
|
date_widget = w.DateInput(state["date"])
|
|
|
|
|
|
date_widget.dateChanged.connect(self._recalc)
|
|
|
|
|
|
self.table.setCellWidget(r, COL_DATE, date_widget)
|
|
|
|
|
|
|
|
|
|
|
|
kind = QComboBox()
|
|
|
|
|
|
for value in SALE_KINDS:
|
|
|
|
|
|
kind.addItem(SALE_KIND_LABELS[value], value)
|
|
|
|
|
|
kind.setCurrentIndex(SALE_KINDS.index(state["kind"]))
|
|
|
|
|
|
kind.currentIndexChanged.connect(lambda _, rr=r: self._on_kind_changed(rr))
|
|
|
|
|
|
self.table.setCellWidget(r, COL_KIND, kind)
|
|
|
|
|
|
|
|
|
|
|
|
who = QComboBox()
|
|
|
|
|
|
who.setEditable(True)
|
|
|
|
|
|
who.setInsertPolicy(QComboBox.NoInsert)
|
|
|
|
|
|
who.addItem("")
|
|
|
|
|
|
for cp in sorted(self.ctx.vault.doc.counterparties, key=lambda c: c.name.lower()):
|
|
|
|
|
|
who.addItem(cp.name)
|
|
|
|
|
|
who.setCurrentText(state["who"])
|
|
|
|
|
|
who.setToolTip("Незнакомое имя будет заведено как новый контрагент.")
|
|
|
|
|
|
self.table.setCellWidget(r, COL_WHO, who)
|
|
|
|
|
|
|
|
|
|
|
|
product = w.ProductCombo(self.products)
|
|
|
|
|
|
if state["product_id"]:
|
|
|
|
|
|
product.select_product(state["product_id"])
|
|
|
|
|
|
product.currentIndexChanged.connect(lambda _, rr=r: self._on_product_changed(rr))
|
|
|
|
|
|
self.table.setCellWidget(r, COL_PRODUCT, product)
|
|
|
|
|
|
|
|
|
|
|
|
qty = w.QtySpin()
|
|
|
|
|
|
qty.set_decimal(state["qty"])
|
|
|
|
|
|
qty.valueChanged.connect(lambda _, rr=r: self._on_amount_changed(rr))
|
|
|
|
|
|
self.table.setCellWidget(r, COL_QTY, qty)
|
|
|
|
|
|
|
|
|
|
|
|
uom = QComboBox()
|
|
|
|
|
|
self.table.setCellWidget(r, COL_UOM, uom)
|
|
|
|
|
|
self._fill_uoms(r, product.current_product_id(), state["uom"])
|
|
|
|
|
|
uom.currentIndexChanged.connect(lambda _, rr=r: self._on_uom_changed(rr))
|
|
|
|
|
|
|
|
|
|
|
|
price = w.MoneySpin(self.ctx.currency)
|
|
|
|
|
|
self.table.setCellWidget(r, COL_PRICE, price)
|
|
|
|
|
|
price.valueChanged.connect(lambda _, rr=r: self._on_amount_changed(rr))
|
|
|
|
|
|
|
|
|
|
|
|
paid = w.MoneySpin(self.ctx.currency)
|
|
|
|
|
|
paid.setProperty(TOUCHED, False)
|
|
|
|
|
|
paid.valueChanged.connect(lambda _, rr=r: self._on_paid_edited(rr))
|
|
|
|
|
|
self.table.setCellWidget(r, COL_PAID, paid)
|
|
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip = w.MoneySpin(self.ctx.currency)
|
|
|
|
|
|
tip.set_decimal(state.get("tip") or 0)
|
|
|
|
|
|
tip.valueChanged.connect(self._recalc)
|
|
|
|
|
|
self.table.setCellWidget(r, COL_TIP, tip)
|
|
|
|
|
|
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
self.table.setItem(r, COL_TOTAL, w.text_item(""))
|
|
|
|
|
|
|
|
|
|
|
|
self._apply_price(r)
|
|
|
|
|
|
self._on_kind_changed(r)
|
|
|
|
|
|
self.table.setCurrentCell(r, COL_PRODUCT)
|
|
|
|
|
|
product.setFocus()
|
|
|
|
|
|
|
|
|
|
|
|
def duplicate_row(self) -> None:
|
|
|
|
|
|
r = self.table.currentRow()
|
|
|
|
|
|
if r < 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.add_row(
|
|
|
|
|
|
{
|
|
|
|
|
|
"date": self.table.cellWidget(r, COL_DATE).get_date(),
|
|
|
|
|
|
"kind": self.table.cellWidget(r, COL_KIND).currentData(),
|
|
|
|
|
|
"who": self.table.cellWidget(r, COL_WHO).currentText(),
|
|
|
|
|
|
"product_id": self.table.cellWidget(r, COL_PRODUCT).current_product_id(),
|
|
|
|
|
|
"qty": self.table.cellWidget(r, COL_QTY).value_decimal(),
|
|
|
|
|
|
"uom": self.table.cellWidget(r, COL_UOM).currentText(),
|
|
|
|
|
|
"price": self.table.cellWidget(r, COL_PRICE).value_decimal(),
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
"tip": self.table.cellWidget(r, COL_TIP).value_decimal(),
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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 remove_row(self) -> None:
|
|
|
|
|
|
r = self.table.currentRow()
|
|
|
|
|
|
if r >= 0:
|
|
|
|
|
|
self.table.removeRow(r)
|
|
|
|
|
|
self._recalc()
|
|
|
|
|
|
|
|
|
|
|
|
# --- реакции ---
|
|
|
|
|
|
|
|
|
|
|
|
def _product(self, product_id):
|
|
|
|
|
|
return next((p for p in self.products if p.id == product_id), None)
|
|
|
|
|
|
|
|
|
|
|
|
def _fill_uoms(self, r: int, product_id, selected: str = "") -> None:
|
|
|
|
|
|
combo = self.table.cellWidget(r, COL_UOM)
|
|
|
|
|
|
product = self._product(product_id)
|
|
|
|
|
|
names = product.uom_names() if product else []
|
|
|
|
|
|
|
|
|
|
|
|
combo.blockSignals(True)
|
|
|
|
|
|
combo.clear()
|
|
|
|
|
|
combo.addItems(names)
|
|
|
|
|
|
if selected and selected in names:
|
|
|
|
|
|
combo.setCurrentText(selected)
|
|
|
|
|
|
combo.blockSignals(False)
|
|
|
|
|
|
combo.setEnabled(len(names) > 1)
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_price(self, r: int) -> None:
|
|
|
|
|
|
product = self._product(self.table.cellWidget(r, COL_PRODUCT).current_product_id())
|
|
|
|
|
|
if product is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
kind = self.table.cellWidget(r, COL_KIND).currentData()
|
|
|
|
|
|
uom = self.table.cellWidget(r, COL_UOM).currentText()
|
|
|
|
|
|
self.table.cellWidget(r, COL_PRICE).set_decimal(price_source_for(kind)(product, uom))
|
|
|
|
|
|
|
|
|
|
|
|
def _on_product_changed(self, r: int) -> None:
|
|
|
|
|
|
self._fill_uoms(r, self.table.cellWidget(r, COL_PRODUCT).current_product_id())
|
|
|
|
|
|
self._apply_price(r)
|
|
|
|
|
|
self._on_amount_changed(r)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_uom_changed(self, r: int) -> None:
|
|
|
|
|
|
self._apply_price(r)
|
|
|
|
|
|
self._on_amount_changed(r)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_kind_changed(self, r: int) -> None:
|
|
|
|
|
|
kind = self.table.cellWidget(r, COL_KIND).currentData()
|
|
|
|
|
|
consumption = kind in CONSUMPTION_KINDS
|
|
|
|
|
|
|
|
|
|
|
|
self._apply_price(r)
|
|
|
|
|
|
paid = self.table.cellWidget(r, COL_PAID)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip = self.table.cellWidget(r, COL_TIP)
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
paid.setEnabled(not consumption)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip.setEnabled(not consumption)
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
self.table.cellWidget(r, COL_PRICE).setEnabled(not consumption)
|
|
|
|
|
|
if consumption:
|
|
|
|
|
|
paid.setProperty(TOUCHED, False)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip.set_decimal(0)
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
self._on_amount_changed(r)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_amount_changed(self, r: int) -> None:
|
|
|
|
|
|
paid = self.table.cellWidget(r, COL_PAID)
|
|
|
|
|
|
# Полная оплата — самый частый случай. Если её правили руками, больше
|
|
|
|
|
|
# не трогаем: значит, там долг.
|
|
|
|
|
|
if not paid.property(TOUCHED):
|
|
|
|
|
|
paid.blockSignals(True)
|
|
|
|
|
|
paid.set_decimal(self._row_total(r))
|
|
|
|
|
|
paid.blockSignals(False)
|
|
|
|
|
|
self._recalc()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_paid_edited(self, r: int) -> None:
|
|
|
|
|
|
self.table.cellWidget(r, COL_PAID).setProperty(TOUCHED, True)
|
|
|
|
|
|
self._recalc()
|
|
|
|
|
|
|
|
|
|
|
|
def _row_total(self, r: int) -> Decimal:
|
|
|
|
|
|
kind = self.table.cellWidget(r, COL_KIND).currentData()
|
|
|
|
|
|
if kind in CONSUMPTION_KINDS:
|
|
|
|
|
|
return m.ZERO
|
|
|
|
|
|
qty = self.table.cellWidget(r, COL_QTY).value_decimal()
|
|
|
|
|
|
price = self.table.cellWidget(r, COL_PRICE).value_decimal()
|
|
|
|
|
|
return m.money(qty * price)
|
|
|
|
|
|
|
|
|
|
|
|
def _recalc(self) -> None:
|
|
|
|
|
|
start, end = self.period_from.get_date(), self.period_to.get_date()
|
|
|
|
|
|
total = m.ZERO
|
|
|
|
|
|
outside = 0
|
|
|
|
|
|
|
|
|
|
|
|
for r in range(self.table.rowCount()):
|
|
|
|
|
|
row_total = self._row_total(r)
|
|
|
|
|
|
total += row_total
|
|
|
|
|
|
|
|
|
|
|
|
item = self.table.item(r, COL_TOTAL)
|
|
|
|
|
|
if item is not None:
|
|
|
|
|
|
item.setText(m.fmt_money(row_total, self.ctx.currency))
|
|
|
|
|
|
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
|
|
|
|
|
|
|
|
|
|
# Дата вне периода — не ошибка, но стоит показать: скорее всего опечатка.
|
|
|
|
|
|
row_date = self.table.cellWidget(r, COL_DATE).get_date()
|
|
|
|
|
|
off = not (start <= row_date <= end)
|
|
|
|
|
|
outside += off
|
|
|
|
|
|
self.table.cellWidget(r, COL_DATE).setStyleSheet(
|
|
|
|
|
|
f"color: {theme.WARN};" if off else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tips = m.money(
|
|
|
|
|
|
sum(
|
|
|
|
|
|
(
|
|
|
|
|
|
self.table.cellWidget(r, COL_TIP).value_decimal()
|
|
|
|
|
|
for r in range(self.table.rowCount())
|
|
|
|
|
|
if self.table.cellWidget(r, COL_TIP) is not None
|
|
|
|
|
|
),
|
|
|
|
|
|
m.ZERO,
|
|
|
|
|
|
)
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
)
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
parts = [
|
|
|
|
|
|
f"Строк: {self.table.rowCount()}",
|
|
|
|
|
|
f"на {m.fmt_money(total, self.ctx.currency)}",
|
|
|
|
|
|
]
|
|
|
|
|
|
if tips > 0:
|
|
|
|
|
|
parts.append(f"чаевых {m.fmt_money(tips, self.ctx.currency)}")
|
|
|
|
|
|
if outside:
|
|
|
|
|
|
parts.append(f"вне периода: {outside}")
|
|
|
|
|
|
self.total_label.setText(" • ".join(parts))
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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 collect(self) -> list[dict]:
|
|
|
|
|
|
"""Собрать строки в описания продаж. Пустые строки просто пропускаются."""
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for r in range(self.table.rowCount()):
|
|
|
|
|
|
product_id = self.table.cellWidget(r, COL_PRODUCT).current_product_id()
|
|
|
|
|
|
qty = self.table.cellWidget(r, COL_QTY).value_decimal()
|
|
|
|
|
|
if not product_id or qty <= 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
kind = self.table.cellWidget(r, COL_KIND).currentData()
|
|
|
|
|
|
consumption = kind in CONSUMPTION_KINDS
|
|
|
|
|
|
price = m.ZERO if consumption else self.table.cellWidget(r, COL_PRICE).value_decimal()
|
|
|
|
|
|
paid = m.ZERO if consumption else self.table.cellWidget(r, COL_PAID).value_decimal()
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip = m.ZERO if consumption else self.table.cellWidget(r, COL_TIP).value_decimal()
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
|
|
|
|
|
|
result.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"date": self.table.cellWidget(r, COL_DATE).get_date(),
|
|
|
|
|
|
"kind": kind,
|
|
|
|
|
|
"who": self.table.cellWidget(r, COL_WHO).currentText().strip(),
|
|
|
|
|
|
"line": {
|
|
|
|
|
|
"product_id": product_id,
|
|
|
|
|
|
"qty": qty,
|
|
|
|
|
|
"uom": self.table.cellWidget(r, COL_UOM).currentText(),
|
|
|
|
|
|
"unit_price": price,
|
|
|
|
|
|
},
|
|
|
|
|
|
"paid": paid,
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
"tip": tip,
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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 result
|
|
|
|
|
|
|
|
|
|
|
|
def accept(self) -> None:
|
|
|
|
|
|
rows = self.collect()
|
|
|
|
|
|
if not rows:
|
|
|
|
|
|
QMessageBox.information(self, "Пусто", "Заполни хотя бы одну строку.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
created = write_sales(self.ctx.vault, rows)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.created = created
|
|
|
|
|
|
super().accept()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_sales(vault, rows: list[dict]) -> int:
|
|
|
|
|
|
"""Записать собранные строки как отдельные продажи.
|
|
|
|
|
|
|
|
|
|
|
|
Незнакомое имя в колонке «Кому» заводится как новый контрагент: при вводе
|
|
|
|
|
|
истории останавливаться и создавать людей вручную — лишняя морока.
|
|
|
|
|
|
"""
|
|
|
|
|
|
known = {c.name.casefold(): c.id for c in vault.doc.counterparties}
|
|
|
|
|
|
created = 0
|
|
|
|
|
|
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
counterparty_id = None
|
|
|
|
|
|
name = row["who"]
|
|
|
|
|
|
if name:
|
|
|
|
|
|
key = name.casefold()
|
|
|
|
|
|
if key not in known:
|
|
|
|
|
|
known[key] = journal.create_counterparty(vault, name).id
|
|
|
|
|
|
counterparty_id = known[key]
|
|
|
|
|
|
|
|
|
|
|
|
journal.create_sale(
|
|
|
|
|
|
vault,
|
|
|
|
|
|
row["date"],
|
|
|
|
|
|
row["kind"],
|
|
|
|
|
|
[row["line"]],
|
|
|
|
|
|
counterparty_id=counterparty_id,
|
|
|
|
|
|
paid_amount=row["paid"],
|
Чаевые
Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не
внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела
бы закрытой, когда за булки ещё должны.
Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые
сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе
поле дёргалось бы посреди набора. То же при приёме оплаты по долгу:
заплатили больше, чем оставалось, — излишек становится чаевыми, а не
теряется, как было раньше.
В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же
рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят
в выручку и не создают долга. Продажа, списанная с нескольких партий,
делит чаевые между ними пропорционально выручке.
По съеденному и подаренному чаевых не бывает — поле отключается.
Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления
этой фичи не заметит.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
|
|
|
|
tip=row.get("tip"),
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
)
|
|
|
|
|
|
created += 1
|
|
|
|
|
|
|
|
|
|
|
|
return created
|