Bladeren bron

托盘出库

flower_bs 3 dagen geleden
bovenliggende
commit
5857049451
100 gewijzigde bestanden met toevoegingen van 493 en 104 verwijderingen
  1. 3 0
      backup/views.py
  2. 9 6
      bin/algorithms.py
  3. 48 0
      bin/services.py
  4. 4 1
      bin/views.py
  5. 2 0
      container/filter.py
  6. 23 0
      container/migrations/0032_containerlistmodel_batch_info_and_more.py
  7. 89 12
      container/models.py
  8. 4 0
      container/urls.py
  9. 132 0
      container/utils.py
  10. 126 12
      container/views.py
  11. 7 29
      data_base/test_erp copy.py
  12. 1 0
      templates/dist/spa/css/30.326b06df.css
  13. 0 1
      templates/dist/spa/css/30.e2633675.css
  14. 1 0
      templates/dist/spa/css/32.6dbcf0b0.css
  15. 0 1
      templates/dist/spa/css/32.cdb5039a.css
  16. 0 1
      templates/dist/spa/css/33.4cad196b.css
  17. 1 0
      templates/dist/spa/css/33.efd62105.css
  18. 1 0
      templates/dist/spa/css/34.12670fd1.css
  19. 0 0
      templates/dist/spa/css/35.9478c981.css
  20. 0 0
      templates/dist/spa/css/36.c4652654.css
  21. 0 0
      templates/dist/spa/css/37.7a23b7fb.css
  22. 0 0
      templates/dist/spa/css/38.0faa4aeb.css
  23. 1 1
      templates/dist/spa/index.html
  24. 1 1
      templates/dist/spa/js/30.a5bfd137.js
  25. BIN
      templates/dist/spa/js/30.8c350f2a.js.gz
  26. BIN
      templates/dist/spa/js/30.a5bfd137.js.gz
  27. 1 0
      templates/dist/spa/js/32.a02b8cc4.js
  28. BIN
      templates/dist/spa/js/32.a02b8cc4.js.gz
  29. 0 1
      templates/dist/spa/js/32.c084dcec.js
  30. BIN
      templates/dist/spa/js/32.c084dcec.js.gz
  31. 1 0
      templates/dist/spa/js/33.478a11fb.js
  32. BIN
      templates/dist/spa/js/33.478a11fb.js.gz
  33. 0 1
      templates/dist/spa/js/33.95204aa3.js
  34. BIN
      templates/dist/spa/js/33.95204aa3.js.gz
  35. 1 0
      templates/dist/spa/js/34.e7f364ee.js
  36. BIN
      templates/dist/spa/js/34.e7f364ee.js.gz
  37. 1 1
      templates/dist/spa/js/34.a8782958.js
  38. 1 1
      templates/dist/spa/js/35.dd905e5f.js
  39. 1 1
      templates/dist/spa/js/36.b90cc851.js
  40. 1 1
      templates/dist/spa/js/37.5984e1b1.js
  41. 1 1
      templates/dist/spa/js/38.fa43a096.js
  42. BIN
      templates/dist/spa/js/38.fa43a096.js.gz
  43. 1 1
      templates/dist/spa/js/39.415db622.js
  44. BIN
      templates/dist/spa/js/41.1e952a05.js.gz
  45. 1 1
      templates/dist/spa/js/40.5563a56f.js
  46. 1 1
      templates/dist/spa/js/41.1e952a05.js
  47. BIN
      templates/dist/spa/js/42.4076e567.js.gz
  48. 1 1
      templates/dist/spa/js/42.96a25cc1.js
  49. 1 1
      templates/dist/spa/js/43.d3fddc8e.js
  50. BIN
      templates/dist/spa/js/43.d3fddc8e.js.gz
  51. BIN
      templates/dist/spa/js/44.7f6d36cb.js.gz
  52. 1 1
      templates/dist/spa/js/44.7f6d36cb.js
  53. BIN
      templates/dist/spa/js/45.7cff4886.js.gz
  54. BIN
      templates/dist/spa/js/45.b3a8ad9b.js.gz
  55. 1 1
      templates/dist/spa/js/45.b3a8ad9b.js
  56. BIN
      templates/dist/spa/js/46.2911339f.js.gz
  57. 1 1
      templates/dist/spa/js/46.67cb56c8.js
  58. BIN
      templates/dist/spa/js/46.67cb56c8.js.gz
  59. BIN
      templates/dist/spa/js/47.89908208.js.gz
  60. 1 1
      templates/dist/spa/js/47.89908208.js
  61. BIN
      templates/dist/spa/js/48.1a3b66a6.js.gz
  62. BIN
      templates/dist/spa/js/48.9be46088.js.gz
  63. BIN
      templates/dist/spa/js/49.30dac62f.js.gz
  64. 1 1
      templates/dist/spa/js/48.9be46088.js
  65. BIN
      templates/dist/spa/js/49.6e6b3353.js.gz
  66. BIN
      templates/dist/spa/js/50.2ffd5e54.js.gz
  67. 1 1
      templates/dist/spa/js/49.30dac62f.js
  68. BIN
      templates/dist/spa/js/50.8d009ac4.js.gz
  69. 1 1
      templates/dist/spa/js/50.2ffd5e54.js
  70. BIN
      templates/dist/spa/js/51.36d36fea.js.gz
  71. BIN
      templates/dist/spa/js/51.b7a98797.js.gz
  72. 1 1
      templates/dist/spa/js/51.b7a98797.js
  73. BIN
      templates/dist/spa/js/52.f0a6b200.js.gz
  74. BIN
      templates/dist/spa/js/52.f6d23b7b.js.gz
  75. 1 1
      templates/dist/spa/js/52.f6d23b7b.js
  76. BIN
      templates/dist/spa/js/53.a2f048ca.js.gz
  77. 1 1
      templates/dist/spa/js/53.58546587.js
  78. BIN
      templates/dist/spa/js/53.58546587.js.gz
  79. BIN
      templates/dist/spa/js/54.2ec8c91b.js.gz
  80. 1 1
      templates/dist/spa/js/54.2ec8c91b.js
  81. BIN
      templates/dist/spa/js/55.a7b0daf2.js.gz
  82. 1 1
      templates/dist/spa/js/55.f8c04687.js
  83. 1 1
      templates/dist/spa/js/56.319ef58f.js
  84. 1 1
      templates/dist/spa/js/57.f3a7ab13.js
  85. 1 1
      templates/dist/spa/js/58.011743c9.js
  86. 1 1
      templates/dist/spa/js/59.c30d655c.js
  87. 1 1
      templates/dist/spa/js/60.daf0ef89.js
  88. 1 1
      templates/dist/spa/js/61.cef6396e.js
  89. 1 1
      templates/dist/spa/js/62.fbae3862.js
  90. BIN
      templates/dist/spa/js/63.d20f3e27.js.gz
  91. 1 1
      templates/dist/spa/js/63.d20f3e27.js
  92. BIN
      templates/dist/spa/js/64.4f926b47.js.gz
  93. 1 1
      templates/dist/spa/js/64.01581ff6.js
  94. 1 1
      templates/dist/spa/js/65.f1d63ed9.js
  95. 1 1
      templates/dist/spa/js/66.ed1f1f6f.js
  96. 1 1
      templates/dist/spa/js/67.5921df2a.js
  97. 1 1
      templates/dist/spa/js/68.d071ca11.js
  98. 1 1
      templates/dist/spa/js/69.b6c41a55.js
  99. 1 1
      templates/dist/spa/js/70.1b180587.js
  100. 0 0
      templates/dist/spa/js/71.af316543.js.gz

