Parcourir la source

fix: 已结算运费在协议保存时拒绝取消关联

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu il y a 1 semaine
Parent
commit
bd669ea77e
2 fichiers modifiés avec 170 ajouts et 5 suppressions
  1. 44 5
      backend/app/crud/sample_freight.py
  2. 126 0
      backend/tests/test_sf_express_settlement.py

+ 44 - 5
backend/app/crud/sample_freight.py

@@ -949,11 +949,13 @@ def link_samples(
     items: List[Dict[str, Any]],
     commit: bool = True,
     user_info: Optional[dict] = None,
+    allow_settled_relink: bool = False,
 ) -> Dict[str, Any]:
     header = get_freight(db, freight_id, tenant_no, user_info)
     if not header:
         raise ValueError("运费批次不存在")
-    assert_freight_mutable(header.settle_status, "items")
+    if not allow_settled_relink:
+        assert_freight_mutable(header.settle_status, "items")
     if not items:
         raise ValueError("请选择要关联的样本")
     if user_info is not None:
@@ -1156,6 +1158,16 @@ def freight_map_for_samples(
     return out
 
 
+def _input_freight_id(raw: Any) -> str:
+    if not isinstance(raw, dict):
+        return ""
+    nested = raw.get("freight") if isinstance(raw.get("freight"), dict) else {}
+    return (
+        str(raw.get("freightId") or raw.get("freight_id") or "").strip()
+        or str(nested.get("id") or "").strip()
+    )
+
+
 def apply_freight_on_protocol_save(
     db: Session,
     *,
@@ -1190,6 +1202,34 @@ def apply_freight_on_protocol_save(
     )
     now = _now()
     affected_freight_ids = {item.freight_id for item in stale_items}
+    stale_by_freight: Dict[str, List[BizSampleFreightItem]] = {}
+    for item in stale_items:
+        stale_by_freight.setdefault(item.freight_id, []).append(item)
+    settled_relink_ids = set()
+    if stale_by_freight:
+        headers = (
+            db.query(BizSampleFreight)
+            .filter(
+                BizSampleFreight.id.in_(list(stale_by_freight.keys())),
+                BizSampleFreight.tenant_no == tenant_no,
+                BizSampleFreight.del_flag == 0,
+            )
+            .all()
+        )
+        header_by_id = {row.id: row for row in headers}
+        for freight_id, grouped in stale_by_freight.items():
+            header = header_by_id.get(freight_id)
+            if header is None or (header.settle_status or "") != "yjs":
+                continue
+            stale_count = len(grouped)
+            keep_count = sum(
+                1
+                for raw in (input_samples or [])
+                if _input_freight_id(raw) == freight_id
+            )
+            if keep_count != stale_count:
+                raise ValueError("已结算的运费不能调整样本")
+            settled_relink_ids.add(freight_id)
     for stale in stale_items:
         stale.del_flag = 1
         stale.update_by = user_id
@@ -1218,10 +1258,7 @@ def apply_freight_on_protocol_save(
             "protocolId": protocol_id,
             "sampleCode": sample_code,
         }
-        freight_id = (
-            str(raw.get("freightId") or raw.get("freight_id") or "").strip()
-            or str((raw.get("freight") or {}).get("id") or "").strip()
-        )
+        freight_id = _input_freight_id(raw)
         draft = raw.get("freightDraft")
         if draft is None:
             draft = raw.get("freight_draft")
@@ -1233,6 +1270,7 @@ def apply_freight_on_protocol_save(
                 user_id=user_id,
                 items=[item],
                 commit=False,
+                allow_settled_relink=freight_id in settled_relink_ids,
             )
             continue
         if isinstance(draft, dict):
@@ -1281,6 +1319,7 @@ def apply_freight_on_protocol_save(
             .first()
         )
         if freight is not None:
+            assert_freight_mutable(freight.settle_status, "delete")
             freight.del_flag = 1
             freight.update_by = user_id
             freight.update_time = now

+ 126 - 0
backend/tests/test_sf_express_settlement.py

@@ -259,3 +259,129 @@ def test_admin_and_market_bodies_accept_sf_without_amount():
     })
     assert draft.waybill_no == "SF100"
     MarketFreightUpdateBody.model_validate({"waybillNo": "SF100"})
