1
0
forked from erp-dev/erp

fix: added delivery_id and delivery_isnull query param to shipment list api

This commit is contained in:
2026-04-21 23:09:58 +08:00
parent 8b1afc64c5
commit 35e6af1128
6 changed files with 160 additions and 2 deletions

View File

@@ -85,6 +85,7 @@ class ShipmentSerializer(serializers.ModelSerializer):
external_finished_products_count = serializers.SerializerMethodField()
merchant_id = serializers.IntegerField(source="merchant.id", read_only=True)
merchant_name = serializers.CharField(source="merchant.name", read_only=True)
delivery_id = serializers.IntegerField(read_only=True, allow_null=True)
sales_items = serializers.SerializerMethodField()
external_finished_products = serializers.SerializerMethodField()
@@ -106,6 +107,7 @@ class ShipmentSerializer(serializers.ModelSerializer):
"status_display",
"external_id",
"geo_coordinates",
"delivery_id",
"status_modified_at",
"cancelled_by_id",
"cancelled_by_name",
@@ -492,6 +494,7 @@ class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
delivery_id = serializers.IntegerField(read_only=True, allow_null=True)
class Meta:
model = Shipment
@@ -503,6 +506,7 @@ class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer):
"status",
"status_display",
"external_id",
"delivery_id",
]
read_only_fields = fields

View File

