forked from erp-dev/erp
1110 lines
37 KiB
Python
1110 lines
37 KiB
Python
"""
|
||
Shipment API 序列化器
|
||
"""
|
||
|
||
from rest_framework import serializers
|
||
|
||
from shipment.models import (
|
||
ExternalFinishedProduct,
|
||
SalesItem,
|
||
Shipment,
|
||
ShipmentStatus,
|
||
ShipmentDelivery,
|
||
ShipmentDeliveryStatus,
|
||
)
|
||
|
||
|
||
SHIPMENT_STAGE_MISSING_ADDRESS = "missing_address"
|
||
SHIPMENT_STAGE_DELIVERABLE = "deliverable"
|
||
SHIPMENT_STAGE_SCHEDULED = "scheduled"
|
||
SHIPMENT_STAGE_DELIVERED = "delivered"
|
||
SHIPMENT_STAGE_CANCELLED = "cancelled"
|
||
|
||
SHIPMENT_STAGE_DISPLAY = {
|
||
SHIPMENT_STAGE_MISSING_ADDRESS: "待补地址",
|
||
SHIPMENT_STAGE_DELIVERABLE: "可送货",
|
||
SHIPMENT_STAGE_SCHEDULED: "已排车",
|
||
SHIPMENT_STAGE_DELIVERED: "已送达",
|
||
SHIPMENT_STAGE_CANCELLED: "已取消",
|
||
}
|
||
|
||
|
||
def resolve_shipment_stage(shipment: Shipment) -> str | None:
|
||
if shipment.status == ShipmentStatus.CANCELLED:
|
||
return SHIPMENT_STAGE_CANCELLED
|
||
|
||
delivery = getattr(shipment, "delivery", None)
|
||
if delivery is not None:
|
||
if delivery.status == ShipmentDeliveryStatus.DELIVERED:
|
||
return SHIPMENT_STAGE_DELIVERED
|
||
if delivery.status in {
|
||
ShipmentDeliveryStatus.PENDING,
|
||
ShipmentDeliveryStatus.IN_TRANSIT,
|
||
}:
|
||
return SHIPMENT_STAGE_SCHEDULED
|
||
return None
|
||
|
||
if (shipment.address or "").strip():
|
||
return SHIPMENT_STAGE_DELIVERABLE
|
||
return SHIPMENT_STAGE_MISSING_ADDRESS
|
||
|
||
|
||
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,
|
||
}
|
||
|
||
|
||
def _resolve_shipment_fabric(shipment: Shipment) -> str | None:
|
||
rel = getattr(shipment, "items", None)
|
||
if rel is None:
|
||
return None
|
||
|
||
first_item = rel.filter(delete_at__isnull=True).order_by("id").first()
|
||
if first_item is None or not first_item.printing_job_id:
|
||
return None
|
||
|
||
from printing.models import PrintingJob
|
||
|
||
printing_job = (
|
||
PrintingJob.objects.filter(id=first_item.printing_job_id)
|
||
.select_related("printing_order")
|
||
.first()
|
||
)
|
||
if printing_job is None or printing_job.printing_order is None:
|
||
return None
|
||
return printing_job.printing_order.fabric
|
||
|
||
|
||
def _resolve_shipment_order_description(shipment: Shipment) -> str | None:
|
||
rel = getattr(shipment, "items", None)
|
||
if rel is None:
|
||
return None
|
||
|
||
first_item = rel.filter(delete_at__isnull=True).order_by("id").first()
|
||
if first_item is None or not first_item.printing_job_id:
|
||
return None
|
||
|
||
from printing.models import PrintingJob
|
||
|
||
printing_job = (
|
||
PrintingJob.objects.filter(id=first_item.printing_job_id)
|
||
.select_related("printing_order")
|
||
.first()
|
||
)
|
||
if printing_job is None or printing_job.printing_order is None:
|
||
return None
|
||
# 目前滚筒预警字段被放在了 printing_order 上,暂时沿用这个字段来返回订单描述;
|
||
return printing_job.printing_order.rolling_warn
|
||
|
||
|
||
class ShipmentSerializer(serializers.ModelSerializer):
|
||
"""
|
||
出货单序列化器(只读,用于返回数据)
|
||
"""
|
||
|
||
customer_name = serializers.CharField(source="customer.name", read_only=True)
|
||
address_id = serializers.IntegerField(
|
||
source="customer_address.id", read_only=True, allow_null=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)
|
||
shipment_stage = serializers.SerializerMethodField()
|
||
shipment_stage_display = serializers.SerializerMethodField()
|
||
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)
|
||
fabric = serializers.SerializerMethodField()
|
||
order_description = serializers.SerializerMethodField()
|
||
delivery_id = serializers.IntegerField(read_only=True, allow_null=True)
|
||
sales_items = serializers.SerializerMethodField()
|
||
external_finished_products = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Shipment
|
||
fields = [
|
||
"id",
|
||
"merchant_id",
|
||
"merchant_name",
|
||
"customer",
|
||
"customer_name",
|
||
"address_id",
|
||
"fabric",
|
||
"order_description",
|
||
"shipment_date",
|
||
"address",
|
||
"contact_name",
|
||
"contact_phone",
|
||
"area",
|
||
"coordinates",
|
||
"remark",
|
||
"status",
|
||
"status_display",
|
||
"shipment_stage",
|
||
"shipment_stage_display",
|
||
"external_id",
|
||
"geo_coordinates",
|
||
"extra",
|
||
"delivery_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_fabric(self, obj):
|
||
return _resolve_shipment_fabric(obj)
|
||
|
||
def get_items_count(self, obj):
|
||
return obj.items.filter(delete_at__isnull=True).count()
|
||
|
||
def get_shipment_stage(self, obj):
|
||
return resolve_shipment_stage(obj)
|
||
|
||
def get_shipment_stage_display(self, obj):
|
||
stage = resolve_shipment_stage(obj)
|
||
return SHIPMENT_STAGE_DISPLAY.get(stage)
|
||
|
||
def get_order_description(self, obj):
|
||
return _resolve_shipment_order_description(obj)
|
||
|
||
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="地址(可选)",
|
||
)
|
||
address_id = serializers.IntegerField(
|
||
required=False,
|
||
allow_null=True,
|
||
min_value=1,
|
||
help_text="客户地址ID(可选)",
|
||
)
|
||
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=100,
|
||
required=False,
|
||
default="",
|
||
allow_blank=True,
|
||
help_text="出货地区(可选)",
|
||
)
|
||
coordinates = serializers.CharField(
|
||
max_length=100,
|
||
required=False,
|
||
allow_blank=True,
|
||
allow_null=True,
|
||
default=None,
|
||
help_text="经纬度字符串(可选)",
|
||
)
|
||
extra = serializers.JSONField(
|
||
required=False,
|
||
allow_null=True,
|
||
default=None,
|
||
help_text="扩展信息(可选)",
|
||
)
|
||
remark = serializers.CharField(
|
||
required=False, default="", allow_blank=True, help_text="备注"
|
||
)
|
||
status = serializers.ChoiceField(
|
||
choices=ShipmentStatus.choices,
|
||
required=False,
|
||
default=ShipmentStatus.DRAFT,
|
||
help_text="出货单状态(可选,默认 1=草稿)",
|
||
)
|
||
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=100,
|
||
required=False,
|
||
default="",
|
||
allow_blank=True,
|
||
help_text="出货地区(可选)",
|
||
)
|
||
coordinates = serializers.CharField(
|
||
max_length=100,
|
||
required=False,
|
||
allow_blank=True,
|
||
allow_null=True,
|
||
default=None,
|
||
help_text="经纬度字符串(可选)",
|
||
)
|
||
extra = serializers.JSONField(
|
||
required=False,
|
||
allow_null=True,
|
||
default=None,
|
||
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=100, required=False, allow_blank=True, help_text="出货地区(可选)"
|
||
)
|
||
coordinates = serializers.CharField(
|
||
max_length=100,
|
||
required=False,
|
||
allow_blank=True,
|
||
allow_null=True,
|
||
help_text="经纬度字符串(可选)",
|
||
)
|
||
extra = serializers.JSONField(
|
||
required=False,
|
||
allow_null=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="外部订单号(可选)",
|
||
)
|
||
geo_coordinates = serializers.JSONField(
|
||
required=False,
|
||
allow_null=True,
|
||
help_text="Geo 系统返回的坐标信息(可空,结构不作校验)",
|
||
)
|
||
|
||
|
||
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)
|
||
fabric = serializers.SerializerMethodField()
|
||
order_description = serializers.SerializerMethodField()
|
||
delivery_id = serializers.IntegerField(read_only=True, allow_null=True)
|
||
|
||
class Meta:
|
||
model = Shipment
|
||
fields = [
|
||
"id",
|
||
"customer",
|
||
"customer_name",
|
||
"fabric",
|
||
"order_description",
|
||
"shipment_date",
|
||
"status",
|
||
"status_display",
|
||
"external_id",
|
||
"delivery_id",
|
||
]
|
||
read_only_fields = fields
|
||
|
||
def get_fabric(self, obj):
|
||
return _resolve_shipment_fabric(obj)
|
||
|
||
def get_order_description(self, obj):
|
||
return _resolve_shipment_order_description(obj)
|
||
|
||
|
||
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",
|
||
"shipment_order_ids",
|
||
"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 ShipmentDeliveryByPrintingOrderSerializer(serializers.ModelSerializer):
|
||
"""
|
||
按生产订单查询送货单的专用 DTO。
|
||
|
||
当前字段基本对齐送货单列表,后续可按该查询场景独立调整。
|
||
"""
|
||
|
||
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)
|
||
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",
|
||
"shipment_order_ids",
|
||
"status",
|
||
"status_display",
|
||
"started_at",
|
||
"delivered_at",
|
||
"cancelled_at",
|
||
"shipments_count",
|
||
"shipments",
|
||
"created_by_id",
|
||
"created_by_name",
|
||
"operator_id",
|
||
"operator_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_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="内部备注(可选)",
|
||
)
|
||
shipment_order_ids = serializers.JSONField(
|
||
required=False,
|
||
allow_null=True,
|
||
default=list,
|
||
help_text="前端自管的出货单顺序 ID 数组(可选,可为 null)",
|
||
)
|
||
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 []
|
||
|
||
def validate_shipment_order_ids(self, value):
|
||
if value is not None and not isinstance(value, list):
|
||
raise serializers.ValidationError("shipment_order_ids 必须是数组或 null")
|
||
return value
|
||
|
||
|
||
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="内部备注(可选)"
|
||
)
|
||
shipment_order_ids = serializers.JSONField(
|
||
required=False,
|
||
allow_null=True,
|
||
help_text="前端自管的出货单顺序 ID 数组(可选,可为 null)",
|
||
)
|
||
shipments = serializers.ListField(
|
||
child=serializers.IntegerField(),
|
||
required=False,
|
||
help_text="要绑定的出货单ID列表(可选,传入即视为替换)",
|
||
)
|
||
|
||
def validate_shipments(self, value):
|
||
return list(dict.fromkeys(value)) if value else []
|
||
|
||
def validate_shipment_order_ids(self, value):
|
||
if value is not None and not isinstance(value, list):
|
||
raise serializers.ValidationError("shipment_order_ids 必须是数组或 null")
|
||
return value
|
||
|
||
|
||
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="新的数量(可选,未传则沿用原销售品数量)",
|
||
)
|