forked from erp-dev/erp
1079 lines
36 KiB
Python
1079 lines
36 KiB
Python
"""
|
||
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.models import SalesItem
|
||
|
||
qs = (
|
||
SalesItem.objects.select_related("shipment", "created_by", "delete_by")
|
||
.filter(delete_at__isnull=True)
|
||
.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
|
||
|
||
delete_sales_item(sales_item, deleted_by=request.user)
|
||
return Response({"detail": "销售品已标记为删除"}, status=status.HTTP_200_OK)
|