فهرست منبع

完成出库库位释放

flower_mr 1 ماه پیش
والد
کامیت
9896b6d9b8

+ 1 - 1
.idea/goods_wms.iml

@@ -4,7 +4,7 @@
     <content url="file://$MODULE_DIR$">
       <excludeFolder url="file://$MODULE_DIR$/.venv" />
     </content>
-    <orderEntry type="jdk" jdkName="Python 3.10 (goods_wms)" jdkType="Python SDK" />
+    <orderEntry type="jdk" jdkName="Python 3.8 (greater_wms)" jdkType="Python SDK" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>
   <component name="PyDocumentationSettings">

+ 1 - 1
.idea/misc.xml

@@ -3,5 +3,5 @@
   <component name="Black">
     <option name="sdkName" value="Python 3.10 (goods_wms)" />
   </component>
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (goods_wms)" project-jdk-type="Python SDK" />
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (greater_wms)" project-jdk-type="Python SDK" />
 </project>

BIN
bin/__pycache__/serializers.cpython-38.pyc


BIN
bin/__pycache__/views.cpython-38.pyc


+ 12 - 1
bin/serializers.py

@@ -1,13 +1,24 @@
 from rest_framework import serializers
 
 from .models import DeviceModel,LocationModel,LocationGroupModel,LocationContainerLink,LocationChangeLog
-
+from container.models import ContainerListModel
+class ContainerSimpleSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = ContainerListModel
+        # fields = ['id', 'container_code', 'status']  # 按需选择字段
+        fields = '__all__'  # 或者展示全部字段
 
 class LocationSerializer(serializers.ModelSerializer):
     class Meta:
         model = LocationModel
         fields = '__all__'
 class LocationListSerializer(serializers.ModelSerializer):
+    current_containers = ContainerSimpleSerializer(
+        many=True,
+        source='active_containers',  
+        read_only=True
+    )
+
     class Meta:
         model = LocationModel
         fields = '__all__'

+ 174 - 4
bin/views.py

@@ -21,6 +21,7 @@ from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer
 # 以后添加模块时,只需要在这里添加即可
 from rest_framework.permissions import AllowAny
 from container.models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel
+from django.db.models import Prefetch
 import copy
 import json
 from collections import defaultdict
@@ -58,16 +59,25 @@ class locationViewSet(viewsets.ModelViewSet):
             return id
         except:
             return None
-
     def get_queryset(self):
         id = self.get_project()
+        # 预取激活的关联托盘(通过中间模型过滤)
+        prefetch_containers = Prefetch(
+            'current_containers',
+            queryset=ContainerListModel.objects.filter(
+                locationcontainerlink__is_active=True  # 确保中间模型字段名正确
+            ).distinct(),
+            to_attr='active_containers'  # 将结果存储到模型的 active_containers 属性
+        )
+
         if self.request.user:
             if id is None:
-                return LocationModel.objects.filter()
+                return LocationModel.objects.prefetch_related(prefetch_containers).all()
             else:
-                return LocationModel.objects.filter( id=id)
+                return LocationModel.objects.prefetch_related(prefetch_containers).filter(id=id)
         else:
             return LocationModel.objects.none()
+
     def get_serializer_class(self):
         if self.action == 'list':
             return LocationListSerializer
@@ -541,7 +551,7 @@ class LocationAllocation:
         if not container_detail:
             print (f"托盘 {container_code} 未组盘")
             logger.error(f"托盘 {container_code} 未组盘_from get_batch_status")
-            return None
+            return 0
 
         batch_status = container_detail.batch.status
         return batch_status
@@ -751,6 +761,135 @@ class LocationAllocation:
             logger.error(f"分配算法异常:{str(e)}")
             return None
 
