feat(backend): support saveMode draft/formal and drafting follow gates

Draft skips business validators and writes drafting; formal promotes drafting to pending_arrange; wait-notice/delete/force allow drafting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-02 11:40:54 +08:00
1 parent f9cdb8852b
commit ae91fbc63e
1 file changed
+89 -14
+89 -14
View File
@@ -140,7 +140,10 @@ from app.services.pl_plan_service import (
set_plan_erp_entered,
sync_transport_prefill_for_plan,
update_plan_product_certs,
assert_can_save_as_draft,
resolve_follow_status_on_save,
validate_create_full,
validate_draft_min_fields,
validate_hazard_cert_ready_for_transport,
validate_plan_party_contracts,
)
@@ -1097,10 +1100,36 @@ def create_plan_full(
) -> PlanWithProducts:
"""创建运输计划(完整表单:供方/需方 + 产品库产品 + 可选预选车辆,不自动安排运输)"""
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
save_mode = (getattr(payload, "save_mode", None) or "formal").strip() or "formal"
is_draft = save_mode == "draft"
multi_stop = uses_multi_stop_payload(payload.load, payload.deliveries)
qty_warnings: list[str] = []
load_body = None
delivery_bodies: list = []
supplier_contracts: list[PlanContractLink] = []
demander_contracts: list[PlanContractLink] = []
if multi_stop:
if is_draft:
validate_draft_min_fields(
belongs_company=payload.belongs_company,
create_date=payload.create_date,
)
if multi_stop and payload.load is not None:
try:
load_body, delivery_bodies = validate_plan_stops_write(
db,
tenant_no,
payload.load,
payload.deliveries,
payload.products,
)
qty_warnings = collect_stop_qty_warnings(
payload.products, load_body, delivery_bodies
)
except HTTPException:
load_body, delivery_bodies = None, []
multi_stop = False
elif multi_stop:
load_body, delivery_bodies = validate_plan_stops_write(
db,
tenant_no,
@@ -1109,8 +1138,6 @@ def create_plan_full(
payload.products,
)
qty_warnings = collect_stop_qty_warnings(payload.products, load_body, delivery_bodies)
supplier_contracts: list[PlanContractLink] = []
demander_contracts: list[PlanContractLink] = []
else:
validate_create_full(payload)
supplier_contracts, demander_contracts = validate_plan_party_contracts(
@@ -1142,7 +1169,9 @@ def create_plan_full(
plan_date=payload.plan_date,
belongs_company=payload.belongs_company,
plan_type=plan_type,
follow_status="pending_arrange",
follow_status=resolve_follow_status_on_save(
save_mode=save_mode, current=None, is_create=True
),
transport_status="pending",
archive_status="unarchived",
complete_status="no",
@@ -1466,18 +1495,34 @@ def update_plan(
if plan.follow_status == "cancelled":
raise HTTPException(status_code=400, detail="已作废的计划不能修改")
save_mode = (getattr(payload, "save_mode", None) or "formal").strip() or "formal"
is_draft = save_mode == "draft"
if is_draft:
assert_can_save_as_draft(plan.follow_status)
validate_draft_min_fields(
belongs_company=(
payload.belongs_company
if payload.belongs_company is not None
else plan.belongs_company
),
create_date=(
payload.create_date if payload.create_date is not None else plan.create_date
),
)
multi_stop = uses_multi_stop_payload(payload.load, payload.deliveries)
qty_warnings: list[str] = []
load_body: Optional[PlanLoadBody] = None
delivery_bodies: list[PlanDeliveryBody] = []
# 更新主表字段(跟进状态仅由业务动作接口维护,忽略客户端写入)
# 更新主表字段(跟进状态仅由业务动作 / saveMode 决议维护,忽略客户端 follow_status)
update_data = payload.model_dump(
exclude_unset=True,
exclude={
"products",
"transport",
"follow_status",
"save_mode",
"supplier_contracts",
"demander_contracts",
"load",
@@ -1501,7 +1546,32 @@ def update_plan(
customer_id = plan.customer_id
customer_name = plan.customer_name
if multi_stop:
if is_draft:
supplier_contracts: list[PlanContractLink] = []
demander_contracts: list[PlanContractLink] = []
if multi_stop and payload.load is not None:
try:
load_body, delivery_bodies = validate_plan_stops_write(
db,
tenant_no,
payload.load,
payload.deliveries,
payload.products,
)
products_for_warn = payload.products if payload.products is not None else None
qty_warnings = collect_stop_qty_warnings(
products_for_warn, load_body, delivery_bodies
)
sync_plan_legacy_from_stops(plan, load_body, delivery_bodies)
except HTTPException:
# 跳过段更新,保留库内原段
load_body, delivery_bodies = None, []
multi_stop = False
if not multi_stop:
supplier_contracts, demander_contracts = load_plan_contracts_grouped(
db, tenant_no, [id]
).get(id, ([], []))
elif multi_stop:
load_body, delivery_bodies = validate_plan_stops_write(
db,
tenant_no,
@@ -1512,8 +1582,8 @@ def update_plan(
products_for_warn = payload.products if payload.products is not None else None
qty_warnings = collect_stop_qty_warnings(products_for_warn, load_body, delivery_bodies)
sync_plan_legacy_from_stops(plan, load_body, delivery_bodies)
supplier_contracts: list[PlanContractLink] = []
demander_contracts: list[PlanContractLink] = []
supplier_contracts = []
demander_contracts = []
else:
supplier_contracts_in = (
payload.supplier_contracts
@@ -1677,6 +1747,11 @@ def update_plan(
)
plan.update_by = user.id
plan.follow_status = resolve_follow_status_on_save(
save_mode=save_mode,
current=plan.follow_status,
is_create=False,
)
dibang_date_warnings = sync_linked_dibang_dates_for_plan(
db,
tenant_no=tenant_no,
@@ -1932,8 +2007,8 @@ def mark_plan_wait_notice(
raise HTTPException(status_code=400, detail="已取消的计划不能标记等通知")
if plan.archive_status == "archived":
raise HTTPException(status_code=400, detail="已归档的计划不能标记等通知")
if plan.follow_status != "pending_arrange":
raise HTTPException(status_code=400, detail="仅待安排状态可标记为等通知")
if plan.follow_status not in ("pending_arrange", "drafting"):
raise HTTPException(status_code=400, detail="仅待安排或草稿中状态可标记为等通知")
plan.follow_status = "wait_notice"
plan.update_by = user.id
@@ -1966,7 +2041,7 @@ def cancel_plan_wait_notice(
FORCE_FOLLOW_STATUS_PERM = "pl:plan:force-follow-status"
ALLOWED_FORCE_FOLLOW_STATUSES = frozenset(
{"wait_notice", "pending_arrange", "arranged", "completed", "cancelled"}
{"drafting", "wait_notice", "pending_arrange", "arranged", "completed", "cancelled"}
)
@@ -1987,7 +2062,7 @@ def force_plan_follow_status(
if next_status not in ALLOWED_FORCE_FOLLOW_STATUSES:
raise HTTPException(
status_code=400,
detail="跟进状态仅支持:等通知/待安排/已安排/已完成/已取消",
detail="跟进状态仅支持:草稿中/等通知/待安排/已安排/已完成/已取消",
)
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
@@ -2484,8 +2559,8 @@ def delete_plan(
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
if plan.follow_status not in ("pending_arrange", "wait_notice"):
raise HTTPException(status_code=400, detail="只能删除待安排或等通知且未安排运输的计划")
if plan.follow_status not in ("pending_arrange", "wait_notice", "drafting"):
raise HTTPException(status_code=400, detail="只能删除待安排、草稿中或等通知且未安排运输的计划")
if plan.transport_status != "pending":
raise HTTPException(status_code=400, detail="已安排运输的计划不能删除")
if plan.archive_status == "archived":