+ 3 - 0
backup/views.py

@@ -17,6 +17,9 @@ def scheduled_backup():
     try:
         backup_path = backup_database()
         logger.info(f"定时备份完成: {backup_path}")
+        from container.utils import update_container_categories_task
+        update_container_categories_task()
+        logger.info(f"定时更新托盘分类完成")
     except Exception as e:
         logger.error(f"定时备份失败: {str(e)}")
 

+ 9 - 6
bin/algorithms.py

@@ -102,12 +102,15 @@ class AllocationAlgorithm:
         return locations if locations else None
       
     def generate_WCS_location(location_list_cnumber):
-        allocation_target_location = (
-                            location_list_cnumber.warehouse_code + '-' 
-                            + f"{int(location_list_cnumber.row):02d}" + '-'  
-                            + f"{int(location_list_cnumber.col):02d}" + '-' 
-                            + f"{int(location_list_cnumber.layer):02d}" 
-                        )
+        if location_list_cnumber not in ['203', '103']:
+            allocation_target_location = (
+                                location_list_cnumber.warehouse_code + '-' 
+                                + f"{int(location_list_cnumber.row):02d}" + '-'  
+                                + f"{int(location_list_cnumber.col):02d}" + '-' 
+                                + f"{int(location_list_cnumber.layer):02d}" 
+                            )
+        else:
+            allocation_target_location = location_list_cnumber
         return allocation_target_location
     
     def generate_WMS_location(location):

+ 48 - 0
bin/services.py

@@ -227,6 +227,54 @@ class AllocationService:
         allocation_target_location = AllocationAlgorithm.generate_WCS_location(location_min_value)
   
         return location_min_value,allocation_target_location, batch_info
