food-records/app/ui/page_dashboard.py

206 lines
7.9 KiB
Python
Raw Normal View History

"""Сводка: успеваю ли я рассчитаться с пекарней."""
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