forked from erp-dev/erp
906 lines
38 KiB
Python
906 lines
38 KiB
Python
"""
|
||
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 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 _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()
|
||
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)
|
||
|
||
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', '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_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)
|
||
|
||
|
||
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)
|
||
|
||
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', '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)
|
||
|
||
|
||
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)",
|
||
)
|
||
|
||
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)
|
||
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_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)
|
||
|
||
class Meta:
|
||
model = models.PrintingJob
|
||
fields = [
|
||
'id', 'original_id', 'merchant_id', 'printing_order', 'printing_order_id', 'external_order_id', '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',
|
||
'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_url(mdy_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)
|
||
|
||
|
||
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)
|
||
|
||
class Meta:
|
||
model = models.PrintingJob
|
||
fields = [
|
||
'id', 'original_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',
|
||
'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)
|
||
|
||
|
||
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', '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 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)
|