Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2e9059c17 | ||
|
|
65aaaae885 | ||
|
|
71051461a2 | ||
|
|
25c12ad4ff | ||
|
|
5b440df17e | ||
|
|
67846493fa | ||
|
|
81154f4c94 | ||
|
|
125398a3f5 | ||
|
|
af730ad050 | ||
|
|
2e21884d65 | ||
|
|
6292badf8e | ||
|
|
5a55e9f574 | ||
|
|
f1b4a5ce26 | ||
|
|
01fef051da |
No files matched your search
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": {
|
||||
"figma": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ _DONATION_STATUS_LABELS = {
|
||||
_VOID_TYPE_FALLBACK_LABELS = {
|
||||
"khhc": "客户毁存",
|
||||
"jcbhg": "检测不合格",
|
||||
"sysfq": "实验室废弃",
|
||||
}
|
||||
|
||||
# 非法来源状态视为终态,不允许再转出
|
||||
|
||||
@@ -2642,6 +2642,7 @@ def update_collection_protocol(
|
||||
source_presign_order_id: Optional[str] = None,
|
||||
remark: Optional[str] = None,
|
||||
commit: bool = True,
|
||||
sync_reception_discard: bool = True,
|
||||
) -> BizCollectionProtocol:
|
||||
obj = get_collection_protocol(
|
||||
db, protocol_id, tenant_no, scope_collector_user_id=scope_collector_user_id
|
||||
@@ -2650,6 +2651,7 @@ def update_collection_protocol(
|
||||
raise ValueError("协议单不存在或无权操作")
|
||||
from_status = (obj.protocol_status or "").strip()
|
||||
status_log_payload: Optional[Dict[str, Any]] = None
|
||||
voided_now = False
|
||||
if protocol_no is not None:
|
||||
pn = protocol_no.strip()
|
||||
if pn != (obj.protocol_no or ""):
|
||||
@@ -2660,6 +2662,8 @@ def update_collection_protocol(
|
||||
next_status = protocol_status.strip()
|
||||
assert_protocol_status_transition(obj.protocol_status or "", next_status, kind="protocol")
|
||||
obj.protocol_status = next_status
|
||||
if next_status == "yzf" and from_status != "yzf":
|
||||
voided_now = True
|
||||
if next_status == "yzf" and void_time is not None:
|
||||
if void_type is None or not str(void_type).strip():
|
||||
raise ValueError("请选择作废类型")
|
||||
@@ -2749,6 +2753,16 @@ def update_collection_protocol(
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
if voided_now and sync_reception_discard:
|
||||
from app.services.protocol_reception_discard_sync import after_protocol_voided
|
||||
|
||||
after_protocol_voided(
|
||||
db,
|
||||
source="cell_storage",
|
||||
protocol_id=obj.id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
if commit:
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
|
||||
@@ -1686,6 +1686,16 @@ def change_status(
|
||||
)
|
||||
|
||||
db.add(obj)
|
||||
if ns == "yzf" and (current or "").strip() != "yzf":
|
||||
from app.services.protocol_reception_discard_sync import after_protocol_voided
|
||||
|
||||
after_protocol_voided(
|
||||
db,
|
||||
source="research_donation",
|
||||
protocol_id=obj.id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
@@ -1033,6 +1033,12 @@ def create_sample_reception(
|
||||
user_id=user_id,
|
||||
create_dept_id=create_dept_id or "",
|
||||
)
|
||||
if conclusion == "bj":
|
||||
from app.services.protocol_reception_discard_sync import after_sample_reception_saved
|
||||
|
||||
after_sample_reception_saved(
|
||||
db, reception=obj, tenant_no=tenant_no, user_id=user_id
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
@@ -1151,6 +1157,12 @@ def update_sample_reception(
|
||||
)
|
||||
check_result_svc.sync_legacy_columns_from_results(db, rec, saved_rows)
|
||||
db.add(rec)
|
||||
if conclusion == "bj":
|
||||
from app.services.protocol_reception_discard_sync import after_sample_reception_saved
|
||||
|
||||
after_sample_reception_saved(
|
||||
db, reception=rec, tenant_no=tenant_no, user_id=user_id
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(rec)
|
||||
return rec
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
"""市场协议作废 ↔ 质检样本接收废弃双向联动。
|
||||
|
||||
规则(细胞存储 / 科研捐赠):
|
||||
1. 协议变为已作废(yzf)时,协议下全部样本接收结论自动为废弃(bj)。
|
||||
2. 协议下全部有效样本均已废弃/拒收(bj)时,协议自动已作废,作废类型为「实验室废弃」。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.models import (
|
||||
BizCollectionProtocol,
|
||||
BizProtocolSample,
|
||||
BizSampleReception,
|
||||
SysDictData,
|
||||
)
|
||||
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
|
||||
|
||||
LAB_VOID_TYPE_CODE = "sysfq"
|
||||
LAB_VOID_TYPE_LABEL = "实验室废弃"
|
||||
VOID_TYPE_DICT = "xyzflx"
|
||||
|
||||
_AUTO_DISCARD_REMARK = "协议作废自动废弃"
|
||||
_AUTO_VOID_DESC = "全部样本接收废弃/拒收,系统自动作废"
|
||||
|
||||
|
||||
def ensure_lab_void_type(db: Session, *, tenant_no: str) -> str:
|
||||
"""确保租户字典 xyzflx 存在「实验室废弃」,返回 dict_value。"""
|
||||
tenant = (tenant_no or "").strip()
|
||||
if not tenant:
|
||||
raise ValueError("租户编号不能为空")
|
||||
|
||||
by_value = (
|
||||
db.query(SysDictData)
|
||||
.filter(
|
||||
SysDictData.dict_type == VOID_TYPE_DICT,
|
||||
SysDictData.dict_value == LAB_VOID_TYPE_CODE,
|
||||
SysDictData.tenant_no == tenant,
|
||||
SysDictData.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if by_value:
|
||||
return LAB_VOID_TYPE_CODE
|
||||
|
||||
by_label = (
|
||||
db.query(SysDictData)
|
||||
.filter(
|
||||
SysDictData.dict_type == VOID_TYPE_DICT,
|
||||
SysDictData.dict_label == LAB_VOID_TYPE_LABEL,
|
||||
SysDictData.tenant_no == tenant,
|
||||
SysDictData.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if by_label and (by_label.dict_value or "").strip():
|
||||
return (by_label.dict_value or "").strip()
|
||||
|
||||
now = datetime.now()
|
||||
db.add(
|
||||
SysDictData(
|
||||
id=generate_uuid(),
|
||||
dict_label=LAB_VOID_TYPE_LABEL,
|
||||
dict_value=LAB_VOID_TYPE_CODE,
|
||||
dict_type=VOID_TYPE_DICT,
|
||||
tenant_no=tenant,
|
||||
order_num=90,
|
||||
remark="issue#31 实验室废弃自动作废类型",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.flush()
|
||||
return LAB_VOID_TYPE_CODE
|
||||
|
||||
|
||||
def _list_cell_samples(db: Session, *, protocol_id: str, tenant_no: str) -> List[BizProtocolSample]:
|
||||
return (
|
||||
db.query(BizProtocolSample)
|
||||
.filter(
|
||||
BizProtocolSample.collection_protocol_id == protocol_id,
|
||||
BizProtocolSample.tenant_no == tenant_no,
|
||||
BizProtocolSample.del_flag == 0,
|
||||
)
|
||||
.order_by(BizProtocolSample.order_num.asc(), BizProtocolSample.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def _list_donation_samples(
|
||||
db: Session, *, donation_id: str, tenant_no: str
|
||||
) -> List[BizResearchDonationSample]:
|
||||
return (
|
||||
db.query(BizResearchDonationSample)
|
||||
.filter(
|
||||
BizResearchDonationSample.research_donation_id == donation_id,
|
||||
BizResearchDonationSample.tenant_no == tenant_no,
|
||||
BizResearchDonationSample.del_flag == 0,
|
||||
)
|
||||
.order_by(
|
||||
BizResearchDonationSample.order_num.asc(),
|
||||
BizResearchDonationSample.create_time.asc(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def _find_reception(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
source: str,
|
||||
sample_code: str,
|
||||
) -> Optional[BizSampleReception]:
|
||||
code = (sample_code or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
return (
|
||||
db.query(BizSampleReception)
|
||||
.filter(
|
||||
BizSampleReception.tenant_no == tenant_no,
|
||||
BizSampleReception.del_flag == 0,
|
||||
BizSampleReception.reception_source == source,
|
||||
BizSampleReception.sample_code == code,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _upsert_discard_reception(
|
||||
db: Session,
|
||||
*,
|
||||
source: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
sample_code: str,
|
||||
protocol_sample_id: str = "",
|
||||
collection_protocol_id: str = "",
|
||||
donation_sample_id: Optional[str] = None,
|
||||
research_donation_id: Optional[str] = None,
|
||||
now: Optional[datetime] = None,
|
||||
) -> None:
|
||||
code = (sample_code or "").strip()
|
||||
if not code:
|
||||
return
|
||||
ts = now or datetime.now()
|
||||
rec = _find_reception(db, tenant_no=tenant_no, source=source, sample_code=code)
|
||||
if rec:
|
||||
if (rec.accept_conclusion or "").strip() == "bj":
|
||||
return
|
||||
rec.accept_conclusion = "bj"
|
||||
rec.update_by = user_id
|
||||
rec.update_time = ts
|
||||
if not (rec.remark or "").strip():
|
||||
rec.remark = _AUTO_DISCARD_REMARK
|
||||
db.add(rec)
|
||||
return
|
||||
|
||||
obj = BizSampleReception(
|
||||
id=generate_uuid(),
|
||||
protocol_sample_id=protocol_sample_id or "",
|
||||
collection_protocol_id=collection_protocol_id or "",
|
||||
reception_time=ts,
|
||||
handler_user_id=user_id,
|
||||
handler_name=None,
|
||||
receive_temperature=None,
|
||||
accept_conclusion="bj",
|
||||
receive_attachments=None,
|
||||
reception_source=source,
|
||||
donation_sample_id=donation_sample_id,
|
||||
research_donation_id=research_donation_id,
|
||||
sample_code=code,
|
||||
lab_storage_code=None,
|
||||
tenant_no=tenant_no,
|
||||
remark=_AUTO_DISCARD_REMARK,
|
||||
create_by=user_id,
|
||||
create_dept_id="",
|
||||
update_by=user_id,
|
||||
create_time=ts,
|
||||
update_time=ts,
|
||||
del_flag=0,
|
||||
check_packaging=0,
|
||||
check_sealing=0,
|
||||
check_no_damage=0,
|
||||
check_no_leakage=0,
|
||||
check_color=0,
|
||||
check_tying_rope=0,
|
||||
check_no_stratification=0,
|
||||
)
|
||||
db.add(obj)
|
||||
|
||||
|
||||
def discard_receptions_for_voided_protocol(
|
||||
db: Session,
|
||||
*,
|
||||
source: str,
|
||||
protocol_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> int:
|
||||
"""协议已作废后,将其下样本接收全部置为废弃。返回处理样本数。"""
|
||||
src = (source or "").strip()
|
||||
pid = (protocol_id or "").strip()
|
||||
tenant = (tenant_no or "").strip()
|
||||
if src not in ("cell_storage", "research_donation") or not pid or not tenant:
|
||||
return 0
|
||||
|
||||
now = datetime.now()
|
||||
if src == "cell_storage":
|
||||
samples = _list_cell_samples(db, protocol_id=pid, tenant_no=tenant)
|
||||
for sample in samples:
|
||||
_upsert_discard_reception(
|
||||
db,
|
||||
source=src,
|
||||
tenant_no=tenant,
|
||||
user_id=user_id,
|
||||
sample_code=sample.sample_code or "",
|
||||
protocol_sample_id=sample.id,
|
||||
collection_protocol_id=pid,
|
||||
now=now,
|
||||
)
|
||||
else:
|
||||
samples = _list_donation_samples(db, donation_id=pid, tenant_no=tenant)
|
||||
for sample in samples:
|
||||
_upsert_discard_reception(
|
||||
db,
|
||||
source=src,
|
||||
tenant_no=tenant,
|
||||
user_id=user_id,
|
||||
sample_code=sample.sample_code or "",
|
||||
donation_sample_id=sample.id,
|
||||
research_donation_id=pid,
|
||||
now=now,
|
||||
)
|
||||
db.flush()
|
||||
return len(samples)
|
||||
|
||||
|
||||
def _all_samples_discarded(
|
||||
db: Session,
|
||||
*,
|
||||
source: str,
|
||||
protocol_id: str,
|
||||
tenant_no: str,
|
||||
) -> bool:
|
||||
src = (source or "").strip()
|
||||
if src == "cell_storage":
|
||||
samples = _list_cell_samples(db, protocol_id=protocol_id, tenant_no=tenant_no)
|
||||
else:
|
||||
samples = _list_donation_samples(db, donation_id=protocol_id, tenant_no=tenant_no)
|
||||
if not samples:
|
||||
return False
|
||||
for sample in samples:
|
||||
code = (sample.sample_code or "").strip()
|
||||
if not code:
|
||||
return False
|
||||
rec = _find_reception(db, tenant_no=tenant_no, source=src, sample_code=code)
|
||||
if not rec or (rec.accept_conclusion or "").strip() != "bj":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def maybe_void_protocol_when_all_samples_discarded(
|
||||
db: Session,
|
||||
*,
|
||||
source: str,
|
||||
protocol_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""若协议下样本全部废弃,则自动作废协议(作废类型=实验室废弃)。返回是否执行了作废。"""
|
||||
src = (source or "").strip()
|
||||
pid = (protocol_id or "").strip()
|
||||
tenant = (tenant_no or "").strip()
|
||||
if src not in ("cell_storage", "research_donation") or not pid or not tenant:
|
||||
return False
|
||||
if not _all_samples_discarded(db, source=src, protocol_id=pid, tenant_no=tenant):
|
||||
return False
|
||||
|
||||
void_type = ensure_lab_void_type(db, tenant_no=tenant)
|
||||
now = datetime.now()
|
||||
|
||||
if src == "cell_storage":
|
||||
obj = (
|
||||
db.query(BizCollectionProtocol)
|
||||
.filter(
|
||||
BizCollectionProtocol.id == pid,
|
||||
BizCollectionProtocol.tenant_no == tenant,
|
||||
BizCollectionProtocol.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not obj:
|
||||
return False
|
||||
status = (obj.protocol_status or "").strip()
|
||||
if status in ("yzf", "sdfq"):
|
||||
return False
|
||||
from app.core.protocol_status_display import assert_protocol_status_transition
|
||||
from app.crud.collection_protocol import update_collection_protocol
|
||||
|
||||
try:
|
||||
assert_protocol_status_transition(status, "yzf", kind="protocol")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
update_collection_protocol(
|
||||
db,
|
||||
pid,
|
||||
tenant_no=tenant,
|
||||
user_id=user_id,
|
||||
protocol_status="yzf",
|
||||
void_time=now,
|
||||
void_desc=_AUTO_VOID_DESC,
|
||||
void_type=void_type,
|
||||
commit=False,
|
||||
sync_reception_discard=False,
|
||||
)
|
||||
return True
|
||||
|
||||
obj = (
|
||||
db.query(BizResearchDonation)
|
||||
.filter(
|
||||
BizResearchDonation.id == pid,
|
||||
BizResearchDonation.tenant_no == tenant,
|
||||
BizResearchDonation.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not obj:
|
||||
return False
|
||||
status = (obj.donation_status or "").strip()
|
||||
if status in ("yzf", "sdfq"):
|
||||
return False
|
||||
|
||||
# 捐赠 change_status 默认 commit;此处内联等价逻辑避免提前提交。
|
||||
from app.core.protocol_status_display import assert_protocol_status_transition
|
||||
from app.crud.dict_data import normalize_void_type
|
||||
from app.crud.research_donation import (
|
||||
_DONATION_STATUS_CHANGE_TYPE,
|
||||
append_donation_status_change,
|
||||
)
|
||||
|
||||
current = obj.donation_status or ""
|
||||
try:
|
||||
assert_protocol_status_transition(current, "yzf", kind="donation")
|
||||
except ValueError:
|
||||
return False
|
||||
normalized = normalize_void_type(db, value=void_type, tenant_no=tenant)
|
||||
obj.donation_status = "yzf"
|
||||
obj.void_time = now
|
||||
obj.void_desc = _AUTO_VOID_DESC
|
||||
obj.void_type = normalized
|
||||
obj.update_by = user_id
|
||||
obj.update_time = now
|
||||
change_type = _DONATION_STATUS_CHANGE_TYPE.get("yzf")
|
||||
if change_type:
|
||||
append_donation_status_change(
|
||||
db,
|
||||
research_donation_id=obj.id,
|
||||
change_type=change_type,
|
||||
from_status=current,
|
||||
to_status="yzf",
|
||||
change_time=now,
|
||||
change_desc=_AUTO_VOID_DESC,
|
||||
void_type=normalized,
|
||||
tenant_no=tenant,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(obj)
|
||||
db.flush()
|
||||
return True
|
||||
|
||||
|
||||
def after_sample_reception_saved(
|
||||
db: Session,
|
||||
*,
|
||||
reception: BizSampleReception,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""样本接收保存后:若结论为废弃,尝试自动作废所属协议。"""
|
||||
if (reception.accept_conclusion or "").strip() != "bj":
|
||||
return
|
||||
source = (reception.reception_source or "cell_storage").strip() or "cell_storage"
|
||||
if source == "cell_storage":
|
||||
protocol_id = (reception.collection_protocol_id or "").strip()
|
||||
else:
|
||||
protocol_id = (reception.research_donation_id or "").strip()
|
||||
if not protocol_id:
|
||||
return
|
||||
maybe_void_protocol_when_all_samples_discarded(
|
||||
db,
|
||||
source=source,
|
||||
protocol_id=protocol_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
def after_protocol_voided(
|
||||
db: Session,
|
||||
*,
|
||||
source: str,
|
||||
protocol_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""协议作废后:同步废弃其下全部样本接收。"""
|
||||
discard_receptions_for_voided_protocol(
|
||||
db,
|
||||
source=source,
|
||||
protocol_id=protocol_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
)
|
||||
@@ -0,0 +1,328 @@
|
||||
"""协议作废 ↔ 样本接收废弃双向联动。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.database import Base
|
||||
from app.models.models import (
|
||||
BizCollectionProtocol,
|
||||
BizProtocolSample,
|
||||
BizProtocolStatusChange,
|
||||
BizSampleReception,
|
||||
SysDictData,
|
||||
)
|
||||
from app.models.research_donation import (
|
||||
BizDonationStatusChange,
|
||||
BizResearchDonation,
|
||||
BizResearchDonationSample,
|
||||
)
|
||||
from app.services.protocol_reception_discard_sync import (
|
||||
LAB_VOID_TYPE_CODE,
|
||||
after_protocol_voided,
|
||||
after_sample_reception_saved,
|
||||
ensure_lab_void_type,
|
||||
)
|
||||
|
||||
|
||||
def _session():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
SysDictData.__table__,
|
||||
BizCollectionProtocol.__table__,
|
||||
BizProtocolSample.__table__,
|
||||
BizProtocolStatusChange.__table__,
|
||||
BizSampleReception.__table__,
|
||||
BizResearchDonation.__table__,
|
||||
BizResearchDonationSample.__table__,
|
||||
BizDonationStatusChange.__table__,
|
||||
],
|
||||
)
|
||||
return sessionmaker(bind=engine)()
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime(2026, 11, 5, 12, 0, 0)
|
||||
|
||||
|
||||
class TestProtocolReceptionDiscardSync(unittest.TestCase):
|
||||
def test_ensure_lab_void_type_creates_dict_row(self):
|
||||
db = _session()
|
||||
code = ensure_lab_void_type(db, tenant_no="tenant-a")
|
||||
self.assertEqual(code, LAB_VOID_TYPE_CODE)
|
||||
row = (
|
||||
db.query(SysDictData)
|
||||
.filter(
|
||||
SysDictData.tenant_no == "tenant-a",
|
||||
SysDictData.dict_type == "xyzflx",
|
||||
SysDictData.dict_value == LAB_VOID_TYPE_CODE,
|
||||
)
|
||||
.one()
|
||||
)
|
||||
self.assertEqual(row.dict_label, "实验室废弃")
|
||||
# idempotent
|
||||
self.assertEqual(ensure_lab_void_type(db, tenant_no="tenant-a"), LAB_VOID_TYPE_CODE)
|
||||
|
||||
def test_protocol_void_marks_all_sample_receptions_discarded(self):
|
||||
db = _session()
|
||||
now = _now()
|
||||
db.add(
|
||||
BizCollectionProtocol(
|
||||
id="p1",
|
||||
protocol_no="CP-1",
|
||||
protocol_status="sxz",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
BizProtocolSample(
|
||||
id="s1",
|
||||
collection_protocol_id="p1",
|
||||
sample_code="ZK001",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
BizProtocolSample(
|
||||
id="s2",
|
||||
collection_protocol_id="p1",
|
||||
sample_code="ZK002",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
BizSampleReception(
|
||||
id="r1",
|
||||
protocol_sample_id="s1",
|
||||
collection_protocol_id="p1",
|
||||
reception_time=now,
|
||||
accept_conclusion="js",
|
||||
reception_source="cell_storage",
|
||||
sample_code="ZK001",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
check_packaging=0,
|
||||
check_sealing=0,
|
||||
check_no_damage=0,
|
||||
check_no_leakage=0,
|
||||
check_color=0,
|
||||
check_tying_rope=0,
|
||||
check_no_stratification=0,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
after_protocol_voided(
|
||||
db,
|
||||
source="cell_storage",
|
||||
protocol_id="p1",
|
||||
tenant_no="tenant-a",
|
||||
user_id="u1",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
rows = (
|
||||
db.query(BizSampleReception)
|
||||
.filter(BizSampleReception.tenant_no == "tenant-a", BizSampleReception.del_flag == 0)
|
||||
.all()
|
||||
)
|
||||
self.assertEqual(len(rows), 2)
|
||||
by_code = {r.sample_code: r.accept_conclusion for r in rows}
|
||||
self.assertEqual(by_code["ZK001"], "bj")
|
||||
self.assertEqual(by_code["ZK002"], "bj")
|
||||
|
||||
def test_all_samples_discarded_auto_voids_protocol_with_lab_type(self):
|
||||
db = _session()
|
||||
now = _now()
|
||||
db.add(
|
||||
BizCollectionProtocol(
|
||||
id="p2",
|
||||
protocol_no="CP-2",
|
||||
protocol_status="sxz",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
BizProtocolSample(
|
||||
id="s3",
|
||||
collection_protocol_id="p2",
|
||||
sample_code="ZK003",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
rec = BizSampleReception(
|
||||
id="r2",
|
||||
protocol_sample_id="s3",
|
||||
collection_protocol_id="p2",
|
||||
reception_time=now,
|
||||
accept_conclusion="bj",
|
||||
reception_source="cell_storage",
|
||||
sample_code="ZK003",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
check_packaging=0,
|
||||
check_sealing=0,
|
||||
check_no_damage=0,
|
||||
check_no_leakage=0,
|
||||
check_color=0,
|
||||
check_tying_rope=0,
|
||||
check_no_stratification=0,
|
||||
)
|
||||
db.add(rec)
|
||||
db.commit()
|
||||
|
||||
after_sample_reception_saved(
|
||||
db, reception=rec, tenant_no="tenant-a", user_id="u1"
|
||||
)
|
||||
db.commit()
|
||||
|
||||
proto = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == "p2").one()
|
||||
self.assertEqual(proto.protocol_status, "yzf")
|
||||
self.assertEqual(proto.void_type, LAB_VOID_TYPE_CODE)
|
||||
self.assertIn("全部样本接收", proto.void_desc or "")
|
||||
|
||||
def test_partial_discard_does_not_void_protocol(self):
|
||||
db = _session()
|
||||
now = _now()
|
||||
db.add(
|
||||
BizCollectionProtocol(
|
||||
id="p3",
|
||||
protocol_no="CP-3",
|
||||
protocol_status="sxz",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
for sid, code in (("s4", "ZK004"), ("s5", "ZK005")):
|
||||
db.add(
|
||||
BizProtocolSample(
|
||||
id=sid,
|
||||
collection_protocol_id="p3",
|
||||
sample_code=code,
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
rec = BizSampleReception(
|
||||
id="r3",
|
||||
protocol_sample_id="s4",
|
||||
collection_protocol_id="p3",
|
||||
reception_time=now,
|
||||
accept_conclusion="bj",
|
||||
reception_source="cell_storage",
|
||||
sample_code="ZK004",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
check_packaging=0,
|
||||
check_sealing=0,
|
||||
check_no_damage=0,
|
||||
check_no_leakage=0,
|
||||
check_color=0,
|
||||
check_tying_rope=0,
|
||||
check_no_stratification=0,
|
||||
)
|
||||
db.add(rec)
|
||||
db.commit()
|
||||
|
||||
after_sample_reception_saved(
|
||||
db, reception=rec, tenant_no="tenant-a", user_id="u1"
|
||||
)
|
||||
db.commit()
|
||||
|
||||
proto = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == "p3").one()
|
||||
self.assertEqual(proto.protocol_status, "sxz")
|
||||
self.assertFalse(proto.void_type)
|
||||
|
||||
def test_donation_void_creates_discard_receptions(self):
|
||||
db = _session()
|
||||
now = _now()
|
||||
db.add(
|
||||
BizResearchDonation(
|
||||
id="d1",
|
||||
donation_no="RD-1",
|
||||
donation_status="sxz",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
BizResearchDonationSample(
|
||||
id="ds1",
|
||||
research_donation_id="d1",
|
||||
sample_code="RD001",
|
||||
tenant_no="tenant-a",
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
after_protocol_voided(
|
||||
db,
|
||||
source="research_donation",
|
||||
protocol_id="d1",
|
||||
tenant_no="tenant-a",
|
||||
user_id="u1",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
rec = (
|
||||
db.query(BizSampleReception)
|
||||
.filter(
|
||||
BizSampleReception.sample_code == "RD001",
|
||||
BizSampleReception.reception_source == "research_donation",
|
||||
)
|
||||
.one()
|
||||
)
|
||||
self.assertEqual(rec.accept_conclusion, "bj")
|
||||
self.assertEqual(rec.research_donation_id, "d1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -60,9 +60,17 @@
|
||||
},
|
||||
"figma": {
|
||||
"references": [
|
||||
"https://www.figma.com/design/jc7D8Fe7lKNTVj4jM3AoGW?node-id=2-2"
|
||||
"https://www.figma.com/design/M9XtXqXRCAKccMWD7BivIL",
|
||||
"https://www.figma.com/design/W3yXdlX4TbQP0PVhFG8cBB"
|
||||
],
|
||||
"status": "user-confirmed-demo-only"
|
||||
"status": "web-and-mobile-design-systems-v1",
|
||||
"authority": {
|
||||
"web-admin": "https://www.figma.com/design/M9XtXqXRCAKccMWD7BivIL",
|
||||
"mobile-h5": "https://www.figma.com/design/W3yXdlX4TbQP0PVhFG8cBB",
|
||||
"mobile-oa-h5": "https://www.figma.com/design/W3yXdlX4TbQP0PVhFG8cBB"
|
||||
},
|
||||
"archive": null,
|
||||
"notes": "Web 用 Fresh Mint(#10b981);移动端用独立 Mobile Design System(规范品牌绿 #0a9f6d),禁止混色。旧整页镜像文件已删除。Figma 色板按规范,不以 styles/tokens.scss 的 #22c55e 覆盖。已补高频状态:Web StateBlock/FormAlert(P01 旁展示);Mobile Empty/ConfirmDialog/WebViewState。"
|
||||
},
|
||||
"delivery": {
|
||||
"provider": "drone",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Issue #31:作废类型字典补充「实验室废弃」(sysfq)
|
||||
-- 为已有 xyzflx 字典的租户补齐;可重复执行。
|
||||
|
||||
INSERT INTO sys_dict_data (
|
||||
id, dict_label, dict_value, dict_type, tenant_no, order_num, remark,
|
||||
create_dept_id, create_time, update_time, del_flag
|
||||
)
|
||||
SELECT
|
||||
MD5(CONCAT(t.tenant_no, ':xyzflx:sysfq')),
|
||||
'实验室废弃',
|
||||
'sysfq',
|
||||
'xyzflx',
|
||||
t.tenant_no,
|
||||
90,
|
||||
'issue#31 实验室废弃',
|
||||
'',
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
FROM (
|
||||
SELECT DISTINCT tenant_no
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = 'xyzflx' AND del_flag = 0
|
||||
) t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_dict_data existing
|
||||
WHERE existing.tenant_no = t.tenant_no
|
||||
AND existing.dict_type = 'xyzflx'
|
||||
AND existing.dict_value = 'sysfq'
|
||||
AND existing.del_flag = 0
|
||||
);
|
||||
@@ -363,7 +363,7 @@ export const taskStatusTagType = createTagTypeGetter(taskStatus)
|
||||
|
||||
export const acceptConclusion: StatusRegistry = {
|
||||
js: { label: '接收', tagType: 'success' },
|
||||
bj: { label: '拒收', tagType: 'danger' }
|
||||
bj: { label: '废弃', tagType: 'danger' }
|
||||
}
|
||||
export const acceptConclusionLabel = createLabelGetter(acceptConclusion)
|
||||
export const acceptConclusionTagType = createTagTypeGetter(acceptConclusion)
|
||||
|
||||
@@ -6,7 +6,8 @@ export const VOID_TYPE_DICT = 'xyzflx'
|
||||
|
||||
const FALLBACK_LABELS: Record<string, string> = {
|
||||
khhc: '客户毁存',
|
||||
jcbhg: '检测不合格'
|
||||
jcbhg: '检测不合格',
|
||||
sysfq: '实验室废弃'
|
||||
}
|
||||
|
||||
function buildFallbackOptions(): Api.Dict.DictDataOption[] {
|
||||
|
||||
@@ -69,8 +69,7 @@
|
||||
@selection-change="(rows: any[]) => (selectedRows = rows)"
|
||||
@row-dblclick="handleEdit"
|
||||
>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -79,6 +78,8 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="orderDate"
|
||||
label="业务日期"
|
||||
|
||||
@@ -132,11 +132,12 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleRowDblClick"
|
||||
>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column label="序号" min-width="64" align="center" header-align="center">
|
||||
<template #default="{ $index }">{{ $index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="collectDate"
|
||||
label="采集日期"
|
||||
|
||||
@@ -144,11 +144,12 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleRowDblClick"
|
||||
>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column label="序号" min-width="64" align="center" header-align="center">
|
||||
<template #default="{ $index }">{{ $index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="collectDate"
|
||||
label="采集日期"
|
||||
|
||||
@@ -105,8 +105,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -115,6 +114,8 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="protocolStatus"
|
||||
label="状态"
|
||||
|
||||
@@ -116,9 +116,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column v-if="!isClinical" prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column v-if="!isClinical" prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -127,6 +125,8 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column v-if="!isClinical" prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column v-if="!isClinical" prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="applicationNo"
|
||||
label="申请单号"
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="`${readonly ? '查看' : editingId ? '编辑' : '新增'}${isTeam ? '项目组' : '项目'}`"
|
||||
width="720px"
|
||||
:width="isTeam && !readonly ? '960px' : '720px'"
|
||||
class="dialog-form-layout"
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
@@ -163,7 +163,7 @@
|
||||
:label="user.name" /></el-select
|
||||
></el-form-item>
|
||||
<template v-if="isTeam">
|
||||
<el-form-item label="成员"
|
||||
<el-form-item label="成员" :class="{ 'form-col-span-2': !readonly }"
|
||||
><el-select v-model="memberIds" multiple filterable
|
||||
><el-option
|
||||
v-for="user in refs.users"
|
||||
@@ -171,7 +171,7 @@
|
||||
:value="user.id"
|
||||
:label="user.name" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="负责地区(信息记录)"
|
||||
<el-form-item label="负责地区(信息记录)" :class="{ 'form-col-span-2': !readonly }"
|
||||
><el-select v-model="regionIds" multiple filterable
|
||||
><el-option
|
||||
v-for="region in refs.regions"
|
||||
@@ -179,7 +179,7 @@
|
||||
:value="region.id"
|
||||
:label="region.name" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="负责医院(信息记录)"
|
||||
<el-form-item label="负责医院(信息记录)" :class="{ 'form-col-span-2': !readonly }"
|
||||
><el-select v-model="hospitalIds" multiple filterable
|
||||
><el-option
|
||||
v-for="hospital in refs.hospitals"
|
||||
@@ -187,7 +187,9 @@
|
||||
:value="hospital.id"
|
||||
:label="hospital.name" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="form.enabled" /></el-form-item>
|
||||
<el-form-item label="启用" :class="{ 'form-col-span-2': !readonly }"
|
||||
><el-switch v-model="form.enabled"
|
||||
/></el-form-item>
|
||||
<div class="form-tip form-col-span-2"
|
||||
>地区、医院仅记录项目组职责,不改变业务地点候选或数据权限。</div
|
||||
>
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
@change="onReceiveAcceptConclusionChange"
|
||||
>
|
||||
<el-radio value="js">接收</el-radio>
|
||||
<el-radio value="bj">拒收</el-radio>
|
||||
<el-radio value="bj">废弃</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="实验室入库编码" prop="labStorageCode">
|
||||
@@ -643,7 +643,7 @@
|
||||
@change="() => onBatchAcceptConclusionChange(entry)"
|
||||
>
|
||||
<el-radio value="js">接收</el-radio>
|
||||
<el-radio value="bj">拒收</el-radio>
|
||||
<el-radio value="bj">废弃</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
|
||||
@@ -93,8 +93,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -103,6 +102,8 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160" />
|
||||
<el-table-column prop="teamName" label="项目组" min-width="150" />
|
||||
<el-table-column
|
||||
prop="donationStatus"
|
||||
label="状态"
|
||||
|
||||
Reference in new issue
Block a user