From d36d9c1d9cbc436fa20d19a3ac6a8a2b5d8d4012 Mon Sep 17 00:00:00 2001 From: colaftc Date: Tue, 31 Mar 2026 19:46:53 +0800 Subject: [PATCH] feat: /api/v1/shipment/sales-items/by-printing-order/KD20135142/ --- api_v1/urls.py | 2 +- api_v1/views/shipment/test_api.py | 50 ++++++++++++ api_v1/views/shipment/views.py | 40 +++++++++- docs/shipment_api.md | 122 +++++++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 9 deletions(-) diff --git a/api_v1/urls.py b/api_v1/urls.py index 6d9c6cd..8d6d372 100644 --- a/api_v1/urls.py +++ b/api_v1/urls.py @@ -319,7 +319,7 @@ urlpatterns = [ name="sales_items_by_customer", ), path( - "shipment/sales-items/by-printing-order//", + "shipment/sales-items/by-printing-order//", SalesItemByPrintingOrderView.as_view(), name="sales_items_by_printing_order", ), diff --git a/api_v1/views/shipment/test_api.py b/api_v1/views/shipment/test_api.py index c306121..304d95f 100644 --- a/api_v1/views/shipment/test_api.py +++ b/api_v1/views/shipment/test_api.py @@ -241,6 +241,56 @@ class SalesItemByPrintingOrderAPITestCase(TestCase): self.assertEqual(item["shipment_id"], self.shipment.id) self.assertEqual(item["shipment_date"], "2026-01-14") + def test_get_sales_items_by_external_order_id(self): + """测试通过 external_order_id 查询销售品""" + url = "/api/v1/shipment/sales-items/by-printing-order/EXT-PO-001/" + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + + self.assertEqual(data["count"], 2) + item_ids = [item["id"] for item in data["results"]] + self.assertIn(self.sales_item1.id, item_ids) + self.assertIn(self.sales_item2.id, item_ids) + self.assertNotIn(self.sales_item3.id, item_ids) + + def test_get_sales_items_by_external_order_id_multiple_matches(self): + """测试 external_order_id 匹配多个生产订单时报错""" + other_order = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric="测试面料3", + width="160cm", + process=self.process, + created_by=self.user, + external_order_id="EXT-DUPLICATED", + ) + printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric="测试面料4", + width="170cm", + process=self.process, + created_by=self.user, + external_order_id="EXT-DUPLICATED", + ) + printing_models.PrintingJob.objects.create( + merchant=self.merchant, + printing_order=other_order, + product=self.product, + quantity=50, + unit="米", + created_by=self.user, + ) + + response = self.client.get( + "/api/v1/shipment/sales-items/by-printing-order/EXT-DUPLICATED/" + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("匹配到多个生产订单", response.json()["detail"]) + def test_get_sales_items_printing_order_not_found(self): """测试生产订单不存在""" url = "/api/v1/shipment/sales-items/by-printing-order/99999/" diff --git a/api_v1/views/shipment/views.py b/api_v1/views/shipment/views.py index 01a257b..d614ebd 100644 --- a/api_v1/views/shipment/views.py +++ b/api_v1/views/shipment/views.py @@ -361,6 +361,7 @@ class SalesItemByPrintingOrderView(APIView): GET /api/v1/shipment/sales-items/by-printing-order// 返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。 + 路径参数既支持内部 ID,也支持 external_order_id。 查询参数: - include_already_has_shipment: 是否包含已关联出货单的销售品(true/false),默认 false @@ -393,12 +394,43 @@ class SalesItemByPrintingOrderView(APIView): permission_classes = [IsAuthenticated] def get(self, request, printing_order_id): - # 验证生产订单是否存在 from printing.models import PrintingOrder - try: - printing_order = PrintingOrder.objects.get(id=printing_order_id) - except PrintingOrder.DoesNotExist: + user = request.user + if getattr(user, "is_superuser", False): + base_queryset = PrintingOrder.objects.all() + else: + emp = getattr(user, "employee", None) + merchant = getattr(emp, "merchant", None) if emp else None + if not merchant: + return Response( + {"detail": f"生产订单 {printing_order_id} 不存在"}, + status=status.HTTP_404_NOT_FOUND, + ) + base_queryset = PrintingOrder.objects.filter(merchant=merchant) + + printing_order = None + if printing_order_id.isdigit(): + printing_order = base_queryset.filter(id=int(printing_order_id)).first() + + if printing_order is None: + matched_orders = list( + base_queryset.filter(external_order_id=printing_order_id).only("id")[:2] + ) + if len(matched_orders) > 1: + return Response( + { + "detail": ( + f"external_order_id {printing_order_id} 匹配到多个生产订单," + "请改用内部ID查询" + ) + }, + status=status.HTTP_400_BAD_REQUEST, + ) + if len(matched_orders) == 1: + printing_order = matched_orders[0] + + if printing_order is None: return Response( {"detail": f"生产订单 {printing_order_id} 不存在"}, status=status.HTTP_404_NOT_FOUND, diff --git a/docs/shipment_api.md b/docs/shipment_api.md index 01eb285..22fde67 100644 --- a/docs/shipment_api.md +++ b/docs/shipment_api.md @@ -8,6 +8,7 @@ - [创建出货单](#创建出货单) - [查询有待出货销售品的客户](#查询有待出货销售品的客户) - [按客户查询销售品](#按客户查询销售品) +- [查询销售品详情](#查询销售品详情) - [通过生产订单查询销售品](#通过生产订单查询销售品) --- @@ -341,6 +342,7 @@ | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 | +| external_order_id | string | 否 | - | 按生产订单外部订单号精确筛选 | | limit | int | 否 | 800 | 分页大小 | | offset | int | 否 | 0 | 偏移量 | @@ -350,6 +352,7 @@ 2. 仅查询当前商户下、`customer_id` 匹配的销售品 3. 默认只返回 `shipment` 为空的销售品(待出货) 4. 可通过 `include_already_has_shipment=true` 包含已出货销售品 +5. 可通过 `external_order_id` 按关联生产订单的外部订单号精确筛选 ### 响应格式 @@ -369,6 +372,7 @@ "remark": "", "printing_job_id": 88, "printing_order_id": 23, + "external_order_id": "KD20135142", "customer_id": 12, "customer_name": "客户A", "shipment_id": null, @@ -393,6 +397,97 @@ --- +## 查询销售品详情 + +查询单个销售品详情,返回列表接口中的全部字段,并额外补充关联生产任务产品图片。 + +### 接口信息 + +- **URL**: `/api/v1/shipment/sales-items//` +- **Method**: `GET` +- **认证**: 需要登录(JWT Token) + +### 路径参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 销售品ID | + +### 业务逻辑 + +1. 仅允许查询当前商户下的销售品 +2. 返回销售品基础信息、关联客户信息、关联出货信息 +3. 额外返回关联 `PrintingJob` 产品图 URL:`product_image_url` +4. 若无关联图片,则 `product_image_url` 返回 `null` + +### 响应格式 + +```json +{ + "id": 101, + "name": "赛扬 190g", + "quantity": "1200.00", + "unit": 1, + "unit_display": "米", + "position": "A1-01", + "remark": "", + "printing_job_id": 88, + "printing_order_id": 23, + "external_order_id": "KD20135142", + "customer_id": 12, + "customer_name": "客户A", + "shipment_id": null, + "shipment_date": null, + "created_at": "2026-03-28T10:00:00Z", + "created_by_id": 1, + "created_by_name": "张三", + "product_image_url": "https://cdn.example.com/products/88/main.jpg" +} +``` + +### 响应字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 销售品ID | +| name | string | 销售品名称 | +| quantity | string | 数量(Decimal,保留2位小数) | +| unit | int | 单位编码(1=米, 2=件, 3=码, 4=个) | +| unit_display | string | 单位显示名称 | +| position | string | 货位(可能为空) | +| remark | string | 备注(可能为空) | +| printing_job_id | int/null | 关联的生产任务ID | +| printing_order_id | int/null | 关联的生产订单ID | +| external_order_id | string/null | 关联生产订单的外部订单号 | +| customer_id | int/null | 销售品关联客户ID | +| customer_name | string/null | 销售品关联客户名称 | +| shipment_id | int/null | 关联的出货单ID,null 表示未出货 | +| shipment_date | string/null | 出货日期(YYYY-MM-DD),null 表示未出货 | +| created_at | string | 创建时间(ISO 8601) | +| created_by_id | int/null | 创建人ID | +| created_by_name | string/null | 创建人名称 | +| product_image_url | string/null | 关联产品主图 URL,无图时为 `null` | + +### 错误响应 + +#### 404 Not Found - 销售品不存在或无权限 + +```json +{ + "detail": "销售品 999 不存在" +} +``` + +### 使用示例 + +```bash +curl -X GET \ + 'https://api.example.com/api/v1/shipment/sales-items/101/' \ + -H 'Authorization: Bearer ' +``` + +--- + ## 通过生产订单查询销售品 查询与指定生产订单(PrintingOrder)关联的所有销售品(SalesItem)。 @@ -407,7 +502,7 @@ | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| -| printing_order_id | int | 是 | 生产订单ID | +| printing_order_id | string | 是 | 生产订单内部ID,或 `external_order_id` | ### 查询参数 @@ -417,9 +512,12 @@ ### 业务逻辑 -1. 根据 `printing_order_id` 获取该生产订单下所有 `PrintingJob` 的 ID -2. 查询 `SalesItem`,过滤 `printing_job_id` 在这些 job ID 中的记录 -3. 根据 `include_already_has_shipment` 参数决定是否过滤已关联出货单的销售品: +1. 先尝试将路径参数按内部生产订单ID查询 +2. 若内部ID未命中,则按 `external_order_id` 查询生产订单 +3. 若 `external_order_id` 命中多条生产订单,则返回 `400 Bad Request` +4. 获取该生产订单下所有 `PrintingJob` 的 ID +5. 查询 `SalesItem`,过滤 `printing_job_id` 在这些 job ID 中的记录 +6. 根据 `include_already_has_shipment` 参数决定是否过滤已关联出货单的销售品: - `false`(默认):只返回 `shipment` 为空的销售品(待出货) - `true`:返回所有销售品(包含已出货的) @@ -502,6 +600,14 @@ } ``` +#### 400 Bad Request - external_order_id 匹配多条生产订单 + +```json +{ + "detail": "external_order_id KD20135142 匹配到多个生产订单,请改用内部ID查询" +} +``` + #### 401 Unauthorized - 未登录 ```json @@ -520,6 +626,14 @@ curl -X GET \ -H 'Authorization: Bearer ' ``` +#### 使用 external_order_id 查询 + +```bash +curl -X GET \ + 'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/KD20135142/' \ + -H 'Authorization: Bearer ' +``` + #### 查询所有销售品(包含已出货) ```bash