+    @transaction.atomic
+    def get_empty_location_list(self, container_code):
+        """
+        智能库位分配核心算法 单个托盘组
+        :param container_code: 托盘码
+        :return: 库位类型分配方案
+        """
+        try:
+            batch = self.get_batch(container_code)
+            if not batch:
+                logger.error("批次信息获取失败")
+                return None
+
+            # 检查已有分配方案
+            existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
+            if existing_solution:
+                return existing_solution.layer_pre_type
+
+            # 获取关键参数
+            # total_pallets = self.get_pallet_count_by_batch(container_code)
+            layer_capacity = self.get_left_locationGroup_number_by_type()
+            current_pressure = self.get_current_pressure()
+
+            # 测试参数
+            total_pallets = 1
+            # layer_capacity = [{'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}, {'T1': 0, 'T2': 0, 'S4': 0, 'T4': 0, 'T5': 21}, {'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}]
+            # current_pressure = [1,0,0]
+            print(f"[1]托盘数目: {total_pallets}")
+            print(f"[2]层容量: {layer_capacity}")
+            # print(f"[3]当前压力: {current_pressure}")
+
+            # 定义库位容量表
+            LOCATION_CAPACITY = {'T1':1, 'T2':2, 'T4':4, 'S4':4, 'T5':5}
+            
+            def allocate(remain, path, pressure,real_pressure,layer_capacity_state, depth=0):
+                # 终止条件
+                if remain <= 0:
+                    return [path,real_pressure]
+                # 深拷贝当前层容量状态
+                new_layer_capacity = copy.deepcopy(layer_capacity_state)
+                # print(f"[2]当前剩余: {new_layer_capacity}")
+                # 压力平衡系数
+                balance_factor = 1.0 - (0.1 * min(depth, 5))
+                # 层选择策略
+                print (f"[3]当前压力: {pressure}")
+                layer_priority = sorted(
+                    [(0, pressure[0]), (1, pressure[1]), (2, pressure[2])],
+                    key=lambda x: (x[1] * balance_factor, x[0])
+                )
+
+                for layer, _ in layer_priority:
+                    # 生成候选库位类型(按效率和容量排序)
+                    # 排序键函数 :
+                    # min(x[1], remain) 计算当前库位类型的容量 c 和剩余数量 remain 中的较小值。
+                    # -min(x[1], remain) 和 -x[1] 都使用了负号,这意味着排序是按降序进行的。
+                    # 首先按 -min(x[1], remain) 排序,即优先选择容量与剩余数量更接近的库位类型。
+                    # 如果有多个库位类型的容量与剩余数量相同,则按 -x[1] 排序,即优先选择容量更大的库位类型。
+                    print(f"[4]当前层: {layer+1}, 剩余: {remain}, 容量状态: {new_layer_capacity[layer]}")
+                    candidates = sorted(
+                        [(t, c) for t, c in LOCATION_CAPACITY.items() 
+                         if new_layer_capacity[layer].get(t,0) > 0],
+                        key=lambda x: (abs(x[1]-remain), -x[1])
+                    )
+
+                    print(f"[4]候选库位类型: {candidates}")
+                    for loc_type, cap in candidates:
+                        # 更新容量状态
+                        updated_capacity = copy.deepcopy(new_layer_capacity)
+                        updated_capacity[layer][loc_type] -= 1  # 占用一个库位组
+                        # 允许适度空间浪费(当剩余<2时)
+                        # effective_cap = min(cap, remain) if (cap - remain) < 2 else cap
+                        effective_cap = min(cap, remain) 
+                        
+                        if effective_cap <= remain:
+                            new_remain = remain - effective_cap
+                            new_pressure = pressure.copy()
+                            for i in range(0, 3):
+                                new_pressure[i] -=1 if new_pressure[i] > 0 else 0
+                            new_pressure[layer] += effective_cap  # 按实际存放数计算压力,此时别的楼层压力可能降下来了
+                            real_pressure[layer] += effective_cap  # 实际压力
+                            result = allocate(
+                                new_remain, 
+                                path + [f"{layer+1}_{loc_type}"],
+                                new_pressure,
+                                real_pressure,
+                                updated_capacity,
+                                depth + 1
+                            )
+                            if result:
+                                print (f"[5]分配方案: {result}")
+                                return result
+
+                return None
+
+            # 执行分配
+            allocation = allocate(total_pallets, [], [current_pressure[0], current_pressure[1],current_pressure[2]],[current_pressure[0], current_pressure[1],current_pressure[2]], layer_capacity)
+            
+            if not allocation:
+                logger.error("无法生成有效分配方案")
+                return None
+
+            # 保存分配方案
+            allocation_json = self.divide_solution_by_layer(allocation[0])
+            print(f"[6]分配方案: {allocation_json}")
+            solution = alloction_pre(
+                batch_number=batch,
+                layer_pre_type =allocation_json
+            )
+            solution_pressure, created = base_location.objects.get_or_create(
+                id=1,
+                defaults={
+                    'layer1_pressure': 0,
+                    'layer2_pressure': 0, 
+                    'layer3_pressure': 0
+                }
+            )
+            solution_pressure.layer1_pressure = allocation[1][0]
+            solution_pressure.layer2_pressure = allocation[1][1]
+            solution_pressure.layer3_pressure = allocation[1][2]
+
+            solution.save()
+            solution_pressure.save()
+
+
+            return [loc.split('_')[1] for loc in allocation[0]]
+
+        except Exception as e:
+            logger.error(f"分配算法异常:{str(e)}")
+            return None
     def divide_solution_by_layer(self, data):
 
         # 统计所有存在的层级
