1
0
forked from erp-dev/erp

feat: added plate order clone

This commit is contained in:
2025-12-16 08:42:53 +08:00
parent 4c9b8e6a42
commit 0638dd1ad3
25 changed files with 46174 additions and 8 deletions

View File

@@ -186,6 +186,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer):
work_state_display = serializers.SerializerMethodField()
is_completed = serializers.BooleanField(read_only=True)
business_object_id = serializers.SerializerMethodField()
batch_advance_records = serializers.SerializerMethodField()
class Meta:
model = models.PrintingJob
@@ -194,6 +195,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer):
'quantity', 'unit', 'size', 'pieces', 'description',
'work_state', 'work_state_display',
'status', 'is_completed', 'business_object_id',
'batch_advance_records',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'status', 'is_completed', 'business_object_id']
@@ -205,6 +207,17 @@ class PrintingJobListSerializer(serializers.ModelSerializer):
def get_work_state_display(self, obj):
return obj.get_work_state_display()
def get_batch_advance_records(self, obj):
"""
批量推进记录(稳定输出 key
- 如果没有批量记录:返回 []
- 仅返回与该 job 相关的批量推进审计记录(按 created_at 倒序)
"""
# 注意:这里不要链式调用 select_related/order_by否则会绕开 viewset 的 prefetch 缓存,导致 N+1 查询。
records = list(obj.batch_advance_records.all())
return PrintingJobBatchAdvanceRecordSerializer(records, many=True).data
class PrintingJobDetailSerializer(serializers.ModelSerializer):
"""印染款式明细详情序列化器"""
@@ -217,6 +230,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer):
is_completed = serializers.BooleanField(read_only=True)
has_started = serializers.BooleanField(read_only=True)
business_object_id = serializers.SerializerMethodField()
batch_advance_records = serializers.SerializerMethodField()
class Meta:
model = models.PrintingJob
@@ -225,6 +239,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer):
'quantity', 'unit', 'size', 'pieces', 'description',
'work_state', 'work_state_display',
'status', 'status_id', 'is_completed', 'has_started', 'business_object_id',
'batch_advance_records',
'created_at', 'updated_at'
]
read_only_fields = [
@@ -239,15 +254,22 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer):
def get_work_state_display(self, obj):
return obj.get_work_state_display()
def get_batch_advance_records(self, obj):
"""见 list serializer同样稳定输出 key"""
records = list(obj.batch_advance_records.all())
return PrintingJobBatchAdvanceRecordSerializer(records, many=True).data
class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
"""印染款式明细创建/更新序列化器"""
batch_advance_records = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.PrintingJob
fields = [
'id', 'printing_order', 'product', 'quantity', 'unit', 'size', 'pieces', 'description',
'work_state',
'batch_advance_records',
]
read_only_fields = ['id']
extra_kwargs = {
@@ -290,6 +312,43 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(message)
return updated_instance
def get_batch_advance_records(self, obj):
"""创建/更新接口也稳定输出该 key通常为空"""
records = list(obj.batch_advance_records.all())
return PrintingJobBatchAdvanceRecordSerializer(records, many=True).data
class PrintingJobBatchAdvanceRecordSerializer(serializers.ModelSerializer):
"""印染任务批量推进记录(用于嵌入 PrintingJob 的序列化结果)"""
state_id = serializers.IntegerField(source='state.id', read_only=True)
state_name = serializers.CharField(source='state.name', read_only=True)
created_by_username = serializers.CharField(source='created_by.username', read_only=True)
created_by_name = serializers.SerializerMethodField()
class Meta:
model = models.PrintingJobBatchAdvanceRecord
fields = [
'id',
'printing_order',
'state',
'state_id',
'state_name',
'created_by',
'created_by_username',
'created_by_name',
'parameters',
'created_at',
]
read_only_fields = fields
def get_created_by_name(self, obj):
user = getattr(obj, 'created_by', None)
if not user:
return None
emp = getattr(user, 'employee', None)
return getattr(emp, 'name', None)
class PlateOrderDesignCodeMixin:
"""确保 design_code 为空时使用主键"""
@@ -320,6 +379,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
process_name = serializers.SerializerMethodField()
plate_image_url = serializers.SerializerMethodField()
last_completed_state = serializers.CharField(read_only=True)
content_type_id = serializers.SerializerMethodField()
class Meta:
model = models.PlateOrder
@@ -340,7 +400,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
'approval_result', 'is_ordered', 'customer_feedback', 'print_count',
'process', 'process_name',
'status', 'status_id', 'is_completed', 'has_started', 'last_completed_state',
'progress_percentage', 'business_object_id',
'progress_percentage', 'business_object_id', 'content_type_id',
'created_at', 'updated_at'
]
read_only_fields = [
@@ -364,6 +424,17 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
return None
return None
def get_content_type_id(self, obj):
"""
返回关联流程实例business_object的 content_type_id。
- 无 business_object返回 None
- business_object 存在但未绑定关联对象:返回 None
"""
if not getattr(obj, 'business_object_id', None):
return None
return obj.business_object.content_type_id
class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSerializer):
"""开版订单详情序列化器"""

