diff --git a/api_v1/views/shipment/serializers.py b/api_v1/views/shipment/serializers.py index 61aee28..7938cde 100644 --- a/api_v1/views/shipment/serializers.py +++ b/api_v1/views/shipment/serializers.py @@ -3,7 +3,7 @@ Shipment API 序列化器 """ from rest_framework import serializers -from shipment.models import Shipment +from shipment.models import ExternalFinishedProduct, SalesItem, Shipment class ShipmentSerializer(serializers.ModelSerializer): @@ -20,6 +20,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) + sales_items = serializers.SerializerMethodField() + external_finished_products = serializers.SerializerMethodField() class Meta: model = Shipment @@ -31,6 +33,8 @@ class ShipmentSerializer(serializers.ModelSerializer): 'cancelled_at', 'cancelled_by_id', 'cancelled_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'] @@ -57,6 +61,51 @@ class ShipmentSerializer(serializers.ModelSerializer): def get_external_finished_products_count(self, obj): return obj.external_finished_products.count() + def get_sales_items(self, obj): + """ + 出货单关联的销售品明细(无则返回空数组)。 + """ + # 优先使用 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 + + 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', + '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): """ diff --git a/api_v1/views/shipment/test_api.py b/api_v1/views/shipment/test_api.py index e08caef..5d3b46f 100644 --- a/api_v1/views/shipment/test_api.py +++ b/api_v1/views/shipment/test_api.py @@ -638,10 +638,22 @@ class ShipmentQueryAPITestCase(TestCase): self.assertIn(self.shipment1.id, ids) self.assertNotIn(self.shipment2.id, ids) + # 关键:列表序列化器必须稳定输出明细数组字段(即使为空) + item = next(it for it in data['results'] if it['id'] == self.shipment1.id) + self.assertIn('sales_items', item) + self.assertIsInstance(item['sales_items'], list) + self.assertIn('external_finished_products', item) + self.assertIsInstance(item['external_finished_products'], list) + def test_retrieve_shipment_success(self): resp = self.client.get(f'/api/v1/shipment/shipments/{self.shipment1.id}/') self.assertEqual(resp.status_code, status.HTTP_200_OK) - self.assertEqual(resp.json()['id'], self.shipment1.id) + result = resp.json() + self.assertEqual(result['id'], self.shipment1.id) + self.assertIn('sales_items', result) + self.assertIsInstance(result['sales_items'], list) + self.assertIn('external_finished_products', result) + self.assertIsInstance(result['external_finished_products'], list) def test_retrieve_other_merchant_shipment_404(self): resp = self.client.get(f'/api/v1/shipment/shipments/{self.shipment2.id}/') diff --git a/docs/2026-01-15_summary.md b/docs/2026-01-15_summary.md index 3f87d22..0a7862c 100644 --- a/docs/2026-01-15_summary.md +++ b/docs/2026-01-15_summary.md @@ -40,5 +40,6 @@ - 补齐出货单查询接口: - `GET /api/v1/shipment/shipments/`:出货单列表(merchant 隔离 + LimitOffsetPagination) - `GET /api/v1/shipment/shipments//`:出货单详情(merchant 隔离) +- 出货单列表序列化器增强:`sales_items` 与 `external_finished_products` 字段稳定输出(无数据也返回空数组) - 更新 `docs/shipment_api.md`:补充“查询出货单(列表/详情)”说明 - 新增 API 测试:覆盖列表/详情的商户隔离与未登录行为 diff --git a/docs/shipment_api.md b/docs/shipment_api.md index c876357..5456f32 100644 --- a/docs/shipment_api.md +++ b/docs/shipment_api.md @@ -49,6 +49,8 @@ "shipment_date": "2026-01-14", "remark": "备注信息", "items_count": 3, + "sales_items": [], + "external_finished_products": [], "created_by_id": 1, "created_by_name": "张三", "created_at": "2026-01-14T10:00:00Z", @@ -108,6 +110,8 @@ "shipment_date": "2026-01-14", "remark": "备注信息", "items_count": 3, + "sales_items": [], + "external_finished_products": [], "created_by_id": 1, "created_by_name": "张三", "created_at": "2026-01-14T10:00:00Z",