Bläddra i källkod

添加移库与释放库位

flower_mr 7 timmar sedan
förälder
incheckning
de090b28e7
50 ändrade filer med 4406 tillägg och 965 borttagningar
  1. 18 0
      bin/migrations/0007_alter_locationgroupmodel_current_batch.py
  2. 1 1
      bin/models.py
  3. 3 3
      bin/queries.py
  4. 11 28
      bin/services.py
  5. 22 0
      bin/updates.py
  6. 2 1
      container/urls.py
  7. 230 14
      container/views.py
  8. 18 0
      erp/migrations/0022_alter_inboundbilloperatelog_log_type.py
  9. 1 0
      erp/models.py
  10. 1 0
      erp/urls.py
  11. 55 34
      erp/views.py
  12. 110 575
      logs/boundBill.log
  13. 314 0
      logs/error.log
  14. 2500 0
      logs/server.log
  15. 0 1
      templates/dist/spa/css/10.74deb849.css
  16. 1 0
      templates/dist/spa/css/10.fcf11716.css
  17. 1 1
      templates/dist/spa/css/3.4200644e.css
  18. 1 0
      templates/dist/spa/css/8.01a63b42.css
  19. 0 1
      templates/dist/spa/css/8.58670660.css
  20. 0 1
      templates/dist/spa/css/chunk-common.ce13bc87.css
  21. 1 0
      templates/dist/spa/css/chunk-common.e1490f3e.css
  22. 1 1
      templates/dist/spa/index.html
  23. 0 1
      templates/dist/spa/js/10.68b30aac.js
  24. BIN
      templates/dist/spa/js/10.68b30aac.js.gz
  25. 1 0
      templates/dist/spa/js/10.6f3e618a.js
  26. BIN
      templates/dist/spa/js/10.6f3e618a.js.gz
  27. 0 1
      templates/dist/spa/js/3.9f1140ab.js
  28. BIN
      templates/dist/spa/js/3.9f1140ab.js.gz
  29. 1 0
      templates/dist/spa/js/3.d126d207.js
  30. BIN
      templates/dist/spa/js/3.d126d207.js.gz
  31. 0 1
      templates/dist/spa/js/8.583d8ab3.js
  32. BIN
      templates/dist/spa/js/8.583d8ab3.js.gz
  33. 1 0
      templates/dist/spa/js/8.5e566b7e.js
  34. BIN
      templates/dist/spa/js/8.5e566b7e.js.gz
  35. 0 1
      templates/dist/spa/js/app.76fa7928.js
  36. BIN
      templates/dist/spa/js/app.76fa7928.js.gz
  37. 1 0
      templates/dist/spa/js/app.f04fecdb.js
  38. BIN
      templates/dist/spa/js/app.f04fecdb.js.gz
  39. 1 0
      templates/dist/spa/js/chunk-common.655d825e.js
  40. BIN
      templates/dist/spa/js/chunk-common.655d825e.js.gz
  41. 0 1
      templates/dist/spa/js/chunk-common.fe167124.js
  42. BIN
      templates/dist/spa/js/chunk-common.fe167124.js.gz
  43. 289 12
      templates/dist/spa/js/vendor.889e7783.js
  44. BIN
      templates/dist/spa/js/vendor.5f12bb2e.js.gz
  45. BIN
      templates/dist/spa/js/vendor.889e7783.js.gz
  46. 1 1
      templates/src/components/containercard.vue
  47. 383 137
      templates/src/components/goodscard.vue
  48. 107 61
      templates/src/pages/erp/erpasn.vue
  49. 329 87
      templates/src/pages/erp/erpdn.vue
  50. 1 1
      utils/throttle.py

+ 18 - 0
bin/migrations/0007_alter_locationgroupmodel_current_batch.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2025-06-03 13:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('bin', '0006_alter_locationcontainerlink_container_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='locationgroupmodel',
+            name='current_batch',
+            field=models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='当前批次'),
+        ),
+    ]

+ 1 - 1
bin/models.py

@@ -543,7 +543,7 @@ class LocationGroupModel(models.Model):
     max_capacity = models.PositiveIntegerField(verbose_name='最大容量')  # 根据类型自动设置
     current_quantity = models.PositiveIntegerField(default=0, verbose_name='当前托盘数')
     current_goods_quantity = models.PositiveIntegerField(default=0, verbose_name='当前货物数')
