Просмотр исходного кода

feat(pl): 现场回执提交显式写入出入库单状态

删除首次提交自动改为已完成的隐式逻辑,改为由客户端传入 status。

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 месяцев назад
Родитель
Сommit
664acff1f3

+ 1 - 0
backend/app/api/pl/public_io_order.py

@@ -93,6 +93,7 @@ def _make_handlers(kind: str):
                 item_qty=qty_map,
                 remark=payload.remark,
                 images=images,
+                status=payload.status,
             )
         except ValueError as exc:
             return error_response(400, str(exc))

+ 1 - 0
backend/app/schemas/pl/io_order.py

@@ -92,6 +92,7 @@ class PublicIoOrderItemFeedback(BaseModel):
 
 
 class PublicIoOrderFeedback(BaseModel):
+    status: str
     remark: Optional[str] = None
     images: list[str] = Field(default_factory=list)
     items: list[PublicIoOrderItemFeedback] = Field(default_factory=list)

+ 7 - 2
backend/app/services/pl_io_order_service.py

@@ -304,6 +304,9 @@ def dump_feedback_images(images: list[str]) -> str:
     return json.dumps(images, ensure_ascii=False)
 
 
+_VALID_SITE_FEEDBACK_STATUSES = frozenset({"in_progress", "completed", "voided"})
+
+
 def apply_site_feedback(
     db: Session,
     *,
@@ -312,10 +315,13 @@ def apply_site_feedback(
     item_qty: dict[str, Decimal],
     remark: Optional[str],
     images: list[str],
+    status: str,
     user_id: Optional[str] = None,
 ) -> None:
     if header.status == "voided":
         raise ValueError("单据已作废,无法提交")
+    if status not in _VALID_SITE_FEEDBACK_STATUSES:
+        raise ValueError(f"无效状态: {status}")
     now = datetime.now()
     for item in items:
         if item.id not in item_qty:
@@ -328,8 +334,7 @@ def apply_site_feedback(
     header.site_feedback_time = now
     if not header.site_first_submitted_at:
         header.site_first_submitted_at = now
-        if header.status == "in_progress":
-            header.status = "completed"
+    header.status = status
     header.update_by = user_id
     db.flush()
 

+ 127 - 0
backend/tests/test_pl_io_order_site_feedback_status.py

@@ -0,0 +1,127 @@
+from datetime import datetime
+from decimal import Decimal
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+
+from app.services.pl_io_order_service import apply_site_feedback
+
+
+def _header(**kwargs):
+    defaults = dict(
+        status="in_progress",
+        site_feedback_remark=None,
+        site_feedback_images=None,
+        site_feedback_time=None,
+        site_first_submitted_at=None,
+        update_by=None,
+    )
+    defaults.update(kwargs)
+    return SimpleNamespace(**defaults)
+
+
+def _item(item_id="item-1"):
+    return SimpleNamespace(
+        id=item_id,
+        actual_qty=None,
+        qty_source=None,
+        update_by=None,
+    )
+
+
+def test_apply_site_feedback_sets_status_completed():
+    header = _header()
+    item = _item()
+    db = MagicMock()
+    apply_site_feedback(
+        db,
+        header=header,
+        items=[item],
+        item_qty={"item-1": Decimal("12.5")},
+        remark="ok",
+        images=["a.jpg"],
+        status="completed",
+    )
+    assert header.status == "completed"
+    assert item.actual_qty == Decimal("12.5")
+    assert header.site_first_submitted_at is not None
+    assert header.site_feedback_remark == "ok"
+    db.flush.assert_called_once()
+
+
+def test_apply_site_feedback_sets_status_voided():
+    header = _header()
+    apply_site_feedback(
+        MagicMock(),
+        header=header,
+        items=[_item()],
+        item_qty={"item-1": Decimal("1")},
+        remark=None,
+        images=[],
+        status="voided",
+    )
+    assert header.status == "voided"
+
+
+def test_apply_site_feedback_keeps_in_progress_when_requested():
+    """首次提交选进行中时,不得隐式改成已完成。"""
+    header = _header(status="in_progress")
+    apply_site_feedback(
+        MagicMock(),
+        header=header,
+        items=[_item()],
+        item_qty={"item-1": Decimal("1")},
+        remark=None,
+        images=[],
+        status="in_progress",
+    )
+    assert header.status == "in_progress"
+    assert header.site_first_submitted_at is not None
+
+
+def test_apply_site_feedback_rejects_invalid_status():
+    header = _header()
+    with pytest.raises(ValueError, match="无效状态"):
+        apply_site_feedback(
+            MagicMock(),
+            header=header,
+            items=[_item()],
+            item_qty={},
+            remark=None,
+            images=[],
+            status="done",
+        )
+
+
+def test_apply_site_feedback_rejects_already_voided():
+    header = _header(status="voided")
+    with pytest.raises(ValueError, match="单据已作废"):
+        apply_site_feedback(
+            MagicMock(),
+            header=header,
+            items=[_item()],
+            item_qty={},
+            remark=None,
+            images=[],
+            status="completed",
+        )
+
+
+def test_apply_site_feedback_does_not_reset_first_submitted_at():
+    first = datetime(2026, 1, 1, 12, 0, 0)
+    header = _header(
+        status="completed",
+        site_first_submitted_at=first,
+    )
+    apply_site_feedback(
+        MagicMock(),
+        header=header,
+        items=[_item()],
+        item_qty={"item-1": Decimal("2")},
+        remark="二改",
+        images=[],
+        status="in_progress",
+    )
+    assert header.status == "in_progress"
+    assert header.site_first_submitted_at == first