View File

@@ -4,6 +4,7 @@ Printing module business logic services
from typing import Dict, Any, Tuple
from django.conf import settings
from django.db import transaction
from django.contrib.contenttypes.models import ContentType
from printing import models as printing_models
from stateflow import models as stateflow_models
from stateflow import services as stateflow_services
@@ -126,10 +127,14 @@ class PrintingJobService:
# 如果 PrintingOrder 有关联的流程,创建 BusinessObject
if printing_order and printing_order.process:
# 将流程实例绑定到该 PrintingJob便于跨模块定位与审计
ct = ContentType.objects.get_for_model(printing_models.PrintingJob)
business_object = stateflow_models.BusinessObject.objects.create(
name=f"PrintingJob-{job.id}",
process=printing_order.process,
description=f"印染任务 {job.id} 的流程实例",
content_type=ct,
object_id=job.id,
)
job.business_object = business_object
job.save()

View File

@@ -2,6 +2,7 @@
PlateOrder API 测试
"""
from django.core.files.uploadedfile import SimpleUploadedFile
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
@@ -294,6 +295,35 @@ class PlateOrderAPITestCase(TestCase):
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(results[0]['design_code'], str(results[0]['id']))
def test_plate_order_list_includes_content_type_id(self):
"""列表接口应包含 content_type_id来源 business_object.content_type_id"""
process = stateflow_models.Process.objects.create(name='P-ct')
ct = ContentType.objects.get_for_model(stateflow_models.Process)
bo = stateflow_models.BusinessObject.objects.create(
name='BO-ct',
process=process,
content_type=ct,
object_id=123,
)
plate_order = printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='DESIGN-CT',
plate_type='圆网',
style_name='款式-ct',
fabric='棉布',
process=process.id,
business_object=bo,
)
response = self.client.get('/api/v1/plate-orders/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
target = next(item for item in results if item['id'] == plate_order.id)
self.assertIn('content_type_id', target)
self.assertEqual(target['content_type_id'], ct.id)
def test_fabric_source_field_in_detail(self):
"""验证布料来源字段在返回中存在"""
plate_order = printing_models.PlateOrder.objects.create(
@@ -687,6 +717,12 @@ class PlateOrderAPITestCase(TestCase):
self.assertIsNotNone(plate_order.business_object)
self.assertEqual(plate_order.business_object.process.id, self.process.id)
# 新逻辑BusinessObject 绑定回 PlateOrder
self.assertEqual(
plate_order.business_object.content_type_id,
ContentType.objects.get_for_model(printing_models.PlateOrder).id
)
self.assertEqual(plate_order.business_object.object_id, plate_order.id)
def test_advance_to_next_state(self):
"""测试推进到下一个状态"""

View File

@@ -7,6 +7,7 @@ from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from basic_info import models as basic_models
from printing import models as printing_models
from stateflow import models as stateflow_models
@@ -108,6 +109,11 @@ class PrintingJobAPITestCase(TestCase):
response = self.client.post('/api/v1/printing-jobs/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# 新增字段:批量推进记录(稳定输出 key默认空数组
self.assertIn('batch_advance_records', response.data)
self.assertIsInstance(response.data['batch_advance_records'], list)
self.assertEqual(len(response.data['batch_advance_records']), 0)
# 验证创建成功
job = printing_models.PrintingJob.objects.filter(
printing_order=self.printing_order,
@@ -121,7 +127,7 @@ class PrintingJobAPITestCase(TestCase):
def test_list_printing_jobs(self):
"""测试获取款式明细列表"""
# 创建测试数据
printing_models.PrintingJob.objects.create(
job1 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=100,
@@ -129,7 +135,7 @@ class PrintingJobAPITestCase(TestCase):
size='50*60',
pieces=10
)
printing_models.PrintingJob.objects.create(
job2 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=200,
@@ -141,6 +147,12 @@ class PrintingJobAPITestCase(TestCase):
response = self.client.get('/api/v1/printing-jobs/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)
# 新增字段:批量推进记录(稳定输出 key
for item in response.data:
self.assertIn('batch_advance_records', item)
self.assertIsInstance(item['batch_advance_records'], list)
self.assertEqual(len(item['batch_advance_records']), 0)
def test_retrieve_printing_job(self):
"""测试获取款式明细详情"""
@@ -160,6 +172,58 @@ class PrintingJobAPITestCase(TestCase):
self.assertEqual(response.data['unit'], '')
self.assertIn('product_name', response.data)
self.assertEqual(response.data['product_name'], self.product.name)
# 新增字段:批量推进记录(稳定输出 key
self.assertIn('batch_advance_records', response.data)
self.assertIsInstance(response.data['batch_advance_records'], list)
self.assertEqual(len(response.data['batch_advance_records']), 0)
def test_batch_advance_records_in_list_and_detail(self):
"""测试 PrintingJob 序列化输出包含批量推进记录(有记录时返回明细,无记录返回空)"""
job = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=100,
unit='',
size='50*60',
pieces=10,
description='批量记录测试'
)
# 创建一条批量推进记录并关联该 job
record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
printing_order=self.printing_order,
state=self.state1,
created_by=self.user,
parameters={'temperature': '25.5', 'operator': '张三'},
)
record.printing_jobs.add(job)
# list: 应包含 batch_advance_records
response = self.client.get('/api/v1/printing-jobs/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
target = next(item for item in response.data if item['id'] == job.id)
self.assertIn('batch_advance_records', target)
self.assertEqual(len(target['batch_advance_records']), 1)
rec = target['batch_advance_records'][0]
self.assertEqual(rec['id'], record.id)
self.assertEqual(rec['printing_order'], self.printing_order.id)
self.assertEqual(rec['state'], self.state1.id)
self.assertEqual(rec['state_id'], self.state1.id)
self.assertEqual(rec['state_name'], self.state1.name)
self.assertEqual(rec['created_by'], self.user.id)
self.assertEqual(rec['created_by_username'], self.user.username)
self.assertEqual(rec['created_by_name'], self.employee.name)
self.assertEqual(rec['parameters']['temperature'], '25.5')
self.assertEqual(rec['parameters']['operator'], '张三')
# detail: 同样应包含 batch_advance_records
response = self.client.get(f'/api/v1/printing-jobs/{job.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('batch_advance_records', response.data)
self.assertEqual(len(response.data['batch_advance_records']), 1)
def test_update_printing_job(self):
"""测试更新款式明细"""
@@ -453,6 +517,9 @@ class PrintingJobAPITestCase(TestCase):
job = printing_models.PrintingJob.objects.get(id=response.data['id'])
self.assertIsNotNone(job.business_object)
self.assertEqual(job.business_object.process, self.process)
# 新逻辑BusinessObject 绑定回 PrintingJob
self.assertEqual(job.business_object.content_type_id, ContentType.objects.get_for_model(printing_models.PrintingJob).id)
self.assertEqual(job.business_object.object_id, job.id)
def test_job_status_in_detail(self):
"""测试任务详情包含状态字段"""

View File

@@ -8,7 +8,7 @@ from rest_framework.permissions import BasePermission
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from django.db.models import CharField
from django.db.models import CharField, Prefetch
from django.db.models.functions import Cast, Coalesce
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as django_filters
@@ -306,6 +306,14 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
queryset = super().get_queryset()
if self.action in ['list', 'retrieve']:
queryset = queryset.select_related('printing_order', 'product')
# 预取批量推进记录(避免 serializer 产生 N+1
queryset = queryset.prefetch_related(
Prefetch(
'batch_advance_records',
queryset=models.PrintingJobBatchAdvanceRecord.objects.select_related('state', 'created_by')
.order_by('-created_at', '-id'),
)
)
return queryset
def destroy(self, request, *args, **kwargs):