2 Commits 5323634f69 ... bbf3cb3e4c

Author SHA1 Message Date
  flower_bs bbf3cb3e4c 推送 1 month ago
  flower_bs 72a3ba7760 托盘详情修复 1 month ago
73 changed files with 2501 additions and 242 deletions
  1. 4 0
      bin/serializers.py
  2. 2 0
      bin/urls.py
  3. 297 3
      bin/views.py
  4. 3 3
      container/container_operate.py
  5. 2 3
      container/models.py
  6. 75 16
      container/views.py
  7. 98 0
      location_statistics/serializers.py
  8. 29 11
      location_statistics/services.py
  9. 226 18
      location_statistics/views.py
  10. 0 1
      templates/dist/spa/css/10.009f6600.css
  11. 1 0
      templates/dist/spa/css/10.d66de84e.css
  12. 0 1
      templates/dist/spa/css/3.98304b39.css
  13. 1 0
      templates/dist/spa/css/3.bfd66d46.css
  14. 0 1
      templates/dist/spa/css/34.0d4c4716.css
  15. 0 0
      templates/dist/spa/css/34.5557cd4a.css
  16. 0 0
      templates/dist/spa/css/35.8f3f6188.css
  17. 0 0
      templates/dist/spa/css/36.44ddcebd.css
  18. 0 0
      templates/dist/spa/css/37.2ac1dad1.css
  19. 0 0
      templates/dist/spa/css/38.12670fd1.css
  20. 0 0
      templates/dist/spa/css/39.9478c981.css
  21. 0 1
      templates/dist/spa/css/4.1df4ea51.css
  22. 1 0
      templates/dist/spa/css/4.66ad7197.css
  23. 0 0
      templates/dist/spa/css/40.c4652654.css
  24. 0 0
      templates/dist/spa/css/41.7a23b7fb.css
  25. 1 0
      templates/dist/spa/css/42.07732723.css
  26. 0 1
      templates/dist/spa/css/chunk-common.ab88e7ab.css
  27. 1 0
      templates/dist/spa/css/chunk-common.e7ea1610.css
  28. 1 1
      templates/dist/spa/index.html
  29. 0 1
      templates/dist/spa/js/10.6404af04.js
  30. BIN
      templates/dist/spa/js/10.6404af04.js.gz
  31. 1 0
      templates/dist/spa/js/10.7083f21f.js
  32. BIN
      templates/dist/spa/js/10.7083f21f.js.gz
  33. 0 1
      templates/dist/spa/js/3.4366e891.js
  34. BIN
      templates/dist/spa/js/3.4366e891.js.gz
  35. 1 0
      templates/dist/spa/js/3.d2c5199b.js
  36. BIN
      templates/dist/spa/js/3.d2c5199b.js.gz
  37. 1 1
      templates/dist/spa/js/35.08b4d310.js
  38. BIN
      templates/dist/spa/js/35.08b4d310.js.gz
  39. 0 1
      templates/dist/spa/js/34.fe660215.js
  40. 1 1
      templates/dist/spa/js/36.0ef8719a.js
  41. BIN
      templates/dist/spa/js/35.73cf8fc6.js.gz
  42. 1 1
      templates/dist/spa/js/37.faa1b078.js
  43. BIN
      templates/dist/spa/js/37.faa1b078.js.gz
  44. BIN
      templates/dist/spa/js/36.0ef8719a.js.gz
  45. 1 1
      templates/dist/spa/js/38.51ed54fd.js
  46. BIN
      templates/dist/spa/js/37.54c90e2b.js.gz
  47. BIN
      templates/dist/spa/js/38.51ed54fd.js.gz
  48. 1 1
      templates/dist/spa/js/39.d57f53e6.js
  49. BIN
      templates/dist/spa/js/39.d57f53e6.js.gz
  50. 1 1
      templates/dist/spa/js/40.0df87f4e.js
  51. 0 1
      templates/dist/spa/js/4.08fdf7c6.js
  52. BIN
      templates/dist/spa/js/4.08fdf7c6.js.gz
  53. 1 0
      templates/dist/spa/js/4.4b393725.js
  54. BIN
      templates/dist/spa/js/4.4b393725.js.gz
  55. 1 1
      templates/dist/spa/js/41.0f33ab80.js
  56. 1 1
      templates/dist/spa/js/42.f341b76d.js
  57. 1 0
      templates/dist/spa/js/42.95116c09.js
  58. BIN
      templates/dist/spa/js/42.95116c09.js.gz
  59. 1 1
      templates/dist/spa/js/app.805f1cc7.js
  60. BIN
      templates/dist/spa/js/app.2dbb2966.js.gz
  61. BIN
      templates/dist/spa/js/app.805f1cc7.js.gz
  62. 1 0
      templates/dist/spa/js/chunk-common.82036531.js
  63. BIN
      templates/dist/spa/js/chunk-common.82036531.js.gz
  64. 0 1
      templates/dist/spa/js/chunk-common.9aae00f4.js
  65. BIN
      templates/dist/spa/js/chunk-common.9aae00f4.js.gz
  66. 1 1
      templates/dist/spa/js/vendor.1ba90913.js
  67. BIN
      templates/dist/spa/js/vendor.1ba90913.js.gz
  68. 1 1
      templates/src/components/containercard copy.vue
  69. 1 1
      templates/src/components/containercard.vue
  70. 7 7
      templates/src/pages/container/containerdetail.vue
  71. 7 7
      templates/src/pages/count/containerDetail.vue
  72. 448 27
      templates/src/pages/stock/management.vue
  73. 1280 124
      templates/src/pages/stock/stockbinlist.vue

+ 4 - 0
bin/serializers.py

@@ -45,6 +45,10 @@ class LocationGroupPostSerializer(serializers.ModelSerializer):
         model = LocationGroupModel
         fields = '__all__'
         read_only_fields = ['id']
+        extra_kwargs = {
+            'current_goods_code': {'required': False, 'allow_blank': True},
+            'current_batch': {'required': False, 'allow_blank': True},
+        }
 
 
 

+ 2 - 0
bin/urls.py

@@ -7,6 +7,8 @@ urlpatterns = [
     re_path(r'^(?P<pk>\d+)/$',  views.locationViewSet.as_view({"get": "retrieve", "put": "update"}), name='location_detail'),
 
     path (r'group/', views.locationGroupViewSet.as_view({"get": "list"}), name='location_list'),
+    path (r'group/check-pallet-consistency/', views.locationGroupViewSet.as_view({"get": "check_pallet_consistency"}), name='location_group_check_pallet_consistency'),
+    path (r'group/fix-pallet-consistency/', views.locationGroupViewSet.as_view({"post": "fix_pallet_consistency"}), name='location_group_fix_pallet_consistency'),
     re_path(r'^group/(?P<pk>\d+)/$',  views.locationGroupViewSet.as_view({"get": "retrieve", "put": "update"}), name='location_detail'),
     # path(r'management/', views.stockshelfViewSet.as_view({"get": "list", "post": "create"}), name="management"),
     # re_path(r'^management/(?P<pk>\d+)/$', views.stockshelfViewSet.as_view({'get': 'retrieve','put': 'update','patch': 'partial_update','delete': 'destroy'}), name="staff_1"),

+ 297 - 3
bin/views.py

@@ -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: 根据托盘码查询批次下托盘总数

+ 3 - 3
container/container_operate.py

@@ -257,7 +257,7 @@ class RecordBuilder:
             "goods_desc": bound_obj.goods_desc,
             "goods_qty": qty_diff,
             "goods_weight": bound_obj.goods_weight,
-            "status": 1,
+            "status": 2,
             "month": data['month'],
             "creater": data.get('creater', 'wms')
         }
