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

843 lines
28 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.
"""
Shipment API 序列化器
"""
from rest_framework import serializers
from shipment.models import (
ExternalFinishedProduct,
SalesItem,
Shipment,
ShipmentStatus,
ShipmentDelivery,
ShipmentDeliveryStatus,
)
def _build_nested_sales_item_context(items):
customer_ids = {item.customer_id for item in items if item.customer_id}
printing_job_ids = {item.printing_job_id for item in items if item.printing_job_id}
customer_name_map = {}
if customer_ids:
from basic_info.models import Customer
customer_name_map = dict(
Customer.objects.filter(id__in=customer_ids).values_list("id", "name")
)
printing_order_map = {}
external_order_id_map = {}
product_image_map = {}
if printing_job_ids:
from printing.models import PrintingJob
printing_jobs = list(
PrintingJob.objects.filter(id__in=printing_job_ids).select_related(
"printing_order", "product"
)
)
printing_order_map = {job.id: job.printing_order_id for job in printing_jobs}
external_order_id_map = {
job.id: getattr(job.printing_order, "external_order_id", None)
for job in printing_jobs
}
for job in printing_jobs:
if not getattr(job, "product", None):
product_image_map[job.id] = None
continue
primary_url = job.product.get_primary_image_url()
if primary_url:
product_image_map[job.id] = primary_url
elif job.product.image:
product_image_map[job.id] = job.product.image.url
else:
product_image_map[job.id] = None
return {
"customer_name_map": customer_name_map,
"printing_order_map": printing_order_map,
"external_order_id_map": external_order_id_map,
"product_image_map": product_image_map,
}
class ShipmentSerializer(serializers.ModelSerializer):
"""
出货单序列化器(只读,用于返回数据)
"""
customer_name = serializers.CharField(source="customer.name", read_only=True)
created_by_id = serializers.IntegerField(
source="created_by.id", read_only=True, allow_null=True
)
created_by_name = serializers.SerializerMethodField()
items_count = serializers.SerializerMethodField()
cancelled_by_id = serializers.IntegerField(
source="cancelled_by.id", read_only=True, allow_null=True
)
cancelled_by_name = serializers.SerializerMethodField()
approved_by_id = serializers.IntegerField(
source="approved_by.id", read_only=True, allow_null=True
)
approved_by_name = serializers.SerializerMethodField()
status_display = serializers.CharField(source="get_status_display", read_only=True)
external_finished_products_count = serializers.SerializerMethodField()
merchant_id = serializers.IntegerField(source="merchant.id", read_only=True)
merchant_name = serializers.CharField(source="merchant.name", read_only=True)
sales_items = serializers.SerializerMethodField()
external_finished_products = serializers.SerializerMethodField()
class Meta:
model = Shipment
fields = [
"id",
"merchant_id",
"merchant_name",
"customer",
"customer_name",
"shipment_date",
"address",
"contact_name",
"contact_phone",
"area",
"remark",
"status",
"status_display",
"external_id",
"status_modified_at",
"cancelled_by_id",
"cancelled_by_name",
"approved_by_id",
"approved_by_name",
"items_count",
"created_by_id",
"created_by_name",
"external_finished_products_count",
"sales_items",
"external_finished_products",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def get_created_by_name(self, obj):
if obj.created_by:
employee = getattr(obj.created_by, "employee", None)
if employee:
return employee.name
return obj.created_by.username
return None
def get_items_count(self, obj):
return obj.items.filter(delete_at__isnull=True).count()
def get_cancelled_by_name(self, obj):
if obj.cancelled_by:
employee = getattr(obj.cancelled_by, "employee", None)
if employee:
return employee.name
return obj.cancelled_by.username
return None
def get_external_finished_products_count(self, obj):
return obj.external_finished_products.count()
def get_approved_by_name(self, obj):
if obj.approved_by:
employee = getattr(obj.approved_by, "employee", None)
if employee:
return employee.name
return obj.approved_by.username
return None
def get_sales_items(self, obj):
"""
出货单关联的销售品明细(无则返回空数组)。
"""
# 优先使用 prefetch 的 related manager兜底为 none()
rel = getattr(obj, "items", None)
items = list(rel.filter(delete_at__isnull=True)) if rel is not None else []
serializer_context = dict(self.context)
serializer_context.update(_build_nested_sales_item_context(items))
return SalesItemDetailSerializer(
items,
many=True,
context=serializer_context,
).data
def get_external_finished_products(self, obj):
"""
出货单关联的外部成品表明细(无则返回空数组)。
"""
rel = getattr(obj, "external_finished_products", None)
products = list(rel.all()) if rel is not None else []
return ExternalFinishedProductSerializer(products, many=True).data
class ExternalFinishedProductSerializer(serializers.ModelSerializer):
"""
外部成品表序列化器(只读)
"""
created_by_id = serializers.IntegerField(
source="created_by.id", read_only=True, allow_null=True
)
created_by_name = serializers.SerializerMethodField()
class Meta:
model = ExternalFinishedProduct
fields = [
"id",
"style_name",
"num_of_rolls",
"remark",
"created_at",
"created_by_id",
"created_by_name",
]
read_only_fields = fields
def get_created_by_name(self, obj):
if obj.created_by:
employee = getattr(obj.created_by, "employee", None)
if employee:
return employee.name
return obj.created_by.username
return None
class ShipmentCreateNormalSerializer(serializers.Serializer):
"""
出货单创建序列化器(普通版)
"""
customer = serializers.IntegerField(help_text="客户ID")
shipment_date = serializers.DateField(help_text="出货日期")
address = serializers.CharField(
max_length=255,
required=False,
default="",
allow_blank=True,
help_text="地址(可选)",
)
contact_name = serializers.CharField(
max_length=100,
required=False,
default="",
allow_blank=True,
help_text="联系人(可选)",
)
contact_phone = serializers.CharField(
max_length=50,
required=False,
default="",
allow_blank=True,
help_text="联系电话(可选)",
)
area = serializers.CharField(
max_length=30,
required=False,
default="",
allow_blank=True,
help_text="出货地区(可选)",
)
remark = serializers.CharField(
required=False, default="", allow_blank=True, help_text="备注"
)
sales_items = serializers.ListField(
child=serializers.IntegerField(),
required=False,
default=list,
help_text="要关联的销售品ID列表",
)
def validate_sales_items(self, value):
# 去重
return list(set(value)) if value else []
class ExternalFinishedProductInputSerializer(serializers.Serializer):
"""外部成品表写入结构external create 专用)"""
style_name = serializers.CharField(max_length=200)
num_of_rolls = serializers.IntegerField(min_value=0)
remark = serializers.CharField(
max_length=200, required=False, allow_blank=True, allow_null=True, default=""
)
class ShipmentCreateExternalSerializer(serializers.Serializer):
"""
出货单创建序列化器external 版)
特点:
- 不绑定任何 SalesItem
- 必须提供 external_id
- 同时写入 ExternalFinishedProduct 列表并关联到 Shipment
"""
customer = serializers.IntegerField(help_text="客户ID")
shipment_date = serializers.DateField(help_text="出货日期")
address = serializers.CharField(
max_length=255,
required=False,
default="",
allow_blank=True,
help_text="地址(可选)",
)
contact_name = serializers.CharField(
max_length=100,
required=False,
default="",
allow_blank=True,
help_text="联系人(可选)",
)
contact_phone = serializers.CharField(
max_length=50,
required=False,
default="",
allow_blank=True,
help_text="联系电话(可选)",
)
area = serializers.CharField(
max_length=30,
required=False,
default="",
allow_blank=True,
help_text="出货地区(可选)",
)
remark = serializers.CharField(
required=False, default="", allow_blank=True, help_text="备注"
)
external_id = serializers.CharField(max_length=120, help_text="外部订单号(必填)")
external_finished_products = serializers.ListField(
child=ExternalFinishedProductInputSerializer(),
required=True,
help_text="外部成品表结构数组(必填)",
)
def validate_external_id(self, value):
value = (value or "").strip()
if not value:
raise serializers.ValidationError("external_id 不能为空")
return value
def validate_external_finished_products(self, value):
if not value:
raise serializers.ValidationError("external_finished_products 不能为空")
return value
class ShipmentUpdateSerializer(serializers.Serializer):
"""
出货单更新序列化器PATCH/PUT
说明:本次新增字段 area需要保证更新入口可写入并回显。
"""
customer = serializers.IntegerField(required=False, help_text="客户ID可选")
shipment_date = serializers.DateField(required=False, help_text="出货日期(可选)")
address = serializers.CharField(
max_length=255, required=False, allow_blank=True, help_text="地址(可选)"
)
contact_name = serializers.CharField(
max_length=100, required=False, allow_blank=True, help_text="联系人(可选)"
)
contact_phone = serializers.CharField(
max_length=50, required=False, allow_blank=True, help_text="联系电话(可选)"
)
area = serializers.CharField(
max_length=30, required=False, allow_blank=True, help_text="出货地区(可选)"
)
remark = serializers.CharField(
required=False, allow_blank=True, help_text="备注(可选)"
)
external_id = serializers.CharField(
max_length=120,
required=False,
allow_blank=True,
allow_null=True,
help_text="外部订单号(可选)",
)
class ShipmentStatusUpdateSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=ShipmentStatus.choices,
help_text="出货单状态1=草稿(未发布), 2=已发布, 3=已取消, 4=已驳回, 5=已审核)",
)
class SalesItemSerializer(serializers.Serializer):
"""
销售品序列化器(只读)
用于返回销售品数据
"""
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True)
quantity = serializers.DecimalField(max_digits=12, decimal_places=2, read_only=True)
unit = serializers.IntegerField(read_only=True)
unit_display = serializers.SerializerMethodField()
position = serializers.CharField(read_only=True)
remark = serializers.CharField(read_only=True)
printing_job_id = serializers.IntegerField(read_only=True)
printing_order_id = serializers.SerializerMethodField()
external_order_id = serializers.SerializerMethodField()
customer_id = serializers.IntegerField(read_only=True)
customer_name = serializers.SerializerMethodField()
shipment_id = serializers.IntegerField(
source="shipment.id", read_only=True, allow_null=True
)
shipment_date = serializers.DateField(
source="shipment.shipment_date", read_only=True, allow_null=True
)
merge_remark = serializers.JSONField(read_only=True, allow_null=True)
created_at = serializers.DateTimeField(read_only=True)
created_by_id = serializers.IntegerField(
source="created_by.id", read_only=True, allow_null=True
)
created_by_name = serializers.SerializerMethodField()
def get_unit_display(self, obj):
return obj.get_unit_display()
def get_printing_order_id(self, obj):
printing_order_map = self.context.get("printing_order_map", {})
if obj.printing_job_id in printing_order_map:
return printing_order_map[obj.printing_job_id]
printing_job = obj.get_printing_job()
if printing_job and getattr(printing_job, "printing_order_id", None):
return printing_job.printing_order_id
return None
def get_external_order_id(self, obj):
external_order_id_map = self.context.get("external_order_id_map", {})
if obj.printing_job_id in external_order_id_map:
return external_order_id_map[obj.printing_job_id]
printing_job = obj.get_printing_job()
if printing_job and getattr(printing_job.printing_order, "external_order_id", None):
return printing_job.printing_order.external_order_id
return None
def get_customer_name(self, obj):
customer_name_map = self.context.get("customer_name_map", {})
if obj.customer_id in customer_name_map:
return customer_name_map[obj.customer_id]
customer = obj.get_customer()
return customer.name if customer else None
def get_created_by_name(self, obj):
if obj.created_by:
employee = getattr(obj.created_by, "employee", None)
if employee:
return employee.name
return obj.created_by.username
return None
class SalesItemDetailSerializer(SalesItemSerializer):
"""
销售品详情序列化器。
在列表字段基础上补充关联印染任务的产品图片。
"""
product_image_url = serializers.SerializerMethodField()
def get_product_image_url(self, obj):
product_image_map = self.context.get("product_image_map", {})
if obj.printing_job_id in product_image_map:
return product_image_map[obj.printing_job_id]
printing_job = obj.get_printing_job()
if not printing_job or not getattr(printing_job, "product", None):
return None
primary_url = printing_job.product.get_primary_image_url()
if primary_url:
return primary_url
if printing_job.product.image:
request = self.context.get("request")
if request:
return request.build_absolute_uri(printing_job.product.image.url)
return printing_job.product.image.url
return None
class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
"""
由未出货销售品反推的客户摘要序列化器。
"""
customer_id = serializers.IntegerField(source="id", read_only=True)
customer_name = serializers.CharField(source="name", read_only=True)
mobile = serializers.CharField(read_only=True, allow_null=True)
area = serializers.CharField(read_only=True, allow_null=True)
unshipped_sales_items_count = serializers.IntegerField(read_only=True)
class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
model = Shipment
fields = [
"id",
"customer",
"customer_name",
"shipment_date",
"status",
"status_display",
"external_id",
]
read_only_fields = fields
class ShipmentDeliverySerializer(serializers.ModelSerializer):
status_display = serializers.CharField(source="get_status_display", read_only=True)
merchant_id = serializers.IntegerField(source="merchant.id", read_only=True)
merchant_name = serializers.CharField(source="merchant.name", read_only=True)
created_by_id = serializers.IntegerField(
source="created_by.id", read_only=True, allow_null=True
)
created_by_name = serializers.SerializerMethodField()
operator_id = serializers.IntegerField(
source="operator.id", read_only=True, allow_null=True
)
operator_name = serializers.CharField(source="operator.name", read_only=True, allow_null=True)
cancelled_by_id = serializers.IntegerField(
source="cancelled_by.id", read_only=True, allow_null=True
)
cancelled_by_name = serializers.SerializerMethodField()
shipments_count = serializers.SerializerMethodField()
shipments = serializers.SerializerMethodField()
class Meta:
model = ShipmentDelivery
fields = [
"id",
"merchant_id",
"merchant_name",
"driver_name",
"vehicle_trip",
"contact_phone",
"vehicle_capacity",
"remark",
"internal_remark",
"status",
"status_display",
"started_at",
"delivered_at",
"cancelled_at",
"shipments_count",
"shipments",
"created_by_id",
"created_by_name",
"operator_id",
"operator_name",
"cancelled_by_id",
"cancelled_by_name",
"created_at",
"updated_at",
]
read_only_fields = fields
def get_created_by_name(self, obj):
if obj.created_by:
employee = getattr(obj.created_by, "employee", None)
if employee:
return employee.name
return obj.created_by.username
return None
def get_cancelled_by_name(self, obj):
if obj.cancelled_by:
employee = getattr(obj.cancelled_by, "employee", None)
if employee:
return employee.name
return obj.cancelled_by.username
return None
def get_shipments_count(self, obj):
return obj.shipments.count()
def get_shipments(self, obj):
return ShipmentDeliveryShipmentSummarySerializer(
obj.shipments.order_by("id"),
many=True,
).data
class ShipmentDeliveryCreateSerializer(serializers.Serializer):
driver_name = serializers.CharField(max_length=100, help_text="司机名")
vehicle_trip = serializers.CharField(max_length=100, help_text="车次")
contact_phone = serializers.CharField(
max_length=50,
required=False,
default="",
allow_blank=True,
help_text="联系电话(可选)",
)
vehicle_capacity = serializers.CharField(
max_length=100,
required=False,
default="",
allow_blank=True,
help_text="车辆容量(可选)",
)
remark = serializers.CharField(
max_length=200,
required=False,
default="",
allow_blank=True,
help_text="备注(可选)",
)
internal_remark = serializers.CharField(
max_length=200,
required=False,
default="",
allow_blank=True,
help_text="内部备注(可选)",
)
shipments = serializers.ListField(
child=serializers.IntegerField(),
required=False,
default=list,
help_text="要关联的出货单ID列表",
)
def validate_shipments(self, value):
return list(dict.fromkeys(value)) if value else []
class ShipmentDeliveryUpdateSerializer(serializers.Serializer):
driver_name = serializers.CharField(
max_length=100, required=False, allow_blank=False, help_text="司机名(可选)"
)
vehicle_trip = serializers.CharField(
max_length=100, required=False, allow_blank=False, help_text="车次(可选)"
)
contact_phone = serializers.CharField(
max_length=50, required=False, allow_blank=True, help_text="联系电话(可选)"
)
vehicle_capacity = serializers.CharField(
max_length=100, required=False, allow_blank=True, help_text="车辆容量(可选)"
)
remark = serializers.CharField(
max_length=200, required=False, allow_blank=True, help_text="备注(可选)"
)
internal_remark = serializers.CharField(
max_length=200, required=False, allow_blank=True, help_text="内部备注(可选)"
)
shipments = serializers.ListField(
child=serializers.IntegerField(),
required=False,
help_text="要绑定的出货单ID列表可选传入即视为替换",
)
def validate_shipments(self, value):
return list(dict.fromkeys(value)) if value else []
class ShipmentDeliveryStatusUpdateSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=[
(ShipmentDeliveryStatus.PENDING, ShipmentDeliveryStatus.PENDING.label),
(ShipmentDeliveryStatus.IN_TRANSIT, ShipmentDeliveryStatus.IN_TRANSIT.label),
(ShipmentDeliveryStatus.DELIVERED, ShipmentDeliveryStatus.DELIVERED.label),
],
help_text="送货单状态1=待送货, 2=送货中, 3=已送达)",
)
class ShipmentDeliveryBindShipmentsSerializer(serializers.Serializer):
shipments = serializers.ListField(
child=serializers.IntegerField(),
required=True,
help_text="要追加绑定到当前送货单的出货单ID列表",
)
def validate_shipments(self, value):
if not value:
raise serializers.ValidationError("shipments 不能为空")
return list(dict.fromkeys(value))
class SalesItemCreateSerializer(serializers.Serializer):
"""
销售品创建序列化器
用于手动创建销售品(当自动转化开关关闭时使用)
"""
printing_job_id = serializers.IntegerField(
required=True, min_value=1, help_text="生产任务ID必填"
)
name = serializers.CharField(
max_length=200, required=True, help_text="销售品名称(必填)"
)
quantity = serializers.CharField(
max_length=20, required=True, help_text="数量必填支持小数100.50"
)
unit = serializers.IntegerField(
required=True, help_text="单位必填1=米, 2=件, 3=码, 4=个"
)
customer_id = serializers.IntegerField(
required=False, allow_null=True, help_text="客户ID可选默认从生产订单获取"
)
remark = serializers.CharField(
max_length=200,
required=False,
allow_blank=True,
default="",
help_text="备注(可选)",
)
position = serializers.CharField(
max_length=200,
required=False,
allow_blank=True,
default="",
help_text="货位(可选)",
)
merge_remark = serializers.JSONField(
required=False,
allow_null=True,
default=None,
help_text="合卷备注JSON可选",
)
_MERGE_REMARK_REQUIRED_KEYS = {
"merge_type": str,
"jobs": list,
"main_job": int,
"quantity": str,
"unit": str,
"job_count": int,
}
def validate_merge_remark(self, value):
"""严格校验 merge_remark 的结构"""
if value is None:
return value
if not isinstance(value, dict):
raise serializers.ValidationError("merge_remark 必须是一个 JSON 对象")
missing = set(self._MERGE_REMARK_REQUIRED_KEYS) - set(value.keys())
if missing:
raise serializers.ValidationError(f"缺少必填字段: {sorted(missing)}")
extra = set(value.keys()) - set(self._MERGE_REMARK_REQUIRED_KEYS)
if extra:
raise serializers.ValidationError(f"包含未知字段: {sorted(extra)}")
for key, expected_type in self._MERGE_REMARK_REQUIRED_KEYS.items():
if not isinstance(value[key], expected_type):
raise serializers.ValidationError(
f"字段 '{key}' 类型错误,期望 {expected_type.__name__}"
f"实际 {type(value[key]).__name__}"
)
# jobs 列表内的元素必须都是整数
if not all(isinstance(j, int) for j in value["jobs"]):
raise serializers.ValidationError("jobs 列表中的元素必须为整数")
if len(value["jobs"]) < 2:
raise serializers.ValidationError("jobs 列表至少需要包含 2 个子单ID")
if value["main_job"] not in value["jobs"]:
raise serializers.ValidationError("main_job 必须是 jobs 列表中的一个成员")
if value["job_count"] != len(value["jobs"]):
raise serializers.ValidationError(
f"job_count ({value['job_count']}) 与 jobs 长度 ({len(value['jobs'])}) 不一致"
)
return value
def validate_unit(self, value):
"""验证单位值是否在允许范围内"""
from shipment.models import UnitChoices
valid_units = [choice[0] for choice in UnitChoices.choices]
if value not in valid_units:
raise serializers.ValidationError(
f"单位值无效。可选值:{dict(UnitChoices.choices)}"
)
return value
def create(self, validated_data):
"""创建销售品"""
from shipment.services import create_sales_item
# 获取当前用户从context传入
created_by = self.context["request"].user
return create_sales_item(
printing_job_id=validated_data["printing_job_id"],
name=validated_data["name"],
quantity=validated_data["quantity"],
unit=validated_data["unit"],
created_by=created_by,
customer_id=validated_data.get("customer_id"),
remark=validated_data.get("remark", ""),
position=validated_data.get("position", ""),
merge_remark=validated_data.get("merge_remark"),
)
class SalesItemUpdateSerializer(serializers.Serializer):
"""
销售品更新序列化器。
当前仅开放数量、备注、货位的修改。
"""
quantity = serializers.CharField(
max_length=20,
required=False,
help_text="数量(可选,支持小数)",
)
remark = serializers.CharField(
max_length=200,
required=False,
allow_blank=True,
help_text="备注(可选)",
)
position = serializers.CharField(
max_length=200,
required=False,
allow_blank=True,
help_text="货位(可选)",
)
def validate(self, attrs):
if not attrs:
raise serializers.ValidationError("至少提供一个可修改字段")
return attrs
class SalesItemRebuildSerializer(serializers.Serializer):
"""
销售品重建序列化器。
"""
new_printing_job_id = serializers.IntegerField(
required=True,
min_value=1,
help_text="新的生产任务ID必填",
)
quantity = serializers.CharField(
max_length=20,
required=False,
allow_blank=False,
help_text="新的数量(可选,未传则沿用原销售品数量)",
)