Ver Fonte

抽检功能完善

flower_bs há 6 dias atrás
pai
commit
01eaae1bd7

+ 8 - 2
bound/views.py

@@ -964,6 +964,7 @@ class BatchContainerAPIView(APIView):
         data = request.data
         batch_id = data.get('batch_id')
         from container.models import ContainerDetailModel
+        from bin.models import LocationContainerLink
 
 
         container_detail_all = ContainerDetailModel.objects.filter(batch_id=batch_id, is_delete=False).exclude(status=3).all()
@@ -971,17 +972,22 @@ class BatchContainerAPIView(APIView):
 
         for container_detail in container_detail_all:
             container_id = container_detail.container_id
+            link_obj = LocationContainerLink.objects.filter(container_id=container_id,is_active=True).first()
+
 
             if container_id not in container_dict:
                 container_dict[container_id] = {
                     'id': container_id,
                     'goods_code': container_detail.goods_code,
                     'goods_desc': container_detail.goods_desc,
-             
                     'container_code': container_detail.container.container_code,
                     'current_location': container_detail.container.current_location,
                     'goods_qty': container_detail.goods_qty-container_detail.goods_out_qty,
-                    'class':container_detail.goods_class
+                    'class':container_detail.goods_class,
+                    'location_c_number' : link_obj.location.c_number if link_obj else None,
+                    'location_max_capacity': link_obj.location.max_capacity if link_obj else None,
+                    'location_group': link_obj.location.location_group if link_obj else None,
+                    'location_code': link_obj.location.location_code if link_obj else None,
                 }
             else:
                 container_dict[container_id]['goods_qty'] += container_detail.goods_qty-container_detail.goods_out_qty

+ 2 - 2
container/urls.py

