forked from erp-dev/erp
399 lines
12 KiB
Python
399 lines
12 KiB
Python
"""
|
||
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/<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()
|
||
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/<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
|
||
|
||
# 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/<id>/
|
||
"""
|
||
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/<printing_order_id>/
|
||
|
||
返回与该 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)
|