"""Order contracts at risk when filters, summary and table receive the blue layout.""" from __future__ import annotations import os import socket from copy import deepcopy from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtCore import QDate, QPoint, QRect, Qt, QTimer from PySide6.QtTest import QTest from PySide6.QtWidgets import QApplication, QLabel from doctor_workstation.core import PermissionSet from doctor_workstation.ui.pages import patients as patients_module from doctor_workstation.ui.pages.patients import PatientOrdersWorkspace, PatientsPage from doctor_workstation.ui.theme import apply_theme class _Repository: def __init__(self) -> None: self.queries: list[dict[str, Any]] = [] self.rows = [ { "id": 901 + index, "order_no": f"PO2026081000{index + 1}", "patient_name": name, "patient_phone_masked": f"186****482{index}", "recipient_phone": f"1860000482{index}", "prescription_id": 801 + index, "diagnosis_id": 501 + index, "amount": 368 + index, "effective_amount": 368 + index, "prescription_audit_status": 1, "payment_slip_audit_status": 1, "fulfillment_status": 2, "assistant_name": "周医助", "doctor_name": "陈医生(演示)", "creator_name": "周医助", # Source-only values must not fill display fields that are absent. "create_time": "2026-09-05 10:15:00", "pay_orders": [{"id": 2001 + index, "pay_amount": 368 + index}], } for index, name in enumerate(("阿青", "林青", "赵青")) ] self.total = 47 self.summary = { "orders": 47, "amount": 12368.5, "pending": 8, "completed": 9, "rejected": 2, "rejection_rate": 4.3, } def patient_orders(self, **query: Any) -> dict[str, Any]: self.queries.append(query) return { "lists": deepcopy(self.rows), "count": self.total, "extend": { "scope": {"label": "测试部门订单范围"}, "summary": deepcopy(self.summary), }, } def list_patients(self, **_query: Any) -> dict[str, Any]: return { "lists": [{"id": 501, "diagnosis_id": 501, "patient_name": "阿青"}], "count": 1, "extend": {"scope": {"label": "测试患者范围"}}, } def patient_progress(self, **_query: Any) -> dict[str, Any]: return { "lists": [], "count": 0, "extend": {"scope": {"label": "测试面诊范围"}}, } def _settle(application: QApplication) -> None: for _ in range(3): application.processEvents() def _click(widget, application: QApplication) -> None: QTest.mouseClick(widget, Qt.MouseButton.LeftButton) _settle(application) def _rect_in(widget, parent) -> QRect: return QRect(widget.mapTo(parent, QPoint()), widget.size()) def _banner_text(workspace: PatientOrdersWorkspace) -> str: return " ".join(label.text() for label in workspace.banner.findChildren(QLabel)) @pytest.fixture(scope="module") def application() -> QApplication: application = QApplication.instance() or QApplication([]) apply_theme(application) return application @pytest.fixture def workspace_factory(application: QApplication, monkeypatch: pytest.MonkeyPatch): def immediate(function: Any, *, on_success=None, on_error=None, on_finished=None): try: result = function() except Exception as error: if on_error: on_error(error) raise else: if on_success: on_success(result) finally: if on_finished: on_finished() def reject_network(*_args: Any, **_kwargs: Any): pytest.fail("Order visual tests must use only local fixture data") monkeypatch.setattr(socket.socket, "connect", reject_network) monkeypatch.setattr(socket.socket, "connect_ex", reject_network) monkeypatch.setattr(socket, "create_connection", reject_network) monkeypatch.setattr(patients_module, "run_async", immediate) opened = [] def create(*, permissions=("*",), width=1270, height=680, page=False): repository = _Repository() widget = ( PatientsPage(repository, permissions=PermissionSet(list(permissions))) if page else PatientOrdersWorkspace(repository, PermissionSet(list(permissions))) ) opened.append(widget) widget.resize(width, height) widget.show() if not page: widget.refresh() _settle(application) return widget, repository yield create for widget in opened: for timer in widget.findChildren(QTimer): timer.stop() widget.close() widget.deleteLater() _settle(application) def test_filter_defaults_and_reset_keep_edited_dates_but_remove_query_limit( application: QApplication, workspace_factory ) -> None: workspace, repository = workspace_factory() default_query = { "keyword": "", "prescription_audit_status": None, "payment_slip_audit_status": None, "fulfillment_status": None, "start_date": "", "end_date": "", "page_no": 1, "page_size": 15, } assert repository.queries[0] == default_query assert all(query == {**default_query, "page_no": index + 1} for index, query in enumerate(repository.queries)) assert not workspace.use_dates.isChecked() assert not workspace.start_date.isEnabled() and not workspace.end_date.isEnabled() assert workspace.start_date.date() == QDate.currentDate().addDays(-30) assert workspace.end_date.date() == QDate.currentDate() assert all( combo.currentData() is None for combo in (workspace.rx_audit, workspace.pay_audit, workspace.fulfillment) ) before_filters = len(repository.queries) workspace.keyword_edit.setText(" 林医生 ") workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2)) workspace.pay_audit.setCurrentIndex(workspace.pay_audit.findData(0)) workspace.fulfillment.setCurrentIndex(workspace.fulfillment.findData(9)) assert len(repository.queries) == before_filters # Changing status does not auto-submit. _click(workspace.use_dates, application) assert workspace.start_date.isEnabled() and workspace.end_date.isEnabled() start, end = QDate(2026, 7, 3), QDate(2026, 8, 8) workspace.start_date.setDate(start) workspace.end_date.setDate(end) _click(workspace.search_button, application) assert repository.queries[before_filters] == { **default_query, "keyword": "林医生", "prescription_audit_status": 2, "payment_slip_audit_status": 0, "fulfillment_status": 9, "start_date": "2026-07-03", "end_date": "2026-08-08", } workspace.pager.load_more() assert repository.queries[-1]["page_no"] == 2 assert repository.queries[-1]["page_size"] == 15 workspace.keyword_edit.setText("新检索") QTest.keyClick(workspace.keyword_edit, Qt.Key.Key_Return) _settle(application) assert next(query for query in repository.queries if query["keyword"] == "新检索")["page_no"] == 1 workspace.start_date.setDate(end.addDays(1)) before = len(repository.queries) _click(workspace.search_button, application) assert len(repository.queries) == before assert workspace.banner.isVisible() assert "开始日期不能晚于结束日期" in _banner_text(workspace) _click(workspace.reset_button, application) assert repository.queries[-1] == {**default_query, "page_no": repository.queries[-1]["page_no"]} assert workspace.keyword_edit.text() == "" assert not workspace.use_dates.isChecked() assert not workspace.start_date.isEnabled() and not workspace.end_date.isEnabled() assert workspace.start_date.date() == end.addDays(1) assert workspace.end_date.date() == end assert not workspace.banner.isVisible() def test_twelve_columns_keep_source_text_distinct_ids_and_missing_display_fields( application: QApplication, workspace_factory ) -> None: workspace, repository = workspace_factory() row = repository.rows[0] row["order_no"] = "PO-非常长的订单号-20260905-ABCDEFGHIJ" row["patient_name"] = "用于检查省略与提示的较长患者姓名" row["doctor_name"] = "用于检查分行显示与完整提示信息的开方医生(演示)" workspace.refresh() _settle(application) table = workspace.table assert [table.horizontalHeaderItem(column).text() for column in range(table.columnCount())] == [ "订单", "患者", "处方 / 诊单", "有效金额", "处方审核", "支付审核", "履约", "支付单", "归属助理", "开方人", "创建人", "创建时间", ] index = next( index for index in range(table.rowCount()) if table.item(index, 0).data(Qt.ItemDataRole.UserRole)["id"] == 901 ) assert table.item(index, 0).text() == f"{row['order_no']} · #901" assert table.item(index, 1).text() == f"{row['patient_name']} · 186****4820" assert table.item(index, 2).text() == "#801 / #501" assert table.item(index, 9).text() == row["doctor_name"] assert table.item(index, 0).toolTip() == table.item(index, 0).text() assert table.item(index, 1).toolTip() == table.item(index, 1).text() assert table.item(index, 9).toolTip() == row["doctor_name"] assert table.item(index, 7).text() == "—" assert table.item(index, 11).text() == "—" for column in range(12): assert not table.isColumnHidden(column) assert table.item(index, column).data(Qt.ItemDataRole.UserRole) == row assert row["recipient_phone"] not in table.item(index, column).text() def test_sorted_selection_buttons_and_menu_emit_current_order_resource_ids( application: QApplication, workspace_factory ) -> None: workspace, _repository = workspace_factory() table = workspace.table emitted = [] workspace.diagnosis_requested.connect(lambda row: emitted.append(("diagnosis", row))) workspace.detail_requested.connect(lambda row: emitted.append(("detail", row))) workspace.action_requested.connect(lambda key, row: emitted.append((key, row))) table.sortItems(0, Qt.SortOrder.AscendingOrder) first_id = table.item(0, 0).data(Qt.ItemDataRole.UserRole)["id"] table.sortItems(0, Qt.SortOrder.DescendingOrder) _settle(application) assert table.item(0, 0).data(Qt.ItemDataRole.UserRole)["id"] != first_id for index in (0, 2, 1): table.selectRow(index) _settle(application) expected = table.item(index, 0).data(Qt.ItemDataRole.UserRole) workspace.diagnosis_button.click() workspace.detail_button.click() next( action for action in workspace.action_menu.actions() if action.text() == "确认发货" ).trigger() assert [kind for kind, _row in emitted[-3:]] == ["diagnosis", "detail", "ship"] for _kind, row in emitted[-3:]: assert (row["id"], row["prescription_id"], row["diagnosis_id"]) == ( expected["id"], expected["prescription_id"], expected["diagnosis_id"], ) @pytest.mark.parametrize( ("permissions", "menu", "detail"), [ (("*",), ["撤回支付审核", "修改快递单号", "确认发货", "上传药房"], True), (("tcm.prescriptionOrder/detail",), [], True), (("tcm.prescriptionOrder/ship",), ["确认发货"], False), (("tcm.prescriptionOrder.detail", "tcm.prescriptionOrder.ship"), [], False), ((), [], False), ], ) def test_selected_order_menu_requires_canonical_permissions( application: QApplication, workspace_factory, permissions, menu, detail ) -> None: workspace, _repository = workspace_factory(permissions=permissions) assert [action.text() for action in workspace.action_menu.actions()] == menu assert workspace.action_button.isVisible() is bool(menu) assert workspace.detail_button.isVisible() is detail assert workspace.diagnosis_button.isEnabled() emitted = [] workspace.detail_requested.connect(lambda row: emitted.append(row["id"])) workspace._request_detail() # The double-click path also checks permission. assert bool(emitted) is detail workspace.permissions = PermissionSet([]) workspace._selection_changed() _settle(application) assert workspace.action_menu.actions() == [] assert not workspace.action_button.isVisible() assert not workspace.detail_button.isVisible() @pytest.mark.parametrize( ("rx", "pay", "fulfillment", "rx_text", "pay_text", "state", "exclusion"), [ (0, 1, 4, "待审核", "已通过", "已取消", "已取消不计入"), (1, 2, 9, "已通过", "已驳回", "拒收", "拒收不计入"), (2, 0, 10, "已驳回", "待审核", "退款", "退款不计入"), (1, 1, 6, "已通过", "已通过", "已签收", ""), ], ) def test_amount_exclusion_reasons_and_independent_status_text_survive_rendering( application: QApplication, workspace_factory, rx, pay, fulfillment, rx_text, pay_text, state, exclusion, ) -> None: workspace, repository = workspace_factory() repository.rows = [ { **repository.rows[0], "prescription_audit_status": rx, "payment_slip_audit_status": pay, "fulfillment_status": fulfillment, "amount_included": not bool(exclusion), "amount_exclusion_text": exclusion, "effective_amount": 12368.5, } ] workspace.refresh() _settle(application) assert workspace.table.item(0, 3).text() == (exclusion or "¥12,368.50") assert [workspace.table.item(0, column).text() for column in (4, 5, 6)] == [ rx_text, pay_text, state, ] if fulfillment == 6: refund = next( action for action in workspace.action_menu.actions() if action.text() == "退款" ) assert refund.property("danger") is True def test_summary_uses_response_scope_and_legacy_aliases_without_recomputing_rows( application: QApplication, workspace_factory ) -> None: workspace, repository = workspace_factory() assert {key: label.text() for key, label in workspace.metrics.items()} == { "orders": "47", "amount": "¥12,368.50", "pending": "8", "completed": "9", "rejected": "2", "rejection_rate": "4.3%", } assert workspace.scope_label.text() == "测试部门订单范围" assert workspace.pager.total == 47 assert workspace.pager.page_size == 15 assert workspace.pager.height() == 24 # Preserve the existing ratio compatibility, including its known ambiguity. repository.summary = { "order_count": 20, "effective_amount": 700.25, "pending_audit": 3, "completed": 4, "rejected": 1, "rejection_rate": 0.05, } workspace.refresh() _settle(application) assert {key: label.text() for key, label in workspace.metrics.items()} == { "orders": "20", "amount": "¥700.25", "pending": "3", "completed": "4", "rejected": "1", "rejection_rate": "5.0%", } before = len(repository.queries) for metric in workspace.metrics.values(): _click(metric, application) assert len(repository.queries) == before # The six metrics are read-only. @pytest.mark.parametrize("silent", [False, True]) def test_loading_freezes_queries_resets_rows_and_ignores_stale_success_and_failure( application: QApplication, workspace_factory, monkeypatch: pytest.MonkeyPatch, silent: bool ) -> None: workspace, repository = workspace_factory() table = workspace.table queued = [] monkeypatch.setattr( patients_module, "run_async", lambda function, **callbacks: queued.append((function, callbacks)), ) workspace.keyword_edit.setText("旧请求") workspace.refresh(silent=silent) workspace.keyword_edit.setText("最新请求") workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2)) workspace.pay_audit.setCurrentIndex(workspace.pay_audit.findData(0)) workspace.fulfillment.setCurrentIndex(workspace.fulfillment.findData(9)) workspace.use_dates.setChecked(True) workspace.start_date.setDate(QDate(2026, 7, 1)) workspace.end_date.setDate(QDate(2026, 8, 1)) workspace.refresh(silent=silent) _settle(application) assert table.rowCount() == 0 assert workspace.content_stack.currentIndex() == 1 assert workspace.pager.isVisible() assert not workspace.banner.isVisible() workspace.keyword_edit.setText("尚未提交") workspace.rx_audit.setCurrentIndex(0) workspace.use_dates.setChecked(False) newer = queued[1][0]() assert repository.queries[-1] == { "keyword": "最新请求", "prescription_audit_status": 2, "payment_slip_audit_status": 0, "fulfillment_status": 9, "start_date": "2026-07-01", "end_date": "2026-08-01", "page_no": 1, "page_size": 15, } newer["extend"]["scope"]["label"] = "最新范围" queued[1][1]["on_success"](newer) current_item = table.item(0, 0) stale = queued[0][0]() assert repository.queries[-1]["keyword"] == "旧请求" queued[0][1]["on_success"](stale) queued[0][1]["on_error"](RuntimeError("过期失败")) _settle(application) assert table.item(0, 0) is current_item assert workspace.scope_label.text() == "最新范围" assert not workspace.banner.isVisible() workspace.keyword_edit.setText("最新请求") workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2)) workspace.use_dates.setChecked(True) workspace.refresh(silent=silent) queued[-1][1]["on_error"](RuntimeError("订单查询失败")) _settle(application) assert table.item(0, 0) is current_item assert workspace.metrics["orders"].text() == "47" assert workspace.banner.isVisible() assert "订单查询失败" in _banner_text(workspace) workspace.refresh(silent=silent) queued[-1][1]["on_success"]({"lists": [], "count": 0}) _settle(application) assert table.rowCount() == 0 assert workspace.content_stack.currentIndex() == 1 assert "当前范围内暂无订单" in " ".join( label.text() for label in workspace.content_stack.currentWidget().findChildren(QLabel) ) assert not workspace.diagnosis_button.isEnabled() assert not workspace.detail_button.isEnabled() assert workspace.action_menu.actions() == [] assert workspace.pager.total == 0 assert workspace.metrics["orders"].text() == "0" assert workspace.metrics["amount"].text() == "¥0.00" assert not workspace.banner.isVisible() @pytest.mark.parametrize(("width", "height"), [(1270, 680), (1014, 490), (760, 380)]) def test_layout_keeps_filters_actions_and_load_status_reachable_at_narrow_viewports( application: QApplication, workspace_factory, width: int, height: int ) -> None: workspace, _repository = workspace_factory(width=width, height=height) table = workspace.table assert workspace.size().width() == width assert workspace.size().height() == height assert table.columnCount() == 12 assert table.horizontalHeader().height() == 46 assert all(table.rowHeight(index) == 68 for index in range(table.rowCount())) assert table.font().pixelSize() == 14 assert workspace.scope_label.font().pixelSize() == 13 assert all(metric.font().pixelSize() == 18 for metric in workspace.metrics.values()) for widget in ( workspace.keyword_edit, workspace.rx_audit, workspace.pay_audit, workspace.fulfillment, workspace.search_button, workspace.reset_button, workspace.use_dates, workspace.start_date, workspace.end_date, ): assert widget.isVisibleTo(workspace) assert workspace.filter_card.rect().contains(_rect_in(widget, workspace.filter_card)) for metric in workspace.metrics.values(): assert workspace.summary_strip.rect().contains(_rect_in(metric, workspace.summary_strip)) if width == 1270: assert workspace.filter_card.height() == 132 assert workspace.summary_strip.height() == 96 if width == 760: assert workspace.filter_card.height() == 184 assert ( len( { metric.mapTo(workspace.summary_strip, QPoint()).y() for metric in workspace.metrics.values() } ) == 2 ) assert table.horizontalScrollBar().maximum() > 0 assert workspace.scroll.verticalScrollBar().maximum() > 0 for host in ( workspace.filter_card, workspace.summary_strip, workspace.action_bar, workspace.pager, ): workspace.scroll.ensureWidgetVisible(host, 0, 0) _settle(application) assert ( workspace.scroll.viewport().rect().contains(_rect_in(host, workspace.scroll.viewport())) ), host.objectName() for button in (workspace.diagnosis_button, workspace.detail_button, workspace.action_button): assert workspace.action_bar.rect().contains(_rect_in(button, workspace.action_bar)) assert workspace.pager.height() == 24 for widget in (workspace.pager.summary_label,): assert workspace.pager.rect().contains(_rect_in(widget, workspace.pager)) table.horizontalScrollBar().setValue(table.horizontalScrollBar().maximum()) table.scrollToBottom() _settle(application) assert table.columnViewportPosition(11) >= 0 assert table.columnViewportPosition(11) + table.columnWidth(11) <= table.viewport().width() assert table.rowViewportPosition(2) + table.rowHeight(2) <= table.viewport().height() def test_tab_switches_keep_patient_styles_scope_deduplication_and_progress_timer( application: QApplication, workspace_factory ) -> None: page, _repository = workspace_factory(width=1328, height=884, page=True) patient = page.patient_workspace progress = page.progress_workspace patient_style = patient.styleSheet() assert patient.table.objectName() == "PatientTable" assert patient.table.columnCount() == 10 assert patient.table.rowHeight(0) == 66 assert patient.table.horizontalHeader().height() == 42 assert patient.table.font().pixelSize() == 14 assert not page.scope_badge.isVisible() assert not patient.search_toolbar.isVisible() page.filter_disclosure.set_expanded(True) _settle(application) assert patient.search_toolbar.isVisible() assert not progress.timer.isActive() page.tabs.setCurrentIndex(2) _settle(application) progress_style = progress.styleSheet() progress_table_font = progress.schedule_table.font() progress_metric_font = progress.overview["total"][0].font() assert progress.timer.interval() == 15_000 assert progress.timer.isActive() assert not page.scope_badge.isVisible() assert not progress.scope_label.isVisible() page.filter_disclosure.set_expanded(True) _settle(application) assert progress.scope_label.isVisible() assert progress_table_font.pixelSize() == 14 assert progress_metric_font.pixelSize() == 18 assert progress.schedule_table.rowCount() == 7 assert progress.schedule_table.columnCount() == 7 page.tabs.setCurrentIndex(1) _settle(application) assert not progress.timer.isActive() assert not page.scope_badge.isVisible() assert not patient.search_toolbar.isVisible() assert page.order_workspace.scope_label.isVisible() assert page.order_workspace.scope_label.text() == "测试部门订单范围" assert patient.styleSheet() == patient_style assert progress.styleSheet() == progress_style assert progress.schedule_table.font() == progress_table_font assert progress.overview["total"][0].font() == progress_metric_font assert patient.table.rowHeight(0) == 66 assert patient.table.horizontalHeader().height() == 42 page.tabs.setCurrentIndex(0) _settle(application) assert patient.search_toolbar.isVisible() assert not page.scope_badge.isVisible() assert not progress.timer.isActive() page.tabs.setCurrentIndex(2) _settle(application) assert not page.scope_badge.isVisible() assert progress.scope_label.isVisible() assert progress.timer.isActive() page.hide() _settle(application) assert not progress.timer.isActive() page.show() _settle(application) assert progress.timer.isActive()