瀏覽代碼

feat(backend): add draft save helpers for plan follow status drafting

Introduce min-field validation and follow_status resolution for saveMode draft/formal.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 月之前
父節點
當前提交
f28e82c1b3
共有 2 個文件被更改,包括 89 次插入 和 0 次删除
  1. 31 0
      backend/app/services/pl_plan_service.py
  2. 58 0
      backend/tests/test_pl_plan_draft_save.py

+ 31 - 0
backend/app/services/pl_plan_service.py

@@ -1263,6 +1263,37 @@ def load_plan_contracts_grouped(
     return {pid: (lists[0], lists[1]) for pid, lists in buckets.items()}
 
 
+DRAFTABLE_FOLLOW_STATUSES = frozenset({"drafting", "pending_arrange"})
+
+
+def validate_draft_min_fields(*, belongs_company: Optional[str], create_date) -> None:
+    if not (belongs_company or "").strip():
+        raise HTTPException(status_code=422, detail="请选择归属公司")
+    # create_date 允许为空,由 create/update 用 _resolve_create_date 兜底当天
+
+
+def resolve_follow_status_on_save(
+    *,
+    save_mode: str,
+    current: Optional[str],
+    is_create: bool,
+) -> str:
+    mode = (save_mode or "formal").strip() or "formal"
+    if mode == "draft":
+        return "drafting"
+    if is_create:
+        return "pending_arrange"
+    if current == "drafting":
+        return "pending_arrange"
+    return (current or "pending_arrange").strip() or "pending_arrange"
+
+
+def assert_can_save_as_draft(current_status: Optional[str]) -> None:
+    status = (current_status or "").strip()
+    if status not in DRAFTABLE_FOLLOW_STATUSES:
+        raise HTTPException(status_code=400, detail="当前状态不可暂存")
+
+
 def validate_create_full(payload: PlanCreateFull) -> None:
     if not (payload.supplier_id or "").strip() or not (payload.supplier_name or "").strip():
         raise HTTPException(status_code=422, detail="请选择供方客户")

+ 58 - 0
backend/tests/test_pl_plan_draft_save.py

@@ -0,0 +1,58 @@
+import pytest
+from fastapi import HTTPException
+
+from app.services.pl_plan_service import (
+    DRAFTABLE_FOLLOW_STATUSES,
+    assert_can_save_as_draft,
+    resolve_follow_status_on_save,
+    validate_draft_min_fields,
+)
+
+
+def test_validate_draft_min_fields_requires_belongs_company():
+    with pytest.raises(HTTPException) as exc:
+        validate_draft_min_fields(belongs_company="", create_date=None)
+    assert exc.value.status_code == 422
+    assert "归属公司" in str(exc.value.detail)
+
+
+def test_validate_draft_min_fields_allows_missing_create_date():
+    validate_draft_min_fields(belongs_company="shanxia", create_date=None)
+
+
+def test_resolve_follow_status_draft_always_drafting():
+    assert (
+        resolve_follow_status_on_save(
+            save_mode="draft", current="pending_arrange", is_create=False
+        )
+        == "drafting"
+    )
+    assert resolve_follow_status_on_save(save_mode="draft", current=None, is_create=True) == "drafting"
+
+
+def test_resolve_follow_status_formal_promotes_drafting():
+    assert (
+        resolve_follow_status_on_save(save_mode="formal", current=None, is_create=True)
+        == "pending_arrange"
+    )
+    assert (
+        resolve_follow_status_on_save(save_mode="formal", current="drafting", is_create=False)
+        == "pending_arrange"
+    )
+    assert (
+        resolve_follow_status_on_save(save_mode="formal", current="arranged", is_create=False)
+        == "arranged"
+    )
+    assert (
+        resolve_follow_status_on_save(save_mode="formal", current="wait_notice", is_create=False)
+        == "wait_notice"
+    )
+
+
+def test_assert_can_save_as_draft_rejects_arranged():
+    assert "drafting" in DRAFTABLE_FOLLOW_STATUSES
+    assert "pending_arrange" in DRAFTABLE_FOLLOW_STATUSES
+    with pytest.raises(HTTPException) as exc:
+        assert_can_save_as_draft("arranged")
+    assert exc.value.status_code == 400
+    assert "不可暂存" in str(exc.value.detail)