""" Shipment API ViewSet """ from rest_framework import status from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework.generics import GenericAPIView from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from flower.viewsets import LimitedLimitOffsetPagination from shipment.models import Shipment from .serializers import ( SalesItemSerializer, ShipmentSerializer, ShipmentCreateNormalSerializer, ShipmentCreateExternalSerializer, ShipmentUpdateSerializer, ) 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) - 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, 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// """ 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() return qs.filter(merchant=merchant) def get(self, request, pk: int): return self.retrieve(request, pk=pk) def patch(self, request, pk: int): """ 更新出货单(部分更新) PATCH /api/v1/shipment/shipments// """ 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 # customer 变更需校验 merchant 一致 if "customer" in data: from basic_info.models import Customer try: customer = Customer.objects.get(id=data["customer"]) except Customer.DoesNotExist: return Response( {"detail": f"客户 {data['customer']} 不存在"}, status=status.HTTP_400_BAD_REQUEST, ) if customer.merchant_id != shipment.merchant_id: return Response( {"detail": "无权限绑定该客户"}, status=status.HTTP_400_BAD_REQUEST ) shipment.customer = customer if "shipment_date" in data: shipment.shipment_date = data["shipment_date"] if "area" in data: shipment.area = (data.get("area") or "").strip() if "remark" in data: shipment.remark = data.get("remark") or "" if "external_id" in data: shipment.external_id = (data.get("external_id") or "").strip() or None shipment.save() return Response(ShipmentSerializer(shipment).data, status=status.HTTP_200_OK) def put(self, request, pk: int): """ 更新出货单(全量更新,当前实现允许缺省字段,等同于 PATCH) PUT /api/v1/shipment/shipments// """ return self.patch(request, pk=pk) 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, 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 SalesItemByPrintingOrderView(APIView): """ 通过生产订单查询销售品 GET /api/v1/shipment/sales-items/by-printing-order// 返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。 查询参数: - 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 try: printing_order = PrintingOrder.objects.get(id=printing_order_id) except PrintingOrder.DoesNotExist: 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, ) # 序列化返回 serializer = SalesItemSerializer(sales_items, many=True) return Response({"count": len(serializer.data), "results": 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) return Response(response_serializer.data, status=status.HTTP_201_CREATED)