@@ -47,8 +47,8 @@ path(r'location_release/',views.ContainerWCSViewSet.as_view({"post": "release_lo
 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'),
-
+path('out_task/', views.OutTaskViewSet.as_view({'post': 'post'  }), name='out_task'),
+path('check_task/', views.OutTaskViewSet.as_view({'post': 'post_check' }), name='out_task_check'),
 path(r'batch/', views.BatchViewSet.as_view({"post": "wcs_post"}), name="Batch"),
 
 path(r'out_detail/', views.OutDetailViewSet.as_view({"get": "list", "post": "create"}), name="Task"),

+ 181 - 30
container/views.py

@@ -1,5 +1,5 @@
 from wsgiref import headers
-from rest_framework.views import APIView
+from rest_framework.viewsets import ViewSet
 from rest_framework import viewsets
 from utils.page import MyPageNumberPagination
 from django.db.models import Prefetch 
@@ -886,6 +886,13 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
                                 status=status.HTTP_500_INTERNAL_SERVER_ERROR)
             OutboundService.process_next_task()
+
+        if task and task.tasktype == 'check' and task.status == 300:
+            success = self.handle_outbound_completion(container_obj, task)
+            if not success:
+                return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
+                                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+            OutboundService.process_next_task()
         
         return Response({
             'code': '200',
@@ -1084,32 +1091,35 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         try:
             allocator = LocationAllocation()
             location_task = task.current_location
-            location_row = location_task.split('-')[1]
-            location_col = location_task.split('-')[2]
-            location_layer = location_task.split('-')[3]
-            location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
-            location_code = location.location_code
-  
+            if location_task == '103' or location_task == '203':
+                return True
+            else:
+                location_row = location_task.split('-')[1]
+                location_col = location_task.split('-')[2]
+                location_layer = location_task.split('-')[3]
+                location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
+                location_code = location.location_code
+    
 
-            # 事务确保原子性
-            with transaction.atomic():
-                # 解除库位与托盘的关联
-                if not allocator.release_location(location_code):
-                    raise Exception("解除库位关联失败")
+                # 事务确保原子性
+                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_status(location_code, 'available'):
+                        raise Exception("库位状态更新失败")
 
-                # 更新库位组的统计信息
-                self.handle_group_location_status(location_code, location.location_group)
+                    # 更新库位组的统计信息
+                    self.handle_group_location_status(location_code, location.location_group)
 
-                # 更新托盘状态为已出库(假设状态3表示已出库)
-                container_obj.status = 3
-                container_obj.save()
+                    # 更新托盘状态为已出库(假设状态3表示已出库)
+                    container_obj.status = 3
+                    container_obj.save()
 
 
-            return True
+                return True
         except Exception as e:
             logger.error(f"出库完成处理失败: {str(e)}")
             return False
@@ -1680,6 +1690,50 @@ class OutboundService:
                 container_obj.save()
             ContainerWCSModel.objects.bulk_create(tasks)
             logger.info(f"已创建 {len(tasks)} 个初始任务")
+    
+    @staticmethod
+    def create_initial_check_tasks(container_list,batch_id):
+        """生成初始任务队列"""
+        with transaction.atomic():
+            current_WCS = ContainerWCSModel.objects.filter(tasktype='check',batch_id = batch_id,is_delete=False,working=1).first()
+            if current_WCS:
+                logger.error(f"当前{batch_id}已有检查任务")
+                return False
+            tasks = []
+            start_sequence = ContainerWCSModel.objects.filter(tasktype='check').count() + 1
+            tasknumber = ContainerWCSModel.objects.filter().count() 
+            tasknumber_index = 1
+            for index, container in enumerate(container_list, start=start_sequence):
+                container_obj = ContainerListModel.objects.filter(id =container['container_number']).first()
+                if container_obj.current_location != container_obj.target_location:
+                    logger.error(f"托盘 {container_obj.container_code} 未到达目的地,不生成任务")
+                    return False
+                batch_obj = BoundBatchModel.objects.filter(id =batch_id).first()
+                month = int(timezone.now().strftime("%Y%m"))
+                task = ContainerWCSModel(
+                    taskid=OutboundService.generate_task_id(),
+                    batch = batch_obj,
+                    batch_out = None,
+                    bound_list = None,
+                    sequence=index,
+                    order_number = container['c_number'],
+                    priority=100,
+                    tasknumber = month*100000+tasknumber_index+tasknumber,
+                    container=container_obj.container_code,
+                    current_location=container_obj.current_location,
+                    target_location="103",
+                    tasktype="check",
+                    month=int(timezone.now().strftime("%Y%m")),
+                    message="等待出库",
+                    status=100,
+                )
+                tasknumber_index += 1
+                tasks.append(task)
+                container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
+                container_obj.target_location = task.target_location
+                container_obj.save()
+            ContainerWCSModel.objects.bulk_create(tasks)
+            logger.info(f"已创建 {len(tasks)} 个初始任务")
 
     @staticmethod
     def insert_new_tasks(new_tasks):
@@ -1775,7 +1829,7 @@ class OutboundService:
 
 
 # 出库任务下发
-class OutTaskViewSet(APIView):
+class OutTaskViewSet(ViewSet):
 
     """
     # fun:get_out_task:下发出库任务
@@ -1799,9 +1853,9 @@ class OutTaskViewSet(APIView):
 
                 if current_WCS.working == 1:
                     OutboundService.process_current_task(current_WCS.taskid)
-                    return Response({"code": "200", "msg": f"下发任务{ current_WCS.taskid }到WCS成功"}, status=200)
+                    return Response({"code": 200, "msg": f"下发任务{ current_WCS.taskid }到WCS成功"}, status=200)
                 else :
-                    return Response({"code": "200", "msg": f"当前任务{current_WCS.taskid}正在处理中"}, status=200)
+                    return Response({"code": 200, "msg": f"当前任务{current_WCS.taskid}正在处理中"}, status=200)
 
 
 
@@ -1812,7 +1866,7 @@ class OutTaskViewSet(APIView):
             ).select_related('batch_number')
             
             if not out_batches.exists():
-                return Response({"code": "404", "msg": "未找到相关出库批次"}, status=404)
+                return Response({"code": 404, "msg": "未找到相关出库批次"}, status=404)
             # 构建批次需求字典
             batch_demand = {}
             for ob in out_batches:
@@ -1846,14 +1900,43 @@ class OutTaskViewSet(APIView):
             # 3. 立即发送第一个任务
             OutboundService.process_next_task()
             
-            return Response({"code": "200", "msg": "Success"}, status=200)
+            return Response({"code": 200, "msg": "下发任务成功"}, status=200)
         except Exception as e:
             logger.error(f"任务生成失败: {str(e)}")
-            return Response({"code": "200", "msg": str(e)}, status=200)
+            return Response({"code": 200, "msg": str(e)}, status=200)
 
 
-
-    # 获取出库需求
+    def post_check(self, request):
+        try:
+            data = self.request.data
+            logger.info(f"收到 出库抽检 推送数据: {data}")
+            # 从请求中获取 batch_id
+            batch_id = data.get('batch_id')
+            container_demand = int(data.get('container_demand'))
+            if not batch_id or not container_demand:
+                return Response({"code": "400", "msg": "缺少抽检数目或批次号"}, status=200)
+            current_WCS = ContainerWCSModel.objects.filter(batch=batch_id,tasktype='check',is_delete=False,working=1).first()
+            if current_WCS:
+                logger.info(f"当前{batch_id}已有出库抽检任务{current_WCS.taskid}")
+                if current_WCS.working == 1:
+                    OutboundService.process_current_task(current_WCS.taskid)
+                    return Response({"code": 200, "msg": f"下发任务{ current_WCS.taskid }到WCS成功"}, status=200)
+                else :
+                    return Response({"code": 200, "msg": f"当前任务{current_WCS.taskid}正在处理中"}, status=200)
+            # 获取批次号
+            generate_result = self.generate_location_by_check(batch_id,container_demand)
+            if generate_result['code'] != '200':
+                return Response(generate_result, status=200)
+            # 创建并处理出库任务
+            container_list = generate_result['data']
+            # 3. 立即发送第一个任务
+            OutboundService.create_initial_check_tasks(container_list,batch_id)
+            OutboundService.process_next_task()
+            return Response({"code": 200, "msg": "下发任务成功"}, status=200)
+        except Exception as e:
+            logger.error(f"任务生成失败: {str(e)}")
+            return Response({"code": 200, "msg": str(e)}, status=200)
+        
     def get_batch_count_by_boundlist(self,bound_list_id):
         try:
             bound_list_obj_all = OutBoundDetailModel.objects.filter(bound_list=bound_list_id).all()
@@ -1946,7 +2029,75 @@ class OutTaskViewSet(APIView):
             )
             .order_by('container_id', '-id')
         )
-        
+
+    def generate_location_by_check(self,batch_id,container_demand):
+        '''
+        根据抽检托盘数目,把相应的库位给到出库任务
+        '''
+        try:
+            return_data = []
+            # 获取已去重的托盘列表
+            container_qs = self.get_container_allocation(batch_id)
+            
+            # 构建托盘信息字典(自动去重)
+            container_map = {}
+            for cd in container_qs:
+                if cd.container_id in container_map:
+                    continue
+                # 获取有效库位信息
+                active_location = next(
+                    (link.location for link in cd.container.active_location 
+                        if link.is_active),
+                        None
+                    )
+                
+                container_map[cd.container_id] = {
+                    'detail': cd,
+                    'container': cd.container,
+                    'location': active_location
+                }
+                # 转换为排序列表
+            container_list = list(container_map.values())
+            
+            sorted_containers = sorted(
+                container_list,
+                key=lambda x: (
+                    self._get_goods_class_priority(x['detail'].goods_class),
+                    -(x['location'].c_number if x['location'] else 0),
+                    # -(x['location'].layer if x['location'] else 0),
+                    # x['location'].row if x['location'] else 0,
+                    # x['location'].col if x['location'] else 0
+                )
+            )
+
+            for item in sorted_containers:
+                if container_demand <= 0:
+                   break
+                # 获取可分配数量
+                # 记录分配信息
+                allocate_container = {
+                    "container_number": item['container'].id,
+                    "batch_id": batch_id,
+                    "location_code": item['location'].location_code if item['location'] else 'N/A',
+                    "allocate_qty": 0,
+                    "c_number": item['location'].c_number if item['location'] else 0
+                }
+                return_data.append(allocate_container)  
+                container_demand -= 1
+
+            # 降重 return_data,以container_number为key
+            return_data = list({v['container_number']: v for v in return_data}.values())
+            
+            # 排序
+            return_data = sorted(return_data, key=lambda x: -x['c_number'])
+
+            return {"code": "200", "msg": "Success", "data": return_data}
+
+        except Exception as e:
+            logger.error(f"出库任务生成失败: {str(e)}", exc_info=True)
+            return {"code": "500", "msg": str(e)}
+
+
     def generate_location_by_demand(self, batch_demand, bound_list_id):
         try:
             return_data = []

+ 0 - 1
templates/dist/spa/css/7.0f1870f4.css

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

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

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

Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 1
templates/dist/spa/index.html


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 1
templates/dist/spa/js/7.90c9f486.js


BIN
templates/dist/spa/js/7.90c9f486.js.gz


Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 0
templates/dist/spa/js/7.dabf770b.js


BIN
templates/dist/spa/js/7.dabf770b.js.gz


Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 1
templates/dist/spa/js/app.5064b60c.js


BIN
templates/dist/spa/js/app.4f087607.js.gz


BIN
templates/dist/spa/js/app.5064b60c.js.gz


Diff do ficheiro suprimidas por serem muito extensas
+ 1056 - 0
templates/src/pages/count/batch copy.vue


+ 113 - 8
templates/src/pages/count/batch.vue

@@ -124,14 +124,30 @@
                 round
                 :icon="props.row.expand ? 'remove' : 'ballot'"
                 @click="handle_row_expand(props.row)"
-              />
-              <q-tooltip
-                content-class="bg-amber text-black shadow-4"
-                :offset="[10, 10]"
-                content-style="font-size: 12px"
               >
-                {{ "查看批次下的托盘" }}
-              </q-tooltip>
+                <q-tooltip
+                  content-class="bg-amber text-black shadow-4"
+                  :offset="[10, 10]"
+                  content-style="font-size: 12px"
+                >
+                  {{ "查看批次下的托盘" }}
+                </q-tooltip>
+              </q-btn>
+
+              <q-btn
+                size="sm"
+                round
+                icon="zoom_out"
+                @click="handleCheck(props.row.id)"
+              >
+                <q-tooltip
+                  content-class="bg-amber text-black shadow-4"
+                  :offset="[10, 10]"
+                  content-style="font-size: 12px"
+                >
+                  {{ "质检抽检" }}
+                </q-tooltip>
+              </q-btn>
             </q-td>
 
             <q-td
@@ -221,6 +237,28 @@
                             </div>
                           </div>
                         </div>
+                        <div class="row">
+                          <div class="col">
+                            <div class="custom-title">
+                              {{ container.location_code }}
+                            </div>
+                          </div>
+                          <div class="col">
+                            <div class="custom-title">
+                              库位组:{{ container.location_group }}
+                            </div>
+                          </div>
+                          <div class="col">
+                            <div class="custom-title">
+                              库位号:{{ container.location_c_number }}
+                            </div>
+                          </div>
+                          <div class="col">
+                            <div class="custom-title">
+                              组容量: {{ container.location_max_capacity }}
+                            </div>
+                          </div>
+                        </div>
                       </span>
                     </template>
                   </q-timeline-entry>
@@ -415,6 +453,31 @@
       :key="select_container_number"
       @close="showInventoryDetails = false"
     />
+    <q-dialog v-model="checkDialog">
+      <q-card style="min-width: 350px">
+        <q-card-section>
+          <div class="text-h6">输入抽检托盘数量</div>
+        </q-card-section>
+
+        <q-card-section class="q-pt-none">
+          <q-input
+            dense
+            v-model="checkQuantity"
+            autofocus
+            label="抽检托盘数量"
+            type="number"
+            min="1"
+            :rules="[(val) => val > 0 || '必须输入大于0的数值']"
+            @keyup.enter="confirmCheck"
+          />
+        </q-card-section>
+
+        <q-card-actions align="right" class="text-primary">
+          <q-btn flat label="取消" v-close-popup />
+          <q-btn flat label="确定" @click="confirmCheck" />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
   </div>
 </template>
 <router-view />
@@ -574,7 +637,10 @@ export default {
       },
       activeSearchField: '',
       activeSearchLabel: '',
-      filterdata: {}
+      filterdata: {},
+      checkDialog: false,
+      checkQuantity: 0,
+      selectedBatchId: null
     }
   },
   computed: {
@@ -587,6 +653,45 @@ export default {
     }
   },
   methods: {
+    handleCheck (batchId) {
+      this.selectedBatchId = batchId
+      this.checkQuantity = 1
+      this.checkDialog = true
+    },
+
+    // 新增方法:确认抽检
+    confirmCheck () {
+      const _this = this
+      if (!this.selectedBatchId || this.checkQuantity <= 0) return
+
+      postauth('container/check_task/', {
+        batch_id: this.selectedBatchId,
+        container_demand: this.checkQuantity
+      })
+        .then((res) => {
+          _this.checkDialog = false
+          if (res.code === 200) {
+            _this.$q.notify({
+              message: `抽检任务已下发: ${res.msg}`,
+              color: 'positive'
+            })
+            // 刷新表格数据
+            _this.getSearchList()
+          } else {
+            _this.$q.notify({
+              message: `抽检失败: ${res.msg}`,
+              color: 'negative'
+            })
+          }
+        })
+        .catch((err) => {
+          _this.checkDialog = false
+          _this.$q.notify({
+            message: `请求错误: ${err.message}`,
+            color: 'negative'
+          })
+        })
+    },
     handleEditRow (row) {
       this.editForm = { ...row } // 复制当前行的数据到表单
       console.log(this.editForm)