1
0
forked from erp-dev/erp

feat: shipment_delivery

This commit is contained in:
2026-04-03 16:55:22 +08:00
parent 371cd796e7
commit 76b30cf4f4
16 changed files with 2513 additions and 9 deletions

View File

@@ -4,7 +4,61 @@ Shipment API 序列化器
from rest_framework import serializers
from shipment.models import ExternalFinishedProduct, SalesItem, Shipment
from shipment.models import (
ExternalFinishedProduct,
SalesItem,
Shipment,
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):
@@ -42,6 +96,9 @@ class ShipmentSerializer(serializers.ModelSerializer):
"customer",
"customer_name",
"shipment_date",
"address",
"contact_name",
"contact_phone",
"area",
"remark",
"status",
@@ -100,7 +157,13 @@ class ShipmentSerializer(serializers.ModelSerializer):
# 优先使用 prefetch 的 related manager兜底为 none()
rel = getattr(obj, "items", None)
items = list(rel.all()) if rel is not None else []
return SalesItemSerializer(items, many=True).data
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):
"""
@@ -150,6 +213,27 @@ 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,
@@ -194,6 +278,27 @@ class ShipmentCreateExternalSerializer(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,
@@ -232,6 +337,15 @@ class ShipmentUpdateSerializer(serializers.Serializer):
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="出货地区(可选)"
)
@@ -360,6 +474,194 @@ class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
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):
"""
销售品创建序列化器