"""Сводка: успеваю ли я рассчитаться с пекарней.""" from __future__ import annotations from PySide6.QtWidgets import ( QGridLayout, QProgressBar, QScrollArea, QVBoxLayout, QWidget, ) from .. import ledger from .. import money as m from . import theme from . import widgets as w class DashboardPage(QWidget): def __init__(self, ctx, parent=None): super().__init__(parent) self.ctx = ctx outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QScrollArea.NoFrame) outer.addWidget(scroll) self.body = QWidget() self.layout = QVBoxLayout(self.body) self.layout.setContentsMargins(20, 18, 20, 18) self.layout.setSpacing(14) scroll.setWidget(self.body) def refresh(self) -> None: # Сводка целиком собирается заново: цифр немного, а следить за тем, # какая карточка устарела, вышло бы дороже, чем перерисовать всё. while self.layout.count(): item = self.layout.takeAt(0) widget = item.widget() if widget is not None: # setParent(None) обязателен: deleteLater откладывает удаление # до следующего прохода цикла событий, и до тех пор старые # карточки остаются детьми body. widget.setParent(None) widget.deleteLater() report = self.ctx.report summary = report.summary currency = self.ctx.currency self.layout.addWidget(w.heading("Сводка")) if report.warnings: self.layout.addWidget(w.warning_banner(report.warnings)) self.layout.addWidget(self._metrics(summary, currency)) if summary.next_due is not None: self.layout.addWidget(self._deadline_card(summary.next_due, currency)) self.layout.addWidget(w.label("Открытые партии", "h2")) self.layout.addWidget(self._open_batches(report, currency)) self.layout.addWidget(w.label("Остатки", "h2")) self.layout.addWidget(self._stock(report, currency)) self.layout.addStretch(1) def _metrics(self, summary, currency: str) -> QWidget: holder = QWidget() grid = QGridLayout(holder) grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(10) overdue_hint = ( f"просрочено партий: {summary.overdue_count}" if summary.overdue_count else "всё в срок" ) cards = [ ( "Долг пекарне", m.fmt_money(summary.owed_to_bakery, currency), overdue_hint, theme.DANGER if summary.overdue_count else theme.TEXT, ), ( "Мне должны", m.fmt_money(summary.receivable, currency), "по неоплаченным продажам", theme.WARN if summary.receivable > 0 else theme.TEXT, ), ( "Прибыль", m.fmt_money(summary.profit, currency), f"наценка {m.fmt_money(summary.gross_margin, currency)} " f"− съедено {m.fmt_money(summary.consumed_cost, currency)}", theme.OK if summary.profit > 0 else theme.DANGER if summary.profit < 0 else theme.TEXT, ), ( "Остаток на складе", m.fmt_qty(summary.stock_qty), f"по себестоимости {m.fmt_money(summary.stock_cost, currency)}", theme.TEXT, ), ] for index, (title, value, hint, color) in enumerate(cards): grid.addWidget(w.metric_card(title, value, hint, color), 0, index) grid.setColumnStretch(index, 1) return holder def _deadline_card(self, report, currency: str) -> QWidget: card = w.card() layout = QVBoxLayout(card) layout.setContentsMargins(16, 14, 16, 14) layout.setSpacing(8) color = theme.STATUS_COLORS.get(report.status, theme.ACCENT) when = ( f"осталось {report.days_left} дн." if report.days_left >= 0 else f"просрочена на {-report.days_left} дн." ) layout.addWidget( w.label( f"Ближайший срок — партия {report.label}, до " f"{report.batch.due_date.strftime('%d.%m.%Y')} ({when})", "h2", color, ) ) bar = QProgressBar() bar.setRange(0, 100) bar.setValue(int(min(100, max(0, report.coverage_pct)))) bar.setFormat(f"собрано {report.coverage_pct}% себестоимости") bar.setStyleSheet(f"QProgressBar::chunk {{ background: {color}; }}") layout.addWidget(bar) layout.addWidget( w.label( f"Надо вернуть {m.fmt_money(report.remaining_to_bakery, currency)} • " f"собрано деньгами {m.fmt_money(report.cash_collected, currency)} • " f"осталось собрать {m.fmt_money(report.still_to_collect, currency)}", "dim", ) ) return card def _open_batches(self, report, currency: str) -> QWidget: table = w.table( ["№", "Дата", "Покрыть до", "Осталось", "Долг", "Собрано", "Покрытие", "Статус"], stretch_column=7, ) table.setMaximumHeight(220) rows = [] for item in sorted(report.open_batches, key=lambda r: r.batch.due_date): color = theme.STATUS_COLORS.get(item.status, "") days = ( f"{item.days_left} дн." if item.days_left >= 0 else f"просрочено на {-item.days_left} дн." ) rows.append( [ w.sortable_num_item(str(item.batch.number), item.batch.number), w.text_item(item.batch.date.strftime("%d.%m.%Y")), w.text_item(item.batch.due_date.strftime("%d.%m.%Y")), w.text_item(days, color), w.sortable_num_item( m.fmt_money(item.remaining_to_bakery, currency), item.remaining_to_bakery ), w.sortable_num_item(m.fmt_money(item.cash_collected, currency), item.cash_collected), w.sortable_num_item(f"{item.coverage_pct}%", item.coverage_pct, color), w.text_item(ledger.STATUS_LABELS.get(item.status, ""), color), ] ) w.fill(table, rows) if not rows: return w.label("Открытых партий нет — пекарня может печь дальше.", "dim") return table def _stock(self, report, currency: str) -> QWidget: rows = [row for row in report.stock if row.qty > 0] if not rows: return w.label("Булок не осталось.", "dim") table = w.table(["Товар", "Остаток", "Ед.", "По себестоимости"]) table.setMaximumHeight(220) w.fill( table, [ [ w.text_item(row.name), w.sortable_num_item(m.fmt_qty(row.qty), row.qty), w.text_item(row.unit), w.sortable_num_item(m.fmt_money(row.cost, currency), row.cost), ] for row in rows ], ) return table