+    
+    @classmethod
+    def _out_allocation(cls, start_point,target_point,container_code):
+        batch_info = LocationQueries.get_batch_info(container_code)
+        start_location = AllocationAlgorithm.generate_WMS_location(start_point)
+        
+        if not start_location :
+            raise ValueError("无效的起始位置")
+
+        
+
+        print(f"[1] 出库任务安排从第{start_location.c_number}个库位:{start_location} 到 {target_point}")
+        start_location.status = 'available'
+        start_location.save()
+    
+
+        print(f"[2] 库位状态更新成功!")
+
+        update_start_location_group_status= LocationUpdates.update_location_group_status(start_location.location_code)    
+        if  not update_start_location_group_status:
+            raise ValueError("[3] 库位组状态更新失败")
+        print(f"[3] 库位组状态更新成功!")
+
+        # update_batch_status = LocationUpdates.update_batch_status(container_code, 2)
+        # if not update_batch_status:
+        #     raise ValueError("[4] 批次状态更新失败")
+        # print(f"[4] 批次状态更新成功!")
+
+        # update_location_group_batch = LocationUpdates.update_location_group_batch(location_min_value,batch_info['number'])
+        # if not update_location_group_batch:
+        #     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_start_location_container_link:
+            raise ValueError("[6] 托盘与库位关联失败")
+        print(f"[6] 托盘与库位关联成功!")
+ 
+        update_location_container_detail = LocationUpdates.update_container_detail_status(container_code, 2)
+        if not update_location_container_detail:
+            raise ValueError("[7] 托盘详情状态更新失败")
+        print(f"[7] 托盘详情状态更新成功!")
+
+        allocation_target_location = AllocationAlgorithm.generate_WCS_location(target_point)
+  
+        return allocation_target_location, batch_info
+
 
     @classmethod
     def _subsequent_allocation(cls, container_code, batch_info, start_point):

+ 4 - 1
bin/views.py

@@ -1071,7 +1071,10 @@ class LocationAllocation:
             """释放库位并更新关联数据"""
             try:
                 location = LocationModel.objects.get(location_code=location_code)
-                links = LocationContainerLink.objects.get(location=location, is_active=True)
+                links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
+                if not links:
+                    logger.error(f"库位{location_code}未关联托盘")
+                    return True
                 print(f"释放库位: {location_code}, 关联托盘: {links.container_id}")
                 # 解除关联并标记为非活跃
                 links.is_active = False

+ 2 - 0
container/filter.py

@@ -56,6 +56,8 @@ class ContainerListFilter(FilterSet):
             "current_location": ['exact', 'icontains'],
             "status": ['exact', 'icontains'],
             "last_operation": ['exact', 'icontains'],
+            "category": ['exact', 'icontains'],
+            
         }
 
 class ContainerDetailFilter(FilterSet):

+ 23 - 0
container/migrations/0032_containerlistmodel_batch_info_and_more.py

@@ -0,0 +1,23 @@
+# Generated by Django 4.1.2 on 2025-09-12 16:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0031_batchlogmodel_goods_std_batchlogmodel_goods_unit_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='containerlistmodel',
+            name='batch_info',
+            field=models.JSONField(default=list, help_text='格式: [{"batch_id":1,"check_status":"合格","bound_number":"B001","qty":100}]', verbose_name='批次信息缓存'),
+        ),
+        migrations.AddField(
+            model_name='containerlistmodel',
+            name='category',
+            field=models.IntegerField(choices=[(0, '整盘'), (1, '散盘'), (2, '空盘在库'), (3, '空盘不在库')], default=3, verbose_name='托盘分类'),
+        ),
+    ]

+ 89 - 12
container/models.py

@@ -7,9 +7,56 @@ from decimal import Decimal
 from django.db.models import Sum
 from datetime import timedelta 
 from django.db.models import Q
+from django.db.models import Sum, F
 import logging
-
 logger = logging.getLogger(__name__)
+
+import threading
+from django.db import close_old_connections
+from concurrent.futures import ThreadPoolExecutor
+import queue
+
+# 创建线程池执行器和任务队列
+log_processing_executor = ThreadPoolExecutor(max_workers=4)
+log_processing_queue = queue.Queue()
+
+def async_worker():
+    """后台线程处理日志任务"""
+    while True:
+        try:
+            # 从队列获取任务
+            logs, created = log_processing_queue.get(timeout=30)
+            close_old_connections()  # 确保每次处理前关闭旧连接
+            
+            # 处理批量日志
+            processed_logs = []
+            for log in logs:
+                try:
+                    aggregate_to_batch_log(log)
+                    count_day_in_out(log)
+                    processed_logs.append(log.id)
+                except Exception as e:
+                    logger.error(f"处理日志时出错: {e}", exc_info=True)
+            
+            # 如果有批次日志需要触发material history更新
+            if created:
+                from bound.models import MaterialStatistics
+                # 这里可以添加批量更新MaterialStatistics的逻辑
+                # ...
+                
+            logger.info(f"已处理 {len(processed_logs)} 条日志")
+            log_processing_queue.task_done()
+            
+        except queue.Empty:
+            # 30秒无任务则进入休眠
+            continue
+        except Exception as e:
+            logger.error(f"后台线程异常: {e}", exc_info=True)
+
+# 启动后台线程处理日志
+threading.Thread(target=async_worker, daemon=True).start()
+
+
 # Create your models here.
 # 主表:托盘数据
 class ContainerListModel(models.Model):
