forked from erp-dev/erp
fix: business.object_id maybe null, missing parameters when plate-Order cloned
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Printing API 序列化器
|
||||
"""
|
||||
import json
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from api_v1.models import UploadedFile
|
||||
from api_v1.utils.media import build_public_media_url
|
||||
@@ -52,8 +54,6 @@ def _build_plate_image_payload(items, request_user):
|
||||
return []
|
||||
|
||||
queryset = UploadedFile.objects.filter(id__in=file_ids, is_deleted=False)
|
||||
if request_user and request_user.is_authenticated:
|
||||
queryset = queryset.filter(owner=request_user)
|
||||
|
||||
files_map = {file.id: file for file in queryset}
|
||||
missing = [str(fid) for fid in file_ids if fid not in files_map]
|
||||
@@ -76,6 +76,48 @@ def _build_plate_image_payload(items, request_user):
|
||||
return payload
|
||||
|
||||
|
||||
class PlateImageInputListSerializer(serializers.ListSerializer):
|
||||
"""
|
||||
兼容 plate_image 的多种入参形态(尤其是 multipart/form-data 场景):
|
||||
- JSON:直接传 array
|
||||
- multipart:常见会把数组作为 JSON 字符串传入(例如 '[{"file_id": 1}]')
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# allow_null=True 时,None 会先到这里
|
||||
if data is None:
|
||||
return []
|
||||
|
||||
# multipart/form-data 下,前端常把数组序列化成字符串
|
||||
if isinstance(data, str):
|
||||
raw = data.strip()
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
raise serializers.ValidationError('plate_image 必须是 JSON 数组或可解析为数组的 JSON 字符串')
|
||||
|
||||
# 兼容单个对象
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
"""
|
||||
DRF 在 multipart/form-data 下会优先用“HTML list”解析(期望 plate_image[0][file_id] 这类键)。
|
||||
但前端常见做法是直接传一个字段 plate_image='[{"file_id":1}]'(JSON 字符串)。
|
||||
这里做一次兜底:若 HTML list 未解析到值,则回退读取原始键值。
|
||||
"""
|
||||
value = super().get_value(dictionary)
|
||||
if value is empty and hasattr(dictionary, 'get'):
|
||||
raw = dictionary.get(self.field_name, empty)
|
||||
if raw is not empty:
|
||||
return raw
|
||||
return value
|
||||
|
||||
|
||||
class PlateImageInputSerializer(serializers.Serializer):
|
||||
file_id = serializers.IntegerField(min_value=1, help_text='上传文件的 ID')
|
||||
name = serializers.CharField(
|
||||
@@ -85,6 +127,9 @@ class PlateImageInputSerializer(serializers.Serializer):
|
||||
help_text='可选的图片名称,默认使用文件原始名称'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = PlateImageInputListSerializer
|
||||
|
||||
|
||||
class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||
"""印染订单列表序列化器"""
|
||||
@@ -374,6 +419,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
salesperson_name = serializers.CharField(source="salesperson.name", read_only=True)
|
||||
merchandiser_name = serializers.CharField(source="merchandiser.name", read_only=True)
|
||||
designer_name = serializers.CharField(source="designer.name", read_only=True)
|
||||
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
|
||||
status = serializers.CharField(read_only=True)
|
||||
progress_percentage = serializers.IntegerField(read_only=True)
|
||||
process_name = serializers.SerializerMethodField()
|
||||
@@ -401,6 +447,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
'process', 'process_name',
|
||||
'status', 'status_id', 'is_completed', 'has_started', 'last_completed_state',
|
||||
'progress_percentage', 'business_object_id', 'content_type_id',
|
||||
'created_by',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
@@ -443,6 +490,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
salesperson_name = serializers.CharField(source="salesperson.name", read_only=True)
|
||||
merchandiser_name = serializers.CharField(source="merchandiser.name", read_only=True)
|
||||
designer_name = serializers.CharField(source="designer.name", read_only=True)
|
||||
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
|
||||
status = serializers.CharField(read_only=True)
|
||||
status_id = serializers.IntegerField(read_only=True)
|
||||
is_completed = serializers.BooleanField(read_only=True)
|
||||
@@ -472,6 +520,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
'process', 'process_name',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id', 'last_completed_state',
|
||||
'created_by',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
@@ -508,6 +557,7 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
allow_null=True,
|
||||
help_text="开版图片列表,需提供已上传的 file_id,可选 name 字段"
|
||||
)
|
||||
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
@@ -523,9 +573,10 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"sample_meter", "required_sample_meters",
|
||||
"required_completion_date", "completion_date",
|
||||
"approval_result", "is_ordered", "customer_feedback",
|
||||
"process"
|
||||
"process",
|
||||
"created_by",
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
read_only_fields = ["id", "created_by"]
|
||||
|
||||
def validate_plate_image(self, value):
|
||||
return value or []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
PlateOrder API 测试
|
||||
"""
|
||||
import json
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
@@ -136,9 +137,13 @@ class PlateOrderAPITestCase(TestCase):
|
||||
self.assertEqual(response.data['image_name'], 'sample.png')
|
||||
self.assertEqual(len(response.data['plate_image']), 1)
|
||||
self.assertEqual(response.data['plate_image'][0]['file_id'], self.upload_file_primary.id)
|
||||
self.assertIn('created_by', response.data)
|
||||
self.assertEqual(response.data['created_by'], self.user.id)
|
||||
|
||||
# 验证数据库中创建了记录
|
||||
self.assertTrue(printing_models.PlateOrder.objects.filter(design_code='DESIGN001').exists())
|
||||
created = printing_models.PlateOrder.objects.get(design_code='DESIGN001')
|
||||
self.assertEqual(created.created_by_id, self.user.id)
|
||||
|
||||
def test_create_plate_order_without_customer(self):
|
||||
"""测试创建开版订单时未提供客户"""
|
||||
@@ -165,6 +170,8 @@ class PlateOrderAPITestCase(TestCase):
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
# CharField 不会验证内容,所以应该成功
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertIn('created_by', response.data)
|
||||
self.assertEqual(response.data['created_by'], self.user.id)
|
||||
|
||||
def test_create_plate_order_with_invalid_plate_image_file(self):
|
||||
data = {
|
||||
@@ -176,6 +183,45 @@ class PlateOrderAPITestCase(TestCase):
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('plate_image', response.data)
|
||||
|
||||
def test_create_plate_order_with_plate_image_json_string_in_multipart(self):
|
||||
"""兼容前端使用 multipart/form-data 时 plate_image 以 JSON 字符串传入"""
|
||||
data = {
|
||||
'customer': str(self.customer.id),
|
||||
'design_code': 'DESIGN_MULTIPART_IMG',
|
||||
'plate_type': '圆网',
|
||||
'plate_image': json.dumps([{'file_id': self.upload_file_primary.id, 'name': '主图'}]),
|
||||
}
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='multipart')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
|
||||
self.assertIn('plate_image', response.data)
|
||||
self.assertEqual(len(response.data['plate_image']), 1)
|
||||
self.assertEqual(response.data['plate_image'][0]['file_id'], self.upload_file_primary.id)
|
||||
|
||||
def test_create_plate_order_can_reference_uploaded_file_owned_by_other_user(self):
|
||||
"""克隆/协作场景:允许引用非本人上传的 UploadedFile(只要文件存在且未删除)"""
|
||||
other_user = User.objects.create_user(
|
||||
username='otheruser',
|
||||
password='pass123',
|
||||
email='other@example.com',
|
||||
)
|
||||
other_file = api_models.UploadedFile.objects.create(
|
||||
owner=other_user,
|
||||
path=SimpleUploadedFile('other.jpg', b'test-image', content_type='image/jpeg'),
|
||||
original_filename='other.jpg',
|
||||
file_size=9,
|
||||
content_type='image/jpeg',
|
||||
)
|
||||
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN_OTHER_OWNER_IMG',
|
||||
'plate_type': '圆网',
|
||||
'plate_image': [{'file_id': other_file.id, 'name': '协作图片'}],
|
||||
}
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
|
||||
self.assertEqual(response.data['plate_image'][0]['file_id'], other_file.id)
|
||||
|
||||
def test_list_plate_orders(self):
|
||||
"""测试获取开版订单列表"""
|
||||
@@ -231,6 +277,7 @@ class PlateOrderAPITestCase(TestCase):
|
||||
if collection:
|
||||
self.assertIn('plate_image', collection[0])
|
||||
self.assertIsInstance(collection[0]['plate_image'], list)
|
||||
self.assertIn('created_by', collection[0])
|
||||
|
||||
def test_retrieve_plate_order(self):
|
||||
"""测试获取单个开版订单详情"""
|
||||
@@ -258,6 +305,8 @@ class PlateOrderAPITestCase(TestCase):
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['design_code'], 'DESIGN001')
|
||||
self.assertIn('created_by', response.data)
|
||||
self.assertIsNone(response.data['created_by'])
|
||||
self.assertEqual(response.data['urgency_level'], '紧急')
|
||||
self.assertEqual(response.data['customer'], self.customer.id)
|
||||
self.assertIn('customer_name', response.data)
|
||||
|
||||
@@ -357,6 +357,12 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# 调用 stateflow 统一服务进行状态流转
|
||||
from stateflow import services as stateflow_services
|
||||
# 修复历史/异常数据:确保该 job 的流程实例正确绑定到 job(避免 BusinessObject.object_id/content_type 为空)
|
||||
stateflow_services.ensure_business_object_bound_to_instance(
|
||||
job.business_object,
|
||||
job,
|
||||
default_name=f"PrintingJob-{job.id}",
|
||||
)
|
||||
success, message, state_log = stateflow_services.advance_to_next_state(
|
||||
job.business_object, request.user, **parameters
|
||||
)
|
||||
@@ -641,6 +647,12 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
{'detail': '开版订单不支持删除操作,请使用作废功能'},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
创建时自动绑定创建人(created_by),不允许前端传参控制。
|
||||
"""
|
||||
serializer.save(created_by=self.request.user)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def invalidate(self, request, pk=None):
|
||||
@@ -739,6 +751,12 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# 调用 stateflow 统一服务进行状态流转
|
||||
from stateflow import services as stateflow_services
|
||||
# 修复历史/异常数据:确保该 plate_order 的流程实例正确绑定到 plate_order(避免 BusinessObject.object_id/content_type 为空)
|
||||
stateflow_services.ensure_business_object_bound_to_instance(
|
||||
plate_order.business_object,
|
||||
plate_order,
|
||||
default_name=f"PlateOrder-{plate_order.id}",
|
||||
)
|
||||
success, message, state_log = stateflow_services.advance_to_next_state(
|
||||
plate_order.business_object, request.user, **parameters
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user