from django.contrib import admin from django.contrib.admin import action from django.contrib.contenttypes.models import ContentType from django.forms import BaseInlineFormSet from django.utils.html import format_html from . import models @admin.register(models.PlateOrder) class PlateOrderAdmin(admin.ModelAdmin): list_display = ( 'id', 'design_code_display', 'customer_name', 'style_name', 'plate_type', 'development_status_display', 'urgency_display', 'progress_display', 'salesperson_name', 'required_completion_date', 'created_at', ) list_filter = ( 'urgency_level', 'plate_type', 'is_ordered', 'is_mark_frame', 'created_at', 'required_completion_date', ) search_fields = ( 'design_code', 'style_name', 'customer__name', 'salesperson__name', 'merchandiser__name', 'designer__name', ) readonly_fields = ('progress_display', 'status_display') actions = ['advance_to_next_state_action', 'step_back_one_state_action', 'reset_progress_action'] autocomplete_fields = ['customer', 'salesperson', 'merchandiser', 'designer'] fieldsets = [ ('基本信息', { 'fields': ['design_code', 'plate_type', 'plate_date', 'urgency_level', 'original_id'] }), ('客户信息', { 'fields': ['customer', 'area', 'default_address', 'salesperson', 'merchandiser', 'designer'] }), ('产品信息', { 'fields': ['style_name', 'fabric', 'fabric_source', 'width', 'production_method'] }), ('开版信息', { 'fields': [ 'plate_method', 'plate_image', 'image_name', 'plate_notes', 'reprint_reason', 'is_mark_frame', 'sample_meter', 'required_sample_meters' ] }), ('质量评级', { 'fields': [ 'drawing_rating', 'color_matching_rating', 'sample_rating', 'difficulty_rating' ] }), ('时间管理', { 'fields': ['required_completion_date', 'completion_date'] }), ('审批与反馈', { 'fields': [ 'approval_result', 'customer_feedback', 'is_ordered' ] }), ('流程管理', { 'fields': ['business_object', 'status_display', 'progress_display'], 'classes': ['collapse'] }), ] @admin.display(description='设计编号') def design_code_display(self, obj): return obj.design_code or (str(obj.id) if obj.id else '-') @admin.display(description='客户') def customer_name(self, obj): return obj.customer.name if obj.customer else '-' @admin.display(description='销售员') def salesperson_name(self, obj): return obj.salesperson.name if obj.salesperson else '-' @admin.display(description='开发进程') def development_status_display(self, obj): return obj.status @admin.display(description='紧急程度') def urgency_display(self, obj): if obj.urgency_level == '加急': return format_html( '⚠️ 加急' ) return obj.urgency_level @admin.display(description='进度') def progress_display(self, obj): return obj.progress_percentage @admin.display(description='当前状态') def status_display(self, obj): return obj.status @action(description='推进到下一个状态') def advance_to_next_state_action(self, request, queryset): from stateflow import services success_count = 0 messages = [] for obj in queryset: if not obj.business_object: messages.append(f"{obj.id}: 没有关联的流程实例,无法推进") continue # 修复历史/异常数据:确保该订单的流程实例正确绑定到订单(避免 BusinessObject.object_id/content_type 为空) services.ensure_business_object_bound_to_instance( obj.business_object, obj, default_name=f"PlateOrder-{obj.id}", ) success, message, state_log = services.advance_to_next_state(obj.business_object, request.user) if success: success_count += 1 messages.append(f"{obj.id}: {message}") self.message_user(request, f'成功推进 {success_count}/{queryset.count()} 个开版订单。详情: {"; ".join(messages[:5])}') @action(description='回退一步') def step_back_one_state_action(self, request, queryset): from stateflow import services success_count = 0 messages = [] for obj in queryset: if not obj.business_object: messages.append(f"{obj.id}: 没有关联的流程实例,无法回退") continue success, message = services.step_back_one_state(obj.business_object, request.user) if success: success_count += 1 messages.append(f"{obj.id}: {message}") self.message_user(request, f'成功回退 {success_count}/{queryset.count()} 个开版订单。详情: {"; ".join(messages[:5])}') @action(description='重置进度') def reset_progress_action(self, request, queryset): from stateflow import services reset_count = 0 for obj in queryset: if not obj.business_object: continue services.reset_business_object_progress(obj.business_object) reset_count += 1 self.message_user(request, f'已重置 {reset_count} 个开版订单的进度。') class PrintingJobInlineFormSet(BaseInlineFormSet): """自定义 formset 处理 PrintingJob 的创建""" def save_new(self, form, commit=True): """保存新的 PrintingJob(暂时不创建 BusinessObject,等 order 保存完成后再创建)""" obj = super().save_new(form, commit=commit) return obj class PrintingJobInline(admin.StackedInline): model = models.PrintingJob formset = PrintingJobInlineFormSet extra = 1 autocomplete_fields = ('product',) # 避免加载所有产品导致页面卡死 @admin.register(models.PrintingOrder) class PrintingOrderAdmin(admin.ModelAdmin): list_display = ( 'oid', 'external_order_id', 'external_customer_name', 'external_employee_name', 'customer_name', 'progress_percent', 'created_at', 'outgoing_date', ) search_fields = ( '=id', 'external_order_id', 'external_customer_id', 'external_customer_name', 'external_employee_name', 'customer__name', 'fabric', 'craft', 'description', ) readonly_fields = ('external_raw_pretty',) fieldsets = ( ('基础信息', { 'fields': ( 'customer', 'fabric', 'width', 'is_urgent', 'area', 'address', 'fabric_source', 'is_fabric_received', 'craft', 'description', 'outgoing_date', 'curve', 'new_curve', 'position', 'printing_warn', 'rolling_warn', 'production_warn', 'is_invalid', 'process', ) }), ('外部同步信息', { 'fields': ( 'external_order_id', 'external_customer_id', 'external_customer_name', 'external_employee_name', 'external_raw_pretty', ), 'classes': ('collapse',), }), ) @admin.display(description='外部订单编号') def external_order_id(self, obj): return obj.external_order_id or '-' @admin.display(description='外部客户名') def external_customer_name(self, obj): return obj.external_customer_name or '-' @admin.display(description='外部员工名') def external_employee_name(self, obj): return obj.external_employee_name or '-' @admin.display(description='外部原始数据') def external_raw_pretty(self, obj): import json return format_html('
{}', json.dumps(obj.external_raw or {}, ensure_ascii=False, indent=2))
def save_formset(self, request, form, formset, change):
"""保存 formset 后,为新创建的 PrintingJob 创建 BusinessObject"""
# 先调用默认的保存
formset.save()
# 此时 PrintingOrder 已经保存完成(包括 process 字段)
# 为所有新创建且没有 business_object 的 job 创建 BusinessObject
if isinstance(formset.model, type) and issubclass(formset.model, models.PrintingJob):
order = form.instance
ct = ContentType.objects.get_for_model(models.PrintingJob)
for job in order.printing_jobs.all():
if not job.business_object and order.process:
from stateflow.models import BusinessObject
business_object = BusinessObject.objects.create(
name=f"PrintingJob-{job.id}",
process=order.process,
description=f"印染任务 {job.id} 的流程实例",
content_type=ct,
object_id=job.id,
)
job.business_object = business_object
job.save(update_fields=['business_object'])
def save_model(self, request, obj, form, change):
"""保存 PrintingOrder 时使用 service 层"""
from api_v1.views.printing.services import PrintingOrderService
if not change: # 新创建
# 使用 service 创建(会自动设置默认 process)
data = {
'customer': obj.customer,
'fabric': obj.fabric,
'width': obj.width,
'is_urgent': obj.is_urgent,
'area': obj.area,
'address': obj.address,
'fabric_source': obj.fabric_source,
'is_fabric_received': obj.is_fabric_received,
'craft': obj.craft,
'description': obj.description,
'outgoing_date': obj.outgoing_date,
'curve': obj.curve,
'new_curve': obj.new_curve,
'position': obj.position,
'printing_warn': obj.printing_warn,
'rolling_warn': obj.rolling_warn,
'production_warn': obj.production_warn,
'is_invalid': obj.is_invalid,
'process': obj.process,
}
created_obj = PrintingOrderService.create_printing_order(data, request.user)
# 将创建的对象的所有字段复制到当前对象
for field in created_obj._meta.fields:
setattr(obj, field.name, getattr(created_obj, field.name))
else: # 更新
# 使用 service 更新
data = {
'customer': obj.customer,
'fabric': obj.fabric,
'width': obj.width,
'is_urgent': obj.is_urgent,
'area': obj.area,
'address': obj.address,
'fabric_source': obj.fabric_source,
'is_fabric_received': obj.is_fabric_received,
'craft': obj.craft,
'description': obj.description,
'outgoing_date': obj.outgoing_date,
'curve': obj.curve,
'new_curve': obj.new_curve,
'position': obj.position,
'printing_warn': obj.printing_warn,
'rolling_warn': obj.rolling_warn,
'production_warn': obj.production_warn,
'is_invalid': obj.is_invalid,
'process': obj.process,
}
success, message, updated_obj = PrintingOrderService.update_printing_order(
obj, data, request.user
)
if not success:
from django.core.exceptions import ValidationError
raise ValidationError(message)
@admin.display(description='客户名称')
def customer_name(self, obj: models.PrintingOrder):
return obj.customer.name
@admin.display(description='订单编号(虚拟)')
def oid(self, obj: models.PrintingOrder):
return obj.human_id
@admin.display(description='进度')
def progress_percent(self, obj: models.PrintingOrder):
return f"{obj.progress}%"
@admin.register(models.PrintingJob)
class PrintingJobAdmin(admin.ModelAdmin):
list_display = (
'id',
'printing_order',
'customer_name',
'product',
'quantity',
'unit',
'size',
'pieces',
'business_object',
'current_progress',
'execution_status',
'created_at',
)
search_fields = (
'=printing_order__id',
'printing_order__external_order_id',
'product__name',
'external_product_name',
)
list_filter = ('created_at', 'printing_order__customer__name')
actions = ['advance_to_next_state_action', 'step_back_one_state_action', 'reset_progress_action']
autocomplete_fields = (
'merchant',
'printing_order',
'product',
'business_object',
'created_by',
)
def save_model(self, request, obj, form, change):
"""保存 PrintingJob 时使用 service 层"""
from api_v1.views.printing.services import PrintingJobService
if not change: # 新创建
# 使用 service 创建(这样会自动创建 BusinessObject)
data = {
'printing_order': obj.printing_order,
'product': obj.product,
'quantity': obj.quantity,
'unit': obj.unit,
'size': obj.size,
'pieces': obj.pieces,
'sub_id': obj.sub_id,
'description': obj.description,
}
created_obj = PrintingJobService.create_printing_job(data, request.user)
# 将创建的对象的 ID 赋值给当前对象(这样 admin 可以正确跳转)
obj.pk = created_obj.pk
obj.id = created_obj.id
else: # 更新
# 使用 service 更新
data = {
'printing_order': obj.printing_order,
'product': obj.product,
'quantity': obj.quantity,
'unit': obj.unit,
'size': obj.size,
'pieces': obj.pieces,
'sub_id': obj.sub_id,
'description': obj.description,
}
success, message, updated_obj = PrintingJobService.update_printing_job(
obj, data, request.user
)
if not success:
from django.core.exceptions import ValidationError
raise ValidationError(message)
@admin.display(description='客户名称')
def customer_name(self, obj: models.PrintingJob):
return obj.printing_order.customer.name
@admin.display(description='当前进度')
def current_progress(self, obj: models.PrintingJob):
"""显示该 job 自己的进度(已完成节点数 / 总节点数)"""
if not obj.business_object or not obj.printing_order.process:
return "0%"
total_nodes = obj.business_object.process.process_nodes.count()
if total_nodes == 0:
return "0%"
completed_nodes = obj.business_object.state_logs.filter(is_cancelled=False).count()
progress = int((completed_nodes / total_nodes) * 100)
return f"{progress}%"
@admin.display(description='执行状态')
def execution_status(self, obj: models.PrintingJob):
return obj.status
@action(description='前进到下一个状态')
def advance_to_next_state_action(self, request, queryset):
from stateflow import services
success_count = 0
messages = []
for obj in queryset:
# 检查是否有 business_object
if not obj.business_object:
messages.append(f"PrintingJob #{obj.id}: 没有关联的业务对象,无法推进")
continue
# 修复历史/异常数据:确保该 job 的流程实例正确绑定到 job(避免 BusinessObject.object_id/content_type 为空)
services.ensure_business_object_bound_to_instance(
obj.business_object,
obj,
default_name=f"PrintingJob-{obj.id}",
)
success, message, state_log = services.advance_to_next_state(obj.business_object, request.user)
if success:
success_count += 1
messages.append(f"PrintingJob #{obj.id}: {message}")
self.message_user(request, f'成功推进 {success_count}/{queryset.count()} 个印染任务。详情: {"; ".join(messages[:5])}')
@action(description='回退一步')
def step_back_one_state_action(self, request, queryset):
from stateflow import services
success_count = 0
messages = []
for obj in queryset:
# 检查是否有 business_object
if not obj.business_object:
messages.append(f"PrintingJob #{obj.id}: 没有关联的业务对象,无法回退")
continue
success, message = services.step_back_one_state(obj.business_object, request.user)
if success:
success_count += 1
messages.append(f"PrintingJob #{obj.id}: {message}")
self.message_user(request, f'成功回退 {success_count}/{queryset.count()} 个印染任务。详情: {"; ".join(messages[:5])}')
@action(description='重置进度')
def reset_progress_action(self, request, queryset):
from stateflow import services
reset_count = 0
for obj in queryset:
# 检查是否有 business_object
if not obj.business_object:
continue
services.reset_business_object_progress(obj.business_object)
reset_count += 1
self.message_user(request, f'已重置 {reset_count} 个印染任务的进度。')
@admin.register(models.PrintingJobBatchAdvanceRecord)
class PrintingJobBatchAdvanceRecordAdmin(admin.ModelAdmin):
list_display = (
'id',
'printing_order',
'state',
'created_by',
'jobs_count',
'created_at',
)
list_filter = (
'state',
'created_by',
'created_at',
)
search_fields = (
'printing_order__customer__name',
'state__name',
'created_by__username',
)
readonly_fields = (
'created_at',
'updated_at',
)
date_hierarchy = 'created_at'
ordering = ('-created_at',)
autocomplete_fields = ('printing_order', 'state', 'created_by', 'printing_jobs')
@admin.display(description='涉及任务数')
def jobs_count(self, obj: models.PrintingJobBatchAdvanceRecord) -> int:
return obj.printing_jobs.count()
@admin.register(models.PrintingOrderExternalSnapshotSyncAudit)
class PrintingOrderExternalSnapshotSyncAuditAdmin(admin.ModelAdmin):
list_display = (
'id',
'external_order_id',
'printing_order',
'operator_user',
'operator_employee',
'allow_reset_stateflow',
'is_success',
'created_at',
'updated_at',
)
list_filter = (
'is_success',
'allow_reset_stateflow',
'created_at',
)
search_fields = (
'external_order_id',
'printing_order__id',
'operator_user__username',
'operator_employee__name',
'failure_reason',
)
readonly_fields = (
'printing_order',
'external_order_id',
'operator_user',
'operator_employee',
'before_snapshot',
'allow_reset_stateflow',
'is_success',
'failure_reason',
'created_at',
'updated_at',
)
date_hierarchy = 'created_at'
ordering = ('-created_at', '-id')
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(models.PlateOrderTiiaUploadFailure)
class PlateOrderTiiaUploadFailureAdmin(admin.ModelAdmin):
list_display = ('id', 'run_date', 'plate_order_id', 'attempts', 'last_attempt_at', 'created_at')
list_filter = ('run_date', 'created_at')
search_fields = ('plate_order_id', 'error')
readonly_fields = ('created_at', 'updated_at')