@@ -271,7 +271,7 @@ class RecordBuilder:
             "goods_desc": '托盘组',
             "goods_qty": qty_diff,
             "goods_weight": 0,
-            "status": 1,
+            "status": 2,
             "month": data['month'],
             "creater": data.get('creater', 'wms')
         }
@@ -328,7 +328,7 @@ class DetailRecordCreator:
         container_obj = ContainerListModel.objects.filter(id=data['container']).first()
         container_obj.status = 5
         container_obj.save()
-        if ContainerDetailModel.objects.filter(container=container_obj,goods_code=data['goods_code'],status=1,is_delete=False).first():
+        if ContainerDetailModel.objects.filter(container=container_obj,goods_code=data['goods_code'],status=2,is_delete=False).first():
             return 
         ContainerDetailModel.objects.create(
             container=container_obj,

+ 2 - 3
container/models.py

@@ -103,9 +103,8 @@ class ContainerListModel(models.Model):
 class ContainerDetailModel(models.Model):
     BATCH_STATUS=(
         (0, '空盘'),
-        (1, '组盘'),
-        (2, '在库'),
-        (3, '已出库')
+        (2, '在盘'),
+        (3, '离库')
     )
     BATCH_CLASS = (
         (1, '成品'),

+ 75 - 16
container/views.py

@@ -2320,24 +2320,83 @@ class OutboundService:
 
     @staticmethod
     def process_next_task():
-        """处理下一个任务"""
-        next_task = ContainerWCSModel.objects.filter(status=100,working=1,is_delete=False).order_by('sequence').first()
-        if not next_task:
+        """处理下一个任务 - 优化:同一批次连续出,支持一次下发两条任务"""
+        # 获取待处理任务,优先按批次排序(同一批次连续出),同一批次内按sequence排序
+        # 使用Case处理batch_out为None的情况,确保有批次的任务优先
+        from django.db.models import F, Case, When, IntegerField
+        
+        def get_pending_tasks():
+            """获取待处理任务查询集"""
+            return ContainerWCSModel.objects.filter(
+                status=100, 
+                working=1, 
+                is_delete=False
+            ).annotate(
+                # 为排序添加批次ID字段,None值排最后
+                batch_out_id_for_sort=Case(
+                    When(batch_out__isnull=False, then=F('batch_out_id')),
+                    default=999999999,  # None值使用大数字,排到最后
+                    output_field=IntegerField()
+                )
+            ).order_by('batch_out_id_for_sort', 'sequence')
+        
+        pending_tasks = get_pending_tasks()
+        
+        if not pending_tasks.exists():
             logger.info("没有待处理任务")
             return
-        location = next_task.current_location
-        if location =='103' or location =='203':
-            logger.info (f"需要跳过该任务")
-            next_task.status = 200
-            next_task.working = 0
-            next_task.save()
-            OutboundService.process_next_task()
-            return
-  
-
-        allocator = LocationAllocation()
-        OutboundService.perform_initial_allocation(allocator, next_task.current_location)
-        OutboundService.send_task_to_wcs(next_task)
+        
+        # 处理任务列表(最多处理2条,保证WCS有缓冲)
+        processed_count = 0
+        max_tasks = 2
+        skip_count = 0
+        max_skip = 5  # 最多跳过5个任务,避免无限循环
+        dispatched_ids = set()
+        
+        while processed_count < max_tasks and skip_count < max_skip:
+            # 重新获取待处理任务(因为可能有任务被跳过)
+            pending_tasks = get_pending_tasks().exclude(pk__in=dispatched_ids)
+            if not pending_tasks.exists():
+                break
+            
+            next_task = pending_tasks.first()
+            dispatched_ids.add(next_task.pk)
+            location = next_task.current_location
+            
+            if location == '103' or location == '203':
+                logger.info(f"需要跳过该任务: {next_task.taskid}, 位置: {location}")
+                next_task.status = 200
+                next_task.working = 0
+                next_task.save()
+                skip_count += 1
+                # 跳过这个任务后,继续处理下一个
+                continue
+            
+            try:
+                allocator = LocationAllocation()
+                allocation_success = OutboundService.perform_initial_allocation(
+                    allocator,
+                    next_task.current_location
+                )
+                if not allocation_success:
+                    logger.warning(f"任务分配失败,跳过: {next_task.taskid}")
+                    skip_count += 1
+                    continue
+                OutboundService.send_task_to_wcs(next_task)
+                # 标记任务为已下发,避免重复下发
+                next_task.status = 150
+                next_task.save(update_fields=['status'])
+                processed_count += 1
+                logger.info(f"成功下发任务: {next_task.taskid}, 批次: {next_task.batch_out_id if next_task.batch_out else '无批次'}")
+            except Exception as e:
+                logger.error(f"任务处理失败: {next_task.taskid}, 错误: {str(e)}")
+                # 处理失败后,继续尝试下一个任务
+                skip_count += 1
+        
+        if processed_count > 0:
+            logger.info(f"本次共下发 {processed_count} 条任务")
+        elif skip_count >= max_skip:
+            logger.warning(f"跳过了 {skip_count} 个任务,未找到可处理的任务")
     
     @staticmethod
     def process_current_task(task_id):

+ 98 - 0
location_statistics/serializers.py

@@ -6,16 +6,36 @@ class LocationStatisticsSerializer(serializers.ModelSerializer):
     
     warehouse_display = serializers.CharField(source='warehouse_name', read_only=True)
     layer_display = serializers.SerializerMethodField()
+    t5_group_count = serializers.SerializerMethodField()
+    t4_group_count = serializers.SerializerMethodField()
+    s4_group_count = serializers.SerializerMethodField()
+    t2_group_count = serializers.SerializerMethodField()
+    t1_group_count = serializers.SerializerMethodField()
+    t5_used_group_count = serializers.SerializerMethodField()
+    t4_used_group_count = serializers.SerializerMethodField()
+    s4_used_group_count = serializers.SerializerMethodField()
+    t2_used_group_count = serializers.SerializerMethodField()
+    t1_used_group_count = serializers.SerializerMethodField()
+    t5_available_group_count = serializers.SerializerMethodField()
+    t4_available_group_count = serializers.SerializerMethodField()
+    s4_available_group_count = serializers.SerializerMethodField()
+    t2_available_group_count = serializers.SerializerMethodField()
+    t1_available_group_count = serializers.SerializerMethodField()
     
     class Meta:
         model = LocationStatistics
         fields = [
             'id', 'warehouse_code', 'warehouse_name', 'warehouse_display', 'layer', 'layer_display',
             't5_total', 't5_used', 't5_available',
+            't5_group_count', 't5_used_group_count', 't5_available_group_count',
             't4_total', 't4_used', 't4_available',
+            't4_group_count', 't4_used_group_count', 't4_available_group_count',
             's4_total', 's4_used', 's4_available',
+            's4_group_count', 's4_used_group_count', 's4_available_group_count',
             't2_total', 't2_used', 't2_available',
+            't2_group_count', 't2_used_group_count', 't2_available_group_count',
             't1_total', 't1_used', 't1_available',
+            't1_group_count', 't1_used_group_count', 't1_available_group_count',
             'total_locations', 'total_used', 'total_available', 'utilization_rate',
             'statistic_time', 'is_latest'
         ]
@@ -23,6 +43,84 @@ class LocationStatisticsSerializer(serializers.ModelSerializer):
     def get_layer_display(self, obj):
         return f"{obj.layer}层"
 
+    def get_t5_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T5')
+
+    def get_t5_used_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T5', 'used')
+
+    def get_t5_available_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T5', 'available')
+
+    def get_t4_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T4')
+
+    def get_t4_used_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T4', 'used')
+
+    def get_t4_available_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T4', 'available')
+
+    def get_s4_group_count(self, obj):
+        return self._get_type_group_count(obj, 'S4')
+
+    def get_s4_used_group_count(self, obj):
+        return self._get_type_group_count(obj, 'S4', 'used')
+
+    def get_s4_available_group_count(self, obj):
+        return self._get_type_group_count(obj, 'S4', 'available')
+
+    def get_t2_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T2')
+
+    def get_t2_used_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T2', 'used')
+
+    def get_t2_available_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T2', 'available')
+
+    def get_t1_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T1')
+
+    def get_t1_used_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T1', 'used')
+
+    def get_t1_available_group_count(self, obj):
+        return self._get_type_group_count(obj, 'T1', 'available')
+
+    def _get_group_stats(self, obj):
+        if not hasattr(self, '_group_stats_cache'):
+            self._group_stats_cache = {}
+
+        key = (obj.warehouse_code, obj.layer, obj.statistic_time)
+        if key not in self._group_stats_cache:
+            self._group_stats_cache[key] = list(
+                LocationGroupStatistics.objects.filter(
+                    warehouse_code=obj.warehouse_code,
+                    layer=obj.layer,
+                    statistic_time=obj.statistic_time
+                )
+            )
+
+        return self._group_stats_cache[key]
+
+    def _get_type_group_count(self, obj, type_code, usage=None):
+        group_stats = self._get_group_stats(obj)
+        count = 0
+        for stat in group_stats:
+            breakdown = stat.location_type_breakdown or {}
+            type_info = breakdown.get(type_code, {})
+            if usage == 'used':
+                if type_info.get('used', 0) > 0:
+                    count += 1
+            elif usage == 'available':
+                if type_info.get('available', 0) > 0:
+                    count += 1
+            else:
+                if type_info.get('total', 0) > 0:
+                    count += 1
+        return count
+
 class LocationGroupStatisticsSerializer(serializers.ModelSerializer):
     """货位组统计序列化器"""
     

+ 29 - 11
location_statistics/services.py

@@ -46,37 +46,55 @@ class LocationStatisticsService:
                     'used': 0,
                     'available': 0
                 }
-            
+
+            status = (location.status or '').lower()
+            is_used = status in {'occupied', 'reserved'}
+            is_available = status == 'available'
+
             # 统计货位类型
             loc_type = location.location_type
             if loc_type not in statistics_data[key]['location_types']:
                 statistics_data[key]['location_types'][loc_type] = {
                     'total': 0, 'used': 0, 'available': 0
                 }
-            
+
             statistics_data[key]['location_types'][loc_type]['total'] += 1
             statistics_data[key]['total'] += 1
-            
+
             # 根据状态统计
-            if location.status in ['occupied', 'reserved']:
+            if is_used:
                 statistics_data[key]['location_types'][loc_type]['used'] += 1
                 statistics_data[key]['used'] += 1
-            elif location.status == 'available':
+            elif is_available:
                 statistics_data[key]['location_types'][loc_type]['available'] += 1
                 statistics_data[key]['available'] += 1
-            
+
             # 统计货位组
             group = location.location_group
             if group not in statistics_data[key]['groups']:
                 statistics_data[key]['groups'][group] = {
-                    'total': 0, 'used': 0, 'available': 0
+                    'total': 0,
+                    'used': 0,
+                    'available': 0,
+                    'types': {}
                 }
-            
+
             statistics_data[key]['groups'][group]['total'] += 1
-            if location.status == 'occupied':
+            if is_used:
                 statistics_data[key]['groups'][group]['used'] += 1
-            elif location.status == 'available':
+            elif is_available:
                 statistics_data[key]['groups'][group]['available'] += 1
+
+            # 统计组内货位类型分布
+            group_types = statistics_data[key]['groups'][group]['types']
+            if loc_type not in group_types:
+                group_types[loc_type] = {'total': 0, 'used': 0, 'available': 0}
+
+            group_types[loc_type]['total'] += 1
+            if is_used:
+                group_types[loc_type]['used'] += 1
+            elif is_available:
+                group_types[loc_type]['available'] += 1
         
         return statistics_data
     
@@ -151,7 +169,7 @@ class LocationStatisticsService:
                     used_locations=group_data['used'],
                     available_locations=group_data['available'],
                     utilization_rate=group_utilization,
-                    location_type_breakdown={},  # 这里可以进一步细化
+                    location_type_breakdown=group_data.get('types', {}),
                     statistic_time=current_time
                 )
                 group_statistics_objects.append(group_stat)

