This commit is contained in:
Your Name
2026-08-12 18:09:57 +08:00
parent a1092c02c3
commit b2eb18c75f
391 changed files with 1666 additions and 638 deletions
@@ -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("浓缩水丸单次用量只能选择 110g。", "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(),