@@ -915,7 +1054,38 @@ class LocationAllocation:
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
             return location_min_value
 
+        elif status == 0:
+            # 4. 获取空库位
+            print (f"空库位")
+            # 创建托盘批次
+            self.create_batch(container_code)
+            location_list = self.get_empty_location_list(container_code)
+            location_min_value = self.get_location_list_remainder(location_list,container_code)
+            print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
+            return location_min_value
+
+    def create_batch(self, container_code):
+        """创建托盘批次"""
+        try:
+            goods_code = "Con_group"
+            bound_month = datetime.datetime.now().strftime("%Y%m")
             
+            batch = BoundBatchModel.objects.create(
+
+
+    def release_location(self, location_code):
+            """释放库位并更新关联数据"""
+            try:
+                location = LocationModel.objects.get(location_code=location_code)
+                links = LocationContainerLink.objects.get(location=location, is_active=True)
+                print(f"释放库位: {location_code}, 关联容器: {links.container_id}")
+                # 解除关联并标记为非活跃
+                links.is_active = False
+                links.save()
+                return True
+            except Exception as e:
+                logger.error(f"释放库位失败: {str(e)}")
+                return False      
   
       
   

BIN
bound/__pycache__/views.cpython-38.pyc


+ 0 - 2
bound/views.py

@@ -9,8 +9,6 @@ from rest_framework.response import Response
 from rest_framework.exceptions import APIException
 from django.utils import timezone
 
-
-
 from .models import BoundListModel, BoundDetailModel,BoundBatchModel, BatchLogModel, OutBatchModel,OutBoundDetailModel
 # from .files import FileListRenderCN, FileDetailRenderCN
 

BIN
container/__pycache__/models.cpython-38.pyc


BIN
container/__pycache__/views.cpython-38.pyc


+ 18 - 0
container/migrations/0010_containeroperationmodel_bound_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2025-05-05 14:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0009_containerwcsmodel_working_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='containeroperationmodel',
+            name='bound_id',
+            field=models.IntegerField(blank=True, null=True, verbose_name='出库申请'),
+        ),
+    ]

+ 18 - 0
container/migrations/0011_containerdetailmodel_goods_out_qty.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2025-05-05 15:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0010_containeroperationmodel_bound_id'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='containerdetailmodel',
+            name='goods_out_qty',
+            field=models.IntegerField(default=0, verbose_name='出库数量'),
+        ),
+    ]

BIN
container/migrations/__pycache__/0010_containeroperationmodel_bound_id.cpython-38.pyc


BIN
container/migrations/__pycache__/0011_containerdetailmodel_goods_out_qty.cpython-38.pyc


+ 5 - 1
container/models.py

@@ -39,6 +39,7 @@ class ContainerDetailModel(models.Model):
     goods_code = models.CharField(max_length=50, verbose_name='货品编码')
     goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
     goods_qty = models.IntegerField(verbose_name='数量')
