forked from erp-dev/erp
506 lines
18 KiB
Python
506 lines
18 KiB
Python
import logging
|
||
|
||
from django.conf import settings
|
||
from django.http import Http404
|
||
from rest_framework import authentication, mixins, serializers
|
||
from rest_framework.authentication import get_authorization_header
|
||
from rest_framework.exceptions import AuthenticationFailed
|
||
from rest_framework.generics import GenericAPIView
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
|
||
from basic_info import models as basic_models
|
||
from flower.viewsets import LimitedLimitOffsetPagination
|
||
from mes import models as mes_models
|
||
from printing.models import PrintingJob
|
||
from shipment.models import SalesItem, Shipment
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AgentAccessPrincipal:
|
||
id = None
|
||
username = "agent"
|
||
is_staff = False
|
||
is_superuser = False
|
||
is_anonymous = False
|
||
is_authenticated = True
|
||
|
||
|
||
class AgentAccessKeyAuthentication(authentication.BaseAuthentication):
|
||
"""
|
||
使用固定 AGENT_ACCESS_KEY 的轻量认证。
|
||
|
||
约定:
|
||
- 通过 Authorization 头直接传入密钥
|
||
- 不使用 Bearer 前缀
|
||
"""
|
||
|
||
def authenticate(self, request):
|
||
configured_key = str(getattr(settings, "AGENT_ACCESS_KEY", "") or "").strip()
|
||
if not configured_key:
|
||
logger.warning("Agent API authentication failed: AGENT_ACCESS_KEY is not configured")
|
||
raise AuthenticationFailed("AGENT_ACCESS_KEY 未配置")
|
||
|
||
provided_key = get_authorization_header(request).decode("utf-8").strip()
|
||
if not provided_key:
|
||
raise AuthenticationFailed("缺少 Authorization 请求头")
|
||
|
||
if provided_key != configured_key:
|
||
raise AuthenticationFailed("AGENT_ACCESS_KEY 无效")
|
||
|
||
return AgentAccessPrincipal(), None
|
||
|
||
def authenticate_header(self, request):
|
||
return "AgentAccessKey"
|
||
|
||
|
||
class AgentUnshippedShipmentQuerySerializer(serializers.Serializer):
|
||
merchant_id = serializers.IntegerField(min_value=1)
|
||
area = serializers.CharField(allow_blank=False, max_length=30)
|
||
limit = serializers.IntegerField(required=False, min_value=1)
|
||
offset = serializers.IntegerField(required=False, min_value=0)
|
||
|
||
def validate(self, attrs):
|
||
area = attrs["area"].strip()
|
||
if not area:
|
||
raise serializers.ValidationError({"area": "地区不能为空"})
|
||
|
||
merchant = basic_models.Merchant.objects.filter(id=attrs["merchant_id"]).first()
|
||
if merchant is None:
|
||
raise serializers.ValidationError({"merchant_id": "商户不存在"})
|
||
|
||
attrs["area"] = area
|
||
attrs["merchant"] = merchant
|
||
return attrs
|
||
|
||
|
||
class AgentSalesItemSerializer(serializers.ModelSerializer):
|
||
unit_display = serializers.CharField(source="get_unit_display", read_only=True)
|
||
printing_job_width = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = SalesItem
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
"quantity",
|
||
"unit",
|
||
"unit_display",
|
||
"position",
|
||
"remark",
|
||
"printing_job_id",
|
||
"printing_job_width",
|
||
]
|
||
|
||
def get_printing_job_width(self, obj):
|
||
width_map = self.context.get("printing_job_width_map", {})
|
||
return width_map.get(obj.printing_job_id)
|
||
|
||
|
||
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()
|
||
|
||
class Meta:
|
||
model = Shipment
|
||
fields = [
|
||
"id",
|
||
"merchant_id",
|
||
"customer",
|
||
"customer_name",
|
||
"fabric",
|
||
"order_description",
|
||
"shipment_date",
|
||
"address",
|
||
"contact_name",
|
||
"contact_phone",
|
||
"area",
|
||
"remark",
|
||
"status",
|
||
"status_display",
|
||
"external_id",
|
||
"geo_coordinates",
|
||
"delivery_id",
|
||
"delivery",
|
||
"sales_items",
|
||
"created_at",
|
||
"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.rolling_warn
|
||
|
||
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
|
||
|
||
|
||
class AgentUnshippedShipmentListView(mixins.ListModelMixin, GenericAPIView):
|
||
"""
|
||
Agent 专用:查询尚未进入送货单的出货单列表。
|
||
|
||
GET /api/v2/ai/shipments/unshipped/?merchant_id=<id>&area=<地区>&limit=<n>&offset=<n>
|
||
"""
|
||
|
||
authentication_classes = [AgentAccessKeyAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
serializer_class = AgentShipmentListSerializer
|
||
pagination_class = LimitedLimitOffsetPagination
|
||
|
||
def get(self, request):
|
||
query_serializer = AgentUnshippedShipmentQuerySerializer(data=request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
self.validated_query = query_serializer.validated_data
|
||
return self.list(request)
|
||
|
||
def list(self, request, *args, **kwargs):
|
||
queryset = self.filter_queryset(self.get_queryset())
|
||
page = self.paginate_queryset(queryset)
|
||
shipments = page if page is not None else queryset
|
||
|
||
# Batch-fetch printing job widths to avoid N+1
|
||
pj_ids = {
|
||
item.printing_job_id
|
||
for shipment in shipments
|
||
for item in shipment.items.all()
|
||
if item.printing_job_id and item.delete_at is None
|
||
}
|
||
width_map = {}
|
||
if pj_ids:
|
||
width_map = {
|
||
job.id: job.printing_order.width
|
||
for job in PrintingJob.objects.filter(id__in=pj_ids).select_related("printing_order")
|
||
}
|
||
|
||
ctx = {**self.get_serializer_context(), "printing_job_width_map": width_map}
|
||
serializer = self.get_serializer(shipments, many=True, context=ctx)
|
||
if page is not None:
|
||
return self.get_paginated_response(serializer.data)
|
||
return Response(serializer.data)
|
||
|
||
def get_queryset(self):
|
||
validated_query = getattr(self, "validated_query", None)
|
||
if validated_query is None:
|
||
query_serializer = AgentUnshippedShipmentQuerySerializer(data=self.request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
validated_query = query_serializer.validated_data
|
||
|
||
merchant = validated_query["merchant"]
|
||
area = validated_query["area"]
|
||
|
||
return (
|
||
Shipment.objects.filter(
|
||
merchant=merchant,
|
||
area=area,
|
||
delivery__isnull=True,
|
||
)
|
||
.select_related("customer", "merchant")
|
||
.prefetch_related("items")
|
||
.order_by("-created_at", "-id")
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Vehicle views
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class AgentMerchantQuerySerializer(serializers.Serializer):
|
||
"""Base serializer that validates merchant_id and returns merchant instance."""
|
||
|
||
merchant_id = serializers.IntegerField(min_value=1)
|
||
|
||
def validate_merchant_id(self, value):
|
||
merchant = basic_models.Merchant.objects.filter(id=value).first()
|
||
if merchant is None:
|
||
raise serializers.ValidationError("商户不存在")
|
||
return merchant
|
||
|
||
|
||
class AgentTransportVehicleCapacitySerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = basic_models.TransportVehicleMaterialCapacity
|
||
fields = ["id", "material_name", "capacity"]
|
||
|
||
|
||
class AgentTransportVehicleSerializer(serializers.ModelSerializer):
|
||
material_capacities = AgentTransportVehicleCapacitySerializer(many=True, read_only=True)
|
||
|
||
class Meta:
|
||
model = basic_models.TransportVehicle
|
||
fields = ["id", "merchant_id", "name", "license_plate", "material_capacities", "created_at", "updated_at"]
|
||
|
||
|
||
class AgentTransportVehicleListView(mixins.ListModelMixin, GenericAPIView):
|
||
"""
|
||
Agent 专用:查询商户下的所有运输车辆及其物料容量。
|
||
|
||
GET /api/v2/ai/transport-vehicles/?merchant_id=<id>
|
||
"""
|
||
|
||
authentication_classes = [AgentAccessKeyAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
serializer_class = AgentTransportVehicleSerializer
|
||
pagination_class = LimitedLimitOffsetPagination
|
||
|
||
def get(self, request):
|
||
query_serializer = AgentMerchantQuerySerializer(data=request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
self.merchant = query_serializer.validated_data["merchant_id"]
|
||
return self.list(request)
|
||
|
||
def get_queryset(self):
|
||
merchant = getattr(self, "merchant", None)
|
||
if merchant is None:
|
||
query_serializer = AgentMerchantQuerySerializer(data=self.request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
merchant = query_serializer.validated_data["merchant_id"]
|
||
return (
|
||
basic_models.TransportVehicle.objects.filter(merchant=merchant)
|
||
.prefetch_related("material_capacities")
|
||
.order_by("id")
|
||
)
|
||
|
||
|
||
class AgentTransportVehicleDetailView(APIView):
|
||
"""
|
||
Agent 专用:通过车牌号查询运输车辆详情。
|
||
|
||
GET /api/v2/ai/transport-vehicles/<license_plate>/?merchant_id=<id>
|
||
|
||
license_plate 在同一商户内唯一(unique_together: merchant + license_plate)。
|
||
"""
|
||
|
||
authentication_classes = [AgentAccessKeyAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get(self, request, license_plate):
|
||
query_serializer = AgentMerchantQuerySerializer(data=request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
merchant = query_serializer.validated_data["merchant_id"]
|
||
|
||
try:
|
||
vehicle = (
|
||
basic_models.TransportVehicle.objects
|
||
.prefetch_related("material_capacities")
|
||
.get(merchant=merchant, license_plate=license_plate)
|
||
)
|
||
except basic_models.TransportVehicle.DoesNotExist:
|
||
raise Http404
|
||
|
||
return Response(AgentTransportVehicleSerializer(vehicle).data)
|
||
|
||
|
||
class AgentMesDeviceCategorySerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = mes_models.DeviceCategory
|
||
fields = ["id", "merchant", "name", "created_at", "updated_at"]
|
||
|
||
|
||
class AgentMesDeviceSerializer(serializers.ModelSerializer):
|
||
category = AgentMesDeviceCategorySerializer(read_only=True)
|
||
capacity_unit_label = serializers.CharField(source='get_capacity_unit_display', read_only=True)
|
||
|
||
class Meta:
|
||
model = mes_models.Device
|
||
fields = [
|
||
"id",
|
||
"merchant",
|
||
"category",
|
||
"name",
|
||
"peak_capacity",
|
||
"capacity_unit",
|
||
"capacity_unit_label",
|
||
"extra",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
|
||
|
||
class AgentMesDeviceListView(mixins.ListModelMixin, GenericAPIView):
|
||
"""
|
||
Agent 专用:查询商户下所有 MES 设备,并返回设备分类信息。
|
||
|
||
GET /api/v2/ai/mes/devices/?merchant_id=<id>
|
||
"""
|
||
|
||
authentication_classes = [AgentAccessKeyAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
serializer_class = AgentMesDeviceSerializer
|
||
pagination_class = LimitedLimitOffsetPagination
|
||
|
||
def get(self, request):
|
||
query_serializer = AgentMerchantQuerySerializer(data=request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
self.merchant = query_serializer.validated_data["merchant_id"]
|
||
return self.list(request)
|
||
|
||
def get_queryset(self):
|
||
merchant = getattr(self, "merchant", None)
|
||
if merchant is None:
|
||
query_serializer = AgentMerchantQuerySerializer(data=self.request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
merchant = query_serializer.validated_data["merchant_id"]
|
||
return mes_models.Device.objects.filter(merchant=merchant).select_related("category").order_by("id")
|
||
|
||
|
||
class AgentMesProductionAssignmentQuerySerializer(AgentMerchantQuerySerializer):
|
||
start_date = serializers.DateField()
|
||
end_date = serializers.DateField()
|
||
device_id = serializers.IntegerField(required=False, min_value=1)
|
||
status = serializers.ChoiceField(required=False, choices=mes_models.ProductionAssignmentStatusEnum.choices)
|
||
|
||
def validate(self, attrs):
|
||
attrs = super().validate(attrs)
|
||
if attrs["start_date"] > attrs["end_date"]:
|
||
raise serializers.ValidationError({"end_date": "结束日期不能早于开始日期"})
|
||
|
||
merchant = attrs["merchant_id"]
|
||
device_id = attrs.get("device_id")
|
||
if device_id is not None:
|
||
device = mes_models.Device.objects.filter(id=device_id, merchant=merchant).first()
|
||
if device is None:
|
||
raise serializers.ValidationError({"device_id": "设备不存在"})
|
||
attrs["device"] = device
|
||
return attrs
|
||
|
||
|
||
class AgentMesProductionAssignmentEmployeeSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = basic_models.Employee
|
||
fields = ["id", "name", "merchant_id"]
|
||
|
||
|
||
class AgentMesProductionAssignmentDeviceSerializer(serializers.ModelSerializer):
|
||
category = AgentMesDeviceCategorySerializer(read_only=True)
|
||
capacity_unit_label = serializers.CharField(source='get_capacity_unit_display', read_only=True)
|
||
|
||
class Meta:
|
||
model = mes_models.Device
|
||
fields = [
|
||
"id",
|
||
"merchant",
|
||
"category",
|
||
"name",
|
||
"peak_capacity",
|
||
"capacity_unit",
|
||
"capacity_unit_label",
|
||
"extra",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
|
||
|
||
class AgentMesProductionAssignmentSerializer(serializers.ModelSerializer):
|
||
device = AgentMesProductionAssignmentDeviceSerializer(read_only=True)
|
||
assigner = AgentMesProductionAssignmentEmployeeSerializer(read_only=True)
|
||
assignee = AgentMesProductionAssignmentEmployeeSerializer(read_only=True)
|
||
status_label = serializers.CharField(source='get_status_display', read_only=True)
|
||
|
||
class Meta:
|
||
model = mes_models.ProductionAssignment
|
||
fields = [
|
||
"id",
|
||
"merchant",
|
||
"device",
|
||
"content_type",
|
||
"object_id",
|
||
"assigner",
|
||
"assignee",
|
||
"production_quantity",
|
||
"status",
|
||
"status_label",
|
||
"extra",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
|
||
|
||
class AgentMesProductionAssignmentListView(mixins.ListModelMixin, GenericAPIView):
|
||
"""
|
||
Agent 专用:查询指定日期范围内创建的生产安排。
|
||
|
||
GET /api/v2/ai/mes/production-assignments/?merchant_id=<id>&start_date=<YYYY-MM-DD>&end_date=<YYYY-MM-DD>
|
||
"""
|
||
|
||
authentication_classes = [AgentAccessKeyAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
serializer_class = AgentMesProductionAssignmentSerializer
|
||
pagination_class = LimitedLimitOffsetPagination
|
||
|
||
def get(self, request):
|
||
query_serializer = AgentMesProductionAssignmentQuerySerializer(data=request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
self.validated_query = query_serializer.validated_data
|
||
return self.list(request)
|
||
|
||
def get_queryset(self):
|
||
validated_query = getattr(self, "validated_query", None)
|
||
if validated_query is None:
|
||
query_serializer = AgentMesProductionAssignmentQuerySerializer(data=self.request.query_params)
|
||
query_serializer.is_valid(raise_exception=True)
|
||
validated_query = query_serializer.validated_data
|
||
|
||
merchant = validated_query["merchant_id"]
|
||
queryset = (
|
||
mes_models.ProductionAssignment.objects.filter(
|
||
merchant=merchant,
|
||
created_at__date__gte=validated_query["start_date"],
|
||
created_at__date__lte=validated_query["end_date"],
|
||
)
|
||
.select_related("device", "device__category", "assigner", "assignee", "content_type")
|
||
.order_by("-created_at", "-id")
|
||
)
|
||
|
||
device = validated_query.get("device")
|
||
if device is not None:
|
||
queryset = queryset.filter(device=device)
|
||
|
||
status_value = validated_query.get("status")
|
||
if status_value is not None:
|
||
queryset = queryset.filter(status=status_value)
|
||
|
||
return queryset
|