"""Долги: кто и сколько должен за взятые булки.""" 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 ..salesfilter import plural from . import theme from . import widgets as w ROLE_SALE_ID = Qt.UserRole + 2 ROLE_COUNTERPARTY = Qt.UserRole + 3 class PaymentDialog(QDialog): def __init__(self, suggested: Decimal, currency: str, parent=None, what: str = "этой продаже"): 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) self.hint = w.label(f"Долг по {what}: {m.fmt_money(suggested, currency)}", "dim") self.hint.setWordWrap(True) layout.addWidget(self.hint) 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, what: str = "этой продаже") -> Decimal | None: """Спросить сумму оплаты. Возвращает None, если отменили.""" dialog = PaymentDialog(suggested, currency, parent, what) 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, ) ) layout.addWidget( w.label( "Выбери человека — рассчитаешься сразу за все его долги, начиная " "со старых. Выбери отдельную продажу — оплата пойдёт только за неё.", "dim", ) ) 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) # Человека можно выбрать целиком и рассчитаться сразу за всё: # деньги приходят одной суммой, а не по продаже за раз. parent.setData(0, ROLE_COUNTERPARTY, debtor.counterparty_id) 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_debtor(self): """Выбранный человек и его общий долг — либо None, если выбрана продажа.""" item = self.tree.currentItem() if item is None or item.parent() is not None: return None cp_id = item.data(0, ROLE_COUNTERPARTY) return next( (d for d in self.ctx.report.debtors if d.counterparty_id == cp_id), None ) 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 not sale_id: QMessageBox.information( self, "Не выбрано", "Выбери человека или конкретную продажу." ) return None return self.ctx.vault.doc.sale(sale_id) def take_payment(self) -> None: debtor = self._selected_debtor() if debtor is not None: self._pay_debtor(debtor) return 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 _pay_debtor(self, debtor) -> None: """Принять деньги от человека сразу за всё, что за ним числится.""" amount = ask_payment( self, debtor.debt, self.ctx.currency, what=f"{debtor.name} — {len(debtor.sales)} " + plural(len(debtor.sales), "продажа", "продажи", "продаж"), ) if amount is None: return try: applied = journal.pay_off_counterparty( self.ctx.vault, debtor.counterparty_id, amount ) except journal.ValidationError as exc: QMessageBox.warning(self, "Не получилось", str(exc)) return self.ctx.changed() if len(applied) > 1: QMessageBox.information( self, "Готово", f"{m.fmt_money(amount, self.ctx.currency)} разнесено " f"по продажам: {len(applied)}, начиная со старых.", ) def settle_full(self) -> None: debtor = self._selected_debtor() if debtor is not None: journal.pay_off_counterparty( self.ctx.vault, debtor.counterparty_id, debtor.debt ) self.ctx.changed() return sale = self._selected_sale() if sale is None or sale.debt <= 0: return journal.add_sale_payment(self.ctx.vault, sale.id, sale.debt) self.ctx.changed()