forked from erp-dev/erp
feat: new module: 'shipment', new signal when process was finished
This commit is contained in:
@@ -257,8 +257,40 @@ def advance_to_next_state(business_object: 'models.BusinessObject', user, **para
|
||||
|
||||
# 检查是否所有状态都已完成
|
||||
after_advance = get_next_pending_state(business_object, include_parameters=False)
|
||||
if after_advance is None:
|
||||
# 所有状态都已完成
|
||||
is_process_completed = (after_advance is None)
|
||||
|
||||
# 发送状态推进信号(在事务内,确保数据已持久化)
|
||||
# sender 使用 content_object 的类,以便监听者按类型过滤
|
||||
from . import signals
|
||||
content_object = business_object.content_object
|
||||
sender_class = content_object.__class__ if content_object else None
|
||||
|
||||
signals.state_advanced.send(
|
||||
sender=sender_class,
|
||||
process_id=business_object.process_id,
|
||||
business_object_id=business_object.id,
|
||||
business_object=business_object,
|
||||
content_object=content_object,
|
||||
state=next_state,
|
||||
state_log=state_log,
|
||||
completed_by=user,
|
||||
is_process_completed=is_process_completed,
|
||||
last_completed_state_id=next_state.id,
|
||||
last_completed_by=user,
|
||||
)
|
||||
|
||||
if is_process_completed:
|
||||
# 流程全部完成,发送流程完成信号
|
||||
signals.process_completed.send(
|
||||
sender=sender_class,
|
||||
process_id=business_object.process_id,
|
||||
business_object_id=business_object.id,
|
||||
business_object=business_object,
|
||||
content_object=content_object,
|
||||
completed_by=user,
|
||||
last_completed_state_id=next_state.id,
|
||||
last_completed_by=user,
|
||||
)
|
||||
return True, f"流程已完成,最后状态: {next_state.name}", state_log
|
||||
|
||||
return True, f"已完成状态: {next_state.name}", state_log
|
||||
@@ -739,101 +771,83 @@ def clone_business_object(
|
||||
- created_at/updated_at 等 ModelBase 字段也会被同步(使用 update 绕过 auto_now*)
|
||||
- **重要**:克隆必须提供新的 object_id(当 source.content_type 非空时),否则克隆与业务绑定无差异,容易造成误用
|
||||
- expected_content_type_id 仅用于校验调用方意图:必须与 source.content_type_id 一致,否则拒绝克隆
|
||||
|
||||
2026-01-13 暂停使用:为避免误用导致流程副本错误,暂时禁用此能力。
|
||||
"""
|
||||
if source is None:
|
||||
raise ValueError("source 不能为空")
|
||||
raise NotImplementedError("clone_business_object is disabled temporarily (2026-01-13)")
|
||||
|
||||
# 新增约束:禁止克隆“未绑定”的 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
|
||||
.select_related('process', 'content_type')
|
||||
.prefetch_related(
|
||||
'state_logs__state',
|
||||
'state_logs__completed_by',
|
||||
'state_logs__parameter_records',
|
||||
)
|
||||
.get(id=source.id)
|
||||
)
|
||||
|
||||
# 校验 content_type 一致性(仅比对 id,不做额外校验)
|
||||
if source.content_type_id != expected_content_type_id:
|
||||
raise ValueError("content_type 与源对象不一致,拒绝克隆")
|
||||
|
||||
# 绑定规则(强制):
|
||||
# - 必须提供新的 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(
|
||||
name=source.name,
|
||||
process=source.process,
|
||||
description=source.description,
|
||||
content_type=source.content_type,
|
||||
object_id=new_object_id,
|
||||
)
|
||||
|
||||
# 同步 BusinessObject 的时间戳字段(保持一致性)
|
||||
models.BusinessObject.objects.filter(id=cloned.id).update(
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
)
|
||||
|
||||
# 复制 state_logs(按完成时间排序,保证克隆后的序列稳定)
|
||||
source_logs = sorted(
|
||||
list(source.state_logs.all()),
|
||||
key=lambda log: (log.completed_at, log.id),
|
||||
)
|
||||
|
||||
for src_log in source_logs:
|
||||
new_log = models.StateFlowRecord.objects.create(
|
||||
business_object=cloned,
|
||||
state=src_log.state,
|
||||
completed_by=src_log.completed_by,
|
||||
is_cancelled=src_log.is_cancelled,
|
||||
cancelled_at=src_log.cancelled_at,
|
||||
)
|
||||
|
||||
# 同步 StateFlowRecord 的时间戳字段(含 completed_at)
|
||||
models.StateFlowRecord.objects.filter(id=new_log.id).update(
|
||||
completed_at=src_log.completed_at,
|
||||
created_at=src_log.created_at,
|
||||
updated_at=src_log.updated_at,
|
||||
cancelled_at=src_log.cancelled_at,
|
||||
)
|
||||
|
||||
# 复制参数记录(按 created_at 排序)
|
||||
src_param_records = sorted(
|
||||
list(src_log.parameter_records.all()),
|
||||
key=lambda rec: (rec.created_at, rec.id),
|
||||
)
|
||||
|
||||
for src_rec in src_param_records:
|
||||
new_rec = models.StateLogParameterRecord.objects.create(
|
||||
state_log=new_log,
|
||||
parameters=copy.deepcopy(src_rec.parameters),
|
||||
remark=src_rec.remark,
|
||||
)
|
||||
models.StateLogParameterRecord.objects.filter(id=new_rec.id).update(
|
||||
created_at=src_rec.created_at,
|
||||
updated_at=src_rec.updated_at,
|
||||
)
|
||||
|
||||
# 重新加载,确保返回对象的字段与 DB 一致(尤其是时间戳)
|
||||
cloned.refresh_from_db()
|
||||
return cloned
|
||||
# 原始实现(保留供后续恢复参考):
|
||||
# if source is None:
|
||||
# raise ValueError("source 不能为空")
|
||||
# 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 = (
|
||||
# models.BusinessObject.objects
|
||||
# .select_related('process', 'content_type')
|
||||
# .prefetch_related(
|
||||
# 'state_logs__state',
|
||||
# 'state_logs__completed_by',
|
||||
# 'state_logs__parameter_records',
|
||||
# )
|
||||
# .get(id=source.id)
|
||||
# )
|
||||
# if source.content_type_id != expected_content_type_id:
|
||||
# raise ValueError("content_type 与源对象不一致,拒绝克隆")
|
||||
# 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(
|
||||
# name=source.name,
|
||||
# process=source.process,
|
||||
# description=source.description,
|
||||
# content_type=source.content_type,
|
||||
# object_id=new_object_id,
|
||||
# )
|
||||
# models.BusinessObject.objects.filter(id=cloned.id).update(
|
||||
# created_at=source.created_at,
|
||||
# updated_at=source.updated_at,
|
||||
# )
|
||||
# source_logs = sorted(
|
||||
# list(source.state_logs.all()),
|
||||
# key=lambda log: (log.completed_at, log.id),
|
||||
# )
|
||||
# for src_log in source_logs:
|
||||
# new_log = models.StateFlowRecord.objects.create(
|
||||
# business_object=cloned,
|
||||
# state=src_log.state,
|
||||
# completed_by=src_log.completed_by,
|
||||
# is_cancelled=src_log.is_cancelled,
|
||||
# cancelled_at=src_log.cancelled_at,
|
||||
# )
|
||||
# models.StateFlowRecord.objects.filter(id=new_log.id).update(
|
||||
# completed_at=src_log.completed_at,
|
||||
# created_at=src_log.created_at,
|
||||
# updated_at=src_log.updated_at,
|
||||
# cancelled_at=src_log.cancelled_at,
|
||||
# )
|
||||
# src_param_records = sorted(
|
||||
# list(src_log.parameter_records.all()),
|
||||
# key=lambda rec: (rec.created_at, rec.id),
|
||||
# )
|
||||
# for src_rec in src_param_records:
|
||||
# new_rec = models.StateLogParameterRecord.objects.create(
|
||||
# state_log=new_log,
|
||||
# parameters=copy.deepcopy(src_rec.parameters),
|
||||
# remark=src_rec.remark,
|
||||
# )
|
||||
# models.StateLogParameterRecord.objects.filter(id=new_rec.id).update(
|
||||
# created_at=src_rec.created_at,
|
||||
# updated_at=src_rec.updated_at,
|
||||
# )
|
||||
# cloned.refresh_from_db()
|
||||
# return cloned
|
||||
|
||||
Reference in New Issue
Block a user