1
0
forked from erp-dev/erp
Files
erpnew/api_v1/views/business/pre_sales/views.py
2026-02-02 17:32:06 +08:00

456 lines
17 KiB
Python

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)