forked from erp-dev/erp
feat: new module: 'shipment', new signal when process was finished
This commit is contained in:
@@ -126,6 +126,7 @@ INSTALLED_APPS = [
|
|||||||
'stateflow',
|
'stateflow',
|
||||||
'sse',
|
'sse',
|
||||||
'api_v2',
|
'api_v2',
|
||||||
|
'shipment',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -5,3 +5,15 @@ class PrintingConfig(AppConfig):
|
|||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'printing'
|
name = 'printing'
|
||||||
verbose_name = '印染管理'
|
verbose_name = '印染管理'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
"""注册信号处理函数"""
|
||||||
|
from stateflow.signals import process_completed
|
||||||
|
from .models import PrintingJob
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
# 监听 PrintingJob 流程完成信号
|
||||||
|
process_completed.connect(
|
||||||
|
handlers.on_printing_job_process_completed,
|
||||||
|
sender=PrintingJob
|
||||||
|
)
|
||||||
83
printing/handlers.py
Normal file
83
printing/handlers.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
Printing 模块信号处理函数
|
||||||
|
|
||||||
|
监听 stateflow 信号,在流程完成时执行业务逻辑。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def on_printing_job_process_completed(sender, **kwargs):
|
||||||
|
"""
|
||||||
|
PrintingJob 流程完成时的处理
|
||||||
|
|
||||||
|
当 PrintingJob 的工艺流程全部完成时,自动创建 SalesItem(销售品)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender: PrintingJob 类
|
||||||
|
content_object: PrintingJob 实例
|
||||||
|
last_completed_by: 最后步骤操作人
|
||||||
|
其他参数见 stateflow.signals.process_completed
|
||||||
|
"""
|
||||||
|
from shipment.models import SalesItem, UnitChoices
|
||||||
|
|
||||||
|
content_object = kwargs.get('content_object')
|
||||||
|
last_completed_by = kwargs.get('last_completed_by')
|
||||||
|
|
||||||
|
if content_object is None:
|
||||||
|
logger.warning('process_completed 信号缺少 content_object')
|
||||||
|
return
|
||||||
|
|
||||||
|
printing_job = content_object
|
||||||
|
|
||||||
|
# 单位映射:PrintingJob.unit(字符串)→ SalesItem.unit(IntegerChoices)
|
||||||
|
unit_mapping = {
|
||||||
|
'米': UnitChoices.METER,
|
||||||
|
'm': UnitChoices.METER,
|
||||||
|
'M': UnitChoices.METER,
|
||||||
|
'件': UnitChoices.PIECE,
|
||||||
|
'码': UnitChoices.YARD,
|
||||||
|
'yd': UnitChoices.YARD,
|
||||||
|
'YD': UnitChoices.YARD,
|
||||||
|
'个': UnitChoices.UNIT,
|
||||||
|
'pcs': UnitChoices.UNIT,
|
||||||
|
'PCS': UnitChoices.UNIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 默认单位为"件"
|
||||||
|
sales_unit = unit_mapping.get(printing_job.unit, UnitChoices.PIECE)
|
||||||
|
|
||||||
|
# 获取产品名称
|
||||||
|
product_name = printing_job.product.name if printing_job.product else '未知产品'
|
||||||
|
|
||||||
|
# 获取客户ID(从主订单获取)
|
||||||
|
customer_id = None
|
||||||
|
if printing_job.printing_order and printing_job.printing_order.customer:
|
||||||
|
customer_id = printing_job.printing_order.customer_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
sales_item = SalesItem.objects.create(
|
||||||
|
shipment=None, # 暂不关联出货单,待后续分配
|
||||||
|
name=product_name,
|
||||||
|
quantity=printing_job.quantity,
|
||||||
|
unit=sales_unit,
|
||||||
|
created_by=last_completed_by,
|
||||||
|
printing_job_id=printing_job.id,
|
||||||
|
customer_id=customer_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f'PrintingJob #{printing_job.id} 流程完成,'
|
||||||
|
f'已创建销售品 SalesItem #{sales_item.id}: '
|
||||||
|
f'{product_name} x {printing_job.quantity}'
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f'PrintingJob #{printing_job.id} 流程完成后创建销售品失败: {e}',
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
1
shipment/__init__.py
Normal file
1
shipment/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Shipment module
|
||||||
49
shipment/admin.py
Normal file
49
shipment/admin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Shipment, SalesItem
|
||||||
|
|
||||||
|
|
||||||
|
class SalesItemInline(admin.TabularInline):
|
||||||
|
model = SalesItem
|
||||||
|
extra = 1
|
||||||
|
fields = ['name', 'quantity', 'unit', 'printing_job_id', 'customer_id', 'created_by']
|
||||||
|
readonly_fields = ['created_by']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Shipment)
|
||||||
|
class ShipmentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'customer', 'shipment_date', 'items_count', 'created_by', 'created_at']
|
||||||
|
list_filter = ['shipment_date', 'created_at']
|
||||||
|
search_fields = ['customer__name', 'remark']
|
||||||
|
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||||
|
date_hierarchy = 'shipment_date'
|
||||||
|
inlines = [SalesItemInline]
|
||||||
|
|
||||||
|
def items_count(self, obj):
|
||||||
|
return obj.items.count()
|
||||||
|
items_count.short_description = '销售品数量'
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change:
|
||||||
|
obj.created_by = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def save_formset(self, request, form, formset, change):
|
||||||
|
instances = formset.save(commit=False)
|
||||||
|
for instance in instances:
|
||||||
|
if isinstance(instance, SalesItem) and not instance.pk:
|
||||||
|
instance.created_by = request.user
|
||||||
|
instance.save()
|
||||||
|
formset.save_m2m()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SalesItem)
|
||||||
|
class SalesItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'name', 'quantity', 'unit', 'shipment', 'printing_job_id', 'customer_id', 'created_by', 'created_at']
|
||||||
|
list_filter = ['unit', 'created_at']
|
||||||
|
search_fields = ['name', 'shipment__customer__name']
|
||||||
|
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change:
|
||||||
|
obj.created_by = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
7
shipment/apps.py
Normal file
7
shipment/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ShipmentConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'shipment'
|
||||||
|
verbose_name = '出货管理'
|
||||||
57
shipment/migrations/0001_initial.py
Normal file
57
shipment/migrations/0001_initial.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2026-01-13 09:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0024_frontend_page_and_visible_pages'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Shipment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('shipment_date', models.DateField(help_text='实际出货日期', verbose_name='出货日期')),
|
||||||
|
('remark', models.TextField(blank=True, default='', verbose_name='备注')),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_shipments', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shipments', to='basic_info.customer', verbose_name='客户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '出货单',
|
||||||
|
'verbose_name_plural': '出货单',
|
||||||
|
'db_table': 'shipment',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SalesItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||||
|
('quantity', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='数量')),
|
||||||
|
('unit', models.IntegerField(choices=[(1, '米'), (2, '件'), (3, '码'), (4, '个')], default=2, verbose_name='单位')),
|
||||||
|
('printing_job_id', models.PositiveIntegerField(blank=True, help_text='关联的 PrintingJob ID(可空)', null=True, verbose_name='关联生产任务ID')),
|
||||||
|
('customer_id', models.PositiveIntegerField(blank=True, help_text='销售品级别的客户ID(可空)', null=True, verbose_name='客户ID')),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_items', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('shipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shipment.shipment', verbose_name='出货单')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '销售品',
|
||||||
|
'verbose_name_plural': '销售品',
|
||||||
|
'db_table': 'sales_item',
|
||||||
|
'ordering': ['id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
19
shipment/migrations/0002_salesitem_shipment_nullable.py
Normal file
19
shipment/migrations/0002_salesitem_shipment_nullable.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2026-01-13 10:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shipment', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesitem',
|
||||||
|
name='shipment',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='为空表示待分配', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shipment.shipment', verbose_name='出货单'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
shipment/migrations/__init__.py
Normal file
0
shipment/migrations/__init__.py
Normal file
154
shipment/models.py
Normal file
154
shipment/models.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from flower.common import ModelBase
|
||||||
|
from basic_info import models as basic_models
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UnitChoices(models.IntegerChoices):
|
||||||
|
"""销售品单位"""
|
||||||
|
METER = 1, '米'
|
||||||
|
PIECE = 2, '件'
|
||||||
|
YARD = 3, '码'
|
||||||
|
UNIT = 4, '个'
|
||||||
|
|
||||||
|
|
||||||
|
class Shipment(ModelBase):
|
||||||
|
"""
|
||||||
|
出货单
|
||||||
|
|
||||||
|
记录一次出货操作,包含多个销售品。
|
||||||
|
"""
|
||||||
|
customer = models.ForeignKey(
|
||||||
|
basic_models.Customer,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='shipments',
|
||||||
|
verbose_name='客户'
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_date = models.DateField(
|
||||||
|
verbose_name='出货日期',
|
||||||
|
help_text='实际出货日期'
|
||||||
|
)
|
||||||
|
|
||||||
|
remark = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
verbose_name='备注'
|
||||||
|
)
|
||||||
|
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name='created_shipments',
|
||||||
|
verbose_name='创建人'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'shipment'
|
||||||
|
verbose_name = '出货单'
|
||||||
|
verbose_name_plural = '出货单'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'出货单 #{self.id} - {self.customer.name}'
|
||||||
|
|
||||||
|
|
||||||
|
class SalesItem(ModelBase):
|
||||||
|
"""
|
||||||
|
销售品
|
||||||
|
|
||||||
|
出货单中的具体商品明细。
|
||||||
|
销售品可以先创建(待分配状态),后续再关联到出货单。
|
||||||
|
"""
|
||||||
|
shipment = models.ForeignKey(
|
||||||
|
Shipment,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='items',
|
||||||
|
verbose_name='出货单',
|
||||||
|
help_text='为空表示待分配'
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name='名称'
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = models.DecimalField(
|
||||||
|
max_digits=12,
|
||||||
|
decimal_places=2,
|
||||||
|
verbose_name='数量'
|
||||||
|
)
|
||||||
|
|
||||||
|
unit = models.IntegerField(
|
||||||
|
choices=UnitChoices.choices,
|
||||||
|
default=UnitChoices.PIECE,
|
||||||
|
verbose_name='单位'
|
||||||
|
)
|
||||||
|
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name='created_sales_items',
|
||||||
|
verbose_name='创建人'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关联的生产明细订单(可空)
|
||||||
|
# 使用 PositiveIntegerField 而非 ForeignKey,避免模块间强依赖
|
||||||
|
printing_job_id = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='关联生产任务ID',
|
||||||
|
help_text='关联的 PrintingJob ID(可空)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 客户ID(可空,用于销售品级别的客户关联)
|
||||||
|
customer_id = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='客户ID',
|
||||||
|
help_text='销售品级别的客户ID(可空)'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'sales_item'
|
||||||
|
verbose_name = '销售品'
|
||||||
|
verbose_name_plural = '销售品'
|
||||||
|
ordering = ['id']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name} x {self.quantity} {self.get_unit_display()}'
|
||||||
|
|
||||||
|
def get_printing_job(self):
|
||||||
|
"""
|
||||||
|
获取关联的 PrintingJob 实例
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PrintingJob 实例,如果不存在则返回 None
|
||||||
|
"""
|
||||||
|
if not self.printing_job_id:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from printing.models import PrintingJob
|
||||||
|
return PrintingJob.objects.get(id=self.printing_job_id)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_customer(self):
|
||||||
|
"""
|
||||||
|
获取关联的 Customer 实例
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Customer 实例,如果不存在则返回 None
|
||||||
|
"""
|
||||||
|
if not self.customer_id:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return basic_models.Customer.objects.get(id=self.customer_id)
|
||||||
|
except basic_models.Customer.DoesNotExist:
|
||||||
|
return None
|
||||||
@@ -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)
|
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
|
||||||
|
|
||||||
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*)
|
- created_at/updated_at 等 ModelBase 字段也会被同步(使用 update 绕过 auto_now*)
|
||||||
- **重要**:克隆必须提供新的 object_id(当 source.content_type 非空时),否则克隆与业务绑定无差异,容易造成误用
|
- **重要**:克隆必须提供新的 object_id(当 source.content_type 非空时),否则克隆与业务绑定无差异,容易造成误用
|
||||||
- expected_content_type_id 仅用于校验调用方意图:必须与 source.content_type_id 一致,否则拒绝克隆
|
- expected_content_type_id 仅用于校验调用方意图:必须与 source.content_type_id 一致,否则拒绝克隆
|
||||||
|
|
||||||
|
2026-01-13 暂停使用:为避免误用导致流程副本错误,暂时禁用此能力。
|
||||||
"""
|
"""
|
||||||
if source is None:
|
raise NotImplementedError("clone_business_object is disabled temporarily (2026-01-13)")
|
||||||
raise ValueError("source 不能为空")
|
|
||||||
|
|
||||||
# 新增约束:禁止克隆“未绑定”的 BusinessObject(会产生新的不可追溯流程实例)
|
# 原始实现(保留供后续恢复参考):
|
||||||
if source.content_type_id is None or source.object_id is None:
|
# if source is None:
|
||||||
raise ValueError("源对象未绑定关联对象(content_type/object_id),禁止克隆")
|
# raise ValueError("source 不能为空")
|
||||||
if expected_content_type_id is None:
|
# if source.content_type_id is None or source.object_id is None:
|
||||||
raise ValueError("必须提供 expected_content_type_id")
|
# raise ValueError("源对象未绑定关联对象(content_type/object_id),禁止克隆")
|
||||||
|
# if expected_content_type_id is None:
|
||||||
# 重新加载 source,确保拿到完整关系(避免调用方未预取导致 N+1)
|
# raise ValueError("必须提供 expected_content_type_id")
|
||||||
source = (
|
# source = (
|
||||||
models.BusinessObject.objects
|
# models.BusinessObject.objects
|
||||||
.select_related('process', 'content_type')
|
# .select_related('process', 'content_type')
|
||||||
.prefetch_related(
|
# .prefetch_related(
|
||||||
'state_logs__state',
|
# 'state_logs__state',
|
||||||
'state_logs__completed_by',
|
# 'state_logs__completed_by',
|
||||||
'state_logs__parameter_records',
|
# 'state_logs__parameter_records',
|
||||||
)
|
# )
|
||||||
.get(id=source.id)
|
# .get(id=source.id)
|
||||||
)
|
# )
|
||||||
|
# if source.content_type_id != expected_content_type_id:
|
||||||
# 校验 content_type 一致性(仅比对 id,不做额外校验)
|
# raise ValueError("content_type 与源对象不一致,拒绝克隆")
|
||||||
if source.content_type_id != expected_content_type_id:
|
# if new_object_id is None:
|
||||||
raise ValueError("content_type 与源对象不一致,拒绝克隆")
|
# raise ValueError("必须提供新的 object_id")
|
||||||
|
# if source.object_id == new_object_id:
|
||||||
# 绑定规则(强制):
|
# raise ValueError("object_id 必须与源对象不同")
|
||||||
# - 必须提供新的 object_id,且与源对象不同
|
# try:
|
||||||
if new_object_id is None:
|
# source.content_type.get_object_for_this_type(pk=new_object_id)
|
||||||
raise ValueError("必须提供新的 object_id")
|
# except ObjectDoesNotExist:
|
||||||
if source.object_id == new_object_id:
|
# raise ValueError(
|
||||||
raise ValueError("object_id 必须与源对象不同")
|
# f"目标关联对象不存在:{source.content_type.app_label}.{source.content_type.model} #{new_object_id}"
|
||||||
# 目标对象必须存在(避免产生悬空绑定)
|
# )
|
||||||
try:
|
# with transaction.atomic():
|
||||||
source.content_type.get_object_for_this_type(pk=new_object_id)
|
# cloned = models.BusinessObject.objects.create(
|
||||||
except ObjectDoesNotExist:
|
# name=source.name,
|
||||||
raise ValueError(
|
# process=source.process,
|
||||||
f"目标关联对象不存在:{source.content_type.app_label}.{source.content_type.model} #{new_object_id}"
|
# description=source.description,
|
||||||
)
|
# content_type=source.content_type,
|
||||||
|
# object_id=new_object_id,
|
||||||
with transaction.atomic():
|
# )
|
||||||
cloned = models.BusinessObject.objects.create(
|
# models.BusinessObject.objects.filter(id=cloned.id).update(
|
||||||
name=source.name,
|
# created_at=source.created_at,
|
||||||
process=source.process,
|
# updated_at=source.updated_at,
|
||||||
description=source.description,
|
# )
|
||||||
content_type=source.content_type,
|
# source_logs = sorted(
|
||||||
object_id=new_object_id,
|
# list(source.state_logs.all()),
|
||||||
)
|
# key=lambda log: (log.completed_at, log.id),
|
||||||
|
# )
|
||||||
# 同步 BusinessObject 的时间戳字段(保持一致性)
|
# for src_log in source_logs:
|
||||||
models.BusinessObject.objects.filter(id=cloned.id).update(
|
# new_log = models.StateFlowRecord.objects.create(
|
||||||
created_at=source.created_at,
|
# business_object=cloned,
|
||||||
updated_at=source.updated_at,
|
# state=src_log.state,
|
||||||
)
|
# completed_by=src_log.completed_by,
|
||||||
|
# is_cancelled=src_log.is_cancelled,
|
||||||
# 复制 state_logs(按完成时间排序,保证克隆后的序列稳定)
|
# cancelled_at=src_log.cancelled_at,
|
||||||
source_logs = sorted(
|
# )
|
||||||
list(source.state_logs.all()),
|
# models.StateFlowRecord.objects.filter(id=new_log.id).update(
|
||||||
key=lambda log: (log.completed_at, log.id),
|
# completed_at=src_log.completed_at,
|
||||||
)
|
# created_at=src_log.created_at,
|
||||||
|
# updated_at=src_log.updated_at,
|
||||||
for src_log in source_logs:
|
# cancelled_at=src_log.cancelled_at,
|
||||||
new_log = models.StateFlowRecord.objects.create(
|
# )
|
||||||
business_object=cloned,
|
# src_param_records = sorted(
|
||||||
state=src_log.state,
|
# list(src_log.parameter_records.all()),
|
||||||
completed_by=src_log.completed_by,
|
# key=lambda rec: (rec.created_at, rec.id),
|
||||||
is_cancelled=src_log.is_cancelled,
|
# )
|
||||||
cancelled_at=src_log.cancelled_at,
|
# for src_rec in src_param_records:
|
||||||
)
|
# new_rec = models.StateLogParameterRecord.objects.create(
|
||||||
|
# state_log=new_log,
|
||||||
# 同步 StateFlowRecord 的时间戳字段(含 completed_at)
|
# parameters=copy.deepcopy(src_rec.parameters),
|
||||||
models.StateFlowRecord.objects.filter(id=new_log.id).update(
|
# remark=src_rec.remark,
|
||||||
completed_at=src_log.completed_at,
|
# )
|
||||||
created_at=src_log.created_at,
|
# models.StateLogParameterRecord.objects.filter(id=new_rec.id).update(
|
||||||
updated_at=src_log.updated_at,
|
# created_at=src_rec.created_at,
|
||||||
cancelled_at=src_log.cancelled_at,
|
# updated_at=src_rec.updated_at,
|
||||||
)
|
# )
|
||||||
|
# cloned.refresh_from_db()
|
||||||
# 复制参数记录(按 created_at 排序)
|
# return cloned
|
||||||
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
|
|
||||||
|
|||||||
55
stateflow/signals.py
Normal file
55
stateflow/signals.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Stateflow 信号定义
|
||||||
|
|
||||||
|
stateflow 模块通过信号通知外部模块状态变化,
|
||||||
|
外部模块可以监听这些信号来执行业务逻辑(通知、簿记等)。
|
||||||
|
|
||||||
|
sender 说明:
|
||||||
|
sender 是 content_object 的类(如 PrintingJob、PlateOrder),
|
||||||
|
监听者可以在 connect 时指定 sender 来过滤只接收特定类型的信号。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
# 在业务模块的 apps.py 中监听
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class MyAppConfig(AppConfig):
|
||||||
|
def ready(self):
|
||||||
|
from stateflow.signals import process_completed
|
||||||
|
from .models import PrintingJob
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
# 只监听 PrintingJob 相关的流程完成信号
|
||||||
|
process_completed.connect(handlers.on_process_completed, sender=PrintingJob)
|
||||||
|
|
||||||
|
# 或监听所有类型(不指定 sender)
|
||||||
|
# process_completed.connect(handlers.on_any_process_completed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.dispatch import Signal
|
||||||
|
|
||||||
|
# 流程完成信号
|
||||||
|
# 参数:
|
||||||
|
# sender: content_object 的类(如 PrintingJob),用于过滤
|
||||||
|
# process_id: 流程 ID
|
||||||
|
# business_object_id: BusinessObject ID
|
||||||
|
# business_object: BusinessObject 实例
|
||||||
|
# content_object: 关联的业务对象实例(如 PrintingJob)
|
||||||
|
# completed_by: 完成操作的用户(本次推进的操作人)
|
||||||
|
# last_completed_state_id: 最后完成的节点 ID
|
||||||
|
# last_completed_by: 最后步骤操作人(User 实例)
|
||||||
|
process_completed = Signal()
|
||||||
|
|
||||||
|
# 状态推进信号(每次推进都会触发)
|
||||||
|
# 参数:
|
||||||
|
# sender: content_object 的类(如 PrintingJob),用于过滤
|
||||||
|
# process_id: 流程 ID
|
||||||
|
# business_object_id: BusinessObject ID
|
||||||
|
# business_object: BusinessObject 实例
|
||||||
|
# content_object: 关联的业务对象实例
|
||||||
|
# state: 刚完成的 State 实例
|
||||||
|
# state_log: StateFlowRecord 实例
|
||||||
|
# completed_by: 完成操作的用户
|
||||||
|
# is_process_completed: 流程是否已全部完成
|
||||||
|
# last_completed_state_id: 最后完成的节点 ID
|
||||||
|
# last_completed_by: 最后步骤操作人(User 实例)
|
||||||
|
state_advanced = Signal()
|
||||||
Reference in New Issue
Block a user