@@ -759,6 +759,11 @@ class SalesItemDetailAPITestCase(APITestCase):
mobile="13800138033",
status=basic_models.EmployeeStatusEnum.ACTIVE,
)
self.no_employee_user = User.objects.create_user(
username="sales_item_detail_no_employee_user",
password="testpass123",
email="sales_item_detail_no_employee@example.com",
)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
@@ -1110,6 +1115,19 @@ class SalesItemDetailAPITestCase(APITestCase):
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(resp.json()["detail"], "已关联出货单的销售品不允许删除")
def test_rebuild_sales_item_user_without_employee_returns_404(self):
self.client.force_authenticate(user=self.no_employee_user)
resp = self.client.post(
f"/api/v1/shipment/sales-items/{self.sales_item.id}/rebuild/",
{
"new_printing_job_id": self.new_printing_job.id,
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
class ShipmentCreateAPITestCase(TestCase):
"""测试创建出货单 API"""
@@ -1672,6 +1690,8 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn("contact_name", item)
self.assertIn("contact_phone", item)
self.assertIn("area", item)
self.assertIn("delivery_id", item)
self.assertIsNone(item["delivery_id"])
self.assertEqual(item["area"], "A1")
self.assertEqual(item["status"], shipment_models.ShipmentStatus.DRAFT)
self.assertEqual(item["status_display"], "草稿(未发布)")
@@ -1707,6 +1727,107 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn(shipment_with_address.id, false_ids)
self.assertNotIn(self.shipment1.id, false_ids)
def test_list_shipments_supports_delivery_id_filter(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant1,
driver_name="过滤司机",
vehicle_trip="FILTER-001",
created_by=self.user1,
)
self.shipment1.delivery = delivery
self.shipment1.save(update_fields=["delivery", "updated_at"])
other_same_merchant = shipment_models.Shipment.objects.create(
merchant=self.merchant1,
customer=self.customer1,
shipment_date="2026-01-17",
created_by=self.user1,
remark="other",
area="A2",
)
resp = self.client.get(f"/api/v1/shipment/shipments/?delivery_id={delivery.id}")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 1)
self.assertEqual(data["results"][0]["id"], self.shipment1.id)
self.assertEqual(data["results"][0]["delivery_id"], delivery.id)
self.assertNotEqual(other_same_merchant.id, self.shipment1.id)
def test_list_shipments_ignores_null_like_delivery_id_filter(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant1,
driver_name="空值司机",
vehicle_trip="FILTER-NULL",
created_by=self.user1,
)
self.shipment1.delivery = delivery
self.shipment1.save(update_fields=["delivery", "updated_at"])
resp = self.client.get("/api/v1/shipment/shipments/?delivery_id=null")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
ids = [it["id"] for it in data["results"]]
self.assertIn(self.shipment1.id, ids)
shipment_item = next(it for it in data["results"] if it["id"] == self.shipment1.id)
self.assertEqual(shipment_item["delivery_id"], delivery.id)
def test_list_shipments_supports_delivery_isnull_true_filter(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant1,
driver_name="未绑定筛选司机",
vehicle_trip="FILTER-ISNULL-TRUE",
created_by=self.user1,
)
self.shipment1.delivery = delivery
self.shipment1.save(update_fields=["delivery", "updated_at"])
shipment_without_delivery = shipment_models.Shipment.objects.create(
merchant=self.merchant1,
customer=self.customer1,
shipment_date="2026-01-18",
created_by=self.user1,
remark="no-delivery",
area="A3",
)
resp = self.client.get("/api/v1/shipment/shipments/?delivery_isnull=true")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
ids = [it["id"] for it in resp.json()["results"]]
self.assertIn(shipment_without_delivery.id, ids)
self.assertNotIn(self.shipment1.id, ids)
def test_list_shipments_supports_delivery_isnull_false_filter(self):
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant1,
driver_name="已绑定筛选司机",
vehicle_trip="FILTER-ISNULL-FALSE",
created_by=self.user1,
)
self.shipment1.delivery = delivery
self.shipment1.save(update_fields=["delivery", "updated_at"])
shipment_models.Shipment.objects.create(
merchant=self.merchant1,
customer=self.customer1,
shipment_date="2026-01-19",
created_by=self.user1,
remark="still-null",
area="A4",
)
resp = self.client.get("/api/v1/shipment/shipments/?delivery_isnull=false")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = resp.json()
self.assertEqual(data["count"], 1)
self.assertEqual(data["results"][0]["id"], self.shipment1.id)
self.assertEqual(data["results"][0]["delivery_id"], delivery.id)
def test_retrieve_shipment_success(self):
resp = self.client.get(f"/api/v1/shipment/shipments/{self.shipment1.id}/")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
@@ -1714,6 +1835,8 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertIn("address", result)
self.assertIn("contact_name", result)
self.assertIn("contact_phone", result)
self.assertIn("delivery_id", result)
self.assertIsNone(result["delivery_id"])
self.assertEqual(result["id"], self.shipment1.id)
self.assertIn("area", result)
self.assertEqual(result["area"], "A1")
@@ -2664,6 +2787,9 @@ class ShipmentDeliveryAPITestCase(APITestCase):
self.assertEqual(data["shipments_count"], 2)
shipment_ids = [item["id"] for item in data["shipments"]]
self.assertEqual(shipment_ids, [self.shipment1.id, self.shipment2.id])
for item in data["shipments"]:
self.assertIn("delivery_id", item)
self.assertEqual(item["delivery_id"], delivery.id)
def test_patch_delivery_updates_fields_and_replaces_shipments(self):
delivery = shipment_models.ShipmentDelivery.objects.create(

View File

@@ -121,6 +121,8 @@ class ShipmentListCreateView(ListModelMixin, GenericAPIView):
支持过滤参数(可选):
- customer: 客户ID
- status: 状态1=草稿, 2=已发布, 3=已取消, 4=已驳回, 5=已审核)
- delivery_id: 送货单ID仅当传入具体ID时过滤null/空值不触发过滤)
- delivery_isnull: 是否仅查询未绑定/已绑定送货单的出货单true/false
- external_id: 外部订单号(精确匹配)
- shipment_date_from: 出货日期起始YYYY-MM-DD
- shipment_date_to: 出货日期结束YYYY-MM-DD包含整天
@@ -151,6 +153,20 @@ class ShipmentListCreateView(ListModelMixin, GenericAPIView):
if status_val:
qs = qs.filter(status=status_val)
delivery_id = self.request.query_params.get("delivery_id")
if delivery_id is not None:
normalized_delivery_id = delivery_id.strip()
if normalized_delivery_id and normalized_delivery_id.lower() not in {"null", "none"}:
qs = qs.filter(delivery_id=normalized_delivery_id)
delivery_isnull = self.request.query_params.get("delivery_isnull")
if delivery_isnull is not None:
normalized_delivery_isnull = delivery_isnull.strip().lower()
if normalized_delivery_isnull in {"1", "true", "yes"}:
qs = qs.filter(delivery_id__isnull=True)
elif normalized_delivery_isnull in {"0", "false", "no"}:
qs = qs.filter(delivery_id__isnull=False)
external_id = self.request.query_params.get("external_id")
if external_id:
qs = qs.filter(external_id=external_id)
@@ -1009,7 +1025,7 @@ class SalesItemDetailView(GenericAPIView):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return SalesItem.objects.none()
return qs.none()
return qs.filter(merchant=merchant)
def get(self, request, pk: int):
@@ -1116,7 +1132,7 @@ class SalesItemRebuildView(GenericAPIView):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return SalesItem.objects.none()
return qs.none()
return qs.filter(merchant=merchant)
def post(self, request, pk: int):