+    goods_out_qty = models.IntegerField(verbose_name='出库数量', default=0)
     goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
     status = models.IntegerField(choices=BATCH_STATUS,default=0, verbose_name='状态')           # 0: 未使用 1: 使用中 2: 已出库
     creater = models.CharField(max_length=50, verbose_name='创建人')
@@ -51,6 +52,8 @@ class ContainerDetailModel(models.Model):
         verbose_name = 'ContainerDetail'
         verbose_name_plural = "ContainerDetail"
         ordering = ['-id']
+    
+
 
 # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录
 class ContainerOperationModel(models.Model):
@@ -63,6 +66,7 @@ class ContainerOperationModel(models.Model):
     month = models.IntegerField(verbose_name='月份')
     container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations')
     operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型')
+    bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True)
 
     batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次')
     goods_code = models.CharField(max_length=50, verbose_name='货品编码')
@@ -96,7 +100,7 @@ class ContainerWCSModel(models.Model):
     )
     taskid = models.CharField(max_length=50, verbose_name='任务ID')
     batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次')
-    batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' ,  null=True, blank=True  )
+    batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True )
     bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单')
     sequence = models.BigIntegerField(verbose_name='任务顺序')
     priority = models.IntegerField(default=100, verbose_name='优先级')

+ 218 - 67
container/views.py

@@ -1,15 +1,12 @@
 from rest_framework import viewsets
 from utils.page import MyPageNumberPagination
-from utils.datasolve import sumOfList, transportation_calculate
-from utils.md5 import Md5
+
 from rest_framework.filters import OrderingFilter
 from django_filters.rest_framework import DjangoFilterBackend
 from rest_framework.response import Response
-from rest_framework.exceptions import APIException
+
 from django.utils import timezone
 import requests
-import json
-from django.conf import settings
 
 from django.db import transaction
 import logging
@@ -17,17 +14,15 @@ from rest_framework import status
 from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,ContainerWCSModel,TaskModel
 from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel,OutBoundDetailModel
 from bin.views import LocationAllocation,base_location
-from bin.models import LocationModel,LocationContainerLink
-# from .files import FileListRenderCN, FileDetailRenderCN
+from bin.models import LocationModel,LocationContainerLink,LocationGroupModel
+from bound.models import BoundBatchModel
 
 from .serializers import ContainerDetailGetSerializer,ContainerDetailPostSerializer
 from .serializers import ContainerListGetSerializer,ContainerListPostSerializer
 from .serializers import ContainerOperationGetSerializer,ContainerOperationPostSerializer
 from .serializers import TaskGetSerializer,TaskPostSerializer
 from .filter import ContainerDetailFilter,ContainerListFilter,ContainerOperationFilter,TaskFilter
-# 以后添加模
-from warehouse.models import ListModel as warehouse
-from staff.models import ListModel as staff
+
 from rest_framework.permissions import AllowAny
 import threading
 from django.db import close_old_connections
@@ -132,7 +127,7 @@ class TaskViewSet(viewsets.ModelViewSet):
             if id is None:
                 return TaskModel.objects.filter()
             else:
-                return TaskModel.objects.filter( id=id)
+                return TaskModel.objects.filter(id=id)
         else:
             return TaskModel.objects.none()
 
@@ -212,14 +207,12 @@ class TaskRollbackMixin:
             
             # 恢复容器详细状态为初始状态(假设原状态为1)
             allocator.update_container_detail_status(container_code, 1)
-            
+        
             # 恢复容器的目标位置为当前所在位置
             container_obj.target_location = task.current_location
             container_obj.save()
-
             # ==================== 删除任务记录 ====================
             task.delete()
-
             # ==================== 其他关联清理 ====================
             # 如果有其他关联数据(如inport_update_task的操作),在此处添加清理逻辑
 
@@ -285,7 +278,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             serializer.save()
              
             # 检查是否已在目标位置
