更新
This commit is contained in:
@@ -76,6 +76,7 @@ _ORDER_DETAIL_PERMISSION = "tcm.prescriptionOrder/detail"
|
||||
_ORDER_LOGS_PERMISSION = "tcm.prescriptionOrder/logs"
|
||||
_DAILY_PERMISSION = "tcm.diagnosis/dailyRecord"
|
||||
_PRESCRIPTION_PERMISSION = "tcm.diagnosis/chufang"
|
||||
_PRESCRIPTION_EDIT_PERMISSION = "cf.prescription/edit"
|
||||
_OFFSET_PERMISSION = "tcm.diagnosis/setRevisitSlotStartOffset"
|
||||
_VIDEO_PERMISSION = "tcm.diagnosis/huifang"
|
||||
_CHAT_PERMISSION = "tcm.diagnosis/chat"
|
||||
@@ -798,6 +799,9 @@ class DiagnosisDialog(QDialog):
|
||||
self._can_prescribe = has_permission(
|
||||
self.permissions, _PRESCRIPTION_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "create_prescription", None))
|
||||
self._can_edit_prescription = has_permission(
|
||||
self.permissions, _PRESCRIPTION_EDIT_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "update_prescription", None))
|
||||
self._can_offset = has_permission(
|
||||
self.permissions, _OFFSET_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "set_revisit_slot_start_offset", None))
|
||||
@@ -826,6 +830,8 @@ class DiagnosisDialog(QDialog):
|
||||
self._daily_mutation_generation = 0
|
||||
self._notes_mutation_generation = 0
|
||||
self._media_generation = 0
|
||||
self._prescription_detail_generation = 0
|
||||
self._prescription_detail_target = 0
|
||||
self._offset_generation = 0
|
||||
self._tab_generations: dict[str, int] = {key: 0 for key, _label, _codes in _TAB_DEFINITIONS}
|
||||
self._loaded_tabs: set[str] = set()
|
||||
@@ -1631,6 +1637,9 @@ class DiagnosisDialog(QDialog):
|
||||
self._can_prescribe = has_permission(
|
||||
self.permissions, _PRESCRIPTION_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "create_prescription", None))
|
||||
self._can_edit_prescription = has_permission(
|
||||
self.permissions, _PRESCRIPTION_EDIT_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "update_prescription", None))
|
||||
self._can_offset = has_permission(
|
||||
self.permissions, _OFFSET_PERMISSION, default=False
|
||||
) and callable(getattr(self.repository, "set_revisit_slot_start_offset", None))
|
||||
@@ -1895,6 +1904,7 @@ class DiagnosisDialog(QDialog):
|
||||
self._daily_mutation_generation += 1
|
||||
self._notes_mutation_generation += 1
|
||||
self._media_generation += 1
|
||||
self._prescription_detail_generation += 1
|
||||
self._offset_generation += 1
|
||||
for key in self._tab_generations:
|
||||
self._tab_generations[key] += 1
|
||||
@@ -2176,6 +2186,7 @@ class DiagnosisDialog(QDialog):
|
||||
self._daily_mutation_generation += 1
|
||||
self._notes_mutation_generation += 1
|
||||
self._media_generation += 1
|
||||
self._prescription_detail_generation += 1
|
||||
self._offset_generation += 1
|
||||
for key in self._tab_generations:
|
||||
self._tab_generations[key] += 1
|
||||
@@ -3012,7 +3023,9 @@ class DiagnosisDialog(QDialog):
|
||||
diagnosis_id=diagnosis_id, appointment_id=appointment_id
|
||||
),
|
||||
"tongue": first_value(diagnosis, "tongue", "tongue_coating"),
|
||||
"tongue_image": first_value(diagnosis, "tongue_image"),
|
||||
"pulse": first_value(diagnosis, "pulse"),
|
||||
"pulse_condition": first_value(diagnosis, "pulse_condition"),
|
||||
"clinical_diagnosis": build_prescription_clinical_diagnosis(diagnosis, case_record),
|
||||
"doctor_name": first_value(
|
||||
appointment, "doctor_name", default=first_value(diagnosis, "doctor_name")
|
||||
@@ -3025,6 +3038,7 @@ class DiagnosisDialog(QDialog):
|
||||
current_user=getattr(self.parentWidget(), "current_user", None),
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_current_diagnosis_detail)
|
||||
dialog.setObjectName("DiagnosisPrescriptionEditor")
|
||||
dialog.setStyleSheet(DIAGNOSIS_QSS)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
@@ -3343,13 +3357,106 @@ class DiagnosisDialog(QDialog):
|
||||
table.set_matrix(matrix)
|
||||
self._semanticize(table, 5)
|
||||
for row_index, row in enumerate(rows):
|
||||
actions = QWidget()
|
||||
actions_layout = QHBoxLayout(actions)
|
||||
actions_layout.setContentsMargins(0, 0, 0, 0)
|
||||
actions_layout.setSpacing(6)
|
||||
button = self._action_button("查看", "打开真实处方详情")
|
||||
button.clicked.connect(
|
||||
lambda _checked=False, source=row: self._view_prescription(source)
|
||||
)
|
||||
table.setCellWidget(row_index, 6, button)
|
||||
actions_layout.addWidget(button)
|
||||
approved = _int(first_value(row, "audit_status", "status", default=-1), -1) == 1
|
||||
voided = _truthy(first_value(row, "void_status", "is_void", default=False))
|
||||
can_edit = (
|
||||
self._editable
|
||||
and self._can_edit_prescription
|
||||
and (not approved or voided)
|
||||
)
|
||||
if can_edit:
|
||||
edit_button = self._action_button("编辑", "使用完整处方表单编辑")
|
||||
edit_button.clicked.connect(
|
||||
lambda _checked=False, source=row: self._edit_prescription(source)
|
||||
)
|
||||
actions_layout.addWidget(edit_button)
|
||||
actions_layout.addStretch(1)
|
||||
table.setCellWidget(row_index, 6, actions)
|
||||
|
||||
def _view_prescription(self, prescription: Any) -> None:
|
||||
prescription_id = _int(first_value(prescription, "id", "prescription_id", default=0), 0)
|
||||
method = getattr(self.repository, "get_prescription", None)
|
||||
if prescription_id <= 0 or not callable(method):
|
||||
self._show_message("无法加载完整处方详情。", "warning")
|
||||
return
|
||||
self._prescription_detail_generation += 1
|
||||
generation = self._prescription_detail_generation
|
||||
self._prescription_detail_target = prescription_id
|
||||
self._show_message("正在加载完整处方详情…", "info")
|
||||
run_async(
|
||||
lambda: method(prescription_id),
|
||||
on_success=lambda detail: self._prescription_detail_loaded(
|
||||
detail, prescription_id, generation, edit=False
|
||||
),
|
||||
on_error=lambda error: self._prescription_detail_error(
|
||||
error, prescription_id, generation
|
||||
),
|
||||
)
|
||||
|
||||
def _edit_prescription(self, prescription: Any) -> None:
|
||||
if (
|
||||
not self._editable
|
||||
or not self._can_edit_prescription
|
||||
):
|
||||
self._show_message("当前账号无处方编辑权限或接口不可用。", "warning")
|
||||
return
|
||||
prescription_id = _int(first_value(prescription, "id", "prescription_id", default=0), 0)
|
||||
if prescription_id <= 0:
|
||||
self._show_message("处方编号不完整,无法编辑。", "warning")
|
||||
return
|
||||
approved = _int(first_value(prescription, "audit_status", "status", default=-1), -1) == 1
|
||||
voided = _truthy(first_value(prescription, "void_status", "is_void", default=False))
|
||||
if approved and not voided:
|
||||
self._show_message("审核通过且未作废的处方只能查看。", "warning")
|
||||
return
|
||||
method = getattr(self.repository, "get_prescription", None)
|
||||
if not callable(method):
|
||||
self._show_message("无法加载完整处方详情。", "warning")
|
||||
return
|
||||
self._prescription_detail_generation += 1
|
||||
generation = self._prescription_detail_generation
|
||||
self._prescription_detail_target = prescription_id
|
||||
self._show_message("正在准备编辑处方…", "info")
|
||||
run_async(
|
||||
lambda: method(prescription_id),
|
||||
on_success=lambda detail: self._prescription_detail_loaded(
|
||||
detail, prescription_id, generation, edit=True
|
||||
),
|
||||
on_error=lambda error: self._prescription_detail_error(
|
||||
error, prescription_id, generation
|
||||
),
|
||||
)
|
||||
|
||||
def _prescription_detail_loaded(
|
||||
self,
|
||||
prescription: Any,
|
||||
prescription_id: int,
|
||||
generation: int,
|
||||
*,
|
||||
edit: bool,
|
||||
) -> None:
|
||||
if (
|
||||
generation != self._prescription_detail_generation
|
||||
or prescription_id != self._prescription_detail_target
|
||||
):
|
||||
return
|
||||
actual_id = _int(first_value(prescription, "id", "prescription_id", default=0), 0)
|
||||
if actual_id != prescription_id:
|
||||
self._show_message("服务端处方与当前行不一致,已停止打开。", "danger")
|
||||
return
|
||||
self._clear_message()
|
||||
if edit:
|
||||
self._open_prescription_edit_dialog(prescription, prescription_id)
|
||||
return
|
||||
from .prescription import PrescriptionDetailDialog
|
||||
|
||||
dialog = PrescriptionDetailDialog(
|
||||
@@ -3360,6 +3467,55 @@ class DiagnosisDialog(QDialog):
|
||||
)
|
||||
dialog.exec()
|
||||
|
||||
def _prescription_detail_error(
|
||||
self, error: Exception, prescription_id: int, generation: int
|
||||
) -> None:
|
||||
if (
|
||||
generation == self._prescription_detail_generation
|
||||
and prescription_id == self._prescription_detail_target
|
||||
):
|
||||
self._show_message(friendly_error(error), "danger")
|
||||
|
||||
def _open_prescription_edit_dialog(self, prescription: Any, prescription_id: int) -> None:
|
||||
from .prescription import PrescriptionEditorDialog
|
||||
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
prescription,
|
||||
mode="edit",
|
||||
current_user=getattr(self.parentWidget(), "current_user", None),
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_current_diagnosis_detail)
|
||||
dialog.setObjectName("DiagnosisPrescriptionEditor")
|
||||
dialog.setStyleSheet(DIAGNOSIS_QSS)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
payload = dialog.payload()
|
||||
self._run_media_mutation(
|
||||
lambda: invoke(
|
||||
self.repository,
|
||||
"update_prescription",
|
||||
prescription=prescription_id,
|
||||
changes=payload,
|
||||
),
|
||||
"处方已保存并重新进入待审核。",
|
||||
"prescription",
|
||||
)
|
||||
|
||||
def _open_current_diagnosis_detail(self, diagnosis_id: int) -> None:
|
||||
if diagnosis_id <= 0 or diagnosis_id != self._diagnosis_id or not self._detail:
|
||||
self._show_message("当前处方关联的诊单详情不可用。", "warning")
|
||||
return
|
||||
from .prescription import DiagnosisDetailDialog
|
||||
|
||||
DiagnosisDetailDialog(
|
||||
self._detail,
|
||||
self,
|
||||
repository=self.repository,
|
||||
permissions=self.permissions,
|
||||
).exec()
|
||||
|
||||
def _stop_inline_recordings(self) -> None:
|
||||
for cell in list(getattr(self, "_inline_recording_cells", [])):
|
||||
with suppress(RuntimeError):
|
||||
|
||||
@@ -76,6 +76,7 @@ from ..widgets import (
|
||||
first_value,
|
||||
friendly_error,
|
||||
get_value,
|
||||
has_permission,
|
||||
invoke,
|
||||
page_items,
|
||||
page_total,
|
||||
@@ -542,6 +543,59 @@ def _duplicate_herb_names(herbs: Sequence[Mapping[str, Any]]) -> list[str]:
|
||||
return duplicates
|
||||
|
||||
|
||||
class MultiSelectComboBox(QComboBox):
|
||||
"""Compact fixed-option multi-select matching Element Plus ``multiple`` selects."""
|
||||
|
||||
def __init__(self, options: Sequence[str], parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setEditable(True)
|
||||
if self.lineEdit() is not None:
|
||||
self.lineEdit().setReadOnly(True)
|
||||
self.lineEdit().setPlaceholderText("请选择忌口内容(可多选)")
|
||||
for option in options:
|
||||
self.addItem(option, option)
|
||||
self.setItemData(self.count() - 1, Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
|
||||
self.view().pressed.connect(self._toggle_item)
|
||||
# QComboBox applies the clicked item's text after ``pressed``. Refresh
|
||||
# on the next event-loop tick so the field always shows every checked
|
||||
# value instead of only the last clicked one.
|
||||
self.currentIndexChanged.connect(lambda _index: QTimer.singleShot(0, self._refresh_text))
|
||||
self._refresh_text()
|
||||
|
||||
def _toggle_item(self, index: Any) -> None:
|
||||
row = index.row()
|
||||
checked = self.itemData(row, Qt.ItemDataRole.CheckStateRole) == Qt.CheckState.Checked
|
||||
self.setItemData(
|
||||
row,
|
||||
Qt.CheckState.Unchecked if checked else Qt.CheckState.Checked,
|
||||
Qt.ItemDataRole.CheckStateRole,
|
||||
)
|
||||
self._refresh_text()
|
||||
|
||||
def set_values(self, values: Iterable[Any]) -> None:
|
||||
selected = {str(value).strip() for value in values if str(value).strip()}
|
||||
for row in range(self.count()):
|
||||
self.setItemData(
|
||||
row,
|
||||
Qt.CheckState.Checked
|
||||
if str(self.itemData(row)).strip() in selected
|
||||
else Qt.CheckState.Unchecked,
|
||||
Qt.ItemDataRole.CheckStateRole,
|
||||
)
|
||||
self._refresh_text()
|
||||
|
||||
def values(self) -> list[str]:
|
||||
return [
|
||||
str(self.itemData(row))
|
||||
for row in range(self.count())
|
||||
if self.itemData(row, Qt.ItemDataRole.CheckStateRole) == Qt.CheckState.Checked
|
||||
]
|
||||
|
||||
def _refresh_text(self) -> None:
|
||||
if self.lineEdit() is not None:
|
||||
self.lineEdit().setText("、".join(self.values()))
|
||||
|
||||
|
||||
class RemoteMedicineComboBox(QComboBox):
|
||||
"""Editable remote medicine selector retaining both id and canonical name."""
|
||||
|
||||
@@ -889,6 +943,10 @@ class HerbEditor(QWidget):
|
||||
self.rows_changed.emit()
|
||||
|
||||
def set_rows(self, herbs: Iterable[Any], *, locked: bool = False) -> None:
|
||||
herb_rows = list(herbs)
|
||||
locked = locked or any(
|
||||
_bool(first_value(herb, "locked", default=False)) for herb in herb_rows
|
||||
)
|
||||
self._suspend_changes = True
|
||||
self.clear()
|
||||
self._locked = locked
|
||||
@@ -904,7 +962,7 @@ class HerbEditor(QWidget):
|
||||
)
|
||||
else:
|
||||
self.lock_banner.clear()
|
||||
for herb in herbs:
|
||||
for herb in herb_rows:
|
||||
row_locked = locked or _bool(first_value(herb, "locked", default=False))
|
||||
self.add_row(herb, locked=row_locked)
|
||||
self._suspend_changes = False
|
||||
@@ -1576,6 +1634,8 @@ class _PrescriptionSectionNavigator:
|
||||
class PrescriptionEditorDialog(QDialog):
|
||||
"""Full issued-prescription add/edit form matching the admin DTO."""
|
||||
|
||||
diagnosis_requested = Signal(int)
|
||||
|
||||
PRESCRIPTION_TYPES = ("浓缩水丸", "饮片", "颗粒", "丸剂", "散剂", "膏方", "汤剂")
|
||||
DIETARY_OPTIONS = (
|
||||
"辛辣食物",
|
||||
@@ -1600,6 +1660,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
*,
|
||||
mode: str = "add",
|
||||
current_user: Any = None,
|
||||
permissions: Any = None,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -1607,7 +1668,15 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.prescription = prescription
|
||||
self.mode = mode
|
||||
self.current_user = current_user
|
||||
self.permissions = (
|
||||
permissions
|
||||
if permissions is not None
|
||||
else getattr(parent, "permissions", None)
|
||||
if parent is not None
|
||||
else None
|
||||
)
|
||||
self._source = _mapping(prescription)
|
||||
self._linked_order_generation = 0
|
||||
self._prescribing_creator_id = _int(
|
||||
first_value(
|
||||
prescription,
|
||||
@@ -1644,6 +1713,10 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.body_layout.setContentsMargins(24, 20, 24, 28)
|
||||
self.body_layout.setSpacing(18)
|
||||
|
||||
self.context_banner = MessageBanner()
|
||||
self.context_banner.setObjectName("PrescriptionEditorContextBanner")
|
||||
self.body_layout.addWidget(self.context_banner)
|
||||
|
||||
self.patient_section = self._build_patient_section()
|
||||
self.diagnosis_section = self._build_diagnosis_section()
|
||||
self.herbs_section = self._build_herbs_section()
|
||||
@@ -1684,6 +1757,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
|
||||
self.tabs = _PrescriptionSectionNavigator(self)
|
||||
self._load_values()
|
||||
self._load_editor_context()
|
||||
|
||||
def _build_header(self) -> QFrame:
|
||||
header = QFrame()
|
||||
@@ -1701,6 +1775,14 @@ class PrescriptionEditorDialog(QDialog):
|
||||
subtitle.setObjectName("PrescriptionDrawerSubtitle")
|
||||
copy.addWidget(subtitle)
|
||||
layout.addLayout(copy, 1)
|
||||
diagnosis_id = _int(self._source.get("diagnosis_id"), 0)
|
||||
self.diagnosis_button = QPushButton("查看患者诊单详情")
|
||||
self.diagnosis_button.setObjectName("PrescriptionDrawerDiagnosisButton")
|
||||
self.diagnosis_button.setVisible(diagnosis_id > 0)
|
||||
self.diagnosis_button.clicked.connect(
|
||||
lambda _checked=False: self.diagnosis_requested.emit(diagnosis_id)
|
||||
)
|
||||
layout.addWidget(self.diagnosis_button, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
mode_badge = QLabel("新增" if self.mode == "add" else "编辑")
|
||||
mode_badge.setObjectName("PrescriptionDrawerMode")
|
||||
layout.addWidget(mode_badge, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
@@ -1800,20 +1882,20 @@ class PrescriptionEditorDialog(QDialog):
|
||||
section, layout = self._section("诊断信息", "辨证信息随处方一并留档")
|
||||
grid = QGridLayout()
|
||||
self._prepare_grid(grid, 2)
|
||||
self.pulse = QLineEdit()
|
||||
self.pulse.setPlaceholderText("请输入面象")
|
||||
self.tongue = QLineEdit()
|
||||
self.tongue.setPlaceholderText("请输入舌象")
|
||||
self.tongue.setPlaceholderText("请输入面象")
|
||||
self.tongue_image = QLineEdit()
|
||||
self.tongue_image.setPlaceholderText("补充舌象详情")
|
||||
self.tongue_image.setPlaceholderText("请输入舌象")
|
||||
self.pulse = QLineEdit()
|
||||
self.pulse.setPlaceholderText("请输入脉象")
|
||||
self.pulse_condition = QLineEdit()
|
||||
self.pulse_condition.setPlaceholderText("补充脉象详情")
|
||||
self.pulse_condition.setPlaceholderText("请输入脉象详情")
|
||||
self.clinical_diagnosis = QTextEdit()
|
||||
self.clinical_diagnosis.setPlaceholderText("请输入临床诊断")
|
||||
self.clinical_diagnosis.setMaximumHeight(86)
|
||||
self._place_field(grid, 0, 0, "面象", self.pulse)
|
||||
self._place_field(grid, 0, 1, "舌象", self.tongue)
|
||||
self._place_field(grid, 1, 0, "舌象详情", self.tongue_image)
|
||||
self._place_field(grid, 0, 0, "面象", self.tongue)
|
||||
self._place_field(grid, 0, 1, "舌象", self.tongue_image)
|
||||
self._place_field(grid, 1, 0, "脉象", self.pulse)
|
||||
self._place_field(grid, 1, 1, "脉象详情", self.pulse_condition)
|
||||
self._place_field(
|
||||
grid,
|
||||
@@ -1871,6 +1953,17 @@ class PrescriptionEditorDialog(QDialog):
|
||||
actions.addWidget(self.add_aux_button)
|
||||
self.import_library_button = QPushButton("从处方库导入")
|
||||
self.import_library_button.clicked.connect(self._import_library)
|
||||
library_default = self.permissions is None
|
||||
self.import_library_button.setVisible(
|
||||
any(
|
||||
has_permission(self.permissions, code, default=library_default)
|
||||
for code in (
|
||||
"cf.prescription/add",
|
||||
"cf.prescription/edit",
|
||||
"tcm.prescriptionLibrary/lists",
|
||||
)
|
||||
)
|
||||
)
|
||||
actions.addWidget(self.import_library_button)
|
||||
self.paste_button = QPushButton("导入药方")
|
||||
self.paste_button.clicked.connect(self._paste_herbs)
|
||||
@@ -1885,6 +1978,10 @@ class PrescriptionEditorDialog(QDialog):
|
||||
)
|
||||
self.herbs.rows_changed.connect(self._update_herb_summary)
|
||||
layout.addWidget(self.herbs)
|
||||
self.linked_order_banner = MessageBanner()
|
||||
self.linked_order_banner.setObjectName("PrescriptionLinkedOrderBanner")
|
||||
self.linked_order_banner.hide()
|
||||
layout.addWidget(self.linked_order_banner)
|
||||
return section
|
||||
|
||||
def _build_usage_section(self) -> QFrame:
|
||||
@@ -1898,7 +1995,6 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.dose_count = QSpinBox()
|
||||
self.dose_count.setRange(1, 999)
|
||||
self.dose_unit = QComboBox()
|
||||
self.dose_unit.setEditable(True)
|
||||
self.dose_unit.addItems(["剂", "丸", "袋", "盒", "瓶", "膏", "贴"])
|
||||
self._place_field(grid, 0, 0, "处方类型", self.prescription_type, required=True)
|
||||
self._place_field(grid, 0, 1, "剂数", self.dose_count)
|
||||
@@ -1910,7 +2006,6 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.dosage_amount.setRange(0, 100000)
|
||||
self.dosage_amount.setDecimals(2)
|
||||
self.dosage_unit = QComboBox()
|
||||
self.dosage_unit.setEditable(True)
|
||||
self.dosage_unit.addItems(["g", "ml"])
|
||||
self.dosage_bag_count = QSpinBox()
|
||||
self.dosage_bag_count.setRange(1, 99)
|
||||
@@ -1922,7 +2017,10 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.usage_days = QSpinBox()
|
||||
self.usage_days.setRange(1, 365)
|
||||
self._place_field(grid, 2, 0, "单次用量", self.dosage_amount)
|
||||
self._place_field(grid, 2, 1, "用量单位", self.dosage_unit)
|
||||
self._main_dosage_unit_field = self._place_field(
|
||||
grid, 2, 1, "用量单位", self.dosage_unit
|
||||
)
|
||||
self._main_dosage_unit_field.hide()
|
||||
self._main_bag_field = self._place_field(grid, 2, 2, "每次袋数", self.dosage_bag_count)
|
||||
self._main_decoction_field = self._place_field(grid, 3, 0, "代煎", self.need_decoction)
|
||||
self._main_bags_field = self._place_field(grid, 3, 1, "每贴出包数", self.bags_per_dose)
|
||||
@@ -1932,15 +2030,12 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.usage_instruction.setMaxLength(200)
|
||||
self._place_field(grid, 4, 1, "用法", self.usage_instruction, column_span=2)
|
||||
self.usage_time = QComboBox()
|
||||
self.usage_time.setEditable(True)
|
||||
self.usage_time.addItems(["饭前", "饭后", "饭中", "空腹", "睡前", "晨起", "随时"])
|
||||
self.usage_way = QComboBox()
|
||||
self.usage_way.setEditable(True)
|
||||
self.usage_way.addItems(
|
||||
["温水送服", "开水冲服", "黄酒送服", "淡盐水送服", "米汤送服", "嚼服", "含化"]
|
||||
)
|
||||
self.dietary_taboo = QLineEdit()
|
||||
self.dietary_taboo.setPlaceholderText("多项用逗号分隔")
|
||||
self.dietary_taboo = MultiSelectComboBox(self.DIETARY_OPTIONS)
|
||||
self._place_field(grid, 5, 0, "服用时间", self.usage_time)
|
||||
self._place_field(grid, 5, 1, "服用方式", self.usage_way)
|
||||
self._place_field(grid, 5, 2, "忌口", self.dietary_taboo)
|
||||
@@ -1972,6 +2067,10 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self._aux_name_field = self._place_field(
|
||||
grid, 8, 2, "辅方模板名", self.aux_prescription_name
|
||||
)
|
||||
# The admin editor keeps this value as import metadata; it is not a
|
||||
# user-editable usage field. Preserve it in the DTO without exposing
|
||||
# an extra workstation-only control.
|
||||
self._aux_name_field.hide()
|
||||
self._aux_decoction_field = self._place_field(
|
||||
grid, 9, 0, "辅方代煎", self.aux_need_decoction
|
||||
)
|
||||
@@ -1985,7 +2084,6 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self._aux_usage_fields = (
|
||||
self._aux_dosage_field,
|
||||
self._aux_bag_field,
|
||||
self._aux_name_field,
|
||||
self._aux_decoction_field,
|
||||
self._aux_bags_field,
|
||||
self._aux_times_field,
|
||||
@@ -2050,6 +2148,11 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.herb_summary.setText(
|
||||
f"共 {len(self.herbs.rows)} 味(主方 {main_count} / 辅方 {aux_count})"
|
||||
" · 可手动添加、从处方库或粘贴药方批量录入"
|
||||
+ (
|
||||
" · 存在重复药名,请核对是否需要合并剂量"
|
||||
if _duplicate_herb_names(self.herbs.values())
|
||||
else ""
|
||||
)
|
||||
)
|
||||
locked = self.herbs.locked
|
||||
self.rp_lock_label.setVisible(locked)
|
||||
@@ -2093,6 +2196,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.prescription_type,
|
||||
source.get("prescription_type") or "浓缩水丸",
|
||||
)
|
||||
self._type_changed()
|
||||
self.dosage_amount.setValue(_float(source.get("dosage_amount"), 1))
|
||||
self.dosage_unit.setCurrentText(
|
||||
str(
|
||||
@@ -2111,11 +2215,12 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.usage_time.setCurrentText(str(source.get("usage_time") or "饭后"))
|
||||
self.usage_way.setCurrentText(str(source.get("usage_way") or "温水送服"))
|
||||
dietary = source.get("dietary_taboo") or ""
|
||||
self.dietary_taboo.setText(
|
||||
"、".join(str(item) for item in dietary)
|
||||
dietary_values = (
|
||||
[str(item) for item in dietary]
|
||||
if isinstance(dietary, (list, tuple))
|
||||
else str(dietary)
|
||||
else [item for item in re.split(r"[,,、]", str(dietary)) if item.strip()]
|
||||
)
|
||||
self.dietary_taboo.set_values(dietary_values)
|
||||
self.usage_notes.setPlainText(str(source.get("usage_notes") or ""))
|
||||
aux = source.get("aux_usage")
|
||||
aux = dict(aux) if isinstance(aux, Mapping) else {}
|
||||
@@ -2143,18 +2248,144 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.visit_no.setReadOnly(True)
|
||||
self._update_herb_summary()
|
||||
|
||||
def _load_editor_context(self) -> None:
|
||||
"""Show the same edit/review context exposed by the admin drawer."""
|
||||
|
||||
messages: list[str] = []
|
||||
if self.mode == "edit":
|
||||
messages.append("保存修改后,处方将重新进入待审核。")
|
||||
if _bool(self._source.get("is_system_auto")):
|
||||
messages.append("当前为空白处方;保存后将转为手工处方。")
|
||||
voided = _bool(self._source.get("void_status"))
|
||||
rejected = _int(self._source.get("audit_status"), -1) == 2
|
||||
if voided and rejected:
|
||||
messages.append("当前处方已作废且已驳回;保存后将取消作废、清除驳回并重新进入待审核。")
|
||||
elif voided:
|
||||
messages.append("当前处方已作废;保存后将取消作废并重新进入待审核。")
|
||||
elif rejected:
|
||||
messages.append("当前处方已驳回;保存后将清除驳回并重新进入待审核。")
|
||||
if _bool(self._source.get("business_prescription_audit_rejected")):
|
||||
remark = str(self._source.get("business_prescription_audit_remark") or "").strip()
|
||||
message = (
|
||||
"业务订单侧处方审核已驳回;保存后关联业务订单的处方和支付单审核会重置为待审。"
|
||||
)
|
||||
if remark:
|
||||
message += f" 驳回意见:{remark}"
|
||||
messages.append(message)
|
||||
if messages:
|
||||
self.context_banner.show_message("\n".join(messages), "warning")
|
||||
else:
|
||||
self.context_banner.hide()
|
||||
|
||||
prescription_id = _int(self._source.get("id"), 0)
|
||||
list_orders = getattr(self.repository, "list_prescription_orders", None)
|
||||
if self.mode != "edit" or prescription_id <= 0 or not callable(list_orders):
|
||||
self.linked_order_banner.hide()
|
||||
return
|
||||
self._linked_order_generation += 1
|
||||
generation = self._linked_order_generation
|
||||
run_async(
|
||||
lambda: list_orders(page_no=1, page_size=5, prescription_id=prescription_id),
|
||||
on_success=lambda result: self._linked_order_loaded(result, generation),
|
||||
on_error=lambda _error: self._linked_order_failed(generation),
|
||||
)
|
||||
|
||||
def _linked_order_loaded(self, result: Any, generation: int) -> None:
|
||||
if generation != self._linked_order_generation:
|
||||
return
|
||||
rows = page_items(result)
|
||||
if not rows:
|
||||
self.linked_order_banner.show_message(
|
||||
"当前处方暂无关联业务订单,或当前账号无订单列表权限。",
|
||||
"info",
|
||||
)
|
||||
return
|
||||
row = rows[0]
|
||||
order_no = str(first_value(row, "order_no", default="") or "").strip() or "—"
|
||||
medication_days = first_value(row, "medication_days", default=None)
|
||||
days_text = f"{medication_days} 天" if _int(medication_days, 0) > 0 else "— 订单未填写"
|
||||
assistant_remark = (
|
||||
str(first_value(row, "remark_assistant", default="") or "").strip() or "— 无"
|
||||
)
|
||||
self.linked_order_banner.show_message(
|
||||
"业务订单要求(请与本处方保持一致)\n"
|
||||
f"最近关联订单:{order_no} 服用天数:{days_text}\n"
|
||||
f"医助备注:{assistant_remark}",
|
||||
"warning",
|
||||
)
|
||||
|
||||
def _linked_order_failed(self, generation: int) -> None:
|
||||
if generation == self._linked_order_generation:
|
||||
self.linked_order_banner.show_message(
|
||||
"当前处方暂无关联业务订单,或当前账号无订单列表权限。",
|
||||
"info",
|
||||
)
|
||||
|
||||
def _type_changed(self, _index: Any = None) -> None:
|
||||
value = self.prescription_type.currentData()
|
||||
if value == "饮片":
|
||||
self.dosage_unit.setCurrentText("ml")
|
||||
if self.dosage_amount.value() <= 0:
|
||||
self.dosage_amount.setValue(50)
|
||||
self.dosage_amount.setDecimals(0)
|
||||
self.dosage_amount.setRange(50, 250)
|
||||
self.dosage_amount.setSingleStep(50)
|
||||
self.dosage_amount.setValue(50)
|
||||
self.dosage_bag_count.setRange(1, 5)
|
||||
self.dosage_bag_count.setValue(1)
|
||||
self.need_decoction.setChecked(False)
|
||||
self.bags_per_dose.setRange(1, 9)
|
||||
self.bags_per_dose.setValue(1)
|
||||
self.aux_dosage_amount.setDecimals(0)
|
||||
self.aux_dosage_amount.setRange(50, 250)
|
||||
self.aux_dosage_amount.setSingleStep(50)
|
||||
self.aux_dosage_amount.setValue(50)
|
||||
self.aux_dosage_bag_count.setRange(1, 5)
|
||||
self.aux_dosage_bag_count.setValue(1)
|
||||
self.aux_need_decoction.setChecked(False)
|
||||
self.aux_bags_per_dose.setRange(1, 9)
|
||||
self.aux_bags_per_dose.setValue(1)
|
||||
elif value == "浓缩水丸":
|
||||
self.dosage_unit.setCurrentText("g")
|
||||
if self.dosage_amount.value() <= 0:
|
||||
self.dosage_amount.setValue(1)
|
||||
self.dosage_amount.setDecimals(0)
|
||||
self.dosage_amount.setRange(1, 10)
|
||||
self.dosage_amount.setSingleStep(1)
|
||||
self.dosage_amount.setValue(1)
|
||||
self.dosage_bag_count.setRange(1, 5)
|
||||
self.dosage_bag_count.setValue(1)
|
||||
self.need_decoction.setChecked(False)
|
||||
self.bags_per_dose.setRange(1, 9)
|
||||
self.bags_per_dose.setValue(1)
|
||||
self.aux_dosage_amount.setDecimals(0)
|
||||
self.aux_dosage_amount.setRange(1, 10)
|
||||
self.aux_dosage_amount.setSingleStep(1)
|
||||
self.aux_dosage_amount.setValue(5)
|
||||
self.aux_dosage_bag_count.setRange(1, 5)
|
||||
self.aux_dosage_bag_count.setValue(1)
|
||||
self.aux_need_decoction.setChecked(False)
|
||||
self.aux_bags_per_dose.setRange(1, 9)
|
||||
self.aux_bags_per_dose.setValue(1)
|
||||
else:
|
||||
self.dosage_unit.setCurrentText("g")
|
||||
self.dosage_amount.setDecimals(2)
|
||||
self.dosage_amount.setRange(0, 100000)
|
||||
self.dosage_amount.setSingleStep(1)
|
||||
self.dosage_amount.setValue(0)
|
||||
self.dosage_bag_count.setRange(1, 5)
|
||||
self.dosage_bag_count.setValue(1)
|
||||
self.need_decoction.setChecked(False)
|
||||
self.bags_per_dose.setRange(1, 9)
|
||||
self.bags_per_dose.setValue(1)
|
||||
self.aux_dosage_amount.setDecimals(2)
|
||||
self.aux_dosage_amount.setRange(0, 100000)
|
||||
self.aux_dosage_amount.setSingleStep(1)
|
||||
self.aux_dosage_amount.setValue(1)
|
||||
self.aux_dosage_bag_count.setRange(1, 5)
|
||||
self.aux_dosage_bag_count.setValue(1)
|
||||
self.aux_need_decoction.setChecked(False)
|
||||
self.aux_bags_per_dose.setRange(1, 9)
|
||||
self.aux_bags_per_dose.setValue(1)
|
||||
self.aux_times_per_day.setValue(3)
|
||||
self.aux_usage_days.setValue(7)
|
||||
self.aux_prescription_name.clear()
|
||||
self._apply_type_visibility()
|
||||
|
||||
def _apply_type_visibility(self) -> None:
|
||||
@@ -2263,11 +2494,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
"usage_instruction": self.usage_instruction.text().strip(),
|
||||
"usage_time": self.usage_time.currentText().strip(),
|
||||
"usage_way": self.usage_way.currentText().strip(),
|
||||
"dietary_taboo": [
|
||||
item.strip()
|
||||
for item in re.split(r"[,,、]", self.dietary_taboo.text())
|
||||
if item.strip()
|
||||
],
|
||||
"dietary_taboo": self.dietary_taboo.values(),
|
||||
"usage_notes": self.usage_notes.toPlainText().strip(),
|
||||
"doctor_name": self.doctor_name.text().strip(),
|
||||
"doctor_signature": self.signature.data_url(),
|
||||
@@ -2309,16 +2536,42 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.validation.show_message(message, "warning")
|
||||
widget.setFocus()
|
||||
return
|
||||
length_checks = (
|
||||
(payload["clinical_diagnosis"], 500, "临床诊断最多 500 个字符。", self.clinical_diagnosis),
|
||||
(payload["usage_instruction"], 200, "用法最多 200 个字符。", self.usage_instruction),
|
||||
(payload["usage_notes"], 200, "其他说明最多 200 个字符。", self.usage_notes),
|
||||
)
|
||||
for value, limit, message, widget in length_checks:
|
||||
if len(str(value)) > limit:
|
||||
self.validation.show_message(message, "warning")
|
||||
widget.setFocus()
|
||||
return
|
||||
prescription_type = str(payload["prescription_type"] or "")
|
||||
has_aux = any(_formula(herb.get("formula_type")) == "辅方" for herb in payload["herbs"])
|
||||
dosage_values = [float(payload["dosage_amount"])]
|
||||
if has_aux:
|
||||
dosage_values.append(float(payload["aux_usage"]["dosage_amount"]))
|
||||
if prescription_type == "浓缩水丸" and any(
|
||||
value not in {float(item) for item in range(1, 11)} for value in dosage_values
|
||||
):
|
||||
self.validation.show_message("浓缩水丸单次用量只能选择 1–10g。", "warning")
|
||||
self.dosage_amount.setFocus()
|
||||
return
|
||||
if prescription_type == "饮片" and any(
|
||||
value not in {50.0, 100.0, 120.0, 150.0, 180.0, 200.0, 250.0}
|
||||
for value in dosage_values
|
||||
):
|
||||
self.validation.show_message(
|
||||
"饮片单次用量只能选择 50、100、120、150、180、200 或 250ml。",
|
||||
"warning",
|
||||
)
|
||||
self.dosage_amount.setFocus()
|
||||
return
|
||||
herbs = payload["herbs"]
|
||||
if not herbs:
|
||||
self.tabs.setCurrentIndex(1)
|
||||
self.validation.show_message("请至少添加一味药材。", "warning")
|
||||
return
|
||||
duplicate_names = _duplicate_herb_names(herbs)
|
||||
if duplicate_names:
|
||||
self.tabs.setCurrentIndex(1)
|
||||
self.validation.show_message("药材不可重复:" + "、".join(duplicate_names), "warning")
|
||||
return
|
||||
for index, herb in enumerate(herbs, 1):
|
||||
if not str(herb.get("name") or "").strip():
|
||||
self.tabs.setCurrentIndex(1)
|
||||
@@ -3115,6 +3368,43 @@ class PrescriptionDetailDialog(QDialog):
|
||||
pdf_button.clicked.connect(self.choose_pdf_path)
|
||||
actions.addWidget(pdf_button)
|
||||
root.addLayout(actions)
|
||||
self.status_banner = MessageBanner()
|
||||
self.status_banner.setObjectName("PrescriptionDetailStatusBanner")
|
||||
status_lines: list[str] = []
|
||||
if _bool(first_value(prescription, "void_status", "is_void", default=False)):
|
||||
void_by = display_text(first_value(prescription, "void_by_name", default=""), "—")
|
||||
void_time = display_text(first_value(prescription, "void_time", default=""), "—")
|
||||
status_lines.append(f"当前处方已作废 作废人:{void_by} 作废时间:{void_time}")
|
||||
audit_status = _int(first_value(prescription, "audit_status", default=-1), -1)
|
||||
audit_label = {0: "待审核", 1: "已通过", 2: "已驳回"}.get(audit_status, "未知")
|
||||
status_lines.append(f"消费者处方审核:{audit_label}")
|
||||
audit_remark = str(first_value(prescription, "audit_remark", default="") or "").strip()
|
||||
audit_by = str(first_value(prescription, "audit_by_name", default="") or "").strip()
|
||||
audit_time = str(first_value(prescription, "audit_time", default="") or "").strip()
|
||||
if audit_by or audit_time or audit_remark:
|
||||
status_lines.append(
|
||||
f"审核人:{audit_by or '—'} 审核时间:{audit_time or '—'}"
|
||||
+ (f" 审核意见:{audit_remark}" if audit_remark else "")
|
||||
)
|
||||
if _bool(first_value(prescription, "business_prescription_audit_rejected", default=False)):
|
||||
business_remark = str(
|
||||
first_value(prescription, "business_prescription_audit_remark", default="") or ""
|
||||
).strip()
|
||||
status_lines.append(
|
||||
"业务订单处方审核:已驳回"
|
||||
+ (f" 驳回意见:{business_remark}" if business_remark else "")
|
||||
)
|
||||
self.status_banner.show_message(
|
||||
"\n".join(status_lines),
|
||||
"danger"
|
||||
if _bool(first_value(prescription, "void_status", default=False))
|
||||
or audit_status == 2
|
||||
or _bool(
|
||||
first_value(prescription, "business_prescription_audit_rejected", default=False)
|
||||
)
|
||||
else "info",
|
||||
)
|
||||
root.addWidget(self.status_banner)
|
||||
self.preview = _PrescriptionPaperPreview(prescription)
|
||||
self.preview.setObjectName("PrescriptionPaperPreview")
|
||||
self.preview.setOpenExternalLinks(False)
|
||||
|
||||
@@ -228,10 +228,19 @@ def _status_value(row: Any) -> int:
|
||||
return _as_int(first_value(row, "status", default=0))
|
||||
|
||||
|
||||
def prescription_action_label(row: Any) -> str:
|
||||
audit = _as_int(first_value(row, "prescription_audit_status", "audit_status"), -1)
|
||||
voided = _as_int(first_value(row, "prescription_void_status", "void_status"), 0)
|
||||
return "查看" if audit == 1 and voided != 1 else "开方"
|
||||
def prescription_action_label(row: Any) -> str:
|
||||
audit = _as_int(first_value(row, "prescription_audit_status", "audit_status"), -1)
|
||||
voided = _as_int(first_value(row, "prescription_void_status", "void_status"), 0)
|
||||
explicit = first_value(row, "has_prescription", default=None)
|
||||
has_prescription = (
|
||||
_as_bool(explicit)
|
||||
if explicit is not None
|
||||
else _as_int(first_value(row, "prescription_id", default=0), 0) > 0
|
||||
or audit in {0, 1, 2}
|
||||
)
|
||||
if not has_prescription:
|
||||
return "开方"
|
||||
return "查看" if audit == 1 and voided != 1 else "编辑处方"
|
||||
|
||||
|
||||
def _patient_cell(_value: Any, row: Any, *, can_plain: bool) -> str:
|
||||
@@ -1177,31 +1186,64 @@ class AppointmentsPage(QWidget):
|
||||
def _prescription_loaded(self, existing: Any, row: Any, generation: int) -> None:
|
||||
if generation != self._prescription_generation:
|
||||
return
|
||||
self.banner.clear()
|
||||
if existing is not None and _as_int(first_value(existing, "id", default=0)) > 0:
|
||||
dialog = PrescriptionDetailDialog(
|
||||
existing,
|
||||
can_open_diagnosis=_canonical_allowed(
|
||||
self.permissions, "tcm.diagnosis/readonlyDetail", default=False
|
||||
),
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_diagnosis_id)
|
||||
dialog.exec()
|
||||
approved = _as_int(first_value(existing, "audit_status", "status"), -1) == 1
|
||||
voided = _as_int(first_value(existing, "void_status", "is_void"), 0) == 1
|
||||
if approved and not voided:
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"新建处方",
|
||||
"已展示当前挂号的处方。是否继续新建一张处方?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
self._begin_case_record_load(row)
|
||||
self.banner.clear()
|
||||
if existing is not None and _as_int(first_value(existing, "id", default=0)) > 0:
|
||||
approved = _as_int(first_value(existing, "audit_status", "status"), -1) == 1
|
||||
voided = _as_int(first_value(existing, "void_status", "is_void"), 0) == 1
|
||||
if not approved or voided:
|
||||
self._open_existing_prescription_editor(existing)
|
||||
else:
|
||||
dialog = PrescriptionDetailDialog(
|
||||
existing,
|
||||
can_open_diagnosis=_canonical_allowed(
|
||||
self.permissions, "tcm.diagnosis/readonlyDetail", default=False
|
||||
),
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_diagnosis_id)
|
||||
dialog.exec()
|
||||
return
|
||||
self._begin_case_record_load(row)
|
||||
|
||||
def _open_existing_prescription_editor(self, prescription: Any) -> None:
|
||||
prescription_id = _as_int(first_value(prescription, "id", "prescription_id", default=0))
|
||||
if prescription_id <= 0:
|
||||
self.banner.show_message("处方编号不完整,无法编辑。", "danger")
|
||||
return
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
prescription,
|
||||
mode="edit",
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||||
if diagnosis_signal is not None:
|
||||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
frozen_payload = MappingProxyType(dialog.payload())
|
||||
self._mutation_pending = True
|
||||
self._action_generation += 1
|
||||
generation = self._action_generation
|
||||
self.banner.show_message("正在保存处方…", "info")
|
||||
run_async(
|
||||
lambda: invoke(
|
||||
self.repository,
|
||||
"update_prescription",
|
||||
prescription=prescription_id,
|
||||
changes=frozen_payload,
|
||||
),
|
||||
on_success=lambda _result: self._prescription_updated(generation),
|
||||
on_error=lambda error: self._action_error(error, generation),
|
||||
on_finished=lambda: self._mutation_finished(generation),
|
||||
)
|
||||
|
||||
def _prescription_updated(self, generation: int) -> None:
|
||||
if generation != self._action_generation:
|
||||
return
|
||||
self.banner.show_message("处方已保存并重新进入待审核。", "success")
|
||||
self.refresh(silent=True)
|
||||
|
||||
def _begin_case_record_load(self, row: Any) -> None:
|
||||
snapshot = deepcopy(row)
|
||||
@@ -1262,8 +1304,10 @@ class AppointmentsPage(QWidget):
|
||||
"visit_no": build_prescription_visit_no(
|
||||
diagnosis_id=diagnosis_id, appointment_id=appointment_id
|
||||
),
|
||||
"tongue": authoritative("tongue", "tongue_coating", default=""),
|
||||
"pulse": authoritative("pulse", default=""),
|
||||
"tongue": authoritative("tongue", "tongue_coating", default=""),
|
||||
"tongue_image": authoritative("tongue_image", default=""),
|
||||
"pulse": authoritative("pulse", default=""),
|
||||
"pulse_condition": authoritative("pulse_condition", default=""),
|
||||
"clinical_diagnosis": build_prescription_clinical_diagnosis(
|
||||
diagnosis, patient, record, case_record
|
||||
),
|
||||
@@ -1278,14 +1322,17 @@ class AppointmentsPage(QWidget):
|
||||
|
||||
def _open_prescription_editor(self, record: Any, case_record: Any) -> None:
|
||||
seed = self._prescription_seed(record, case_record)
|
||||
dialog = PrescriptionEditorDialog(
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
seed,
|
||||
mode="add",
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
parent=self,
|
||||
)
|
||||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||||
if diagnosis_signal is not None:
|
||||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
payload = dialog.payload()
|
||||
payload["diagnosis_id"] = seed["diagnosis_id"]
|
||||
|
||||
@@ -270,10 +270,19 @@ def is_diagnosis_confirmed(record: Any) -> bool:
|
||||
return _as_bool(first_value(record, "diagnosis_confirmed", "confirmed", default=False))
|
||||
|
||||
|
||||
def prescription_action_label(record: Any) -> str:
|
||||
audit = _as_int(first_value(record, "prescription_audit_status", "audit_status"), -1)
|
||||
voided = _as_int(first_value(record, "prescription_void_status", "void_status"), 0)
|
||||
return "查看处方" if audit == 1 and voided != 1 else "开方"
|
||||
def prescription_action_label(record: Any) -> str:
|
||||
audit = _as_int(first_value(record, "prescription_audit_status", "audit_status"), -1)
|
||||
voided = _as_int(first_value(record, "prescription_void_status", "void_status"), 0)
|
||||
explicit = first_value(record, "has_prescription", default=None)
|
||||
has_prescription = (
|
||||
_as_bool(explicit)
|
||||
if explicit is not None
|
||||
else _as_int(first_value(record, "prescription_id", default=0), 0) > 0
|
||||
or audit in {0, 1, 2}
|
||||
)
|
||||
if not has_prescription:
|
||||
return "开方"
|
||||
return "查看处方" if audit == 1 and voided != 1 else "编辑处方"
|
||||
|
||||
|
||||
def can_void_prescription(record: Any) -> bool:
|
||||
@@ -2692,35 +2701,56 @@ class ConsultationsPage(QWidget):
|
||||
def _prescription_loaded(self, existing: Any, record: Any, mode: str, generation: int) -> None:
|
||||
if generation != self._prescription_generation:
|
||||
return
|
||||
self.banner.clear()
|
||||
self._last_prescription = existing
|
||||
if mode == "void":
|
||||
self._confirm_void(existing)
|
||||
return
|
||||
if existing is not None:
|
||||
detail = PrescriptionDetailDialog(
|
||||
existing,
|
||||
can_open_diagnosis=_canonical_allowed(
|
||||
self.permissions, "tcm.diagnosis/readonlyDetail"
|
||||
),
|
||||
parent=self,
|
||||
)
|
||||
detail.diagnosis_requested.connect(self._open_diagnosis_id)
|
||||
detail.exec()
|
||||
approved = _as_int(first_value(existing, "audit_status", "status"), -1) == 1
|
||||
voided = _as_int(first_value(existing, "void_status", "is_void"), 0) == 1
|
||||
if not approved or voided:
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"新建处方",
|
||||
"已展示当前挂号的处方。是否继续新建一张处方?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Yes:
|
||||
self._begin_case_record_load(record)
|
||||
return
|
||||
self._begin_case_record_load(record)
|
||||
self.banner.clear()
|
||||
self._last_prescription = existing
|
||||
if mode == "void":
|
||||
self._confirm_void(existing)
|
||||
return
|
||||
if existing is not None:
|
||||
approved = _as_int(first_value(existing, "audit_status", "status"), -1) == 1
|
||||
voided = _as_int(first_value(existing, "void_status", "is_void"), 0) == 1
|
||||
if not approved or voided:
|
||||
self._open_existing_prescription_editor(existing)
|
||||
else:
|
||||
detail = PrescriptionDetailDialog(
|
||||
existing,
|
||||
can_open_diagnosis=_canonical_allowed(
|
||||
self.permissions, "tcm.diagnosis/readonlyDetail"
|
||||
),
|
||||
parent=self,
|
||||
)
|
||||
detail.diagnosis_requested.connect(self._open_diagnosis_id)
|
||||
detail.exec()
|
||||
return
|
||||
self._begin_case_record_load(record)
|
||||
|
||||
def _open_existing_prescription_editor(self, prescription: Any) -> None:
|
||||
prescription_id = _as_int(first_value(prescription, "id", "prescription_id", default=0))
|
||||
if prescription_id <= 0:
|
||||
self.banner.show_message("处方编号不完整,无法编辑。", "danger")
|
||||
return
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
prescription,
|
||||
mode="edit",
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||||
if diagnosis_signal is not None:
|
||||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
frozen_payload = MappingProxyType(dialog.payload())
|
||||
self._run_mutation(
|
||||
lambda: invoke(
|
||||
self.repository,
|
||||
"update_prescription",
|
||||
prescription=prescription_id,
|
||||
changes=frozen_payload,
|
||||
),
|
||||
"处方已保存并重新进入待审核。",
|
||||
)
|
||||
|
||||
def _begin_case_record_load(self, record: Any) -> None:
|
||||
record_snapshot = deepcopy(record)
|
||||
@@ -2789,8 +2819,10 @@ class ConsultationsPage(QWidget):
|
||||
"visit_no": build_prescription_visit_no(
|
||||
diagnosis_id=diagnosis_id, appointment_id=appointment_id
|
||||
),
|
||||
"tongue": authoritative("tongue", "tongue_coating", default=""),
|
||||
"pulse": authoritative("pulse", default=""),
|
||||
"tongue": authoritative("tongue", "tongue_coating", default=""),
|
||||
"tongue_image": authoritative("tongue_image", default=""),
|
||||
"pulse": authoritative("pulse", default=""),
|
||||
"pulse_condition": authoritative("pulse_condition", default=""),
|
||||
"clinical_diagnosis": build_prescription_clinical_diagnosis(
|
||||
diagnosis, patient, record, case_record
|
||||
),
|
||||
@@ -2804,14 +2836,17 @@ class ConsultationsPage(QWidget):
|
||||
|
||||
def _open_prescription_editor(self, record: Any, case_record: Any) -> None:
|
||||
seed = self._prescription_seed(record, case_record)
|
||||
dialog = PrescriptionEditorDialog(
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
seed,
|
||||
mode="add",
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
parent=self,
|
||||
)
|
||||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||||
if diagnosis_signal is not None:
|
||||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
payload = dialog.payload()
|
||||
payload["diagnosis_id"] = seed["diagnosis_id"]
|
||||
|
||||
@@ -69,10 +69,8 @@ def _truthy(value: Any) -> bool:
|
||||
|
||||
|
||||
def prescription_status(row: Any) -> tuple[str, str]:
|
||||
"""Return the admin-equivalent combined status label and visual kind."""
|
||||
"""Return the admin audit-column status; void state has its own column."""
|
||||
|
||||
if _int(first_value(row, "void_status", "is_void"), 0) == 1:
|
||||
return "已作废", "danger"
|
||||
if _truthy(first_value(row, "business_prescription_audit_rejected", default=False)):
|
||||
return "已驳回", "danger"
|
||||
status = _int(first_value(row, "audit_status", "status", default=0), 0)
|
||||
@@ -718,6 +716,7 @@ class PrescriptionsPage(QWidget):
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_diagnosis)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
self._save_prescription(dialog.payload(), None)
|
||||
|
||||
@@ -739,6 +738,7 @@ class PrescriptionsPage(QWidget):
|
||||
current_user=self.current_user,
|
||||
parent=self,
|
||||
)
|
||||
dialog.diagnosis_requested.connect(self._open_diagnosis)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
self._save_prescription(
|
||||
dialog.payload(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user