Files
zyt/app/tests/test_prescription_ui.py
T
2026-08-14 14:37:30 +08:00

1015 lines
36 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(10).text() == "操作"
assert page.table.cellWidget(0, 2) is not None
assert page.table.cellWidget(0, 5) is not None
assert page.table.cellWidget(0, 10) is not None
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)
# 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_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()