1
0
forked from erp-dev/erp

feat: added some filed to shipment serializers

This commit is contained in:
2026-04-23 17:51:48 +08:00
parent 35a03218d8
commit d4fbb2c7fe
11 changed files with 287 additions and 1 deletions

View File

@@ -62,6 +62,48 @@ def _build_nested_sales_item_context(items):
}
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
return printing_job.printing_order.description
class ShipmentSerializer(serializers.ModelSerializer):
"""
出货单序列化器(只读,用于返回数据)
@@ -85,6 +127,8 @@ class ShipmentSerializer(serializers.ModelSerializer):
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()
@@ -97,6 +141,8 @@ class ShipmentSerializer(serializers.ModelSerializer):
"merchant_name",
"customer",
"customer_name",
"fabric",
"order_description",
"shipment_date",
"address",
"contact_name",
@@ -132,9 +178,15 @@ class ShipmentSerializer(serializers.ModelSerializer):
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_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)
@@ -494,6 +546,8 @@ class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
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:
@@ -502,6 +556,8 @@ class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer):
"id",
"customer",
"customer_name",
"fabric",
"order_description",
"shipment_date",
"status",
"status_display",
@@ -510,6 +566,12 @@ class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer):
]
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)
@@ -542,6 +604,7 @@ class ShipmentDeliverySerializer(serializers.ModelSerializer):
"vehicle_capacity",
"remark",
"internal_remark",
"shipment_order_ids",
"status",
"status_display",
"started_at",
@@ -617,6 +680,12 @@ class ShipmentDeliveryCreateSerializer(serializers.Serializer):
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,
@@ -627,6 +696,11 @@ class ShipmentDeliveryCreateSerializer(serializers.Serializer):
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(
@@ -647,6 +721,11 @@ class ShipmentDeliveryUpdateSerializer(serializers.Serializer):
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,
@@ -656,6 +735,11 @@ class ShipmentDeliveryUpdateSerializer(serializers.Serializer):
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(

View File

@@ -1633,6 +1633,7 @@ class ShipmentQueryAPITestCase(TestCase):
process=self.process1,
created_by=self.user1,
external_order_id="QUERY-PO-001",
description="查询订单备注",
)
self.printing_job1 = printing_models.PrintingJob.objects.create(
merchant=self.merchant1,
@@ -1692,6 +1693,8 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn("area", item)
self.assertIn("delivery_id", item)
self.assertIsNone(item["delivery_id"])
self.assertEqual(item["fabric"], "查询面料")
self.assertEqual(item["order_description"], "查询订单备注")
self.assertEqual(item["area"], "A1")
self.assertEqual(item["status"], shipment_models.ShipmentStatus.DRAFT)
self.assertEqual(item["status_display"], "草稿(未发布)")
@@ -1837,6 +1840,8 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn("contact_phone", result)
self.assertIn("delivery_id", result)
self.assertIsNone(result["delivery_id"])
self.assertEqual(result["fabric"], "查询面料")
self.assertEqual(result["order_description"], "查询订单备注")
self.assertEqual(result["id"], self.shipment1.id)
self.assertIn("area", result)
self.assertEqual(result["area"], "A1")
@@ -1949,6 +1954,25 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn("remark", target)
self.assertEqual(target["remark"], "X备注")
def test_list_shipments_without_sales_items_returns_null_fabric(self):
shipment_without_sales_items = shipment_models.Shipment.objects.create(
merchant=self.merchant1,
customer=self.customer1,
shipment_date="2026-01-20",
created_by=self.user1,
remark="no-items",
area="A5",
)
resp = self.client.get("/api/v1/shipment/shipments/")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
item = next(it for it in resp.json()["results"] if it["id"] == shipment_without_sales_items.id)
self.assertIn("fabric", item)
self.assertIsNone(item["fabric"])
self.assertIn("order_description", item)
self.assertIsNone(item["order_description"])
def test_retrieve_other_merchant_shipment_404(self):
resp = self.client.get(f"/api/v1/shipment/shipments/{self.shipment2.id}/")
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
@@ -2697,6 +2721,7 @@ class ShipmentDeliveryAPITestCase(APITestCase):
"vehicle_capacity": "9.6米厢车",
"remark": "先装车",
"internal_remark": "注意对账",
"shipment_order_ids": [self.shipment2.id, self.shipment1.id],
"shipments": [self.shipment1.id, self.shipment2.id],
},
format="json",
@@ -2710,6 +2735,7 @@ class ShipmentDeliveryAPITestCase(APITestCase):
self.assertEqual(data["vehicle_capacity"], "9.6米厢车")
self.assertEqual(data["remark"], "先装车")
self.assertEqual(data["internal_remark"], "注意对账")
self.assertEqual(data["shipment_order_ids"], [self.shipment2.id, self.shipment1.id])
self.assertEqual(data["status"], shipment_models.ShipmentDeliveryStatus.PENDING)
self.assertEqual(data["shipments_count"], 2)
self.assertEqual(data["merchant_id"], self.merchant.id)
@@ -2722,6 +2748,34 @@ class ShipmentDeliveryAPITestCase(APITestCase):
self.assertIsNotNone(self.shipment1.delivery_id)
self.assertEqual(self.shipment1.delivery_id, self.shipment2.delivery_id)
def test_create_delivery_defaults_shipment_order_ids_to_empty_list(self):
resp = self.client.post(
"/api/v1/shipment/deliveries/",
{
"driver_name": "张司机",
"vehicle_trip": "KD-001-A",
"shipments": [self.shipment1.id],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
self.assertEqual(resp.json()["shipment_order_ids"], [])
def test_create_delivery_rejects_non_array_shipment_order_ids(self):
resp = self.client.post(
"/api/v1/shipment/deliveries/",
{
"driver_name": "张司机",
"vehicle_trip": "KD-001-B",
"shipment_order_ids": {"bad": "value"},
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("shipment_order_ids", resp.json())
def test_create_delivery_rejects_shipment_bound_to_other_delivery(self):
resp = self.client.post(
"/api/v1/shipment/deliveries/",
@@ -2817,6 +2871,11 @@ class ShipmentDeliveryAPITestCase(APITestCase):
for item in data["shipments"]:
self.assertIn("delivery_id", item)
self.assertEqual(item["delivery_id"], delivery.id)
self.assertIn("fabric", item)
self.assertIn("order_description", item)
first_shipment = next(item for item in data["shipments"] if item["id"] == self.shipment1.id)
self.assertIsNone(first_shipment["fabric"])
self.assertIsNone(first_shipment["order_description"])
def test_patch_delivery_updates_fields_and_replaces_shipments(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
@@ -2837,6 +2896,7 @@ class ShipmentDeliveryAPITestCase(APITestCase):
"vehicle_capacity": "13米高栏",
"remark": "改派车辆",
"internal_remark": "已电话确认",
"shipment_order_ids": [self.shipment2.id],
"shipments": [self.shipment2.id],
},
format="json",
@@ -2850,6 +2910,7 @@ class ShipmentDeliveryAPITestCase(APITestCase):
self.assertEqual(data["vehicle_capacity"], "13米高栏")
self.assertEqual(data["remark"], "改派车辆")
self.assertEqual(data["internal_remark"], "已电话确认")
self.assertEqual(data["shipment_order_ids"], [self.shipment2.id])
self.assertEqual(data["operator_id"], self.employee.id)
self.assertEqual(data["shipments_count"], 1)
self.assertEqual(data["shipments"][0]["id"], self.shipment2.id)
@@ -2859,6 +2920,45 @@ class ShipmentDeliveryAPITestCase(APITestCase):
self.assertIsNone(self.shipment1.delivery_id)
self.assertEqual(self.shipment2.delivery_id, delivery.id)
def test_patch_delivery_allows_null_shipment_order_ids(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant,
driver_name="张司机",
vehicle_trip="KD-005-N",
shipment_order_ids=[self.shipment1.id],
created_by=self.user,
)
resp = self.client.patch(
f"/api/v1/shipment/deliveries/{delivery.id}/",
{
"shipment_order_ids": None,
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertIsNone(resp.json()["shipment_order_ids"])
def test_patch_delivery_rejects_non_array_shipment_order_ids(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant,
driver_name="张司机",
vehicle_trip="KD-005-C",
created_by=self.user,
)
resp = self.client.patch(
f"/api/v1/shipment/deliveries/{delivery.id}/",
{
"shipment_order_ids": "not-a-list",
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("shipment_order_ids", resp.json())
def test_modify_delivery_status_sets_timestamps(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant,

View File

@@ -423,6 +423,7 @@ class ShipmentDeliveryListCreateView(ListModelMixin, GenericAPIView):
contact_phone=serializer.validated_data.get("contact_phone", ""),
vehicle_capacity=serializer.validated_data.get("vehicle_capacity", ""),
shipment_ids=serializer.validated_data.get("shipments", []),
shipment_order_ids=serializer.validated_data.get("shipment_order_ids", []),
created_by=request.user,
remark=serializer.validated_data.get("remark", ""),
internal_remark=serializer.validated_data.get("internal_remark", ""),
@@ -483,7 +484,7 @@ class ShipmentDeliveryDetailView(RetrieveModelMixin, GenericAPIView):
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import update_shipment_delivery
from shipment.services import UNSET, update_shipment_delivery
try:
delivery = update_shipment_delivery(
@@ -493,6 +494,11 @@ class ShipmentDeliveryDetailView(RetrieveModelMixin, GenericAPIView):
contact_phone=serializer.validated_data.get("contact_phone"),
vehicle_capacity=serializer.validated_data.get("vehicle_capacity"),
shipment_ids=serializer.validated_data.get("shipments"),
shipment_order_ids=(
serializer.validated_data["shipment_order_ids"]
if "shipment_order_ids" in serializer.validated_data
else UNSET
),
remark=serializer.validated_data.get("remark"),
internal_remark=serializer.validated_data.get("internal_remark"),
operator=request.user,