1
0
forked from erp-dev/erp

fix: business.object_id maybe null, missing parameters when plate-Order cloned

This commit is contained in:
2025-12-17 11:02:07 +08:00
parent 639efe8421
commit 51fad953c3
24 changed files with 13247 additions and 47 deletions

View File

@@ -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 []

View File

@@ -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)

View File

@@ -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
)