This commit is contained in:
Your Name
2026-08-13 16:37:37 +08:00
parent cbe71905c0
commit 21790e35f4
11 changed files with 1708 additions and 434 deletions
File diff suppressed because it is too large Load Diff
@@ -68,6 +68,39 @@ def _truthy(value: Any) -> bool:
return bool(value)
def _row_mapping(value: Any) -> dict[str, Any]:
"""Return only fields actually supplied by an API row when possible."""
if isinstance(value, Mapping):
return dict(value)
raw = getattr(value, "raw", None)
if isinstance(raw, Mapping) and raw:
return dict(raw)
fields = getattr(value, "__dataclass_fields__", {})
return {
name: getattr(value, name, None)
for name in fields
if name != "raw"
}
def merge_prescription_detail(row: Any, detail: Any) -> dict[str, Any]:
"""Match the admin editor's ``{...listRow, ...detail}`` state merge."""
merged = _row_mapping(row)
merged.update(_row_mapping(detail))
# Repository models retain the original API row in ``raw`` and expose
# canonical alias fields (for example ``patient_phone`` -> ``phone``) on
# the dataclass. Add those aliases without letting an omitted detail field
# overwrite a list-only audit/void value with its model default.
for source in (detail, row):
fields = getattr(source, "__dataclass_fields__", {})
for name in fields:
if name != "raw" and name not in merged:
merged[name] = getattr(source, name, None)
return merged
def prescription_status(row: Any) -> tuple[str, str]:
"""Return the admin audit-column status; void state has its own column."""
@@ -730,7 +763,11 @@ class PrescriptionsPage(QWidget):
or not can_edit_or_delete(row)
):
return
self._load_detail(row, self._open_editor, message="正在准备编辑处方…")
self._load_detail(
row,
lambda detail: self._open_editor(merge_prescription_detail(row, detail)),
message="正在准备编辑处方…",
)
def _open_editor(self, detail: Any) -> None:
dialog = PrescriptionEditorDialog(
@@ -69,13 +69,13 @@ def test_editor_is_a_fixed_header_body_footer_right_drawer(
origin = host.mapToGlobal(QPoint(0, 0))
assert editor.windowFlags() & Qt.WindowType.FramelessWindowHint
assert editor.width() == 1200
assert editor.width() == 860
assert editor.height() == host.height()
assert editor.x() + editor.width() == origin.x() + host.width()
assert editor.y() == origin.y()
assert editor.save_button.text() == "确定"
assert editor.windowTitle() == "新增处方"
assert not editor.findChildren(QTabWidget)
assert editor.tabs.count() == 4
assert editor.tabs.tabText(2) == "剂型与用法"
assert editor.body_scroll.verticalScrollBar().maximum() > 0
assert editor.body_scroll.horizontalScrollBar().maximum() == 0
@@ -110,7 +110,35 @@ def test_drawer_uses_full_width_on_a_narrow_host(application: QApplication) -> N
assert editor.width() == host.width()
assert editor.x() == origin.x()
assert editor.body_scroll.horizontalScrollBar().maximum() == 0
assert editor.herbs._grid_columns == 3
assert editor.herbs.table_mode
assert editor.add_main_button.text() == "添加主方药材"
editor.close()
host.close()
application.processEvents()
def test_decoction_usage_fields_pack_without_a_hidden_column_gap(
application: QApplication,
) -> None:
host = QWidget()
host.resize(1440, 900)
host.show()
source = _seed()
source["prescription_type"] = "饮片"
editor = PrescriptionEditorDialog(_repository(), source, parent=host)
_show(editor, application)
assert editor._main_decoction_field.isVisible()
assert editor._main_bags_field.isVisible()
assert editor._main_bag_field.isHidden()
assert editor._main_dosage_field.y() == editor._main_decoction_field.y()
gap = editor._main_decoction_field.x() - (
editor._main_dosage_field.x() + editor._main_dosage_field.width()
)
assert 0 <= gap <= 24
assert abs(editor._main_dosage_field.width() - editor._main_decoction_field.width()) < 40
assert editor.cancel_button.x() < editor.save_button.x()
editor.close()
host.close()
@@ -74,6 +74,8 @@ class _DiagnosisRepository:
"height": 162.5,
"weight": 52.0,
"fasting_blood_sugar": "6.2",
"diagnosis_type": "first_visit",
"local_hospital_name": "杭州市第一人民医院",
"chief_complaint": "乏力",
"symptoms": "口干",
"appetite": ["一般", "少食"],
+160 -4
View File
@@ -7,13 +7,13 @@ from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QEvent, QObject, QSize, Qt
from PySide6.QtCore import QDate, QEvent, QObject, QSize, Qt
from PySide6.QtGui import QColor, QImage, QPainter
from PySide6.QtPdf import QPdfDocument
from PySide6.QtWidgets import QApplication, QDialog, QWidget
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QPushButton, QWidget
from doctor_workstation import app as app_module
from doctor_workstation.core import PermissionSet
from doctor_workstation.core import PermissionSet, Prescription
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui.dialogs import prescription as dialog_module
from doctor_workstation.ui.dialogs.prescription import (
@@ -325,6 +325,8 @@ def test_editor_builds_complete_add_payload(
)
user = SimpleNamespace(id=7, name="周医生")
editor = PrescriptionEditorDialog(repository, mode="add", current_user=user)
# The admin editor starts with an empty table; adding a herb is explicit.
editor.herbs.add_row(formula_type="主方")
editor.patient_name.setText("林晓岚")
editor.clinical_diagnosis.setPlainText("脾气虚")
editor.herbs.rows[0].medicine.set_value(31, "黄芪")
@@ -465,6 +467,7 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
"clinical_diagnosis": "气阴两虚",
"doctor_name": "周医生",
"audit_status": 2,
"audit_remark": "请补充辨证依据",
"void_status": 1,
"is_system_auto": 1,
"business_prescription_audit_rejected": 1,
@@ -473,6 +476,12 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
}
editor = PrescriptionEditorDialog(Repository(), source, mode="edit")
assert editor.windowTitle() == "编辑处方"
assert editor.patient_name.isReadOnly()
assert editor.visit_no.isReadOnly()
assert not hasattr(editor, "phone")
assert editor.gender_male.isChecked()
assert editor.payload()["gender"] == 1
assert editor.tongue.text() == "面象哨兵"
assert editor.tongue_image.text() == "舌象哨兵"
assert editor.pulse.text() == "脉象哨兵"
@@ -483,11 +492,17 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
assert payload["pulse"] == "脉象哨兵"
assert payload["pulse_condition"] == "脉象详情哨兵"
assert "取消作废、清除驳回" in editor.context_banner.label.text()
assert "空白处方" in editor.context_banner.label.text()
assert "系统自动生成" in editor.context_banner.label.text()
assert "请补充辨证依据" in editor.context_banner.label.text()
assert "业务订单侧" not in editor.context_banner.label.text()
assert calls == [{"page_no": 1, "page_size": 5, "prescription_id": 91}]
assert "PO-901" in editor.linked_order_banner.label.text()
assert "14 天" in editor.linked_order_banner.label.text()
assert "按两周疗程复核" in editor.linked_order_banner.label.text()
assert editor.linked_order_banner.objectName() == "MessageBanner"
assert editor.linked_order_banner.property("kind") == "warning"
assert "#FDF6EC" in editor.drawer_surface.styleSheet()
assert 'QFrame#MessageBanner[kind="warning"]' in editor.drawer_surface.styleSheet()
editor.close()
application.processEvents()
@@ -516,6 +531,9 @@ def test_editor_preserves_global_lock_and_admin_type_constraints(
)
assert editor.herbs.locked
assert all(row.locked for row in editor.herbs.rows)
locked_payload = editor.payload()["herbs"]
assert locked_payload[0]["locked"] is True
assert "locked" not in locked_payload[1]
assert not editor.add_main_button.isEnabled()
assert editor.import_library_button.isHidden()
assert editor._aux_name_field.isHidden()
@@ -537,6 +555,144 @@ def test_editor_preserves_global_lock_and_admin_type_constraints(
application.processEvents()
def test_editor_usage_fields_are_dropdowns_and_date_opens_calendar(
application: QApplication,
) -> None:
editor = PrescriptionEditorDialog(
SimpleNamespace(),
{
"prescription_type": "饮片",
"prescription_date": "2026-08-01",
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
},
mode="edit",
)
assert isinstance(editor.dosage_amount, QComboBox)
assert not editor.dosage_amount.isEditable()
assert editor.dosage_amount.itemText(0).endswith("ml")
assert isinstance(editor.bags_per_dose, QComboBox)
assert editor.bags_per_dose.itemText(0) == "1包"
assert editor.bags_per_dose.maximum() == 9
assert isinstance(editor.dosage_bag_count, QComboBox)
assert editor.dosage_bag_count.itemText(0) == "1袋"
assert isinstance(editor.prescription_type, QComboBox)
assert isinstance(editor.usage_time, QComboBox)
assert isinstance(editor.usage_way, QComboBox)
assert "down-arrow" in editor.drawer_surface.styleSheet()
assert "__CHEVRON_URL__" not in editor.drawer_surface.styleSheet()
assert editor.prescription_type.cursor().shape() == Qt.CursorShape.PointingHandCursor
assert editor.date_edit.cursor().shape() == Qt.CursorShape.PointingHandCursor
assert editor.date_edit.calendarPopup()
calendar_button = editor.date_edit.findChild(QPushButton, "PrescriptionDateButton")
assert calendar_button is not None
assert not calendar_button.icon().isNull()
assert editor.date_edit.date() == QDate(2026, 8, 1)
editor.show()
application.processEvents()
editor.date_edit.open_calendar()
application.processEvents()
assert editor.date_edit.calendarWidget().isVisible()
editor.date_edit.calendarWidget().clicked.emit(QDate(2026, 8, 13))
application.processEvents()
assert not editor.date_edit.calendarWidget().isVisible()
assert editor.payload()["prescription_date"] == "2026-08-13"
editor.bags_per_dose.setValue(4)
assert editor.payload()["bags_per_dose"] == 4
editor.close()
application.processEvents()
def test_editor_matches_admin_empty_rows_dosage_choices_and_detail_merge(
application: QApplication,
) -> None:
editor = PrescriptionEditorDialog(
SimpleNamespace(),
mode="add",
current_user=SimpleNamespace(id=7, name="周医生"),
)
assert editor.herbs.rows == []
editor.herbs.add_row(formula_type="主方")
assert editor.payload()["herbs"] == [
{"name": "", "dosage": 0.0, "formula_type": "主方"}
]
editor.need_decoction.setChecked(True)
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("饮片"))
assert editor.need_decoction.isChecked()
assert [
editor.dosage_amount.itemData(index)
for index in range(editor.dosage_amount.count())
] == [50.0, 100.0, 120.0, 150.0, 180.0, 200.0, 250.0]
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("颗粒"))
assert editor.dosage_amount.optional_value() is None
assert "dosage_amount" not in editor.payload()
merged = prescription_module.merge_prescription_detail(
{
"id": 91,
"audit_status": 1,
"void_status": 1,
"business_prescription_audit_rejected": 1,
"business_prescription_audit_remark": "订单驳回",
},
{"id": 91, "patient_name": "林晓岚", "herbs": []},
)
assert merged["patient_name"] == "林晓岚"
assert merged["void_status"] == 1
assert merged["business_prescription_audit_rejected"] == 1
assert merged["business_prescription_audit_remark"] == "订单驳回"
model_detail = Prescription.from_dict(
{"id": 91, "patient_phone": "13800138000", "patient_name": "林晓岚"}
)
model_merged = prescription_module.merge_prescription_detail(
{"id": 91, "void_status": 1},
model_detail,
)
assert model_merged["phone"] == "13800138000"
assert model_merged["void_status"] == 1
editor.close()
application.processEvents()
def test_paste_parser_matches_admin_zero_and_note_rules() -> None:
assert parse_pasted_herbs("黄芪0g") == []
assert parse_pasted_herbs("黄芪(炙)、党参各6g") == [
{"name": "黄芪", "dosage": 6.0},
{"name": "党参", "dosage": 6.0},
]
def test_editor_only_shows_business_rejection_for_consumer_approved_rows(
application: QApplication,
) -> None:
editor = PrescriptionEditorDialog(
SimpleNamespace(),
{
"id": 92,
"audit_status": 1,
"business_prescription_audit_rejected": 1,
"business_prescription_audit_remark": "重新核对剂量",
"patient_name": "林晓岚",
"clinical_diagnosis": "气阴两虚",
"doctor_name": "周医生",
"herbs": [{"name": "黄芪", "dosage": 15, "formula_type": "主方"}],
},
mode="edit",
)
message = editor.context_banner.label.text()
assert "业务订单侧「处方审核」已驳回" in message
assert "重新核对剂量" in message
editor.close()
application.processEvents()
def test_audit_reject_requires_remark(
application: QApplication,
) -> None: