food-market/app/ui/page_debts.py
Claude efce552e2b Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.

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-22 21:25:39 +03:00

193 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Долги: кто и сколько должен за взятые булки."""
from __future__ import annotations
from decimal import Decimal
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor
from PySide6.QtWidgets import (
QDialog,
QFormLayout,
QHeaderView,
QLineEdit,
QMessageBox,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from .. import journal
from .. import money as m
from ..models import SALE_KIND_LABELS
from . import theme
from . import widgets as w
ROLE_SALE_ID = Qt.UserRole + 2
class PaymentDialog(QDialog):
def __init__(self, suggested: Decimal, currency: str, parent=None):
super().__init__(parent)
self.setWindowTitle("Принять оплату")
self.setMinimumWidth(340)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 16, 20, 16)
layout.setSpacing(10)
form = QFormLayout()
form.setSpacing(8)
self.date = w.DateInput()
form.addRow("Дата", self.date)
self.amount = w.MoneySpin(currency)
self.amount.set_decimal(suggested)
self.amount.selectAll()
form.addRow("Сумма", self.amount)
self.note = QLineEdit()
form.addRow("Заметка", self.note)
layout.addLayout(form)
layout.addWidget(w.label(f"Долг по этой продаже: {m.fmt_money(suggested, currency)}", "dim"))
layout.addWidget(
w.row(None, w.button("Отмена", on_click=self.reject), w.button("Принять", "primary", self.accept))
)
self.amount.setFocus()
def ask_payment(parent, suggested: Decimal, currency: str) -> Decimal | None:
"""Спросить сумму оплаты. Возвращает None, если отменили."""
dialog = PaymentDialog(suggested, currency, parent)
if dialog.exec() != QDialog.Accepted:
return None
return dialog.amount.value_decimal()
class DebtsPage(QWidget):
"""Дерево: человек → его неоплаченные продажи."""
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("Долги"))
self.total_label = w.label("", "h2")
layout.addWidget(
w.row(
w.button("Принять оплату", "primary", self.take_payment),
w.button("Погасить целиком", on_click=self.settle_full),
None,
self.total_label,
)
)
self.tree = QTreeWidget()
self.tree.setHeaderLabels(["Кто / что", "Дата", "Сумма", "Оплачено", "Долг"])
self.tree.setAlternatingRowColors(True)
self.tree.setRootIsDecorated(True)
self.tree.header().setStretchLastSection(False)
self.tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
self.tree.itemDoubleClicked.connect(self.take_payment)
layout.addWidget(self.tree, 1)
self.empty = w.label("Никто ничего не должен.", "dim")
layout.addWidget(self.empty)
# --- отрисовка ---
def refresh(self) -> None:
doc = self.ctx.vault.doc
currency = self.ctx.currency
debtors = self.ctx.report.debtors
self.tree.clear()
for debtor in debtors:
parent = QTreeWidgetItem(
[debtor.name, "", "", "", m.fmt_money(debtor.debt, currency)]
)
font = parent.font(0)
font.setBold(True)
parent.setFont(0, font)
parent.setFont(4, font)
parent.setForeground(4, QColor(theme.WARN))
parent.setTextAlignment(4, Qt.AlignRight | Qt.AlignVCenter)
for sale in sorted(debtor.sales, key=lambda s: s.date):
what = ", ".join(
f"{doc.product_name(line.product_id)} × {m.fmt_qty(line.qty)} "
f"{line.unit_name(doc.product(line.product_id))}"
for line in sale.lines
)
child = QTreeWidgetItem(
[
f"{SALE_KIND_LABELS.get(sale.kind, sale.kind)}: {what}",
sale.date.strftime("%d.%m.%Y"),
m.fmt_money(sale.total, currency),
m.fmt_money(sale.paid, currency),
m.fmt_money(sale.debt, currency),
]
)
child.setData(0, ROLE_SALE_ID, sale.id)
for column in (2, 3, 4):
child.setTextAlignment(column, Qt.AlignRight | Qt.AlignVCenter)
parent.addChild(child)
self.tree.addTopLevelItem(parent)
parent.setExpanded(True)
for column in range(1, 5):
self.tree.resizeColumnToContents(column)
total = self.ctx.report.summary.receivable
self.total_label.setText(f"Мне должны: {m.fmt_money(total, currency)}")
self.empty.setVisible(not debtors)
self.tree.setVisible(bool(debtors))
# --- действия ---
def _selected_sale(self):
item = self.tree.currentItem()
if item is None:
QMessageBox.information(self, "Не выбрано", "Выбери конкретную продажу.")
return None
sale_id = item.data(0, ROLE_SALE_ID)
if sale_id is None:
QMessageBox.information(
self, "Выбери продажу", "Раскрой человека и выбери, за что он платит."
)
return None
return self.ctx.vault.doc.sale(sale_id)
def take_payment(self) -> None:
sale = self._selected_sale()
if sale is None:
return
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 settle_full(self) -> None:
sale = self._selected_sale()
if sale is None:
return
if sale.debt <= 0:
return
journal.add_sale_payment(self.ctx.vault, sale.id, sale.debt)
self.ctx.changed()