-    current_batch = models.CharField(max_length=50, default='', verbose_name='当前批次')
+    current_batch = models.CharField(max_length=50, default='', verbose_name='当前批次',null=True,blank=True)
     current_goods_code = models.CharField(max_length=50, default='', verbose_name='当前货物编码')
     access_priority = models.IntegerField(
         default=0, 

+ 3 - 3
bin/queries.py

@@ -57,14 +57,14 @@ class LocationQueries:
         if not detail:
             return {
                 'status': None,
-                'number': None,
+                'number': 'ContainerGroup'+str(container_code)+str(timezone.now().strftime('%Y%m%d')),
                 'container': container,
                 'class': 2 ,
             }
         elif detail.batch is None:
             return {
                 'status': None,
-                'number': None,
+                'number': 'ContainerGroup'+str(container_code)+str(timezone.now().strftime('%Y%m%d')),
                 'container': container,
                 'class': 2,
             }
@@ -80,7 +80,7 @@ class LocationQueries:
                     item.save()
                 return {
                     'status': None,
-                    'number': None,
+                    'number': 'ContainerScattered'+str(container_code)+str(timezone.now().strftime('%Y%m%d')),
                     'container': container,
                     'class': 3,
                 }

+ 11 - 28
bin/services.py

@@ -16,12 +16,12 @@ class AllocationService:
     @classmethod
     @transaction.atomic
     def allocate(cls, container_code, start_point):
-        logger.info(f"请求托盘:{container_code},请求位置:{start_point}")
+        # logger.info(f"请求托盘:{container_code},请求位置:{start_point}")
 
         batch_info = LocationQueries.get_batch_info(container_code)
         if not batch_info :
             raise ValueError("无效的托盘或批次信息")
-        if not batch_info['number']:
+        if not batch_info['status']:
             if batch_info['class'] == 2:
                 return cls._container_group_allocation(container_code,batch_info, start_point)
             else:
@@ -58,29 +58,11 @@ class AllocationService:
             return cls._execute_allocation(locations, start_point,container_code,batch_info)
         else:
             return cls._subsequent_allocation(container_code, batch_info, start_point)
-            # locations = LocationQueries.get_left_location_group(batch_info['number'])
-            # if not locations:
-            #     layer_cap = LocationQueries.get_group_capacity()
-            #     pressure = LocationQueries.get_current_pressure()
-            #     finish_task_sum = sum(LocationQueries.get_current_finish_task(container_code,batch_info))
-
-            #     solution, new_pressure = AllocationAlgorithm.generate_plan(total-finish_task_sum, layer_cap, pressure)
-                
-            #     if not solution:
-            #         raise RuntimeError("无法生成有效分配方案")
-
-            #     formatted = json.loads(AllocationAlgorithm.format_solution(solution))
-            #     LocationUpdates.save_allocation_plan(batch_info['number'], formatted, new_pressure)
-            #     # 使用距离最近的库位,增加切换
-            #     locations = AllocationAlgorithm.allocation_plan_left_right(formatted,batch_info['number'] ,start_point, container_code)
-            #     return cls._execute_allocation(locations, start_point,container_code,batch_info)
-            # else:
-            #     return cls._execute_allocation(locations, start_point,container_code,batch_info)   
 
     @classmethod
     def _container_group_allocation(cls,container_code, batch_info, start_point):
         total = 1
-        batch_info['number'] = 'ContainerGroup'+str(container_code)+str(timezone.now().strftime('%Y%m%d'))
+        # batch_info['number'] = 'ContainerGroup'+str(container_code)+str(timezone.now().strftime('%Y%m%d'))
         if not total:
             raise ValueError("无效的托盘或批次信息")
        
@@ -100,7 +82,7 @@ class AllocationService:
     @classmethod
     def _container_scattered_allocation(cls,container_code, batch_info, start_point):
         total = 1
-        batch_info['number'] = 'ContainerScattered'+str(container_code)+str(timezone.now().strftime('%Y%m%d'))
+        # batch_info['number'] = 'ContainerScattered'+str(container_code)+str(timezone.now().strftime('%Y%m%d'))
         layer_cap = LocationQueries.get_group_capacity()
         pressure = LocationQueries.get_current_pressure()
 
@@ -211,7 +193,7 @@ class AllocationService:
         start_location.status = 'available'
         start_location.save()
     
-        location_min_value.status = 'occupied'
+        location_min_value.status = 'reserved'
         location_min_value.save()
         print(f"[2] 库位状态更新成功!")
 
@@ -231,8 +213,9 @@ class AllocationService:
             raise ValueError("[5] 批次信息更新失败")
         print(f"[5] 批次信息更新成功!")
 
+        update_start_location_container_link = LocationUpdates.disable_link_container(start_location.location_code, container_code)
         update_location_container_link = LocationUpdates.link_container(location_min_value.location_code, container_code)
-        if not update_location_container_link:
+        if not update_location_container_link or not update_start_location_container_link:
             raise ValueError("[6] 托盘与库位关联失败")
         print(f"[6] 托盘与库位关联成功!")
  
@@ -254,11 +237,11 @@ class AllocationService:
         if not locations:
             layer_cap = LocationQueries.get_group_capacity()
             pressure = LocationQueries.get_current_pressure()
-            finish_task_sum = sum(LocationQueries.get_current_finish_task(container_code,batch_info))
+            finish_task_sum = sum(LocationQueries.get_current_finish_task(container_code,batch_info)) 
             print(f"当前已完成任务: {finish_task_sum}, 总任务数: {total},散盘任务数: {scatter_total}")
-            if total-finish_task_sum-scatter_total == 0:
-                total += 1
-            solution, new_pressure = AllocationAlgorithm.generate_plan(total-finish_task_sum-scatter_total, layer_cap, pressure)
+            if total-scatter_total <= 0:
+                total = 1  + scatter_total
+            solution, new_pressure = AllocationAlgorithm.generate_plan(total-scatter_total, layer_cap, pressure)
             if not solution:
                 raise RuntimeError("无法生成有效分配方案")
 

+ 22 - 0
bin/updates.py

@@ -31,6 +31,27 @@ class LocationUpdates:
             return True
         except Exception as e:
             raise RuntimeError(f"关联更新失败: {str(e)}")
+        
+    @transaction.atomic
+    def disable_link_container(location_code, container_code):
+        try:
+            location = LocationModel.objects.select_for_update().get(
+                location_code=location_code
+            )
+            container = ContainerListModel.objects.get(
+                container_code=container_code
+            )
+            link = LocationContainerLink.objects.filter(
+                location=location,
+                container=container
+            ).first()
+            if link:
+                link.is_active = False
+                link.save()
+            return True
+        except Exception as e:       
+            logger.error(f"关联更新失败:{str(e)}")
+            return False
 
     @transaction.atomic
     def update_batch_status(container_code, status):
@@ -163,6 +184,7 @@ class LocationUpdates:
             # 3. 更新库位组状态
             if current == 0:
                 location_group.status = 'available'
+                location_group.current_batch = None
             elif current == location_group.max_capacity:
                 location_group.status = 'full'
             else:

+ 2 - 1
container/urls.py

@@ -42,7 +42,8 @@ re_path(r'^task/(?P<pk>\d+)/$', views.TaskViewSet.as_view({
     'patch': 'partial_update',
 }), name="Task_1"),
 
-path(r'container_wcs/', views.ContainerWCSViewSet.as_view({"get": "get_container_wcs","put": "update_container_wcs"}), name='ContainerWCS'),
+path(r'location_release/',views.ContainerWCSViewSet.as_view({"post": "release_location"}), name='ContainerWCS'),
+path(r'container_wcs/', views.ContainerWCSViewSet.as_view({"get": "get_container_wcs","put": "update_container_wcs","post": "generate_move_task"}), name='ContainerWCS'),
 re_path(r'container_wcs/update/', views.ContainerWCSViewSet.as_view({"get": "update_container_wcs"}), name='ContainerWCS1'),
 
 path(r'out_task/', views.OutTaskViewSet.as_view(), name='OutTask'),

+ 230 - 14
container/views.py

@@ -33,6 +33,8 @@ import threading
 from django.db import close_old_connections
 from bin.services import AllocationService
 from collections import defaultdict
+from django.db.models import Sum
+from staff.models import ListModel as StaffListModel
 logger = logging.getLogger(__name__)
 # 托盘列表视图
 class ContainerListViewSet(viewsets.ModelViewSet):
@@ -312,6 +314,90 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
     """
     authentication_classes = []  # 禁用所有认证类
     permission_classes = [AllowAny]  # 允许任意访问
+
+    def generate_move_task(self, request, *args, **kwargs):
+        data = self.request.data
+        container = data.get('container_code')
+        start_location = data.get('start_location')
+        target_location = data.get('target_location')
+        logger.info(f"请求托盘:{container},起始位置:{start_location},目标位置:{target_location}")
+        data_return = {}
+
+        try:
+            container_obj = ContainerListModel.objects.filter(container_code=container).first()
+            if not container_obj:
+                data_return = {
+                    'code': '400',
+                    'message': '托盘编码不存在',
+                    'data': data
+                }
+                return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
+
+    
+            # 检查是否已在目标位置
+            if target_location == str(container_obj.target_location) and target_location!= '203' and target_location!= '103':
+                logger.info(f"托盘 {container} 已在目标位置")
+                data_return = {
+                    'code': '200',
+                    'message': '当前位置已是目标位置',
+                    'data': data
+                }
+            else:
+                # 生成任务
+                current_task = ContainerWCSModel.objects.filter(
+                    container=container, 
+                    tasktype='inbound',
+                    working = 1,
+                 
+                ).exclude(status=300).first()
+
+                if current_task:
+                    data_return = {
+                        'code': '200',
+                        'message': '任务已存在,重新下发',
+                        'data': current_task.to_dict()
+                    }
+                else:
+                    # todo: 这里的入库操作记录里面的记录的数量不对
+                    location_min_value,allocation_target_location, batch_info = AllocationService._move_allocation(start_location, target_location,container)
+                    batch_id = batch_info['number']
+                    if batch_info['class'] == 2:
+                        self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,location_min_value.c_number) 
+                        self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
+                    elif batch_info['class'] == 3:
+                        self.generate_task_no_batch(container, target_location, allocation_target_location,batch_id,location_min_value.c_number) 
+                        self.generate_move_container_operate(container_obj, allocation_target_location)
+                    else:   
+                        self.generate_task(container, start_location, allocation_target_location,batch_id,location_min_value.c_number)  # 生成任务
+                        self.generate_move_container_operate(container_obj, allocation_target_location)
+
+                    current_task = ContainerWCSModel.objects.get(
+                        container=container, 
+                        tasktype='inbound',
+                        working=1,
+                    )
+                    data_return = {
+                        'code': '200',
+                        'message': '任务下发成功',
+                        'data': current_task.to_dict()
+                    }
+                    container_obj.target_location = allocation_target_location
+                    container_obj.save()
+                    if batch_info['class'] == 1 or batch_info['class'] == 3:
+                        self.inport_update_task(current_task.id, container_obj.id)
+
+
+            http_status = status.HTTP_200_OK if data_return['code'] == '200' else status.HTTP_400_BAD_REQUEST
+            return Response(data_return, status=http_status)
+
+        except Exception as e:
+            logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
+            return Response(
+                {'code': '500', 'message': '服务器内部错误', 'data': None},
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR
+            )
+
+
  
     def get_container_wcs(self, request, *args, **kwargs):
         data = self.request.data
@@ -371,10 +457,10 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                         self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
                     elif batch_info['class'] == 3:
                         self.generate_task_no_batch(container, current_location, allocation_target_location,batch_id,location_min_value.c_number) 
-                        self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
+                        self.generate_container_operate(container_obj, allocation_target_location)
                     else:   
                         self.generate_task(container, current_location, allocation_target_location,batch_id,location_min_value.c_number)  # 生成任务
-                        self.generate_container_operate(container_obj, batch_id, allocation_target_location)
+                        self.generate_container_operate(container_obj, allocation_target_location)
 
                     current_task = ContainerWCSModel.objects.get(
                         container=container, 
@@ -403,23 +489,117 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             )
 
     @transaction.atomic
-    def generate_container_operate(self, container_obj, bound_number,allocation_target_location):
-        batch_obj = BoundBatchModel.objects.filter(bound_number=bound_number).first()
+    # def generate_container_operate(self, container_obj, bound_number,allocation_target_location):
+    def generate_container_operate(self, container_obj, allocation_target_location):
+ 
+        # 获取容器中所有有效的批次明细(排除已删除和状态3的)
+        container_detaillist = ContainerDetailModel.objects.filter(
+            container=container_obj,
+            is_delete=False
+        ).exclude(status=3)
+        
+         # 优化查询 - 预取批次对象
+        container_detaillist = container_detaillist.select_related('batch')
+        
+        # 创建批次数量字典
+        batch_totals = {}
+        for detail in container_detaillist:
+            batch_id = detail.batch_id
+            if batch_id not in batch_totals:
+                batch_totals[batch_id] = {
+                    'obj': detail.batch,  # 批次对象
+                    'total_qty': 0
+                }
+            batch_totals[batch_id]['total_qty'] += detail.goods_qty
+        # 当前月份(单次计算多次使用)
+        current_month = int(timezone.now().strftime("%Y%m"))
+        current_time = timezone.now()
+        current_location = container_obj.current_location
+        
+        # 为每个批次创建操作记录
+        for batch_id, data in batch_totals.items():
+            batch_obj = data['obj']
+            goods_qty = data['total_qty']
+            ContainerOperationModel.objects.create(
+                month=current_month,
+                container=container_obj,
+                goods_code=batch_obj.goods_code,
+                goods_desc=batch_obj.goods_desc,
+                operation_type="inbound",
+                batch_id=batch_obj.id,
+                goods_qty=goods_qty,
+                goods_weight=goods_qty,
+                from_location=current_location,
+                to_location=allocation_target_location,
+                timestamp=current_time,
+                operator="WMS",
+                memo=f"WCS入库: 批次: {batch_obj.bound_number}, 数量: {goods_qty}"  # 使用实际容器中的数量
+            )
+        
+    @transaction.atomic
+    def generate_move_container_operate(self, container_obj, allocation_target_location):
+     
+        # 获取容器中所有有效的批次明细
+        container_detaillist = ContainerDetailModel.objects.filter(
+            container=container_obj,
+            is_delete=False
+        ).exclude(status=3)
+        
+        # 优化查询 - 预取批次对象
+        container_detaillist = container_detaillist.select_related('batch')
+        
+        # 创建批次数量字典
+        batch_totals = {}
+        for detail in container_detaillist:
+            batch_id = detail.batch_id
+            if batch_id not in batch_totals:
+                batch_totals[batch_id] = {
+                    'obj': detail.batch,  # 批次对象
+                    'total_qty': 0
+                }
+            batch_totals[batch_id]['total_qty'] += detail.goods_qty
+        
+        # 获取当前时间和位置信息
+        current_month = int(timezone.now().strftime("%Y%m"))
+        current_time = timezone.now()
+        current_location = container_obj.current_location
+        
+        # 为每个批次创建操作记录
+        for batch_id, data in batch_totals.items():
+            batch_obj = data['obj']
+            goods_qty = data['total_qty']
+            
+            ContainerOperationModel.objects.create(
+                month=current_month,
+                container=container_obj,
+                goods_code=batch_obj.goods_code,
+                goods_desc=batch_obj.goods_desc,
+                operation_type="adjust",  # 操作类型改为移动
+                batch_id=batch_obj.id,
+                goods_qty=goods_qty,
+                goods_weight=goods_qty,
+                from_location=current_location,
+                to_location=allocation_target_location,
+                timestamp=current_time,
+                operator="WMS",
+                memo=f"托盘移动: 批次: {batch_obj.bound_number}, 数量: {goods_qty}"
+            )
+
+    @transaction.atomic
+    def generate_move_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location):
         ContainerOperationModel.objects.create(
             month = int(timezone.now().strftime("%Y%m")),
             container = container_obj,
-            goods_code = batch_obj.goods_code,
-            goods_desc = batch_obj.goods_desc,
-            operation_type ="inbound",
-            batch_id = batch_obj.id,
-            
-            goods_qty = batch_obj.goods_qty,
-            goods_weight = batch_obj.goods_qty,
+            goods_code = 'container',
+            goods_desc = '托盘组',
+            operation_type ="adjust",
+            goods_qty = 1,
+            goods_weight = 0,
             from_location = container_obj.current_location,
             to_location= allocation_target_location,
             timestamp=timezone.now(),
-            operator="WMS",
-            memo=f"WCS入库: 批次: {bound_number}, 数量: {batch_obj.goods_qty}"
+            memo=f"托盘组移库:从{container_obj.current_location}移库到{allocation_target_location}"
+
         )
     @transaction.atomic
     def generate_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location):
@@ -483,7 +663,6 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
 
     def generate_task_no_batch(self, container, current_location, target_location,batch_id,location_c_number):
     
- 
         data_tosave = {
             'container': container,
             'batch': None,
@@ -766,6 +945,43 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR
             )
          
+    def release_location(self,request):
+        """释放库位"""
+        """添加权限管理"""
+        token = self.request.META.get('HTTP_TOKEN')
+        if not token:
+            return Response({'code': '401', 'message': '请登录', 'data': None},
+                        status=status.HTTP_200_OK)
+        user = StaffListModel.objects.filter(openid=token).first()
+        if not user:
+            return Response({'code': '401', 'message': '请登录', 'data': None},
+                        status=status.HTTP_200_OK)
+        if user.staff_type not in ['Supervisor', 'Manager','Admin']:
+            return Response({'code': '401', 'message': '无权限', 'data': None},
+                            status=status.HTTP_200_OK)
+        try:
+            location_release = request.data.get('location_release')
+            location_row = location_release.split('-')[1]
+            location_col = location_release.split('-')[2]
+            location_layer = location_release.split('-')[3]
+            location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
+            location_code = location.location_code
+            allocator = LocationAllocation()
+            with transaction.atomic():
+                if not allocator.release_location(location_code):
+                    raise Exception("解除库位关联失败")
+                
+                if not allocator.update_location_status(location_code, 'available'):
+                    raise Exception("库位状态更新失败")
+                
+                if not allocator.update_location_group_status(location_code):
+                    raise Exception("库位组状态更新失败")
+        except Exception as e:
+            logger.error(f"解除库位关联失败: {str(e)}")
+            return Response({'code': '500', 'message': e, 'data': None},
+                        status=status.HTTP_200_OK)
+        return Response({'code': '200', 'message': '解除库位关联成功', 'data': None},
+                        status=status.HTTP_200_OK)
 
     def handle_outbound_completion(self, container_obj, task):
         """处理出库完成后的库位释放和状态更新"""

+ 18 - 0
erp/migrations/0022_alter_inboundbilloperatelog_log_type.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2025-06-05 22:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('erp', '0021_alter_inboundbill_bound_status_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='inboundbilloperatelog',
+            name='log_type',
+            field=models.CharField(choices=[('create', '创建'), ('update', '更新'), ('erp_audit', 'erp审核'), ('erp_save', 'erp保存'), ('wms_audit', 'wms审核'), ('update_batch', 'erp质检'), ('delete', '删除'), ('cancel', '取消'), ('submit', '提交'), ('confirm', '确认'), ('reject', '驳回'), ('confirm_submit', '确认提交'), ('confirm_audit', '确认审核'), ('confirm_cancel', '确认取消')], max_length=20, verbose_name='操作类型'),
+        ),
+    ]

+ 1 - 0
erp/models.py

@@ -42,6 +42,7 @@ class InboundBillOperateLog(models.Model):
         ('update', '更新'),
 
         ('erp_audit', 'erp审核'),
+        ('erp_save', 'erp保存'),
         ('wms_audit', 'wms审核'),
 
         ('update_batch', 'erp质检'),

+ 1 - 0
erp/urls.py

@@ -13,6 +13,7 @@ urlpatterns = [
     path('inboundBills/log/', views.InboundBillsLog.as_view({"get": "list"}),name="inboundBillslog"),
     path('outboundBills/log/', views.OutboundBillsLog.as_view({"get": "list"}),name="outboundBillslog"),
     path('inboundBills/number/', views.InboundBills.as_view({"post": "get_bound_number"}),name="inboundBillsnumber"),
+    path('outboundBills/number/', views.OutboundBills.as_view({"post": "get_bound_number"}),name="outboundBillsnumber"),
 
     # path('inboundBills/audit/', views.bound_apply.as_view(), name="inboundBills-audit"),
     # path('inboundBills/save/', views.bound_apply.as_view(), name="inboundBills-save"),

+ 55 - 34
erp/views.py

@@ -1050,7 +1050,19 @@ class BatchUpdate(APIView):
                     
                     if material_status == 'passing':
                         try:
+                            bill_obj = InboundBill.objects.get(billId=material_billId)
+                            if not bill_obj:
+                                logger.info("入库单不存在")
+                                fail_materials.append({
+                                    **material,
+                                    "error": "入库单不存在"
+                                })
+                                fail_count += 1
+                                continue
+                            
+                            bill_obj.erp_audit_id = material_audit_id
                             instance = MaterialDetail.objects.get(bound_billId_id=material_billId,entryIds=material_entryId)
+                            
                             if not instance:
                                 logger.info("物料明细不存在")
                                 fail_materials.append({
@@ -1067,16 +1079,7 @@ class BatchUpdate(APIView):
                                 )
                             instance.status = 1
                          
-                            bill_obj = InboundBill.objects.get(billId=material_billId)
-                            if not bill_obj:
-                                logger.info("入库单不存在")
-                                fail_materials.append({
-                                    **material,
-                                    "error": "入库单不存在"
-                                })
-                                fail_count += 1
-                                continue
-                            bill_obj.erp_audit_id = material_audit_id
+
                             logger.info("[1]入库单号%s,物料明细%s,更新状态,审核通过%s",bill_obj.number,material_entryId,material_audit_id)   
                             logger.info("[2]入库单号%s,物料明细%s,更新状态,审核通过%s",bill_obj.number,material_entryId,bill_obj.erp_audit_id) 
                             
@@ -1198,7 +1201,7 @@ class AccessToken(APIView):
 class ERPSyncBase:
     """ERP同步基类(作为发送方)"""
     max_retries = 30  # 最大重试次数
-    retry_delay = 3
+    retry_delay = 30
       # 重试间隔秒数
     
     def __init__(self, wms_bill,entryIds):
@@ -1236,7 +1239,7 @@ class ERPSyncBase:
                     self.get_erp_endpoint(),
                     json=self.build_erp_payload(),
                     headers=headers,
-                    timeout=10
+                    timeout=60
                 )
                 response.raise_for_status()
 
@@ -1343,7 +1346,7 @@ class OtherInboundAuditSync(ERPSyncBase):
     # 处理响应
 
     def process_erp_response(self, response):
-        logger.info("ERP审核响应:",response)
+        logger.info("ERP审核响应:",response['message'])
         return response['status']
 """调拨入库审核"""
 class TransferInboundAuditSync(ERPSyncBase):
@@ -1431,7 +1434,7 @@ class ProductionOutboundAuditSync(ERPSyncBase):
     # 处理响应
 
     def process_erp_response(self, response):
-        logger.info("ERP审核响应:",response)
+        logger.info("ERP审核响应:",response['message'],response['status'])
         return response['status']
 
 """采购入库保存"""
@@ -1454,7 +1457,7 @@ class PurchaseInboundSaveSync(ERPSyncBase):
         # }
         return {
             "purinbill": {
-                "billId":self.wms_bill.erp_audit_id,
+                "billId":self.wms_bill.erp_save_id,
                 "entryIds": 
                     self.entryIds,
                 
@@ -1462,7 +1465,9 @@ class PurchaseInboundSaveSync(ERPSyncBase):
         }
                  
     def process_erp_response(self, response):
-        logger.info("ERP审核响应:",response)
+        logger.info("ERP审核响应:",response['message'])
+        # if response['status']:
+            
         return response['status']
 """销售出库保存"""
 class SaleOutboundSaveSync(ERPSyncBase):
@@ -1474,17 +1479,17 @@ class SaleOutboundSaveSync(ERPSyncBase):
         
     def build_erp_payload(self):
 
-        #         {
-        # 	"purinbill":{
-        # 		"billId":"1745732021174",
+        #       {
+        # 	"saloutbill":{
+        # 		"billId":"1745732823064",
         # 		"entryIds":[
-        # 			"1745732021087"
+        # 			"1745732823073"
         # 		]
         # 	}
         # }
         return {
-            "purinbill": {
-                "billId":self.wms_bill.erp_audit_id,
+            "saloutbill": {
+                "billId":self.wms_bill.erp_save_id,
                 "entryIds": 
                     self.entryIds,
                 
@@ -1611,27 +1616,31 @@ class bound_apply(APIView):
   
                 if bill_type == 2:
                     # 采购入库保存
-                    bill_obj = InboundBill.objects.filter(id=bill_id).first()
+                    bill_obj = InboundBill.objects.filter(billId=bill_id).first()
+                    entryIds = list(MaterialDetail.objects.filter(bound_billId=bill_id).values_list('entryIds', flat=True))
                     if bill_obj:
-                        sync_obj = PurchaseInboundSaveSync(bill_obj,[])
-                        bill_obj.erp_save_id = sync_obj.execute_sync()
-                        bill_obj.save()
-                        return Response({'message': '保存成功'}, status=status.HTTP_200_OK)
+                        logger.info(f"[单据ID]:{bill_obj.billId}")
+                        sync_obj = PurchaseInboundSaveSync(bill_obj,entryIds)
+                        sync_result = sync_obj.execute_sync()
+                        
+                        return Response({'message': sync_result}, status=status.HTTP_200_OK)
                     else:
                         return Response({'message': '单据不存在'}, status=status.HTTP_400_BAD_REQUEST)
             elif bill_base == 1:
-                if bill_type == 3:
+                if bill_type == 1:
                     # 销售出库保存
-                    bill_obj = OutboundBill.objects.filter(id=bill_id).first()
+                    bill_obj = OutboundBill.objects.filter(billId=bill_id).first()
+                    entryIds = list(OutMaterialDetail.objects.filter(bound_billId=bill_id).values_list('entryIds', flat=True))
+
                     if bill_obj:
-                        sync_obj = SaleOutboundSaveSync(bill_obj,[])
-                        bill_obj.erp_save_id = sync_obj.execute_sync()
-                        bill_obj.save()
-                        return Response({'message': '保存成功'}, status=status.HTTP_200_OK)
+                        sync_obj = SaleOutboundSaveSync(bill_obj,entryIds)
+                        sync_result = sync_obj.execute_sync()
+                        
+                        return Response({'message': sync_result}, status=status.HTTP_200_OK)
                     else:
                         return Response({'message': '单据不存在'}, status=status.HTTP_400_BAD_REQUEST)
             else:
-                return Response({'message': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+                return Response({'message': '无需保存'}, status=status.HTTP_200_OK)
         except Exception as e:
             return Response({'message': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
                         
@@ -1731,6 +1740,18 @@ class OutboundBills(viewsets.ModelViewSet):
         else:
             return self.http_method_not_allowed(request=self.request)
         
+    def get_bound_number(self,request):
+        billid = request.data.get('billid', None)
+        bill_obj = OutboundBill.objects.filter(billId=billid).first()
+        return_data = {}
+        if bill_obj:
+            return_data['bill_number'] = bill_obj.number
+            return_data['bill_audit'] = bill_obj.erp_audit_id
+            return_data['bill_save'] = bill_obj.erp_save_id
+            return_data['bill_status'] = bill_obj.bound_status
+            return_data['bill_type'] = bill_obj.type
+        return Response(return_data,status=status.HTTP_200_OK)
+        
 class Materials(viewsets.ModelViewSet):
     """
         retrieve:

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 110 - 575
logs/boundBill.log


+ 314 - 0
logs/error.log

@@ -9412,3 +9412,317 @@ Traceback (most recent call last):
   File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\db\backends\sqlite3\base.py", line 357, in execute
     return Database.Cursor.execute(self, query, params)
 django.db.utils.OperationalError: no such table: user_profile
+[2025-06-03 11:34:54,786][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 11:35:56,570][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 11:35:56,571][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 11:36:37,814][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 11:36:37,815][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 13:09:11,475][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:09:11,476][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 13:12:11,794][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:12:11,797][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 13:13:12,397][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:46:07,715][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:46:07,716][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 13:55:50,101][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:56:04,505][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:56:22,445][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 13:56:26,100][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 15:29:52,924][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 15:29:52,925][django.server.log_message():187] [ERROR] "POST /container/container_wcs/ HTTP/1.1" 500 60
+[2025-06-03 15:35:28,927][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 15:36:06,030][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-03 15:36:48,007][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-05 15:10:56,546][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\generic\base.py", line 103, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 511, in dispatch
+    self.response = self.finalize_response(request, response, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 423, in finalize_response
+    assert isinstance(response, HttpResponseBase), (
+AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'NoneType'>`
+[2025-06-05 15:10:56,550][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 98088
+[2025-06-05 15:13:56,686][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\generic\base.py", line 103, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 511, in dispatch
+    self.response = self.finalize_response(request, response, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 423, in finalize_response
+    assert isinstance(response, HttpResponseBase), (
+AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'NoneType'>`
+[2025-06-05 15:13:56,693][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 98088
+[2025-06-05 15:34:13,278][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+[2025-06-05 15:34:13,281][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 255
+[2025-06-05 15:34:37,492][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+[2025-06-05 15:34:37,494][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 255
+[2025-06-05 15:52:41,483][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+[2025-06-05 15:52:41,486][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 302
+[2025-06-05 15:54:08,426][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+[2025-06-05 15:54:08,427][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 62
+[2025-06-05 15:55:19,590][django.request.log_response():241] [ERROR] Internal Server Error: /wms/boundBills/save/
+[2025-06-05 15:55:19,590][django.server.log_message():187] [ERROR] "POST /wms/boundBills/save/ HTTP/1.1" 500 57
+[2025-06-05 16:51:01,837][django.request.log_response():241] [ERROR] Internal Server Error: /warehouse/boundcodetype/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\utils\throttle.py", line 36, in allow_request
+    throttle_last_create_time = throttle_allocationlist.first().create_time
+AttributeError: 'NoneType' object has no attribute 'create_time'
+[2025-06-05 16:51:01,844][django.server.log_message():187] [ERROR] "GET /warehouse/boundcodetype/ HTTP/1.1" 500 122528
+[2025-06-05 18:18:29,216][django.request.log_response():241] [ERROR] Internal Server Error: /wms/outboundBills/number/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 117, in view
+    handler = getattr(self, action)
+AttributeError: 'OutboundBillsLog' object has no attribute 'get_bound_number'
+[2025-06-05 18:18:29,230][django.server.log_message():187] [ERROR] "POST /wms/outboundBills/number/ HTTP/1.1" 500 89044
+[2025-06-05 23:36:45,539][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-05 23:40:47,215][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-05 23:43:03,697][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-05 23:58:57,296][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-05 23:59:09,005][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-06 00:00:07,271][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-06 00:00:16,094][django.request.log_response():241] [ERROR] Internal Server Error: /container/container_wcs/
+[2025-06-09 15:53:37,487][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 472, in thread_handler
+    raise exc_info[1]
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 42, in inner
+    response = await get_response(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
+    response = await wrapped_callback(
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 435, in __call__
+    ret = await asyncio.wait_for(future, timeout=None)
+  File "D:\language\python38\lib\asyncio\tasks.py", line 455, in wait_for
+    return await fut
+  File "D:\language\python38\lib\concurrent\futures\thread.py", line 57, in run
+    result = self.fn(*self.args, **self.kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 476, in thread_handler
+    return func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\.\utils\throttle.py", line 20, in allow_request
+    openid = request.auth.openid
+AttributeError: 'NoneType' object has no attribute 'openid'
+[2025-06-09 16:05:02,846][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 472, in thread_handler
+    raise exc_info[1]
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 42, in inner
+    response = await get_response(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
+    response = await wrapped_callback(
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 435, in __call__
+    ret = await asyncio.wait_for(future, timeout=None)
+  File "D:\language\python38\lib\asyncio\tasks.py", line 455, in wait_for
+    return await fut
+  File "D:\language\python38\lib\concurrent\futures\thread.py", line 57, in run
+    result = self.fn(*self.args, **self.kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 476, in thread_handler
+    return func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\.\utils\throttle.py", line 20, in allow_request
+    openid = request.auth.openid
+AttributeError: 'NoneType' object has no attribute 'openid'
+[2025-06-09 16:07:26,844][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 472, in thread_handler
+    raise exc_info[1]
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 42, in inner
+    response = await get_response(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
+    response = await wrapped_callback(
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 435, in __call__
+    ret = await asyncio.wait_for(future, timeout=None)
+  File "D:\language\python38\lib\asyncio\tasks.py", line 455, in wait_for
+    return await fut
+  File "D:\language\python38\lib\concurrent\futures\thread.py", line 57, in run
+    result = self.fn(*self.args, **self.kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 476, in thread_handler
+    return func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\.\utils\throttle.py", line 20, in allow_request
+    openid = request.auth.openid
+AttributeError: 'NoneType' object has no attribute 'openid'
+[2025-06-09 16:09:05,672][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 472, in thread_handler
+    raise exc_info[1]
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 42, in inner
+    response = await get_response(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
+    response = await wrapped_callback(
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 435, in __call__
+    ret = await asyncio.wait_for(future, timeout=None)
+  File "D:\language\python38\lib\asyncio\tasks.py", line 455, in wait_for
+    return await fut
+  File "D:\language\python38\lib\concurrent\futures\thread.py", line 57, in run
+    result = self.fn(*self.args, **self.kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\asgiref\sync.py", line 476, in thread_handler
+    return func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "D:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\.\utils\throttle.py", line 20, in allow_request
+    openid = request.auth.openid
+AttributeError: 'NoneType' object has no attribute 'openid'
+[2025-06-09 16:10:08,581][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 497, in dispatch
+    self.initial(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 416, in initial
+    self.check_throttles(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 359, in check_throttles
+    if not throttle.allow_request(request, self):
+  File "D:\Document\code\vue\greater_wms\utils\throttle.py", line 20, in allow_request
+    openid = request.auth.openid
+AttributeError: 'NoneType' object has no attribute 'openid'
+[2025-06-09 16:10:08,583][django.server.log_message():187] [ERROR] "POST /container/location_release/ HTTP/1.1" 500 121856
+[2025-06-09 16:11:14,634][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 506, in dispatch
+    response = handler(request, *args, **kwargs)
+TypeError: release_location() missing 1 required positional argument: 'request'
+[2025-06-09 16:11:14,636][django.server.log_message():187] [ERROR] "POST /container/location_release/ HTTP/1.1" 500 108721
+[2025-06-09 16:12:10,820][django.request.log_response():241] [ERROR] Internal Server Error: /container/location_release/
+Traceback (most recent call last):
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
+    response = get_response(request)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
+    response = wrapped_callback(request, *callback_args, **callback_kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
+    return view_func(*args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\viewsets.py", line 125, in view
+    return self.dispatch(request, *args, **kwargs)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
+    response = self.handle_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
+    self.raise_uncaught_exception(exc)
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
+    raise exc
+  File "d:\Document\code\vue\greater_wms\.venv\lib\site-packages\rest_framework\views.py", line 506, in dispatch
+    response = handler(request, *args, **kwargs)
+TypeError: release_location() missing 1 required positional argument: 'request'
+[2025-06-09 16:12:10,822][django.server.log_message():187] [ERROR] "POST /container/location_release/ HTTP/1.1" 500 108721

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 2500 - 0
logs/server.log


+ 0 - 1
templates/dist/spa/css/10.74deb849.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-7676631a]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-7676631a]{background-color:rgba(25,118,210,0.1)}[data-v-7676631a] .q-field__label{margin-top:8px;align-self:center}[data-v-7676631a] .q-field__control-container{padding-left:50px;margin-top:-5px}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/css/10.fcf11716.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
templates/dist/spa/css/3.4200644e.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/css/8.01a63b42.css


+ 0 - 1
templates/dist/spa/css/8.58670660.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-53c28a9a]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-53c28a9a]{background-color:rgba(25,118,210,0.1)}[data-v-53c28a9a] .q-field__label{margin-top:8px;align-self:center}[data-v-53c28a9a] .q-field__control-container{padding-left:50px;margin-top:-5px}.q-timeline__subtitle[data-v-53c28a9a]{font-size:14px;margin-bottom:8px}

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

@@ -1 +0,0 @@
-[data-v-6cb08e33] .q-field__label{margin-top:8px;align-self:center}[data-v-6cb08e33] .q-field__control-container{padding-left:50px;margin-top:-5px}

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

@@ -0,0 +1 @@
+[data-v-0c2e412a] .q-field__label{margin-top:8px;align-self:center}[data-v-0c2e412a] .q-field__control-container{padding-left:50px;margin-top:-5px}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
templates/dist/spa/index.html


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/10.68b30aac.js


BIN
templates/dist/spa/js/10.68b30aac.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/10.6f3e618a.js


BIN
templates/dist/spa/js/10.6f3e618a.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/3.9f1140ab.js


BIN
templates/dist/spa/js/3.9f1140ab.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/3.d126d207.js


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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/8.583d8ab3.js


BIN
templates/dist/spa/js/8.583d8ab3.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/8.5e566b7e.js


BIN
templates/dist/spa/js/8.5e566b7e.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/app.76fa7928.js


BIN
templates/dist/spa/js/app.76fa7928.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/app.f04fecdb.js


BIN
templates/dist/spa/js/app.f04fecdb.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/chunk-common.655d825e.js


BIN
templates/dist/spa/js/chunk-common.655d825e.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/chunk-common.fe167124.js


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


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 289 - 12
templates/dist/spa/js/vendor.889e7783.js


BIN
templates/dist/spa/js/vendor.5f12bb2e.js.gz


BIN
templates/dist/spa/js/vendor.889e7783.js.gz


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

@@ -460,7 +460,7 @@ export default {
           return '出库'
         case 'inbound':
           return '入库'
-        case '4':
+        case 'adjust':
           return '移库'
         case '5':
           return '调拨'

+ 383 - 137
templates/src/components/goodscard.vue

@@ -1,5 +1,124 @@
 <template>
   <div :style="{ backgroundColor: bgColor }">
+    <q-dialog v-model="release_dialog">
+      <q-card style="min-width: 400px">
+        <q-bar class="bg-light-blue-10 text-white rounded-borders">
+          <div>释放库位</div>
+          <q-space />
+          <q-btn dense flat icon="close" v-close-popup />
+        </q-bar>
+        <q-card-section>
+          <q-item>
+            <q-item-section>
+              <q-item-label>库位信息</q-item-label>
+              <q-item-label caption>
+                {{ formatLocation(current_location) }}
+              </q-item-label>
+            </q-item-section>
+          </q-item>
+
+          <q-separator class="q-my-md" />
+
+          <div class="q-gutter-y-md">
+            <div class="text-subtitle1">库位状态</div>
+            <q-item>
+              <q-item-section>
+                <q-item-label
+                  >托盘编码:{{ container_code || "无" }}</q-item-label
+                >
+                <q-item-label caption>{{
+                  location_status_display
+                }}</q-item-label>
+              </q-item-section>
+            </q-item>
+
+            <q-separator class="q-my-md" />
+
+            <div v-if="container_code">
+              <div class="text-subtitle1">确认释放操作</div>
+              <div class="q-mt-sm text-grey-7">
+                此操作将解除托盘与库位的关联,并将库位状态重置为可用
+              </div>
+            </div>
+
+            <div v-else class="text-subtitle1 text-primary">
+              当前库位为空,无需释放
+            </div>
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn flat label="取消" v-close-popup />
+          <q-btn
+            color="primary"
+            label="确认释放"
+            :disable="!container_code"
+            @click="confirmRelease"
+          />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+    <q-dialog v-model="move_dialog">
+      <q-card style="min-width: 400px">
+        <q-bar class="bg-light-blue-10 text-white rounded-borders">
+          <div>移动托盘</div>
+          <q-space />
+          <q-btn dense flat icon="close" v-close-popup />
+        </q-bar>
+
+        <q-card-section>
+          <q-item>
+            <q-item-section>
+              <q-item-label>起始库位</q-item-label>
+              <q-item-label caption>{{
+                formatLocation(start_loc)
+              }}</q-item-label>
+            </q-item-section>
+          </q-item>
+
+          <q-separator class="q-my-md" />
+
+          <div class="q-gutter-y-md">
+            <div class="text-subtitle1">选择目标库位</div>
+            <div class="row q-gutter-sm">
+              <q-select
+                class="col"
+                v-model="target_loc.layer"
+                :options="layerOptions"
+                label="层"
+                dense
+                emit-value
+                map-options
+              />
+              <q-select
+                class="col"
+                v-model="target_loc.row"
+                :options="rowOptions"
+                label="行"
+                dense
+                emit-value
+                map-options
+              />
+              <q-select
+                class="col"
+                v-model="target_loc.col"
+                :options="colOptions"
+                label="列"
+                dense
+                emit-value
+                map-options
+              />
+            </div>
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn flat label="取消" v-close-popup />
+          <q-btn color="primary" label="确认移动" @click="confirmMove" />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+
     <q-dialog
       v-model="storage_dialog"
       transition-show="jump-down"
@@ -16,30 +135,32 @@
           </div>
           <q-space></q-space>
           {{ layerIndex }} 层 {{ rowIndex }} 行 {{ colIndex }}列
-
-          <!-- <q-btn dense flat icon="close" v-close-popup>
-            <q-tooltip
-              content-class="bg-amber text-black shadow-4"
-              :offset="[20, 20]"
-              content-style="font-size: 12px"
-            >
-              {{ $t("index.close") }}</q-tooltip
-            >
-          </q-btn> -->
         </q-bar>
         <q-card-section class="q-pt-md">
           <q-tabs v-model="activeTab">
-            <q-tab name="tab1" label="货架属性" />
             <q-tab name="tab2" label="批次信息" />
             <q-tab name="tab3" label="库存明细" />
           </q-tabs>
         </q-card-section>
         <!-- 选项卡内容 -->
         <q-tab-panels v-model="activeTab" animated>
-          <q-tab-panel name="tab1" style="height: 300px"> </q-tab-panel>
           <q-tab-panel name="tab2" style="height: 300px">
             <template>
-              <div class="text-h6 q-mb-md">{{ "批次信息" }}</div>
+              <div class="text-h6 q-mb-md row items-center justify-between">
+                <div class="col">{{ "批次信息" }}</div>
+                <div class="col-auto">
+                  <q-btn
+                    color="primary"
+                    label="释放库位"
+                    @click="showReleaseDialog"
+                  />
+                  <q-btn
+                    color="primary"
+                    label="移动库位"
+                    @click="showMoveDialog"
+                  />
+                </div>
+              </div>
               <q-table
                 v-if="storage_form.length > 0"
                 :data="storage_form"
@@ -344,7 +465,7 @@
 </template>
 
 <script>
-import { getauth } from "boot/axios_request";
+import { getauth, postauth } from 'boot/axios_request'
 
 export default {
   props: {
@@ -352,232 +473,357 @@ export default {
     colIndex: Number,
     layerIndex: Number,
     selectedShelfType: String,
-    goodsData: Object,
+    goodsData: Object
   },
-  data() {
+  data () {
     return {
-      pathname: "bin/",
-      pathnamecontainer: "container/locationdetail/",
+      pathname: 'bin/',
+      pathnamecontainer: 'container/locationdetail/',
       container_id: 123456,
       container_code: 0,
       results: [],
       storage_form: [],
       elevator_form: {
-        ip: "",
+        ip: '',
         port: 8080,
         // username: 'admin',
         // password: '123456',
-        status: "未连接",
+        status: '未连接'
       },
       conveyor_form: {
-        ip: "",
+        ip: '',
         port: 8080,
         // destination: 'A010203',
-        status: "未连接",
+        status: '未连接'
       },
       showInventoryDetails: true,
       inventoryDetails: [
         {
           id: 1,
-          batch: "",
+          batch: '',
           quantity: 0,
-          location: "",
+          location: ''
         },
         {
           id: 2,
-          batch: "",
+          batch: '',
           quantity: 0,
-          location: "",
-        },
+          location: ''
+        }
       ],
       columns_batch: [
         {
-          name: "bound_number",
-          label: "批次",
+          name: 'bound_number',
+          label: '批次',
           field: (row) => row.bound_number,
-          align: "center",
+          align: 'center'
         },
         {
-          name: "plan_weight",
-          label: "当前库位容纳重量",
+          name: 'plan_weight',
+          label: '当前库位容纳重量',
           field: (row) => row.total_batch_qty,
-          align: "center",
-        },
+          align: 'center'
+        }
       ],
       columns_results: [
         {
-          label: "物料编码",
+          label: '物料编码',
           field: (row) => row.goods_code,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "物料名称",
+          label: '物料名称',
           field: (row) => row.goods_desc,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "物料批次",
+          label: '物料批次',
           field: (row) => row.bound_number,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "每件重量",
+          label: '每件重量',
           field: (row) => row.goods_qty,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "件数",
+          label: '件数',
           field: (row) => row.group_qty,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "批次计划重量",
+          label: '批次计划重量',
           field: (row) => row.batch_total_qty,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "在库重量",
+          label: '在库重量',
           field: (row) => row.batch_total_in_qty,
-          align: "center",
+          align: 'center'
         },
         {
-          label: "录入时间",
+          label: '录入时间',
           field: (row) => row.create_time.slice(0, 10),
-          align: "center",
-        },
+          align: 'center'
+        }
       ],
       inventoryColumns: [
-        { name: "batch", label: "批次", field: "batch", align: "left" },
-        { name: "quantity", label: "数量", field: "quantity", align: "right" },
-        { name: "location", label: "位置", field: "location", align: "left" },
+        { name: 'batch', label: '批次', field: 'batch', align: 'left' },
+        { name: 'quantity', label: '数量', field: 'quantity', align: 'right' },
+        { name: 'location', label: '位置', field: 'location', align: 'left' }
       ],
-      user_id: "",
-      auth_id: "",
+      user_id: '',
+      auth_id: '',
       onlyread: true,
       clickedinput: false,
       storage_dialog: false,
       elevator_dialog: false,
       conveyor_dialog: false,
-      bgColor: "transparent",
+      bgColor: 'transparent',
 
-      shelf_type: { shelf_type: "undefined" },
-      ip_change: { ip_address: "192.168.1.100", port: 8080, status: "offline" },
+      shelf_type: { shelf_type: 'undefined' },
+      ip_change: { ip_address: '192.168.1.100', port: 8080, status: 'offline' },
       goods_change: {
-        shelf_status: "未满",
-        goods_batch: "20230701-001",
-        goods_code: "A010203",
-        goods_name: "存货货物",
-        goods_std: "规格型号",
-        goods_desc: "存货描述",
+        shelf_status: '未满',
+        goods_batch: '20230701-001',
+        goods_code: 'A010203',
+        goods_name: '存货货物',
+        goods_std: '规格型号',
+        goods_desc: '存货描述',
         goods_qty: 123456,
-        goods_unit: "kg",
+        goods_unit: 'kg',
         goods_price: 123456,
-        goods_notes: "备注",
+        goods_notes: '备注',
         goods_in: 123456,
-        goods_out: 123456,
+        goods_out: 123456
       },
 
-      error1: this.$t("stock.shelf.error1"),
-      shelfLocal: "",
-      activeTab: "tab2",
-    };
+      error1: this.$t('stock.shelf.error1'),
+      shelfLocal: '',
+      activeTab: 'tab2',
+      start_loc: {
+        layer: 1,
+        row: 1,
+        col: 1
+      },
+      target_loc: {
+        layer: 1,
+        row: 1,
+        col: 1
+      },
+      release_dialog: false,
+      current_location: {
+        layer: this.layerIndex,
+        row: this.rowIndex,
+        col: this.colIndex
+      },
+      move_dialog: false,
+      layerOptions: Array.from({ length: 3 }, (_, i) => ({
+        label: `${i + 1}层`,
+        value: i + 1
+      })),
+      rowOptions: Array.from({ length: 17 }, (_, i) => ({
+        label: `${i + 1}行`,
+        value: i + 1
+      })),
+      colOptions: Array.from({ length: 29 }, (_, i) => ({
+        label: `${i + 1}列`,
+        value: i + 1
+      }))
+    }
   },
-  created() {
-    this.shelfLocal = this.selectedShelfType;
-    this.handleclick();
+  created () {
+    this.shelfLocal = this.selectedShelfType
+    this.handleclick()
   },
 
   methods: {
-    prepareDialog() {
-      this.onlyread = true;
+    prepareDialog () {
+      this.onlyread = true
     },
+    formatLocation (loc) {
+      return `W01-${loc.row.toString().padStart(2, '0')}-${loc.col
+        .toString()
+        .padStart(2, '0')}-${loc.layer.toString().padStart(2, '0')}`
+    },
+
+    
+    showReleaseDialog () {
+      if (this.container_code) {
+        this.location_status_display = `托盘: ${this.container_code} - 占用中`
+      } else {
+        this.location_status_display = '库位空闲'
+      }
+      this.current_location = {
+        layer: this.layerIndex,
+        row: this.rowIndex,
+        col: this.colIndex
+      }
+      this.release_dialog = true
+    },
+    // 确认释放库位
+    async confirmRelease () {
+      try {
+        const locationCode = this.formatLocation(this.current_location)
+        const payload = {
+          location_release: locationCode,
+        }
+
+        const response = await postauth('container/location_release/', payload)
 
-    handleclick() {
-      this.shelfLocal = this.selectedShelfType;
+        if (response.code === '200') {
+          this.$q.notify({
+            color: 'positive',
+            message: '库位释放成功',
+            icon: 'check_circle'
+          })
 
-      if (this.shelfLocal === "undefined") {
-        (this.clickedinput = true),
-          (this.storage_dialog = false),
-          (this.elevator_dialog = false),
-          (this.conveyor_dialog = false);
+          // 重置库位状态
+          this.container_code = ''
+          this.storage_form = []
+          this.results = []
+          this.release_dialog = false
+
+          // 刷新父组件状态
+          this.$emit('location-released')
+        } else {
+          throw new Error(response.message || '释放操作失败')
+        }
+      } catch (error) {
+        console.error('库位释放失败', error)
+        this.$q.notify({
+          color: 'negative',
+          message: '库位释放失败',
+          caption: error.message || '请检查网络连接'
+        })
       }
-      if (this.shelfLocal === "storage") {
-        this.getList(),
-          (this.clickedinput = false),
-          (this.storage_dialog = true),
-          (this.elevator_dialog = false),
-          (this.conveyor_dialog = false);
+    },
+    showMoveDialog () {
+      this.start_loc = {
+        layer: this.layerIndex,
+        row: this.rowIndex,
+        col: this.colIndex
+      }
+      this.target_loc = { ...this.start_loc }
+      this.move_dialog = true
+    },
+    // 新增:确认移动操作
+    async confirmMove () {
+      const payload = {
+        container_code: this.container_code,
+        start_location: this.formatLocation(this.start_loc),
+        target_location: this.formatLocation(this.target_loc)
+      }
+
+      try {
+        const response = await postauth('container/container_wcs/', payload)
+        console.log('托盘移动指令发送成功', response)
+        if (response.code === '200') {
+          this.$q.notify({
+            color: 'positive',
+            message: '托盘移动指令已发送'
+          })
+          this.move_dialog = false
+        }
+      } catch (error) {
+        console.error('托盘移动失败', error)
+        this.$q.notify({
+          color: 'negative',
+          message: '托盘移动失败',
+          caption: error.message || '请检查网络连接'
+        })
+      }
+    },
+    handleclick () {
+      this.shelfLocal = this.selectedShelfType
+
+      if (this.shelfLocal === 'undefined') {
+        this.clickedinput = true
+        this.storage_dialog = false
+        this.elevator_dialog = false
+        this.conveyor_dialog = false
+      }
+      if (this.shelfLocal === 'storage') {
+        this.getList()
+        this.clickedinput = false
+        this.storage_dialog = true
+        this.elevator_dialog = false
+        this.conveyor_dialog = false
       }
-      if (this.shelfLocal === "elevator") {
-        this.getList(),
-          (this.clickedinput = false),
-          (this.storage_dialog = false),
-          (this.elevator_dialog = true),
-          (this.conveyor_dialog = false);
+      if (this.shelfLocal === 'elevator') {
+        this.getList()
+        this.clickedinput = false
+        this.storage_dialog = false
+        this.elevator_dialog = true
+        this.conveyor_dialog = false
       }
-      if (this.shelfLocal === "conveyor") {
-        this.getList(),
-          (this.clickedinput = false),
-          (this.storage_dialog = false),
-          (this.elevator_dialog = false),
-          (this.conveyor_dialog = true);
+      if (this.shelfLocal === 'conveyor') {
+        this.getList()
+        this.clickedinput = false
+        this.storage_dialog = false
+        this.elevator_dialog = false
+        this.conveyor_dialog = true
       }
     },
 
-    getList() {
-      var _this = this;
-      _this.storage_form = [];
-      _this.results = [];
+    getList () {
+      var _this = this
+      _this.storage_form = []
+      _this.results = []
       _this.inventoryDetails = [
         {
           id: 1,
-          batch: "",
+          batch: '',
           quantity: 0,
-          location: "",
+          location: ''
         },
         {
           id: 2,
-          batch: "",
+          batch: '',
           quantity: 0,
-          location: "",
-        },
-      ];
-      getauth(_this.pathname + _this.goodsData.id + "/")
+          location: ''
+        }
+      ]
+      getauth(_this.pathname + _this.goodsData.id + '/')
         .then((res1) => {
-          console.log(res1);
-          console.log(res1.current_containers);
+          console.log(res1)
+          console.log(res1.current_containers)
           if (res1.current_containers.length === 0) {
-            console.log("当前托盘ID为空");
+            console.log('当前托盘ID为空')
             _this.$q.notify({
-              message: "当前库位为空",
-              icon: "info",
-              color: "info",
-            });
-            return;
+              message: '当前库位为空',
+              icon: 'info',
+              color: 'info'
+            })
+            _this.container_code = ''
+            _this.storage_form = []
+            _this.results = []
+            return
           }
-          _this.container_id = res1.current_containers[0].id;
-          console.log("当前托盘ID", _this.container_id);
-          console.log("当前托盘ID长度", res1.current_containers.length);
+          _this.container_id = res1.current_containers[0].id
+          _this.container_code = res1.current_containers[0].container_code
+          console.log('当前托盘ID', _this.container_id)
+          console.log('当前托盘ID长度', res1.current_containers.length)
 
           getauth(
-            _this.pathnamecontainer + "?container=" + _this.container_id
+            _this.pathnamecontainer + '?container=' + _this.container_id
           ).then((res) => {
-            var data = res.data;
-            this.container_code = data.container_code;
-            this.storage_form = data.batch_totals;
-            this.results = data.results;
-          });
+            var data = res.data
+
+            this.storage_form = data.batch_totals
+            this.results = data.results
+          })
         })
         .catch((err) => {
           _this.$q.notify({
             message: err.detail,
-            icon: "close",
-            color: "negative",
-          });
-        });
-    },
-  },
-};
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+    }
+  }
+}
 </script>
 
 <style scoped>

+ 107 - 61
templates/src/pages/erp/erpasn.vue

@@ -349,59 +349,24 @@
               <q-card class="q-mb-md" bordered>
                 <q-card-section>
                   <template>
-                    <div class="text-h6 q-mb-md">{{ "批次信息" }}</div>
+                    <div class="text-h6 q-mb-md" style="max-width: 800px">
+                      {{ "批次信息" }}
+
+                    </div>
                     <template v-if="batch_detail.length > 0">
-                      <div
-                        v-for="(item, index) in batch_detail"
-                        :key="index"
-                        class="row q-col-gutter-md q-mb-sm"
+                      <q-table
+                        v-if="batch_detail.length > 0"
+                        :data="batch_detail"
+                        :columns="columns_batch"
+                        row-key="id"
+                        flat
+                        bordered
+                        hide-pagination
+                        class="my-sticky-table scrollable-table"
+                        :container-style="{ height: 'auto' }"
+                        :pagination="{ rowsPerPage: 0 }"
                       >
-                        <div class="col-2" v-if="onlyread">
-                          <q-input
-                            v-model="item.entryIds"
-                            :label="'明细id'"
-                            :readonly="onlyread"
-                            dense
-                            outlined
-                          />
-                        </div>
-                        <div class="col-3">
-                          <q-input
-                            v-model="item.production_batch"
-                            :label="'批次号'"
-                            :readonly="onlyread"
-                            dense
-                            outlined
-                          />
-                        </div>
-                        <div class="co-4">
-                          <q-input
-                            v-model="item.goods_name"
-                            :label="'货物名称'"
-                            :readonly="onlyread"
-                            dense
-                            outlined
-                          />
-                        </div>
-                        <div class="col-2">
-                          <q-input
-                            v-model="item.plan_qty"
-                            :label="'计划数量'"
-                            :readonly="onlyread"
-                            dense
-                            outlined
-                          />
-                        </div>
-                        <div class="col-2">
-                          <q-input
-                            v-model="item.goods_unit"
-                            :label="'单位'"
-                            :readonly="onlyread"
-                            dense
-                            outlined
-                          />
-                        </div>
-                      </div>
+                      </q-table>
                     </template>
                   </template>
                 </q-card-section>
@@ -551,11 +516,10 @@
 
 <script>
 import { getauth, postauth, deleteauth } from 'boot/axios_request'
-
+import moment from 'moment'
 import { date, exportFile, LocalStorage } from 'quasar'
 
 export default {
-
   name: 'Pageasnlist',
   data () {
     return {
@@ -617,6 +581,58 @@ export default {
         },
         { name: 'action', label: '操作', align: 'center' }
       ],
+      // 列定义
+      columns_batch: [
+        {
+          name: 'id',
+          label: '主单ID',
+          field: (row) => row.bound_billId,
+          align: 'center'
+        },
+        {
+          name: 'id',
+          label: '详单ID',
+          field: (row) => row.entryIds,
+          align: 'center'
+        },
+        {
+          name: 'production_batch',
+          label: 'ERP批次号',
+          field: (row) => row.production_batch,
+          align: 'center'
+        },
+        {
+          name: 'goods_code',
+          label: '货物编码',
+          field: (row) => row.goods_code,
+          align: 'center'
+        },
+        {
+          name: 'goods_name',
+          label: '货物名称',
+          field: (row) => row.goods_name,
+          align: 'center'
+        },
+        {
+          name: 'plan_qty',
+          label: '计划数量',
+          field: (row) => row.plan_qty,
+          align: 'center'
+        },
+        {
+          name: 'goods_unit',
+          label: '单位',
+          field: (row) => row.goods_unit,
+          align: 'center'
+        },
+        {
+          name: 'create_time',
+          label: '创建时间',
+          field: (row) => moment(row.create_time).format('YYYY-MM-DD'),
+          align: 'center'
+        }
+      ],
+
       filter: '',
       product_filter: '',
       pagination: {
@@ -689,16 +705,23 @@ export default {
       if (item.type === 1) {
         _this.get_log_list()
       } else if (item.type === 2) {
-        postauth('wms/boundBills/audit/', { billid: _this.detailid, billtype: _this.detail_type, billbase: 0 })
-          .catch((err) => {
-            _this.$q.notify({
-              message: err.detail || '审核失败',
-              icon: 'close',
-              color: 'negative'
-            })
+        postauth('wms/boundBills/audit/', {
+          billid: _this.detailid,
+          billtype: _this.detail_type,
+          billbase: 0
+        }).catch((err) => {
+          _this.$q.notify({
+            message: err.detail || '审核失败',
+            icon: 'close',
+            color: 'negative'
           })
+        })
       } else if (item.type === 3) {
-        postauth('wms/boundBills/save/', { billid: _this.detailid, billtype: _this.detail_type, billbase: 0 })
+        postauth('wms/boundBills/save/', {
+          billid: _this.detailid,
+          billtype: _this.detail_type,
+          billbase: 0
+        })
       }
     },
     get_log_list () {
@@ -1178,4 +1201,27 @@ export default {
   font-size: 14px;
   margin-bottom: 8px;
 }
+
+/* 修改滚动样式 */
+.scrollable-table {
+  flex: 1; /* 允许表格填充剩余空间 */
+  display: flex;
+  flex-direction: column;
+}
+
+.scrollable-table .q-table__container {
+  flex: 1;
+  overflow: auto;
+}
+
+.scrollable-table .q-table__top {
+  position: sticky;
+  top: 0;
+  background: white;
+  z-index: 1;
+}
+
+.my-sticky-table .q-table__middle {
+  overflow-y: auto !important;
+}
 </style>

+ 329 - 87
templates/src/pages/erp/erpdn.vue

@@ -117,6 +117,7 @@
                   map-options
                   transition-show="scale"
                   transition-hide="scale"
+                  :rules="[(val) => (val && val.length > 0) || error1]"
                 />
               </q-td>
             </template>
@@ -141,6 +142,7 @@
                   map-options
                   transition-show="scale"
                   transition-hide="scale"
+                  :rules="[(val) => (val && val.length > 0) || error1]"
                 />
               </q-td>
             </template>
@@ -286,7 +288,7 @@
       transition-show="jump-down"
       transition-hide="jump-up"
     >
-      <q-card style="min-width: 900px; position: relative">
+      <q-card style="min-width: 950px; position: relative">
         <q-bar
           class="bg-light-blue-10 text-white rounded-borders"
           style="height: 50px"
@@ -308,12 +310,12 @@
         <q-card-section class="q-pt-md">
           <q-tabs v-model="activeTab">
             <q-tab name="tab1" label="单据信息" />
-            <!-- <q-tab name="tab2" label="货物信息" /> -->
+            <q-tab name="tab2" label="对接信息" />
           </q-tabs>
         </q-card-section>
         <!-- 选项卡内容 -->
         <q-tab-panels v-model="activeTab" animated>
-          <q-tab-panel name="tab1" style="height: 70px">
+          <q-tab-panel name="tab1" style="min-height: 609px">
             <div class="row q-gutter-x-md">
               <div class="col column q-gutter-y-md">
                 <q-input
@@ -336,74 +338,150 @@
                 />
               </div>
             </div>
-          </q-tab-panel>
-        </q-tab-panels>
-
-        <div
-          style="float: right; padding: 15px 15px 50px 15px; min-width: 100%"
-          flow="row wrap"
-        >
-          <q-card class="q-mb-md" bordered>
-            <q-card-section>
-              <template>
-                <div class="text-h6 q-mb-md">{{ "批次信息" }}</div>
-                <template v-if="batch_detail.length > 0">
-                  <div
-                    v-for="(item, index) in batch_detail"
-                    :key="index"
-                    class="row q-col-gutter-md q-mb-sm"
-                  >
-                    <div class="col">
-                      <q-input
-                        v-model="item.entryIds"
-                        :label="'明细id'"
-                        :readonly="onlyread"
-                        dense
-                        outlined
-                      />
-                    </div>
-                    <div class="col" style="min-width: 150px">
-                      <q-input
-                        v-model="item.production_batch"
-                        :label="'批次号'"
-                        :readonly="onlyread"
-                        dense
-                        outlined
-                      />
-                    </div>
-                    <div class="col">
-                      <q-input
-                        v-model="item.goods_name"
-                        :label="'货物名称'"
-                        :readonly="onlyread"
-                        dense
-                        outlined
-                      />
+            <div
+              style="
+                float: right;
+                padding: 15px 15px 50px 15px;
+                min-width: 100%;
+              "
+              flow="row wrap"
+            >
+              <q-card class="q-mb-md" bordered>
+                <q-card-section>
+                  <template>
+                    <div class="text-h6 q-mb-md" style="max-width: 800px">
+                      {{ "批次信息" }}
                     </div>
-                    <div class="col">
-                      <q-input
-                        v-model="item.goods_out_qty"
-                        :label="'计划数量'"
-                        :readonly="onlyread"
-                        dense
-                        outlined
-                      />
+                    <template v-if="batch_detail.length > 0">
+                      <q-table
+                        v-if="batch_detail.length > 0"
+                        :data="batch_detail"
+                        :columns="columns_batch"
+                        row-key="id"
+                        flat
+                        bordered
+                        hide-pagination
+                        class="my-sticky-table scrollable-table"
+                        :container-style="{ height: 'auto' }"
+                        :pagination="{ rowsPerPage: 0 }"
+                      >
+                      </q-table>
+                    </template>
+                  </template>
+                </q-card-section>
+              </q-card>
+            </div>
+          </q-tab-panel>
+          <q-tab-panel name="tab2" style="min-height: 300px">
+            <div class="row q-gutter-x-md">
+              <div class="col column q-gutter-y-md">
+                <q-input
+                  dense
+                  outlined
+                  square
+                  v-model="table_detail.date"
+                  :label="'单据时间'"
+                  :readonly="true"
+                />
+              </div>
+              <div class="col column q-gutter-y-md">
+                <q-input
+                  dense
+                  outlined
+                  square
+                  v-model="table_detail.number"
+                  :label="'单据编码'"
+                  :readonly="true"
+                />
+              </div>
+            </div>
+            <div
+              style="
+                float: right;
+                padding: 15px 15px 50px 15px;
+                min-width: 100%;
+              "
+              flow="row wrap"
+            >
+              <q-card class="q-mb-md" bordered>
+                <q-card-section>
+                  <template>
+                    <div class="text-h6 q-mb">
+                      {{ formatBSType(detail_type) }}对接信息
+                      <q-btn dense flat icon="cached" @click="get_log_list()">
+                        <q-tooltip
+                          content-class="bg-amber text-black shadow-4"
+                          :offset="[20, 20]"
+                          content-style="font-size: 12px"
+                        >
+                          刷新对接日志
+                        </q-tooltip>
+                      </q-btn>
+                      <q-space></q-space>
                     </div>
-                    <div class="col">
-                      <q-input
-                        v-model="item.goods_unit"
-                        :label="'单位'"
-                        :readonly="onlyread"
-                        dense
-                        outlined
-                      />
+                    <div class="row q-gutter-x-md">
+                      <div class="q-pa-md col">
+                        <q-timeline color="primary">
+                          <q-timeline-entry
+                            v-for="(item, index) in logline"
+                            :key="index"
+                            :subtitle="
+                              item.create_time + ':' + item.log_content
+                            "
+                            side="left"
+                          >
+                          </q-timeline-entry>
+                        </q-timeline>
+                      </div>
+                      <div class="q-pa-md col">
+                        <q-timeline color="primary">
+                          <q-timeline-entry
+                            v-for="(item, index) in timeline"
+                            :key="index"
+                            :subtitle="item.timestamp"
+                            side="left"
+                          >
+                            <div class="row items-center">
+                              <q-btn
+                                round
+                                dense
+                                flat
+                                icon="done_outline"
+                                color="primary"
+                                class="q-mr-sm"
+                                @click="check_erp(item)"
+                              />
+                              <q-expansion-item
+                                class="col"
+                                v-model="item.expanded"
+                                expand-icon-class="text-primary"
+                                :label="item.title"
+                                header-class="text-primary"
+                              >
+                                <div class="q-pl-md">
+                                  <div class="text-body2">
+                                    {{ item.content }}
+                                  </div>
+                                  <!-- 可添加更多详情 -->
+                                  <div
+                                    v-if="item.details"
+                                    class="text-caption text-grey q-pt-sm"
+                                  >
+                                    {{ item.details }}
+                                  </div>
+                                </div>
+                              </q-expansion-item>
+                            </div>
+                          </q-timeline-entry>
+                        </q-timeline>
+                      </div>
                     </div>
-                  </div>
-                </template>
-              </template>
-            </q-card-section>
-          </q-card>
-        </div>
+                  </template>
+                </q-card-section>
+              </q-card>
+            </div>
+          </q-tab-panel>
+        </q-tab-panels>
       </q-card>
     </q-dialog>
     <q-dialog
@@ -436,14 +514,11 @@
 <router-view />
 
 <script>
-import { getauth, postauth, putauth, deleteauth } from 'boot/axios_request'
-
-import { date, exportFile, LocalStorage, QToggle } from 'quasar'
+import { getauth, postauth, deleteauth } from 'boot/axios_request'
+import moment from 'moment'
+import { date, exportFile, LocalStorage } from 'quasar'
 
 export default {
-  components: {
-    QToggle
-  },
   name: 'Pageasnlist',
   data () {
     return {
@@ -458,17 +533,14 @@ export default {
       authin: '0',
       warehouse_code: '',
       warehouse_name: '',
-
       searchUrl: '',
       pathname: 'wms/outboundBills/',
       pathfilename: 'bound/file/',
-
       pathname_previous: '',
       pathname_next: '',
       separator: 'cell',
       loading: false,
       height: '',
-
       printObj: {
         id: 'printMe',
         popTitle: this.$t('inbound.asn')
@@ -485,10 +557,8 @@ export default {
       bound_department_list: [],
       bound_department_map: [],
       bound_status_list: [],
-
       product_list: [],
       product_map: [],
-
       columns: [
         { name: 'detail', label: '详情', field: 'detail', align: 'center' },
         { name: 'billId', label: '表单序号', field: 'billId', align: 'center' },
@@ -510,6 +580,58 @@ export default {
         },
         { name: 'action', label: '操作', align: 'center' }
       ],
+      // 列定义
+      columns_batch: [
+        {
+          name: 'id',
+          label: '主单ID',
+          field: (row) => row.bound_billId,
+          align: 'center'
+        },
+        {
+          name: 'id',
+          label: '详单ID',
+          field: (row) => row.entryIds,
+          align: 'center'
+        },
+        {
+          name: 'production_batch',
+          label: 'ERP批次号',
+          field: (row) => row.production_batch,
+          align: 'center'
+        },
+        {
+          name: 'goods_code',
+          label: '货物编码',
+          field: (row) => row.goods_code,
+          align: 'center'
+        },
+        {
+          name: 'goods_name',
+          label: '货物名称',
+          field: (row) => row.goods_name,
+          align: 'center'
+        },
+        {
+          name: 'plan_qty',
+          label: '计划数量',
+          field: (row) => row.goods_out_qty,
+          align: 'center'
+        },
+        {
+          name: 'goods_unit',
+          label: '单位',
+          field: (row) => row.goods_unit,
+          align: 'center'
+        },
+        {
+          name: 'create_time',
+          label: '创建时间',
+          field: (row) => moment(row.create_time).format('YYYY-MM-DD'),
+          align: 'center'
+        }
+      ],
+
       filter: '',
       product_filter: '',
       pagination: {
@@ -530,7 +652,6 @@ export default {
       detailid: 0,
       bar_code: '',
       error1: this.$t('goods.view_goodslist.error1'),
-
       max: 0,
       total: 0,
       paginationIpt: 1,
@@ -540,7 +661,32 @@ export default {
       activeTab: 'tab1',
       confirmForm: false,
       choose_bill_id: '',
-      choose_bill_code: ''
+      choose_bill_code: '',
+      logline: [],
+      timeline: [
+        {
+          timestamp: '出库汇报',
+          title: '单据编码',
+          content: '暂无',
+          expanded: true,
+          type: 1
+        },
+        {
+          timestamp: '出库审核',
+          title: '单据编码',
+          content: '暂无',
+          expanded: true,
+          type: 2
+        },
+        {
+          timestamp: '出库保存',
+          title: '单据编码',
+          content: '暂无',
+          expanded: true,
+          type: 3
+        }
+      ],
+      detail_type: 0
     }
   },
   computed: {
@@ -553,6 +699,77 @@ export default {
     }
   },
   methods: {
+    check_erp (item) {
+      var _this = this
+      if (item.type === 1) {
+        _this.get_log_list()
+      } else if (item.type === 2) {
+        postauth('wms/boundBills/audit/', {
+          billid: _this.detailid,
+          billtype: _this.detail_type,
+          billbase: 1
+        }).catch((err) => {
+          _this.$q.notify({
+            message: err.detail || '审核失败',
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+      } else if (item.type === 3) {
+        postauth('wms/boundBills/save/', {
+          billid: _this.detailid,
+          billtype: _this.detail_type,
+          billbase: 1
+        })
+      }
+    },
+    get_log_list () {
+      var _this = this
+
+      getauth('wms/outboundBills/log/?billid=' + _this.detailid)
+        .then((res) => {
+          if (res && res.results) {
+            _this.logline = res.results
+          } else {
+            console.error('响应中没有 results 属性', res)
+          }
+        })
+        .catch((err) => {
+          _this.$q.notify({
+            message: err.detail || '获取日志列表时出错',
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+
+      postauth('wms/outboundBills/number/', { billid: _this.detailid })
+        .then((res) => {
+          _this.timeline[0].title = res.bill_number
+          _this.timeline[1].title = res.bill_audit
+          _this.timeline[2].title = res.bill_save
+          _this.detail_type = res.bill_type
+          if (res.bill_status === 0) {
+            _this.timeline[0].content = 'ERP已发起出库申请,请审核'
+            _this.timeline[1].content = '待审核'
+            _this.timeline[2].content = '待保存'
+          } else if (res.bill_status === 1) {
+            _this.timeline[0].content = 'ERP已发起出库申请,已审核'
+            _this.timeline[1].content = '已质检或无需质检'
+            _this.timeline[2].content = '待入库'
+          } else if (res.bill_status === 2) {
+            _this.timeline[0].content = 'ERP已发起出库申请,已审核'
+            _this.timeline[1].content = '已质检或无需质检'
+            _this.timeline[2].content = '已入库'
+          }
+        })
+        .catch((err) => {
+          _this.$q.notify({
+            message: err.detail || '获取单据编号时出错',
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+    },
     formatBSType (type) {
       switch (type) {
         case 1:
@@ -589,7 +806,6 @@ export default {
         base_type: '0',
         page_size: _this.pagination.rowsPerPage
       }
-
       // 创建URLSearchParams处理参数
       const queryParams = new URLSearchParams({
         ...baseParams,
@@ -602,11 +818,9 @@ export default {
           queryParams.delete(key)
         }
       })
-
       getauth(`${_this.pathname}?${queryParams}`)
         .then((res) => {
           _this.table_list = res.results
-
           _this.total = res.count
           _this.max = Math.ceil(res.count / _this.pagination.rowsPerPage) || 0
           _this.pathname_previous = res.previous
@@ -635,7 +849,6 @@ export default {
       }
       this.getSearchList(this.current)
     },
-
     // 带搜索条件加载
     getSearchList (page = 1) {
       this.current = page
@@ -751,14 +964,14 @@ export default {
       }
       postauth('wms/generateoutbound', params)
         .then((res) => {
-          if (res.code == 200) {
+          if (res.code === 200) {
             _this.$q.notify({
               message: '出库库单生成成功,请到出库管理中查看',
               icon: 'check',
               color: 'green'
             })
           }
-          if (res.code == 400) {
+          if (res.code === 400) {
             _this.$q.notify({
               message: res.error,
               icon: 'info',
@@ -803,6 +1016,8 @@ export default {
           })
         })
       console.log('batch查询的结果是:', _this.batch_detail)
+      console.log('batch长度查询的结果是:', _this.batch_detail.length)
+      _this.get_log_list()
     },
     deleteDataSubmit () {
       var _this = this
@@ -978,4 +1193,31 @@ export default {
   padding-left: 50px;
   margin-top: -5px;
 }
+.q-timeline__subtitle {
+  font-size: 14px;
+  margin-bottom: 8px;
+}
+
+/* 修改滚动样式 */
+.scrollable-table {
+  flex: 1; /* 允许表格填充剩余空间 */
+  display: flex;
+  flex-direction: column;
+}
+
+.scrollable-table .q-table__container {
+  flex: 1;
+  overflow: auto;
+}
+
+.scrollable-table .q-table__top {
+  position: sticky;
+  top: 0;
+  background: white;
+  z-index: 1;
+}
+
+.my-sticky-table .q-table__middle {
+  overflow-y: auto !important;
+}
 </style>

+ 1 - 1
utils/throttle.py

@@ -10,7 +10,7 @@ class VisitThrottle(BaseThrottle):
     def allow_request(self, request, view):
         if request.path in ['/api/docs/', '/api/debug/', '/api/']:
             return (False, None)
-        elif request.path in ['/container/container_wcs/','/container/container_wcs/update/',
+        elif request.path in ['/container/container_wcs/','/container/container_wcs/update/','/container/location_release/',
                                 '/container/batch/','/wms/createInboundApply','/wms/createOutboundApply'
                                 ,'/wms/productInfo','/wms/updateBatchInfo']:
             return True