@@ -21,6 +68,13 @@ class ContainerListModel(models.Model):
         (4, '已出库'),
         (5, '空托盘组')
     )
+    # 新增托盘分类字段
+    CONTAINER_CATEGORY_CHOICES = (
+        (0, '整盘'),
+        (1, '散盘'),
+        (2, '空盘在库'),
+        (3, '空盘不在库'),
+    )
    
     container_code = models.IntegerField( verbose_name='托盘编号',unique=True)
     current_location = models.CharField(max_length=50, verbose_name='当前库位', default='N/A')
@@ -28,6 +82,17 @@ class ContainerListModel(models.Model):
     available= models.BooleanField(default=True, verbose_name='可用')
     status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='托盘状态')
     last_operation = models.DateTimeField(auto_now=True, verbose_name='最后操作时间')
+    category = models.IntegerField(
+        choices=CONTAINER_CATEGORY_CHOICES, 
+        default=3,  # 默认空盘不在库
+        verbose_name='托盘分类'
+    )
+    # 新增批次数组字段(缓存)
+    batch_info = models.JSONField(
+        default=list,
+        verbose_name='批次信息缓存',
+        help_text='格式: [{"batch_id":1,"check_status":"合格","bound_number":"B001","qty":100}]'
+    )
 
     class Meta:
         db_table = 'container_list'
@@ -461,17 +526,6 @@ def create_material_history( instance, update):
         stats.save()      
 
 
-# 简化的信号处理器
-# @receiver(post_save, sender=ContainerDetailLogModel)
-def handle_container_detail_log(sender, instance, created, **kwargs):
-    """创建托盘日志后立即关联到批次日志"""
-    if created:
-        try:
-            aggregate_to_batch_log(instance)
-            count_day_in_out(instance)
-
-        except Exception as e:
-            print(f"Error aggregating log: {e}")
 
 
 
@@ -543,13 +597,35 @@ def container_detail_pre_save(sender, instance, **kwargs):
             # 批量创建日志
             if logs:
                 created_logs = ContainerDetailLogModel.objects.bulk_create(logs)
+                # print(f"创建{len(created_logs)}条ContainerDetailLogModel日志")
                 for log in created_logs:
                 # 由于bulk_create不会触发信号,我们手动调用信号处理函数
                     handle_container_detail_log(ContainerDetailLogModel, log, created=True)
                 
         except ContainerDetailModel.DoesNotExist:
             pass
+        
+@receiver(post_save, sender=ContainerDetailLogModel)
+def handle_container_detail_log(sender, instance, created, **kwargs):
+    """将日志放入队列进行后台处理"""
+    if created:
+        # 检查实例是否已处理过(防止多次入队)
+        if hasattr(instance, '_is_enqueued'):
+            return
+        instance._is_enqueued = True
+        try:
+            log_processing_queue.put(([instance], created), block=False)
 
+            logger.info(f"已放入队列处理: {instance.id}")
+        except queue.Full:
+            print("日志处理队列已满,将同步处理")
+            logger.warning("日志处理队列已满,将同步处理")
+            # 队列满时同步处理当前日志
+            aggregate_to_batch_log(instance)
+            count_day_in_out(instance)
+            if created:
+                # 创建物料变动历史记录
+                create_material_history(instance, update=True)
 
 @receiver(post_save, sender=ContainerDetailModel)
 def container_detail_post_save(sender, instance, created, **kwargs):
@@ -564,6 +640,7 @@ def container_detail_post_save(sender, instance, created, **kwargs):
         )
 
 
