Browse Source

托盘详情修复

flower_bs 3 weeks ago
parent
commit
72a3ba7760

+ 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, '成品'),

+ 63 - 16
container/views.py

@@ -2320,24 +2320,71 @@ 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个任务,避免无限循环
+        
+        while processed_count < max_tasks and skip_count < max_skip:
+            # 重新获取待处理任务(因为可能有任务被跳过)
+            pending_tasks = get_pending_tasks()
+            if not pending_tasks.exists():
+                break
+            
+            next_task = pending_tasks.first()
+            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()
+                OutboundService.perform_initial_allocation(allocator, next_task.current_location)
+                OutboundService.send_task_to_wcs(next_task)
+                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']
             }
         }

+ 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:

+ 447 - 19
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>
@@ -450,6 +486,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 +551,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 +785,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 +903,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 +1095,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 +1269,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 +1300,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 +1465,8 @@ export default {
           };
         });
 
+        _this.applyHighlight();
+
         _this.$q.notify({
           message: "刷新成功",
           icon: "done",
@@ -1602,10 +1994,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 +2026,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 +2047,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;