+ 226 - 18
location_statistics/views.py

@@ -6,7 +6,8 @@ from datetime import timedelta
 from .services import LocationStatisticsService
 from .models import  LocationGroupStatistics
 from django.db import transaction
-from django.db.models import Count, Q
+from django.db.models import Count, Q, Prefetch
+from decimal import Decimal, InvalidOperation
 import logging
 
 logger = logging.getLogger(__name__)
@@ -145,19 +146,43 @@ class LocationConsistencyCheckView(APIView):
     
     def post(self, request):
         warehouse_code = request.data.get('warehouse_code')
-        layer = request.GET.get('layer')
-        if int(layer) < 1 :
-            layer = None
-        auto_fix = request.data.get('auto_fix', False)
-        
-        checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
-        result = checker.check_all()
+        layer_param = request.GET.get('layer')
+        layer = None
+        if layer_param is not None:
+            try:
+                layer_value = int(layer_param)
+                if layer_value > 0:
+                    layer = layer_value
+            except (TypeError, ValueError):
+                layer = None
+        auto_fix = self._to_bool(request.data.get('auto_fix', False))
+        fix_scope = request.data.get('fix_scope')
+        
+        checker = LocationConsistencyChecker(
+            warehouse_code,
+            layer,
+            auto_fix,
+            fix_scope=fix_scope
+        )
+        checker.check_all()
         
         return Response({
             'success': True,
-            'data': checker.generate_report()
+            'data': checker.generate_report(),
+            'auto_fix': auto_fix,
+            'fix_scope': checker.fix_scope
         })
 
