diff --git a/.env b/.env index 4eebf53..fefb949 100644 --- a/.env +++ b/.env @@ -10,3 +10,4 @@ PRINTING_SALES_ITEM_SOURCE_STATE_ID=2 WECOM_WEBHOOK_KEY=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205 PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id} AGENT_ACCESS_KEY=hophopkk +PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED=False diff --git a/api_v1/views/shipment/serializers.py b/api_v1/views/shipment/serializers.py index 8871c3a..6327b5b 100644 --- a/api_v1/views/shipment/serializers.py +++ b/api_v1/views/shipment/serializers.py @@ -62,6 +62,48 @@ def _build_nested_sales_item_context(items): } +def _resolve_shipment_fabric(shipment: Shipment) -> str | None: + rel = getattr(shipment, "items", None) + if rel is None: + return None + + first_item = rel.filter(delete_at__isnull=True).order_by("id").first() + if first_item is None or not first_item.printing_job_id: + return None + + from printing.models import PrintingJob + + printing_job = ( + PrintingJob.objects.filter(id=first_item.printing_job_id) + .select_related("printing_order") + .first() + ) + if printing_job is None or printing_job.printing_order is None: + return None + return printing_job.printing_order.fabric + + +def _resolve_shipment_order_description(shipment: Shipment) -> str | None: + rel = getattr(shipment, "items", None) + if rel is None: + return None + + first_item = rel.filter(delete_at__isnull=True).order_by("id").first() + if first_item is None or not first_item.printing_job_id: + return None + + from printing.models import PrintingJob + + printing_job = ( + PrintingJob.objects.filter(id=first_item.printing_job_id) + .select_related("printing_order") + .first() + ) + if printing_job is None or printing_job.printing_order is None: + return None + return printing_job.printing_order.description + + class ShipmentSerializer(serializers.ModelSerializer): """ 出货单序列化器(只读,用于返回数据) @@ -85,6 +127,8 @@ 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) + fabric = serializers.SerializerMethodField() + order_description = serializers.SerializerMethodField() delivery_id = serializers.IntegerField(read_only=True, allow_null=True) sales_items = serializers.SerializerMethodField() external_finished_products = serializers.SerializerMethodField() @@ -97,6 +141,8 @@ class ShipmentSerializer(serializers.ModelSerializer): "merchant_name", "customer", "customer_name", + "fabric", + "order_description", "shipment_date", "address", "contact_name", @@ -132,9 +178,15 @@ class ShipmentSerializer(serializers.ModelSerializer): return obj.created_by.username return None + def get_fabric(self, obj): + return _resolve_shipment_fabric(obj) + def get_items_count(self, obj): return obj.items.filter(delete_at__isnull=True).count() + def get_order_description(self, obj): + return _resolve_shipment_order_description(obj) + def get_cancelled_by_name(self, obj): if obj.cancelled_by: employee = getattr(obj.cancelled_by, "employee", None) @@ -494,6 +546,8 @@ 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) + fabric = serializers.SerializerMethodField() + order_description = serializers.SerializerMethodField() delivery_id = serializers.IntegerField(read_only=True, allow_null=True) class Meta: @@ -502,6 +556,8 @@ class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer): "id", "customer", "customer_name", + "fabric", + "order_description", "shipment_date", "status", "status_display", @@ -510,6 +566,12 @@ class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer): ] read_only_fields = fields + def get_fabric(self, obj): + return _resolve_shipment_fabric(obj) + + def get_order_description(self, obj): + return _resolve_shipment_order_description(obj) + class ShipmentDeliverySerializer(serializers.ModelSerializer): status_display = serializers.CharField(source="get_status_display", read_only=True) @@ -542,6 +604,7 @@ class ShipmentDeliverySerializer(serializers.ModelSerializer): "vehicle_capacity", "remark", "internal_remark", + "shipment_order_ids", "status", "status_display", "started_at", @@ -617,6 +680,12 @@ class ShipmentDeliveryCreateSerializer(serializers.Serializer): allow_blank=True, help_text="内部备注(可选)", ) + shipment_order_ids = serializers.JSONField( + required=False, + allow_null=True, + default=list, + help_text="前端自管的出货单顺序 ID 数组(可选,可为 null)", + ) shipments = serializers.ListField( child=serializers.IntegerField(), required=False, @@ -627,6 +696,11 @@ class ShipmentDeliveryCreateSerializer(serializers.Serializer): def validate_shipments(self, value): return list(dict.fromkeys(value)) if value else [] + def validate_shipment_order_ids(self, value): + if value is not None and not isinstance(value, list): + raise serializers.ValidationError("shipment_order_ids 必须是数组或 null") + return value + class ShipmentDeliveryUpdateSerializer(serializers.Serializer): driver_name = serializers.CharField( @@ -647,6 +721,11 @@ class ShipmentDeliveryUpdateSerializer(serializers.Serializer): internal_remark = serializers.CharField( max_length=200, required=False, allow_blank=True, help_text="内部备注(可选)" ) + shipment_order_ids = serializers.JSONField( + required=False, + allow_null=True, + help_text="前端自管的出货单顺序 ID 数组(可选,可为 null)", + ) shipments = serializers.ListField( child=serializers.IntegerField(), required=False, @@ -656,6 +735,11 @@ class ShipmentDeliveryUpdateSerializer(serializers.Serializer): def validate_shipments(self, value): return list(dict.fromkeys(value)) if value else [] + def validate_shipment_order_ids(self, value): + if value is not None and not isinstance(value, list): + raise serializers.ValidationError("shipment_order_ids 必须是数组或 null") + return value + class ShipmentDeliveryStatusUpdateSerializer(serializers.Serializer): status = serializers.ChoiceField( diff --git a/api_v1/views/shipment/test_api.py b/api_v1/views/shipment/test_api.py index b5eb23c..507b2a6 100644 --- a/api_v1/views/shipment/test_api.py +++ b/api_v1/views/shipment/test_api.py @@ -1633,6 +1633,7 @@ class ShipmentQueryAPITestCase(TestCase): process=self.process1, created_by=self.user1, external_order_id="QUERY-PO-001", + description="查询订单备注", ) self.printing_job1 = printing_models.PrintingJob.objects.create( merchant=self.merchant1, @@ -1692,6 +1693,8 @@ class ShipmentQueryAPITestCase(TestCase): self.assertIn("area", item) self.assertIn("delivery_id", item) self.assertIsNone(item["delivery_id"]) + self.assertEqual(item["fabric"], "查询面料") + self.assertEqual(item["order_description"], "查询订单备注") self.assertEqual(item["area"], "A1") self.assertEqual(item["status"], shipment_models.ShipmentStatus.DRAFT) self.assertEqual(item["status_display"], "草稿(未发布)") @@ -1837,6 +1840,8 @@ class ShipmentQueryAPITestCase(TestCase): self.assertIn("contact_phone", result) self.assertIn("delivery_id", result) self.assertIsNone(result["delivery_id"]) + self.assertEqual(result["fabric"], "查询面料") + self.assertEqual(result["order_description"], "查询订单备注") self.assertEqual(result["id"], self.shipment1.id) self.assertIn("area", result) self.assertEqual(result["area"], "A1") @@ -1949,6 +1954,25 @@ class ShipmentQueryAPITestCase(TestCase): self.assertIn("remark", target) self.assertEqual(target["remark"], "X备注") + def test_list_shipments_without_sales_items_returns_null_fabric(self): + shipment_without_sales_items = shipment_models.Shipment.objects.create( + merchant=self.merchant1, + customer=self.customer1, + shipment_date="2026-01-20", + created_by=self.user1, + remark="no-items", + area="A5", + ) + + resp = self.client.get("/api/v1/shipment/shipments/") + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + item = next(it for it in resp.json()["results"] if it["id"] == shipment_without_sales_items.id) + self.assertIn("fabric", item) + self.assertIsNone(item["fabric"]) + self.assertIn("order_description", item) + self.assertIsNone(item["order_description"]) + def test_retrieve_other_merchant_shipment_404(self): resp = self.client.get(f"/api/v1/shipment/shipments/{self.shipment2.id}/") self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) @@ -2697,6 +2721,7 @@ class ShipmentDeliveryAPITestCase(APITestCase): "vehicle_capacity": "9.6米厢车", "remark": "先装车", "internal_remark": "注意对账", + "shipment_order_ids": [self.shipment2.id, self.shipment1.id], "shipments": [self.shipment1.id, self.shipment2.id], }, format="json", @@ -2710,6 +2735,7 @@ class ShipmentDeliveryAPITestCase(APITestCase): self.assertEqual(data["vehicle_capacity"], "9.6米厢车") self.assertEqual(data["remark"], "先装车") self.assertEqual(data["internal_remark"], "注意对账") + self.assertEqual(data["shipment_order_ids"], [self.shipment2.id, self.shipment1.id]) self.assertEqual(data["status"], shipment_models.ShipmentDeliveryStatus.PENDING) self.assertEqual(data["shipments_count"], 2) self.assertEqual(data["merchant_id"], self.merchant.id) @@ -2722,6 +2748,34 @@ class ShipmentDeliveryAPITestCase(APITestCase): self.assertIsNotNone(self.shipment1.delivery_id) self.assertEqual(self.shipment1.delivery_id, self.shipment2.delivery_id) + def test_create_delivery_defaults_shipment_order_ids_to_empty_list(self): + resp = self.client.post( + "/api/v1/shipment/deliveries/", + { + "driver_name": "张司机", + "vehicle_trip": "KD-001-A", + "shipments": [self.shipment1.id], + }, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + self.assertEqual(resp.json()["shipment_order_ids"], []) + + def test_create_delivery_rejects_non_array_shipment_order_ids(self): + resp = self.client.post( + "/api/v1/shipment/deliveries/", + { + "driver_name": "张司机", + "vehicle_trip": "KD-001-B", + "shipment_order_ids": {"bad": "value"}, + }, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("shipment_order_ids", resp.json()) + def test_create_delivery_rejects_shipment_bound_to_other_delivery(self): resp = self.client.post( "/api/v1/shipment/deliveries/", @@ -2817,6 +2871,11 @@ class ShipmentDeliveryAPITestCase(APITestCase): for item in data["shipments"]: self.assertIn("delivery_id", item) self.assertEqual(item["delivery_id"], delivery.id) + self.assertIn("fabric", item) + self.assertIn("order_description", item) + first_shipment = next(item for item in data["shipments"] if item["id"] == self.shipment1.id) + self.assertIsNone(first_shipment["fabric"]) + self.assertIsNone(first_shipment["order_description"]) def test_patch_delivery_updates_fields_and_replaces_shipments(self): delivery = shipment_models.ShipmentDelivery.objects.create( @@ -2837,6 +2896,7 @@ class ShipmentDeliveryAPITestCase(APITestCase): "vehicle_capacity": "13米高栏", "remark": "改派车辆", "internal_remark": "已电话确认", + "shipment_order_ids": [self.shipment2.id], "shipments": [self.shipment2.id], }, format="json", @@ -2850,6 +2910,7 @@ class ShipmentDeliveryAPITestCase(APITestCase): self.assertEqual(data["vehicle_capacity"], "13米高栏") self.assertEqual(data["remark"], "改派车辆") self.assertEqual(data["internal_remark"], "已电话确认") + self.assertEqual(data["shipment_order_ids"], [self.shipment2.id]) self.assertEqual(data["operator_id"], self.employee.id) self.assertEqual(data["shipments_count"], 1) self.assertEqual(data["shipments"][0]["id"], self.shipment2.id) @@ -2859,6 +2920,45 @@ class ShipmentDeliveryAPITestCase(APITestCase): self.assertIsNone(self.shipment1.delivery_id) self.assertEqual(self.shipment2.delivery_id, delivery.id) + def test_patch_delivery_allows_null_shipment_order_ids(self): + delivery = shipment_models.ShipmentDelivery.objects.create( + merchant=self.merchant, + driver_name="张司机", + vehicle_trip="KD-005-N", + shipment_order_ids=[self.shipment1.id], + created_by=self.user, + ) + + resp = self.client.patch( + f"/api/v1/shipment/deliveries/{delivery.id}/", + { + "shipment_order_ids": None, + }, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + self.assertIsNone(resp.json()["shipment_order_ids"]) + + def test_patch_delivery_rejects_non_array_shipment_order_ids(self): + delivery = shipment_models.ShipmentDelivery.objects.create( + merchant=self.merchant, + driver_name="张司机", + vehicle_trip="KD-005-C", + created_by=self.user, + ) + + resp = self.client.patch( + f"/api/v1/shipment/deliveries/{delivery.id}/", + { + "shipment_order_ids": "not-a-list", + }, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("shipment_order_ids", resp.json()) + def test_modify_delivery_status_sets_timestamps(self): delivery = shipment_models.ShipmentDelivery.objects.create( merchant=self.merchant, diff --git a/api_v1/views/shipment/views.py b/api_v1/views/shipment/views.py index c828766..c59d165 100644 --- a/api_v1/views/shipment/views.py +++ b/api_v1/views/shipment/views.py @@ -423,6 +423,7 @@ class ShipmentDeliveryListCreateView(ListModelMixin, GenericAPIView): contact_phone=serializer.validated_data.get("contact_phone", ""), vehicle_capacity=serializer.validated_data.get("vehicle_capacity", ""), shipment_ids=serializer.validated_data.get("shipments", []), + shipment_order_ids=serializer.validated_data.get("shipment_order_ids", []), created_by=request.user, remark=serializer.validated_data.get("remark", ""), internal_remark=serializer.validated_data.get("internal_remark", ""), @@ -483,7 +484,7 @@ class ShipmentDeliveryDetailView(RetrieveModelMixin, GenericAPIView): if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - from shipment.services import update_shipment_delivery + from shipment.services import UNSET, update_shipment_delivery try: delivery = update_shipment_delivery( @@ -493,6 +494,11 @@ class ShipmentDeliveryDetailView(RetrieveModelMixin, GenericAPIView): contact_phone=serializer.validated_data.get("contact_phone"), vehicle_capacity=serializer.validated_data.get("vehicle_capacity"), shipment_ids=serializer.validated_data.get("shipments"), + shipment_order_ids=( + serializer.validated_data["shipment_order_ids"] + if "shipment_order_ids" in serializer.validated_data + else UNSET + ), remark=serializer.validated_data.get("remark"), internal_remark=serializer.validated_data.get("internal_remark"), operator=request.user, diff --git a/api_v2/views/ai.py b/api_v2/views/ai.py index 0d0f69a..3541efd 100644 --- a/api_v2/views/ai.py +++ b/api_v2/views/ai.py @@ -101,6 +101,8 @@ 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) + fabric = serializers.SerializerMethodField() + order_description = serializers.SerializerMethodField() delivery_id = serializers.IntegerField(read_only=True, allow_null=True) sales_items = serializers.SerializerMethodField() @@ -111,6 +113,8 @@ class AgentShipmentListSerializer(serializers.ModelSerializer): "merchant_id", "customer", "customer_name", + "fabric", + "order_description", "shipment_date", "address", "contact_name", @@ -128,6 +132,46 @@ class AgentShipmentListSerializer(serializers.ModelSerializer): "updated_at", ] + def get_fabric(self, obj): + rel = getattr(obj, "items", None) + if rel is None: + return None + + first_item = next((item for item in obj.items.all() if item.delete_at is None), None) + if first_item is None or not first_item.printing_job_id: + return None + + from printing.models import PrintingJob + + printing_job = ( + PrintingJob.objects.filter(id=first_item.printing_job_id) + .select_related("printing_order") + .first() + ) + if printing_job is None or printing_job.printing_order is None: + return None + return printing_job.printing_order.fabric + + def get_order_description(self, obj): + rel = getattr(obj, "items", None) + if rel is None: + return None + + first_item = next((item for item in obj.items.all() if item.delete_at is None), None) + if first_item is None or not first_item.printing_job_id: + return None + + from printing.models import PrintingJob + + printing_job = ( + PrintingJob.objects.filter(id=first_item.printing_job_id) + .select_related("printing_order") + .first() + ) + if printing_job is None or printing_job.printing_order is None: + return None + return printing_job.printing_order.description + def get_sales_items(self, obj): items = [i for i in obj.items.all() if i.delete_at is None] return AgentSalesItemSerializer(items, many=True, context=self.context).data diff --git a/docs/api_v2_agent_api.md b/docs/api_v2_agent_api.md index 02d459c..7ebe0fc 100644 --- a/docs/api_v2_agent_api.md +++ b/docs/api_v2_agent_api.md @@ -45,6 +45,8 @@ Authorization: "merchant_id": 10, "customer": 5, "customer_name": "客户A", + "fabric": "棉布 40S", + "order_description": "订单备注", "shipment_date": "2026-04-14", "address": "广州市天河区", "contact_name": "张三", @@ -154,6 +156,8 @@ Authorization: - "未出货"定义:`delivery` 为空,即尚未关联送货单 - `delivery_id` 始终返回送货单ID;未关联时为 `null` +- `fabric` 来自该出货单所关联销售品对应生产订单的 `fabric`;若当前出货单没有销售品则返回 `null` +- `order_description` 来自该出货单所关联销售品对应生产订单的 `description`;若当前出货单没有销售品则返回 `null` - 结果按 `created_at` 降序排列 - 只返回 `merchant_id` 对应商户的数据 - `sales_items` 包含该出货单的所有销售品(软删除的条目自动排除) diff --git a/docs/shipment_api.md b/docs/shipment_api.md index 58c5f9d..a84f6dd 100644 --- a/docs/shipment_api.md +++ b/docs/shipment_api.md @@ -53,6 +53,8 @@ "merchant_name": "测试印花厂", "customer": 1, "customer_name": "客户A", + "fabric": "棉布 40S", + "order_description": "订单备注", "shipment_date": "2026-01-14", "status": 1, "status_display": "草稿(未发布)", @@ -79,6 +81,8 @@ - `sales_items` 现在返回的是带图片字段的销售品详情结构 - `delivery_id` 始终返回送货单ID;未绑定送货单时为 `null` +- `fabric` 来自该出货单所关联销售品对应生产订单的 `fabric`;若当前出货单没有销售品,则返回 `null` +- `order_description` 来自该出货单所关联销售品对应生产订单的 `description`;若当前出货单没有销售品,则返回 `null` - `delivery_id=null` 不会触发过滤;如需按是否绑定送货单筛选,请使用 `delivery_isnull` - 若销售品关联的 `PrintingJob.product` 存在主图,则会返回 `product_image_url` - 若无关联图片,则 `product_image_url` 为 `null` @@ -226,6 +230,8 @@ | merchant_name | string | 所属商户名称(从当前登录用户推导) | | customer | int | 客户ID | | customer_name | string | 客户名称 | +| fabric | string/null | 面料;来自关联生产订单,若当前出货单没有销售品则为 null | +| order_description | string/null | 订单备注;来自关联生产订单的 `description`,若当前出货单没有销售品则为 null | | shipment_date | string | 出货日期 | | delivery_id | int/null | 关联送货单ID;未绑定时为 null | | address | string | 地址 | diff --git a/docs/shipment_delivery.md b/docs/shipment_delivery.md index 55d1f2b..a170f88 100644 --- a/docs/shipment_delivery.md +++ b/docs/shipment_delivery.md @@ -47,6 +47,7 @@ - `vehicle_capacity` - `remark` - `internal_remark` +- `shipment_order_ids` - `status` - `started_at` - `delivered_at` @@ -67,6 +68,7 @@ - `merchant` 用于多商户隔离 - 创建、修改、状态流转时,会根据 `request.user.employee` 自动写入 `operator` - 取消送货单时会写入 `cancelled_at` 与 `cancelled_by` +- `shipment_order_ids` 是前端自管的出货单顺序 JSON 字段;后端只保存和返回,不据此自动排序 `shipments` ## API 列表 @@ -112,6 +114,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit "vehicle_capacity": "9.6米厢车", "remark": "先装车", "internal_remark": "注意对账", + "shipment_order_ids": [11, 10], "status": 1, "status_display": "待送货", "started_at": null, @@ -123,6 +126,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit "id": 10, "customer": 5, "customer_name": "客户A", + "order_description": null, "shipment_date": "2026-04-03", "status": 2, "status_display": "已发布", @@ -156,6 +160,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit "vehicle_capacity": "9.6米厢车", "remark": "先装车", "internal_remark": "注意对账", + "shipment_order_ids": [11, 10], "shipments": [10, 11] } ``` @@ -168,6 +173,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit - `vehicle_capacity`: 可选,车辆容量 - `remark`: 可选,备注 - `internal_remark`: 可选,内部备注 +- `shipment_order_ids`: 可选,前端自管的出货单顺序 JSON 数组;可传 `null`;未传时默认 `[]` - `shipments`: 可选,出货单 ID 列表 创建规则: @@ -198,6 +204,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit "vehicle_capacity": "13米高栏", "remark": "改派车辆", "internal_remark": "已电话确认", + "shipment_order_ids": [11, 12], "shipments": [11, 12] } ``` @@ -205,6 +212,7 @@ GET /api/v1/shipment/deliveries/?status=2&driver_name=张&vehicle_trip=001&limit 修改规则: - `driver_name`、`vehicle_trip`、`contact_phone`、`vehicle_capacity`、`remark`、`internal_remark` 可单独修改 +- `shipment_order_ids` 按普通字段处理,后端仅保存与返回;值必须是数组或 `null` - `shipments` 如果传入,则视为“整体替换当前绑定的出货单集合” - 替换绑定时,出货单仍然必须满足“当前商户、已审核、未绑定到其他送货单” - 更新接口不支持直接修改 `status` diff --git a/shipment/migrations/0023_shipmentdelivery_shipment_order_ids.py b/shipment/migrations/0023_shipmentdelivery_shipment_order_ids.py new file mode 100644 index 0000000..6d0149c --- /dev/null +++ b/shipment/migrations/0023_shipmentdelivery_shipment_order_ids.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shipment', '0022_shipment_rejected_sales_item_ids'), + ] + + operations = [ + migrations.AddField( + model_name='shipmentdelivery', + name='shipment_order_ids', + field=models.JSONField(blank=True, default=list, help_text='前端自管的出货单展示顺序,后端不据此重排 shipments 关系', null=True, verbose_name='出货单顺序ID列表'), + ), + ] \ No newline at end of file diff --git a/shipment/models.py b/shipment/models.py index cb87c0e..fdaaf09 100644 --- a/shipment/models.py +++ b/shipment/models.py @@ -561,6 +561,14 @@ class ShipmentDelivery(ModelBase): verbose_name='内部备注', ) + shipment_order_ids = models.JSONField( + null=True, + blank=True, + default=list, + verbose_name='出货单顺序ID列表', + help_text='前端自管的出货单展示顺序,后端不据此重排 shipments 关系', + ) + started_at = models.DateTimeField( null=True, blank=True, diff --git a/shipment/services.py b/shipment/services.py index 87814ab..b410e5b 100644 --- a/shipment/services.py +++ b/shipment/services.py @@ -28,6 +28,9 @@ from shipment.models import ( from shipment.signals import shipment_created +UNSET = object() + + def _resolve_user_merchant(user): emp = getattr(user, "employee", None) return getattr(emp, "merchant", None) if emp else None @@ -405,6 +408,7 @@ def create_shipment_delivery( contact_phone: str = "", vehicle_capacity: str = "", shipment_ids: list[int], + shipment_order_ids: list | None = None, created_by, remark: str = "", internal_remark: str = "", @@ -434,6 +438,7 @@ def create_shipment_delivery( vehicle_capacity=(vehicle_capacity or "").strip(), remark=(remark or "").strip(), internal_remark=(internal_remark or "").strip(), + shipment_order_ids=shipment_order_ids, created_by=created_by, operator=operator, ) @@ -453,6 +458,7 @@ def update_shipment_delivery( contact_phone: str | None = None, vehicle_capacity: str | None = None, shipment_ids: list[int] | None = None, + shipment_order_ids=UNSET, remark: str | None = None, internal_remark: str | None = None, operator=None, @@ -481,6 +487,9 @@ def update_shipment_delivery( if internal_remark is not None: delivery.internal_remark = (internal_remark or "").strip() + if shipment_order_ids is not UNSET: + delivery.shipment_order_ids = shipment_order_ids + if shipment_ids is not None: shipments = _resolve_delivery_shipments( shipment_ids=shipment_ids,