1
0
forked from erp-dev/erp
Files
erpnew/api_v1/views/printing/serializers.py

733 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
from printing import models
from .services import PrintingOrderService, PrintingJobService
from basic_info.models import Customer, Employee
def _build_absolute_media_url(url: str | None, request):
return build_public_media_url(url, request=request)
def _serialize_plate_images(raw_value, request):
if not raw_value:
return []
serialized = []
if isinstance(raw_value, list):
iterable = raw_value
elif isinstance(raw_value, dict):
iterable = [raw_value]
elif isinstance(raw_value, str):
iterable = [{'url': raw_value}]
else:
iterable = []
for entry in iterable:
if isinstance(entry, str):
data = {'url': entry}
elif isinstance(entry, dict):
data = dict(entry)
else:
continue
url = data.get('url') or data.get('path')
data['url'] = _build_absolute_media_url(url, request)
if 'path' not in data and isinstance(url, str):
data['path'] = url
serialized.append(data)
return serialized
def _build_plate_image_payload(items, request_user):
if items is None:
return None
if not items:
return []
file_ids = [item['file_id'] for item in items if item.get('file_id') is not None]
if not file_ids:
return []
queryset = UploadedFile.objects.filter(id__in=file_ids, is_deleted=False)
files_map = {file.id: file for file in queryset}
missing = [str(fid) for fid in file_ids if fid not in files_map]
if missing:
raise serializers.ValidationError({'plate_image': f'以下文件不存在或已删除: {", ".join(missing)}'})
payload = []
for item in items:
file = files_map[item['file_id']]
name = item.get('name') or file.original_filename or file.path.name
payload.append({
'file_id': file.id,
'name': name,
'path': file.path.name,
'url': file.file_url,
'size': file.file_size,
'content_type': file.content_type,
'uploaded_at': file.created_at.isoformat(),
})
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(
required=False,
allow_blank=True,
allow_null=True,
help_text='可选的图片名称,默认使用文件原始名称'
)
class Meta:
list_serializer_class = PlateImageInputListSerializer
class PrintingOrderListSerializer(serializers.ModelSerializer):
"""印染订单列表序列化器"""
customer_name = serializers.CharField(source='customer.name', read_only=True)
customer_phone = serializers.CharField(source='customer.mobile', read_only=True)
process_name = serializers.CharField(source='process.name', read_only=True)
progress = serializers.IntegerField(read_only=True)
jobs_status_summary = serializers.SerializerMethodField()
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
created_by_name = serializers.SerializerMethodField()
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'customer', 'customer_name', 'customer_phone',
'fabric', 'width', 'is_urgent', 'area', 'address', 'curve',
'is_fabric_received', 'outgoing_date', 'is_invalid', 'new_curve',
'process', 'process_name', 'progress', 'position', 'print_count',
'jobs_status_summary',
'created_by', 'created_by_name',
'created_at', 'updated_at',
]
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
def get_jobs_status_summary(self, obj):
"""
返回订单下所有 PrintingJob 按当前状态分组的数量汇总
返回格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...]
- state_name: 状态名称(已完成的返回"已完成"
- state_id: 状态ID已完成的返回 None
- count: 该状态下的 job 数量
"""
# 使用预取的 printing_jobs避免 N+1
jobs = getattr(obj, '_prefetched_objects_cache', {}).get('printing_jobs') or obj.printing_jobs.all()
from collections import Counter
status_counter = Counter()
for job in jobs:
# 获取 job 的当前状态显示名称
status_name = job.status # 调用 @property返回状态名或"已完成"
status_id = job.status_id # 状态ID已完成时为 None
status_counter[(status_name, status_id)] += 1
# 转换为列表格式
return [
{'state_name': name, 'state_id': sid, 'count': count}
for (name, sid), count in status_counter.items()
]
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 PrintingOrderDetailSerializer(serializers.ModelSerializer):
"""印染订单详情序列化器"""
customer_name = serializers.CharField(source='customer.name', read_only=True)
customer_phone = serializers.CharField(source='customer.mobile', read_only=True)
customer_area = serializers.CharField(source='customer.area', read_only=True)
process_name = serializers.CharField(source='process.name', read_only=True)
created_by_name = serializers.SerializerMethodField()
progress = serializers.IntegerField(read_only=True)
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'customer', 'customer_name', 'customer_phone', 'customer_area',
'fabric', 'width', 'is_urgent', 'area', 'address', 'fabric_source',
'is_fabric_received', 'craft', 'description', 'outgoing_date',
'curve', 'new_curve', 'position', 'created_by_name',
'printing_warn', 'rolling_warn', 'production_warn',
'is_invalid', 'process', 'process_name', 'progress', 'print_count',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
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 PrintingOrderCreateUpdateSerializer(serializers.ModelSerializer):
"""印染订单创建/更新序列化器"""
class Meta:
model = models.PrintingOrder
fields = [
'id', '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'
]
read_only_fields = ['id']
def validate_customer(self, value):
"""验证客户是否存在"""
if not value:
raise serializers.ValidationError("客户不能为空")
return value
def validate(self, attrs):
"""验证流程修改权限"""
# 更新时验证 process 修改权限
if self.instance and 'process' in attrs:
new_process = attrs['process']
old_process = self.instance.process
if new_process != old_process:
if not PrintingOrderService.can_change_process(self.instance):
raise serializers.ValidationError({
'process': '存在已开始的印染任务,无法修改流程'
})
return attrs
def create(self, validated_data):
"""创建订单,使用 service 层"""
user = self.context['request'].user
return PrintingOrderService.create_printing_order(validated_data, user)
def update(self, instance, validated_data):
"""更新订单,使用 service 层"""
user = self.context['request'].user
success, message, updated_instance = PrintingOrderService.update_printing_order(
instance, validated_data, user
)
if not success:
raise serializers.ValidationError(message)
return updated_instance
class PrintingJobListSerializer(serializers.ModelSerializer):
"""印染款式明细列表序列化器"""
printing_order_id = serializers.CharField(source='printing_order.human_id', read_only=True)
product_name = serializers.CharField(source='product.name', read_only=True)
product_image_url = serializers.SerializerMethodField()
status = serializers.CharField(read_only=True)
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
fields = [
'id', 'original_id', 'printing_order', 'printing_order_id', 'product', 'product_name',
'product_image_url',
'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']
def get_business_object_id(self, obj):
"""安全地获取 business_object_id"""
return obj.business_object.id if obj.business_object else None
def get_product_image_url(self, obj):
"""
获取产品图片 URL
优先级:
1. mdy_image_url明道云图片
2. description JSON 中的图片 URL
3. image 字段(本地上传的图片)
"""
if not obj.product:
return None
# 先尝试 get_primary_image_urlmdy_image_url 或 description JSON
primary_url = obj.product.get_primary_image_url()
if primary_url:
return primary_url
# fallback 到本地上传的 image 字段
if obj.product.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.product.image.url)
return obj.product.image.url
return None
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):
"""印染款式明细详情序列化器"""
printing_order_id = serializers.CharField(source='printing_order.human_id', read_only=True)
product_name = serializers.CharField(source='product.name', read_only=True)
product_code = serializers.CharField(source='product.human_id', read_only=True)
status = serializers.CharField(read_only=True)
status_id = serializers.IntegerField(read_only=True)
work_state_display = serializers.SerializerMethodField()
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
fields = [
'id', 'original_id', 'printing_order', 'printing_order_id', 'product', 'product_name', 'product_code',
'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 = [
'id', 'created_at', 'updated_at',
'status', 'status_id', 'is_completed', 'has_started', 'business_object_id'
]
def get_business_object_id(self, obj):
"""安全地获取 business_object_id"""
return obj.business_object.id if obj.business_object else None
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', 'original_id', 'printing_order', 'product', 'quantity', 'unit', 'size', 'pieces', 'description',
'work_state',
'batch_advance_records',
]
read_only_fields = ['id']
extra_kwargs = {
'size': {'required': False, 'allow_null': True, 'allow_blank': True},
'pieces': {'required': False, 'allow_null': True},
'description': {'required': False, 'allow_null': True, 'allow_blank': True},
'work_state': {'required': False},
}
def validate_printing_order(self, value):
"""验证印染订单是否存在"""
if not value:
raise serializers.ValidationError("印染订单不能为空")
return value
def validate_product(self, value):
"""验证产品是否存在"""
if not value:
raise serializers.ValidationError("产品不能为空")
return value
def validate_quantity(self, value):
"""验证数量"""
if value <= 0:
raise serializers.ValidationError("数量必须大于0")
return value
def create(self, validated_data):
"""创建任务,使用 service 层"""
user = self.context['request'].user
return PrintingJobService.create_printing_job(validated_data, user)
def update(self, instance, validated_data):
"""更新任务,使用 service 层"""
user = self.context['request'].user
success, message, updated_instance = PrintingJobService.update_printing_job(
instance, validated_data, user
)
if not success:
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',
'only_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 为空时使用主键"""
@staticmethod
def _normalize_design_code(design_code: str | None, instance_id: int | None) -> str | None:
if design_code:
return design_code
if instance_id:
return str(instance_id)
return design_code
def to_representation(self, instance):
data = super().to_representation(instance)
data['design_code'] = self._normalize_design_code(data.get('design_code'), instance.id)
data['plate_image'] = _serialize_plate_images(getattr(instance, 'plate_image', None), self.context.get('request'))
return data
class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSerializer):
"""开版订单列表序列化器"""
customer_name = serializers.CharField(source="customer.name", read_only=True)
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()
plate_image_url = serializers.SerializerMethodField()
last_completed_state = serializers.CharField(read_only=True)
content_type_id = serializers.SerializerMethodField()
created_by_name = serializers.SerializerMethodField()
class Meta:
model = models.PlateOrder
fields = [
'id', 'original_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason',
'urgency_level', 'is_invalid',
'customer', 'customer_name', 'area', 'default_address',
'salesperson', 'salesperson_name',
'merchandiser', 'merchandiser_name',
'designer', 'designer_name',
'style_name', 'fabric', 'fabric_source', 'width', 'production_method',
'is_mark_frame', 'drawing_rating', 'color_matching_rating',
'sample_rating', 'difficulty_rating',
'sample_meter', 'required_sample_meters',
'required_completion_date', 'completion_date',
'approval_result', 'is_ordered', 'customer_feedback', 'print_count',
'process', 'process_name', 'created_by_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 = [
'id', 'status', 'progress_percentage', 'last_completed_state',
'created_at', 'updated_at', 'print_count'
]
def get_plate_image_url(self, obj):
images = _serialize_plate_images(getattr(obj, 'plate_image', None), self.context.get('request'))
return [entry.get('url') for entry in images if entry.get('url')]
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)
def get_process_name(self, obj) ->str | None:
"""获取流程名称"""
if obj.process:
try:
from stateflow.models import Process
process = Process.objects.get(id=obj.process)
return process.name
except Process.DoesNotExist:
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):
"""开版订单详情序列化器"""
customer_name = serializers.CharField(source="customer.name", read_only=True)
customer_phone = serializers.CharField(source="customer.mobile", read_only=True)
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)
has_started = serializers.BooleanField(read_only=True)
progress_percentage = serializers.IntegerField(read_only=True)
business_object_id = serializers.SerializerMethodField()
plate_image_url = serializers.SerializerMethodField()
process_name = serializers.SerializerMethodField()
last_completed_state = serializers.CharField(read_only=True)
class Meta:
model = models.PlateOrder
fields = [
'id', 'original_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason',
'urgency_level', 'is_invalid',
'customer', 'customer_name', 'customer_phone', 'area', 'default_address',
'salesperson', 'salesperson_name',
'merchandiser', 'merchandiser_name',
'designer', 'designer_name',
'style_name', 'fabric', 'fabric_source', 'width', 'production_method',
'is_mark_frame', 'drawing_rating', 'color_matching_rating',
'sample_rating', 'difficulty_rating',
'sample_meter', 'required_sample_meters',
'required_completion_date', 'completion_date',
'approval_result', 'is_ordered', 'customer_feedback', 'print_count',
'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 = [
'id', 'status', 'status_id', 'is_completed', 'has_started',
'progress_percentage', 'business_object_id', 'last_completed_state',
'created_at', 'updated_at', 'print_count'
]
def get_business_object_id(self, obj):
"""安全地获取 business_object_id"""
return obj.business_object.id if obj.business_object else None
def get_plate_image_url(self, obj):
images = _serialize_plate_images(getattr(obj, 'plate_image', None), self.context.get('request'))
return [entry.get('url') for entry in images if entry.get('url')]
def get_process_name(self, obj):
"""获取流程名称"""
if obj.process:
try:
from stateflow.models import Process
process = Process.objects.get(id=obj.process)
return process.name
except Process.DoesNotExist:
return None
return None
class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
"""开版订单创建/更新序列化器"""
plate_image = PlateImageInputSerializer(
many=True,
required=False,
allow_null=True,
help_text="开版图片列表,需提供已上传的 file_id可选 name 字段"
)
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
class Meta:
model = models.PlateOrder
fields = [
"id", "original_id", "design_code", "plate_type", "plate_date", "plate_method",
"plate_image", "image_name", "plate_notes", "reprint_reason",
"urgency_level", "is_invalid",
"customer", "area", "default_address",
"salesperson", "merchandiser", "designer",
"style_name", "fabric", "fabric_source", "width", "production_method",
"is_mark_frame", "drawing_rating", "color_matching_rating",
"sample_rating", "difficulty_rating",
"sample_meter", "required_sample_meters",
"required_completion_date", "completion_date",
"approval_result", "is_ordered", "customer_feedback",
"process",
"created_by",
]
read_only_fields = ["id", "created_by"]
def validate_plate_image(self, value):
return value or []
def validate_customer(self, value):
"""验证客户是否存在"""
if not value:
raise serializers.ValidationError("客户不能为空")
return value
def validate_salesperson(self, value):
"""验证业务员是否存在"""
if value and not Employee.objects.filter(id=value.id).exists():
raise serializers.ValidationError("业务员不存在")
return value
def validate_merchandiser(self, value):
"""验证跟单员是否存在"""
if value and not Employee.objects.filter(id=value.id).exists():
raise serializers.ValidationError("跟单员不存在")
return value
def validate_designer(self, value):
"""验证设计师是否存在"""
if value and not Employee.objects.filter(id=value.id).exists():
raise serializers.ValidationError("设计师不存在")
return value
def validate_sample_meter(self, value):
"""验证样品米数"""
if value is not None and value < 0:
raise serializers.ValidationError("样品米数不能为负数")
return value
def validate_required_sample_meters(self, value):
"""验证所需样品米数"""
if value is not None and value < 0:
raise serializers.ValidationError("所需样品米数不能为负数")
return value
def validate_process(self, value):
"""验证流程是否存在"""
if value:
from stateflow.models import Process
if not Process.objects.filter(id=value).exists():
raise serializers.ValidationError("流程不存在")
return value
def validate(self, attrs):
"""交叉验证"""
return attrs
def create(self, validated_data):
plate_images = validated_data.pop('plate_image', None)
if plate_images is not None:
validated_data['plate_image'] = _build_plate_image_payload(plate_images, self._request_user)
return super().create(validated_data)
def update(self, instance, validated_data):
plate_images = validated_data.pop('plate_image', None)
if plate_images is not None:
validated_data['plate_image'] = _build_plate_image_payload(plate_images, self._request_user)
return super().update(instance, validated_data)
@property
def _request_user(self):
request = self.context.get('request')
return getattr(request, 'user', None)