+    @staticmethod
+    def _to_bool(value):
+        if isinstance(value, bool):
+            return value
+        if isinstance(value, str):
+            return value.strip().lower() in {'1', 'true', 'yes', 'y', 'on'}
+        if isinstance(value, int):
+            return value != 0
+        return False
+
 class LocationConsistencyChecker:
     TARGET_LOCATION_TYPES = ['T5', 'T4', 'S4', 'T2', 'T1']
     """
@@ -165,7 +190,7 @@ class LocationConsistencyChecker:
     用于检测库位状态与Link记录的一致性以及库位组的状态一致性
     """
     
-    def __init__(self, warehouse_code=None, layer=None, auto_fix=False):
+    def __init__(self, warehouse_code=None, layer=None, auto_fix=False, fix_scope=None):
         """
         初始化检测器
         
@@ -173,16 +198,37 @@ class LocationConsistencyChecker:
             warehouse_code: 指定仓库代码,如果为None则检测所有仓库
             layer: 指定楼层,如果为None则检测所有楼层
             auto_fix: 是否自动修复检测到的问题
+            fix_scope: 修复范围(locations/details/groups),None表示全部
         """
         self.warehouse_code = warehouse_code
         self.layer = layer
+        if self.layer is not None:
+            try:
+                layer_int = int(self.layer)
+                self.layer = layer_int if layer_int > 0 else None
+            except (TypeError, ValueError):
+                self.layer = None
         self.auto_fix = auto_fix
+        if isinstance(fix_scope, (list, tuple, set)):
+            fix_scope = [scope for scope in fix_scope if scope in {'locations', 'groups', 'details'}]
+        elif isinstance(fix_scope, str):
+            if fix_scope == 'all':
+                fix_scope = None
+            elif fix_scope in {'locations', 'groups', 'details'}:
+                fix_scope = [fix_scope]
+            else:
+                fix_scope = None
+        else:
+            fix_scope = None
+        self.fix_scope = fix_scope or ['locations', 'groups', 'details']
         self.results = {
             'check_time': timezone.now(),
             'location_errors': [],
             'group_errors': [],
+            'detail_errors': [],
             'fixed_locations': [],
             'fixed_groups': [],
+            'fixed_details': [],
             'repair_errors': [],
             'summary': {
                 'total_locations': 0,
@@ -191,8 +237,12 @@ class LocationConsistencyChecker:
                 'total_groups': 0,
                 'checked_groups': 0,
                 'error_groups': 0,
+                'total_details': 0,
+                'checked_details': 0,
+                'error_details': 0,
                 'fixed_location_count': 0,
-                'fixed_group_count': 0
+                'fixed_group_count': 0,
+                'fixed_detail_count': 0
             }
         }
     
@@ -203,6 +253,9 @@ class LocationConsistencyChecker:
         # 检测库位状态与Link记录的一致性
         self.check_location_link_consistency()
         
+        # 检测托盘明细状态一致性
+        self.check_container_detail_status()
+        
         # 检测库位组状态一致性
         self.check_group_consistency()
         
@@ -211,7 +264,8 @@ class LocationConsistencyChecker:
             self.fix_detected_issues()
         
         logger.info(f"库位一致性检测完成,发现{self.results['summary']['error_locations']}个库位问题,"
-                   f"{self.results['summary']['error_groups']}个库位组问题")
+                   f"{self.results['summary']['error_groups']}个库位组问题,"
+                   f"{self.results['summary']['error_details']}条托盘明细问题")
         
         return self.results
     
@@ -282,6 +336,107 @@ class LocationConsistencyChecker:
         
         return True, current_status, None
     
+    def check_container_detail_status(self):
+        """检测托盘明细状态的一致性"""
+        from container.models import ContainerDetailModel
+        from bin.models import LocationContainerLink
+        
+        details_qs = (
+            ContainerDetailModel.objects.filter(is_delete=False)
+            .select_related('container', 'batch')
+            .prefetch_related(
+                Prefetch(
+                    'container__location_links',
+                    queryset=LocationContainerLink.objects.filter(is_active=True).select_related('location'),
+                    to_attr='active_links'
+                )
+            )
+        )
+        
+        total_checked = 0
+        
+        for detail in details_qs:
+            if not detail.container:
+                continue
+            
+            if not self._detail_in_scope(detail):
+                continue
+            
+            goods_qty = self._to_decimal(detail.goods_qty)
+            goods_out_qty = self._to_decimal(detail.goods_out_qty)
+            remaining_qty = goods_qty - goods_out_qty
+            expected_status, error_type = self._determine_detail_status(remaining_qty)
+            
+            total_checked += 1
+            self.results['summary']['checked_details'] += 1
+            
+            if expected_status is None:
+                continue
+            
+            if detail.status != expected_status:
+                self.results['summary']['error_details'] += 1
+                self.results['detail_errors'].append({
+                    'detail_id': detail.id,
+                    'container_id': detail.container_id,
+                    'container_code': getattr(detail.container, 'container_code', None),
+                    'batch_id': detail.batch_id,
+                    'batch_number': getattr(detail.batch, 'bound_number', None) if detail.batch else None,
+                    'goods_qty': str(goods_qty),
+                    'goods_out_qty': str(goods_out_qty),
+                    'remaining_qty': str(remaining_qty),
+                    'current_status': detail.status,
+                    'current_status_display': self._status_display(detail.status),
+                    'expected_status': expected_status,
+                    'expected_status_display': self._status_display(expected_status),
+                    'error_type': error_type,
+                    'detected_at': timezone.now()
+                })
+        
+        self.results['summary']['total_details'] = total_checked
+    
+    def _detail_in_scope(self, detail):
+        """判断托盘明细是否在当前检测范围内"""
+        if not (self.warehouse_code or self.layer):
+            return True
+        
+        active_links = getattr(detail.container, 'active_links', None)
+        if not active_links:
+            return False
+        
+        warehouse_ok = True
+        layer_ok = True
+        
+        if self.warehouse_code:
+            warehouse_ok = any(link.location.warehouse_code == self.warehouse_code for link in active_links)
+        
+        if self.layer is not None:
+            layer_ok = any(link.location.layer == self.layer for link in active_links)
+        
+        return warehouse_ok and layer_ok
+    
+    def _to_decimal(self, value):
+        if isinstance(value, Decimal):
+            return value
+        if value is None:
+            return Decimal('0')
+        try:
+            return Decimal(str(value))
+        except (InvalidOperation, TypeError, ValueError):
+            return Decimal('0')
+    
+    def _determine_detail_status(self, remaining_qty):
+        if remaining_qty > 0:
+            return 2, 'detail_should_be_in_stock'
+        return 3, 'detail_should_be_outbound'
+    
+    def _status_display(self, status):
+        status_map = {
+            0: '空盘',
+            2: '在盘',
+            3: '离库'
+        }
+        return status_map.get(status, str(status) if status is not None else '未知')
+    
     def check_group_consistency(self):
         """检测库位组状态的一致性"""
         from bin.models import LocationGroupModel
@@ -380,15 +535,20 @@ class LocationConsistencyChecker:
         logger.info("开始修复检测到的不一致问题")
         
         # 修复库位状态不一致
-        if self.results['location_errors']:
+        if 'locations' in self.fix_scope and self.results['location_errors']:
             self._fix_location_issues()
         
+        # 修复托盘明细状态不一致
+        if 'details' in self.fix_scope and self.results['detail_errors']:
+            self._fix_detail_issues()
+        
         # 修复库位组状态不一致