+
 class MaterialChangeHistory(models.Model):
     """物料库存变动历史记录(与批次日志多对一)"""
     material_stats = models.ForeignKey(

+ 4 - 0
container/urls.py

@@ -9,6 +9,9 @@ re_path(r'^list/(?P<pk>\d+)/$', views.ContainerListViewSet.as_view({
     'patch': 'partial_update',
 }), name="ContainerList_1"),
 path(r'check/', views.ContainerListViewSet.as_view({"get": "check_container_postion"}), name="ContainerList"),
+path(r'update_categories/', views.ContainerListViewSet.as_view({"get": "update_container_categories"}), name="ContainerList"),
+
+
 path(r'detail/', views.ContainerDetailViewSet.as_view( {"get": "list","post": "create"}), name="ContainerDetail"),
 re_path(r'^detail/(?P<pk>\d+)/$', views.ContainerDetailViewSet.as_view({
     'get': 'retrieve',
@@ -45,6 +48,7 @@ re_path(r'^task/(?P<pk>\d+)/$', views.TaskViewSet.as_view({
 
 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'),
+path(r'issue_outbound/', views.ContainerWCSViewSet.as_view({"post": "generate_out_task"}), name='ContainerWCS1'),
 re_path(r'container_wcs/update/', views.ContainerWCSViewSet.as_view({"get": "update_container_wcs"}), name='ContainerWCS1'),
 
 path('out_task/', views.OutTaskViewSet.as_view({'post': 'post'  }), name='out_task'),

+ 132 - 0
container/utils.py

@@ -0,0 +1,132 @@
+from django.db.models import Sum, F
+from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel,ContainerWCSModel
+from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel
+from .serializers import *
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.utils import timezone
+from decimal import Decimal
+import logging
+logger = logging.getLogger(__name__)
+
+
+def update_container_category_for_container(container_id):
+    """
+    按需更新单个托盘的分类和批次信息
+    """
+    try:
+        container = ContainerListModel.objects.get(id=container_id)
+        
+        # 获取托盘所有有效物料详情
+        details = container.details.filter(
+            is_delete=False
+        ).exclude(status=3).annotate(
+            available_qty=F('goods_qty') - F('goods_out_qty')
+        ).filter(available_qty__gt=0)
+        
+        # 计算总物料量
+        total_qty = details.aggregate(total=Sum('available_qty'))['total'] or 0
+        
+        # 收集批次信息
+        batch_info = []
+        for detail in details:
+            batch = detail.batch
+            if batch:
+                batch_info.append({
+                    "batch_id": batch.id,
+                    "check_status": batch.check_status,
+                    "bound_number": batch.bound_number,
+                    "qty": float(detail.available_qty)  # 转换为 float
+                })
+        
+        # 确定托盘分类
+        if total_qty == 0:
+            # 空盘
+            category = 2 if container.status == 2 else 3  # 2=在库, 3=不在库
+        else:
+            # 有物料
+            batch_count = details.values('batch').distinct().count()
+            category = 0 if batch_count == 1 else 1  # 0=整盘, 1=散盘
+        
+        # 更新
+        container.category = category
+        container.batch_info = batch_info
+        container.save(update_fields=['category', 'batch_info'])
+        
+        return True
+    except Exception as e:
+        logger.error(f"更新托盘分类时出错: {e}", exc_info=True)
+        return False
+
+def batch_update_container_categories(container_ids):
+    """
+    批量更新多个托盘的分类和批次信息
+    """
+    updated_count = 0
+    error_ids = []
+    
+    for container_id in container_ids:
+        try:
+
+            container = ContainerListModel.objects.get(id=container_id)
+            
+            # 获取托盘所有有效物料详情
+            details = container.details.filter(
+                is_delete=False
+            ).exclude(status=3).annotate(
+                available_qty=F('goods_qty') - F('goods_out_qty')
+            ).filter(available_qty__gt=0)
+            
+            # 计算总物料量
+            total_qty = details.aggregate(total=Sum('available_qty'))['total'] or 0
+            
+            # 收集批次信息,合并批次数量
+            batch_info = []
+            batch_map = {}
+            for detail in details:
+                batch = detail.batch
+                if batch:
+                    if batch.id in batch_map:
+                        batch_map[batch.id]['qty'] += float(detail.available_qty)  # 转换为 float
+                    else:
+                        batch_map[batch.id] = {
+                            "batch_id": batch.id,
+                            "check_status": batch.check_status,
+                            "bound_number": batch.bound_number,
+                            "qty": float(detail.available_qty)  # 转换为 float
+                        }
+            
+            batch_info = list(batch_map.values())
+
+            # 确定托盘分类
+            if total_qty == 0:
+                # 空盘
+                category = 2 if container.current_location not in ['N/A', '103','203'] else 3  # 2=在库, 3=不在库
+            else:
+                # 有物料
+                batch_count = details.values('batch').distinct().count()
+                category = 0 if batch_count == 1 else 1  # 0=整盘, 1=散盘
+            
+            # 更新
+            container.category = category
+            container.batch_info = batch_info
+            container.save(update_fields=['category', 'batch_info'])
+            
+            updated_count += 1
+        except Exception as e:
+            logger.error(f"更新托盘 {container_id} 分类时出错: {e}", exc_info=True)
+            error_ids.append(container_id)
+    
+    return {
+        "updated": updated_count,
+        "errors": error_ids,
+        "total": len(container_ids)
+    }
+
+def update_container_categories_task():
+    """后台任务:更新所有托盘的分类和批次信息"""
+    container_ids = ContainerListModel.objects.values_list('id', flat=True)
+    result = batch_update_container_categories(list(container_ids))
+    print(f"后台任务更新托盘分类: 成功 {result['updated']}, 失败 {len(result['errors'])}")
+    logger.info(f"后台任务更新托盘分类: 成功 {result['updated']}, 失败 {len(result['errors'])}")
+    return result

+ 126 - 12
container/views.py

@@ -39,6 +39,37 @@ from django.db.models import Sum
 from staff.models import ListModel as StaffListModel
 logger = logging.getLogger(__name__)
 
+# 托盘分类视图
+# 借助LocationContainerLink,其中
+# 库位-托盘关联表(记录实时存放关系)
+# class LocationContainerLink(models.Model):
+#     location = models.ForeignKey(
+#             LocationModel, 
+#             on_delete=models.CASCADE,
+#             related_name='container_links', 
+#             verbose_name='库位'
+#         )
+    
+#     container = models.ForeignKey(
+#         ContainerListModel,
+#         on_delete=models.CASCADE,
+#         related_name='location_links',  
+#         verbose_name='关联托盘'
+#     )
+
+#     task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, null=True, blank=True)
+#     task_detail = models.ForeignKey(TaskModel, on_delete=models.CASCADE, null=True, blank=True)
+#     put_time = models.DateTimeField(auto_now_add=True, verbose_name='上架时间')
+#     operator = models.CharField(max_length=50, verbose_name='操作人')
+#     is_active = models.BooleanField(default=True, verbose_name='是否有效')
+# 借助LocationContainerLink中的is_active字段,可以查询托盘是否在库位中,
+# 若is_active为True,则托盘在库位中,否则托盘不在库位中。同时,再增加字段来显示在库托盘中的存储的物料信息。
+# class containerclassViewSet(viewsets.ModelViewSet):
+
+
+
+
+
 # 托盘流水汇总批次流水
 class batchLogModelViewSet(viewsets.ModelViewSet):
 
@@ -209,6 +240,8 @@ class ContainerListViewSet(viewsets.ModelViewSet):
         else:
             return self.http_method_not_allowed(request=self.request)
 
+    
+
     def create(self, request, *args, **kwargs):
         # 创建托盘:托盘码五位数字(唯一),当前库位,目标库位,状态,最后操作时间
 
@@ -252,6 +285,11 @@ class ContainerListViewSet(viewsets.ModelViewSet):
         # 如果没有分页,返回完整结果(不推荐)
         serializer = ContainerListGetSerializer(container_list, many=True)
         return Response(serializer.data, status=200)
+    
+    def update_container_categories(self, request, *args, **kwargs):
+        from .utils import update_container_categories_task
+        update_container_categories_task()
+        return Response({'message': '托盘分类更新任务已触发'}, status=200)
 
 # wcs任务视图
 class WCSTaskViewSet(viewsets.ModelViewSet):
@@ -496,7 +534,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         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}")
+        logger.info(f"移库请求托盘:{container},起始位置:{start_location},目标位置:{target_location}")
         data_return = {}
 
         try:
@@ -574,13 +612,89 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR
             )
 
+    def generate_out_task(self, request, *args, **kwargs):
+        data = self.request.data
+        container = data.get('container_code')
+        start_location = data.get('current_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) :
+                logger.info(f"托盘 {container} 已在目标位置")
+                data_return = {
+                    'code': '200',
+                    'message': '当前位置已是目标位置',
+                    'data': data
+                }
+            else:
+                # 生成任务
+                current_task = ContainerWCSModel.objects.filter(
+                    container=container, 
+                    tasktype='outbound',
+                    working = 1,
+                 
+                ).exclude(status=300).first()
+
+                if current_task:
+                    data_return = {
+                        'code': '200',
+                        'message': '任务已存在,重新下发',
+                        'data': current_task.to_dict()
+                    }
+                else:
+                    # todo: 这里的入库操作记录里面的记录的数量不对
+                    allocation_target_location, batch_info = AllocationService._out_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,1,'outbound') 
+                        self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location,"outbound")
+                    elif batch_info['class'] == 3:
+                        self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,1,'outbound') 
+                        self.generate_move_container_operate(container_obj, allocation_target_location,"outbound")
+                    else:   
+                        self.generate_task(container, start_location, allocation_target_location,batch_id,1,'outbound')  # 生成任务
+                        self.generate_move_container_operate(container_obj, allocation_target_location,"outbound")
+
+                    current_task = ContainerWCSModel.objects.get(
+                        container=container, 
+                        tasktype='outbound',
+                        working=1,
+                    )
+                    OutboundService.send_task_to_wcs(current_task)  
+                    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
         container = data.get('container_number')
         current_location = data.get('current_location')
-        logger.info(f"请求托盘:{container},请求位置:{current_location}")
+        logger.info(f"入库请求托盘:{container},请求位置:{current_location}")
         data_return = {}
 
         try:
@@ -714,7 +828,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             )
         
     
-    def generate_move_container_operate(self, container_obj, allocation_target_location):
+    def generate_move_container_operate(self, container_obj, allocation_target_location,operate_type="adjust"):
      
         # 获取容器中所有有效的批次明细
         container_detaillist = ContainerDetailModel.objects.filter(
@@ -751,7 +865,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 container=container_obj,
                 goods_code=batch_obj.goods_code,
                 goods_desc=batch_obj.goods_desc,
-                operation_type="adjust",  # 操作类型改为移动
+                operation_type=operate_type,  # 操作类型改为移动
                 batch_id=batch_obj.id,
                 goods_qty=goods_qty,
                 goods_weight=goods_qty,
@@ -779,24 +893,24 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
 
         )
     
-    def generate_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location):
+    def generate_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location,operate_type="inbound"):
        
         ContainerOperationModel.objects.create(
             month = int(timezone.now().strftime("%Y%m")),
             container = container_obj,
             goods_code = 'container',
             goods_desc = '托盘组',
-            operation_type ="inbound",
+            operation_type =operate_type,
             goods_qty = 1,
             goods_weight = 0,
             from_location = container_obj.current_location,
             to_location= allocation_target_location,
             timestamp=timezone.now(),
-            memo=f"WCS库: 批次: {bound_number}, 数量: 1"
+            memo=f"WCSs手动出库库: 批次: {bound_number}, 数量: 1"
 
         )
     
