192 lines
6.4 KiB
Python
192 lines
6.4 KiB
Python
|
|
"""Долги: кто и сколько должен за взятые булки."""
|
|||
|
|
|
|||
|
|
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)}"
|
|||
|
|
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()
|