-        if self.results['group_errors']:
+        if 'groups' in self.fix_scope and self.results['group_errors']:
             self._fix_group_issues()
         
         logger.info(f"修复完成: {self.results['summary']['fixed_location_count']}个库位, "
-                   f"{self.results['summary']['fixed_group_count']}个库位组")
+                   f"{self.results['summary']['fixed_group_count']}个库位组, "
+                   f"{self.results['summary']['fixed_detail_count']}条托盘明细")
     
     def _fix_location_issues(self):
         """修复库位状态不一致问题"""
@@ -478,21 +638,67 @@ class LocationConsistencyChecker:
                     'error_type': error.get('error_type')
                 })
     
+    def _fix_detail_issues(self):
+        """修复托盘明细状态不一致问题"""
+        from container.models import ContainerDetailModel
+        
+        for error in self.results['detail_errors']:
+            expected_status = error.get('expected_status')
+            if expected_status not in [2, 3]:
+                continue
+            
+            try:
+                with transaction.atomic():
+                    detail = ContainerDetailModel.objects.select_for_update().select_related('container').get(id=error['detail_id'])
+                    if detail.status == expected_status:
+                        continue
+                    
+                    old_status = detail.status
+                    detail.status = expected_status
+                    detail.save(update_fields=['status'])
+                    
+                    self.results['summary']['fixed_detail_count'] += 1
+                    error['fixed'] = True
+                    error['fixed_at'] = timezone.now()
+                    error['new_status'] = expected_status
+                    error['new_status_display'] = self._status_display(expected_status)
+                    self.results['fixed_details'].append({
+                        'detail_id': detail.id,
+                        'container_code': getattr(detail.container, 'container_code', None),
+                        'old_status': old_status,
+                        'new_status': expected_status,
+                        'fixed_at': timezone.now(),
+                        'error_type': error.get('error_type')
+                    })
+            except ContainerDetailModel.DoesNotExist:
+                logger.warning(f"托盘明细 {error.get('detail_id')} 不存在,跳过修复")
+            except Exception as e:
+                logger.error(f"修复托盘明细{error.get('detail_id')}失败: {str(e)}")
+                self.results['repair_errors'].append({
+                    'type': 'detail_fix_error',
+                    'detail_id': error.get('detail_id'),
+                    'error_message': str(e),
+                    'error_type': error.get('error_type')
+                })
+    
     def get_summary(self):
         """获取检测摘要"""
         return {
             'check_time': self.results['check_time'],
             'total_checked': {
                 'locations': self.results['summary']['checked_locations'],
-                'groups': self.results['summary']['checked_groups']
+                'groups': self.results['summary']['checked_groups'],
+                'details': self.results['summary']['checked_details']
             },
             'errors_found': {
                 'locations': self.results['summary']['error_locations'],
-                'groups': self.results['summary']['error_groups']
+                'groups': self.results['summary']['error_groups'],
+                'details': self.results['summary']['error_details']
             },
             'fixed': {
                 'locations': self.results['summary']['fixed_location_count'],
-                'groups': self.results['summary']['fixed_group_count']
+                'groups': self.results['summary']['fixed_group_count'],
+                'details': self.results['summary']['fixed_detail_count']
             },
             'has_errors': len(self.results['repair_errors']) > 0
         }
@@ -506,8 +712,10 @@ class LocationConsistencyChecker:
             'details': {
                 'location_errors': self.results['location_errors'],
                 'group_errors': self.results['group_errors'],
+                'detail_errors': self.results['detail_errors'],
                 'fixed_locations': self.results['fixed_locations'],
                 'fixed_groups': self.results['fixed_groups'],
+                'fixed_details': self.results['fixed_details'],
                 'repair_errors': self.results['repair_errors']
             }
         }

