forked from erp-dev/erp
feat: added plate order clone
This commit is contained in:
@@ -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):
|
||||
"""开版订单详情序列化器"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
"""测试推进到下一个状态"""
|
||||
|
||||
@@ -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):
|
||||
"""测试任务详情包含状态字段"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user