+
+
+def _protocol_freight_db():
+    from sqlalchemy import create_engine
+    from sqlalchemy.orm import sessionmaker
+
+    from app.models.base import Base
+    from app.models.models import (
+        BizCellSampleType,
+        BizCollectionProtocol,
+        BizProtocolPartyCustomer,
+        BizProtocolSample,
+        BizSampleFreight,
+        BizSampleFreightItem,
+        SysDictData,
+    )
+
+    engine = create_engine("sqlite:///:memory:")
+    Base.metadata.create_all(
+        engine,
+        tables=[
+            BizSampleFreight.__table__,
+            BizSampleFreightItem.__table__,
+            BizProtocolSample.__table__,
+            BizCollectionProtocol.__table__,
+            BizCellSampleType.__table__,
+            BizProtocolPartyCustomer.__table__,
+            SysDictData.__table__,
+        ],
+    )
+    return sessionmaker(bind=engine)()
+
+
+def _seed_settled_protocol_freight(db, sample_id="s-old"):
+    from app.models.models import BizSampleFreight, BizSampleFreightItem
+
+    now = datetime(2026, 10, 21, 9, 0, 0)
+    freight = BizSampleFreight(
+        id="f-yjs",
+        freight_no="YF1",
+        transport_mode="sfkd",
+        freight_amount=None,
+        waybill_no="SF001",
+        settle_status="yjs",
+        tenant_no="tenant-a",
+        create_dept_id="",
+        create_time=now,
+        update_time=now,
+        del_flag=0,
+    )
+    item = BizSampleFreightItem(
+        id="fi-1",
+        freight_id=freight.id,
+        sample_source="cell_storage",
+        sample_id=sample_id,
+        sample_code="C1",
+        protocol_id="p1",
+        tenant_no="tenant-a",
+        create_dept_id="",
+        create_time=now,
+        update_time=now,
+        del_flag=0,
+    )
+    db.add_all([freight, item])
+    db.commit()
+    return freight, item
+
+
+def test_settled_protocol_save_without_freight_id_keeps_links():
+    from types import SimpleNamespace
+
+    from app.crud.sample_freight import apply_freight_on_protocol_save
+    from app.models.models import BizSampleFreight, BizSampleFreightItem
+
+    db = _protocol_freight_db()
+    freight, item = _seed_settled_protocol_freight(db)
+    try:
+        with pytest.raises(ValueError, match="已结算的运费不能调整样本"):
+            apply_freight_on_protocol_save(
+                db,
+                tenant_no="tenant-a",
+                user_id="u1",
+                sample_source="cell_storage",
+                protocol_id="p1",
+                saved_samples=[SimpleNamespace(id="s-new", sample_code="C2")],
+                input_samples=[{}],
+            )
+        db.expire_all()
+        item = db.query(BizSampleFreightItem).filter_by(id=item.id).one()
+        freight = db.query(BizSampleFreight).filter_by(id=freight.id).one()
+        assert item.del_flag == 0
+        assert freight.del_flag == 0
+    finally:
+        db.close()
+
+
+def test_settled_protocol_save_relinks_same_count_to_new_sample():
+    from types import SimpleNamespace
+
+    from app.crud.sample_freight import apply_freight_on_protocol_save
+    from app.models.models import BizSampleFreight, BizSampleFreightItem
+
+    db = _protocol_freight_db()
+    freight, _item = _seed_settled_protocol_freight(db)
+    try:
+        apply_freight_on_protocol_save(
+            db,
+            tenant_no="tenant-a",
+            user_id="u1",
+            sample_source="cell_storage",
+            protocol_id="p1",
+            saved_samples=[SimpleNamespace(id="s-new", sample_code="C2")],
+            input_samples=[{"freightId": freight.id}],
+        )
+        db.expire_all()
+        freight = db.query(BizSampleFreight).filter_by(id=freight.id).one()
+        assert freight.del_flag == 0
+        assert freight.settle_status == "yjs"
+        live = (
+            db.query(BizSampleFreightItem)
+            .filter_by(freight_id=freight.id, sample_id="s-new", del_flag=0)
+            .one()
+        )
+        assert live.protocol_id == "p1"
+    finally:
+        db.close()