certflow.views.sale_plan_query_view 源代码

"""销售计划查询视图(任务 2.1 第二域:QueryView 迁移 BaseQueryView)

``SalePlanQueryView`` 是 ``BaseQueryView`` 的「宿主驱动」渲染器:它复用 ``BaseQueryView``
的 ``EnhancedTable`` + 分页栏,但**不**使用 ``BaseQueryView`` 自带的 ``FilterBar`` /
搜索/刷新按钮——这些高级筛选(快查预设、特殊值、视图列、保存筛选)由宿主 ``QueryView``
自己的 ``FilterBar`` 承担,并通过 ``run_query`` 注入条件与翻页控制。

渲染保真:移植 ``QueryView.display_results`` 的证书字段回退 / 计划日期归一 / 生产状态
图标逻辑,使迁移后显示与既有销售计划查询完全一致(纯收敛,行为不变)。

首屏加载延迟到 ``showEvent``(由宿主决定),避免构造期在无事件循环环境阻塞。
"""

from __future__ import annotations

from typing import Any

from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
    QComboBox,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QPushButton,
    QSpinBox,
    QTableWidgetItem,
    QVBoxLayout,
)

from certflow.utils.date_utils import normalize_plan_date
from certflow.views.bases.base_query_view import BaseQueryView
from certflow.views.bases.datasources.sale_plan import SalePlanDataSource
from certflow.widgets.enhanced_table import EnhancedTable

# 纯展示字段(非数据库列,不可用于排序)
_DISPLAY_ONLY_FIELDS = frozenset({"progress_display", "ready_display"})

# 证书字段回退映射(cert_* 为空时回退合同字段)
_CERT_FALLBACK = {
    "cert_product_name": "product_name",
    "cert_product_model": "product_model",
    "cert_product_spec": "product_spec",
}

# 仅存于关联 Certificate 的字段
_CERT_ONLY_FIELDS = frozenset(
    {
        "print_status",
        "engrave_status",
        "certificate_no",
        "print_time",
        "engraved_time",
        "is_engraved",
    }
)


