# 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`。