-    def generate_task(self, container, current_location, target_location,batch_id,location_c_number):
+    def generate_task(self, container, current_location, target_location,batch_id,location_c_number,tasktype='inbound'):
         batch = BoundBatchModel.objects.filter(bound_number=batch_id).first()
         batch_detail = BoundDetailModel.objects.filter(bound_batch=batch).first()
         if not batch:
@@ -814,7 +928,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             'current_location': current_location,
             'month': timezone.now().strftime('%Y%m'),
             'target_location': target_location,  
-            'tasktype': 'inbound',
+            'tasktype': tasktype,
             'status': 103,
             'is_delete': False
         }
@@ -838,7 +952,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
         data_tosave['tasknumber'] = number_id
         ContainerWCSModel.objects.create(**data_tosave)
 
-    def generate_task_no_batch(self, container, current_location, target_location,batch_id,location_c_number):
+    def generate_task_no_batch(self, container, current_location, target_location,batch_id,location_c_number,tasktype='inbound'):
     
         data_tosave = {
             'container': container,
@@ -852,7 +966,7 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             'current_location': current_location,
             'month': timezone.now().strftime('%Y%m'),
             'target_location': target_location,  
-            'tasktype': 'inbound',
+            'tasktype': tasktype,
             'status': 103,
             'is_delete': False
         }