[文档] class SalePlanQueryView(BaseQueryView): """销售计划结果区:BaseQueryView 宿主驱动渲染器。 信号: selection_changed:选中行变化(供宿主更新「选中 N 条」)。 """ selection_changed = Signal() def __init__( self, session: Any = None, db_manager: Any = None, parent: Any = None, ) -> None: super().__init__(session=session, db_manager=db_manager, parent=parent) self._view_columns: list[dict] = [] self._records: list[Any] = [] # ============================================================ # UI:仅保留表格 + 分页(隐藏 BaseQueryView 自带筛选条/搜索/刷新按钮) # ============================================================ def _setup_ui(self) -> None: layout = QVBoxLayout(self) self.table = EnhancedTable() self.table.setColumnCount(0) self.table.sort_requested.connect(self._on_sort_requested) self.table.itemSelectionChanged.connect(self.selection_changed) layout.addWidget(self.table, stretch=1) pg = QHBoxLayout() self.prev_btn = QPushButton("上一页") self.prev_btn.clicked.connect(self._prev_page) pg.addWidget(self.prev_btn) self.page_spin = QSpinBox() self.page_spin.setMinimum(1) self.page_spin.valueChanged.connect(self._on_page_spin) pg.addWidget(self.page_spin) self.page_label = QLabel("/ 1 页") pg.addWidget(self.page_label) self.next_btn = QPushButton("下一页") self.next_btn.clicked.connect(self._next_page) pg.addWidget(self.next_btn) pg.addWidget(QLabel("每页")) self.page_size_combo = QComboBox() self.page_size_combo.addItems(["20", "50", "100", "200"]) self.page_size_combo.setCurrentText("50") self.page_size_combo.currentTextChanged.connect(self._on_page_size_changed) pg.addWidget(self.page_size_combo) self.total_label = QLabel("共 0 条") pg.addWidget(self.total_label) pg.addStretch() layout.addLayout(pg) # ============================================================ # 数据源装配(宿主驱动,构造期不触发查询) # ============================================================
[文档] def set_controller(self, controller: Any) -> None: """装配 QueryController 数据源(不触发查询;首屏由宿主 showEvent 触发)。""" self.data_source = SalePlanDataSource(controller) columns = self.data_source.get_columns() self.table.setColumnCount(len(columns)) self.table.setHorizontalHeaderLabels([h for h, _ in columns]) for idx, (_, field) in enumerate(columns): self.table.set_header_field(idx, field)
[文档] def set_view_columns(self, columns: list[dict]) -> None: """按视图模板列配置表头(含展示字段不可排序处理),并恢复列状态/排序指示。""" self._view_columns = list(columns) headers = [col.get("label", col.get("field", "")) for col in columns] self.table.setColumnCount(len(headers)) self.table.setHorizontalHeaderLabels(headers) for col, col_def in enumerate(columns): field = col_def.get("field", "") if field in _DISPLAY_ONLY_FIELDS: field = "" self.table.set_header_field(col, field) header = self.table.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
# ============================================================ # 排序(覆盖基类:SalePlanQueryView 无 filter_bar,不调用 _do_search) # ============================================================ def _on_sort_requested(self, field: str, desc: bool) -> None: """列头点击排序:发射信号由宿主处理(不调用基类 _do_search)。""" # 由宿主 QueryView 监听 sort_requested 信号来处理排序 pass # ============================================================ # 查询(由宿主注入条件 / 排序 / 翻页) # ============================================================
[文档] def run_query( self, conditions: dict, or_filters: Any = None, order_by: str | None = None, order_desc: bool = False, page: int = 1, page_size: int = 50, reset_page: bool = True, ) -> None: """宿主驱动查询:经 SalePlanDataSource 走服务端分页/排序。""" if self.data_source is None: return None if reset_page: page = 1 self.current_page = page self.page_size = page_size result = self.data_source.query( conditions, self.current_page, self.page_size, order_by, order_desc, or_filters, ) self.display( result["records"], result["total"], result["total_pages"], result["page"], ) return result
[文档] def display( self, records: list[Any], total: int, total_pages: int, page: int, columns: list[dict] | None = None, ) -> None: """填充表格并更新分页(渲染保真见 ``_render``)。""" if columns is not None: self._view_columns = list(columns) self._records = records self._total = total self._total_pages = total_pages self.current_page = page self._render(records) self._update_pagination()
# ============================================================ # 保真渲染(移植自 QueryView.display_results) # ============================================================ @staticmethod def _extract_cert_values(record: Any, cert_only_fields: frozenset[str]) -> dict[str, Any]: cert_values: dict[str, Any] = {} certs = getattr(record, "certificates", None) if not certs: return cert_values for cert in certs: cert_dict = cert.__dict__ for field_name in cert_only_fields: if field_name not in cert_values: v = cert_dict.get(field_name) if v is not None: cert_values[field_name] = v return cert_values @staticmethod def _resolve_cell_value( col: int, field_name: str, row_data: dict[str, Any], cert_values: dict[str, Any], cert_col_indices: set[int], fallback_cols: dict[int, str], plan_date_col: int | None, ) -> str: value = cert_values.get(field_name) if col in cert_col_indices else row_data.get(field_name) if col in fallback_cols and not value: value = row_data.get(fallback_cols[col], "") if col == plan_date_col: value = normalize_plan_date(value) or "" if isinstance(value, bool): return str(value) return str(value or "") def _render(self, records: list[Any]) -> None: columns = self._view_columns col_field_names = [col_def["field"] for col_def in columns] cert_col_indices = {i for i, fn in enumerate(col_field_names) if fn in _CERT_ONLY_FIELDS} fallback_cols = { i: _CERT_FALLBACK[fn] for i, fn in enumerate(col_field_names) if fn in _CERT_FALLBACK } plan_date_col = next((i for i, fn in enumerate(col_field_names) if fn == "plan_date"), None) status_col = next( (i for i, fn in enumerate(col_field_names) if fn == "production_status"), None ) show_raw_tooltip = getattr(self, "_show_raw_tooltip", True) show_icon = getattr(self, "_show_icon", True) self.table.setUpdatesEnabled(False) header = self.table.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeMode.Fixed) self.table.setRowCount(len(records)) for row_idx, record in enumerate(records): row_data = {k: v for k, v in record.__dict__.items() if not k.startswith("_")} for fn in col_field_names: if fn not in row_data and fn not in cert_col_indices: row_data[fn] = getattr(record, fn, None) if "product_code" in col_field_names: display = getattr(record, "product_code_display", None) if display: row_data["product_code"] = display cert_values = ( self._extract_cert_values(record, _CERT_ONLY_FIELDS) if cert_col_indices else {} ) for col_idx, field_name in enumerate(col_field_names): text = self._resolve_cell_value( col_idx, field_name, row_data, cert_values, cert_col_indices, fallback_cols, plan_date_col, ) item = QTableWidgetItem(text) if ( status_col is not None and col_idx == status_col and show_raw_tooltip and getattr(record, "execution_status", None) ): raw_exec = str(getattr(record, "execution_status", "")) if raw_exec and raw_exec != getattr(record, "production_status", ""): item.setToolTip(f"原始执行情况: {raw_exec}") if show_icon: item.setText(f"📋 {text}") self.table.setItem(row_idx, col_idx, item) header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.table.setUpdatesEnabled(True) # ============================================================ # 选中 / 记录访问(供宿主上下文菜单、批量动作、打印跳转) # ============================================================ @property def records(self) -> list[Any]: return self._records
[文档] def selected_rows(self) -> set[int]: return {i.row() for i in self.table.selectedIndexes()}
[文档] def selected_records(self) -> list[Any]: rows = self.selected_rows() return [self._records[r] for r in rows if r < len(self._records)]
[文档] def selected_sale_plan_ids(self) -> list[int]: return [rec.id for rec in self.selected_records() if getattr(rec, "id", None) is not None]
# ============================================================ # 列状态(委托 EnhancedTable;键由宿主按视图管理) # ============================================================
[文档] def save_column_state(self) -> Any: return self.table.save_state()
[文档] def restore_column_state(self, state: Any) -> None: if state: self.table.restore_state(state)