|
|
@@ -11,6 +11,7 @@ from django.utils import timezone
|
|
|
from django.db import transaction
|
|
|
import logging
|
|
|
from rest_framework import status
|
|
|
+from rest_framework.decorators import action
|
|
|
from .models import DeviceModel,LocationModel,LocationGroupModel,LocationContainerLink,LocationChangeLog,alloction_pre,base_location
|
|
|
from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel
|
|
|
|
|
|
@@ -21,7 +22,7 @@ from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer
|
|
|
# 以后添加模块时,只需要在这里添加即可
|
|
|
from rest_framework.permissions import AllowAny
|
|
|
from container.models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel
|
|
|
-from django.db.models import Prefetch
|
|
|
+from django.db.models import Prefetch, Count, Q
|
|
|
import copy
|
|
|
import json
|
|
|
from collections import defaultdict
|
|
|
@@ -461,8 +462,8 @@ class locationGroupViewSet(viewsets.ModelViewSet):
|
|
|
if group_obj:
|
|
|
data['id'] = group_obj.id
|
|
|
logger.info(f"库位组 {group_code} 已存在")
|
|
|
- # 更新现有库位组
|
|
|
- serializer = LocationGroupPostSerializer(group_obj, data=data)
|
|
|
+ # 更新现有库位组,支持部分字段更新
|
|
|
+ serializer = LocationGroupPostSerializer(group_obj, data=data, partial=True)
|
|
|
try:
|
|
|
serializer.is_valid(raise_exception=True)
|
|
|
serializer.save()
|
|
|
@@ -501,6 +502,299 @@ class locationGroupViewSet(viewsets.ModelViewSet):
|
|
|
data['id'] = serializer_list.data.get('id')
|
|
|
return Response(data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
+ def check_pallet_consistency(self, request):
|
|
|
+ """
|
|
|
+ 检测库位组当前托盘数与激活的Link记录数量是否一致
|
|
|
+ """
|
|
|
+ if not request.auth:
|
|
|
+ return Response(
|
|
|
+ {'code': '401', 'message': '未授权访问', 'data': None},
|
|
|
+ status=status.HTTP_401_UNAUTHORIZED
|
|
|
+ )
|
|
|
+
|
|
|
+ only_inconsistent = str(request.query_params.get('only_inconsistent', 'true')).lower() in {'1', 'true', 'yes', 'on'}
|
|
|
+ warehouse_code = request.query_params.get('warehouse_code')
|
|
|
+ group_code = request.query_params.get('group_code')
|
|
|
+ layer_param = request.query_params.get('layer')
|
|
|
+
|
|
|
+ filters = {'is_active': True}
|
|
|
+ if warehouse_code:
|
|
|
+ filters['warehouse_code'] = warehouse_code
|
|
|
+ if group_code:
|
|
|
+ filters['group_code'] = group_code
|
|
|
+
|
|
|
+ if layer_param:
|
|
|
+ try:
|
|
|
+ filters['layer'] = int(layer_param)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ pass
|
|
|
+
|
|
|
+ prefetch_active_links = Prefetch(
|
|
|
+ 'location_items',
|
|
|
+ queryset=LocationModel.objects.prefetch_related(
|
|
|
+ Prefetch(
|
|
|
+ 'container_links',
|
|
|
+ queryset=LocationContainerLink.objects.filter(is_active=True),
|
|
|
+ to_attr='active_links'
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ to_attr='prefetched_locations'
|
|
|
+ )
|
|
|
+
|
|
|
+ groups_qs = LocationGroupModel.objects.filter(**filters).annotate(
|
|
|
+ active_pallets=Count(
|
|
|
+ 'location_items__container_links',
|
|
|
+ filter=Q(location_items__container_links__is_active=True),
|
|
|
+ distinct=True
|
|
|
+ )
|
|
|
+ ).prefetch_related(prefetch_active_links)
|
|
|
+
|
|
|
+ total_groups = groups_qs.count()
|
|
|
+ inconsistent_groups = 0
|
|
|
+ result_data = []
|
|
|
+
|
|
|
+ for group in groups_qs:
|
|
|
+ locations = getattr(group, 'prefetched_locations', [])
|
|
|
+ actual_quantity = getattr(group, 'active_pallets', 0)
|
|
|
+ recorded_quantity = group.current_quantity
|
|
|
+ is_consistent = recorded_quantity == actual_quantity
|
|
|
+
|
|
|
+ if not is_consistent:
|
|
|
+ inconsistent_groups += 1
|
|
|
+
|
|
|
+ if only_inconsistent and is_consistent:
|
|
|
+ continue
|
|
|
+
|
|
|
+ location_details = []
|
|
|
+ if not is_consistent:
|
|
|
+ for location in locations:
|
|
|
+ active_links = getattr(location, 'active_links', [])
|
|
|
+ active_count = len(active_links)
|
|
|
+ location_details.append({
|
|
|
+ 'location_id': location.id,
|
|
|
+ 'location_code': location.location_code,
|
|
|
+ 'status': location.status,
|
|
|
+ 'recorded_quantity': location.current_quantity,
|
|
|
+ 'link_quantity': active_count,
|
|
|
+ 'difference': active_count - location.current_quantity
|
|
|
+ })
|
|
|
+
|
|
|
+ result_data.append({
|
|
|
+ 'group_id': group.id,
|
|
|
+ 'group_code': group.group_code,
|
|
|
+ 'group_name': group.group_name,
|
|
|
+ 'warehouse_code': group.warehouse_code,
|
|
|
+ 'layer': group.layer,
|
|
|
+ 'status': group.status,
|
|
|
+ 'recorded_quantity': recorded_quantity,
|
|
|
+ 'link_quantity': actual_quantity,
|
|
|
+ 'difference': actual_quantity - recorded_quantity,
|
|
|
+ 'is_consistent': is_consistent,
|
|
|
+ 'locations': location_details
|
|
|
+ })
|
|
|
+
|
|
|
+ log_operation(
|
|
|
+ request=self.request,
|
|
|
+ operation_content=f"执行库位组托盘数一致性检测,共检测 {total_groups} 个库位组,发现 {inconsistent_groups} 个异常",
|
|
|
+ operation_level="view",
|
|
|
+ operator=self.request.auth.name if self.request.auth else None,
|
|
|
+ module_name="库位"
|
|
|
+ )
|
|
|
+
|
|
|
+ response_data = {
|
|
|
+ 'timestamp': timezone.now().isoformat(),
|
|
|
+ 'summary': {
|
|
|
+ 'total_groups': total_groups,
|
|
|
+ 'inconsistent_groups': inconsistent_groups,
|
|
|
+ 'consistent_groups': total_groups - inconsistent_groups
|
|
|
+ },
|
|
|
+ 'data': result_data
|
|
|
+ }
|
|
|
+ return Response(response_data, status=status.HTTP_200_OK)
|
|
|
+
|
|
|
+ @action(methods=['post'], detail=False, url_path='fix-pallet-consistency')
|
|
|
+ def fix_pallet_consistency(self, request):
|
|
|
+ """
|
|
|
+ 修复库位组托盘数不一致问题:
|
|
|
+ - 将库位组记录的 current_quantity 校准为实际激活托盘数
|
|
|
+ - 将组内库位的 current_quantity 校准为各自的激活托盘数
|
|
|
+ """
|
|
|
+ if not request.auth:
|
|
|
+ return Response(
|
|
|
+ {'code': '401', 'message': '未授权访问', 'data': None},
|
|
|
+ status=status.HTTP_401_UNAUTHORIZED
|
|
|
+ )
|
|
|
+
|
|
|
+ group_codes = request.data.get('group_codes')
|
|
|
+ warehouse_code = request.data.get('warehouse_code')
|
|
|
+ group_code_filter = None
|
|
|
+ if isinstance(group_codes, (list, tuple, set)):
|
|
|
+ group_code_filter = [code for code in group_codes if code]
|
|
|
+
|
|
|
+ layer_param = request.data.get('layer')
|
|
|
+
|
|
|
+ filters = {'is_active': True}
|
|
|
+ if warehouse_code:
|
|
|
+ filters['warehouse_code'] = warehouse_code
|
|
|
+ if layer_param is not None:
|
|
|
+ try:
|
|
|
+ filters['layer'] = int(layer_param)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ pass
|
|
|
+ if group_code_filter:
|
|
|
+ filters['group_code__in'] = group_code_filter
|
|
|
+
|
|
|
+ prefetch_active_links = Prefetch(
|
|
|
+ 'location_items',
|
|
|
+ queryset=LocationModel.objects.prefetch_related(
|
|
|
+ Prefetch(
|
|
|
+ 'container_links',
|
|
|
+ queryset=LocationContainerLink.objects.filter(is_active=True),
|
|
|
+ to_attr='active_links'
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ to_attr='prefetched_locations'
|
|
|
+ )
|
|
|
+
|
|
|
+ annotated_groups = list(
|
|
|
+ LocationGroupModel.objects.filter(**filters)
|
|
|
+ .annotate(
|
|
|
+ active_pallets=Count(
|
|
|
+ 'location_items__container_links',
|
|
|
+ filter=Q(location_items__container_links__is_active=True),
|
|
|
+ distinct=True,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .values('id', 'group_code', 'active_pallets')
|
|
|
+ )
|
|
|
+
|
|
|
+ if not annotated_groups:
|
|
|
+ return Response(
|
|
|
+ {
|
|
|
+ 'timestamp': timezone.now().isoformat(),
|
|
|
+ 'summary': {
|
|
|
+ 'total_groups': 0,
|
|
|
+ 'processed_groups': 0,
|
|
|
+ 'fixed_groups': 0,
|
|
|
+ 'fixed_locations': 0,
|
|
|
+ 'skipped_groups': 0,
|
|
|
+ },
|
|
|
+ 'details': {
|
|
|
+ 'fixed_groups': [],
|
|
|
+ 'fixed_locations': [],
|
|
|
+ 'skipped_groups': [],
|
|
|
+ }
|
|
|
+ },
|
|
|
+ status=status.HTTP_200_OK
|
|
|
+ )
|
|
|
+
|
|
|
+ group_stats_map = {
|
|
|
+ item['id']: {
|
|
|
+ 'group_code': item['group_code'],
|
|
|
+ 'active_pallets': item.get('active_pallets') or 0,
|
|
|
+ }
|
|
|
+ for item in annotated_groups
|
|
|
+ }
|
|
|
+ group_ids = list(group_stats_map.keys())
|
|
|
+
|
|
|
+ groups_qs = LocationGroupModel.objects.filter(id__in=group_ids).prefetch_related(prefetch_active_links)
|
|
|
+
|
|
|
+ total_groups = len(group_ids)
|
|
|
+
|
|
|
+ fixed_groups = []
|
|
|
+ fixed_locations = []
|
|
|
+ skipped_groups = []
|
|
|
+ processed_groups = 0
|
|
|
+
|
|
|
+ try:
|
|
|
+ with transaction.atomic():
|
|
|
+ for group in groups_qs.select_for_update():
|
|
|
+ stats = group_stats_map.get(group.id, {})
|
|
|
+ actual_quantity = stats.get('active_pallets', 0)
|
|
|
+ group_code = stats.get('group_code') or group.group_code
|
|
|
+ processed_groups += 1
|
|
|
+ recorded_quantity = group.current_quantity or 0
|
|
|
+ if recorded_quantity != actual_quantity:
|
|
|
+ old_quantity = recorded_quantity
|
|
|
+ group.current_quantity = actual_quantity
|
|
|
+ group.save(update_fields=['current_quantity'])
|
|
|
+ fixed_groups.append({
|
|
|
+ 'group_id': group.id,
|
|
|
+ 'group_code': group_code,
|
|
|
+ 'old_quantity': old_quantity,
|
|
|
+ 'new_quantity': actual_quantity,
|
|
|
+ 'difference': actual_quantity - old_quantity,
|
|
|
+ })
|
|
|
+
|
|
|
+ locations = getattr(group, 'prefetched_locations', [])
|
|
|
+ for location in locations:
|
|
|
+ active_links = getattr(location, 'active_links', [])
|
|
|
+ active_count = len(active_links)
|
|
|
+ recorded_loc_qty = location.current_quantity or 0
|
|
|
+ if recorded_loc_qty != active_count:
|
|
|
+ old_loc_qty = recorded_loc_qty
|
|
|
+ LocationModel.objects.filter(pk=location.id).update(
|
|
|
+ current_quantity=active_count,
|
|
|
+ update_time=timezone.now()
|
|
|
+ )
|
|
|
+ fixed_locations.append({
|
|
|
+ 'location_id': location.id,
|
|
|
+ 'location_code': location.location_code,
|
|
|
+ 'group_code': group_code,
|
|
|
+ 'old_quantity': old_loc_qty,
|
|
|
+ 'new_quantity': active_count,
|
|
|
+ 'difference': active_count - old_loc_qty,
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ skipped_groups.append({
|
|
|
+ 'group_id': group.id,
|
|
|
+ 'group_code': group_code,
|
|
|
+ 'recorded_quantity': recorded_quantity,
|
|
|
+ 'actual_quantity': actual_quantity,
|
|
|
+ })
|
|
|
+
|
|
|
+ summary = {
|
|
|
+ 'total_groups': total_groups,
|
|
|
+ 'processed_groups': processed_groups,
|
|
|
+ 'fixed_groups': len(fixed_groups),
|
|
|
+ 'fixed_locations': len(fixed_locations),
|
|
|
+ 'skipped_groups': len(skipped_groups),
|
|
|
+ }
|
|
|
+
|
|
|
+ log_operation(
|
|
|
+ request=self.request,
|
|
|
+ operation_content=(
|
|
|
+ f"执行托盘数修复:共处理 {processed_groups} 个库位组,"
|
|
|
+ f"修复 {len(fixed_groups)} 个库位组,调整 {len(fixed_locations)} 个库位"
|
|
|
+ ),
|
|
|
+ operation_level="update",
|
|
|
+ operator=self.request.auth.name if self.request.auth else None,
|
|
|
+ module_name="库位"
|
|
|
+ )
|
|
|
+
|
|
|
+ return Response(
|
|
|
+ {
|
|
|
+ 'timestamp': timezone.now().isoformat(),
|
|
|
+ 'summary': summary,
|
|
|
+ 'details': {
|
|
|
+ 'fixed_groups': fixed_groups,
|
|
|
+ 'fixed_locations': fixed_locations,
|
|
|
+ 'skipped_groups': skipped_groups,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ status=status.HTTP_200_OK
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as exc:
|
|
|
+ log_failure_operation(
|
|
|
+ request=self.request,
|
|
|
+ operation_content=f"托盘数修复失败: {str(exc)}",
|
|
|
+ operation_level="update",
|
|
|
+ operator=self.request.auth.name if self.request.auth else None,
|
|
|
+ module_name="库位"
|
|
|
+ )
|
|
|
+ raise
|
|
|
+
|
|
|
class LocationAllocation:
|
|
|
# 入库规则函数
|
|
|
# fun:get_pallet_count_by_batch: 根据托盘码查询批次下托盘总数
|