-            if current_location == str(container_obj.target_location):
+            if current_location == str(container_obj.target_location) and current_location!= '203' and current_location!= '103':
                 logger.info(f"托盘 {container} 已在目标位置")
                 data_return = {
                     'code': '200',
@@ -295,8 +288,9 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             else:
                 current_task = ContainerWCSModel.objects.filter(
                     container=container, 
-                    tasktype='inbound'
-                ).first()
+                    tasktype='inbound',
+                 
+                ).exclude(status=300).first()
 
                 if current_task:
                     
@@ -412,19 +406,20 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         # 生成唯一递增的 taskid
         last_task = ContainerWCSModel.objects.filter(
             month=data_tosave['month'],
-          
-        ).order_by('-taskid').first()
+        ).order_by('-tasknumber').first()
 
         if last_task:
-            last_id = int(last_task.taskid.split('-')[-1])
-            new_id = f"{last_id + 1:05}"
+            
+            number_id = last_task.tasknumber + 1
+            new_id = f"{number_id:05d}"
         else:
             new_id = "00001"
-
+            number_id = f"{data_tosave['month']}{new_id}"
+        
         data_tosave['taskid'] = f"inbound-{data_tosave['month']}-{new_id}"
         logger.info(f"生成入库任务: {data_tosave['taskid']}")
         # 每月生成唯一递增的 taskNumber
-        data_tosave['tasknumber'] = f"{data_tosave['month']}{new_id}"
+        data_tosave['tasknumber'] = number_id
         ContainerWCSModel.objects.create(**data_tosave)
 
     def update_container_wcs(self, request, *args, **kwargs):
@@ -439,12 +434,26 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
 
             # 更新容器数据
             if not self.update_container_data(container_obj, data):
-                return Response({'code': '400', 'message': '数据更新失败', 'data': data}, 
-                            status=status.HTTP_400_BAD_REQUEST)
+                return Response(
+                    {'code': '400', 'message': '数据更新失败', 'data': data},
+                    status=status.HTTP_400_BAD_REQUEST
+                )
 
             # 处理位置逻辑
+            task = ContainerWCSModel.objects.filter(
+                container=container_obj.container_code,
+                tasktype='inbound'
+                ).first()
             if self.is_already_at_target(container_obj, data.get('current_location')):
                 return self.handle_target_reached(container_obj, data)
+            elif task:
+               
+                data_return = {
+                    'code': '200',
+                    'message': '任务已存在,重新下发',
+                    'data': task.to_dict()
+                }
+                return Response(data_return, status=status.HTTP_200_OK)
             else:
                 return self.handle_new_allocation(container_obj, data)
 
@@ -494,7 +503,11 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         if task and task.tasktype == 'inbound':
             self.update_storage_system(container_obj)
         
