1
0
forked from erp-dev/erp
Files
erpnew/docs/shipment_api.md

846 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Shipment API 文档
出货管理模块 API 文档,包含出货单和销售品相关接口。
## 目录
- [查询出货单](#查询出货单)
- [出货单状态流转接口](./shipment_status_api.md)
- [销售品删除接口](./sales_item_delete_api.md)
- [创建出货单](#创建出货单)
- [查询有待出货销售品的客户](#查询有待出货销售品的客户)
- [按客户查询销售品](#按客户查询销售品)
- [查询销售品详情](#查询销售品详情)
- [通过生产订单查询销售品](#通过生产订单查询销售品)
---
## 查询出货单
### 出货单列表
- **URL**: `/api/v1/shipment/shipments/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
#### 查询参数(可选)
| 参数 | 类型 | 说明 |
|------|------|------|
| limit | int | 分页大小LimitOffsetPagination |
| offset | int | 偏移量 |
| customer | int | 客户ID |
| status | int | 状态1=草稿(未发布), 2=已发布, 3=已取消, 4=已驳回, 5=已审核) |
| delivery_id | int | 送货单ID。仅当传入具体ID时过滤`null``None`、空字符串时不触发过滤 |
| delivery_isnull | bool | 是否按送货单是否为空过滤。传 `true/1/yes` 仅返回 `delivery_id = null`;传 `false/0/no` 仅返回 `delivery_id != null` |
| external_id | string | 外部订单号(精确匹配) |
| shipment_date_from | string | 出货日期起始YYYY-MM-DD |
| shipment_date_to | string | 出货日期结束YYYY-MM-DD |
#### 响应格式
使用 LimitOffsetPagination
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 1,
"customer_name": "客户A",
"fabric": "棉布 40S",
"order_description": "滚筒预警",
"shipment_date": "2026-01-14",
"status": 1,
"status_display": "草稿(未发布)",
"delivery_id": null,
"status_modified_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
"approved_by_id": null,
"approved_by_name": null,
"remark": "备注信息",
"items_count": 3,
"sales_items": [],
"external_finished_products": [],
"created_by_id": 1,
"created_by_name": "张三",
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
]
}
```
说明:
- `sales_items` 现在返回的是带图片字段的销售品详情结构
- `delivery_id` 始终返回送货单ID未绑定送货单时为 `null`
- `fabric` 来自该出货单所关联销售品对应生产订单的 `fabric`;若当前出货单没有销售品,则返回 `null`
- `order_description` 当前返回该出货单所关联销售品对应生产订单的 `rolling_warn`;若当前出货单没有销售品,则返回 `null`
- `delivery_id=null` 不会触发过滤;如需按是否绑定送货单筛选,请使用 `delivery_isnull`
- 若销售品关联的 `PrintingJob.product` 存在主图,则会返回 `product_image_url`
- 若无关联图片,则 `product_image_url``null`
- 若出货单已经被驳回,则其销售品会在驳回时被自动解绑,因此此处的 `sales_items` 会变为空数组
### 出货单详情
- **URL**: `/api/v1/shipment/shipments/<id>/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
#### 查询参数(可选)
| 参数 | 类型 | 说明 |
|------|------|------|
| status | int | 可选状态过滤;取值为 1=草稿(未发布), 2=已发布, 3=已取消, 4=已驳回, 5=已审核。传入后仅当该出货单状态匹配时才返回详情 |
说明:
- 详情响应同样始终包含 `delivery_id`;未绑定送货单时返回 `null`
- 详情中的 `sales_items` 同样返回带 `product_image_url` 的销售品详情结构
---
### 更新出货单
- **URL**: `/api/v1/shipment/shipments/<id>/`
- **Method**: `PATCH` / `PUT`
- **认证**: 需要登录JWT Token
说明:
- 仅允许修改业务数据字段:`customer``shipment_date``address``contact_name``contact_phone``area``remark``external_id`
- 该接口不允许修改 `status`
- 仅“未关联送货单”的出货单允许修改
- 一旦出货单已关联送货单,会返回 `400 Bad Request`
请求体示例:
```json
{
"address": "宁波市滨海路 9 号",
"contact_name": "王五",
"contact_phone": "13700137000",
"area": "更新地区",
"remark": "更新备注"
}
```
错误示例:
```json
{
"detail": "已关联送货单的出货单不允许修改"
}
```
---
## 创建出货单
创建出货单并关联销售品。
业务规则补充:
-`sales_items` 非空时,所有销售品都必须关联有效的 `printing_job`
- 并且这些销售品必须全部来自同一个 `printing_order`
- 如果存在缺少生产任务、生产任务不存在、或跨生产订单混装,接口会拒绝创建
- 新创建的出货单默认状态为 `草稿(未发布)`
- 创建成功后会触发 `shipment_created` 领域信号,并通过异步 Celery task 发送企业微信通知
### 接口信息
- **URL**: `/api/v1/shipment/shipments/`
- **Method**: `POST`
- **认证**: 需要登录JWT Token
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer | int | 是 | 客户ID |
| shipment_date | string | 是 | 出货日期YYYY-MM-DD |
| address | string | 否 | 地址(可空字符串,长度<=255 |
| contact_name | string | 否 | 联系人(可空字符串,长度<=100 |
| contact_phone | string | 否 | 联系电话(可空字符串,长度<=50 |
| area | string | 否 | 出货地区(可空字符串,长度<=30 |
| remark | string | 否 | 备注 |
| sales_items | array[int] | 否 | 要关联的销售品ID列表 |
### 请求示例
```json
{
"customer": 1,
"shipment_date": "2026-01-14",
"address": "杭州市测试路 1 号",
"contact_name": "张三",
"contact_phone": "13800138000",
"area": "华东",
"remark": "备注信息",
"sales_items": [1, 2, 3]
}
```
### 响应格式
```json
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 1,
"customer_name": "客户A",
"shipment_date": "2026-01-14",
"address": "杭州市测试路 1 号",
"contact_name": "张三",
"contact_phone": "13800138000",
"area": "华东",
"remark": "备注信息",
"status": 1,
"status_display": "草稿(未发布)",
"delivery_id": null,
"status_modified_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
"approved_by_id": null,
"approved_by_name": null,
"items_count": 3,
"sales_items": [],
"external_finished_products": [],
"created_by_id": 1,
"created_by_name": "张三",
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 出货单ID |
| merchant_id | int | 所属商户ID从当前登录用户推导 |
| merchant_name | string | 所属商户名称(从当前登录用户推导) |
| customer | int | 客户ID |
| customer_name | string | 客户名称 |
| fabric | string/null | 面料;来自关联生产订单,若当前出货单没有销售品则为 null |
| order_description | string/null | 滚筒预警;来自关联生产订单的 `rolling_warn`,若当前出货单没有销售品则为 null |
| shipment_date | string | 出货日期 |
| delivery_id | int/null | 关联送货单ID未绑定时为 null |
| address | string | 地址 |
| contact_name | string | 联系人 |
| contact_phone | string | 联系电话 |
| area | string | 出货地区 |
| remark | string | 备注 |
| status | int | 状态枚举1=草稿(未发布), 2=已发布, 3=已取消, 4=已驳回, 5=已审核) |
| status_display | string | 状态显示名称 |
| status_modified_at | string/null | 最后一次状态修改时间 |
| cancelled_by_id | int/null | 取消人 ID仅当进入已取消时可能有值 |
| cancelled_by_name | string/null | 取消人名称,仅当进入已取消时可能有值 |
| approved_by_id | int/null | 审核人 ID仅当进入已审核时可能有值 |
| approved_by_name | string/null | 审核人名称,仅当进入已审核时可能有值 |
| items_count | int | 关联的销售品数量 |
| created_by_id | int | 创建人ID |
| created_by_name | string | 创建人名称 |
| created_at | string | 创建时间 |
| updated_at | string | 更新时间 |
说明:
- 后端内部会在驳回时记录 `rejected_sales_item_ids` 审计快照,但该字段当前不对前端返回
### 状态流转规则
- `草稿(未发布)` 只能流转到 `已发布`
- `已发布` 可以流转到 `已审核` / `已驳回` / `已取消`
- `已驳回` 目前只能流转到 `已取消`
- `已审核` 可以流转到 `已取消`
- `已取消` 不可再流转到其他状态
- 重复设置同一状态保持幂等,不报错
- 进入 `已审核` 时必须提供审核人
- 进入 `已驳回` 时,系统会自动解绑当前出货单下的销售品,并将它们退回待分配池
- 驳回时解绑前的销售品 ID 会保存到后端审计字段 `rejected_sales_item_ids`
### 错误响应
#### 400 Bad Request - 客户不存在
```json
{
"detail": "客户 999 不存在"
}
```
#### 400 Bad Request - 销售品不存在
```json
{
"detail": "以下销售品不存在: [999]"
}
```
#### 400 Bad Request - 销售品已关联其他出货单
```json
{
"detail": "以下销售品已关联到其他出货单: [1, 2]"
}
```
#### 400 Bad Request - 销售品缺少关联生产任务
```json
{
"detail": "以下销售品缺少关联生产任务,无法创建出货单: [1]"
}
```
#### 400 Bad Request - 销售品关联的生产任务不存在
```json
{
"detail": "以下销售品关联的生产任务不存在,无法创建出货单: [1]"
}
```
#### 400 Bad Request - 销售品来自不同生产订单
```json
{
"detail": "出货单中的销售品必须来自同一个生产订单"
}
```
---
## 创建出货单external 版)
创建出货单但**不绑定任何销售品**,同时写入并关联“外部成品表”(用于兼容外部遗留系统)。
### 接口信息
- **URL**: `/api/v1/shipment/shipments/external/`
- **Method**: `POST`
- **认证**: 需要登录JWT Token
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer | int | 是 | 客户ID |
| shipment_date | string | 是 | 出货日期YYYY-MM-DD |
| address | string | 否 | 地址(可空字符串,长度<=255 |
| contact_name | string | 否 | 联系人(可空字符串,长度<=100 |
| contact_phone | string | 否 | 联系电话(可空字符串,长度<=50 |
| area | string | 否 | 出货地区(可空字符串,长度<=30 |
| remark | string | 否 | 备注 |
| external_id | string | 是 | 外部订单号(长度<=120 |
| external_finished_products | array[object] | 是 | 外部成品表结构数组(至少 1 条) |
`external_finished_products` 每项结构:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| style_name | string | 是 | 款式名称 |
| num_of_rolls | int | 是 | 卷数 |
| remark | string | 否 | 备注(可空) |
### 请求示例
```json
{
"customer": 1,
"shipment_date": "2026-01-14",
"address": "绍兴市仓库 2 号",
"contact_name": "李四",
"contact_phone": "13900139000",
"area": "华南",
"remark": "external 备注(可选)",
"external_id": "EXT-ORDER-001",
"external_finished_products": [
{"style_name": "款式A", "num_of_rolls": 2, "remark": "A备注"},
{"style_name": "款式B", "num_of_rolls": 5}
]
}
```
### 响应示例
```json
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 1,
"customer_name": "客户A",
"shipment_date": "2026-01-14",
"address": "绍兴市仓库 2 号",
"contact_name": "李四",
"contact_phone": "13900139000",
"area": "华南",
"remark": "external 备注(可选)",
"status": 1,
"status_display": "草稿(未发布)",
"external_id": "EXT-ORDER-001",
"status_modified_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
"approved_by_id": null,
"approved_by_name": null,
"items_count": 0,
"external_finished_products_count": 2,
"created_by_id": 1,
"created_by_name": "张三",
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
```
### 错误响应
#### 400 Bad Request - external_id 为空
```json
{
"external_id": ["external_id 不能为空"]
}
```
#### 400 Bad Request - external_finished_products 为空
```json
{
"external_finished_products": ["external_finished_products 不能为空"]
}
```
---
## 查询有待出货销售品的客户
查询当前商户下“存在未出货销售品”的客户列表。
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/customers/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
### 查询参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| limit | int | 否 | 分页大小LimitOffsetPagination |
| offset | int | 否 | 偏移量 |
### 业务逻辑
1. 仅统计当前商户下的销售品
2. 只统计 `shipment` 为空的销售品(未出货)
3. 只返回至少拥有 1 条未出货销售品的客户
4. 返回客户基础信息及未出货销售品数量
补充说明:
- 若某销售品原先绑定在一个出货单上,而该出货单后来被驳回,这些销售品会被自动解绑,因此会重新计入这里的“未出货销售品”统计
### 响应格式
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"customer_id": 12,
"customer_name": "客户A",
"mobile": "13800000000",
"area": "杭州",
"unshipped_sales_items_count": 18
}
]
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| count | int | 结果总数 |
| next | string/null | 下一页链接 |
| previous | string/null | 上一页链接 |
| results[].customer_id | int | 客户ID |
| results[].customer_name | string | 客户名称 |
| results[].mobile | string/null | 客户手机号 |
| results[].area | string/null | 客户地区 |
| results[].unshipped_sales_items_count | int | 该客户未出货销售品数量 |
---
## 按客户查询销售品
查询指定客户下的销售品列表,默认仅返回未出货销售品。
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/by-customer/<customer_id>/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer_id | int | 是 | 客户ID |
### 查询参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 |
| external_order_id | string | 否 | - | 按生产订单外部订单号精确筛选 |
| limit | int | 否 | 800 | 分页大小 |
| offset | int | 否 | 0 | 偏移量 |
### 业务逻辑
1. 仅允许查询当前商户下的客户
2. 仅查询当前商户下、`customer_id` 匹配的销售品
3. 默认只返回 `shipment` 为空的销售品(待出货)
4. 可通过 `include_already_has_shipment=true` 包含已出货销售品
5. 可通过 `external_order_id` 按关联生产订单的外部订单号精确筛选
补充说明:
- 被驳回出货单解绑的销售品会重新满足 `shipment = null` 条件,因此默认查询会再次返回它们
### 响应格式
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"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": "张三"
}
]
}
```
### 错误响应
#### 404 Not Found - 客户不存在或无权限
```json
{
"detail": "客户 999 不存在"
}
```
---
## 查询销售品详情
查询单个销售品详情,返回列表接口中的全部字段,并额外补充关联生产任务产品图片。
### 接口信息
- **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
### 接口信息
- **URL**: `/api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/`
- **Method**: `GET`
- **认证**: 需要登录JWT Token
### 路径参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| printing_order_id | string | 是 | 生产订单内部ID`external_order_id` |
### 查询参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 |
### 业务逻辑
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`:返回所有销售品(包含已出货的)
补充说明:
- 若某销售品所在出货单已被驳回,该销售品会在驳回时自动解绑,因此默认查询会再次将其视为待出货销售品
### 响应格式
```json
{
"count": 2,
"results": [
{
"id": 1,
"name": "产品A - 红色",
"quantity": "100.00",
"unit": 1,
"unit_display": "米",
"position": "A1-01",
"remark": "加急处理",
"printing_job_id": 123,
"printing_order_id": 456,
"customer_id": null,
"customer_name": null,
"shipment_id": null,
"shipment_date": null,
"created_at": "2026-01-14T10:00:00Z",
"created_by_id": 1,
"created_by_name": "张三"
},
{
"id": 2,
"name": "产品B - 蓝色",
"quantity": "50.50",
"unit": 1,
"unit_display": "米",
"position": "",
"remark": "",
"printing_job_id": 124,
"printing_order_id": 456,
"customer_id": 10,
"customer_name": "客户A",
"shipment_id": 5,
"shipment_date": "2026-01-13",
"created_at": "2026-01-13T15:30:00Z",
"created_by_id": 2,
"created_by_name": "李四"
}
]
}
```
### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| count | int | 结果总数 |
| results | array | 销售品列表 |
| results[].id | int | 销售品ID |
| results[].name | string | 销售品名称 |
| results[].quantity | string | 数量Decimal保留2位小数 |
| results[].unit | int | 单位编码1=米, 2=件, 3=码, 4=个) |
| results[].unit_display | string | 单位显示名称 |
| results[].position | string | 货位(可能为空) |
| results[].remark | string | 备注(可能为空) |
| results[].printing_job_id | int/null | 关联的生产任务ID |
| results[].printing_order_id | int/null | 关联的生产订单ID |
| results[].customer_id | int/null | 销售品级别的客户ID |
| results[].customer_name | string/null | 销售品关联客户名称 |
| results[].shipment_id | int/null | 关联的出货单IDnull 表示未出货 |
| results[].shipment_date | string/null | 出货日期YYYY-MM-DDnull 表示未出货 |
| results[].created_at | string | 创建时间ISO 8601 |
| results[].created_by_id | int/null | 创建人ID |
| results[].created_by_name | string/null | 创建人名称 |
### 错误响应
#### 404 Not Found - 生产订单不存在
```json
{
"detail": "生产订单 999 不存在"
}
```
#### 400 Bad Request - external_order_id 匹配多条生产订单
```json
{
"detail": "external_order_id KD20135142 匹配到多个生产订单请改用内部ID查询"
}
```
#### 401 Unauthorized - 未登录
```json
{
"detail": "Authentication credentials were not provided."
}
```
### 使用示例
#### 查询待出货的销售品(默认)
```bash
curl -X GET \
'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/123/' \
-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
curl -X GET \
'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/123/?include_already_has_shipment=true' \
-H 'Authorization: Bearer <token>'
```
---
## 单位编码对照表
| 编码 | 名称 |
|------|------|
| 1 | 米 |
| 2 | 件 |
| 3 | 码 |
| 4 | 个 |
---
## 相关模块
- `shipment/services.py`: 业务逻辑层
- `api_v1/views/shipment/`: API 视图层
- `shipment/models.py`: 数据模型SalesItem, Shipment
---
## 外部系统兼容(数据模型说明)
为兼容外部遗留系统Shipment 模块新增了“外部成品表”模型:
- **关系**`Shipment` 1 → N `ExternalFinishedProduct`
- 外键在 `ExternalFinishedProduct.shipment`(可空)
- **Shipment 外部字段**
- `external_id`:外部订单号(可空)