425 lines
13 KiB
Python
425 lines
13 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 Qt
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
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,
|
|
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) == ("已作废", "danger")
|
|
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_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() == 9
|
|
assert not page.view_button.isHidden() and page.view_button.isEnabled()
|
|
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
|
|
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 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_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)
|
|
editor.patient_name.setText("林晓岚")
|
|
editor.clinical_diagnosis.setPlainText("脾气虚")
|
|
editor.herbs.rows[0].medicine.set_value(31, "黄芪")
|
|
editor.herbs.rows[0].dosage.setValue(15)
|
|
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["doctor_signature"].startswith("data:image/png;base64,")
|
|
assert isinstance(payload["aux_usage"], dict)
|
|
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,
|
|
"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
|
|
viewer = PrescriptionDetailDialog(prescription)
|
|
assert "中医处方笺" in viewer.document.toHtml()
|
|
viewer.close()
|
|
order.close()
|
|
application.processEvents()
|
|
|
|
|
|
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()
|