-        if task and task.tasktype == 'outbound':
+        if task and task.tasktype == 'outbound' 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({
@@ -518,6 +531,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         task = ContainerWCSModel.objects.filter(tasknumber=taskNumber).first()
         if task:
             task.status = 300
+            task.message = '任务已完成'
             task.working = 0
             task.save()
 
@@ -651,6 +665,77 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             )
          
 
+    def handle_outbound_completion(self, container_obj, task):
+        """处理出库完成后的库位释放和状态更新"""
+        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
+  
+
+            # 事务确保原子性
+            with transaction.atomic():
+                # 解除库位与托盘的关联
+                if not allocator.release_location(location_code):
+                    raise Exception("解除库位关联失败")
+
+                # 更新库位状态为可用
+                if not allocator.update_location_status(location_code, 'available'):
+                    raise Exception("库位状态更新失败")
+
+                # 更新库位组的统计信息
+                self.handle_group_location_status(location_code, location.location_group)
+
+                # 更新容器状态为已出库(假设状态3表示已出库)
+                container_obj.status = 3
+                container_obj.save()
+
+   
+
+            return True
+        except Exception as e:
+            logger.error(f"出库完成处理失败: {str(e)}")
+            return False
+
+    def handle_group_location_status(self,location_code,location_group):
+        """
+        处理库位组和库位的关联关系
+        :param location_code: 库位编码
+        :param location_group: 库位组编码
+        :return:
+        """
+        # 1. 获取库位空闲状态的库位数目
+        location_obj_number = LocationModel.objects.filter(
+            location_group=location_group,
+            status='available'
+        ).all().count()
+        # 2. 获取库位组对象
+        logger.info(f"库位组 {location_group} 下的库位数目:{location_obj_number}")
+        # 1. 获取库位和库位组的关联关系
+        location_group_obj = LocationGroupModel.objects.filter(
+            group_code=location_group
+        ).first()
+        if not location_group_obj:
+            logger.info(f"库位组 {location_group} 不存在")
+            return None
+        else:
+            if location_obj_number == 0:
+                # 库位组库位已满,更新库位组状态为full
+                location_group_obj.status = 'full'
+                location_group_obj.save()
+            elif location_obj_number < location_group_obj.max_capacity:
+                location_group_obj.status = 'occupied'
+                location_group_obj.save()
+            else:
+                location_group_obj.status = 'available'
+                location_group_obj.current_batch = ''
+                location_group_obj.current_goods_code = ''
+                location_group_obj.save()
+
 
 # PDA组盘入库 将扫描到的托盘编码和批次信息保存到数据库
 # 1. 先查询托盘对象,如果不存在,则创建托盘对象
@@ -926,7 +1011,6 @@ class ContainerOperateViewSet(viewsets.ModelViewSet):
         headers = self.get_success_headers(serializer.data)
         return Response(serializer.data, status=200, headers=headers)
 
-
 class OutboundService:
     @staticmethod
     def generate_task_id():
@@ -939,41 +1023,8 @@ class OutboundService:
         sequence = last_task.sequence + 1 if last_task else 1
         return f"outbound-{month}-{sequence:05d}"
 
-    # @staticmethod
-    # def send_task_to_wcs(task):
-    #     """发送任务到WCS"""
-    #     send_data = {
-    #         "taskid": task.taskid,
-    #         "container": task.container,
-    #         "current_location": task.current_location,
-    #         "target_location": task.target_location,
-    #         "tasktype": task.tasktype,
-    #         "month": task.month,
-    #         "message": task.message,
-    #         "status": task.status,
-    #         "taskNumber": task.tasknumber
-    #     }
-    #     try:
-    #         requests.post("http://127.0.0.1:8008/container/batch/", json=send_data, timeout=10)
-    #         response = requests.post("http://192.168.18.67:1616//wcs/WebApi/getOutTask", json=send_data, timeout=10)
-    #         if response.status_code == 200:
-    #             task.status = 200
-    #             task.save()/wcs/WebApi/getOutTask
-    #             logger.info(f"任务 {task.taskid} 已发送")
-    #             return True
-    #         else:
-    #             logger.error(f"WCS返回错误: {response.text}")
-    #             task.status = 400
-    #             task.save()
-    #             return False
-    #     except Exception as e:
-    #         logger.error(f"发送失败: {str(e)}")
-     
-
-    #    return False
-
     @staticmethod
-    def send_task_to_wcs(task):
+    def send_task_to_wcs(task):             
         """异步发送任务到WCS(非阻塞版本)"""
         # 提取任务关键数据用于线程(避免直接传递ORM对象)
         task_data = {
@@ -996,9 +1047,7 @@ class OutboundService:
             }
             }
         }
-        container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
-        container_obj.target_location = task.target_location
-        container_obj.save()
+        
         # 创建并启动线程
         thread = threading.Thread(
             target=OutboundService._async_send_handler,
@@ -1054,7 +1103,6 @@ class OutboundService:
             current_WCS = ContainerWCSModel.objects.filter(tasktype='outbound',bound_list_id = bound_list_id).first()
             if current_WCS:
                 logger.error(f"当前{bound_list_id}已有出库任务")
-          
                 return False
             tasks = []
             start_sequence = ContainerWCSModel.objects.filter(tasktype='outbound').count() + 1
@@ -1089,6 +1137,9 @@ class OutboundService:
                 )
                 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)} 个初始任务")
 
@@ -1138,8 +1189,28 @@ class OutboundService:
         if not next_task:
             logger.info("没有待处理任务")
             return
