forked from erp-dev/erp
feat: added some filed to shipment serializers
This commit is contained in:
1
.env
1
.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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,6 +45,8 @@ Authorization: <AGENT_ACCESS_KEY>
|
||||
"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: <AGENT_ACCESS_KEY>
|
||||
|
||||
- "未出货"定义:`delivery` 为空,即尚未关联送货单
|
||||
- `delivery_id` 始终返回送货单ID;未关联时为 `null`
|
||||
- `fabric` 来自该出货单所关联销售品对应生产订单的 `fabric`;若当前出货单没有销售品则返回 `null`
|
||||
- `order_description` 来自该出货单所关联销售品对应生产订单的 `description`;若当前出货单没有销售品则返回 `null`
|
||||
- 结果按 `created_at` 降序排列
|
||||
- 只返回 `merchant_id` 对应商户的数据
|
||||
- `sales_items` 包含该出货单的所有销售品(软删除的条目自动排除)
|
||||
|
||||
@@ -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 | 地址 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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列表'),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user