1133 lines
39 KiB
Python
1133 lines
39 KiB
Python
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.QtCore import QDate, QEvent, QObject, QSize, Qt
|
||
from PySide6.QtGui import QColor, QImage, QPainter
|
||
from PySide6.QtPdf import QPdfDocument
|
||
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QPushButton, QWidget
|
||
|
||
from doctor_workstation import app as app_module
|
||
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 (
|
||
AuditPrescriptionDialog,
|
||
PrescriptionDetailDialog,
|
||
PrescriptionEditorDialog,
|
||
PrescriptionOrderDialog,
|
||
PrescriptionTemplateDialog,
|
||
RemoteMedicineComboBox,
|
||
build_prescription_clinical_diagnosis,
|
||
build_prescription_visit_no,
|
||
parse_pasted_herbs,
|
||
render_prescription_html,
|
||
)
|
||
from doctor_workstation.ui.pages import prescription_library as library_module
|
||
from doctor_workstation.ui.pages import prescriptions as prescription_module
|
||
from doctor_workstation.ui.pages.prescription_library import PrescriptionLibraryPage
|
||
from doctor_workstation.ui.pages.prescriptions import (
|
||
PrescriptionsPage,
|
||
can_audit,
|
||
can_create_order,
|
||
can_edit_or_delete,
|
||
can_patch_patient,
|
||
prescription_status,
|
||
)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def application() -> QApplication:
|
||
return QApplication.instance() or QApplication([])
|
||
|
||
|
||
@pytest.fixture
|
||
def immediate_async(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
def run_immediately(
|
||
function: Any,
|
||
*args: Any,
|
||
on_success: Any = None,
|
||
on_error: Any = None,
|
||
on_finished: Any = None,
|
||
**kwargs: Any,
|
||
) -> object:
|
||
try:
|
||
result = function(*args, **kwargs)
|
||
except Exception as error:
|
||
if on_error:
|
||
on_error(error)
|
||
else:
|
||
if on_success:
|
||
on_success(result)
|
||
finally:
|
||
if on_finished:
|
||
on_finished()
|
||
return object()
|
||
|
||
monkeypatch.setattr(dialog_module, "run_async", run_immediately)
|
||
monkeypatch.setattr(library_module, "run_async", run_immediately)
|
||
monkeypatch.setattr(prescription_module, "run_async", run_immediately)
|
||
|
||
|
||
def test_admin_status_and_action_guards_are_exact() -> None:
|
||
pending = {
|
||
"id": 1,
|
||
"audit_status": 0,
|
||
"void_status": 0,
|
||
"has_prescription_order": 0,
|
||
}
|
||
approved = {**pending, "audit_status": 1}
|
||
rejected = {
|
||
**approved,
|
||
"business_prescription_audit_rejected": 1,
|
||
"business_prescription_audit_remark": "剂量需调整",
|
||
}
|
||
voided = {**approved, "void_status": 1}
|
||
|
||
assert prescription_status(pending) == ("待审核", "warning")
|
||
assert prescription_status(approved) == ("已通过", "success")
|
||
assert prescription_status(rejected) == ("已驳回", "danger")
|
||
assert prescription_status(voided) == ("已通过", "success")
|
||
assert can_patch_patient(pending)
|
||
assert can_create_order(pending)
|
||
assert can_audit(pending)
|
||
assert can_edit_or_delete(pending)
|
||
assert not can_audit(approved)
|
||
assert not can_edit_or_delete(approved)
|
||
assert can_edit_or_delete(voided)
|
||
assert not can_patch_patient(voided)
|
||
|
||
|
||
def test_paste_parser_matches_common_admin_recipe_forms() -> None:
|
||
parsed = parse_pasted_herbs("Rp: 黄芪15 党参12、茯苓10g\n柴胡、白术各6克\n饭后温服")
|
||
|
||
assert parsed == [
|
||
{"name": "黄芪", "dosage": 15.0},
|
||
{"name": "党参", "dosage": 12.0},
|
||
{"name": "茯苓", "dosage": 10.0},
|
||
{"name": "柴胡", "dosage": 6.0},
|
||
{"name": "白术", "dosage": 6.0},
|
||
]
|
||
|
||
|
||
def test_prescription_seed_helpers_match_admin_visit_no_and_clinical() -> None:
|
||
assert build_prescription_visit_no(diagnosis_id=15534) == "1K00015534"
|
||
assert build_prescription_visit_no(appointment_id=401, diagnosis_id=15534) == "1K00000401"
|
||
assert build_prescription_visit_no() == ""
|
||
|
||
assert build_prescription_clinical_diagnosis({"diagnosis_type": "follow_up"}) == "复诊"
|
||
assert build_prescription_clinical_diagnosis({"clinical_diagnosis": "follow_up"}) == "复诊"
|
||
assert (
|
||
build_prescription_clinical_diagnosis(
|
||
{"symptoms": "肝郁脾虚", "diagnosis_type": "follow_up"}
|
||
)
|
||
== "肝郁脾虚"
|
||
)
|
||
assert build_prescription_clinical_diagnosis({"clinical_diagnosis": "气阴两虚"}) == "气阴两虚"
|
||
|
||
|
||
def test_remote_medicine_selector_rejects_new_free_text(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = SimpleNamespace(list_medicines=lambda **_kwargs: {"lists": [], "count": 0})
|
||
selector = RemoteMedicineComboBox(repository, name="历史药名")
|
||
|
||
assert selector.has_valid_selection
|
||
selector._queue_search("随意输入")
|
||
selector._timer.stop()
|
||
selector.setEditText("随意输入")
|
||
assert not selector.has_valid_selection
|
||
selector.set_value(11, "黄芪")
|
||
assert selector.has_valid_selection
|
||
selector.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_template_disable_edit_controls_import_not_template_maintenance(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
repository = SimpleNamespace(
|
||
list_medicines=lambda **_kwargs: {
|
||
"lists": [{"id": 11, "name": "黄芪"}],
|
||
"count": 1,
|
||
}
|
||
)
|
||
template = {
|
||
"id": 8,
|
||
"prescription_name": "益气方",
|
||
"formula_type": "主方",
|
||
"is_public": 1,
|
||
"disable_edit": 1,
|
||
"herbs": [{"medicine_id": 11, "name": "黄芪", "dosage": 15}],
|
||
}
|
||
dialog = PrescriptionTemplateDialog(repository, template, mode="edit")
|
||
|
||
assert dialog.disable_edit_check.isChecked()
|
||
assert dialog.herbs.rows[0].medicine.isEnabled()
|
||
assert dialog.herbs.rows[0].dosage.isEnabled()
|
||
assert dialog.payload() == {
|
||
"id": 8,
|
||
"prescription_name": "益气方",
|
||
"formula_type": "主方",
|
||
"is_public": 1,
|
||
"disable_edit": 1,
|
||
"herbs": [{"medicine_id": 11, "name": "黄芪", "dosage": 15.0}],
|
||
}
|
||
dialog.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_library_page_uses_canonical_permissions_and_full_columns(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
class Repository:
|
||
def list_prescription_templates(self, **_filters: Any) -> dict[str, Any]:
|
||
return {
|
||
"lists": [
|
||
{
|
||
"id": 3,
|
||
"prescription_name": "安神方",
|
||
"formula_type": "辅方",
|
||
"herbs": [{"name": "酸枣仁", "dosage": 12}],
|
||
"is_public": 0,
|
||
"disable_edit": 1,
|
||
"creator_id": 9,
|
||
"creator_name": "张医生",
|
||
"create_time": "2026-08-10 12:00:00",
|
||
}
|
||
],
|
||
"count": 1,
|
||
}
|
||
|
||
permissions = {
|
||
"wcf.prescription/add",
|
||
"wcf.prescription/read",
|
||
"wcf.prescription/edit",
|
||
"wcf.prescription/delete",
|
||
}
|
||
page = PrescriptionLibraryPage(
|
||
Repository(),
|
||
permissions,
|
||
SimpleNamespace(id=9, root=0, role_ids=[]),
|
||
)
|
||
page.refresh()
|
||
page.table.selectRow(0)
|
||
page._selection_changed()
|
||
|
||
assert page.table.columnCount() == 10
|
||
assert [
|
||
page.table.horizontalHeaderItem(index).text()
|
||
for index in range(page.table.columnCount())
|
||
] == [
|
||
"ID",
|
||
"处方名称",
|
||
"处方类型",
|
||
"药材数量",
|
||
"药材组成(部分)",
|
||
"功效主治",
|
||
"公开范围",
|
||
"创建人",
|
||
"创建时间",
|
||
"操作",
|
||
]
|
||
assert page.table.cellWidget(0, 2) is not None
|
||
assert page.table.cellWidget(0, 6) is not None
|
||
assert page.table.cellWidget(0, 9) is not None
|
||
assert not page.view_button.isHidden() and page.view_button.isEnabled()
|
||
assert not page.ai_button.isHidden() and page.ai_button.isEnabled()
|
||
assert page.ai_button.text() == "AI解释"
|
||
assert not page.edit_button.isHidden() and page.edit_button.isEnabled()
|
||
assert not page.delete_button.isHidden() and page.delete_button.isEnabled()
|
||
assert page.pager.page_size == 15
|
||
assert page.metric_cards["total"].value_label.text() == "1"
|
||
assert page.metric_cards["private"].value_label.text() == "1"
|
||
assert page.metric_cards["public"].value_label.text() == "0"
|
||
assert page.metric_cards["month"].value_label.text() == "1"
|
||
page.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_issued_page_sends_exact_filter_dto_and_row_guards(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
calls: list[dict[str, Any]] = []
|
||
pending = {
|
||
"id": 21,
|
||
"sn": "CF-21",
|
||
"patient_name": "林晓岚",
|
||
"gender": 0,
|
||
"age": 33,
|
||
"audit_status": 0,
|
||
"void_status": 0,
|
||
"has_prescription_order": 0,
|
||
"creator_id": 7,
|
||
"doctor_name": "周医生",
|
||
"prescription_date": "2026-08-10",
|
||
"create_time": "2026-08-10 12:00:00",
|
||
"herbs": [{"name": "黄芪", "dosage": 15}],
|
||
}
|
||
|
||
class Repository:
|
||
def list_diagnosis_doctors(self) -> list[dict[str, Any]]:
|
||
return [{"id": 9, "name": "孙医生"}]
|
||
|
||
def list_prescriptions(self, **filters: Any) -> dict[str, Any]:
|
||
calls.append(filters)
|
||
return {"lists": [pending], "count": 1}
|
||
|
||
permissions = {
|
||
"cf.prescription/add",
|
||
"cf.prescription/read",
|
||
"cf.prescription/edit",
|
||
"cf.prescription/audit",
|
||
"cf.prescription/del",
|
||
"tcm.prescription/patchPatient",
|
||
"tcm.prescriptionOrder/create",
|
||
"tcm.prescriptionOrder/lists",
|
||
}
|
||
page = PrescriptionsPage(
|
||
Repository(),
|
||
permissions,
|
||
SimpleNamespace(id=7, name="周医生"),
|
||
)
|
||
page.refresh()
|
||
page.table.selectRow(0)
|
||
page._selection_changed()
|
||
|
||
assert page.table.columnCount() == 11
|
||
assert page.table.horizontalHeaderItem(2).text() == "操作"
|
||
assert page.table.cellWidget(0, 2) is not None
|
||
assert page.table.cellWidget(0, 3) is not None
|
||
assert page.table.cellWidget(0, 6) is not None
|
||
row_edit = next(
|
||
button
|
||
for button in page.table.cellWidget(0, 2).findChildren(QPushButton)
|
||
if button.accessibleName() == "编辑处方"
|
||
)
|
||
assert row_edit.text() == "编辑"
|
||
assert row_edit.isEnabled()
|
||
|
||
assert calls == [
|
||
{
|
||
"page_no": 1,
|
||
"page_size": 15,
|
||
"sn": "",
|
||
"patient_name": "",
|
||
"audit_filter": "",
|
||
"source_filter": "",
|
||
"start_time": "",
|
||
"end_time": "",
|
||
}
|
||
]
|
||
page.quick_date.setCurrentIndex(1)
|
||
assert len(calls) == 2
|
||
assert calls[-1]["start_time"].endswith("00:00:00")
|
||
assert calls[-1]["end_time"].endswith("23:59:59")
|
||
assert page.audit_button.isEnabled()
|
||
assert page.doctor_filter._options[9] == "孙医生"
|
||
assert page.patch_button.isEnabled()
|
||
assert page.create_order_button.isEnabled()
|
||
assert page.edit_button.isEnabled()
|
||
assert page.delete_button.isEnabled()
|
||
|
||
page.table.set_rows([{**pending, "audit_status": 1}])
|
||
page.table.selectRow(0)
|
||
page._selection_changed()
|
||
assert not page.audit_button.isEnabled()
|
||
assert not page.edit_button.isEnabled()
|
||
assert not page.delete_button.isEnabled()
|
||
assert page.patch_button.isEnabled()
|
||
assert page.create_order_button.isEnabled()
|
||
page.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_issued_row_edit_targets_clicked_prescription_without_checkbox(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
requested: list[int] = []
|
||
opened: list[dict[str, Any]] = []
|
||
callbacks: list[str] = []
|
||
rows = [
|
||
{
|
||
"id": 11,
|
||
"sn": "CF-11",
|
||
"patient_name": "患者甲",
|
||
"audit_status": 0,
|
||
"void_status": 0,
|
||
"creator_id": 7,
|
||
},
|
||
{
|
||
"id": 22,
|
||
"sn": "CF-22",
|
||
"patient_name": "患者乙",
|
||
"audit_status": 0,
|
||
"void_status": 0,
|
||
"creator_id": 7,
|
||
},
|
||
]
|
||
|
||
class Repository:
|
||
def list_diagnosis_doctors(self) -> list[dict[str, Any]]:
|
||
return []
|
||
|
||
def list_prescriptions(self, **_filters: Any) -> dict[str, Any]:
|
||
return {"lists": rows, "count": len(rows)}
|
||
|
||
def get_prescription(self, prescription_id: int) -> dict[str, Any]:
|
||
requested.append(prescription_id)
|
||
return {
|
||
**next(row for row in rows if row["id"] == prescription_id),
|
||
"clinical_diagnosis": "脾气虚",
|
||
}
|
||
|
||
page = PrescriptionsPage(
|
||
Repository(),
|
||
PermissionSet(["cf.prescription/edit"]),
|
||
SimpleNamespace(id=7, name="周医生"),
|
||
)
|
||
page.refresh()
|
||
page._open_editor = lambda detail: opened.append(detail) # type: ignore[method-assign]
|
||
|
||
assert page.table.current_data()["id"] == 11
|
||
assert all(
|
||
page.table.item(row_index, 0).checkState() == Qt.CheckState.Unchecked
|
||
for row_index in range(page.table.rowCount())
|
||
)
|
||
second_actions = page.table.cellWidget(1, 2)
|
||
assert second_actions is not None
|
||
second_edit = next(
|
||
button
|
||
for button in second_actions.findChildren(QPushButton)
|
||
if button.accessibleName() == "编辑处方"
|
||
)
|
||
|
||
second_edit.click()
|
||
application.processEvents()
|
||
|
||
assert requested == [22]
|
||
assert [detail["id"] for detail in opened] == [22]
|
||
assert page.table.current_data()["id"] == 22
|
||
assert all(
|
||
page.table.item(row_index, 0).checkState() == Qt.CheckState.Unchecked
|
||
for row_index in range(page.table.rowCount())
|
||
)
|
||
|
||
page._run_row_action({"id": 999}, lambda: callbacks.append("stale"))
|
||
assert callbacks == []
|
||
page._set_mutation_pending(True)
|
||
assert not second_edit.isEnabled()
|
||
page._set_mutation_pending(False)
|
||
assert second_edit.isEnabled()
|
||
page.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_editor_builds_complete_add_payload(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
repository = SimpleNamespace(
|
||
list_medicines=lambda **_kwargs: {
|
||
"lists": [{"id": 31, "name": "黄芪"}],
|
||
"count": 1,
|
||
}
|
||
)
|
||
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, "黄芪")
|
||
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()
|
||
|
||
assert payload["creator_id"] == 7
|
||
assert payload["audit_status"] == 0
|
||
assert payload["patient_name"] == "林晓岚"
|
||
assert payload["clinical_diagnosis"] == "脾气虚"
|
||
assert payload["herbs"] == [
|
||
{
|
||
"medicine_id": 31,
|
||
"name": "黄芪",
|
||
"dosage": 15.0,
|
||
"formula_type": "主方",
|
||
}
|
||
]
|
||
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_prescription_workflows_never_show_orphan_child_controls(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
shown: list[tuple[str, str]] = []
|
||
|
||
class OrphanShowRecorder(QObject):
|
||
def eventFilter(self, watched: QObject, event: QEvent) -> bool: # noqa: N802
|
||
if (
|
||
event.type() == QEvent.Type.Show
|
||
and isinstance(watched, QWidget)
|
||
and not isinstance(watched, QDialog)
|
||
and watched.parentWidget() is None
|
||
):
|
||
text = getattr(watched, "text", lambda: "")()
|
||
shown.append((type(watched).__name__, str(text)))
|
||
return False
|
||
|
||
repository = SimpleNamespace(
|
||
list_medicines=lambda **_kwargs: {
|
||
"lists": [{"id": 31, "name": "黄芪"}],
|
||
"count": 1,
|
||
}
|
||
)
|
||
prescription = {
|
||
"id": 12,
|
||
"diagnosis_id": 6,
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800000000",
|
||
"audit_status": 1,
|
||
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
|
||
}
|
||
recorder = OrphanShowRecorder(application)
|
||
application.installEventFilter(recorder)
|
||
widgets: list[QWidget] = []
|
||
try:
|
||
widgets.extend(
|
||
[
|
||
dialog_module.HerbRowWidget(
|
||
repository,
|
||
prescription["herbs"][0],
|
||
show_formula=True,
|
||
locked=False,
|
||
),
|
||
dialog_module.HerbEditor(
|
||
repository,
|
||
show_formula=True,
|
||
show_actions=True,
|
||
),
|
||
PrescriptionEditorDialog(
|
||
repository,
|
||
prescription,
|
||
mode="edit",
|
||
permissions=PermissionSet(
|
||
["cf.prescription/edit", "tcm.prescriptionLibrary/lists"]
|
||
),
|
||
),
|
||
PrescriptionDetailDialog(
|
||
prescription,
|
||
can_open_diagnosis=True,
|
||
can_open_orders=True,
|
||
),
|
||
PrescriptionOrderDialog(
|
||
repository,
|
||
prescription,
|
||
can_view_internal_cost=True,
|
||
can_edit_pharmacy_remark=True,
|
||
),
|
||
]
|
||
)
|
||
finally:
|
||
application.removeEventFilter(recorder)
|
||
|
||
assert shown == []
|
||
for widget in widgets:
|
||
widget.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,
|
||
"audit_remark": "请补充辨证依据",
|
||
"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.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() == "脉象哨兵"
|
||
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 "请补充辨证依据" 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()
|
||
|
||
|
||
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)
|
||
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()
|
||
|
||
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_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:
|
||
dialog = AuditPrescriptionDialog({"id": 4})
|
||
dialog._choose("reject")
|
||
assert dialog.action == ""
|
||
assert not dialog.banner.isHidden()
|
||
dialog.remark.setPlainText("剂量需调整")
|
||
dialog._choose("reject")
|
||
assert dialog.action == "reject"
|
||
assert dialog.payload() == {
|
||
"id": 4,
|
||
"action": "reject",
|
||
"remark": "剂量需调整",
|
||
}
|
||
dialog.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_order_payload_and_a4_print_document(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
repository = SimpleNamespace(
|
||
list_paid_prescription_orders=lambda diagnosis_id: {
|
||
"lists": [{"id": 88, "order_no": "PAY-88", "amount": 100}],
|
||
"deposit_min_amount": 100,
|
||
}
|
||
)
|
||
prescription = {
|
||
"id": 12,
|
||
"diagnosis_id": 6,
|
||
"sn": "CF-12",
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800000000",
|
||
"gender": 0,
|
||
"age": 33,
|
||
"clinical_diagnosis": "脾气虚",
|
||
"doctor_name": "周医生",
|
||
"prescription_date": "2026-08-10",
|
||
"audit_status": 1,
|
||
"dose_count": 7,
|
||
"dose_unit": "剂",
|
||
"usage_days": 7,
|
||
"times_per_day": 2,
|
||
"dosage_amount": 5,
|
||
"dosage_bag_count": 2,
|
||
"dosage_unit": "g",
|
||
"usage_way": "温水送服",
|
||
"usage_time": "饭后",
|
||
"herbs": [
|
||
{"name": "黄芪", "dosage": 15, "formula_type": "主方"},
|
||
{"name": "酸枣仁", "dosage": 12, "formula_type": "辅方"},
|
||
],
|
||
}
|
||
order = PrescriptionOrderDialog(repository, prescription)
|
||
order._load_paid_orders()
|
||
order.shipping_province.setText("四川省")
|
||
order.shipping_city.setText("成都市")
|
||
order.shipping_district.setText("双流区")
|
||
order.shipping_address.setText("黄龙大道 280 号")
|
||
order.amount.setValue(100)
|
||
order.paid_orders.item(0).setCheckState(Qt.CheckState.Checked)
|
||
payload = order.payload()
|
||
|
||
assert payload["prescription_id"] == 12
|
||
assert payload["diagnosis_id"] == 6
|
||
assert payload["pay_order_ids"] == [88]
|
||
assert payload["amount"] == 100
|
||
assert payload["ship_mode"] == "gancao"
|
||
|
||
rendered = render_prescription_html(prescription)
|
||
assert "林晓岚" in rendered
|
||
assert "黄芪" in rendered
|
||
assert "酸枣仁" in rendered
|
||
assert "服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点" in rendered
|
||
assert '<table align="center" class="paper" width="100%"' in rendered
|
||
assert 'height="1123"' not in rendered
|
||
assert '<col width="44"/><col/><col width="64"/><col width="24"/>' in rendered
|
||
assert "Rp." in rendered
|
||
assert "药房联" in rendered
|
||
assert "主方" in rendered
|
||
assert "辅方" in rendered
|
||
assert "105克" in rendered
|
||
assert "84克" in rendered
|
||
assert "剂量:</span> 27克" in rendered
|
||
assert "每天2次, 一次2袋, 每袋5g, 温水送服, 饭后" in rendered
|
||
assert "主服法:" in rendered
|
||
assert "辅服法:" in rendered
|
||
assert "处方编号:" in rendered
|
||
assert "流转编号(挂号):" in rendered
|
||
assert "成都双流甄养堂互联网医院有限公司 联系方式:4001667339" in rendered
|
||
assert "四川省成都市双流区黄甲街道黄龙大道二段280号" in rendered
|
||
user_rendered = render_prescription_html(prescription, variant="user")
|
||
assert "成都双流甄养堂互联网医院 处方笺" in user_rendered
|
||
assert "用量" in user_rendered
|
||
assert "总量" not in user_rendered
|
||
assert "主服法" not in user_rendered
|
||
assert "类型:" not in user_rendered
|
||
assert "黄芪 (15克)" not in user_rendered
|
||
assert "黄芪" in user_rendered
|
||
viewer = PrescriptionDetailDialog(prescription)
|
||
document_html = viewer.document.toHtml()
|
||
assert "用药 (单剂)" in document_html
|
||
assert viewer.preview.__class__.__name__ == "_PrescriptionPaperPreview"
|
||
assert [viewer.tabs.tabText(index) for index in range(2)] == ["药房联", "处方联"]
|
||
viewer.close()
|
||
order.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_prescription_pdf_export_writes_visible_content(
|
||
application: QApplication,
|
||
tmp_path: Any,
|
||
) -> None:
|
||
prescription = {
|
||
"id": 12,
|
||
"sn": "CF-12",
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800000000",
|
||
"gender": 0,
|
||
"age": 33,
|
||
"clinical_diagnosis": "脾气虚",
|
||
"doctor_name": "周医生",
|
||
"audit_status": 1,
|
||
"dose_count": 7,
|
||
"herbs": [{"name": "黄芪", "dosage": 15, "formula_type": "主方"}],
|
||
}
|
||
image = dialog_module.render_prescription_slip_image(prescription)
|
||
ink = dialog_module.count_slip_ink_pixels(image)
|
||
page = QImage(1240, 1754, QImage.Format.Format_RGB32)
|
||
page.fill(QColor("#ffffff"))
|
||
painter = QPainter(page)
|
||
dialog_module._draw_slip_image_on_page(painter, image)
|
||
painter.end()
|
||
page_ink = dialog_module.count_slip_ink_pixels(page)
|
||
png_path = tmp_path / "prescription-slip.png"
|
||
assert image.save(str(png_path), "PNG")
|
||
viewer = PrescriptionDetailDialog(prescription)
|
||
output = tmp_path / "prescription-slip.pdf"
|
||
app_module._install_chinese_translations(application)
|
||
viewer.export_pdf(output)
|
||
payload = output.read_bytes()
|
||
pdf = QPdfDocument()
|
||
assert pdf.load(str(output)) == QPdfDocument.Error.None_
|
||
assert pdf.pageCount() == 1
|
||
page_size = pdf.pagePointSize(0)
|
||
rendered_page = pdf.render(0, QSize(1240, 1754))
|
||
visible_ink = sum(
|
||
1
|
||
for y in range(0, rendered_page.height(), 3)
|
||
for x in range(0, rendered_page.width(), 3)
|
||
if (color := rendered_page.pixelColor(x, y)).alpha() > 0 and color.lightness() < 245
|
||
)
|
||
viewer.close()
|
||
application.processEvents()
|
||
|
||
assert dialog_module._ensure_slip_fonts()
|
||
assert image.format() == QImage.Format.Format_RGB32
|
||
assert image.width() >= 700
|
||
assert image.height() >= 400
|
||
assert ink > 200
|
||
assert page_ink > 200
|
||
assert png_path.stat().st_size > 40_000
|
||
assert payload.startswith(b"%PDF")
|
||
assert b"/Image" in payload or b"/XObject" in payload
|
||
assert len(payload) > 50_000
|
||
assert page_size.width() == pytest.approx(595.0, abs=2.0)
|
||
assert page_size.height() == pytest.approx(842.0, abs=2.0)
|
||
assert visible_ink > 1_000
|
||
|
||
|
||
def test_prescription_detail_can_open_immutable_case_record_tab(
|
||
application: QApplication,
|
||
) -> None:
|
||
prescription = {
|
||
"id": 12,
|
||
"diagnosis_id": 6,
|
||
"patient_name": "林晓岚",
|
||
"case_record": {
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800138000",
|
||
"diagnosis_type": "follow_up",
|
||
"local_hospital_name": "市中医院",
|
||
"symptoms": "口渴乏力",
|
||
"tongue_coating": "舌红少苔",
|
||
},
|
||
}
|
||
viewer = PrescriptionDetailDialog(prescription, initial_tab="case")
|
||
|
||
assert viewer.tabs.count() == 3
|
||
assert viewer.tabs.tabText(0) == "药房联"
|
||
assert viewer.tabs.tabText(1) == "处方联"
|
||
assert viewer.tabs.tabText(viewer.tabs.currentIndex()) == "详细病历"
|
||
assert viewer.case_document is not None
|
||
case_html = viewer.case_document.toHtml()
|
||
source_html = dialog_module.render_case_record_html(prescription)
|
||
assert "甄养堂 详细病历" in case_html
|
||
assert '<table class="cr-grid"' in source_html
|
||
assert "follow_up" not in case_html
|
||
assert "复诊" in case_html
|
||
assert "市中医院" in case_html
|
||
assert "口渴乏力" in case_html
|
||
assert viewer.case_preview is not None
|
||
assert viewer.case_preview.objectName() == "CaseRecordPaperPreview"
|
||
viewer.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_case_record_translates_snapshot_dictionary_codes_to_chinese() -> None:
|
||
# 处方快照存的是开方当时的原始 code,没有后端补的 *_text。
|
||
prescription = {
|
||
"id": 21,
|
||
"diagnosis_id": 9,
|
||
"case_record": {
|
||
"diagnosis": {
|
||
"appetite": "dry,bitter",
|
||
"water_intake": "one_bottle",
|
||
"weight_change": "lose_10_jin",
|
||
"fatty_liver_degree": "mild",
|
||
"past_history": "hypertension,diabetes",
|
||
"sleep_condition": "many_dreams",
|
||
}
|
||
},
|
||
}
|
||
|
||
case_html = dialog_module.render_case_record_html(prescription)
|
||
|
||
assert "干、苦" in case_html
|
||
assert "1瓶矿泉水" in case_html
|
||
assert "瘦10斤" in case_html
|
||
assert "轻度" in case_html
|
||
assert "高血压、糖尿病" in case_html
|
||
assert "多梦" in case_html
|
||
for code in ("lose_10_jin", "one_bottle", "many_dreams"):
|
||
assert code not in case_html
|
||
|
||
|
||
def test_case_record_tab_exports_case_record_as_a3_pdf(
|
||
application: QApplication,
|
||
tmp_path: Any,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
prescription = {
|
||
"id": 12,
|
||
"diagnosis_id": 8169,
|
||
"patient_name": "何福萍",
|
||
"case_record": {
|
||
"diagnosis_id": 8169,
|
||
"patient_name": "何福萍",
|
||
"phone": "13800138000",
|
||
"diagnosis_type": "follow_up",
|
||
"local_hospital_name": "市中医院",
|
||
"symptoms": "口渴、乏力、睡眠欠佳",
|
||
"tongue_coating": "舌红少苔",
|
||
"pulse": "脉细数",
|
||
"doctor_advice": "规律复诊",
|
||
},
|
||
}
|
||
case_render_calls: list[str] = []
|
||
original_case_renderer = dialog_module.render_case_record_image
|
||
|
||
def render_case(*args: Any, **kwargs: Any) -> QImage:
|
||
case_render_calls.append("case")
|
||
return original_case_renderer(*args, **kwargs)
|
||
|
||
def reject_prescription_render(*_args: Any, **_kwargs: Any) -> QImage:
|
||
pytest.fail("详细病历页签不应调用处方笺渲染器")
|
||
|
||
monkeypatch.setattr(dialog_module, "render_case_record_image", render_case)
|
||
monkeypatch.setattr(
|
||
dialog_module,
|
||
"render_prescription_slip_image",
|
||
reject_prescription_render,
|
||
)
|
||
app_module._install_chinese_translations(application)
|
||
viewer = PrescriptionDetailDialog(prescription)
|
||
case_index = next(
|
||
index for index in range(viewer.tabs.count()) if viewer.tabs.tabText(index) == "详细病历"
|
||
)
|
||
viewer.tabs.setCurrentIndex(case_index)
|
||
output = tmp_path / "case-record.pdf"
|
||
|
||
viewer.export_pdf(output)
|
||
|
||
pdf = QPdfDocument()
|
||
assert pdf.load(str(output)) == QPdfDocument.Error.None_
|
||
assert pdf.pageCount() == 1
|
||
page_size = pdf.pagePointSize(0)
|
||
rendered_page = pdf.render(0, QSize(1400, 1980))
|
||
visible_ink = sum(
|
||
1
|
||
for y in range(0, rendered_page.height(), 4)
|
||
for x in range(0, rendered_page.width(), 4)
|
||
if (color := rendered_page.pixelColor(x, y)).alpha() > 0 and color.lightness() < 245
|
||
)
|
||
viewer.close()
|
||
application.processEvents()
|
||
|
||
assert case_render_calls == ["case"]
|
||
assert page_size.width() == pytest.approx(842.0, abs=2.0)
|
||
assert page_size.height() == pytest.approx(1191.0, abs=2.0)
|
||
assert visible_ink > 1_000
|
||
|
||
|
||
def test_demo_repository_pages_render_offscreen(
|
||
application: QApplication,
|
||
immediate_async: None,
|
||
) -> None:
|
||
repository = DemoDoctorRepository()
|
||
user = SimpleNamespace(id=1, name="演示医生", root=1, role_ids=[0])
|
||
library = PrescriptionLibraryPage(repository, None, user)
|
||
issued = PrescriptionsPage(repository, None, user)
|
||
library.refresh()
|
||
issued.refresh()
|
||
|
||
assert library.table.rowCount() > 0
|
||
assert issued.table.rowCount() > 0
|
||
library.close()
|
||
issued.close()
|
||
application.processEvents()
|