+        allocator = LocationAllocation()
+        OutboundService.perform_initial_allocation(allocator, next_task.current_location)
         OutboundService.send_task_to_wcs(next_task)
 
+    def perform_initial_allocation(allocator, location):
+        """执行初始库位分配操作"""
+        location_row = location.split('-')[1]
+        location_col = location.split('-')[2]
+        location_layer = location.split('-')[3]
+        location_code = LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first().location_code
+        if not location_code:
+            logger.error(f"未找到库位: {location}")
+        operations = [
+            (allocator.update_location_status,location_code, 'reserved'),
+            (allocator.update_location_group_status,location_code)
+        ]
+        
+        for func, *args in operations:
+            if not func(*args):
+                logger.error(f"分配操作失败: {func.__name__}")
+                return False
+        return True
 
 class OutTaskViewSet(viewsets.ModelViewSet):
 
@@ -1163,12 +1234,18 @@ class OutTaskViewSet(viewsets.ModelViewSet):
             
             logger.info(f"出库批次数量: {batch_count}")
             # 获取需要出库的托盘列表
-            generate_result = self.generate_location_by_demand(batch_count)
+            generate_result = self.generate_location_by_demand(batch_count,bound_list_id)
             if generate_result['code'] != '200':
+                current_WCS = ContainerWCSModel.objects.filter(tasktype='outbound',bound_list_id = bound_list_id).first()
+                if current_WCS:
+                    OutboundService.process_next_task()
+                    return Response({"code": "200", "msg": "Success 再次发送任务"}, status=200)
+
                 return Response(generate_result, status=500)
             container_list = generate_result['data']
             
             logger.info(f"生成出库任务: {container_list}")
+
             #  2. 生成初始任务
             OutboundService.create_initial_tasks(container_list,bound_list_id)
             
@@ -1180,6 +1257,8 @@ class OutTaskViewSet(viewsets.ModelViewSet):
             logger.error(f"任务生成失败: {str(e)}")
             return Response({"code": "500", "msg": str(e)}, status=500)
 
+
+
     # 获取出库需求
     def get_batch_count_by_boundlist(self,bound_list_id):
         try:
@@ -1251,13 +1330,14 @@ class OutTaskViewSet(viewsets.ModelViewSet):
             logger.error(f"查询{status}状态的批次数量失败: {str(e)}")
             return {}
 
-    def generate_location_by_demand(self,demand_list):
+    def generate_location_by_demand(self,demand_list,bound_list_id):
         # demand_list {1: 25, 2: 17}
         try:
             return_location =[]
             for demand_id, demand_qty in demand_list.items():
                 container_list = self.get_location_by_status_and_batch(2, demand_id)
                 if not container_list:
+
                     return {"code": "500", "msg": f"批次 {demand_id} 不存在"}
                 container_id_list = container_list.keys()
                 container_order = self.get_order_by_batch(container_id_list,demand_id) 
@@ -1273,22 +1353,93 @@ class OutTaskViewSet(viewsets.ModelViewSet):
                 current_qty = 0
                 for container in order:
                     container_detail_obj = ContainerDetailModel.objects.filter(container_id=container['container_number'],batch_id=demand_id,status=2).all()
+                    container_obj = ContainerListModel.objects.filter(container_code=container['container_number']).first()
+                    if not container_obj:
+                        return {"code": "500", "msg": f"托盘 {container['container_number']} 不存在"}
                     if not container_detail_obj:
                         return {"code": "500", "msg": f"托盘上无该批次,请检查{container['container_number']} 不存在"}
                     goods_qty = 0 
                     for obj in container_detail_obj:
                         goods_qty += obj.goods_qty
                     if current_qty < demand_qty:
+                        now_qty = current_qty
                         current_qty += goods_qty
                         return_location.append(container)
+                        logger.info(f"批次 {demand_id} 托盘 {container['container_number']} 当前数量 {current_qty}")
+                        self.create_or_update_container_operation(container_obj,demand_id,bound_list_id,203,min(demand_qty-now_qty,goods_qty),min(demand_qty-now_qty,goods_qty))
+                        self.update_container_detail_out_qty(container_obj,demand_id)
                     else:
                         break
             return {"code": "200", "msg": "Success", "data": return_location}
         except Exception as e:
             return {"code": "500", "msg": str(e)}
 
