This commit is contained in:
Your Name
2026-08-12 18:09:57 +08:00
parent a1092c02c3
commit b2eb18c75f
391 changed files with 1666 additions and 638 deletions
+89 -1
View File
@@ -3,12 +3,13 @@
from __future__ import annotations
import os
from types import SimpleNamespace
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtWidgets import QApplication, QLabel
from PySide6.QtWidgets import QApplication, QDialog, QLabel
from doctor_workstation.core.errors import ApiProtocolError
from doctor_workstation.core.models import Appointment, PageResult
@@ -174,6 +175,93 @@ def test_diagnosis_and_patient_ids_stay_distinct_for_video() -> None:
)
assert prescription_action_label(approved) == "查看"
pending = Appointment.from_dict(
{
"id": 103,
"prescription_audit_status": 0,
"prescription_void_status": 0,
"has_prescription": 1,
}
)
assert prescription_action_label(pending) == "编辑处方"
def test_appointment_pending_prescription_uses_full_edit_contract(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
updated: list[tuple[int, dict[str, Any]]] = []
existing = {
"id": 81,
"diagnosis_id": 501,
"audit_status": 0,
"void_status": 0,
"patient_name": "林晓岚",
}
class Repository:
def update_prescription(
self, prescription: int, changes: dict[str, Any]
) -> dict[str, Any]:
updated.append((prescription, dict(changes)))
return {"id": prescription}
class AcceptedEditor:
def __init__(self, _repository: Any, source: Any, **kwargs: Any) -> None:
assert source is existing
assert kwargs["mode"] == "edit"
def exec(self) -> QDialog.DialogCode:
return QDialog.DialogCode.Accepted
def payload(self) -> dict[str, Any]:
return {"id": 81, "clinical_diagnosis": "脾气虚"}
def run_immediately(function: Any, **callbacks: Any) -> object:
try:
result = function()
except Exception as error:
callbacks["on_error"](error)
else:
callbacks["on_success"](result)
finally:
callbacks["on_finished"]()
return object()
monkeypatch.setattr(appointments_module, "PrescriptionEditorDialog", AcceptedEditor)
monkeypatch.setattr(appointments_module, "run_async", run_immediately)
page = AppointmentsPage(Repository(), permissions=PermissionSet(["*"]))
monkeypatch.setattr(page, "refresh", lambda **_kwargs: None)
page._prescription_loaded(existing, {"id": 101}, page._prescription_generation)
assert updated == [(81, {"id": 81, "clinical_diagnosis": "脾气虚"})]
page.close()
application.processEvents()
def test_appointment_prescription_seed_keeps_admin_observation_fields(
application: QApplication,
) -> None:
page = AppointmentsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
seed = page._prescription_seed(
{"id": 101, "appointment_id": 101, "diagnosis_id": 501},
{
"id": 501,
"patient_name": "林晓岚",
"tongue": "面象哨兵",
"tongue_image": "舌象哨兵",
"pulse": "脉象哨兵",
"pulse_condition": "脉象详情哨兵",
},
)
assert seed["tongue"] == "面象哨兵"
assert seed["tongue_image"] == "舌象哨兵"
assert seed["pulse"] == "脉象哨兵"
assert seed["pulse_condition"] == "脉象详情哨兵"
page.close()
application.processEvents()
def test_appointments_page_default_query_is_today_pending(
application: QApplication,
+89 -11
View File
@@ -132,10 +132,20 @@ def test_nested_appointments_confirmation_and_prescription_labels() -> None:
prescription_action_label({"prescription_audit_status": 1, "prescription_void_status": 0})
== "查看处方"
)
assert (
prescription_action_label({"prescription_audit_status": 1, "prescription_void_status": 1})
== ""
)
assert (
prescription_action_label({"prescription_audit_status": 1, "prescription_void_status": 1})
== "编辑处"
)
assert (
prescription_action_label(
{
"has_prescription": 1,
"prescription_audit_status": 0,
"prescription_void_status": 0,
}
)
== "编辑处方"
)
def test_default_query_matches_admin_today_and_page_size_contract(
@@ -313,7 +323,7 @@ def test_prescription_query_error_is_fail_closed_without_diagnosis_fallback(
application.processEvents()
def test_new_prescription_uses_authoritative_case_snapshot_and_exact_ids(
def test_new_prescription_uses_authoritative_case_snapshot_and_exact_ids(
application: QApplication,
immediate_async: None,
monkeypatch: pytest.MonkeyPatch,
@@ -321,8 +331,16 @@ def test_new_prescription_uses_authoritative_case_snapshot_and_exact_ids(
calls: list[tuple[str, int]] = []
created: list[dict[str, Any]] = []
dialog_seeds: list[dict[str, Any]] = []
case_record = {
"diagnosis": {"id": 501, "patient_name": "林晓岚", "chief_complaint": "咳嗽"},
case_record = {
"diagnosis": {
"id": 501,
"patient_name": "林晓岚",
"chief_complaint": "咳嗽",
"tongue": "面象哨兵",
"tongue_image": "舌象哨兵",
"pulse": "脉象哨兵",
"pulse_condition": "脉象详情哨兵",
},
"patient": {"id": 301, "gender": 2, "age": 36},
}
@@ -370,10 +388,70 @@ def test_new_prescription_uses_authoritative_case_snapshot_and_exact_ids(
assert created[0]["appointment_id"] == 202
assert created[0]["case_record"] == case_record
assert created[0]["case_record"] is not case_record
assert dialog_seeds[0]["case_record"] == case_record
assert dialog_seeds[0]["case_record"] is not case_record
page.close()
application.processEvents()
assert dialog_seeds[0]["case_record"] == case_record
assert dialog_seeds[0]["case_record"] is not case_record
assert dialog_seeds[0]["tongue"] == "面象哨兵"
assert dialog_seeds[0]["tongue_image"] == "舌象哨兵"
assert dialog_seeds[0]["pulse"] == "脉象哨兵"
assert dialog_seeds[0]["pulse_condition"] == "脉象详情哨兵"
page.close()
application.processEvents()
def test_existing_pending_prescription_is_edited_in_place_not_duplicated(
application: QApplication,
immediate_async: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
updated: list[tuple[int, dict[str, Any]]] = []
created: list[dict[str, Any]] = []
existing = {
"id": 901,
"diagnosis_id": 501,
"patient_name": "林晓岚",
"audit_status": 0,
"void_status": 0,
"clinical_diagnosis": "气虚",
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
}
class Repository:
def get_prescription_by_appointment(self, appointment_id: int) -> dict[str, Any]:
assert appointment_id == 202
return existing
def update_prescription(
self, prescription: int, changes: dict[str, Any]
) -> dict[str, Any]:
updated.append((prescription, dict(changes)))
return {"id": prescription}
def create_prescription(self, prescription: dict[str, Any]) -> dict[str, Any]:
created.append(dict(prescription))
return {"id": 999}
def list_consultations(self, **_kwargs: Any) -> dict[str, Any]:
return {"lists": [], "count": 0}
class AcceptedEditor:
def __init__(self, _repository: Any, source: Any, **kwargs: Any) -> None:
assert source is existing
assert kwargs["mode"] == "edit"
def exec(self) -> QDialog.DialogCode:
return QDialog.DialogCode.Accepted
def payload(self) -> dict[str, Any]:
return {"id": 901, "clinical_diagnosis": "气血两虚"}
monkeypatch.setattr(consultations_module, "PrescriptionEditorDialog", AcceptedEditor)
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
page._begin_prescription_load(_row(appointment_id=202), mode="open")
assert updated == [(901, {"id": 901, "clinical_diagnosis": "气血两虚"})]
assert created == []
page.close()
application.processEvents()
def test_switching_rows_invalidates_prescription_worker_and_clears_busy(
@@ -7,7 +7,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QPoint, Qt
from PySide6.QtWidgets import QApplication, QTabWidget, QWidget
from PySide6.QtWidgets import QApplication, QDialog, QTabWidget, QWidget
from doctor_workstation.ui.dialogs.prescription import PrescriptionEditorDialog
@@ -31,9 +31,10 @@ def _seed() -> dict[str, object]:
"gender": 0,
"age": 34,
"visit_no": "1K00000401",
"tongue": "舌淡红、苔薄白",
"pulse": "面色少华",
"pulse_condition": "脉细",
"tongue": "面色少华",
"tongue_image": "舌淡红、苔薄白",
"pulse": "脉细",
"pulse_condition": "沉取无力",
"clinical_diagnosis": "气阴两虚",
"doctor_name": "陈医生",
"herbs": [
@@ -116,7 +117,7 @@ def test_drawer_uses_full_width_on_a_narrow_host(application: QApplication) -> N
application.processEvents()
def test_duplicate_guard_and_admin_dto_survive_the_visual_restructure(
def test_duplicate_warning_and_admin_dto_survive_the_visual_restructure(
application: QApplication,
) -> None:
source = _seed()
@@ -138,11 +139,10 @@ def test_duplicate_guard_and_admin_dto_survive_the_visual_restructure(
assert payload["herbs"][0]["formula_type"] == "主方"
assert payload["herbs"][1]["formula_type"] == "辅方"
assert all(bool(row.property("duplicate")) for row in editor.herbs.rows)
assert "存在重复药名" in editor.herb_summary.text()
editor.accept()
assert editor.result() == 0
assert editor.tabs.currentIndex() == 1
assert "药材不可重复:黄芪" in editor.validation.label.text()
assert editor.result() == QDialog.DialogCode.Accepted
editor.close()
application.processEvents()
+77 -4
View File
@@ -129,6 +129,31 @@ def test_canonical_permission_helper_rejects_dot_aliases_and_accepts_wildcards()
assert not widgets.has_permission({}, "cf.prescription/edit")
def test_diagnosis_prescription_edit_uses_admin_edit_permission(
application: QApplication,
) -> None:
repository = SimpleNamespace(
create_prescription=lambda **_kwargs: {},
update_prescription=lambda **_kwargs: {},
)
create_only = DiagnosisDialog(
repository,
permissions=PermissionSet(["tcm.diagnosis/chufang"]),
)
assert create_only._can_prescribe
assert not create_only._can_edit_prescription
create_only.close()
edit_only = DiagnosisDialog(
repository,
permissions=PermissionSet(["cf.prescription/edit"]),
)
assert not edit_only._can_prescribe
assert edit_only._can_edit_prescription
edit_only.close()
application.processEvents()
def test_diagnosis_masks_sensitive_fields_and_loads_exact_context_orders(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
@@ -190,7 +215,7 @@ def test_diagnosis_edit_checks_unique_identity_and_saves_expanded_dto(
application.processEvents()
def test_duplicate_herbs_are_rejected_for_templates_and_issued_prescriptions(
def test_duplicate_herbs_match_admin_warning_for_issued_prescriptions(
application: QApplication,
) -> None:
herbs = [
@@ -216,9 +241,9 @@ def test_duplicate_herbs_are_rejected_for_templates_and_issued_prescriptions(
editor.clinical_diagnosis.setPlainText("气虚")
editor.signature._has_strokes = True
editor.herbs.set_rows(herbs, locked=False)
assert "存在重复药名" in editor.herb_summary.text()
editor.accept()
assert editor.result() == QDialog.DialogCode.Rejected
assert "药材不可重复:黄芪" in editor.validation.label.text()
assert editor.result() == QDialog.DialogCode.Accepted
template.close()
editor.close()
application.processEvents()
@@ -358,7 +383,7 @@ def test_diagnosis_detail_requires_permission_and_ignores_stale_target(
shown: list[int] = []
class FakeDiagnosisDetailDialog:
def __init__(self, detail: dict[str, Any], _parent: Any) -> None:
def __init__(self, detail: dict[str, Any], _parent: Any, **_kwargs: Any) -> None:
shown.append(detail["id"])
def exec(self) -> int:
@@ -384,3 +409,51 @@ def test_diagnosis_detail_requires_permission_and_ignores_stale_target(
denied.close()
allowed.close()
application.processEvents()
def test_diagnosis_prescription_view_fetches_full_detail_and_ignores_stale_row(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
requested: list[int] = []
callbacks: list[tuple[Any, dict[str, Any]]] = []
shown: list[int] = []
class Repository:
def get_prescription(self, prescription_id: int) -> dict[str, Any]:
requested.append(prescription_id)
return {
"id": prescription_id,
"patient_name": f"患者{prescription_id}",
"doctor_signature": "data:image/png;base64,detail",
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
}
def queue_async(function: Any, **options: Any) -> object:
callbacks.append((function, options))
return object()
class FakePrescriptionDetailDialog:
def __init__(self, detail: dict[str, Any], **_kwargs: Any) -> None:
assert detail["doctor_signature"].endswith("detail")
assert detail["herbs"][0]["name"] == "黄芪"
shown.append(detail["id"])
def exec(self) -> int:
return 0
monkeypatch.setattr(diagnosis_module, "run_async", queue_async)
monkeypatch.setattr(dialog_module, "PrescriptionDetailDialog", FakePrescriptionDetailDialog)
dialog = DiagnosisDialog(Repository(), permissions=PermissionSet(["*"]))
dialog._view_prescription({"id": 11, "herbs": []})
dialog._view_prescription({"id": 12, "herbs": []})
old_function, old_options = callbacks[0]
old_options["on_success"](old_function())
assert shown == []
new_function, new_options = callbacks[1]
new_options["on_success"](new_function())
assert requested == [11, 12]
assert shown == [12]
dialog.close()
application.processEvents()
+109 -1
View File
@@ -10,6 +10,7 @@ import pytest
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
from doctor_workstation.core import PermissionSet
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui.dialogs import prescription as dialog_module
from doctor_workstation.ui.dialogs.prescription import (
@@ -88,7 +89,7 @@ def test_admin_status_and_action_guards_are_exact() -> None:
assert prescription_status(pending) == ("待审核", "warning")
assert prescription_status(approved) == ("已通过", "success")
assert prescription_status(rejected) == ("已驳回", "danger")
assert prescription_status(voided) == ("作废", "danger")
assert prescription_status(voided) == ("通过", "success")
assert can_patch_patient(pending)
assert can_create_order(pending)
assert can_audit(pending)
@@ -325,6 +326,8 @@ def test_editor_builds_complete_add_payload(
editor.clinical_diagnosis.setPlainText("脾气虚")
editor.herbs.rows[0].medicine.set_value(31, "黄芪")
editor.herbs.rows[0].dosage.setValue(15)
editor.dietary_taboo.set_values(["辛辣食物", "浓茶"])
assert editor.dietary_taboo.lineEdit().text() == "辛辣食物、浓茶"
editor.signature._has_strokes = True
payload = editor.payload()
@@ -341,12 +344,117 @@ def test_editor_builds_complete_add_payload(
}
]
assert payload["doctor_name"] == "周医生"
assert payload["dietary_taboo"] == ["辛辣食物", "浓茶"]
assert payload["doctor_signature"].startswith("data:image/png;base64,")
assert isinstance(payload["aux_usage"], dict)
editor.close()
application.processEvents()
def test_editor_matches_admin_four_observation_fields_and_edit_context(
application: QApplication,
immediate_async: None,
) -> None:
calls: list[dict[str, Any]] = []
class Repository:
def list_prescription_orders(self, **filters: Any) -> dict[str, Any]:
calls.append(filters)
return {
"lists": [
{
"order_no": "PO-901",
"medication_days": 14,
"remark_assistant": "按两周疗程复核",
}
],
"count": 1,
}
source = {
"id": 91,
"diagnosis_id": 501,
"patient_name": "林晓岚",
"visit_no": "1K00000501",
"tongue": "面象哨兵",
"tongue_image": "舌象哨兵",
"pulse": "脉象哨兵",
"pulse_condition": "脉象详情哨兵",
"clinical_diagnosis": "气阴两虚",
"doctor_name": "周医生",
"audit_status": 2,
"void_status": 1,
"is_system_auto": 1,
"business_prescription_audit_rejected": 1,
"business_prescription_audit_remark": "订单审核意见",
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
}
editor = PrescriptionEditorDialog(Repository(), source, mode="edit")
assert editor.tongue.text() == "面象哨兵"
assert editor.tongue_image.text() == "舌象哨兵"
assert editor.pulse.text() == "脉象哨兵"
assert editor.pulse_condition.text() == "脉象详情哨兵"
payload = editor.payload()
assert payload["tongue"] == "面象哨兵"
assert payload["tongue_image"] == "舌象哨兵"
assert payload["pulse"] == "脉象哨兵"
assert payload["pulse_condition"] == "脉象详情哨兵"
assert "取消作废、清除驳回" in editor.context_banner.label.text()
assert "空白处方" 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()
editor.close()
application.processEvents()
def test_editor_preserves_global_lock_and_admin_type_constraints(
application: QApplication,
) -> None:
editor = PrescriptionEditorDialog(
SimpleNamespace(),
mode="add",
current_user=SimpleNamespace(id=7, name="周医生"),
permissions=PermissionSet([]),
)
editor.herbs.set_rows(
[
{
"medicine_id": 31,
"name": "酸枣仁",
"dosage": 12,
"formula_type": "辅方",
"locked": True,
},
{"medicine_id": 32, "name": "黄芪", "dosage": 15, "formula_type": "主方"},
],
locked=False,
)
assert editor.herbs.locked
assert all(row.locked for row in editor.herbs.rows)
assert not editor.add_main_button.isEnabled()
assert editor.import_library_button.isHidden()
assert editor._aux_name_field.isHidden()
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("饮片"))
assert editor.dosage_amount.minimum() == 50
assert editor.dosage_amount.maximum() == 250
assert editor.dosage_amount.value() == 50
assert editor.bags_per_dose.maximum() == 9
assert editor.aux_dosage_amount.value() == 50
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("浓缩水丸"))
assert editor.dosage_amount.minimum() == 1
assert editor.dosage_amount.maximum() == 10
assert editor.dosage_amount.value() == 1
assert editor.dosage_bag_count.maximum() == 5
assert editor.aux_dosage_amount.value() == 5
editor.close()
application.processEvents()
def test_audit_reject_requires_remark(
application: QApplication,
) -> None: