import json import re from django.contrib.contenttypes.models import ContentType from django.db.models import ProtectedError from django.shortcuts import get_object_or_404 from rest_framework import permissions, serializers, status from rest_framework.response import Response from rest_framework.views import APIView from mes.models import CapacityUnitEnum, ProductionAssignment, ProductionAssignmentStatusEnum from mes import services as mes_services from mes.models import Device, DeviceCategory EXTRA_JSON_PATH_SEGMENT_RE = re.compile(r'^[A-Za-z0-9_-]+$') def _get_employee(request): employee = getattr(request.user, "employee", None) if employee is None: raise serializers.ValidationError("当前用户未关联员工") return employee def _employee_payload(employee): if employee is None: return None return { "id": employee.id, "name": employee.name, "merchant_id": employee.merchant_id, } def _user_payload(user): if user is None: return None return { "id": user.id, "username": user.username, } def _parse_extra_filter(extra_path: str | None, extra_value: str | None): if not extra_path and extra_value is None: return None, mes_services.UNSET if not extra_path or extra_value is None: raise serializers.ValidationError({'detail': 'extra_path 和 extra_value 必须同时传入'}) normalized_segments = [segment.strip() for segment in extra_path.split('.') if segment.strip()] if not normalized_segments: raise serializers.ValidationError({'detail': 'extra_path 不合法'}) for segment in normalized_segments: if not EXTRA_JSON_PATH_SEGMENT_RE.match(segment): raise serializers.ValidationError({'detail': 'extra_path 不合法'}) try: parsed_value = json.loads(extra_value) except json.JSONDecodeError: parsed_value = extra_value return normalized_segments, parsed_value class DeviceCategoryWriteSerializer(serializers.Serializer): name = serializers.CharField(allow_blank=False, max_length=100) class DeviceWriteSerializer(serializers.Serializer): name = serializers.CharField(required=False, allow_blank=False, max_length=100) category = serializers.IntegerField(required=False, min_value=1) peak_capacity = serializers.IntegerField(required=False, min_value=1) capacity_unit = serializers.ChoiceField(required=False, choices=CapacityUnitEnum.choices) extra = serializers.JSONField(required=False, allow_null=True) def __init__(self, *args, **kwargs): self.is_create = kwargs.pop("is_create", False) super().__init__(*args, **kwargs) if self.is_create: self.fields["name"].required = True self.fields["category"].required = True self.fields["peak_capacity"].required = True def validate_category(self, value): employee = self.context["employee"] try: return DeviceCategory.objects.get(id=value, merchant=employee.merchant) except DeviceCategory.DoesNotExist as exc: raise serializers.ValidationError("设备分类不存在") from exc class DeviceCategorySerializer(serializers.ModelSerializer): created_by = serializers.SerializerMethodField() operator = serializers.SerializerMethodField() class Meta: model = DeviceCategory fields = [ "id", "merchant", "name", "created_by", "operator", "created_at", "updated_at", ] read_only_fields = fields def get_created_by(self, obj): return _user_payload(obj.created_by) def get_operator(self, obj): return _employee_payload(obj.operator) class DeviceSerializer(serializers.ModelSerializer): created_by = serializers.SerializerMethodField() operator = serializers.SerializerMethodField() category_name = serializers.CharField(source="category.name", read_only=True) category = serializers.IntegerField(source="category_id", read_only=True) capacity_unit_label = serializers.CharField(source='get_capacity_unit_display', read_only=True) class Meta: model = Device fields = [ "id", "merchant", "category", "category_name", "name", "peak_capacity", "capacity_unit", "capacity_unit_label", "extra", "created_by", "operator", "created_at", "updated_at", ] read_only_fields = fields def get_created_by(self, obj): return _user_payload(obj.created_by) def get_operator(self, obj): return _employee_payload(obj.operator) class ProductionAssignmentWriteSerializer(serializers.Serializer): device = serializers.IntegerField(required=False, allow_null=True, min_value=1) content_type = serializers.IntegerField(required=False, min_value=1) object_id = serializers.IntegerField(required=False, min_value=1) assigner = serializers.IntegerField(required=False, min_value=1) assignee = serializers.IntegerField(required=False, min_value=1, allow_null=True) production_quantity = serializers.IntegerField(required=False, min_value=1) status = serializers.ChoiceField(required=False, choices=ProductionAssignmentStatusEnum.choices) extra = serializers.JSONField(required=False, allow_null=True) def __init__(self, *args, **kwargs): self.is_create = kwargs.pop("is_create", False) super().__init__(*args, **kwargs) if self.is_create: self.fields["content_type"].required = True self.fields["object_id"].required = True self.fields["assigner"].required = True self.fields["production_quantity"].required = True def validate_device(self, value): if value is None: return None employee = self.context["employee"] try: return Device.objects.get(id=value, merchant=employee.merchant) except Device.DoesNotExist as exc: raise serializers.ValidationError("设备不存在") from exc def validate_content_type(self, value): try: return ContentType.objects.get(id=value) except ContentType.DoesNotExist as exc: raise serializers.ValidationError("关联对象类型不存在") from exc def validate_assigner(self, value): employee = self.context["employee"] try: return employee.merchant.employees.get(id=value) except Exception as exc: raise serializers.ValidationError("指派者不存在") from exc def validate_assignee(self, value): if value is None: return None employee = self.context["employee"] try: return employee.merchant.employees.get(id=value) except Exception as exc: raise serializers.ValidationError("被指派人不存在") from exc class ProductionAssignmentSerializer(serializers.ModelSerializer): device_name = serializers.SerializerMethodField(read_only=True) assigner = serializers.SerializerMethodField() assignee = serializers.SerializerMethodField() created_by = serializers.SerializerMethodField() operator = serializers.SerializerMethodField() status_label = serializers.CharField(source='get_status_display', read_only=True) class Meta: model = ProductionAssignment fields = [ 'id', 'merchant', 'device', 'device_name', 'content_type', 'object_id', 'assigner', 'assignee', 'production_quantity', 'status', 'status_label', 'extra', 'created_by', 'operator', 'created_at', 'updated_at', ] read_only_fields = fields def get_device_name(self, obj): if obj.device is None: return None return obj.device.name def get_assigner(self, obj): return _employee_payload(obj.assigner) def get_assignee(self, obj): return _employee_payload(obj.assignee) def get_created_by(self, obj): return _user_payload(obj.created_by) def get_operator(self, obj): return _employee_payload(obj.operator) class DeviceCategoryListCreateView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): employee = _get_employee(request) queryset = mes_services.list_device_categories_for_merchant(merchant=employee.merchant) name = request.query_params.get("name") if name: queryset = queryset.filter(name__icontains=name.strip()) return Response(DeviceCategorySerializer(queryset, many=True).data) def post(self, request): employee = _get_employee(request) serializer = DeviceCategoryWriteSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: category = mes_services.create_device_category( merchant=employee.merchant, name=serializer.validated_data["name"], created_by=request.user, operator=employee, ) except ValueError as exc: return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(DeviceCategorySerializer(category).data, status=status.HTTP_201_CREATED) class DeviceCategoryDetailView(APIView): permission_classes = [permissions.IsAuthenticated] def get_object(self, request, category_id): employee = _get_employee(request) return get_object_or_404( mes_services.list_device_categories_for_merchant(merchant=employee.merchant), id=category_id, ) def get(self, request, category_id): category = self.get_object(request, category_id) return Response(DeviceCategorySerializer(category).data) def patch(self, request, category_id): employee = _get_employee(request) category = self.get_object(request, category_id) serializer = DeviceCategoryWriteSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) try: category = mes_services.update_device_category( category=category, operator=employee, name=serializer.validated_data.get("name", mes_services.UNSET), ) except ValueError as exc: return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(DeviceCategorySerializer(category).data) def delete(self, request, category_id): employee = _get_employee(request) category = self.get_object(request, category_id) try: mes_services.delete_device_category(category=category, operator=employee) except ProtectedError: return Response({"detail": "设备分类已被设备引用,不能删除"}, status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_204_NO_CONTENT) class DeviceListCreateView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): employee = _get_employee(request) category_id = request.query_params.get("category") extra_json_path, extra_json_value = _parse_extra_filter( request.query_params.get("extra_path"), request.query_params.get("extra_value"), ) queryset = mes_services.list_devices_for_merchant( merchant=employee.merchant, category_id=int(category_id) if category_id else None, extra_json_path=extra_json_path, extra_json_value=extra_json_value, ) name = request.query_params.get("name") if name: queryset = queryset.filter(name__icontains=name.strip()) return Response(DeviceSerializer(queryset, many=True).data) def post(self, request): employee = _get_employee(request) serializer = DeviceWriteSerializer(data=request.data, is_create=True, context={"employee": employee}) serializer.is_valid(raise_exception=True) try: device = mes_services.create_device( merchant=employee.merchant, category=serializer.validated_data["category"], name=serializer.validated_data["name"], created_by=request.user, operator=employee, peak_capacity=serializer.validated_data['peak_capacity'], capacity_unit=serializer.validated_data.get('capacity_unit', CapacityUnitEnum.METER), extra=serializer.validated_data.get("extra"), ) except ValueError as exc: return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(DeviceSerializer(device).data, status=status.HTTP_201_CREATED) class DeviceDetailView(APIView): permission_classes = [permissions.IsAuthenticated] def get_object(self, request, device_id): employee = _get_employee(request) return get_object_or_404( mes_services.list_devices_for_merchant(merchant=employee.merchant), id=device_id, ) def get(self, request, device_id): device = self.get_object(request, device_id) return Response(DeviceSerializer(device).data) def patch(self, request, device_id): employee = _get_employee(request) device = self.get_object(request, device_id) serializer = DeviceWriteSerializer(data=request.data, partial=True, context={"employee": employee}) serializer.is_valid(raise_exception=True) try: device = mes_services.update_device( device=device, operator=employee, name=serializer.validated_data.get("name", mes_services.UNSET), category=serializer.validated_data.get("category", mes_services.UNSET), peak_capacity=serializer.validated_data.get('peak_capacity', mes_services.UNSET), capacity_unit=serializer.validated_data.get('capacity_unit', mes_services.UNSET), extra=serializer.validated_data.get("extra", mes_services.UNSET), ) except ValueError as exc: return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(DeviceSerializer(device).data) def delete(self, request, device_id): employee = _get_employee(request) device = self.get_object(request, device_id) mes_services.delete_device(device=device, operator=employee) return Response(status=status.HTTP_204_NO_CONTENT) class ProductionAssignmentListCreateView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): employee = _get_employee(request) device_id = request.query_params.get('device') content_type_id = request.query_params.get('content_type') object_id = request.query_params.get('object_id') status_value = request.query_params.get('status') queryset = mes_services.list_production_assignments_for_merchant( merchant=employee.merchant, device_id=int(device_id) if device_id else None, content_type_id=int(content_type_id) if content_type_id else None, object_id=int(object_id) if object_id else None, status=int(status_value) if status_value else None, ) return Response(ProductionAssignmentSerializer(queryset, many=True).data) def post(self, request): employee = _get_employee(request) serializer = ProductionAssignmentWriteSerializer(data=request.data, is_create=True, context={'employee': employee}) serializer.is_valid(raise_exception=True) try: assignment = mes_services.create_production_assignment( merchant=employee.merchant, device=serializer.validated_data['device'], content_type=serializer.validated_data['content_type'], object_id=serializer.validated_data['object_id'], assigner=serializer.validated_data['assigner'], assignee=serializer.validated_data.get('assignee'), production_quantity=serializer.validated_data['production_quantity'], status=serializer.validated_data.get('status', ProductionAssignmentStatusEnum.DRAFT), created_by=request.user, operator=employee, extra=serializer.validated_data.get('extra'), ) except ValueError as exc: return Response({'detail': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(ProductionAssignmentSerializer(assignment).data, status=status.HTTP_201_CREATED) class ProductionAssignmentDetailView(APIView): permission_classes = [permissions.IsAuthenticated] def get_object(self, request, assignment_id): employee = _get_employee(request) return get_object_or_404( mes_services.list_production_assignments_for_merchant(merchant=employee.merchant), id=assignment_id, ) def get(self, request, assignment_id): assignment = self.get_object(request, assignment_id) return Response(ProductionAssignmentSerializer(assignment).data) def patch(self, request, assignment_id): employee = _get_employee(request) assignment = self.get_object(request, assignment_id) serializer = ProductionAssignmentWriteSerializer(data=request.data, partial=True, context={'employee': employee}) serializer.is_valid(raise_exception=True) try: assignment = mes_services.update_production_assignment( assignment=assignment, operator=employee, device=serializer.validated_data.get('device', mes_services.UNSET), assignee=serializer.validated_data.get('assignee', mes_services.UNSET), production_quantity=serializer.validated_data.get('production_quantity', mes_services.UNSET), status=serializer.validated_data.get('status', mes_services.UNSET), extra=serializer.validated_data.get('extra', mes_services.UNSET), ) except ValueError as exc: return Response({'detail': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(ProductionAssignmentSerializer(assignment).data) def delete(self, request, assignment_id): employee = _get_employee(request) assignment = self.get_object(request, assignment_id) mes_services.delete_production_assignment(assignment=assignment, operator=employee) return Response(status=status.HTTP_204_NO_CONTENT)