+ 0 - 1
templates/dist/spa/css/10.009f6600.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-96afaa0a]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-96afaa0a]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-96afaa0a]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-96afaa0a]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-96afaa0a]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-96afaa0a]{color:#485573}

+ 1 - 0
templates/dist/spa/css/10.d66de84e.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-655448f2]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-655448f2]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-655448f2]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-655448f2]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-655448f2]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-655448f2]{color:#485573}

File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/css/3.98304b39.css


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/css/3.bfd66d46.css


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/css/34.0d4c4716.css


templates/dist/spa/css/35.5557cd4a.css → templates/dist/spa/css/34.5557cd4a.css


templates/dist/spa/css/36.8f3f6188.css → templates/dist/spa/css/35.8f3f6188.css


templates/dist/spa/css/37.44ddcebd.css → templates/dist/spa/css/36.44ddcebd.css


templates/dist/spa/css/38.2ac1dad1.css → templates/dist/spa/css/37.2ac1dad1.css


templates/dist/spa/css/39.12670fd1.css → templates/dist/spa/css/38.12670fd1.css


templates/dist/spa/css/40.9478c981.css → templates/dist/spa/css/39.9478c981.css


+ 0 - 1
templates/dist/spa/css/4.1df4ea51.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-b05782d8]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-b05782d8]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-b05782d8]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-b05782d8]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-b05782d8]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-b05782d8]{color:#485573}

+ 1 - 0
templates/dist/spa/css/4.66ad7197.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-8dee4bfc]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-8dee4bfc]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-8dee4bfc]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-8dee4bfc]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-8dee4bfc]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-8dee4bfc]{color:#485573}

templates/dist/spa/css/41.c4652654.css → templates/dist/spa/css/40.c4652654.css


templates/dist/spa/css/42.7a23b7fb.css → templates/dist/spa/css/41.7a23b7fb.css


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/css/42.07732723.css


+ 0 - 1
templates/dist/spa/css/chunk-common.ab88e7ab.css

@@ -1 +0,0 @@
-[data-v-1c8c3736] .q-field__label{margin-top:8px;align-self:center}[data-v-1c8c3736] .q-field__control-container{padding-left:50px;margin-top:-5px}[data-v-1c8c3736] .q-table .q-editable:hover{background-color:#f0f8ff;cursor:pointer}[data-v-1c8c3736] .q-field__native{padding:5px 8px}[data-v-1c8c3736] .q-table tr.editing{background-color:#e8f5e9!important}

+ 1 - 0
templates/dist/spa/css/chunk-common.e7ea1610.css

@@ -0,0 +1 @@
+[data-v-e67ff212] .q-field__label{margin-top:8px;align-self:center}[data-v-e67ff212] .q-field__control-container{padding-left:50px;margin-top:-5px}[data-v-e67ff212] .q-table .q-editable:hover{background-color:#f0f8ff;cursor:pointer}[data-v-e67ff212] .q-field__native{padding:5px 8px}[data-v-e67ff212] .q-table tr.editing{background-color:#e8f5e9!important}

File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/index.html


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/10.6404af04.js


BIN
templates/dist/spa/js/10.6404af04.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/10.7083f21f.js


BIN
templates/dist/spa/js/10.7083f21f.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/3.4366e891.js


BIN
templates/dist/spa/js/3.4366e891.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/3.d2c5199b.js


BIN
templates/dist/spa/js/3.d2c5199b.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/35.08b4d310.js


BIN
templates/dist/spa/js/35.08b4d310.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/34.fe660215.js


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/36.0ef8719a.js


BIN
templates/dist/spa/js/35.73cf8fc6.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/37.faa1b078.js


BIN
templates/dist/spa/js/37.faa1b078.js.gz


BIN
templates/dist/spa/js/36.0ef8719a.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/38.51ed54fd.js


BIN
templates/dist/spa/js/37.54c90e2b.js.gz


BIN
templates/dist/spa/js/38.51ed54fd.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/39.d57f53e6.js


BIN
templates/dist/spa/js/39.d57f53e6.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/40.0df87f4e.js


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/4.08fdf7c6.js


BIN
templates/dist/spa/js/4.08fdf7c6.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/4.4b393725.js


BIN
templates/dist/spa/js/4.4b393725.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/41.0f33ab80.js


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/42.f341b76d.js


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/42.95116c09.js


BIN
templates/dist/spa/js/42.95116c09.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/app.805f1cc7.js


BIN
templates/dist/spa/js/app.2dbb2966.js.gz


BIN
templates/dist/spa/js/app.805f1cc7.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/chunk-common.82036531.js


BIN
templates/dist/spa/js/chunk-common.82036531.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/chunk-common.9aae00f4.js


BIN
templates/dist/spa/js/chunk-common.9aae00f4.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/vendor.1ba90913.js


BIN
templates/dist/spa/js/vendor.1ba90913.js.gz


+ 1 - 1
templates/src/components/containercard copy.vue

@@ -475,7 +475,7 @@ export default {
       _this.operate_detail = []
       var operate_detail_container = []
       _this.linkedList.clear()
-      getauth('container/operate/?status=1&container=' + _this.containerNumber)
+      getauth('container/operate/?status=2&container=' + _this.containerNumber)
         .then((res) => {
           _this.operate_detail = res.results
           if (_this.operate_detail.length === 0) return

+ 1 - 1
templates/src/components/containercard.vue

@@ -635,7 +635,7 @@ export default {
       _this.operate_detail = []
       var operate_detail_container = []
       _this.linkedList.clear()
-      getauth('container/operate/?status=1&container=' + _this.containerNumber)
+      getauth('container/operate/?status=2&container=' + _this.containerNumber)
         .then((res) => {
           _this.operate_detail = res.results
           if (_this.operate_detail.length === 0) return

+ 7 - 7
templates/src/pages/container/containerdetail.vue

@@ -564,8 +564,8 @@ export default {
 
     checkStatusToText (status) {
       const statusTexts = {
-        1: '组盘',
-        2: '在',
+        0: '空盘',
+        2: '在',
         3: '离库'
       }
 
@@ -574,9 +574,9 @@ export default {
     getRowStyle (row) {
       // 根据check_status值返回不同的背景色
       const statusColors = {
-        1: '#fff9c4', // 更浅的黄色 - 待审核
-        2: '#c8e6c9', // 更浅的绿色 - 质检合格
-        3: '#ffcdd2' // 更浅的红色 - 质检问题
+        0: '#fff9c4', // 空盘
+        2: '#c8e6c9', // 在盘
+        3: '#ffcdd2' // 离库
       }
 
       const color = statusColors[row.status] || ''
@@ -607,8 +607,8 @@ export default {
 
         case 'status':
           return [
-            { label: '组盘', value: 1 },
-            { label: '在', value: 2 },
+            { label: '空盘', value: 0 },
+            { label: '在', value: 2 },
             { label: '离库', value: 3 }
           ]
         default:

+ 7 - 7
templates/src/pages/count/containerDetail.vue

@@ -560,8 +560,8 @@ export default {
 
     checkStatusToText(status) {
       const statusTexts = {
-        1: "组盘",
-        2: "在",
+        0: "空盘",
+        2: "在",
         3: "离库",
       };
 
@@ -570,9 +570,9 @@ export default {
     getRowStyle(row) {
       // 根据check_status值返回不同的背景色
       const statusColors = {
-        1: "#fff9c4", // 更浅的黄色 - 待审核
-        2: "#c8e6c9", // 更浅的绿色 - 质检合格
-        3: "#ffcdd2", // 更浅的红色 - 质检问题
+        0: "#fff9c4", // 空盘
+        2: "#c8e6c9", // 在盘
+        3: "#ffcdd2", // 离库
       };
 
       const color = statusColors[row.status] || "";
@@ -603,8 +603,8 @@ export default {
 
         case "status":
           return [
-            { label: "组盘", value: 1 },
-            { label: "在", value: 2 },
+            { label: "空盘", value: 0 },
+            { label: "在", value: 2 },
             { label: "离库", value: 3 },
           ];
         default:

+ 448 - 27
templates/src/pages/stock/management.vue

@@ -100,14 +100,21 @@
                   class="grid-row"
                   :style="{ cursor: 'pointer' }"
                 >
-                  <div
-                    v-for="(col, colIndex) in shelf.cols"
-                    :key="`col-${colIndex}|${shelf.layer_now}`"
-                    class="grid-item"
-                    :style="{ cursor: 'pointer' }"
-                  >
+                    <div
+                      v-for="(col, colIndex) in shelf.cols"
+                      :key="`col-${colIndex}|${shelf.layer_now}`"
+                      class="grid-item"
+                      :style="{ cursor: 'pointer' }"
+                    >
                     <div
                       class="select-item"
+                      :class="{
+                        'highlight-available': isHighlighted(
+                          shelf.rows - rowIndex,
+                          colIndex + 1,
+                          shelf.layer_now
+                        ),
+                      }"
                       v-if="
                         shouldShowButton(
                           shelf.rows - rowIndex,
@@ -270,24 +277,53 @@
 
                   <div class="type-list">
                     <div v-for="t in typeList" :key="t.key" class="type-item">
-                      <div class="type-info">
-                        <div class="type-name">{{ t.label }}</div>
-                        <div class="type-total">
-                          共 {{ stats[t.totalKey] }} 个
-                        </div>
+                    <div class="type-info">
+                      <div class="type-name">{{ t.label }}</div>
+                      <div class="type-total">
+                        <span class="meta-item">
+                          {{ stats[t.groupKey] || 0 }} 组
+                        </span>
+                        <span class="meta-separator">/</span>
+                        <span class="meta-item">
+                          {{ stats[t.totalKey] || 0 }} 个
+                        </span>
                       </div>
+                    </div>
                       <div class="type-details">
                         <div class="detail-item">
                           <span class="detail-label">占用</span>
-                          <span class="detail-value used">{{
-                            stats[t.usedKey]
-                          }}</span>
+                          <div class="detail-value used">
+                            <span class="meta-item">
+                              {{ stats[t.usedGroupKey] || 0 }} 组
+                            </span>
+                            <span class="meta-separator">/</span>
+                            <span class="meta-item">
+                              {{ stats[t.usedKey] || 0 }} 个
+                            </span>
+                          </div>
                         </div>
                         <div class="detail-item">
                           <span class="detail-label">可用</span>
-                          <span class="detail-value available">{{
-                            stats[t.availKey]
-                          }}</span>
+                          <div
+                            class="detail-value available"
+                            :class="{
+                              'active-highlight': isHighlightActive(t, 'available'),
+                            }"
+                            role="button"
+                            tabindex="0"
+                            @click.stop="toggleHighlightAvailable(t)"
+                            @keydown.enter.stop="toggleHighlightAvailable(t)"
+                            @keydown.space.prevent.stop="toggleHighlightAvailable(t)"
+                            title="点击高亮当前楼层内可用库位"
+                          >
+                            <span class="meta-item">
+                              {{ stats[t.availGroupKey] || 0 }} 组
+                            </span>
+                            <span class="meta-separator">/</span>
+                            <span class="meta-item">
+                              {{ stats[t.availKey] || 0 }} 个
+                            </span>
+                          </div>
                         </div>
                       </div>
                     </div>
@@ -302,14 +338,7 @@
                     {{ stats.statistic_time_display || "-" }}
                   </div>
                   <div class="action-buttons q-mt-sm">
-                    <q-btn
-                      icon="refresh"
-                      label="刷新统计"
-                      @click="getStatistics()"
-                      flat
-                      dense
-                      class="full-width q-mb-sm"
-                    />
+    
                     <q-btn
                       icon="verified"
                       :label="$t('validate') || '数据校验'"
@@ -450,6 +479,34 @@
                 <div v-else class="no-errors">未发现位置级别错误</div>
               </div>
 
+              <div class="error-section q-mt-lg">
+                <div class="section-header">
+                  <q-icon name="inventory_2" class="q-mr-sm" />
+                  <span>托盘明细错误</span>
+                  <q-badge color="negative" class="q-ml-sm">
+                    {{ checkResult?.details?.detail_errors?.length || 0 }}
+                  </q-badge>
+                </div>
+
+                <q-table
+                  v-if="checkResult?.details?.detail_errors?.length"
+                  :data="checkResult.details.detail_errors"
+                  :columns="detailColumns"
+                  row-key="detail_id"
+                  flat
+                  bordered
+                  dense
+                  class="error-table q-mt-md"
+                >
+                  <template v-slot:body-cell-detected_at="props">
+                    <q-td :props="props">
+                      {{ formatDateTime(props.value) }}
+                    </q-td>
+                  </template>
+                </q-table>
+                <div v-else class="no-errors">未发现托盘明细错误</div>
+              </div>
+
               <div class="error-section q-mt-lg">
                 <div class="section-header">
                   <q-icon name="folder" class="q-mr-sm" />
@@ -487,6 +544,24 @@
 
           <q-card-actions align="right" class="dialog-actions">
             <q-btn label="关闭" flat @click="showCheckDialog = false" />
+            <q-btn
+              label="修复分组状态"
+              icon="sync_alt"
+              flat
+              color="primary"
+              :loading="fixingGroups"
+              :disable="!checkResult?.details?.group_errors?.length"
+              @click="fixGroupStatus"
+            />
+            <q-btn
+              label="修复托盘状态"
+              icon="build"
+              flat
+              color="primary"
+              :loading="fixingDetails"
+              :disable="!checkResult?.details?.detail_errors?.length"
+              @click="fixContainerDetailStatus"
+            />
             <q-btn
               label="导出报告"
               icon="download"
@@ -703,41 +778,67 @@ export default {
         {
           key: "t5",
           label: "T5",
+          typeCode: "T5",
           totalKey: "t5_total",
+          groupKey: "t5_group_count",
           usedKey: "t5_used",
+          usedGroupKey: "t5_used_group_count",
           availKey: "t5_available",
+          availGroupKey: "t5_available_group_count",
         },
         {
           key: "t4",
           label: "T4",
+          typeCode: "T4",
           totalKey: "t4_total",
+          groupKey: "t4_group_count",
           usedKey: "t4_used",
+          usedGroupKey: "t4_used_group_count",
           availKey: "t4_available",
+          availGroupKey: "t4_available_group_count",
         },
         {
           key: "s4",
           label: "S4",
+          typeCode: "S4",
           totalKey: "s4_total",
+          groupKey: "s4_group_count",
           usedKey: "s4_used",
+          usedGroupKey: "s4_used_group_count",
           availKey: "s4_available",
+          availGroupKey: "s4_available_group_count",
         },
         {
           key: "t2",
           label: "T2",
+          typeCode: "T2",
           totalKey: "t2_total",
+          groupKey: "t2_group_count",
           usedKey: "t2_used",
+          usedGroupKey: "t2_used_group_count",
           availKey: "t2_available",
+          availGroupKey: "t2_available_group_count",
         },
         {
           key: "t1",
           label: "T1",
+          typeCode: "T1",
           totalKey: "t1_total",
+          groupKey: "t1_group_count",
           usedKey: "t1_used",
+          usedGroupKey: "t1_used_group_count",
           availKey: "t1_available",
+          availGroupKey: "t1_available_group_count",
         },
       ],
 
       checking: false,
+      highlightState: {
+        active: false,
+        typeCode: null,
+        status: null,
+      },
+      highlightedBins: {},
       showCheckDialog: false,
       checkResult: null,
       checkColumns: [
@@ -795,8 +896,62 @@ export default {
         },
       ],
 
+      detailColumns: [
+        {
+          name: "container_code",
+          label: "托盘编码",
+          field: "container_code",
+          align: "left",
+        },
+        {
+          name: "batch_number",
+          label: "批次号",
+          field: "batch_number",
+          align: "left",
+        },
+        {
+          name: "goods_qty",
+          label: "初始数量",
+          field: "goods_qty",
+          align: "right",
+        },
+        {
+          name: "goods_out_qty",
+          label: "已出库数量",
+          field: "goods_out_qty",
+          align: "right",
+        },
+        {
+          name: "remaining_qty",
+          label: "剩余数量",
+          field: "remaining_qty",
+          align: "right",
+        },
+        {
+          name: "current_status",
+          label: "当前状态",
+          field: "current_status_display",
+          align: "left",
+        },
+        {
+          name: "expected_status",
+          label: "期望状态",
+          field: "expected_status_display",
+          align: "left",
+        },
+        {
+          name: "detected_at",
+          label: "检测时间",
+          field: "detected_at",
+          align: "left",
+        },
+      ],
+
       showGroupStatsDialog: false,
       showGroupStatsResult: false,
+
+      fixingDetails: false,
+      fixingGroups: false,
       loadingGroupStats: false,
       groupStatsParams: {
         min_utilization: 100,
@@ -933,6 +1088,90 @@ export default {
       });
     },
 
+    toggleHighlightAvailable(typeConfig) {
+      const typeCode = typeConfig.typeCode || typeConfig.label;
+      const isSameSelection =
+        this.highlightState.active &&
+        this.highlightState.typeCode === typeCode &&
+        this.highlightState.status === "available";
+
+      if (isSameSelection) {
+        this.clearHighlight();
+        return;
+      }
+
+      this.highlightState = {
+        active: true,
+        typeCode,
+        status: "available",
+      };
+      this.applyHighlight();
+
+      if (!Object.keys(this.highlightedBins).length) {
+        this.$q.notify({
+          message: `当前楼层暂无 ${typeCode} 可用库位`,
+          color: "warning",
+        });
+      } else {
+        this.$q.notify({
+          message: `已高亮 ${typeCode} 类型的可用库位`,
+          color: "positive",
+          position: "top",
+        });
+      }
+    },
+
+    applyHighlight() {
+      if (!this.highlightState.active) {
+        this.highlightedBins = {};
+        return;
+      }
+
+      const highlightMap = {};
+      Object.entries(this.goodsMap || {}).forEach(([key, bin]) => {
+        if (!bin) {
+          return;
+        }
+        if (bin.location_type !== this.highlightState.typeCode) {
+          return;
+        }
+
+        if (this.highlightState.status === "available") {
+          if (bin.status === "available") {
+            highlightMap[key] = true;
+          }
+          return;
+        }
+      });
+
+      this.highlightedBins = highlightMap;
+    },
+
+    clearHighlight() {
+      if (!this.highlightState.active) {
+        return;
+      }
+      this.highlightState = {
+        active: false,
+        typeCode: null,
+        status: null,
+      };
+      this.highlightedBins = {};
+    },
+
+    isHighlighted(row, col, layer) {
+      return Boolean(this.highlightedBins[`${row}-${col}-${layer}`]);
+    },
+
+    isHighlightActive(typeConfig, status) {
+      const typeCode = typeConfig.typeCode || typeConfig.label;
+      return (
+        this.highlightState.active &&
+        this.highlightState.typeCode === typeCode &&
+        this.highlightState.status === status
+      );
+    },
+
     exportGroupStats() {
       if (!this.groupStatsResult?.data) return;
 
@@ -1023,9 +1262,13 @@ export default {
     runCheck() {
       const layer = this.shelf.layer_now || 1;
       const url = `/location_statistics/CheckView/?layer=${layer}`;
+      const payload = {};
+      if (this.warehouse_code) {
+        payload.warehouse_code = this.warehouse_code;
+      }
 
       this.checking = true;
-      postauth(url, {})
+      postauth(url, payload)
         .then((res) => {
           if (res && res.data) {
             if (res.data.success === false) {
@@ -1050,6 +1293,146 @@ export default {
         });
     },
 
+    fixContainerDetailStatus() {
+      if (this.fixingDetails) {
+        return;
+      }
+      const hasErrors =
+        this.checkResult &&
+        this.checkResult.details &&
+        this.checkResult.details.detail_errors &&
+        this.checkResult.details.detail_errors.length;
+      if (!hasErrors) {
+        this.$q.notify({
+          type: "info",
+          message: "当前没有需要修复的托盘明细",
+        });
+        return;
+      }
+
+      const layer = this.shelf.layer_now || 1;
+      const url = `/location_statistics/CheckView/?layer=${layer}`;
+      const payload = {
+        auto_fix: true,
+        fix_scope: ["details"],
+      };
+      if (this.warehouse_code) {
+        payload.warehouse_code = this.warehouse_code;
+      }
+
+      this.fixingDetails = true;
+      postauth(url, payload)
+        .then((res) => {
+          if (res && res.data) {
+            if (res.data.success === false) {
+              this.$q.notify({
+                type: "negative",
+                message: "修复失败: 服务返回错误",
+              });
+              return;
+            }
+            const data = res.data.data || res.data;
+            if (data) {
+              this.checkResult = data;
+              this.$q.notify({
+                type: "positive",
+                message: "托盘明细状态修复完成",
+              });
+            } else {
+              this.$q.notify({
+                type: "warning",
+                message: "修复完成,但未获取新的检测结果",
+              });
+            }
+          } else {
+            this.$q.notify({
+              type: "warning",
+              message: "修复完成,但无返回数据",
+            });
+          }
+        })
+        .catch((err) => {
+          this.$q.notify({
+            type: "negative",
+            message: "修复失败: " + (err && err.message),
+          });
+        })
+        .finally(() => {
+          this.fixingDetails = false;
+        });
+    },
+
+    fixGroupStatus() {
+      if (this.fixingGroups) {
+        return;
+      }
+
+      const hasGroupErrors =
+        this.checkResult &&
+        this.checkResult.details &&
+        this.checkResult.details.group_errors &&
+        this.checkResult.details.group_errors.length;
+
+      if (!hasGroupErrors) {
+        this.$q.notify({
+          type: "info",
+          message: "当前没有需要修复的分组状态",
+        });
+        return;
+      }
+
+      const layer = this.shelf.layer_now || 1;
+      const url = `/location_statistics/CheckView/?layer=${layer}`;
+      const payload = {
+        auto_fix: true,
+        fix_scope: ["groups"],
+      };
+      if (this.warehouse_code) {
+        payload.warehouse_code = this.warehouse_code;
+      }
+
+      this.fixingGroups = true;
+      postauth(url, payload)
+        .then((res) => {
+          if (res && res.data) {
+            if (res.data.success === false) {
+              this.$q.notify({
+                type: "negative",
+                message: "修复失败: 服务返回错误",
+              });
+              return;
+            }
+            const data = res.data.data || res.data;
+            if (data) {
+              this.checkResult = data;
+              this.$q.notify({
+                type: "positive",
+                message: "分组状态修复完成",
+              });
+            } else {
+              this.$q.notify({
+                type: "warning",
+                message: "修复完成,但未获取新的检测结果",
+              });
+            }
+          } else {
+            this.$q.notify({
+              type: "warning",
+              message: "修复完成,但无返回数据",
+            });
+          }
+        })
+        .catch((err) => {
+          this.$q.notify({
+            type: "negative",
+            message: "修复失败: " + (err && err.message),
+          });
+        })
+        .finally(() => {
+          this.fixingGroups = false;
+        });
+    },
+
     shouldShowButton(row, col, layer) {
       const bin = this.goodsMap[`${row}-${col}-${layer}`];
       return ["T1", "T2", "T4", "T5", "S4", "M1", "E1", "C1", "B1"].includes(
@@ -1075,6 +1458,8 @@ export default {
           };
         });
 
+        _this.applyHighlight();
+
         _this.$q.notify({
           message: "刷新成功",
           icon: "done",
@@ -1602,10 +1987,22 @@ export default {
 }
 
 .type-total {
+  display: flex;
+  align-items: center;
+  gap: 4px;
   font-size: 11px;
   color: #999;
 }
 
+.type-total .meta-item {
+  display: inline-flex;
+  align-items: center;
+}
+
+.meta-separator {
+  color: #ccc;
+}
+
 .type-details {
   display: flex;
   gap: 12px;
@@ -1622,11 +2019,19 @@ export default {
 }
 
 .detail-value {
-  display: block;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
   font-size: 12px;
   font-weight: 500;
 }
 
+.detail-value .meta-item {
+  display: inline-flex;
+  align-items: center;
+}
+
 .detail-value.used {
   color: #f57c00;
 }
@@ -1635,6 +2040,22 @@ export default {
   color: #4caf50;
 }
 
+.detail-value.available {
+  cursor: pointer;
+  user-select: none;
+}
+
+.detail-value.available.active-highlight {
+  color: #2e7d32;
+  font-weight: 600;
+}
+
+.select-item.highlight-available {
+  box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.45);
+  transform: translateY(-2px) scale(1.05);
+  border-color: #2e7d32 !important;
+}
+
 .action-buttons {
   display: flex;
   flex-direction: column;

File diff suppressed because it is too large
+ 1280 - 124
templates/src/pages/stock/stockbinlist.vue