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

1053 lines
44 KiB
Python
Raw Permalink 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 django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from rest_framework.fields import empty
from api_v1.models import UploadedFile
from api_v1.external_datetime import parse_external_china_datetime
from api_v1.utils.media import build_public_media_url
from api_v1.views.shipment.serializers import SalesItemSerializer
from printing import models
from .services import PrintingOrderService, PrintingJobService
from basic_info.models import Customer, Employee
def _include_sales_order_bound(context) -> bool:
request = context.get('request') if context else None
if not request:
return True
raw_value = request.query_params.get('include_sales_order_bound')
return str(raw_value).lower() not in {'0', 'false', 'no'}
def _get_is_sales_order_bound(obj) -> bool:
annotated_value = getattr(obj, 'annotated_is_sales_order_bound', None)
if annotated_value is not None:
return bool(annotated_value)
return bool(obj.is_sales_order_bound)
def _build_absolute_media_url(url: str | None, request):
return build_public_media_url(url, request=request)
def _resolve_printing_order_external_first_record_field(obj, field_name):
if not getattr(obj, 'external_order_id', None):
return None
try:
raw = obj.external_raw or {}
first_record = raw.get('first_record') or {}
value = first_record.get(field_name)
except AttributeError:
return None
value = str(value or '').strip()
return value or None
def resolve_printing_order_bianhao_kd(obj):
return _resolve_printing_order_external_first_record_field(obj, 'BianHaoKD')
def resolve_printing_order_kd_riqi(obj):
value = getattr(obj, 'kd_riqi', None)
if value is not None:
return serializers.DateTimeField().to_representation(value)
raw_value = _resolve_printing_order_external_first_record_field(obj, 'KdRiQi')
parsed = parse_external_china_datetime(raw_value)
if parsed is None:
return raw_value
return serializers.DateTimeField().to_representation(parsed)
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.SerializerMethodField()
customer_phone = serializers.SerializerMethodField()
process_name = serializers.CharField(source='process.name', read_only=True)
progress = serializers.IntegerField(read_only=True)
jobs_status_summary = serializers.SerializerMethodField()
jobs_last_status_summary = serializers.SerializerMethodField()
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
created_by_name = serializers.SerializerMethodField()
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
external_order_id = serializers.CharField(read_only=True)
external_customer_id = serializers.CharField(read_only=True)
external_customer_name = serializers.CharField(read_only=True)
external_employee_name = serializers.CharField(read_only=True)
bianhao_kd = serializers.SerializerMethodField()
kd_riqi = serializers.SerializerMethodField()
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'merchant_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', 'jobs_last_status_summary',
'external_order_id', 'bianhao_kd', 'kd_riqi', 'external_customer_id', 'external_customer_name', 'external_employee_name',
'created_by', 'created_by_name',
'created_at', 'updated_at',
]
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id']
def get_customer_name(self, obj):
if hasattr(obj, 'customer_name'):
return obj.customer_name
try:
customer = getattr(obj, 'customer', None)
except ObjectDoesNotExist:
return None
return getattr(customer, 'name', None)
def get_customer_phone(self, obj):
if hasattr(obj, 'customer_phone'):
return obj.customer_phone
try:
customer = getattr(obj, 'customer', None)
except ObjectDoesNotExist:
return None
return getattr(customer, 'mobile', None)
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_jobs_last_status_summary(self, obj):
"""
返回订单下所有 PrintingJob 按最后完成状态分组的数量汇总
返回格式: [{"state_name": "进度一", "count": 3}, ...]
- state_name: 最后完成的状态名称
- count: 该状态下的 job 数量
注意: 未开始的 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 的最后完成状态名称
last_completed = job.last_completed_state # 调用 @property未开始时返回空字符串
# 只统计有已完成状态的 job
if last_completed:
status_counter[last_completed] += 1
# 转换为列表格式
return [
{'state_name': name, 'count': count}
for name, 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)
def get_bianhao_kd(self, obj):
return resolve_printing_order_bianhao_kd(obj)
def get_kd_riqi(self, obj):
return resolve_printing_order_kd_riqi(obj)
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)
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
external_order_id = serializers.CharField(read_only=True)
external_customer_id = serializers.CharField(read_only=True)
external_customer_name = serializers.CharField(read_only=True)
external_employee_name = serializers.CharField(read_only=True)
bianhao_kd = serializers.SerializerMethodField()
kd_riqi = serializers.SerializerMethodField()
class Meta:
model = models.PrintingOrder
fields = [
'id', 'human_id', 'merchant_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',
'external_order_id', 'bianhao_kd', 'kd_riqi', 'external_customer_id', 'external_customer_name', 'external_employee_name',
'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', 'merchant_id']
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_bianhao_kd(self, obj):
return resolve_printing_order_bianhao_kd(obj)
def get_kd_riqi(self, obj):
return resolve_printing_order_kd_riqi(obj)
class PrintingOrderCreateUpdateSerializer(serializers.ModelSerializer):
"""印染订单创建/更新序列化器"""
# 兼容前端只传日期YYYY-MM-DD会解析为 00:00:00
# 同时也接受 ISO8601 datetime含时区
outgoing_date = serializers.DateTimeField(
required=False,
allow_null=True,
input_formats=[
"iso-8601",
"%Y-%m-%d",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
],
help_text="出货日期时间(可仅传 YYYY-MM-DD将自动视为 00:00:00",
)
kd_riqi = serializers.DateTimeField(
required=False,
allow_null=True,
input_formats=[
"iso-8601",
"%Y-%m-%d",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
],
help_text="外部开单日期时间(可仅传 YYYY-MM-DD将自动视为 00:00:00",
)
class Meta:
model = models.PrintingOrder
fields = [
'id', 'customer', 'fabric', 'width', 'is_urgent', 'area', 'address',
'fabric_source', 'is_fabric_received', 'craft', 'description',
'outgoing_date', 'kd_riqi', 'curve', 'new_curve', 'position',
'printing_warn', 'rolling_warn', 'production_warn', 'is_invalid',
'process', 'external_order_id',
]
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)
external_order_id = serializers.CharField(source='printing_order.external_order_id', read_only=True)
customer_name = serializers.CharField(source='printing_order.customer.name', read_only=True, allow_null=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()
has_started = serializers.BooleanField(read_only=True)
is_completed = serializers.BooleanField(read_only=True)
is_production_completed = serializers.BooleanField(read_only=True)
progress_percentage = serializers.FloatField(read_only=True)
last_completed_state = serializers.CharField(read_only=True)
business_object_id = serializers.SerializerMethodField()
batch_advance_records = serializers.SerializerMethodField()
saleitems = serializers.SerializerMethodField()
is_sales_order_bound = serializers.SerializerMethodField()
billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
fabric = serializers.SerializerMethodField()
curve = serializers.CharField(source='printing_order.curve', read_only=True, allow_null=True)
new_curve = serializers.CharField(source='printing_order.new_curve', read_only=True, allow_null=True)
class Meta:
model = models.PrintingJob
fields = [
'id', 'original_id', 'sub_id', 'merchant_id', 'printing_order', 'printing_order_id', 'external_order_id',
'customer_name', 'product', 'product_name',
'product_image_url', 'has_started',
'quantity', 'billed_quantity', 'unit', 'size', 'pieces', 'description',
'work_state', 'work_state_display',
'status', 'is_completed', 'is_production_completed', 'progress_percentage', 'last_completed_state',
'business_object_id',
'is_sales_order_bound',
'batch_advance_records',
'saleitems',
'fabric',
'curve',
'new_curve',
'created_at', 'updated_at'
]
read_only_fields = [
'id', 'created_at', 'updated_at',
'status', 'is_completed', 'is_production_completed', 'progress_percentage', 'last_completed_state',
'business_object_id', 'is_sales_order_bound', 'billed_quantity', 'merchant_id'
]
def to_representation(self, instance):
data = super().to_representation(instance)
if not _include_sales_order_bound(self.context):
data.pop('is_sales_order_bound', None)
return data
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
def get_saleitems(self, obj):
items = getattr(obj, '_saleitems_cache', None)
if items is None:
from shipment.services import get_active_sales_items_queryset
items = list(
get_active_sales_items_queryset().select_related('shipment', 'created_by')
.filter(printing_job_id=obj.id)
.order_by('id')
)
return SalesItemSerializer(items, many=True).data
def get_is_sales_order_bound(self, obj):
return _get_is_sales_order_bound(obj)
def get_fabric(self, obj):
return obj.printing_order.fabric
class PrintingJobDetailSerializer(serializers.ModelSerializer):
"""印染款式明细详情序列化器"""
printing_order_id = serializers.CharField(source='printing_order.human_id', read_only=True)
external_order_id = serializers.CharField(source='printing_order.external_order_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)
is_production_completed = serializers.BooleanField(read_only=True)
has_started = serializers.BooleanField(read_only=True)
progress_percentage = serializers.FloatField(read_only=True)
last_completed_state = serializers.CharField(read_only=True)
business_object_id = serializers.SerializerMethodField()
batch_advance_records = serializers.SerializerMethodField()
saleitems = serializers.SerializerMethodField()
is_sales_order_bound = serializers.SerializerMethodField()
billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
fabric = serializers.SerializerMethodField()
curve = serializers.CharField(source='printing_order.curve', read_only=True, allow_null=True)
new_curve = serializers.CharField(source='printing_order.new_curve', read_only=True, allow_null=True)
class Meta:
model = models.PrintingJob
fields = [
'id', 'original_id', 'sub_id', 'merchant_id', 'printing_order', 'printing_order_id', 'external_order_id', 'product', 'product_name', 'product_code',
'quantity', 'billed_quantity', 'unit', 'size', 'pieces', 'description',
'work_state', 'work_state_display',
'status', 'status_id', 'is_completed', 'is_production_completed', 'has_started',
'progress_percentage', 'last_completed_state',
'business_object_id',
'is_sales_order_bound',
'batch_advance_records',
'saleitems',
'fabric',
'curve',
'new_curve',
'created_at', 'updated_at'
]
read_only_fields = [
'id', 'created_at', 'updated_at',
'status', 'status_id', 'is_completed', 'is_production_completed', 'has_started',
'progress_percentage', 'last_completed_state',
'business_object_id', 'is_sales_order_bound', 'billed_quantity', 'merchant_id'
]
def to_representation(self, instance):
data = super().to_representation(instance)
if not _include_sales_order_bound(self.context):
data.pop('is_sales_order_bound', None)
return data
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
def get_saleitems(self, obj):
items = getattr(obj, '_saleitems_cache', None)
if items is None:
from shipment.services import get_active_sales_items_queryset
items = list(
get_active_sales_items_queryset().select_related('shipment', 'created_by')
.filter(printing_job_id=obj.id)
.order_by('id')
)
return SalesItemSerializer(items, many=True).data
def get_is_sales_order_bound(self, obj):
return _get_is_sales_order_bound(obj)
def get_fabric(self, obj):
return obj.printing_order.fabric
class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
"""印染款式明细创建/更新序列化器"""
external_order_id = serializers.CharField(source='printing_order.external_order_id', read_only=True)
batch_advance_records = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.PrintingJob
fields = [
'id', 'original_id', 'sub_id', 'printing_order', 'external_order_id', 'product', 'quantity', 'unit', 'size', 'pieces', 'description',
'work_state',
'batch_advance_records',
]
read_only_fields = ['id', 'external_order_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 层"""
# 不允许修改 printing_order 绑定关系(避免业务对象流程不一致)
if 'printing_order' in validated_data:
new_order = validated_data.get('printing_order')
if new_order and new_order.id != getattr(instance, 'printing_order_id', None):
raise serializers.ValidationError({'printing_order': '不允许修改印染订单绑定关系'})
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 PlateOrderKanbanListSerializer(serializers.ModelSerializer):
"""开版订单看板列表序列化器(精简版,仅包含看板实际使用的字段,消除 N+1 查询)"""
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)
created_by_name = serializers.CharField(source="created_by.employee.name", read_only=True, default=None)
process_name = serializers.CharField(source="annotated_process_name", read_only=True, default=None)
plate_image = serializers.SerializerMethodField()
plate_image_url = serializers.SerializerMethodField()
last_completed_state = serializers.CharField(read_only=True)
class Meta:
model = models.PlateOrder
fields = [
'id', 'design_code', 'customer_name', 'area',
'salesperson_name', 'merchandiser_name', 'fabric_source', 'fabric',
'width', 'plate_type', 'plate_date', 'plate_method',
'plate_image', 'plate_image_url', 'plate_notes', 'reprint_reason',
'urgency_level', 'production_method', 'is_mark_frame',
'drawing_rating', 'color_matching_rating', 'sample_rating',
'difficulty_rating', 'style_name', 'required_sample_meters',
'required_completion_date', 'completion_date', 'approval_result',
'customer_feedback', 'last_completed_state', 'business_object_id',
'original_id', 'process_name', 'print_count', 'created_at',
'created_by_name',
]
def _get_cached_plate_images(self, obj):
cache_attr = '_cached_plate_images'
images = getattr(obj, cache_attr, None)
if images is None:
images = _serialize_plate_images(
getattr(obj, 'plate_image', None),
self.context.get('request'),
)
setattr(obj, cache_attr, images)
return images
def get_plate_image(self, obj):
return self._get_cached_plate_images(obj)
def get_plate_image_url(self, obj):
images = self._get_cached_plate_images(obj)
return [entry.get('url') for entry in images if entry.get('url')]
def to_representation(self, instance):
data = super().to_representation(instance)
if not data.get('design_code') and instance.id:
data['design_code'] = str(instance.id)
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()
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
class Meta:
model = models.PlateOrder
fields = [
'id', 'original_id', 'merchant_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', 'merchant_id'
]
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)
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
class Meta:
model = models.PlateOrder
fields = [
'id', 'original_id', 'merchant_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', 'merchant_id'
]
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):
# 若更新涉及 process 变更,需要重建并重新绑定 BusinessObject若有未撤销进度则拒绝
new_process_id = validated_data.get('process', None)
if new_process_id is not None and new_process_id != getattr(instance, 'process', None):
from stateflow.models import Process
from stateflow.services import relink_business_object_for_instance
try:
new_process = Process.objects.get(id=new_process_id)
except Process.DoesNotExist:
# 理论上 validate_process 已校验;这里兜底
raise serializers.ValidationError({'process': '流程不存在'})
relinked = relink_business_object_for_instance(
instance=instance,
new_process=new_process,
default_name=f"PlateOrder-{instance.pk}",
default_description=f"开版订单 {instance.pk} 的流程实例",
)
if relinked is None:
raise serializers.ValidationError({'process': '该订单流程已存在有效进度,无法修改流程'})
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)