1
0
forked from erp-dev/erp
Files
erpnew/api_v1/views/shipment/views.py

1082 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Shipment API ViewSet
"""
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from flower.viewsets import LimitedLimitOffsetPagination
from shipment.models import Shipment, ShipmentDelivery
from .serializers import (
SalesItemDetailSerializer,
SalesItemSerializer,
SalesItemUpdateSerializer,
ShipmentStatusUpdateSerializer,
ShipmentDeliveryBindShipmentsSerializer,
ShipmentDeliveryCreateSerializer,
ShipmentDeliverySerializer,
ShipmentDeliveryStatusUpdateSerializer,
ShipmentDeliveryUpdateSerializer,
ShipmentSerializer,
ShipmentCreateNormalSerializer,
ShipmentCreateExternalSerializer,
ShipmentSalesItemCustomerSerializer,
ShipmentUpdateSerializer,
)
def _build_sales_item_serializer_context(items):
customer_ids = {item.customer_id for item in items if item.customer_id}
printing_job_ids = {item.printing_job_id for item in items if item.printing_job_id}
customer_name_map = {}
if customer_ids:
from basic_info.models import Customer
customer_name_map = dict(
Customer.objects.filter(id__in=customer_ids).values_list("id", "name")
)
printing_order_map = {}
external_order_id_map = {}
product_image_map = {}
if printing_job_ids:
from printing.models import PrintingJob
printing_jobs = list(
PrintingJob.objects.filter(id__in=printing_job_ids)
.select_related("printing_order", "product")
)
printing_order_map = {
job.id: job.printing_order_id for job in printing_jobs
}
external_order_id_map = {
job.id: getattr(job.printing_order, "external_order_id", None)
for job in printing_jobs
}
for job in printing_jobs:
if not getattr(job, "product", None):
product_image_map[job.id] = None
continue
primary_url = job.product.get_primary_image_url()
if primary_url:
product_image_map[job.id] = primary_url
elif job.product.image:
product_image_map[job.id] = job.product.image.url
else:
product_image_map[job.id] = None
return {
"customer_name_map": customer_name_map,
"printing_order_map": printing_order_map,
"external_order_id_map": external_order_id_map,
"product_image_map": product_image_map,
}
class ShipmentListCreateView(ListModelMixin, GenericAPIView):
"""
出货单:查询列表 / 创建
- GET /api/v1/shipment/shipments/
- POST /api/v1/shipment/shipments/
请求体:
{
"customer": 1,
"shipment_date": "2026-01-14",
"remark": "备注信息(可选)",
"sales_items": [1, 2, 3]
}
返回:
{
"id": 1,
"customer": 1,
"customer_name": "客户A",
"shipment_date": "2026-01-14",
"remark": "备注信息",
"items_count": 3,
"created_by_id": 1,
"created_by_name": "张三",
"created_at": "2026-01-14T10:00:00Z",
"updated_at": "2026-01-14T10:00:00Z"
}
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSerializer
pagination_class = LimitedLimitOffsetPagination
def get_queryset(self):
"""
仅返回当前用户所属商户的出货单merchant 隔离)。
支持过滤参数(可选):
- customer: 客户ID
- status: 状态1=草稿, 2=已发布, 3=已取消, 4=已驳回, 5=已审核)
- external_id: 外部订单号(精确匹配)
- shipment_date_from: 出货日期起始YYYY-MM-DD
- shipment_date_to: 出货日期结束YYYY-MM-DD包含整天
"""
qs = (
Shipment.objects.all()
.select_related("merchant", "customer", "created_by", "cancelled_by")
.prefetch_related(
"items",
"external_finished_products",
)
)
user = self.request.user
if not getattr(user, "is_superuser", False):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Shipment.objects.none()
qs = qs.filter(merchant=merchant)
# optional filters
customer_id = self.request.query_params.get("customer")
if customer_id:
qs = qs.filter(customer_id=customer_id)
status_val = self.request.query_params.get("status")
if status_val:
qs = qs.filter(status=status_val)
external_id = self.request.query_params.get("external_id")
if external_id:
qs = qs.filter(external_id=external_id)
date_from = self.request.query_params.get("shipment_date_from")
if date_from:
qs = qs.filter(shipment_date__gte=date_from)
date_to = self.request.query_params.get("shipment_date_to")
if date_to:
# 通过 <= 过滤日期
qs = qs.filter(shipment_date__lte=date_to)
return qs.order_by("-created_at", "-id")
def get(self, request):
return self.list(request)
def post(self, request):
# 验证请求数据
serializer = ShipmentCreateNormalSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
# 调用业务逻辑
from shipment.services import create_shipment
try:
shipment = create_shipment(
customer_id=data["customer"],
shipment_date=data["shipment_date"],
sales_item_ids=data["sales_items"],
created_by=request.user,
address=data.get("address", ""),
contact_name=data.get("contact_name", ""),
contact_phone=data.get("contact_phone", ""),
remark=data.get("remark", ""),
area=data.get("area", ""),
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
# 返回创建的出货单
response_serializer = ShipmentSerializer(shipment)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ShipmentDetailView(RetrieveModelMixin, GenericAPIView):
"""
出货单详情
GET /api/v1/shipment/shipments/<id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSerializer
def get_queryset(self):
qs = (
Shipment.objects.all()
.select_related("merchant", "customer", "created_by", "cancelled_by")
.prefetch_related(
"items",
"external_finished_products",
)
)
user = self.request.user
if getattr(user, "is_superuser", False):
return qs
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Shipment.objects.none()
qs = qs.filter(merchant=merchant)
status_val = self.request.query_params.get("status")
if status_val:
qs = qs.filter(status=status_val)
return qs
def get(self, request, pk: int):
return self.retrieve(request, pk=pk)
def patch(self, request, pk: int):
"""
更新出货单(部分更新)
PATCH /api/v1/shipment/shipments/<id>/
"""
shipment = self.get_object()
serializer = ShipmentUpdateSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
from shipment.services import update_shipment
try:
shipment = update_shipment(
shipment,
customer_id=data.get("customer"),
shipment_date=data.get("shipment_date"),
address=data.get("address"),
contact_name=data.get("contact_name"),
contact_phone=data.get("contact_phone"),
area=data.get("area"),
remark=data.get("remark"),
external_id=data.get("external_id"),
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
return Response(ShipmentSerializer(shipment).data, status=status.HTTP_200_OK)
def put(self, request, pk: int):
"""
更新出货单(全量更新,当前实现允许缺省字段,等同于 PATCH
PUT /api/v1/shipment/shipments/<id>/
"""
return self.patch(request, pk=pk)
class ShipmentStatusUpdateView(APIView):
"""
更新出货单状态
PATCH /api/v1/shipment/shipments/<id>/status/
PUT /api/v1/shipment/shipments/<id>/status/
"""
permission_classes = [IsAuthenticated]
def get_queryset(self):
qs = Shipment.objects.all().select_related(
"merchant",
"customer",
"created_by",
"cancelled_by",
"approved_by",
).prefetch_related(
"items",
"external_finished_products",
)
user = self.request.user
if getattr(user, "is_superuser", False):
return qs
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Shipment.objects.none()
return qs.filter(merchant=merchant)
def patch(self, request, pk: int):
shipment = self.get_queryset().filter(id=pk).first()
if shipment is None:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = ShipmentStatusUpdateSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.models import ShipmentStatus
from shipment.services import modify_status
target_status = serializer.validated_data["status"]
approved_by = request.user if target_status == ShipmentStatus.APPROVED else None
try:
shipment = modify_status(
shipment,
target_status=target_status,
operator=request.user,
approved_by=approved_by,
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
return Response(ShipmentSerializer(shipment).data, status=status.HTTP_200_OK)
def put(self, request, pk: int):
return self.patch(request, pk=pk)
class ShipmentDeliveryListCreateView(ListModelMixin, GenericAPIView):
"""
送货单:查询列表 / 创建
- GET /api/v1/shipment/deliveries/
- POST /api/v1/shipment/deliveries/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentDeliverySerializer
pagination_class = LimitedLimitOffsetPagination
def get_queryset(self):
qs = ShipmentDelivery.objects.all().select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
)
user = self.request.user
if not getattr(user, "is_superuser", False):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return ShipmentDelivery.objects.none()
qs = qs.filter(merchant=merchant)
status_val = self.request.query_params.get("status")
if status_val:
qs = qs.filter(status=status_val)
driver_name = (self.request.query_params.get("driver_name") or "").strip()
if driver_name:
qs = qs.filter(driver_name__icontains=driver_name)
vehicle_trip = (self.request.query_params.get("vehicle_trip") or "").strip()
if vehicle_trip:
qs = qs.filter(vehicle_trip__icontains=vehicle_trip)
return qs.order_by("-created_at", "-id")
def get(self, request):
return self.list(request)
def post(self, request):
serializer = ShipmentDeliveryCreateSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import create_shipment_delivery
try:
delivery = create_shipment_delivery(
driver_name=serializer.validated_data["driver_name"],
vehicle_trip=serializer.validated_data["vehicle_trip"],
contact_phone=serializer.validated_data.get("contact_phone", ""),
vehicle_capacity=serializer.validated_data.get("vehicle_capacity", ""),
shipment_ids=serializer.validated_data.get("shipments", []),
created_by=request.user,
remark=serializer.validated_data.get("remark", ""),
internal_remark=serializer.validated_data.get("internal_remark", ""),
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
delivery = ShipmentDelivery.objects.select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
).get(id=delivery.id)
return Response(
ShipmentDeliverySerializer(delivery).data,
status=status.HTTP_201_CREATED,
)
class ShipmentDeliveryDetailView(RetrieveModelMixin, GenericAPIView):
"""
送货单详情 / 修改 / 删除
- GET /api/v1/shipment/deliveries/<id>/
- PATCH /api/v1/shipment/deliveries/<id>/
- PUT /api/v1/shipment/deliveries/<id>/
- DELETE /api/v1/shipment/deliveries/<id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentDeliverySerializer
def get_queryset(self):
qs = ShipmentDelivery.objects.all().select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
)
user = self.request.user
if getattr(user, "is_superuser", False):
return qs
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return ShipmentDelivery.objects.none()
return qs.filter(merchant=merchant)
def get(self, request, pk: int):
return self.retrieve(request, pk=pk)
def patch(self, request, pk: int):
delivery = self.get_object()
serializer = ShipmentDeliveryUpdateSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import update_shipment_delivery
try:
delivery = update_shipment_delivery(
delivery,
driver_name=serializer.validated_data.get("driver_name"),
vehicle_trip=serializer.validated_data.get("vehicle_trip"),
contact_phone=serializer.validated_data.get("contact_phone"),
vehicle_capacity=serializer.validated_data.get("vehicle_capacity"),
shipment_ids=serializer.validated_data.get("shipments"),
remark=serializer.validated_data.get("remark"),
internal_remark=serializer.validated_data.get("internal_remark"),
operator=request.user,
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
delivery = self.get_queryset().get(id=delivery.id)
return Response(
ShipmentDeliverySerializer(delivery).data,
status=status.HTTP_200_OK,
)
def put(self, request, pk: int):
return self.patch(request, pk=pk)
def delete(self, request, pk: int):
delivery = self.get_object()
from shipment.services import delete_shipment_delivery
delete_shipment_delivery(delivery)
return Response(status=status.HTTP_204_NO_CONTENT)
class ShipmentDeliveryStatusUpdateView(APIView):
"""
修改送货单状态
POST /api/v1/shipment/deliveries/<id>/status/
"""
permission_classes = [IsAuthenticated]
def post(self, request, pk: int):
user = request.user
queryset = ShipmentDelivery.objects.all()
if not getattr(user, "is_superuser", False):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
queryset = queryset.filter(merchant=merchant)
delivery = queryset.filter(id=pk).first()
if delivery is None:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
serializer = ShipmentDeliveryStatusUpdateSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import modify_shipment_delivery_status
try:
delivery = modify_shipment_delivery_status(
delivery,
target_status=serializer.validated_data["status"],
operator=request.user,
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
delivery = ShipmentDelivery.objects.select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
).get(id=delivery.id)
return Response(
ShipmentDeliverySerializer(delivery).data,
status=status.HTTP_200_OK,
)
class ShipmentDeliveryCancelView(APIView):
"""
取消送货单
POST /api/v1/shipment/deliveries/<id>/cancel/
"""
permission_classes = [IsAuthenticated]
def post(self, request, pk: int):
if not request.user.has_perm("shipment.cancel_shipmentdelivery"):
return Response(
{"detail": "没有权限取消送货单"},
status=status.HTTP_403_FORBIDDEN,
)
user = request.user
queryset = ShipmentDelivery.objects.all()
if not getattr(user, "is_superuser", False):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
queryset = queryset.filter(merchant=merchant)
delivery = queryset.filter(id=pk).first()
if delivery is None:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
from shipment.services import cancel_shipment_delivery
delivery = cancel_shipment_delivery(
delivery,
cancelled_by=request.user,
operator=request.user,
)
delivery = ShipmentDelivery.objects.select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
).get(id=delivery.id)
return Response(
ShipmentDeliverySerializer(delivery).data,
status=status.HTTP_200_OK,
)
class ShipmentDeliveryBindShipmentsView(APIView):
"""
追加绑定出货单到现有送货单
POST /api/v1/shipment/deliveries/<id>/bind-shipments/
"""
permission_classes = [IsAuthenticated]
def post(self, request, pk: int):
user = request.user
queryset = ShipmentDelivery.objects.all()
if not getattr(user, "is_superuser", False):
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
queryset = queryset.filter(merchant=merchant)
delivery = queryset.filter(id=pk).first()
if delivery is None:
return Response({"detail": "未找到送货单"}, status=status.HTTP_404_NOT_FOUND)
serializer = ShipmentDeliveryBindShipmentsSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import bind_shipments_to_delivery
try:
delivery = bind_shipments_to_delivery(
delivery,
shipment_ids=serializer.validated_data["shipments"],
operator=request.user,
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
delivery = ShipmentDelivery.objects.select_related(
"merchant", "created_by", "operator", "cancelled_by"
).prefetch_related(
"shipments",
"shipments__customer",
).get(id=delivery.id)
return Response(
ShipmentDeliverySerializer(delivery).data,
status=status.HTTP_200_OK,
)
class ShipmentExternalCreateView(APIView):
"""
创建出货单external 版)
POST /api/v1/shipment/shipments/external/
特点:
- external_id 必填
- external_finished_products 必填(数组)
- 不绑定任何销售品
"""
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = ShipmentCreateExternalSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
from shipment.services import create_external_shipment
try:
shipment = create_external_shipment(
customer_id=data["customer"],
shipment_date=data["shipment_date"],
external_id=data["external_id"],
external_finished_products=data["external_finished_products"],
created_by=request.user,
address=data.get("address", ""),
contact_name=data.get("contact_name", ""),
contact_phone=data.get("contact_phone", ""),
remark=data.get("remark", ""),
area=data.get("area", ""),
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
response_serializer = ShipmentSerializer(shipment)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ShipmentSalesItemCustomerListView(GenericAPIView):
"""
从未出货销售品反推出“当前可出货客户”列表。
GET /api/v1/shipment/sales-items/customers/
"""
permission_classes = [IsAuthenticated]
serializer_class = ShipmentSalesItemCustomerSerializer
pagination_class = LimitedLimitOffsetPagination
def get_queryset(self):
user = self.request.user
if getattr(user, "is_superuser", False):
from basic_info.models import Customer, Merchant
merchant_id = self.request.query_params.get("merchant")
if not merchant_id:
return Customer.objects.none()
try:
merchant = Merchant.objects.get(id=merchant_id)
except Merchant.DoesNotExist:
return Customer.objects.none()
else:
from basic_info.models import Customer
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Customer.objects.none()
from shipment.services import get_customers_with_unshipped_sales_items
return get_customers_with_unshipped_sales_items(merchant=merchant)
def get(self, request):
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page if page is not None else queryset, many=True)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
class SalesItemByPrintingOrderView(APIView):
"""
通过生产订单查询销售品
GET /api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/
返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。
路径参数既支持内部 ID也支持 external_order_id。
查询参数:
- include_already_has_shipment: 是否包含已关联出货单的销售品true/false默认 false
返回:
{
"count": 5,
"results": [
{
"id": 1,
"name": "产品A",
"quantity": "100.00",
"unit": 1,
"unit_display": "",
"position": "A1-01",
"remark": "",
"printing_job_id": 123,
"customer_id": null,
"shipment_id": null,
"shipment_date": null,
"created_at": "2026-01-14T10:00:00Z",
"created_by_id": 1,
"created_by_name": "张三"
},
...
]
}
"""
permission_classes = [IsAuthenticated]
def get(self, request, printing_order_id):
from printing.models import PrintingOrder
user = request.user
if getattr(user, "is_superuser", False):
base_queryset = PrintingOrder.objects.all()
else:
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response(
{"detail": f"生产订单 {printing_order_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
base_queryset = PrintingOrder.objects.filter(merchant=merchant)
printing_order = None
if printing_order_id.isdigit():
printing_order = base_queryset.filter(id=int(printing_order_id)).first()
if printing_order is None:
matched_orders = list(
base_queryset.filter(external_order_id=printing_order_id).only("id")[:2]
)
if len(matched_orders) > 1:
return Response(
{
"detail": (
f"external_order_id {printing_order_id} 匹配到多个生产订单,"
"请改用内部ID查询"
)
},
status=status.HTTP_400_BAD_REQUEST,
)
if len(matched_orders) == 1:
printing_order = matched_orders[0]
if printing_order is None:
return Response(
{"detail": f"生产订单 {printing_order_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
# 获取查询参数
include_already_has_shipment = (
request.query_params.get("include_already_has_shipment", "false").lower()
== "true"
)
# 调用 shipment 业务逻辑
from shipment.services import get_sales_items_by_printing_order
sales_items = get_sales_items_by_printing_order(
printing_order_id=printing_order_id,
include_already_has_shipment=include_already_has_shipment,
merchant=printing_order.merchant,
)
# 序列化返回
items = list(sales_items)
serializer = SalesItemSerializer(
items,
many=True,
context=_build_sales_item_serializer_context(items),
)
return Response({"count": len(serializer.data), "results": serializer.data})
class SalesItemByCustomerView(GenericAPIView):
"""
通过客户查询销售品。
GET /api/v1/shipment/sales-items/by-customer/<customer_id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = SalesItemSerializer
pagination_class = LimitedLimitOffsetPagination
def get(self, request, customer_id: int):
from basic_info.models import Customer
user = request.user
if getattr(user, "is_superuser", False):
customer = Customer.objects.filter(id=customer_id).first()
else:
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return Response(
{"detail": f"客户 {customer_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
customer = Customer.objects.filter(id=customer_id, merchant=merchant).first()
if customer is None:
return Response(
{"detail": f"客户 {customer_id} 不存在"},
status=status.HTTP_404_NOT_FOUND,
)
include_already_has_shipment = (
request.query_params.get("include_already_has_shipment", "false").lower()
== "true"
)
external_order_id = (request.query_params.get("external_order_id") or "").strip()
from shipment.services import get_sales_items_by_customer
queryset = get_sales_items_by_customer(
merchant=customer.merchant,
customer_id=customer.id,
include_already_has_shipment=include_already_has_shipment,
external_order_id=external_order_id or None,
)
page = self.paginate_queryset(queryset)
items = list(page) if page is not None else list(queryset)
serializer = self.get_serializer(
items,
many=True,
context=_build_sales_item_serializer_context(items),
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
class SalesItemCreateView(APIView):
"""
手动创建销售品
POST /api/v1/shipment/sales-items/
当自动转化销售品开关关闭时,通过此接口手动创建销售品。
请求体:
{
"printing_job_id": 123,
"name": "产品名称",
"quantity": "100.50",
"unit": 1,
"customer_id": 456,
"remark": "备注信息",
"position": "A1-01"
}
参数说明:
- printing_job_id: 生产任务ID必填
- name: 销售品名称(必填)
- quantity: 数量(必填,支持小数)
- unit: 单位必填1=米, 2=件, 3=码, 4=个
- customer_id: 客户ID可选默认从生产订单获取
- remark: 备注(可选)
- position: 货位(可选)
返回:
{
"id": 1,
"name": "产品名称",
"quantity": "100.50",
"unit": 1,
"unit_display": "",
"position": "A1-01",
"remark": "备注信息",
"printing_job_id": 123,
"customer_id": 456,
"shipment_id": null,
"shipment_date": null,
"created_at": "2026-01-14T10:00:00Z",
"created_by_id": 1,
"created_by_name": "张三"
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
from .serializers import SalesItemCreateSerializer
serializer = SalesItemCreateSerializer(
data=request.data, context={"request": request}
)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
sales_item = serializer.save()
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
# 返回创建的销售品
response_serializer = SalesItemSerializer(
sales_item,
context=_build_sales_item_serializer_context([sales_item]),
)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class SalesItemDetailView(GenericAPIView):
"""
销售品详情。
GET /api/v1/shipment/sales-items/<id>/
PATCH /api/v1/shipment/sales-items/<id>/
"""
permission_classes = [IsAuthenticated]
serializer_class = SalesItemDetailSerializer
def get_queryset(self):
from shipment.services import get_active_sales_items_queryset
qs = (
get_active_sales_items_queryset()
.select_related("shipment", "created_by", "delete_by")
.order_by("id")
)
user = self.request.user
if getattr(user, "is_superuser", False):
return qs
emp = getattr(user, "employee", None)
merchant = getattr(emp, "merchant", None) if emp else None
if not merchant:
return SalesItem.objects.none()
return qs.filter(merchant=merchant)
def get(self, request, pk: int):
sales_item = self.get_queryset().filter(id=pk).first()
if sales_item is None:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = self.get_serializer(
sales_item,
context={
**_build_sales_item_serializer_context([sales_item]),
"request": request,
},
)
return Response(serializer.data)
def patch(self, request, pk: int):
sales_item = self.get_queryset().filter(id=pk).first()
if sales_item is None:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
allowed_fields = {"quantity", "remark", "position"}
unexpected_fields = sorted(set(request.data.keys()) - allowed_fields)
if unexpected_fields:
return Response(
{
"detail": (
"销售品仅允许修改以下字段: "
f"{', '.join(sorted(allowed_fields))}"
f"不支持字段: {', '.join(unexpected_fields)}"
)
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = SalesItemUpdateSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from shipment.services import update_sales_item
try:
sales_item = update_sales_item(
sales_item,
quantity=serializer.validated_data.get("quantity"),
remark=serializer.validated_data.get("remark"),
position=serializer.validated_data.get("position"),
operator=request.user,
)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
response_serializer = self.get_serializer(
sales_item,
context={
**_build_sales_item_serializer_context([sales_item]),
"request": request,
},
)
return Response(response_serializer.data, status=status.HTTP_200_OK)
def delete(self, request, pk: int):
if not request.user.has_perm("shipment.soft_delete_salesitem"):
return Response(
{"detail": "没有权限删除销售品"},
status=status.HTTP_403_FORBIDDEN,
)
sales_item = self.get_queryset().filter(id=pk).first()
if sales_item is None:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
from shipment.services import delete_sales_item
try:
delete_sales_item(sales_item, deleted_by=request.user)
except ValueError as e:
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
return Response({"detail": "销售品已标记为删除"}, status=status.HTTP_200_OK)