from decimal import Decimal from rest_framework import status, views, serializers, pagination from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Sum from django.utils import dateparse, timezone from api_v1.views.stock_change_views.mixins import StockChangeViewMixin from business import models as business_models from business import pre_order_services from business import allocation_services from stock import models as stock_models class AllocationRecordSerializer(serializers.ModelSerializer): stock_ids = serializers.SerializerMethodField(read_only=True) stock_details = serializers.SerializerMethodField(read_only=True) scanned_by_name = serializers.CharField(source='scanned_by.name', read_only=True) class Meta: model = business_models.AllocationRecord fields = [ 'id', 'pre_sales_order_item', 'stock_ids', 'stock_details', 'quantity', 'unit', 'status', 'scanned_by', 'scanned_by_name', 'scanned_at', 'remarks', 'created_at', ] read_only_fields = [ 'id', 'pre_sales_order_item', 'scanned_by', 'scanned_by_name', 'scanned_at', 'created_at', ] def get_stock_ids(self, obj: business_models.AllocationRecord): return obj.stock_id_list def get_stock_details(self, obj: business_models.AllocationRecord): stock_ids = obj.stock_id_list if not stock_ids: return [] details_by_id = stock_models.StockChangeDetail.objects.select_related( 'product' ).in_bulk(stock_ids) return [ StockChangeDetailShortcutSerializer(details_by_id[stock_id]).data for stock_id in stock_ids if stock_id in details_by_id ] class StockChangeDetailShortcutSerializer(serializers.ModelSerializer): product_name = serializers.CharField(source='product.name', read_only=True) class Meta: model = stock_models.StockChangeDetail fields = [ 'product_id', 'product_name', 'quantity', ] read_only_fields = fields class PreSalesOrderItemSerializer(serializers.ModelSerializer): allocated_quantity = serializers.SerializerMethodField(read_only=True) progress = serializers.SerializerMethodField(read_only=True) allocation_status = serializers.SerializerMethodField(read_only=True) class Meta: model = business_models.PreSalesOrderItem fields = [ 'id', 'product_id', 'product_name', 'color', 'quantity', 'unit', 'spec', 'remarks', 'allocated_quantity', 'progress', 'allocation_status', ] read_only_fields = ['id'] def _get_allocated_quantity(self, obj: business_models.PreSalesOrderItem) -> Decimal: result = obj.allocation_records.filter( status=business_models.AllocationRecordStatusEnum.ACTIVE ).aggregate(total=Sum('quantity')) return result.get('total') or Decimal('0') def get_allocated_quantity(self, obj: business_models.PreSalesOrderItem) -> str: return str(self._get_allocated_quantity(obj)) def get_progress(self, obj: business_models.PreSalesOrderItem) -> str: required = obj.quantity or Decimal('0') allocated = self._get_allocated_quantity(obj) if required <= 0: return '0' return str((allocated / required).quantize(Decimal('0.0001'))) def get_allocation_status(self, obj: business_models.PreSalesOrderItem) -> int: required = obj.quantity or Decimal('0') allocated = self._get_allocated_quantity(obj) if allocated <= 0: return 1 if allocated < required: return 2 return 3 class PreSalesOrderItemDetailSerializer(PreSalesOrderItemSerializer): allocation_records = serializers.SerializerMethodField(read_only=True) class Meta(PreSalesOrderItemSerializer.Meta): fields = PreSalesOrderItemSerializer.Meta.fields + [ 'allocation_records', ] def get_allocation_records(self, obj: business_models.PreSalesOrderItem): """返回非已撤销的配货记录""" active_records = obj.allocation_records.exclude( status=business_models.AllocationRecordStatusEnum.CANCELLED ) return AllocationRecordSerializer(active_records, many=True).data class PreSalesOrderSerializer(serializers.ModelSerializer): human_id = serializers.CharField(read_only=True) customer_name = serializers.CharField(source='customer.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True) items = PreSalesOrderItemSerializer(many=True, read_only=True) allocated_quantity = serializers.SerializerMethodField(read_only=True) progress = serializers.SerializerMethodField(read_only=True) class Meta: model = business_models.PreSalesOrder fields = [ 'id', 'human_id', 'merchant', 'customer', 'customer_name', 'warehouse', 'warehouse_name', 'operator', 'operator_name', 'kind', 'remarks', 'created_at', 'items', 'allocated_quantity', 'progress', ] read_only_fields = [ 'id', 'human_id', 'created_at', 'customer_name', 'warehouse_name', 'operator_name', 'items', 'allocated_quantity', 'progress', ] def get_allocated_quantity(self, obj: business_models.PreSalesOrder) -> str: """计算整单已配货总量""" total = Decimal('0') for item in obj.items.all(): result = item.allocation_records.filter( status=business_models.AllocationRecordStatusEnum.ACTIVE ).aggregate(total=Sum('quantity')) total += result.get('total') or Decimal('0') return str(total) def get_progress(self, obj: business_models.PreSalesOrder) -> str: """计算整单配货进度(已配货总量/总需求量)""" required_total = Decimal('0') allocated_total = Decimal('0') for item in obj.items.all(): required_total += item.quantity or Decimal('0') result = item.allocation_records.filter( status=business_models.AllocationRecordStatusEnum.ACTIVE ).aggregate(total=Sum('quantity')) allocated_total += result.get('total') or Decimal('0') if required_total <= 0: return '0' return str((allocated_total / required_total).quantize(Decimal('0.0001'))) class PreSalesOrderDetailSerializer(PreSalesOrderSerializer): items = PreSalesOrderItemDetailSerializer(many=True, read_only=True) class PreSalesOrderPagination(pagination.LimitOffsetPagination): default_limit = 20 max_limit = 100 class PreSalesOrderView(StockChangeViewMixin, views.APIView): """预销售单查询与创建""" permission_classes = [IsAuthenticated] pagination_class = PreSalesOrderPagination def get(self, request): if not self.check_employee_permission(request): return self.permission_error_response('无权限访问') merchant = request.user.employee.merchant queryset = pre_order_services.list_pre_sales_orders(merchant=merchant) customer_id = request.query_params.get('customer') or request.query_params.get('customer_id') warehouse_id = request.query_params.get('warehouse') or request.query_params.get('warehouse_id') kind = request.query_params.get('kind') human_id_contains = request.query_params.get('human_id__icontains') created_from = request.query_params.get('created_at_from') created_to = request.query_params.get('created_at_to') try: if customer_id: queryset = queryset.filter(customer_id=int(customer_id)) if warehouse_id: queryset = queryset.filter(warehouse_id=int(warehouse_id)) if kind: queryset = queryset.filter(kind=int(kind)) except (TypeError, ValueError): return Response({'error': '筛选参数必须为整数'}, status=status.HTTP_400_BAD_REQUEST) if human_id_contains: queryset = queryset.filter(human_id__icontains=str(human_id_contains).strip()) def _parse_datetime(value: str): dt = dateparse.parse_datetime(value) if dt is None: d = dateparse.parse_date(value) if d is None: return None dt = timezone.datetime.combine(d, timezone.datetime.min.time()) if timezone.is_naive(dt): dt = timezone.make_aware(dt, timezone.get_current_timezone()) return dt if created_from: dt_from = _parse_datetime(created_from) if dt_from is None: return Response({'error': 'created_at_from 格式不正确'}, status=status.HTTP_400_BAD_REQUEST) queryset = queryset.filter(created_at__gte=dt_from) if created_to: dt_to = _parse_datetime(created_to) if dt_to is None: return Response({'error': 'created_at_to 格式不正确'}, status=status.HTTP_400_BAD_REQUEST) queryset = queryset.filter(created_at__lte=dt_to) paginator = self.pagination_class() page = paginator.paginate_queryset(queryset.order_by('-created_at'), request, view=self) serializer = PreSalesOrderSerializer(page, many=True) return paginator.get_paginated_response(serializer.data) def post(self, request): if not self.check_employee_permission(request): return self.permission_error_response('无权限访问') merchant = request.user.employee.merchant employee = request.user.employee data = request.data or {} try: pre_sales_order = pre_order_services.create_pre_sales_order( merchant=merchant, customer_id=data.get('customer') or data.get('customer_id'), warehouse_id=data.get('warehouse') or data.get('warehouse_id'), operator=employee, created_by=request.user, kind=data.get('kind'), items=data.get('items'), remarks=data.get('remarks', ''), ) except ValueError as exc: return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(PreSalesOrderSerializer(pre_sales_order).data, status=status.HTTP_201_CREATED) class PreSalesOrderDetailView(StockChangeViewMixin, views.APIView): permission_classes = [IsAuthenticated] def _get_order(self, request, pk: int): if not self.check_employee_permission(request): return None, self.permission_error_response('无权限访问') merchant = request.user.employee.merchant try: order = pre_order_services.get_pre_sales_order( merchant=merchant, pre_sales_order_id=pk, ) return order, None except ValueError: return None, self.not_found_response('预销售单不存在') def get(self, request, pk: int): order, error_response = self._get_order(request, pk) if error_response: return error_response return Response(PreSalesOrderDetailSerializer(order).data, status=status.HTTP_200_OK) def put(self, request, pk: int): return self._update(request, pk) def patch(self, request, pk: int): return self._update(request, pk) def _update(self, request, pk: int): order, error_response = self._get_order(request, pk) if error_response: return error_response data = request.data or {} try: updated = pre_order_services.update_pre_sales_order( pre_sales_order=order, customer_id=data.get('customer') or data.get('customer_id'), warehouse_id=data.get('warehouse') or data.get('warehouse_id'), kind=data.get('kind'), items=data.get('items'), remarks=data.get('remarks'), ) except ValueError as exc: return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(PreSalesOrderSerializer(updated).data, status=status.HTTP_200_OK) def delete(self, request, pk: int): order, error_response = self._get_order(request, pk) if error_response: return error_response pre_order_services.delete_pre_sales_order(pre_sales_order=order) return Response(status=status.HTTP_204_NO_CONTENT) class PreSalesOrderConvertToSalesOrderView(StockChangeViewMixin, views.APIView): permission_classes = [IsAuthenticated] def post(self, request, pk: int): if not self.check_employee_permission(request): return self.permission_error_response('无权限访问') merchant = request.user.employee.merchant employee = request.user.employee try: sales_order = pre_order_services.convert_pre_sales_order_to_sales_order( merchant=merchant, pre_sales_order_id=pk, operator=employee, created_by=request.user, ) except PermissionError as exc: return Response({'error': str(exc)}, status=status.HTTP_403_FORBIDDEN) except business_models.PreSalesOrder.DoesNotExist: return self.not_found_response('预销售单不存在') except ValueError as exc: return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response( { 'id': sales_order.id, 'human_id': sales_order.human_id, 'status': sales_order.status, 'message': '预销售单已转换为销售单,等待审批', }, status=status.HTTP_201_CREATED, ) class PreSalesOrderItemAllocationView(StockChangeViewMixin, views.APIView): permission_classes = [IsAuthenticated] def _get_item(self, request, item_id: int): if not self.check_employee_permission(request): return None, self.permission_error_response('无权限访问') merchant = request.user.employee.merchant try: item = business_models.PreSalesOrderItem.objects.select_related( 'pre_sales_order', ).get(id=item_id, pre_sales_order__merchant=merchant) return item, None except business_models.PreSalesOrderItem.DoesNotExist: return None, self.not_found_response('预销售单明细不存在') def post(self, request, item_id: int): item, error_response = self._get_item(request, item_id) if error_response: return error_response data = request.data or {} try: record = allocation_services.create_allocation_record( item=item, stock_ids=data.get('stock_ids') or [], quantity=data.get('quantity'), unit=data.get('unit'), scanned_by=request.user.employee, remarks=data.get('remarks'), ) except ValueError as exc: return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(AllocationRecordSerializer(record).data, status=status.HTTP_201_CREATED) class PreSalesOrderItemAllocationDetailView(StockChangeViewMixin, views.APIView): permission_classes = [IsAuthenticated] def delete(self, request, item_id: int, pk: int): if not self.check_employee_permission(request): return self.permission_error_response('无权限访问') merchant = request.user.employee.merchant try: record = business_models.AllocationRecord.objects.select_related( 'pre_sales_order_item', 'pre_sales_order_item__pre_sales_order', ).get( id=pk, pre_sales_order_item_id=item_id, pre_sales_order_item__pre_sales_order__merchant=merchant, ) except business_models.AllocationRecord.DoesNotExist: return self.not_found_response('配货记录不存在') updated = allocation_services.cancel_allocation_record_with_unfreeze( record=record, cancelled_by=request.user, ) return Response(AllocationRecordSerializer(updated).data, status=status.HTTP_200_OK)