1
0
forked from erp-dev/erp

fix: business.object_id maybe null, missing parameters when plate-Order cloned

This commit is contained in:
2025-12-17 11:02:07 +08:00
parent 639efe8421
commit 51fad953c3
24 changed files with 13247 additions and 47 deletions

View File

@@ -4,11 +4,58 @@ Stateflow业务逻辑服务层
import copy
from typing import List, Optional, Tuple
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from . import models
User = get_user_model()
def ensure_business_object_bound_to_instance(
business_object: 'models.BusinessObject',
instance,
*,
default_name: str | None = None,
) -> bool:
"""
确保 BusinessObject 的 GenericForeignKey 绑定到指定实例。
说明:
- 这是一个“自愈”辅助函数,用于修复历史数据/非标准创建路径导致的 content_type/object_id 为空或不一致。
- 仅在调用方明确知道该 BusinessObject 应当绑定到 instance 时使用(例如 PrintingJob.business_object 一对一)。
返回:
- True本次发生了更新并保存
- False无需更新或参数不足business_object/instance/instance.pk 为空)
"""
if not business_object or instance is None:
return False
instance_id = getattr(instance, 'pk', None)
if not instance_id:
return False
ct = ContentType.objects.get_for_model(instance.__class__)
update_fields: list[str] = []
if business_object.content_type_id != ct.id:
business_object.content_type = ct
update_fields.append('content_type')
if business_object.object_id != instance_id:
business_object.object_id = instance_id
update_fields.append('object_id')
if default_name is not None and not (business_object.name or '').strip():
business_object.name = default_name
update_fields.append('name')
if not update_fields:
return False
business_object.save(update_fields=update_fields)
return True
def get_last_completed_state(business_object: 'models.BusinessObject') -> Optional['models.State']:
"""
@@ -127,6 +174,17 @@ def advance_to_next_state(business_object: 'models.BusinessObject', user, **para
- message: 提示信息
- state_log: 创建的状态日志(成功时)
"""
# 业务约束BusinessObject 必须能追溯到真实业务对象,否则状态流转记录没有业务意义。
# DB 允许为空仅为历史兼容;但新增数据一律禁止。
if business_object.content_type_id is None or business_object.object_id is None:
return False, "业务对象未绑定关联对象content_type/object_id禁止推进", None
if business_object.content_object is None:
return (
False,
f"业务对象绑定的关联对象不存在:{business_object.content_type.app_label}.{business_object.content_type.model} #{business_object.object_id}",
None,
)
can_advance, reason = can_advance_to_next_state(business_object)
if not can_advance:
return False, reason, None
@@ -598,6 +656,12 @@ def clone_business_object(
if source is None:
raise ValueError("source 不能为空")
# 新增约束:禁止克隆“未绑定”的 BusinessObject会产生新的不可追溯流程实例
if source.content_type_id is None or source.object_id is None:
raise ValueError("源对象未绑定关联对象content_type/object_id禁止克隆")
if expected_content_type_id is None:
raise ValueError("必须提供 expected_content_type_id")
# 重新加载 source确保拿到完整关系避免调用方未预取导致 N+1
source = (
models.BusinessObject.objects
@@ -610,23 +674,23 @@ def clone_business_object(
.get(id=source.id)
)
# 校验 content_type 一致性(不做额外校验,仅比对 id
# 校验 content_type 一致性(仅比对 id不做额外校验)
if source.content_type_id != expected_content_type_id:
raise ValueError("content_type 与源对象不一致,拒绝克隆")
# 如果源对象未绑定 content_type则拒绝克隆
if source.content_type_id is None:
return ValueError("源对象未绑定 content_type拒绝克隆")
else:
if new_object_id is None:
raise ValueError("必须提供新的 object_id")
if source.object_id == new_object_id:
raise ValueError("object_id 必须与源对象不同")
content_type = models.ContentType.objects.get(id=source.content_type_id)
actual_object = content_type.model_class().objects.get(id=new_object_id)
if actual_object is None:
raise ValueError("实际对象不存在,拒绝克隆")
# 绑定规则(强制):
# - 必须提供新的 object_id且与源对象不同
if new_object_id is None:
raise ValueError("必须提供新的 object_id")
if source.object_id == new_object_id:
raise ValueError("object_id 必须与源对象不同")
# 目标对象必须存在(避免产生悬空绑定)
try:
source.content_type.get_object_for_this_type(pk=new_object_id)
except ObjectDoesNotExist:
raise ValueError(
f"目标关联对象不存在:{source.content_type.app_label}.{source.content_type.model} #{new_object_id}"
)
with transaction.atomic():
cloned = models.BusinessObject.objects.create(
@@ -685,8 +749,4 @@ def clone_business_object(
# 重新加载,确保返回对象的字段与 DB 一致(尤其是时间戳)
cloned.refresh_from_db()
actual_object.refresh_from_db()
actual_object.business_object_id = cloned.id
actual_object.save()
return cloned