@@ -2287,7 +2401,7 @@ class OutTaskViewSet(ViewSet):
                 left_qty += add_qty
                 last_out_qty = cd.goods_out_qty
                 cd.goods_out_qty += add_qty
-                print(f"{left_qty/25} 更新托盘 {cd.container.container_code} 批次 {cd.batch_id} 出库数量: {cd.goods_out_qty}")
+                # print(f"{left_qty/25} 更新托盘 {cd.container.container_code} 批次 {cd.batch_id} 出库数量: {cd.goods_out_qty}")
 
                 cd.save()
 

+ 7 - 29
data_base/test_erp copy.py

@@ -14,41 +14,19 @@ def setup_django():
     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'greaterwms.settings')
     django.setup()
 
-def test_summary_methods():
 
+def main():
     try:
-        from bound.models import MaterialStatistics
-        
-        print("测试报表数据生成方法...")
+        print(" 开始测功能")
         
+        # 执行测试
+        from container.utils import update_container_categories_task
 
-        end_date = timezone.now()
-        start_date = end_date - timedelta(days=7)
-        
-        # ===== 测试方法1:update_summary =====
-        print(" 测试 update_summary 方法...")
+        # 调用测试方法
+        update_container_categories_task()
 
-        
-        print(" 所有报表功能测试通过")
-        return True
-    
-    except Exception as e:
-        print(f" 测试失败:{str(e)}")
-        import traceback
-        traceback.print_exc()
-        return False
+        print(" 结束测功能")
 
