1
0
forked from erp-dev/erp

feat: /api/v1/shipment/sales-items/by-printing-order/KD20135142/

This commit is contained in:
2026-03-31 19:46:53 +08:00
parent 72840b7277
commit d36d9c1d9c
4 changed files with 205 additions and 9 deletions

View File

@@ -319,7 +319,7 @@ urlpatterns = [
name="sales_items_by_customer", name="sales_items_by_customer",
), ),
path( path(
"shipment/sales-items/by-printing-order/<int:printing_order_id>/", "shipment/sales-items/by-printing-order/<str:printing_order_id>/",
SalesItemByPrintingOrderView.as_view(), SalesItemByPrintingOrderView.as_view(),
name="sales_items_by_printing_order", name="sales_items_by_printing_order",
), ),

View File

@@ -241,6 +241,56 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
self.assertEqual(item["shipment_id"], self.shipment.id) self.assertEqual(item["shipment_id"], self.shipment.id)
self.assertEqual(item["shipment_date"], "2026-01-14") 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): def test_get_sales_items_printing_order_not_found(self):
"""测试生产订单不存在""" """测试生产订单不存在"""
url = "/api/v1/shipment/sales-items/by-printing-order/99999/" url = "/api/v1/shipment/sales-items/by-printing-order/99999/"

View File

@@ -361,6 +361,7 @@ class SalesItemByPrintingOrderView(APIView):
GET /api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/ GET /api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/
返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。 返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。
路径参数既支持内部 ID也支持 external_order_id。
查询参数: 查询参数:
- include_already_has_shipment: 是否包含已关联出货单的销售品true/false默认 false - include_already_has_shipment: 是否包含已关联出货单的销售品true/false默认 false
@@ -393,12 +394,43 @@ class SalesItemByPrintingOrderView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, printing_order_id): def get(self, request, printing_order_id):
# 验证生产订单是否存在
from printing.models import PrintingOrder from printing.models import PrintingOrder
try: user = request.user
printing_order = PrintingOrder.objects.get(id=printing_order_id) if getattr(user, "is_superuser", False):
except PrintingOrder.DoesNotExist: 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( return Response(
{"detail": f"生产订单 {printing_order_id} 不存在"}, {"detail": f"生产订单 {printing_order_id} 不存在"},
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,

View File

@@ -8,6 +8,7 @@
- [创建出货单](#创建出货单) - [创建出货单](#创建出货单)
- [查询有待出货销售品的客户](#查询有待出货销售品的客户) - [查询有待出货销售品的客户](#查询有待出货销售品的客户)
- [按客户查询销售品](#按客户查询销售品) - [按客户查询销售品](#按客户查询销售品)
- [查询销售品详情](#查询销售品详情)
- [通过生产订单查询销售品](#通过生产订单查询销售品) - [通过生产订单查询销售品](#通过生产订单查询销售品)
--- ---
@@ -341,6 +342,7 @@
| 参数 | 类型 | 必填 | 默认值 | 说明 | | 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------| |------|------|------|--------|------|
| include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 | | include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 |
| external_order_id | string | 否 | - | 按生产订单外部订单号精确筛选 |
| limit | int | 否 | 800 | 分页大小 | | limit | int | 否 | 800 | 分页大小 |
| offset | int | 否 | 0 | 偏移量 | | offset | int | 否 | 0 | 偏移量 |
@@ -350,6 +352,7 @@
2. 仅查询当前商户下、`customer_id` 匹配的销售品 2. 仅查询当前商户下、`customer_id` 匹配的销售品
3. 默认只返回 `shipment` 为空的销售品(待出货) 3. 默认只返回 `shipment` 为空的销售品(待出货)
4. 可通过 `include_already_has_shipment=true` 包含已出货销售品 4. 可通过 `include_already_has_shipment=true` 包含已出货销售品
5. 可通过 `external_order_id` 按关联生产订单的外部订单号精确筛选
### 响应格式 ### 响应格式
@@ -369,6 +372,7 @@
"remark": "", "remark": "",
"printing_job_id": 88, "printing_job_id": 88,
"printing_order_id": 23, "printing_order_id": 23,
"external_order_id": "KD20135142",
"customer_id": 12, "customer_id": 12,
"customer_name": "客户A", "customer_name": "客户A",
"shipment_id": null, "shipment_id": null,
@@ -393,6 +397,97 @@
--- ---
## 查询销售品详情
查询单个销售品详情,返回列表接口中的全部字段,并额外补充关联生产任务产品图片。
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/<id>/`
- **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 | 关联的出货单IDnull 表示未出货 |
| shipment_date | string/null | 出货日期YYYY-MM-DDnull 表示未出货 |
| 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 <token>'
```
---
## 通过生产订单查询销售品 ## 通过生产订单查询销售品
查询与指定生产订单PrintingOrder关联的所有销售品SalesItem 查询与指定生产订单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 1. 先尝试将路径参数按内部生产订单ID查询
2. 查询 `SalesItem`,过滤 `printing_job_id` 在这些 job ID 中的记录 2. 若内部ID未命中则按 `external_order_id` 查询生产订单
3. 根据 `include_already_has_shipment` 参数决定是否过滤已关联出货单的销售品: 3. `external_order_id` 命中多条生产订单,则返回 `400 Bad Request`
4. 获取该生产订单下所有 `PrintingJob` 的 ID
5. 查询 `SalesItem`,过滤 `printing_job_id` 在这些 job ID 中的记录
6. 根据 `include_already_has_shipment` 参数决定是否过滤已关联出货单的销售品:
- `false`(默认):只返回 `shipment` 为空的销售品(待出货) - `false`(默认):只返回 `shipment` 为空的销售品(待出货)
- `true`:返回所有销售品(包含已出货的) - `true`:返回所有销售品(包含已出货的)
@@ -502,6 +600,14 @@
} }
``` ```
#### 400 Bad Request - external_order_id 匹配多条生产订单
```json
{
"detail": "external_order_id KD20135142 匹配到多个生产订单请改用内部ID查询"
}
```
#### 401 Unauthorized - 未登录 #### 401 Unauthorized - 未登录
```json ```json
@@ -520,6 +626,14 @@ curl -X GET \
-H 'Authorization: Bearer <token>' -H 'Authorization: Bearer <token>'
``` ```
#### 使用 external_order_id 查询
```bash
curl -X GET \
'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/KD20135142/' \
-H 'Authorization: Bearer <token>'
```
#### 查询所有销售品(包含已出货) #### 查询所有销售品(包含已出货)
```bash ```bash