|
@@ -140,7 +140,10 @@ from app.services.pl_plan_service import (
|
|
|
set_plan_erp_entered,
|
|
set_plan_erp_entered,
|
|
|
sync_transport_prefill_for_plan,
|
|
sync_transport_prefill_for_plan,
|
|
|
update_plan_product_certs,
|
|
update_plan_product_certs,
|
|
|
|
|
+ assert_can_save_as_draft,
|
|
|
|
|
+ resolve_follow_status_on_save,
|
|
|
validate_create_full,
|
|
validate_create_full,
|
|
|
|
|
+ validate_draft_min_fields,
|
|
|
validate_hazard_cert_ready_for_transport,
|
|
validate_hazard_cert_ready_for_transport,
|
|
|
validate_plan_party_contracts,
|
|
validate_plan_party_contracts,
|
|
|
)
|
|
)
|
|
@@ -1097,10 +1100,36 @@ def create_plan_full(
|
|
|
) -> PlanWithProducts:
|
|
) -> PlanWithProducts:
|
|
|
"""创建运输计划(完整表单:供方/需方 + 产品库产品 + 可选预选车辆,不自动安排运输)"""
|
|
"""创建运输计划(完整表单:供方/需方 + 产品库产品 + 可选预选车辆,不自动安排运输)"""
|
|
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
|
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)
|
|
multi_stop = uses_multi_stop_payload(payload.load, payload.deliveries)
|
|
|
qty_warnings: list[str] = []
|
|
qty_warnings: list[str] = []
|
|
|
-
|
|
|
|
|
- if multi_stop:
|
|
|
|
|
|
|
+ load_body = None
|
|
|
|
|
+ delivery_bodies: list = []
|
|
|
|
|
+ supplier_contracts: list[PlanContractLink] = []
|
|
|
|
|
+ demander_contracts: list[PlanContractLink] = []
|
|
|
|
|
+
|
|
|
|
|
+ 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(
|
|
load_body, delivery_bodies = validate_plan_stops_write(
|
|
|
db,
|
|
db,
|
|
|
tenant_no,
|
|
tenant_no,
|
|
@@ -1109,8 +1138,6 @@ def create_plan_full(
|
|
|
payload.products,
|
|
payload.products,
|
|
|
)
|
|
)
|
|
|
qty_warnings = collect_stop_qty_warnings(payload.products, load_body, delivery_bodies)
|
|
qty_warnings = collect_stop_qty_warnings(payload.products, load_body, delivery_bodies)
|
|
|
- supplier_contracts: list[PlanContractLink] = []
|
|
|
|
|
- demander_contracts: list[PlanContractLink] = []
|
|
|
|
|
else:
|
|
else:
|
|
|
validate_create_full(payload)
|
|
validate_create_full(payload)
|
|
|
supplier_contracts, demander_contracts = validate_plan_party_contracts(
|
|
supplier_contracts, demander_contracts = validate_plan_party_contracts(
|
|
@@ -1142,7 +1169,9 @@ def create_plan_full(
|
|
|
plan_date=payload.plan_date,
|
|
plan_date=payload.plan_date,
|
|
|
belongs_company=payload.belongs_company,
|
|
belongs_company=payload.belongs_company,
|
|
|
plan_type=plan_type,
|
|
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",
|
|
transport_status="pending",
|
|
|
archive_status="unarchived",
|
|
archive_status="unarchived",
|
|
|
complete_status="no",
|
|
complete_status="no",
|
|
@@ -1466,18 +1495,34 @@ def update_plan(
|
|
|
if plan.follow_status == "cancelled":
|
|
if plan.follow_status == "cancelled":
|
|
|
raise HTTPException(status_code=400, detail="已作废的计划不能修改")
|
|
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)
|
|
multi_stop = uses_multi_stop_payload(payload.load, payload.deliveries)
|
|
|
qty_warnings: list[str] = []
|
|
qty_warnings: list[str] = []
|
|
|
load_body: Optional[PlanLoadBody] = None
|
|
load_body: Optional[PlanLoadBody] = None
|
|
|
delivery_bodies: list[PlanDeliveryBody] = []
|
|
delivery_bodies: list[PlanDeliveryBody] = []
|
|
|
|
|
|
|
|
- # 更新主表字段(跟进状态仅由业务动作接口维护,忽略客户端写入)
|
|
|
|
|
|
|
+ # 更新主表字段(跟进状态仅由业务动作 / saveMode 决议维护,忽略客户端 follow_status)
|
|
|
update_data = payload.model_dump(
|
|
update_data = payload.model_dump(
|
|
|
exclude_unset=True,
|
|
exclude_unset=True,
|
|
|
exclude={
|
|
exclude={
|
|
|
"products",
|
|
"products",
|
|
|
"transport",
|
|
"transport",
|
|
|
"follow_status",
|
|
"follow_status",
|
|
|
|
|
+ "save_mode",
|
|
|
"supplier_contracts",
|
|
"supplier_contracts",
|
|
|
"demander_contracts",
|
|
"demander_contracts",
|
|
|
"load",
|
|
"load",
|
|
@@ -1501,7 +1546,32 @@ def update_plan(
|
|
|
customer_id = plan.customer_id
|
|
customer_id = plan.customer_id
|
|
|
customer_name = plan.customer_name
|
|
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(
|
|
load_body, delivery_bodies = validate_plan_stops_write(
|
|
|
db,
|
|
db,
|
|
|
tenant_no,
|
|
tenant_no,
|
|
@@ -1512,8 +1582,8 @@ def update_plan(
|
|
|
products_for_warn = payload.products if payload.products is not None else None
|
|
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)
|
|
qty_warnings = collect_stop_qty_warnings(products_for_warn, load_body, delivery_bodies)
|
|
|
sync_plan_legacy_from_stops(plan, 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:
|
|
else:
|
|
|
supplier_contracts_in = (
|
|
supplier_contracts_in = (
|
|
|
payload.supplier_contracts
|
|
payload.supplier_contracts
|
|
@@ -1677,6 +1747,11 @@ def update_plan(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
plan.update_by = user.id
|
|
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(
|
|
dibang_date_warnings = sync_linked_dibang_dates_for_plan(
|
|
|
db,
|
|
db,
|
|
|
tenant_no=tenant_no,
|
|
tenant_no=tenant_no,
|
|
@@ -1932,8 +2007,8 @@ def mark_plan_wait_notice(
|
|
|
raise HTTPException(status_code=400, detail="已取消的计划不能标记等通知")
|
|
raise HTTPException(status_code=400, detail="已取消的计划不能标记等通知")
|
|
|
if plan.archive_status == "archived":
|
|
if plan.archive_status == "archived":
|
|
|
raise HTTPException(status_code=400, detail="已归档的计划不能标记等通知")
|
|
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.follow_status = "wait_notice"
|
|
|
plan.update_by = user.id
|
|
plan.update_by = user.id
|
|
@@ -1966,7 +2041,7 @@ def cancel_plan_wait_notice(
|
|
|
|
|
|
|
|
FORCE_FOLLOW_STATUS_PERM = "pl:plan:force-follow-status"
|
|
FORCE_FOLLOW_STATUS_PERM = "pl:plan:force-follow-status"
|
|
|
ALLOWED_FORCE_FOLLOW_STATUSES = frozenset(
|
|
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:
|
|
if next_status not in ALLOWED_FORCE_FOLLOW_STATUSES:
|
|
|
raise HTTPException(
|
|
raise HTTPException(
|
|
|
status_code=400,
|
|
status_code=400,
|
|
|
- detail="跟进状态仅支持:等通知/待安排/已安排/已完成/已取消",
|
|
|
|
|
|
|
+ detail="跟进状态仅支持:草稿中/等通知/待安排/已安排/已完成/已取消",
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
|
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())
|
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
|
|
plan = _get_plan(db, tenant_no, id, user)
|
|
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":
|
|
if plan.transport_status != "pending":
|
|
|
raise HTTPException(status_code=400, detail="已安排运输的计划不能删除")
|
|
raise HTTPException(status_code=400, detail="已安排运输的计划不能删除")
|
|
|
if plan.archive_status == "archived":
|
|
if plan.archive_status == "archived":
|