1
0
forked from erp-dev/erp

feat: app verion && some field modify (printing & shipment)

This commit is contained in:
2026-07-08 20:36:51 +08:00
parent ddbf798665
commit 36e4bb6de6
32 changed files with 3465 additions and 26 deletions

View File

@@ -0,0 +1,826 @@
# Shipment 模块 API v1 文档
面向前端对接,基于当前代码生成。接口统一前缀为 `/api/v1/`
## 通用说明
- 认证:所有接口均要求登录认证。
- 商户隔离:普通用户只能访问自己员工所属商户的数据;超级管理员部分接口可跨商户,具体见接口说明。
- 分页:列表接口使用 `limit``offset`,分页响应一般为 `{ "count": 0, "next": null, "previous": null, "results": [] }`
- 错误:业务校验失败通常返回 `400 { "detail": "错误原因" }`;不存在通常返回 `404`
## 枚举
### 出货单状态 `ShipmentStatus`
| 值 | 含义 |
| --- | --- |
| `1` | 草稿(未发布) |
| `2` | 已发布 |
| `3` | 已取消 |
| `4` | 已驳回 |
| `5` | 已审核 |
### 送货单状态 `ShipmentDeliveryStatus`
| 值 | 含义 |
| --- | --- |
| `1` | 待送货 |
| `2` | 送货中 |
| `3` | 已送达 |
### 销售品单位 `UnitChoices`
| 值 | 含义 |
| --- | --- |
| `1` | 米 |
| `2` | 件 |
| `3` | 码 |
| `4` | 个 |
## 公共响应结构
### Shipment
```json
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"customer": 10,
"customer_name": "客户A",
"address_id": 5,
"fabric": "面料信息",
"order_description": "订单描述",
"shipment_date": "2026-01-14",
"address": "杭州市测试路 1 号",
"contact_name": "张三",
"contact_phone": "13800138000",
"area": "华东",
"coordinates": "120.1551,30.2741",
"remark": "备注",
"status": 1,
"status_display": "草稿(未发布)",
"external_id": null,
"geo_coordinates": null,
"extra": {"source": "customer_address"},
"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": 2,
"created_by_id": 1,
"created_by_name": "张三",
"external_finished_products_count": 0,
"sales_items": [],
"external_finished_products": [],
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `address_id` | integer/null | 关联客户地址 ID只表示关联关系 |
| `address` | string | 出货单地址快照,由请求体写入 |
| `contact_name` | string | 联系人快照,由请求体写入 |
| `contact_phone` | string | 联系电话快照,由请求体写入 |
| `area` | string | 地区快照,最大 100 字符 |
| `coordinates` | string/null | 经纬度字符串,最大 100 字符 |
| `geo_coordinates` | object/array/null | 历史 Geo 字段,保留兼容老数据 |
| `extra` | object/array/null | 扩展 JSON |
重要行为:传 `address_id` 时,后端只校验并保存关联,不会用 `CustomerAddress` 自动覆盖 `address/contact_name/contact_phone/area/coordinates/extra`。这些快照字段仍以请求体为准。
### SalesItem
```json
{
"id": 1,
"name": "销售品A",
"quantity": "100.00",
"unit": 1,
"unit_display": "米",
"position": "A1-01",
"remark": "备注",
"printing_job_id": 123,
"printing_order_id": 456,
"external_order_id": "KD20441758",
"customer_id": 10,
"customer_name": "客户A",
"shipment_id": null,
"shipment_date": null,
"merge_remark": null,
"created_at": "2026-01-14T10:00:00Z",
"created_by_id": 1,
"created_by_name": "张三"
}
```
详情接口额外包含:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `product_image_url` | string/null | 关联生产任务产品主图 URL |
### ShipmentDelivery
```json
{
"id": 1,
"merchant_id": 1,
"merchant_name": "测试印花厂",
"driver_name": "司机A",
"vehicle_trip": "浙A12345",
"contact_phone": "13800138000",
"vehicle_capacity": "4.2米",
"remark": "备注",
"internal_remark": "内部备注",
"shipment_order_ids": [1, 2, 3],
"status": 1,
"status_display": "待送货",
"started_at": null,
"delivered_at": null,
"cancelled_at": null,
"shipments_count": 2,
"shipments": [],
"created_by_id": 1,
"created_by_name": "张三",
"operator_id": null,
"operator_name": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
```
`shipments` 内元素为送货单内出货单摘要:
```json
{
"id": 1,
"customer": 10,
"customer_name": "客户A",
"fabric": "面料信息",
"order_description": "订单描述",
"shipment_date": "2026-01-14",
"status": 5,
"status_display": "已审核",
"external_id": null,
"delivery_id": 1
}
```
## 出货单接口
### 查询出货单列表
`GET /api/v1/shipment/shipments/`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `customer` | integer | 否 | 客户 ID |
| `status` | integer | 否 | 出货单状态 |
| `delivery_id` | integer | 否 | 送货单 ID空字符串、`null``none` 不触发过滤 |
| `delivery_isnull` | boolean | 否 | `true/1/yes` 查未绑定送货单,`false/0/no` 查已绑定送货单 |
| `external_id` | string | 否 | 外部订单号精确匹配 |
| `shipment_date_from` | date | 否 | 出货日期起始,`YYYY-MM-DD` |
| `shipment_date_to` | date | 否 | 出货日期结束,`YYYY-MM-DD` |
| `only_address_null` | boolean | 否 | `true` 查地址为空字符串,`false` 查地址非空 |
| `limit` | integer | 否 | 分页大小 |
| `offset` | integer | 否 | 分页偏移 |
响应:分页 `Shipment[]`
### 创建出货单
`POST /api/v1/shipment/shipments/`
请求体:
```json
{
"customer": 10,
"address_id": 5,
"shipment_date": "2026-01-14",
"address": "杭州市测试路 1 号",
"contact_name": "张三",
"contact_phone": "13800138000",
"area": "华东",
"coordinates": "120.1551,30.2741",
"extra": {"source": "customer_address"},
"remark": "备注",
"sales_items": [1, 2, 3]
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `customer` | integer | 是 | 客户 ID |
| `address_id` | integer/null | 否 | 客户地址 ID仅校验和关联 |
| `shipment_date` | date | 是 | 出货日期 |
| `address` | string | 否 | 地址快照,默认 `""`,最大 255 |
| `contact_name` | string | 否 | 联系人快照,默认 `""`,最大 100 |
| `contact_phone` | string | 否 | 联系电话快照,默认 `""`,最大 50 |
| `area` | string | 否 | 地区快照,默认 `""`,最大 100 |
| `coordinates` | string/null | 否 | 经纬度字符串,最大 100 |
| `extra` | JSON/null | 否 | 扩展信息 |
| `remark` | string | 否 | 出货单备注,默认 `""` |
| `sales_items` | integer[] | 否 | 要关联的销售品 ID 列表,默认 `[]`,后端会去重 |
业务规则:
- 用户必须关联商户。
- 客户必须属于当前商户。
- `address_id` 存在时,客户地址必须存在、未软删除、属于当前商户、属于请求中的 `customer`
- `sales_items` 非空时,销售品必须存在、未删除、未绑定其他出货单、属于当前商户,并且都来自同一个生产订单。
响应:`201 Shipment`
### 创建外部出货单
`POST /api/v1/shipment/shipments/external/`
用于没有内部销售品、但需要记录外部成品表的出货单。
请求体:
```json
{
"customer": 10,
"shipment_date": "2026-01-14",
"address": "杭州市测试路 1 号",
"contact_name": "张三",
"contact_phone": "13800138000",
"area": "华东",
"coordinates": "120.1551,30.2741",
"extra": {"source": "external"},
"remark": "备注",
"external_id": "EXT-001",
"external_finished_products": [
{
"style_name": "款式A",
"num_of_rolls": 10,
"remark": "成品备注"
}
]
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `customer` | integer | 是 | 客户 ID |
| `shipment_date` | date | 是 | 出货日期 |
| `address` | string | 否 | 地址,最大 255 |
| `contact_name` | string | 否 | 联系人,最大 100 |
| `contact_phone` | string | 否 | 联系电话,最大 50 |
| `area` | string | 否 | 地区,最大 100 |
| `coordinates` | string/null | 否 | 经纬度字符串,最大 100 |
| `extra` | JSON/null | 否 | 扩展信息 |
| `remark` | string | 否 | 备注 |
| `external_id` | string | 是 | 外部订单号,不能为空,最大 120 |
| `external_finished_products` | array | 是 | 外部成品表,不能为空 |
`external_finished_products` 元素:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `style_name` | string | 是 | 款式名,最大 200 |
| `num_of_rolls` | integer | 是 | 卷数,最小 0 |
| `remark` | string/null | 否 | 备注,最大 200 |
响应:`201 Shipment`
### 获取出货单详情
`GET /api/v1/shipment/shipments/{id}/`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `status` | integer | 否 | 可选状态过滤;不匹配则返回 404 |
响应:`200 Shipment`
### 更新出货单
`PATCH /api/v1/shipment/shipments/{id}/`
`PUT /api/v1/shipment/shipments/{id}/`
当前 `PUT` 行为等同 `PATCH`,允许只传部分字段。
请求体:
```json
{
"customer": 10,
"shipment_date": "2026-01-15",
"address": "新地址",
"contact_name": "李四",
"contact_phone": "13900139000",
"area": "华南",
"coordinates": null,
"extra": null,
"remark": "新备注",
"external_id": "EXT-002",
"geo_coordinates": {"lng": 120.1, "lat": 30.2}
}
```
可更新字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `customer` | integer | 客户 ID |
| `shipment_date` | date | 出货日期 |
| `address` | string | 地址,最大 255 |
| `contact_name` | string | 联系人,最大 100 |
| `contact_phone` | string | 联系电话,最大 50 |
| `area` | string | 地区,最大 100 |
| `coordinates` | string/null | 经纬度字符串;传 `null` 可清空 |
| `extra` | JSON/null | 扩展信息;传 `null` 可清空 |
| `remark` | string | 备注 |
| `external_id` | string/null | 外部订单号;空字符串或 `null` 会清空 |
| `geo_coordinates` | JSON/null | 当前 serializer 接收该字段,但业务更新服务未保存它,前端不建议使用 |
业务规则:
- 已关联送货单的出货单不允许修改业务数据。
- 修改客户时,新客户必须存在且属于同一商户。
响应:`200 Shipment`
### 修改出货单状态
`PATCH /api/v1/shipment/shipments/{id}/status/`
`PUT /api/v1/shipment/shipments/{id}/status/`
请求体:
```json
{
"status": 5
}
```
响应:`200 Shipment`
## 送货单接口
### 查询送货单列表
`GET /api/v1/shipment/deliveries/`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `status` | integer | 否 | 送货单状态 |
| `driver_name` | string | 否 | 司机名模糊匹配 |
| `vehicle_trip` | string | 否 | 车次模糊匹配 |
| `limit` | integer | 否 | 分页大小 |
| `offset` | integer | 否 | 分页偏移 |
响应:分页 `ShipmentDelivery[]`
### 创建送货单
`POST /api/v1/shipment/deliveries/`
请求体:
```json
{
"driver_name": "司机A",
"vehicle_trip": "浙A12345",
"contact_phone": "13800138000",
"vehicle_capacity": "4.2米",
"remark": "备注",
"internal_remark": "内部备注",
"shipment_order_ids": [3, 2, 1],
"shipments": [1, 2, 3]
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `driver_name` | string | 是 | 司机名,最大 100 |
| `vehicle_trip` | string | 是 | 车次,最大 100 |
| `contact_phone` | string | 否 | 联系电话,默认 `""`,最大 50 |
| `vehicle_capacity` | string | 否 | 车辆容量,默认 `""`,最大 100 |
| `remark` | string | 否 | 备注,默认 `""`,最大 200 |
| `internal_remark` | string | 否 | 内部备注,默认 `""`,最大 200 |
| `shipment_order_ids` | array/null | 否 | 前端自管出货单排序 ID 数组,可为 `null` |
| `shipments` | integer[] | 否 | 要关联的出货单 ID 列表,默认 `[]`,后端会去重 |
响应:`201 ShipmentDelivery`
### 按生产订单查询送货单
`GET /api/v1/shipment/deliveries/by-printing-order/{printing_order_id}/`
路径参数:
- `printing_order_id` 支持内部 `PrintingOrder.id``external_order_id`
- 如果 `external_order_id` 匹配多个生产订单,返回 `500 { "message": "...请改用内部ID查询" }`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `status` | integer | 否 | 送货单状态 |
| `driver_name` | string | 否 | 模糊匹配 |
| `vehicle_trip` | string | 否 | 模糊匹配 |
| `contact_phone` | string | 否 | 模糊匹配 |
| `vehicle_capacity` | string | 否 | 模糊匹配 |
| `remark` | string | 否 | 模糊匹配 |
| `internal_remark` | string | 否 | 模糊匹配 |
| `started_at_from` | date/datetime | 否 | 开始送货时间起 |
| `started_at_to` | date/datetime | 否 | 开始送货时间止 |
| `delivered_at_from` | date/datetime | 否 | 送达时间起 |
| `delivered_at_to` | date/datetime | 否 | 送达时间止 |
| `cancelled_at_from` | date/datetime | 否 | 取消时间起 |
| `cancelled_at_to` | date/datetime | 否 | 取消时间止 |
| `created_at_from` | date/datetime | 否 | 创建时间起 |
| `created_at_to` | date/datetime | 否 | 创建时间止 |
| `shipment_date_from` | date | 否 | 关联出货日期起 |
| `shipment_date_to` | date | 否 | 关联出货日期止 |
| `ordering` | string | 否 | 可选 `id/created_at/updated_at/started_at/delivered_at/cancelled_at/status`,支持前缀 `-` |
| `limit` | integer | 否 | 分页大小 |
| `offset` | integer | 否 | 分页偏移 |
响应:分页 `ShipmentDelivery[]`,字段使用专用 DTO不包含 `cancelled_by_id/cancelled_by_name`
### 获取送货单详情
`GET /api/v1/shipment/deliveries/{id}/`
响应:`200 ShipmentDelivery`
### 更新送货单
`PATCH /api/v1/shipment/deliveries/{id}/`
`PUT /api/v1/shipment/deliveries/{id}/`
当前 `PUT` 行为等同 `PATCH`
请求体:
```json
{
"driver_name": "司机B",
"vehicle_trip": "浙B12345",
"contact_phone": "13900139000",
"vehicle_capacity": "9.6米",
"remark": "新备注",
"internal_remark": "新内部备注",
"shipment_order_ids": [2, 1],
"shipments": [1, 2]
}
```
可更新字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `driver_name` | string | 司机名,最大 100不允许空字符串 |
| `vehicle_trip` | string | 车次,最大 100不允许空字符串 |
| `contact_phone` | string | 联系电话,最大 50 |
| `vehicle_capacity` | string | 车辆容量,最大 100 |
| `remark` | string | 备注,最大 200 |
| `internal_remark` | string | 内部备注,最大 200 |
| `shipment_order_ids` | array/null | 前端自管排序 ID 数组,传入即更新 |
| `shipments` | integer[] | 传入即替换当前绑定出货单 |
响应:`200 ShipmentDelivery`
### 删除送货单
`DELETE /api/v1/shipment/deliveries/{id}/`
响应:`204 No Content`
### 修改送货单状态
`POST /api/v1/shipment/deliveries/{id}/status/`
请求体:
```json
{
"status": 2
}
```
允许状态:`1``2``3`
响应:`200 ShipmentDelivery`
### 取消送货单
`POST /api/v1/shipment/deliveries/{id}/cancel/`
权限:需要 `shipment.cancel_shipmentdelivery`
响应:`200 ShipmentDelivery`
### 追加绑定出货单到送货单
`POST /api/v1/shipment/deliveries/{id}/bind-shipments/`
请求体:
```json
{
"shipments": [4, 5]
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `shipments` | integer[] | 是 | 要追加绑定的出货单 ID不能为空后端会去重 |
响应:`200 ShipmentDelivery`
## 销售品接口
### 查询当前可出货客户
`GET /api/v1/shipment/sales-items/customers/`
从未出货销售品反推出客户列表。
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `merchant` | integer | 超级管理员必填 | 超级管理员必须传商户 ID普通用户忽略 |
| `customer_name` | string | 否 | 客户名称模糊匹配 |
| `limit` | integer | 否 | 分页大小 |
| `offset` | integer | 否 | 分页偏移 |
响应:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"customer_id": 10,
"customer_name": "客户A",
"mobile": "13900139000",
"area": "杭州",
"unshipped_sales_items_count": 3
}
]
}
```
### 按客户查询销售品
`GET /api/v1/shipment/sales-items/by-customer/{customer_id}/`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `include_already_has_shipment` | boolean | 否 | 是否包含已关联出货单销售品,默认 `false`;只有字符串 `true` 会视为真 |
| `external_order_id` | string | 否 | 按生产订单外部订单号过滤 |
| `limit` | integer | 否 | 分页大小 |
| `offset` | integer | 否 | 分页偏移 |
响应:分页 `SalesItem[]`
### 按客户查询销售品分组
`GET /api/v1/shipment/sales-items/by-customer/{customer_id}/group/`
第一层按 `external_order_id` 分组并分页,第二层按 `name` 分组。
查询参数同“按客户查询销售品”。
响应:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"external_order_id": "KD20441758",
"count": 2,
"name_groups": [
{
"name": "销售品A",
"count": 2,
"items": []
}
]
}
]
}
```
排序:`external_order_id = null` 排最后。
### 按生产订单查询销售品
`GET /api/v1/shipment/sales-items/by-printing-order/{printing_order_id}/`
路径参数:
- `printing_order_id` 支持内部 `PrintingOrder.id``external_order_id`
- 如果 `external_order_id` 匹配多个生产订单,此接口返回 `400 { "detail": "...请改用内部ID查询" }`
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `include_already_has_shipment` | boolean | 否 | 是否包含已关联出货单销售品,默认 `false`;只有字符串 `true` 会视为真 |
响应:
```json
{
"count": 2,
"results": []
}
```
`results``SalesItem[]`,该接口不使用标准分页。
### 按生产订单查询销售品分组
`GET /api/v1/shipment/sales-items/by-printing-order/{printing_order_id}/group/`
`printing_job_id` 分组并分页。
查询参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `include_already_has_shipment` | boolean | 否 | 是否包含已关联出货单销售品,默认 `false` |
| `limit` | integer | 否 | 分页大小,作用于分组数量 |
| `offset` | integer | 否 | 分页偏移,作用于分组数量 |
响应:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"printing_job_id": 123,
"count": 2,
"items": []
}
]
}
```
### 手动创建销售品
`POST /api/v1/shipment/sales-items/`
请求体:
```json
{
"printing_job_id": 123,
"name": "销售品A",
"quantity": "100.50",
"unit": 1,
"customer_id": 10,
"remark": "备注",
"position": "A1-01",
"merge_remark": {
"merge_type": "merge",
"jobs": [1, 2],
"main_job": 1,
"quantity": "100.50",
"unit": "米",
"job_count": 2
}
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `printing_job_id` | integer | 是 | 生产任务 ID最小 1 |
| `name` | string | 是 | 销售品名称,最大 200 |
| `quantity` | string | 是 | 数量,最大 20支持小数 |
| `unit` | integer | 是 | 单位:`1/2/3/4` |
| `customer_id` | integer/null | 否 | 客户 ID不传时默认从生产订单获取 |
| `remark` | string | 否 | 备注,最大 200 |
| `position` | string | 否 | 货位,最大 200 |
| `merge_remark` | object/null | 否 | 合卷备注 JSON结构严格校验 |
`merge_remark` 必须只包含这些字段:`merge_type``jobs``main_job``quantity``unit``job_count`
响应:`201 SalesItem`
### 获取销售品详情
`GET /api/v1/shipment/sales-items/{id}/`
响应:`200 SalesItem`,包含 `product_image_url`
### 更新销售品
`PATCH /api/v1/shipment/sales-items/{id}/`
请求体只能包含以下字段:
```json
{
"quantity": "80.00",
"remark": "新备注",
"position": "B2-01"
}
```
可更新字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `quantity` | string | 数量,支持小数 |
| `remark` | string | 备注,最大 200 |
| `position` | string | 货位,最大 200 |
如果请求包含其他字段,返回 `400`
响应:`200 SalesItem`,包含 `product_image_url`
### 删除销售品
`DELETE /api/v1/shipment/sales-items/{id}/`
权限:需要 `shipment.soft_delete_salesitem`
响应:
```json
{
"detail": "销售品已标记为删除"
}
```
### 重建销售品
`POST /api/v1/shipment/sales-items/{id}/rebuild/`
仅销售品创建者可执行。
请求体:
```json
{
"new_printing_job_id": 456,
"quantity": "50.00"
}
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `new_printing_job_id` | integer | 是 | 新生产任务 ID最小 1 |
| `quantity` | string | 否 | 新数量;不传则沿用原销售品数量 |
响应:`200 SalesItem`
## 前端重点注意
- 创建出货单时,`address_id` 不会自动填充地址快照。前端若选择客户地址,需要同时把地址表中的 `address/contact_name/contact_phone/area/coordinates/extra` 放入创建请求。
- `coordinates` 是新的字符串字段,前端不要再把新数据写到 `geo_coordinates`
- `geo_coordinates` 是历史兼容字段,读取时可能存在旧值,但更新接口目前不保存它。
- `area` 现在最大 100 字符,可以直接承载客户地址的 `area`
- `extra` 可用于承载客户地址扩展信息,允许对象、数组或 `null`
- 更新出货单时,传 `coordinates: null``extra: null` 可以清空对应字段;不传则不修改。
- 按生产订单查送货单的 `external_order_id` 多匹配错误返回 `500 + message`;按生产订单查销售品的同类错误返回 `400 + detail`