View File

@@ -101,6 +101,7 @@ class AgentSalesItemSerializer(serializers.ModelSerializer):
class AgentShipmentListSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
delivery_id = serializers.IntegerField(read_only=True, allow_null=True)
sales_items = serializers.SerializerMethodField()
class Meta:
@@ -120,6 +121,7 @@ class AgentShipmentListSerializer(serializers.ModelSerializer):
"status_display",
"external_id",
"geo_coordinates",
"delivery_id",
"delivery",
"sales_items",
"created_at",

View File

@@ -55,6 +55,7 @@ Authorization: <AGENT_ACCESS_KEY>
"status_display": "待出货",
"external_id": null,
"geo_coordinates": {"lat": 23.1291, "lng": 113.2644},
"delivery_id": null,
"delivery": null,
"sales_items": [
{
@@ -152,6 +153,7 @@ Authorization: <AGENT_ACCESS_KEY>
说明:
- "未出货"定义:`delivery` 为空,即尚未关联送货单
- `delivery_id` 始终返回送货单ID未关联时为 `null`
- 结果按 `created_at` 降序排列
- 只返回 `merchant_id` 对应商户的数据
- `sales_items` 包含该出货单的所有销售品(软删除的条目自动排除)

View File

@@ -31,6 +31,8 @@
| 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 |
@@ -54,6 +56,7 @@
"shipment_date": "2026-01-14",
"status": 1,
"status_display": "草稿(未发布)",
"delivery_id": null,
"status_modified_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
@@ -75,6 +78,8 @@
说明:
- `sales_items` 现在返回的是带图片字段的销售品详情结构
- `delivery_id` 始终返回送货单ID未绑定送货单时为 `null`
- `delivery_id=null` 不会触发过滤;如需按是否绑定送货单筛选,请使用 `delivery_isnull`
- 若销售品关联的 `PrintingJob.product` 存在主图,则会返回 `product_image_url`
- 若无关联图片,则 `product_image_url``null`
- 若出货单已经被驳回,则其销售品会在驳回时被自动解绑,因此此处的 `sales_items` 会变为空数组
@@ -93,6 +98,7 @@
说明:
- 详情响应同样始终包含 `delivery_id`;未绑定送货单时返回 `null`
- 详情中的 `sales_items` 同样返回带 `product_image_url` 的销售品详情结构
---
@@ -195,6 +201,7 @@
"remark": "备注信息",
"status": 1,
"status_display": "草稿(未发布)",
"delivery_id": null,
"status_modified_at": null,
"cancelled_by_id": null,
"cancelled_by_name": null,
@@ -220,6 +227,7 @@
| customer | int | 客户ID |
| customer_name | string | 客户名称 |
| shipment_date | string | 出货日期 |
| delivery_id | int/null | 关联送货单ID未绑定时为 null |
| address | string | 地址 |
| contact_name | string | 联系人 |
| contact_phone | string | 联系电话 |