-def main():
-    try:
-        print(" 开始测试物料统计报表功能")
-        
-        # 执行测试
-        success = test_summary_methods()
-        
-        if success:
-            print(" 测试成功:报表功能正常")
-        else:
-            print(" 测试失败:报表功能异常")
     
     except Exception as e:
         print(f" 测试失败:{str(e)}")

+ 1 - 0
templates/dist/spa/css/30.326b06df.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-ff155758]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-ff155758]{background-color:rgba(25,118,210,0.1)}

+ 0 - 1
templates/dist/spa/css/30.e2633675.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-8214f958]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-8214f958]{background-color:rgba(25,118,210,0.1)}

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


+ 0 - 1
templates/dist/spa/css/32.cdb5039a.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-0e5da179]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-0e5da179]{background-color:rgba(25,118,210,0.1)}

+ 0 - 1
templates/dist/spa/css/33.4cad196b.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-9cd534ee]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-9cd534ee]{background-color:rgba(25,118,210,0.1)}

+ 1 - 0
templates/dist/spa/css/33.efd62105.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-a6505960]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-a6505960]{background-color:rgba(25,118,210,0.1)}

+ 1 - 0
templates/dist/spa/css/34.12670fd1.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-464fb894]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-464fb894]{background-color:rgba(25,118,210,0.1)}

templates/dist/spa/css/34.9478c981.css → templates/dist/spa/css/35.9478c981.css


templates/dist/spa/css/35.c4652654.css → templates/dist/spa/css/36.c4652654.css


templates/dist/spa/css/36.7a23b7fb.css → templates/dist/spa/css/37.7a23b7fb.css


templates/dist/spa/css/37.0faa4aeb.css → templates/dist/spa/css/38.0faa4aeb.css


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


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


BIN
templates/dist/spa/js/30.8c350f2a.js.gz


BIN
templates/dist/spa/js/30.a5bfd137.js.gz


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


BIN
templates/dist/spa/js/32.a02b8cc4.js.gz


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


BIN
templates/dist/spa/js/32.c084dcec.js.gz


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


BIN
templates/dist/spa/js/33.478a11fb.js.gz


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


BIN
templates/dist/spa/js/33.95204aa3.js.gz


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


BIN
templates/dist/spa/js/34.e7f364ee.js.gz


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


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


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


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


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


BIN
templates/dist/spa/js/38.fa43a096.js.gz


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


BIN
templates/dist/spa/js/41.1e952a05.js.gz


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


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


BIN
templates/dist/spa/js/42.4076e567.js.gz


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


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


BIN
templates/dist/spa/js/43.d3fddc8e.js.gz


BIN
templates/dist/spa/js/44.7f6d36cb.js.gz


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


BIN
templates/dist/spa/js/45.7cff4886.js.gz


BIN
templates/dist/spa/js/45.b3a8ad9b.js.gz


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


BIN
templates/dist/spa/js/46.2911339f.js.gz


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


BIN
templates/dist/spa/js/46.67cb56c8.js.gz


BIN
templates/dist/spa/js/47.89908208.js.gz


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


BIN
templates/dist/spa/js/48.1a3b66a6.js.gz


BIN
templates/dist/spa/js/48.9be46088.js.gz


BIN
templates/dist/spa/js/49.30dac62f.js.gz


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


BIN
templates/dist/spa/js/49.6e6b3353.js.gz


BIN
templates/dist/spa/js/50.2ffd5e54.js.gz


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


BIN
templates/dist/spa/js/50.8d009ac4.js.gz


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


BIN
templates/dist/spa/js/51.36d36fea.js.gz


BIN
templates/dist/spa/js/51.b7a98797.js.gz


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


BIN
templates/dist/spa/js/52.f0a6b200.js.gz


BIN
templates/dist/spa/js/52.f6d23b7b.js.gz


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


BIN
templates/dist/spa/js/53.a2f048ca.js.gz


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


BIN
templates/dist/spa/js/53.58546587.js.gz


BIN
templates/dist/spa/js/54.2ec8c91b.js.gz


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


BIN
templates/dist/spa/js/55.a7b0daf2.js.gz


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


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


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


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


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


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


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


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


BIN
templates/dist/spa/js/63.d20f3e27.js.gz


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


BIN
templates/dist/spa/js/64.4f926b47.js.gz


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


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


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


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


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


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


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


+ 0 - 0
templates/dist/spa/js/71.af316543.js.gz


Some files were not shown because too many files changed in this diff