forked from erp-dev/erp
feat: mes module
This commit is contained in:
467
api_v2/views/mes.py
Normal file
467
api_v2/views/mes.py
Normal file
@@ -0,0 +1,467 @@
|
||||
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, 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["device"].required = True
|
||||
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):
|
||||
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.CharField(source='device.name', 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_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)
|
||||
Reference in New Issue
Block a user