+    def create_or_update_container_operation(self,container_obj,batch_id,bound_id,to_location,goods_qty,goods_weight):
+        try:
+            container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,bound_id=bound_id,operation_type="outbound").first()
+            
+            if container_operation_obj:
+                logger.info(f"[0]查询出库任务: {container_operation_obj.operation_type} ")
+                logger.info(f"更新出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
+                container_operation_obj.to_location = to_location
+                container_operation_obj.goods_qty = goods_qty
+                container_operation_obj.goods_weight = goods_weight
+                container_operation_obj.save()
+       
+            else:
+                logger.info(f"创建出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
+                batch = BoundBatchModel.objects.filter(id=batch_id).first()
+                if not batch:
+                    return {"code": "500", "msg": f"批次 {batch_id} 不存在"}
+                ContainerOperationModel.objects.create(
+                    month = int(timezone.now().strftime("%Y%m")),
+                    container = container_obj,
+                    goods_code = batch.goods_code,
+                    goods_desc = batch.goods_desc,
+                    operation_type ="outbound",
+                    batch_id = batch_id,
+                    bound_id = bound_id,
+                    goods_qty = goods_qty,
+                    goods_weight = goods_weight,
+                    from_location = container_obj.current_location,
+                    to_location= to_location,
+                    timestamp=timezone.now(),
+                    operator="WMS",
+                    memo=f"出库需求: {bound_id}, 批次: {batch_id}, 数量: {goods_qty}"
+                )
+         
+                return {"code": "200", "msg": "Success"}
+        except Exception as e:
+            return {"code": "500", "msg": str(e)}
     
 
+    def update_container_detail_out_qty(self,container_obj,batch_id):
+        try:
+            logger.info(f"[1]更新托盘出库数量: {container_obj.container_code} 批次 {batch_id} ")
+            container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,operation_type="outbound").all()
+            if not container_operation_obj:
+                logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务")
+                return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务"}
+            container_detail_obj = ContainerDetailModel.objects.filter(container=container_obj,batch_id=batch_id,status=2).first()
+            if not container_detail_obj:
+                logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息")
+                return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息"}
+            out_qty = 0
+            for obj in container_operation_obj:
+                out_qty += obj.goods_qty
+                if out_qty >= container_detail_obj.goods_qty:
+                    out_qty = container_detail_obj.goods_qty
+                    container_detail_obj.status = 3
+                    break
+                if out_qty == 0:
+                    logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量")
+                    return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量"}
+            container_detail_obj.goods_out_qty = out_qty
+            container_detail_obj.save()
+            return {"code": "200", "msg": "Success"}
+
+        except Exception as e:
+            return {"code": "500", "msg": str(e)}
 
 class BatchViewSet(viewsets.ModelViewSet):
     authentication_classes = []  # 禁用所有认证类

BIN
db.sqlite3


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 8 - 0
logs/boundBill.log


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1364 - 0
logs/error.log


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1368 - 0
logs/server.log


+ 5 - 3
templates/src/components/goodscard.vue

@@ -379,9 +379,7 @@ export default {
                 .then(res1 => {
                     console.log(res1)
                     console.log(res1.current_containers)
-                    _this.container_id = res1.current_containers[0]
-                    console.log("当前托盘ID",_this.container_id)
-                    if(_this.container_id == undefined)
+                    if(res1.current_containers.length == 0)
                     {
                         console.log("当前托盘ID为空")
                         _this.$q.notify({
@@ -391,6 +389,10 @@ export default {
                         });
                         return
                     }
+                    _this.container_id = res1.current_containers[0].id
+                    console.log("当前托盘ID",_this.container_id)
+                    console.log("当前托盘ID长度",res1.current_containers.length)
+               
                     getauth(_this.pathnamecontainer + '?container='+_this.container_id)
                     .then(res => {
                         var results = res.results[0]