forked from erp-dev/erp
fix: added delivery_id and delivery_isnull query param to shipment list api
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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` 包含该出货单的所有销售品(软删除的条目自动排除)
|
||||
|
||||
@@ -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 | 联系电话 |
|
||||
|
||||
Reference in New Issue
Block a user