1
0
forked from erp-dev/erp
Files
erpnew/stateflow/services.py

786 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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']:
"""
获取最后完成的状态
返回:最后一个已完成的状态节点,如果没有则返回 None
应用场景:
- 显示"刚完成了什么"
- 查询最后完成状态的参数
- 审计日志
"""
last_log = business_object.state_logs.filter(
is_cancelled=False
).order_by('-completed_at', '-id').first()
return last_log.state if last_log else None
def get_next_pending_state_simple(business_object: 'models.BusinessObject') -> Optional['models.State']:
"""
获取下一个待执行的状态简化版只返回State对象
返回:下一个待执行的状态节点,如果没有则返回 None
应用场景:
- 显示"接下来要做什么"
- 获取下一步需要的参数
- 流程推进前的检查
"""
next_pending = get_next_pending_state(business_object, include_parameters=False)
return next_pending['state'] if next_pending else None
def get_business_object_current_state(business_object: 'models.BusinessObject') -> Optional['models.State']:
"""
获取业务对象的当前状态(可配置模式)
根据 settings.STATEFLOW_CURRENT_STATE_MODE 决定返回:
- 'NEXT' (默认): 下一个待执行的节点
- 'LAST': 最后完成的节点
注意:此函数保持原有接口不变,但行为可配置
推荐:在新代码中直接使用 get_last_completed_state() 或 get_next_pending_state_simple() 以明确语义
NEXT模式的规则
- 返回下一个待执行的节点(第一个未完成的节点)
- 如果所有节点都已完成,返回 None
- 配合进度可以判断具体状态:
- 进度 0% + current_state 非空 → 显示"未开始"
- 进度 > 0% + current_state 非空 → 显示 current_state.name正在进行
- current_state = None → 显示"已完成"
LAST模式的规则
- 返回最后一个已完成的节点
- 如果没有完成任何节点,返回 None
"""
from django.conf import settings
mode = getattr(settings, 'STATEFLOW_CURRENT_STATE_MODE', 'NEXT')
if mode == 'LAST':
return get_last_completed_state(business_object)
else: # 默认 'NEXT'
return get_next_pending_state_simple(business_object)
def get_business_object_state_status(business_object: 'models.BusinessObject', state: 'models.State') -> str:
"""
获取业务对象中某个状态的状态
返回值:
- 'not_started': 未开始
- 'completed': 已完成
注意:由于 current_state 现在表示"最后完成的状态",不再有 'in_progress' 概念
"""
# 检查是否已完成且未被撤销
is_completed = business_object.state_logs.filter(state=state, is_cancelled=False).exists()
if is_completed:
return 'completed'
return 'not_started'
def get_completed_node_ids(business_object: 'models.BusinessObject') -> List[int]:
"""获取订单已完成且未撤销的节点ID列表按完成顺序"""
return list(
business_object.state_logs.filter(is_cancelled=False)
.order_by('completed_at')
.values_list('state_id', flat=True)
)
def get_progress_percentage(business_object: 'models.BusinessObject') -> float:
"""计算订单进度百分比"""
total_nodes = business_object.process.process_nodes.count()
if total_nodes == 0:
return 0.0
completed_count = business_object.state_logs.filter(is_cancelled=False).count()
return (completed_count / total_nodes) * 100
def advance_to_next_state(business_object: 'models.BusinessObject', user, **parameters) -> Tuple[bool, str, Optional['models.StateFlowRecord']]:
"""
将业务对象推进到下一个状态(完成下一个未完成的节点)
参数:
business_object: BusinessObject 实例
user: 操作用户
**parameters: 状态参数(可选)
返回:
tuple: (success: bool, message: str, state_log: StateFlowRecord|None)
- success: 是否成功
- message: 提示信息
- state_log: 创建的状态日志(成功时)
"""
# 业务约束BusinessObject 必须能追溯到真实业务对象,否则状态流转记录没有业务意义。
# DB 允许为空仅为历史兼容;但实际运行中存在“先创建 BO、后绑定到业务模型”的非标准路径。
# 为避免这类数据导致推进失败,这里做一次自愈:
# - 若 BO 未绑定 content_type/object_id或 content_object 解析不到实例)
# - 则尝试通过常见的一对一反向关系(如 printing_job/plate_order推断真实业务对象并补齐绑定
if business_object.content_type_id is None or business_object.object_id is None or business_object.content_object is None:
inferred_instance = None
inferred_default_name = None
# PrintingJob.business_object -> related_name='printing_job'
try:
inferred_instance = getattr(business_object, 'printing_job', None)
if inferred_instance is not None:
inferred_default_name = f"PrintingJob-{getattr(inferred_instance, 'pk', '')}"
except (ObjectDoesNotExist, AttributeError):
inferred_instance = None
# PlateOrder.business_object -> related_name='plate_order'
if inferred_instance is None:
try:
inferred_instance = getattr(business_object, 'plate_order', None)
if inferred_instance is not None:
inferred_default_name = f"PlateOrder-{getattr(inferred_instance, 'pk', '')}"
except (ObjectDoesNotExist, AttributeError):
inferred_instance = None
if inferred_instance is not None:
ensure_business_object_bound_to_instance(
business_object,
inferred_instance,
default_name=inferred_default_name,
)
# GenericForeignKey 可能缓存了旧值,刷新以确保后续 content_object 判断正确
business_object.refresh_from_db(fields=['content_type', 'object_id', 'name'])
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
with transaction.atomic():
# 获取下一个待执行的节点
next_pending = get_next_pending_state(business_object, include_parameters=False)
if not next_pending:
return False, "没有待执行的节点", None
next_state = next_pending['state']
# 验证必填参数
if parameters:
is_valid, missing_params = validate_required_parameters(next_state, **parameters)
if not is_valid:
return False, f"缺失必填参数: {', '.join(missing_params)}", None
else:
# 检查是否有必填参数
required_params = get_required_parameters(next_state)
if required_params.exists():
required_keys = list(required_params.values_list('key', flat=True))
return False, f"缺失必填参数: {', '.join(required_keys)}", None
# 创建状态流转记录
state_log = models.StateFlowRecord.objects.create(
business_object=business_object,
state=next_state,
completed_by=user
)
# 如果提供了参数,创建参数记录
if parameters:
create_parameter_record(state_log, **parameters)
# 检查是否所有状态都已完成
after_advance = get_next_pending_state(business_object, include_parameters=False)
if after_advance is None:
# 所有状态都已完成
return True, f"流程已完成,最后状态: {next_state.name}", state_log
return True, f"已完成状态: {next_state.name}", state_log
def reset_business_object_progress(business_object: 'models.BusinessObject') -> None:
"""
重置业务对象进度
注意:不删除历史记录,而是标记所有记录为已撤销,并记录撤销时间
这样可以保留完整的操作历史,支持审计需求
"""
from django.utils import timezone
business_object.state_logs.filter(is_cancelled=False).update(
is_cancelled=True,
cancelled_at=timezone.now()
)
# 向后兼容的别名
reset_order_progress = reset_business_object_progress
def step_back_one_state(business_object: 'models.BusinessObject', user) -> Tuple[bool, str]:
"""
回退一步(撤销最后一次完成的状态)
这是 advance_to_next_state 的逆向操作
返回: (是否成功, 消息)
"""
from django.utils import timezone
with transaction.atomic():
# 获取最后一条有效的状态流转记录(按完成时间倒序)
last_record = (
business_object.state_logs
.filter(is_cancelled=False)
.order_by('-completed_at', '-id')
.first()
)
# 如果没有任何有效记录,说明当前没有任何状态流转,无需回退
if not last_record:
return False, "当前没有任何状态流转记录,无法回退"
# 标记最后一条记录为已撤销
last_record.is_cancelled = True
last_record.cancelled_at = timezone.now()
last_record.save(update_fields=['is_cancelled', 'cancelled_at'])
return True, f"已回退状态: {last_record.state.name}"
def get_current_state_parameters(business_object: 'models.BusinessObject') -> List['models.StateParameter']:
"""获取订单当前状态的参数列表"""
current_state = get_business_object_current_state(business_object)
if current_state:
return list(current_state.parameters.all())
return []
def get_display_status(business_object: 'models.BusinessObject') -> str:
"""
获取用于显示的状态文本
规则:
- 如果没有 business_object返回第一个节点名称如果有流程否则返回空字符串
- 如果 current_state 为 None返回"已完成"
- 否则:返回 current_state 的名称
注意:此函数废除了"未开始"的概念,直接显示节点名称
返回值: 状态显示文本(字符串)
"""
if not business_object:
return ''
current_state = get_business_object_current_state(business_object)
if current_state is None:
# 没有待执行的节点,已完成
return '已完成'
# 直接返回当前状态名称,不管进度如何
return current_state.name
def get_overall_status(business_object: 'models.BusinessObject') -> str:
"""
获取业务对象的整体状态
返回值:
- 'in_progress': 进行中(有部分状态已完成,但未完成所有)
- 'completed': 已完成(所有状态都已完成)
注意:废除了 'not_started' 状态进度为0时也返回 'in_progress'
"""
# 检查是否有下一个待执行节点
next_pending = get_next_pending_state(business_object, include_parameters=False)
if next_pending is None:
# 没有待执行节点,说明所有状态都已完成
return 'completed'
return 'in_progress'
def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple[bool, str]:
"""
检查业务对象是否可以推进到下一个状态
返回: (是否可以推进, 原因)
"""
# 检查流程是否有节点
first_node = business_object.process.process_nodes.order_by('order', 'id').first()
if not first_node:
return False, "流程没有任何节点"
# 获取下一个待执行节点
next_pending = get_next_pending_state(business_object, include_parameters=False)
if next_pending is None:
# 没有待执行节点,流程已完成
return False, "流程已完成,无法继续推进"
next_state = next_pending['state']
return True, f"可以推进到: {next_state.name}"
def get_business_object_state_timeline(business_object: 'models.BusinessObject') -> List[dict]:
"""
获取业务对象状态时间线(包括未开始和已完成的状态)
返回格式:
[
{
'state': State对象,
'status': 'not_started' | 'completed' | 'cancelled',
'order': 顺序号,
'completed_at': 完成时间(如果已完成),
'completed_by': 完成人(如果已完成),
'cancelled_at': 撤销时间(如果已撤销),
'is_cancelled': 是否已撤销,
},
...
]
状态判断规则:
- cancelled: 有日志记录且已撤销
- completed: 有日志记录且未撤销
- not_started: 没有日志记录(还未完成)
注意:移除了 'in_progress' 状态,因为在当前设计中:
- current_state 表示"下一个待执行的节点",它还未开始执行,所以是 not_started
- 一个节点要么已完成,要么还未开始,没有"进行中"的中间状态
"""
timeline = []
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
# 构建状态日志映射(包括已撤销的记录)
state_logs_map = {
log.state_id: log
for log in business_object.state_logs.select_related('completed_by')
}
# 获取 current_state下一个待执行的节点
current_state = get_business_object_current_state(business_object)
current_state_id = current_state.id if current_state else None
# 检查是否有任何完成记录
has_any_completed = business_object.state_logs.filter(is_cancelled=False).exists()
for node in process_nodes:
state = node.state
log = state_logs_map.get(state.id)
if log:
if log.is_cancelled:
status = 'cancelled'
else:
status = 'completed'
completed_at = log.completed_at
completed_by = log.completed_by
cancelled_at = log.cancelled_at
is_cancelled = log.is_cancelled
else:
# 未开始的节点
# current_state 是"下一个待执行的节点",它还未开始,所以状态是 not_started
status = 'not_started'
completed_at = None
completed_by = None
cancelled_at = None
is_cancelled = False
timeline.append({
'state': state,
'status': status,
'order': node.order,
'completed_at': completed_at,
'completed_by': completed_by,
'cancelled_at': cancelled_at,
'is_cancelled': is_cancelled,
})
return timeline
def get_next_pending_state(
business_object: 'models.BusinessObject',
include_parameters: bool = True
) -> Optional[dict]:
"""
获取指定业务对象的下一个待执行节点(第一个未完成的节点)
Args:
business_object: 业务对象
include_parameters: 是否包含参数列表,默认 True
Returns:
包含状态信息的字典,如果没有待执行节点则返回 None
{
'state': State对象,
'order': 节点顺序号,
'parameters': [StateParameter列表] (如果 include_parameters=True)
}
"""
# 获取所有节点(按顺序)
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
if not process_nodes.exists():
return None
# 获取已完成且未被撤销的状态ID集合
completed_state_ids = set(
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True)
)
# 找到第一个未完成的节点
for node in process_nodes:
if node.state_id not in completed_state_ids:
result = {
'state': node.state,
'order': node.order,
}
if include_parameters:
result['parameters'] = list(node.state.parameters.all())
return result
# 所有节点都已完成
return None
def get_all_pending_states(business_object: 'models.BusinessObject') -> List[dict]:
"""
获取指定业务对象所有待执行的节点列表(不包含参数)
Args:
business_object: 业务对象
Returns:
待执行节点列表,每个元素包含:
{
'state': State对象,
'order': 节点顺序号
}
"""
# 获取所有节点
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
# 获取已完成且未被撤销的状态ID集合
completed_state_ids = set(
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True)
)
# 找到所有未完成的节点
pending_nodes = []
for node in process_nodes:
if node.state_id not in completed_state_ids:
pending_nodes.append({
'state': node.state,
'order': node.order,
})
return pending_nodes
def get_state_parameters(
state: 'models.State',
required_only: bool = False
) -> List['models.StateParameter']:
"""
获取指定节点的所有参数列表
Args:
state: 状态节点
required_only: 是否仅包含必填参数,默认 False
Returns:
参数列表
"""
return state.get_parameters(required_only=required_only)
def get_required_parameters(state: 'models.State'):
"""
获取状态的所有必填参数
参数:
state: State 实例
返回:
QuerySet: 必填的 StateParameter 对象
"""
return state.parameters.filter(is_required=True)
def validate_required_parameters(state: 'models.State', **kwargs) -> Tuple[bool, List[str]]:
"""
验证必填参数是否全部提供
参数:
state: State 实例
**kwargs: 用户提供的参数字典
返回:
tuple: (is_valid: bool, missing_params: list)
- is_valid: 是否通过验证
- missing_params: 缺失的必填参数 key 列表
"""
required_params = get_required_parameters(state)
required_keys = list(required_params.values_list('key', flat=True))
provided_keys = set(kwargs.keys())
missing_keys = [key for key in required_keys if key not in provided_keys]
is_valid = len(missing_keys) == 0
return is_valid, missing_keys
def create_parameter_record(state_log: 'models.StateFlowRecord', remark: str = '', **parameters) -> 'models.StateLogParameterRecord':
"""
为状态流转记录创建参数记录
参数:
state_log: StateFlowRecord 实例
remark: 备注
**parameters: 参数字典(直接保存,不做验证)
返回:
StateLogParameterRecord: 创建的参数记录
"""
record = models.StateLogParameterRecord.objects.create(
state_log=state_log,
parameters=parameters, # 直接保存为 JSON
remark=remark
)
return record
def add_parameters_to_state_log(state_log: 'models.StateFlowRecord', remark: str = '', **parameters) -> 'models.StateLogParameterRecord':
"""
为已有的状态流转记录补充参数支持重复key
参数:
state_log: StateFlowRecord 实例
remark: 备注
**parameters: 参数字典
返回:
StateLogParameterRecord: 创建的参数记录
"""
return create_parameter_record(state_log, remark, **parameters)
def get_process_nodes(business_object: 'models.BusinessObject') -> List[dict]:
"""
获取业务对象所属流程的所有状态节点列表
参数:
business_object: BusinessObject 实例
返回:
节点列表,每个元素包含:
{
'id': ProcessNode ID,
'state': State对象,
'state_id': State ID,
'state_name': State 名称,
'order': 节点顺序号
}
"""
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
return [
{
'id': node.id,
'state': node.state,
'state_id': node.state_id,
'state_name': node.state.name,
'order': node.order,
}
for node in process_nodes
]
def clone_business_object(
source: 'models.BusinessObject',
*,
new_object_id: int,
expected_content_type_id: int,
) -> 'models.BusinessObject':
"""
克隆一个 BusinessObject含所有状态流转记录与工艺参数记录
目标:
- 不影响原对象
- 克隆出“业务数据完全一致”的新对象process/描述/关联对象/日志/参数等)
- 返回新的 BusinessObject 实例
说明:
- 会克隆 source.state_logs 的所有记录(含已撤销记录)
- 会克隆每条 StateFlowRecord 下的所有 StateLogParameterRecord含参数 JSON 与 remark
- 会尽量保持 completed_at / cancelled_at / 参数记录 created_at 与原对象一致
- created_at/updated_at 等 ModelBase 字段也会被同步(使用 update 绕过 auto_now*
- **重要**:克隆必须提供新的 object_id当 source.content_type 非空时),否则克隆与业务绑定无差异,容易造成误用
- expected_content_type_id 仅用于校验调用方意图:必须与 source.content_type_id 一致,否则拒绝克隆
"""
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
.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