From f28e82c1b3a4591f357ce9b5644cfc1b146efdbd Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 2 Sep 2026 11:39:04 +0800 Subject: [PATCH] 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 --- backend/app/services/pl_plan_service.py | 31 +++++++++++++ backend/tests/test_pl_plan_draft_save.py | 58 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 backend/tests/test_pl_plan_draft_save.py diff --git a/backend/app/services/pl_plan_service.py b/backend/app/services/pl_plan_service.py index 9d12de9bf..10ed7d184 100644 --- a/backend/app/services/pl_plan_service.py +++ b/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="请选择供方客户") diff --git a/backend/tests/test_pl_plan_draft_save.py b/backend/tests/test_pl_plan_draft_save.py new file mode 100644 index 000000000..ba946ceb1 --- /dev/null +++ b/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)