flower_bs 4 месяцев назад
Родитель
Сommit
be7ad18bfc
100 измененных файлов с 1202 добавлено и 207 удалено
  1. 1 0
      greaterwms/settings.py
  2. 1 1
      greaterwms/urls.py
  3. 0 0
      location_statistics/__init__.py
  4. 3 0
      location_statistics/admin.py
  5. 6 0
      location_statistics/apps.py
  6. 74 0
      location_statistics/migrations/0001_initial.py
  7. 17 0
      location_statistics/migrations/0002_alter_locationgroupstatistics_options.py
  8. 0 0
      location_statistics/migrations/__init__.py
  9. 77 0
      location_statistics/models.py
  10. 43 0
      location_statistics/serializers.py
  11. 189 0
      location_statistics/services.py
  12. 3 0
      location_statistics/tests.py
  13. 10 0
      location_statistics/urls.py
  14. 550 0
      location_statistics/views.py
  15. 51 124
      reportcenter/files.py
  16. 39 0
      reportcenter/filter.py
  17. 18 0
      reportcenter/serializers.py
  18. 4 0
      reportcenter/urls.py
  19. 96 63
      reportcenter/views.py
  20. 1 0
      templates/dist/spa/css/13.8b9d1b53.css
  21. 0 0
      templates/dist/spa/css/14.84ffb38a.css
  22. 0 0
      templates/dist/spa/css/15.285d986e.css
  23. 0 0
      templates/dist/spa/css/16.80435c82.css
  24. 0 0
      templates/dist/spa/css/17.074df1c7.css
  25. 0 0
      templates/dist/spa/css/18.f57b1220.css
  26. 0 0
      templates/dist/spa/css/19.9e7bbbba.css
  27. 0 0
      templates/dist/spa/css/20.296f042c.css
  28. 0 0
      templates/dist/spa/css/21.8fe0c02a.css
  29. 0 0
      templates/dist/spa/css/22.abdf7366.css
  30. 0 0
      templates/dist/spa/css/23.83beb898.css
  31. 0 0
      templates/dist/spa/css/24.601677c3.css
  32. 0 0
      templates/dist/spa/css/25.6dea747f.css
  33. 0 0
      templates/dist/spa/css/26.f721cf95.css
  34. 0 0
      templates/dist/spa/css/27.ed8e81e9.css
  35. 0 0
      templates/dist/spa/css/28.eed22a1c.css
  36. 0 0
      templates/dist/spa/css/29.2f5a0931.css
  37. 1 0
      templates/dist/spa/css/3.4891132a.css
  38. 0 1
      templates/dist/spa/css/3.8440293a.css
  39. 0 0
      templates/dist/spa/css/30.e6923623.css
  40. 0 0
      templates/dist/spa/css/31.01a9029f.css
  41. 0 0
      templates/dist/spa/css/32.9b0c5133.css
  42. 0 0
      templates/dist/spa/css/33.0d4c4716.css
  43. 0 0
      templates/dist/spa/css/34.c527e777.css
  44. 0 0
      templates/dist/spa/css/35.8f3f6188.css
  45. 0 0
      templates/dist/spa/css/36.44ddcebd.css
  46. 0 0
      templates/dist/spa/css/37.2ac1dad1.css
  47. 0 0
      templates/dist/spa/css/38.12670fd1.css
  48. 0 0
      templates/dist/spa/css/39.9478c981.css
  49. 0 0
      templates/dist/spa/css/40.c4652654.css
  50. 0 0
      templates/dist/spa/css/41.7a23b7fb.css
  51. 0 0
      templates/dist/spa/css/42.2594d0b9.css
  52. 0 0
      templates/dist/spa/css/43.0faa4aeb.css
  53. 1 1
      templates/dist/spa/index.html
  54. BIN
      templates/dist/spa/js/13.07a76275.js.gz
  55. 1 0
      templates/dist/spa/js/13.ac106c63.js
  56. BIN
      templates/dist/spa/js/13.ac106c63.js.gz
  57. 1 1
      templates/dist/spa/js/13.07a76275.js
  58. BIN
      templates/dist/spa/js/14.0b3059c5.js.gz
  59. BIN
      templates/dist/spa/js/14.8aad49d1.js.gz
  60. 1 1
      templates/dist/spa/js/14.8aad49d1.js
  61. BIN
      templates/dist/spa/js/15.b7dd6c80.js.gz
  62. 1 1
      templates/dist/spa/js/15.9a14922a.js
  63. BIN
      templates/dist/spa/js/15.9a14922a.js.gz
  64. BIN
      templates/dist/spa/js/17.35a9a046.js.gz
  65. 1 1
      templates/dist/spa/js/16.499d4616.js
  66. BIN
      templates/dist/spa/js/16.499d4616.js.gz
  67. 1 1
      templates/dist/spa/js/17.35a9a046.js
  68. BIN
      templates/dist/spa/js/18.18213a6e.js.gz
  69. BIN
      templates/dist/spa/js/18.e5ecfb11.js.gz
  70. BIN
      templates/dist/spa/js/19.4e20799a.js.gz
  71. 1 1
      templates/dist/spa/js/18.e5ecfb11.js
  72. BIN
      templates/dist/spa/js/19.b1cca1b7.js.gz
  73. 1 1
      templates/dist/spa/js/19.4e20799a.js
  74. BIN
      templates/dist/spa/js/20.4730a8aa.js.gz
  75. BIN
      templates/dist/spa/js/20.5869c533.js.gz
  76. BIN
      templates/dist/spa/js/21.43a20687.js.gz
  77. 1 1
      templates/dist/spa/js/20.5869c533.js
  78. BIN
      templates/dist/spa/js/21.6eee568e.js.gz
  79. 1 1
      templates/dist/spa/js/21.43a20687.js
  80. BIN
      templates/dist/spa/js/22.ea906fdf.js.gz
  81. BIN
      templates/dist/spa/js/23.7d6e0d41.js.gz
  82. 1 1
      templates/dist/spa/js/22.f4d41e96.js
  83. BIN
      templates/dist/spa/js/22.f4d41e96.js.gz
  84. 1 1
      templates/dist/spa/js/23.7d6e0d41.js
  85. BIN
      templates/dist/spa/js/24.31e1a220.js.gz
  86. BIN
      templates/dist/spa/js/24.cb0c46eb.js.gz
  87. 1 1
      templates/dist/spa/js/24.cb0c46eb.js
  88. BIN
      templates/dist/spa/js/25.91acd9c1.js.gz
  89. 1 1
      templates/dist/spa/js/25.2de0f4a3.js
  90. BIN
      templates/dist/spa/js/25.2de0f4a3.js.gz
  91. 1 1
      templates/dist/spa/js/26.7d399143.js
  92. BIN
      templates/dist/spa/js/26.7d399143.js.gz
  93. BIN
      templates/dist/spa/js/28.8d0422a3.js.gz
  94. 1 1
      templates/dist/spa/js/27.02dd357d.js
  95. BIN
      templates/dist/spa/js/27.02dd357d.js.gz
  96. BIN
      templates/dist/spa/js/29.1bae39fb.js.gz
  97. 1 1
      templates/dist/spa/js/28.8d0422a3.js
  98. BIN
      templates/dist/spa/js/29.64a81974.js.gz
  99. 0 1
      templates/dist/spa/js/3.5b010f7e.js
  100. 0 0
      templates/dist/spa/js/3.5b010f7e.js.gz

+ 1 - 0
greaterwms/settings.py

@@ -37,6 +37,7 @@ INSTALLED_APPS = [
     'warehouse.apps.WarehouseConfig',
     'reportcenter.apps.ReportcenterConfig',
     # 'asn.apps.AsnConfig',
+    'location_statistics.apps.LocationStatisticsConfig',
     'bound.apps.BoundConfig',
     'container.apps.ContainerConfig',
     'bin.apps.BinConfig',

+ 1 - 1
greaterwms/urls.py

@@ -31,7 +31,7 @@ urlpatterns = [
     path('container/', include('container.urls')),
     path ('wms/', include('erp.urls')),
     path ('backup/', include('backup.urls')),
-
+    path('location_statistics/', include('location_statistics.urls')),
 
     re_path(r'^favicon\.ico$', views.favicon, name='favicon'),
     re_path('^css/.*$', views.css, name='css'),

+ 0 - 0
location_statistics/__init__.py


+ 3 - 0
location_statistics/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
location_statistics/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class LocationStatisticsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'location_statistics'

+ 74 - 0
location_statistics/migrations/0001_initial.py

@@ -0,0 +1,74 @@
+# Generated by Django 4.1.2 on 2025-10-29 19:46
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LocationGroupStatistics',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('warehouse_code', models.CharField(max_length=255, verbose_name='仓库代码')),
+                ('warehouse_name', models.CharField(max_length=255, verbose_name='仓库名称')),
+                ('layer', models.IntegerField(verbose_name='楼层')),
+                ('location_group', models.CharField(max_length=20, verbose_name='库位组')),
+                ('total_locations', models.IntegerField(default=0, verbose_name='组内总货位数')),
+                ('used_locations', models.IntegerField(default=0, verbose_name='组内已用货位数')),
+                ('available_locations', models.IntegerField(default=0, verbose_name='组内可用货位数')),
+                ('utilization_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='使用率(%)')),
+                ('location_type_breakdown', models.JSONField(default=dict, verbose_name='货位类型细分')),
+                ('statistic_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='统计时间')),
+            ],
+            options={
+                'verbose_name': '货位组统计',
+                'verbose_name_plural': '货位组统计',
+                'db_table': 'location_group_statistics',
+                'ordering': ['warehouse_code', 'layer', 'location_group'],
+            },
+        ),
+        migrations.CreateModel(
+            name='LocationStatistics',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('warehouse_code', models.CharField(max_length=255, verbose_name='仓库代码')),
+                ('warehouse_name', models.CharField(max_length=255, verbose_name='仓库名称')),
+                ('layer', models.IntegerField(verbose_name='楼层')),
+                ('t5_total', models.IntegerField(default=0, verbose_name='5货位总数')),
+                ('t5_used', models.IntegerField(default=0, verbose_name='5货位已用')),
+                ('t5_available', models.IntegerField(default=0, verbose_name='5货位可用')),
+                ('t4_total', models.IntegerField(default=0, verbose_name='4货位总数')),
+                ('t4_used', models.IntegerField(default=0, verbose_name='4货位已用')),
+                ('t4_available', models.IntegerField(default=0, verbose_name='4货位可用')),
+                ('s4_total', models.IntegerField(default=0, verbose_name='4单货位总数')),
+                ('s4_used', models.IntegerField(default=0, verbose_name='4单货位已用')),
+                ('s4_available', models.IntegerField(default=0, verbose_name='4单货位可用')),
+                ('t2_total', models.IntegerField(default=0, verbose_name='2货位总数')),
+                ('t2_used', models.IntegerField(default=0, verbose_name='2货位已用')),
+                ('t2_available', models.IntegerField(default=0, verbose_name='2货位可用')),
+                ('t1_total', models.IntegerField(default=0, verbose_name='散货位总数')),
+                ('t1_used', models.IntegerField(default=0, verbose_name='散货位已用')),
+                ('t1_available', models.IntegerField(default=0, verbose_name='散货位可用')),
+                ('total_locations', models.IntegerField(default=0, verbose_name='总货位数')),
+                ('total_used', models.IntegerField(default=0, verbose_name='总已用数')),
+                ('total_available', models.IntegerField(default=0, verbose_name='总可用数')),
+                ('utilization_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='使用率(%)')),
+                ('statistic_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='统计时间')),
+                ('is_latest', models.BooleanField(default=False, verbose_name='是否最新统计')),
+            ],
+            options={
+                'verbose_name': '货位统计',
+                'verbose_name_plural': '货位统计',
+                'db_table': 'location_statistics',
+                'ordering': ['-statistic_time', 'warehouse_code', 'layer'],
+                'unique_together': {('warehouse_code', 'layer', 'statistic_time')},
+            },
+        ),
+    ]

+ 17 - 0
location_statistics/migrations/0002_alter_locationgroupstatistics_options.py

@@ -0,0 +1,17 @@
+# Generated by Django 4.1.2 on 2025-10-29 20:05
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('location_statistics', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='locationgroupstatistics',
+            options={'ordering': ['warehouse_code', 'layer', 'total_locations'], 'verbose_name': '货位组统计', 'verbose_name_plural': '货位组统计'},
+        ),
+    ]

+ 0 - 0
location_statistics/migrations/__init__.py


+ 77 - 0
location_statistics/models.py

@@ -0,0 +1,77 @@
+from django.db import models
+from django.utils import timezone
+from django.core.cache import cache
+
+class LocationStatistics(models.Model):
+    """货位统计模型"""
+    warehouse_code = models.CharField(max_length=255, verbose_name="仓库代码")
+    warehouse_name = models.CharField(max_length=255, verbose_name="仓库名称")
+    layer = models.IntegerField(verbose_name="楼层")
+    
+    # 按货位类型统计
+    t5_total = models.IntegerField(default=0, verbose_name="5货位总数")
+    t5_used = models.IntegerField(default=0, verbose_name="5货位已用")
+    t5_available = models.IntegerField(default=0, verbose_name="5货位可用")
+    
+    t4_total = models.IntegerField(default=0, verbose_name="4货位总数")
+    t4_used = models.IntegerField(default=0, verbose_name="4货位已用")
+    t4_available = models.IntegerField(default=0, verbose_name="4货位可用")
+    
+    s4_total = models.IntegerField(default=0, verbose_name="4单货位总数")
+    s4_used = models.IntegerField(default=0, verbose_name="4单货位已用")
+    s4_available = models.IntegerField(default=0, verbose_name="4单货位可用")
+    
+    t2_total = models.IntegerField(default=0, verbose_name="2货位总数")
+    t2_used = models.IntegerField(default=0, verbose_name="2货位已用")
+    t2_available = models.IntegerField(default=0, verbose_name="2货位可用")
+    
+    t1_total = models.IntegerField(default=0, verbose_name="散货位总数")
+    t1_used = models.IntegerField(default=0, verbose_name="散货位已用")
+    t1_available = models.IntegerField(default=0, verbose_name="散货位可用")
+    
+    # 汇总统计
+    total_locations = models.IntegerField(default=0, verbose_name="总货位数")
+    total_used = models.IntegerField(default=0, verbose_name="总已用数")
+    total_available = models.IntegerField(default=0, verbose_name="总可用数")
+    utilization_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name="使用率(%)")
+    
+    # 统计时间
+    statistic_time = models.DateTimeField(default=timezone.now, verbose_name="统计时间")
+    is_latest = models.BooleanField(default=False, verbose_name="是否最新统计")
+    
+    class Meta:
+        db_table = 'location_statistics'
+        verbose_name = '货位统计'
+        verbose_name_plural = "货位统计"
+        ordering = ['-statistic_time', 'warehouse_code', 'layer']
+        unique_together = ['warehouse_code', 'layer', 'statistic_time']
+    
+    def __str__(self):
+        return f"{self.warehouse_name}-{self.layer}层-{self.statistic_time.strftime('%Y-%m-%d %H:%M')}"
+
+class LocationGroupStatistics(models.Model):
+    """货位组统计模型"""
+    warehouse_code = models.CharField(max_length=255, verbose_name="仓库代码")
+    warehouse_name = models.CharField(max_length=255, verbose_name="仓库名称")
+    layer = models.IntegerField(verbose_name="楼层")
+    location_group = models.CharField(max_length=20, verbose_name="库位组")
+    
+    # 统计信息
+    total_locations = models.IntegerField(default=0, verbose_name="组内总货位数")
+    used_locations = models.IntegerField(default=0, verbose_name="组内已用货位数")
+    available_locations = models.IntegerField(default=0, verbose_name="组内可用货位数")
+    utilization_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name="使用率(%)")
+    
+    # 按货位类型细分
+    location_type_breakdown = models.JSONField(default=dict, verbose_name="货位类型细分")
+    
+    statistic_time = models.DateTimeField(default=timezone.now, verbose_name="统计时间")
+    
+    class Meta:
+        db_table = 'location_group_statistics'
+        verbose_name = '货位组统计'
+        verbose_name_plural = "货位组统计"
+        ordering = ['warehouse_code', 'layer', 'total_locations']
+    
+    def __str__(self):
+        return f"{self.warehouse_name}-{self.layer}层-{self.location_group}"

+ 43 - 0
location_statistics/serializers.py

@@ -0,0 +1,43 @@
+from rest_framework import serializers
+from .models import LocationStatistics, LocationGroupStatistics
+
+class LocationStatisticsSerializer(serializers.ModelSerializer):
+    """货位统计序列化器"""
+    
+    warehouse_display = serializers.CharField(source='warehouse_name', read_only=True)
+    layer_display = serializers.SerializerMethodField()
+    
+    class Meta:
+        model = LocationStatistics
+        fields = [
+            'id', 'warehouse_code', 'warehouse_name', 'warehouse_display', 'layer', 'layer_display',
+            't5_total', 't5_used', 't5_available',
+            't4_total', 't4_used', 't4_available',
+            's4_total', 's4_used', 's4_available',
+            't2_total', 't2_used', 't2_available',
+            't1_total', 't1_used', 't1_available',
+            'total_locations', 'total_used', 'total_available', 'utilization_rate',
+            'statistic_time', 'is_latest'
+        ]
+    
+    def get_layer_display(self, obj):
+        return f"{obj.layer}层"
+
+class LocationGroupStatisticsSerializer(serializers.ModelSerializer):
+    """货位组统计序列化器"""
+    
+    warehouse_display = serializers.CharField(source='warehouse_name', read_only=True)
+    layer_display = serializers.SerializerMethodField()
+    
+    class Meta:
+        model = LocationGroupStatistics
+        fields = [
+            'id', 'warehouse_code', 'warehouse_name', 'warehouse_display',
+            'layer', 'layer_display', 'location_group',
+            'total_locations', 'used_locations', 'available_locations', 'utilization_rate',
+            'location_type_breakdown', 'statistic_time'
+        ]
+        ordering = ['warehouse_code', 'layer', 'total_locations']
+    
+    def get_layer_display(self, obj):
+        return f"{obj.layer}层"

+ 189 - 0
location_statistics/services.py

@@ -0,0 +1,189 @@
+from django.db.models import Count, Q
+from django.utils import timezone
+from bin.models import LocationModel
+from .models import LocationStatistics, LocationGroupStatistics
+
+class LocationStatisticsService:
+    """货位统计服务类"""
+    TARGET_LOCATION_TYPES = ['T5', 'T4', 'S4', 'T2', 'T1']
+
+    @staticmethod
+    def calculate_location_statistics(warehouse_code=None, layer=None):
+        """
+        计算货位统计信息
+        
+        Args:
+            warehouse_code: 仓库代码,如果为None则统计所有仓库
+            layer: 楼层,如果为None则统计所有楼层
+        """
+        # 构建查询条件
+        # 构建查询条件 - 只统计指定的货位类型
+        filters = {
+            'is_active': True,
+            'location_type__in': LocationStatisticsService.TARGET_LOCATION_TYPES
+        }
+        if warehouse_code:
+            filters['warehouse_code'] = warehouse_code
+        if layer is not None:
+            filters['layer'] = layer
+        
+        # 获取所有符合条件的货位
+        locations = LocationModel.objects.filter(**filters)
+        
+        # 按仓库和楼层分组统计
+        statistics_data = {}
+        
+        for location in locations:
+            key = f"{location.warehouse_code}_{location.layer}"
+            if key not in statistics_data:
+                statistics_data[key] = {
+                    'warehouse_code': location.warehouse_code,
+                    'warehouse_name': location.warehouse_name,
+                    'layer': location.layer,
+                    'location_types': {},
+                    'groups': {},
+                    'total': 0,
+                    'used': 0,
+                    'available': 0
+                }
+            
+            # 统计货位类型
+            loc_type = location.location_type
+            if loc_type not in statistics_data[key]['location_types']:
+                statistics_data[key]['location_types'][loc_type] = {
+                    'total': 0, 'used': 0, 'available': 0
+                }
+            
+            statistics_data[key]['location_types'][loc_type]['total'] += 1
+            statistics_data[key]['total'] += 1
+            
+            # 根据状态统计
+            if location.status in ['occupied', 'reserved']:
+                statistics_data[key]['location_types'][loc_type]['used'] += 1
+                statistics_data[key]['used'] += 1
+            elif location.status == 'available':
+                statistics_data[key]['location_types'][loc_type]['available'] += 1
+                statistics_data[key]['available'] += 1
+            
+            # 统计货位组
+            group = location.location_group
+            if group not in statistics_data[key]['groups']:
+                statistics_data[key]['groups'][group] = {
+                    'total': 0, 'used': 0, 'available': 0
+                }
+            
+            statistics_data[key]['groups'][group]['total'] += 1
+            if location.status == 'occupied':
+                statistics_data[key]['groups'][group]['used'] += 1
+            elif location.status == 'available':
+                statistics_data[key]['groups'][group]['available'] += 1
+        
+        return statistics_data
+    
+    @staticmethod
+    def save_statistics_to_db(statistics_data):
+        """将统计结果保存到数据库"""
+        current_time = timezone.now()
+        
+        # 先将之前的最新标记清除
+        LocationStatistics.objects.filter(is_latest=True).update(is_latest=False)
+        
+        statistics_objects = []
+        group_statistics_objects = []
+        
+        for key, data in statistics_data.items():
+            # 计算使用率
+            utilization_rate = 0
+            if data['total'] > 0:
+                utilization_rate = round((data['used'] / data['total']) * 100, 2)
+            
+            # 创建主统计对象
+            stat = LocationStatistics(
+                warehouse_code=data['warehouse_code'],
+                warehouse_name=data['warehouse_name'],
+                layer=data['layer'],
+                statistic_time=current_time,
+                is_latest=True,
+                total_locations=data['total'],
+                total_used=data['used'],
+                total_available=data['available'],
+                utilization_rate=utilization_rate
+            )
+            
+            # 设置各类型货位统计
+            loc_types = data['location_types']
+            for loc_type, counts in loc_types.items():
+                if loc_type == 'T5':
+                    stat.t5_total = counts['total']
+                    stat.t5_used = counts['used']
+                    stat.t5_available = counts['available']
+                elif loc_type == 'T4':
+                    stat.t4_total = counts['total']
+                    stat.t4_used = counts['used']
+                    stat.t4_available = counts['available']
+                elif loc_type == 'S4':
+                    stat.s4_total = counts['total']
+                    stat.s4_used = counts['used']
+                    stat.s4_available = counts['available']
+                elif loc_type == 'T2':
+                    stat.t2_total = counts['total']
+                    stat.t2_used = counts['used']
+                    stat.t2_available = counts['available']
+                elif loc_type == 'T1':
+                    stat.t1_total = counts['total']
+                    stat.t1_used = counts['used']
+                    stat.t1_available = counts['available']
+            
+            statistics_objects.append(stat)
+            
+            # 创建货位组统计对象
+            for group_name, group_data in data['groups'].items():
+                group_utilization = 0
+                if group_data['total'] > 0:
+                    group_utilization = round((group_data['used'] / group_data['total']) * 100, 2)
+                
+                group_stat = LocationGroupStatistics(
+                    warehouse_code=data['warehouse_code'],
+                    warehouse_name=data['warehouse_name'],
+                    layer=data['layer'],
+                    location_group=group_name,
+                    total_locations=group_data['total'],
+                    used_locations=group_data['used'],
+                    available_locations=group_data['available'],
+                    utilization_rate=group_utilization,
+                    location_type_breakdown={},  # 这里可以进一步细化
+                    statistic_time=current_time
+                )
+                group_statistics_objects.append(group_stat)
+        
+        # 批量保存
+        LocationStatistics.objects.bulk_create(statistics_objects)
+        LocationGroupStatistics.objects.bulk_create(group_statistics_objects)
+        
+        return len(statistics_objects)
+    
+    @staticmethod
+    def get_latest_statistics(warehouse_code=None, layer=None):
+        """获取最新统计信息"""
+        filters = {'is_latest': True}
+        if warehouse_code:
+            filters['warehouse_code'] = warehouse_code
+        if layer is not None:
+            filters['layer'] = layer
+        
+        return LocationStatistics.objects.filter(**filters).order_by('warehouse_code', 'layer')
+    
+    @staticmethod
+    def get_statistics_by_time_range(start_time, end_time, warehouse_code=None, layer=None):
+        """按时间范围获取统计信息"""
+        filters = {
+            'statistic_time__gte': start_time,
+            'statistic_time__lte': end_time
+        }
+        if warehouse_code:
+            filters['warehouse_code'] = warehouse_code
+        if layer is not None:
+            filters['layer'] = layer
+        
+        return LocationStatistics.objects.filter(**filters).order_by('statistic_time', 'warehouse_code', 'layer')
+

+ 3 - 0
location_statistics/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 10 - 0
location_statistics/urls.py

@@ -0,0 +1,10 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+    path('statistics/', views.LocationStatisticsView.as_view(), name='location-statistics'),
+    path('statistics/history/', views.LocationStatisticsHistoryView.as_view(), name='location-statistics-history'),
+    path('group-statistics/', views.LocationGroupStatisticsView.as_view(), name='location-group-statistics'),
+    path('refresh-statistics/', views.LocationStatisticsView.as_view(), name='refresh-location-statistics'),
+    path('CheckView/', views.LocationConsistencyCheckView.as_view(), name='refresh-location-group-statistics'),
+]

+ 550 - 0
location_statistics/views.py

@@ -0,0 +1,550 @@
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from django.utils import timezone
+from datetime import timedelta
+from .services import LocationStatisticsService
+from .models import  LocationGroupStatistics
+from django.db import transaction
+from django.db.models import Count, Q
+import logging
+
+logger = logging.getLogger(__name__)
+from .serializers import LocationStatisticsSerializer, LocationGroupStatisticsSerializer
+
+class LocationStatisticsView(APIView):
+    """货位统计API视图"""
+    
+    def get(self, request):
+        """获取货位统计信息"""
+        warehouse_code = request.GET.get('warehouse_code')
+        layer = request.GET.get('layer')
+        
+        # 尝试从参数转换层数
+        if layer is not None:
+            try:
+                layer = int(layer)
+            except ValueError:
+                layer = None
+    
+        # 从数据库获取最新统计
+        statistics = LocationStatisticsService.get_latest_statistics(warehouse_code, layer)
+        serializer = LocationStatisticsSerializer(statistics, many=True)
+        
+        response_data = {
+            'timestamp': timezone.now().isoformat(),
+            'data': serializer.data
+        }
+        
+        return Response(response_data)
+    
+    def post(self, request):
+        """手动触发统计计算"""
+        warehouse_code = request.data.get('warehouse_code')
+        layer = request.data.get('layer')
+        
+        # 计算统计信息
+        stats_data = LocationStatisticsService.calculate_location_statistics(warehouse_code, layer)
+        
+        # 保存到数据库
+        count = LocationStatisticsService.save_statistics_to_db(stats_data)
+        
+
+        
+        return Response({
+            'message': f'成功统计了{count}条记录',
+            'timestamp': timezone.now().isoformat()
+        }, status=status.HTTP_201_CREATED)
+
+class LocationStatisticsHistoryView(APIView):
+    """货位统计历史数据API视图"""
+    
+    def get(self, request):
+        """获取历史统计信息"""
+        warehouse_code = request.GET.get('warehouse_code')
+        layer = request.GET.get('layer')
+        hours = int(request.GET.get('hours', 24))  # 默认最近24小时
+        
+        end_time = timezone.now()
+        start_time = end_time - timedelta(hours=hours)
+        
+        statistics = LocationStatisticsService.get_statistics_by_time_range(
+            start_time, end_time, warehouse_code, layer
+        )
+        
+        serializer = LocationStatisticsSerializer(statistics, many=True)
+        
+        return Response({
+            'period': f'{start_time} - {end_time}',
+            'data': serializer.data
+        })
+
+class LocationGroupStatisticsView(APIView):
+    """货位组统计API视图"""
+    
+    def get(self, request):
+        """获取货位组统计信息"""
+        warehouse_code = request.GET.get('warehouse_code')
+        layer = request.GET.get('layer')
+        
+        # 新增过滤参数
+        min_utilization = float(request.GET.get('min_utilization', 0))  # 最小使用率,默认0
+        min_used_locations = int(request.GET.get('min_used_locations', 0))  # 最小已用货位数,默认0
+        
+        filters = {}
+        if warehouse_code:
+            filters['warehouse_code'] = warehouse_code
+        if layer:
+            try:
+                filters['layer'] = int(layer)
+            except ValueError:
+                pass
+        
+        # 获取最新的组统计
+        from django.db.models import Max
+        latest_stats = LocationGroupStatistics.objects.filter(
+            **filters
+        ).values('warehouse_code', 'layer', 'location_group').annotate(
+            latest_time=Max('statistic_time')
+        )
+        
+        group_stats = []
+        for stat in latest_stats:
+            latest_record = LocationGroupStatistics.objects.filter(
+                warehouse_code=stat['warehouse_code'],
+                layer=stat['layer'],
+                location_group=stat['location_group'],
+                statistic_time=stat['latest_time']
+            ).first()
+            
+            # 根据参数过滤
+            if latest_record:
+                # 检查使用率条件
+                utilization_ok = latest_record.utilization_rate >= min_utilization
+                # 检查已用货位数条件
+                used_locations_ok = latest_record.used_locations >= min_used_locations
+                
+                if utilization_ok and used_locations_ok:
+                    group_stats.append(latest_record)
+        
+        serializer = LocationGroupStatisticsSerializer(group_stats, many=True)
+        
+        return Response({
+            'timestamp': timezone.now().isoformat(),
+            'filters': {
+                'min_utilization': min_utilization,
+                'min_used_locations': min_used_locations,
+                'total_records': len(group_stats)
+                
+            },
+            'data': serializer.data
+        })
+    
+class LocationConsistencyCheckView(APIView):
+    """库位一致性检查API"""
+    
+    def post(self, request):
+        warehouse_code = request.data.get('warehouse_code')
+        layer = request.GET.get('layer')
+        if int(layer) < 1 :
+            layer = None
+        auto_fix = request.data.get('auto_fix', False)
+        
+        checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
+        result = checker.check_all()
+        
+        return Response({
+            'success': True,
+            'data': checker.generate_report()
+        })
+
+class LocationConsistencyChecker:
+    TARGET_LOCATION_TYPES = ['T5', 'T4', 'S4', 'T2', 'T1']
+    """
+    库位一致性检测器
+    用于检测库位状态与Link记录的一致性以及库位组的状态一致性
+    """
+    
+    def __init__(self, warehouse_code=None, layer=None, auto_fix=False):
+        """
+        初始化检测器
+        
+        Args:
+            warehouse_code: 指定仓库代码,如果为None则检测所有仓库
+            layer: 指定楼层,如果为None则检测所有楼层
+            auto_fix: 是否自动修复检测到的问题
+        """
+        self.warehouse_code = warehouse_code
+        self.layer = layer
+        self.auto_fix = auto_fix
+        self.results = {
+            'check_time': timezone.now(),
+            'location_errors': [],
+            'group_errors': [],
+            'fixed_locations': [],
+            'fixed_groups': [],
+            'repair_errors': [],
+            'summary': {
+                'total_locations': 0,
+                'checked_locations': 0,
+                'error_locations': 0,
+                'total_groups': 0,
+                'checked_groups': 0,
+                'error_groups': 0,
+                'fixed_location_count': 0,
+                'fixed_group_count': 0
+            }
+        }
+    
+    def check_all(self):
+        """执行所有检测"""
+        logger.info(f"开始库位一致性检测,仓库: {self.warehouse_code}, 楼层: {self.layer}")
+        
+        # 检测库位状态与Link记录的一致性
+        self.check_location_link_consistency()
+        
+        # 检测库位组状态一致性
+        self.check_group_consistency()
+        
+        # 如果启用自动修复,执行修复
+        if self.auto_fix:
+            self.fix_detected_issues()
+        
+        logger.info(f"库位一致性检测完成,发现{self.results['summary']['error_locations']}个库位问题,"
+                   f"{self.results['summary']['error_groups']}个库位组问题")
+        
+        return self.results
+    
+    def check_location_link_consistency(self):
+        """检测库位状态与Link记录的一致性"""
+        from bin.models import LocationModel, LocationContainerLink
+        
+        # 构建查询条件
+        filters = {'is_active': True, 'location_type__in': self.TARGET_LOCATION_TYPES}
+        if self.warehouse_code:
+            filters['warehouse_code'] = self.warehouse_code
+        if self.layer is not None:
+            filters['layer'] = self.layer
+        
+        # 获取库位并预取Link记录
+        locations = LocationModel.objects.filter(**filters).prefetch_related('container_links')
+        self.results['summary']['total_locations'] = locations.count()
+        
+        for location in locations:
+            self.results['summary']['checked_locations'] += 1
+            
+            # 获取该库位的活跃Link记录数量
+            active_links_count = location.container_links.filter(is_active=True).count()
+            
+            # 根据状态判断是否一致
+            is_consistent, expected_status, error_type = self._check_location_consistency(
+                location.status, active_links_count
+            )
+            
+            if not is_consistent:
+                self.results['summary']['error_locations'] += 1
+                self.results['location_errors'].append({
+                    'location_id': location.id,
+                    'location_code': location.location_code,
+                    'warehouse_code': location.warehouse_code,
+                    'current_status': location.status,
+                    'expected_status': expected_status,
+                    'active_links_count': active_links_count,
+                    'layer': location.layer,
+                    'row': location.row,
+                    'col': location.col,
+                    'location_group': location.location_group,
+                    'error_type': error_type,
+                    'detected_at': timezone.now()
+                })
+    
+    def _check_location_consistency(self, current_status, active_links_count):
+        """
+        检查单个库位的状态一致性
+        
+        Returns:
+            tuple: (是否一致, 预期状态, 错误类型)
+        """
+        if current_status == 'available':
+            # available状态应该没有活跃的Link记录
+            if active_links_count > 0:
+                return False, 'occupied', 'available_but_has_links'
+        
+        elif current_status in ['occupied','reserved']:
+            # occupied状态应该至少有一个活跃的Link记录
+            if active_links_count != 1:
+                return False, 'available', 'occupied_but_no_links'
+        
+        elif current_status in ['disabled',  'maintenance']:
+            # 这些特殊状态可以有Link记录,但通常不应该有
+            if active_links_count > 0:
+                return False, current_status, 'special_status_has_links'
+        
+        return True, current_status, None
+    
+    def check_group_consistency(self):
+        """检测库位组状态的一致性"""
+        from bin.models import LocationGroupModel
+        
+        # 构建查询条件
+        filters = {'is_active': True}
+        if self.warehouse_code:
+            filters['warehouse_code'] = self.warehouse_code
+        if self.layer is not None:
+            filters['layer'] = self.layer
+        
+        # 获取库位组并预取关联的库位
+        groups = LocationGroupModel.objects.filter(**filters).prefetch_related('location_items')
+        self.results['summary']['total_groups'] = groups.count()
+        
+        for group in groups:
+            self.results['summary']['checked_groups'] += 1
+            
+            # 获取组内所有库位
+            group_locations = group.location_items.filter(is_active=True)
+            
+            # 统计组内库位的状态分布
+            status_counts = self._get_group_status_distribution(group_locations)
+            total_locations = group_locations.count()
+            
+            # 根据组内库位状态推断组应该的状态
+            is_consistent, expected_status, error_type = self._check_group_consistency(
+                group.status, status_counts, total_locations
+            )
+            
+            if not is_consistent:
+                self.results['summary']['error_groups'] += 1
+                self.results['group_errors'].append({
+                    'group_id': group.id,
+                    'group_code': group.group_code,
+                    'group_name': group.group_name,
+                    'warehouse_code': group.warehouse_code,
+                    'current_status': group.status,
+                    'expected_status': expected_status,
+                    'total_locations': total_locations,
+                    'status_distribution': status_counts,
+                    'layer': group.layer,
+                    'error_type': error_type,
+                    'detected_at': timezone.now()
+                })
+    
+    def _get_group_status_distribution(self, group_locations):
+        """获取组内库位状态分布"""
+        return group_locations.aggregate(
+            available_count=Count('id', filter=Q(status='available')),
+            occupied_count=Count('id', filter=Q(status='occupied')),
+            disabled_count=Count('id', filter=Q(status='disabled')),
+            reserved_count=Count('id', filter=Q(status='reserved')),
+            maintenance_count=Count('id', filter=Q(status='maintenance'))
+        )
+    
+    def _check_group_consistency(self, current_status, status_counts, total_locations):
+        """
+        检查库位组状态一致性
+        
+        Returns:
+            tuple: (是否一致, 预期状态, 错误类型)
+        """
+        if total_locations == 0:
+            # 空组应该是available或disabled
+            if current_status not in ['available', 'disabled']:
+                return False, 'available', 'empty_group_wrong_status'
+        else:
+            # 根据库位状态分布推断组状态
+            if status_counts['occupied_count'] == total_locations:
+                # 所有库位都被占用,组状态应该是full
+                if current_status != 'full':
+                    return False, 'full', 'all_occupied_but_not_full'
+            
+            elif status_counts['occupied_count'] > 0:
+                # 有库位被占用,组状态应该是occupied
+                if current_status != 'occupied':
+                    return False, 'occupied', 'has_occupied_but_wrong_status'
+            
+            elif status_counts['available_count'] == total_locations:
+                # 所有库位都可用,组状态应该是available
+                if current_status != 'available':
+                    return False, 'available', 'all_available_but_wrong_status'
+            
+            # 检查特殊状态
+            elif status_counts['disabled_count'] > 0 and current_status != 'disabled':
+                return False, 'disabled', 'has_disabled_but_wrong_status'
+            
+            elif status_counts['maintenance_count'] > 0 and current_status != 'maintenance':
+                return False, 'maintenance', 'has_maintenance_but_wrong_status'
+        
+        return True, current_status, None
+    
+    def fix_detected_issues(self):
+        """修复检测到的不一致问题"""
+        logger.info("开始修复检测到的不一致问题")
+        
+        # 修复库位状态不一致
+        if self.results['location_errors']:
+            self._fix_location_issues()
+        
+        # 修复库位组状态不一致
+        if self.results['group_errors']:
+            self._fix_group_issues()
+        
+        logger.info(f"修复完成: {self.results['summary']['fixed_location_count']}个库位, "
+                   f"{self.results['summary']['fixed_group_count']}个库位组")
+    
+    def _fix_location_issues(self):
+        """修复库位状态不一致问题"""
+        from bin.models import LocationModel
+        
+        for error in self.results['location_errors']:
+            try:
+                with transaction.atomic():
+                    location = LocationModel.objects.select_for_update().get(id=error['location_id'])
+                    
+                    # 只有明确有预期状态时才修复
+                    if error['expected_status'] and error['expected_status'] != 'need_check':
+                        old_status = location.status
+                        location.status = error['expected_status']
+                        location.save()
+                        
+                        self.results['summary']['fixed_location_count'] += 1
+                        
+                        self.results['fixed_locations'].append({
+                            'location_id': location.id,
+                            'location_code': location.location_code,
+                            'old_status': old_status,
+                            'new_status': location.status,
+                            'fixed_at': timezone.now(),
+                            'error_type': error['error_type']
+                        })
+                    else:
+                        # 标记为需要手动检查
+                        self.results['fixed_locations'].append({
+                            'location_id': location.id,
+                            'location_code': location.location_code,
+                            'status': '需要手动检查',
+                            'reason': '无法自动确定正确状态',
+                            'error_type': error['error_type']
+                        })
+                        
+            except Exception as e:
+                logger.error(f"修复库位{error.get('location_id')}时出错: {str(e)}")
+                self.results['repair_errors'].append({
+                    'type': 'location_fix_error',
+                    'location_id': error.get('location_id'),
+                    'error_message': str(e),
+                    'error_type': error.get('error_type')
+                })
+    
+    def _fix_group_issues(self):
+        """修复库位组状态不一致问题"""
+        from bin.models import LocationGroupModel
+        
+        for error in self.results['group_errors']:
+            try:
+                with transaction.atomic():
+                    group = LocationGroupModel.objects.select_for_update().get(id=error['group_id'])
+                    
+                    # 只有明确有预期状态时才修复
+                    if error['expected_status'] and error['expected_status'] != 'need_check':
+                        old_status = group.status
+                        group.status = error['expected_status']
+                        group.save()
+                        
+                        self.results['summary']['fixed_group_count'] += 1
+                        
+                        self.results['fixed_groups'].append({
+                            'group_id': group.id,
+                            'group_code': group.group_code,
+                            'old_status': old_status,
+                            'new_status': group.status,
+                            'fixed_at': timezone.now(),
+                            'error_type': error['error_type']
+                        })
+                    else:
+                        # 标记为需要手动检查
+                        self.results['fixed_groups'].append({
+                            'group_id': group.id,
+                            'group_code': group.group_code,
+                            'status': '需要手动检查',
+                            'reason': '无法自动确定正确状态',
+                            'error_type': error.get('error_type')
+                        })
+                        
+            except Exception as e:
+                logger.error(f"修复库位组{error.get('group_id')}时出错: {str(e)}")
+                self.results['repair_errors'].append({
+                    'type': 'group_fix_error',
+                    'group_id': error.get('group_id'),
+                    'error_message': str(e),
+                    'error_type': error.get('error_type')
+                })
+    
+    def get_summary(self):
+        """获取检测摘要"""
+        return {
+            'check_time': self.results['check_time'],
+            'total_checked': {
+                'locations': self.results['summary']['checked_locations'],
+                'groups': self.results['summary']['checked_groups']
+            },
+            'errors_found': {
+                'locations': self.results['summary']['error_locations'],
+                'groups': self.results['summary']['error_groups']
+            },
+            'fixed': {
+                'locations': self.results['summary']['fixed_location_count'],
+                'groups': self.results['summary']['fixed_group_count']
+            },
+            'has_errors': len(self.results['repair_errors']) > 0
+        }
+    
+    def generate_report(self):
+        """生成检测报告"""
+        summary = self.get_summary()
+        
+        report = {
+            'summary': summary,
+            'details': {
+                'location_errors': self.results['location_errors'],
+                'group_errors': self.results['group_errors'],
+                'fixed_locations': self.results['fixed_locations'],
+                'fixed_groups': self.results['fixed_groups'],
+                'repair_errors': self.results['repair_errors']
+            }
+        }
+        
+        return report
+
+
+# 便捷函数
+def check_location_consistency(warehouse_code=None, layer=None, auto_fix=False):
+    """
+    便捷函数:检查库位一致性
+    
+    Args:
+        warehouse_code: 仓库代码
+        layer: 楼层
+        auto_fix: 是否自动修复
+    
+    Returns:
+        dict: 检测结果
+    """
+    checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
+    return checker.check_all()
+
+
+def get_consistency_report(warehouse_code=None, layer=None, auto_fix=False):
+    """
+    获取详细的检测报告
+    
+    Args:
+        warehouse_code: 仓库代码
+        layer: 楼层
+        auto_fix: 是否自动修复
+    
+    Returns:
+        dict: 检测报告
+    """
+    checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
+    checker.check_all()
+    return checker.generate_report()
+

+ 51 - 124
reportcenter/files.py

@@ -50,138 +50,65 @@ class FileFlowListRenderCN(CSVStreamingRenderer):
     header = file_headers_list()
     labels = cn_data_header_list()
 
-"""
 
-class MaterialChangeHistory(models.Model):
 
-    material_stats = models.ForeignKey(
-        MaterialStatistics,
-        on_delete=models.CASCADE,
-        related_name='history_records',
-        verbose_name="关联物料"
-    )
-    
-    # 与批次日志建立多对一关系
-    batch_log = models.ForeignKey(batchLogModel,on_delete=models.CASCADE, related_name='material_history', verbose_name="关联批次日志",primary_key=False)
+def BatchList_file_headers_list():
+    return [
+            'id',
+			'bound_number',
+			'sourced_number',
+	
+			'bound_batch_order',
+		
+			'warehouse_name',
+			'goods_code',
+			'goods_desc',
+			'goods_std',
+			'goods_unit',
+			'goods_qty',
+			'goods_package',
+			'goods_in_qty',
+			'goods_in_location_qty',
+			'goods_out_qty',
+
+		
+			'container_number',
+	
+			'check_status'
+    ]
 
-    goods_code = models.CharField(max_length=50, verbose_name='货品编码')
-    goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
-    goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
-    goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
-    # 时间戳记录(使用批次日志的时间)
-    change_time = models.DateTimeField(
-        verbose_name="变动时间"
-    )
-    
-    # 库存变动情况
-    in_quantity = models.DecimalField(
-        max_digits=10, decimal_places=3,
-        default=Decimal('0'),
-        verbose_name="入库数量"
-    )
-    
-    out_quantity = models.DecimalField(
-        max_digits=10, decimal_places=3,
-        default=Decimal('0'),
-        verbose_name="出库数量"
-    )
-    
-    # 变更类型(从批次日志获取)
-    change_type = models.CharField(
-        max_length=20,
-        verbose_name="变动类型"
-    )
-    
-    # 变更时的库存快照
-    opening_quantity = models.DecimalField(
-        max_digits=10, decimal_places=3,
-        default=Decimal('0'),
-        verbose_name="期初数量"
-    )
-    
-    closing_quantity = models.DecimalField(
-        max_digits=10, decimal_places=3,
-        default=Decimal('0'),
-        verbose_name="期末数量"
-    
-class batchLogModel(models.Model):
 
-    LOG_TYPES = (
-        ('create', '创建'),
-        ('update', '更新'),
-        ('delete', '删除'),
-        ('out', '出库'),
-        ('cancel_out', '取消出库'),
-        ('status_change', '状态变更'),
-    )
-    bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='logs', null=True, blank=True)
-    batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, related_name='logs')
-    log_type = models.CharField(max_length=20, choices=LOG_TYPES, verbose_name='日志类型')
-    goods_code = models.CharField(max_length=50, verbose_name='货品编码')
-    goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
-    goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
-    goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
-    goods_in_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新入数量', null=True, blank=True)
-    goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出数量', null=True, blank=True)
-    detail_logs = models.ManyToManyField(
-        ContainerDetailLogModel,
-        related_name='batch_logs',
-        verbose_name='关联托盘日志'
-    )
-    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')   
 
-    
-class ContainerDetailLogModel(models.Model):
+def BatchList_cn_data_header_list():
+    return dict([
+        ('id', '序号'),
+        ('bound_number', '入库批次号'),
+        ('goods_qty', '商品数量'),
+        ('goods_in_qty', '组盘入库数量'),
+        ('goods_in_location_qty', '库位入库数量'),
+        ('goods_out_qty', '出库数量'),
+
+
+        ('sourced_number', 'ERP下发批次号'),
  
-    LOG_TYPES = (
-        ('create', '创建'),
-        ('update', '更新'),
-        ('delete', '删除'),
-        ('out', '出库'),
-        ('cancel_out', '取消出库'),
-        ('status_change', '状态变更'),
-    )
-    
-    # 关联的托盘明细
-    container_detail = models.ForeignKey(
-        ContainerDetailModel, 
-        on_delete=models.CASCADE,
-        related_name='logs'
-    )
-    
-    # 日志类型
-    log_type = models.CharField(
-        max_length=20,
-        choices=LOG_TYPES,
-        verbose_name='日志类型'
-    )
-    
-    # 原值
-    old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
-    old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True)
-    old_status = models.IntegerField(
-        choices=ContainerDetailModel.BATCH_STATUS,
-        null=True,
-        blank=True,
-        verbose_name='原状态'
-    )
-    
-    # 新值
-    new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
-    new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True)
-    new_status = models.IntegerField(
-        choices=ContainerDetailModel.BATCH_STATUS,
-        null=True,
-        blank=True,
-        verbose_name='新状态'
-    )
+        ('bound_batch_order', '批次顺序'),
+
+        ('warehouse_name', '仓库名称'),
+        ('goods_code', '商品编码'),
+        ('goods_desc', '商品描述'),
+        ('goods_std', '商品标准'),
+        ('goods_unit', '商品单位'),
+        ('goods_package', '商品包装'),
+      
+        ('container_number', '托盘数目'),
     
-    # 元信息
-    creater = models.CharField(max_length=50, verbose_name='操作人')
-    create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
-    tobatchlog = models.BooleanField(default=False, verbose_name='是否转移到批次日志')
+        ('check_status', '质检状态')
+    ])
+
+class FileBatchListRenderCN(CSVStreamingRenderer):
+    header = BatchList_file_headers_list()
+    labels = BatchList_cn_data_header_list()
 
-"""
 def  MaterialChangeHistory_file_headers_list():
     return [
         'id',

+ 39 - 0
reportcenter/filter.py

@@ -1,5 +1,6 @@
 from django_filters import FilterSet
 from .models import flowModel,bigScreenModel
+from  bound.models import BoundBatchModel
 from django_filters import rest_framework as filters
 
 from container.models import MaterialChangeHistory,batchLogModel,ContainerDetailLogModel
@@ -12,6 +13,44 @@ class bigScreenFilter(FilterSet):
             'day_out': ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
             'day': ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
         }
+
+class batchfilter(FilterSet):
+    class Meta:
+        model = BoundBatchModel
+        fields = {
+            "bound_number" : ['exact', 'icontains'],
+            "sourced_number" : ['exact', 'icontains'],
+            "bound_month" : ['exact', 'icontains'],
+            "bound_batch_order" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "warehouse_code" : ['exact', 'icontains'],
+            "warehouse_name" : ['exact', 'icontains'],
+            "goods_code" : ['exact', 'icontains'],
+            "goods_desc" : ['exact', 'icontains'],
+            "goods_std" : ['exact', 'icontains'],
+            "goods_unit" : ['exact', 'icontains'],
+            "goods_qty" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_package" : ['exact', 'icontains'],
+            "goods_in_qty" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_in_location_qty" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_out_qty" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_reserve_qty" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "status" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "container_number" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_weight" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "goods_total_weight" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "note" : ['exact', 'icontains'],
+            "creater" : ['exact', 'icontains'],
+            "openid" : ['exact', 'icontains'],
+            "is_delete" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "create_time" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "update_time" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "relate_material" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in'],
+            "check_status" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "check_user" : ['exact', 'icontains'],
+            "check_time" : ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+        }
+ 
+
 class FlowFilter(FilterSet):
     class Meta:
         model = flowModel

+ 18 - 0
reportcenter/serializers.py

@@ -1,8 +1,26 @@
 from rest_framework import serializers
 from .models import flowModel,bigScreenModel
+from bound.models import BoundBatchModel
 from container.models import MaterialChangeHistory,batchLogModel,ContainerDetailLogModel
 from decimal import Decimal
 
+class batchlistSerializer(serializers.ModelSerializer):
+    check_status =serializers.SerializerMethodField()
+    class Meta:
+        model = BoundBatchModel
+        exclude = ['is_delete', 'create_time', 'update_time', 
+                   'relate_material','goods_weight','goods_total_weight','warehouse_code','bound_month','goods_reserve_qty','check_time','check_user','status',
+                   'creater','openid']
+        ordering = ['-id','bound_month', 'bound_batch_order']
+    def get_check_status(self, obj):
+        mapping = {
+            0: '待质检',
+            1: '质检合格',
+            2: '质检不合格'
+            }
+        return mapping.get(obj.check_status, '未知状态')
+
+
 class bigScreenSerializer(serializers.ModelSerializer):
     class Meta:
         model = bigScreenModel

+ 4 - 0
reportcenter/urls.py

@@ -17,4 +17,8 @@ urlpatterns = [
 
     path(r'bigScreen/day/', views.bigScreenModelViewSet.as_view({"get": "list",  }), name="management"),
 
+    path(r'batchlist/', views.BatchListViewSet.as_view({"get": "list",  }), name="management"),
+    path(r'batchfile/', views.BatchFileViewSet.as_view({"get": "list",  }), name="management"),
+
+
 ]

+ 96 - 63
reportcenter/views.py

@@ -12,9 +12,10 @@ from rest_framework import status
 
 from reportcenter.models import flowModel as flowlist
 from .models import bigScreenModel
+from bound.models import BoundBatchModel
 from . import serializers
-from .filter import FlowFilter, MaterialChangeHistoryFilter, batchLogFilter, ContainerDetailLogFilter,bigScreenFilter
-from .files import  FileFlowListRenderCN, MaterialChangeHistoryRenderCN, batchLogRenderCN, ContainerDetailLogRenderCN
+from .filter import FlowFilter, MaterialChangeHistoryFilter, batchLogFilter, ContainerDetailLogFilter,bigScreenFilter,batchfilter
+from .files import  FileFlowListRenderCN, MaterialChangeHistoryRenderCN, batchLogRenderCN, ContainerDetailLogRenderCN,FileBatchListRenderCN
 
 from container.models import MaterialChangeHistory,batchLogModel,ContainerDetailLogModel
 
@@ -843,67 +844,99 @@ class FlowsStatsViewSet(viewsets.ModelViewSet):
         else:
             return self.http_method_not_allowed(request=self.request)
 
-    # def create(self, request, *args, **kwargs):
-    #     data = request.data.copy()
-    #     warehouse_code = data.get('warehouse_code')
-    #     warehouse_name = data.get('warehouse_name')
-    #     shelf_type = data.get('shelf_type')
-    #     shelf_name = data.get('shelf_name')
-        
-    #     rows = int(data.get('rows', 0))
-    #     cols = int(data.get('cols', 0))
-    #     layers = int(data.get('layers', 0))
-
-        
-    #     existing_positions = flowlist.get_existing_positions(warehouse_code,shelf_name, rows, cols, layers)
-
-    #     # 生成所有可能的坐标
-    #     instances = []
-    #     for r in range(1, rows+1):
-    #         for c in range(1, cols+1):
-    #             for l in range(1, layers+1):
-    #                 if (r, c, l) not in existing_positions  :
-    #                     instances.append(flowlist(
-    #                         warehouse_code=warehouse_code,
-    #                         warehouse_name=warehouse_name,
-    #                         shelf_type=shelf_type,
-    #                         shelf_name=shelf_name,
-    #                         row=r,
-    #                         col=c,
-    #                         layer=l,
-
-    #                     ))
-    #     try:
-    #         with transaction.atomic():
-    #             flowlist.objects.bulk_create(instances, batch_size=1000)
-    #     except Exception as e:
-    #         return Response(
-    #             {"error": str(e)},
-    #             status=status.HTTP_500_INTERNAL_SERVER_ERROR
-    #         )
-
-    #     return Response({
-    #         "created": len(instances),
-    #         "skipped": (rows*cols*layers) - len(instances)
-    #     }, status=status.HTTP_201_CREATED)
-        
-    # def update(self, request, pk):
-    #     qs = self.get_object()
-    #     print(qs.id)
-    #     # return Response({"detail": "暂不支持修改"}, status=status.HTTP_400_BAD_REQUEST)
-       
-    #     data = self.request.data
-    #     print(data)
-    #     serializer = self.get_serializer(qs, data=data)
-    #     serializer.is_valid(raise_exception=True)
-    #     serializer.save()
-    #     headers = self.get_success_headers(serializer.data)
-    #     return Response(serializer.data, status=200, headers=headers)
-
-    # def retrieve(self, request, pk):
-    #     qs = self.get_object()
-    #     serializer = self.get_serializer(qs)
-    #     return Response(serializer.data)
+
+class BatchListViewSet(viewsets.ModelViewSet):
+
+    filter_backends = [DjangoFilterBackend, OrderingFilter, ]
+    ordering_fields = ['id', "create_time", "update_time", ]
+
+    filter_class = batchfilter
+    pagination_class = MyPageNumberPagination
+   
+
+
+    """
+        list:
+            Response a data list(all)
+        post:
+            Create a new data(create)
+    """
+    
+
+    def get_project(self):
+        try:
+            id = self.kwargs.get('pk')
+            return id
+        except:
+            return None
+    
+
+    def get_queryset(self):
+        id = self.get_project()
+
+        if self.request.user:
+            if id is None:
+                return BoundBatchModel.objects.filter()
+            else:
+                return BoundBatchModel.objects.filter(id=id)
+        else:
+            return BoundBatchModel.objects.none()
+
+    def get_serializer_class(self):
+        if self.action in ['list']:
+            return serializers.batchlistSerializer
+        # elif self.action in ['retrieve','update',]:
+        #     return serializers.stocklistpartialSerializer
+        else:
+            return self.http_method_not_allowed(request=self.request)
+
+class BatchFileViewSet(viewsets.ModelViewSet):
+    renderer_classes = (FileBatchListRenderCN, ) + tuple(api_settings.DEFAULT_RENDERER_CLASSES)
+    filter_backends = [DjangoFilterBackend, OrderingFilter, ]
+    ordering_fields = [ "bound_month" ,"bound_batch_order", "update_time", ]
+    filter_class = batchfilter
+
+    def get_project(self):
+        try:
+            id = self.kwargs.get('pk')
+            return id
+        except:
+            return None
+
+    def get_queryset(self):
+        id = self.get_project()
+        if self.request.user:
+            if id is None:
+                return BoundBatchModel.objects.filter()
+            else:
+                return BoundBatchModel.objects.filter(id=id)
+        else:
+            return BoundBatchModel.objects.none()
+
+    def get_serializer_class(self):
+        if self.action in ['list']:
+            return serializers.batchlistSerializer
+        else:
+            return self.http_method_not_allowed(request=self.request)
+
+    def get_render(self, data):
+        return FileBatchListRenderCN().render(data)
+
+
+    def list(self, request, *args, **kwargs):
+        from datetime import datetime
+        dt = datetime.now()
+        data = (
+            serializers.batchlistSerializer(instance).data
+            for instance in self.filter_queryset(self.get_queryset())
+        )
+        renderer = self.get_render(data)
+        response = StreamingHttpResponse(
+            renderer,
+            content_type="text/csv"
+        )
+        response['Content-Disposition'] = "attachment; filename='批次报表_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        return response
 
 class bigScreenModelViewSet(viewsets.ModelViewSet):
     filter_backends = [DjangoFilterBackend, OrderingFilter, ]

+ 1 - 0
templates/dist/spa/css/13.8b9d1b53.css

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

templates/dist/spa/css/13.84ffb38a.css → templates/dist/spa/css/14.84ffb38a.css


templates/dist/spa/css/14.285d986e.css → templates/dist/spa/css/15.285d986e.css


templates/dist/spa/css/15.80435c82.css → templates/dist/spa/css/16.80435c82.css


templates/dist/spa/css/16.074df1c7.css → templates/dist/spa/css/17.074df1c7.css


templates/dist/spa/css/17.f57b1220.css → templates/dist/spa/css/18.f57b1220.css


templates/dist/spa/css/18.9e7bbbba.css → templates/dist/spa/css/19.9e7bbbba.css


templates/dist/spa/css/19.296f042c.css → templates/dist/spa/css/20.296f042c.css


templates/dist/spa/css/20.8fe0c02a.css → templates/dist/spa/css/21.8fe0c02a.css


templates/dist/spa/css/21.abdf7366.css → templates/dist/spa/css/22.abdf7366.css


templates/dist/spa/css/22.83beb898.css → templates/dist/spa/css/23.83beb898.css


templates/dist/spa/css/23.601677c3.css → templates/dist/spa/css/24.601677c3.css


templates/dist/spa/css/24.6dea747f.css → templates/dist/spa/css/25.6dea747f.css


templates/dist/spa/css/25.f721cf95.css → templates/dist/spa/css/26.f721cf95.css


templates/dist/spa/css/26.ed8e81e9.css → templates/dist/spa/css/27.ed8e81e9.css


templates/dist/spa/css/27.eed22a1c.css → templates/dist/spa/css/28.eed22a1c.css


templates/dist/spa/css/28.2f5a0931.css → templates/dist/spa/css/29.2f5a0931.css


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
templates/dist/spa/css/3.4891132a.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
templates/dist/spa/css/3.8440293a.css


templates/dist/spa/css/29.e6923623.css → templates/dist/spa/css/30.e6923623.css


templates/dist/spa/css/30.01a9029f.css → templates/dist/spa/css/31.01a9029f.css


templates/dist/spa/css/31.9b0c5133.css → templates/dist/spa/css/32.9b0c5133.css


templates/dist/spa/css/32.0d4c4716.css → templates/dist/spa/css/33.0d4c4716.css


templates/dist/spa/css/33.c527e777.css → templates/dist/spa/css/34.c527e777.css


templates/dist/spa/css/34.8f3f6188.css → templates/dist/spa/css/35.8f3f6188.css


templates/dist/spa/css/35.44ddcebd.css → templates/dist/spa/css/36.44ddcebd.css


templates/dist/spa/css/36.2ac1dad1.css → templates/dist/spa/css/37.2ac1dad1.css


templates/dist/spa/css/37.12670fd1.css → templates/dist/spa/css/38.12670fd1.css


templates/dist/spa/css/38.9478c981.css → templates/dist/spa/css/39.9478c981.css


templates/dist/spa/css/39.c4652654.css → templates/dist/spa/css/40.c4652654.css


templates/dist/spa/css/40.7a23b7fb.css → templates/dist/spa/css/41.7a23b7fb.css


templates/dist/spa/css/41.2594d0b9.css → templates/dist/spa/css/42.2594d0b9.css


templates/dist/spa/css/42.0faa4aeb.css → templates/dist/spa/css/43.0faa4aeb.css


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/index.html


BIN
templates/dist/spa/js/13.07a76275.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
templates/dist/spa/js/13.ac106c63.js


BIN
templates/dist/spa/js/13.ac106c63.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/13.07a76275.js


BIN
templates/dist/spa/js/14.0b3059c5.js.gz


BIN
templates/dist/spa/js/14.8aad49d1.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/14.8aad49d1.js


BIN
templates/dist/spa/js/15.b7dd6c80.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/15.9a14922a.js


BIN
templates/dist/spa/js/15.9a14922a.js.gz


BIN
templates/dist/spa/js/17.35a9a046.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/16.499d4616.js


BIN
templates/dist/spa/js/16.499d4616.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/17.35a9a046.js


BIN
templates/dist/spa/js/18.18213a6e.js.gz


BIN
templates/dist/spa/js/18.e5ecfb11.js.gz


BIN
templates/dist/spa/js/19.4e20799a.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/18.e5ecfb11.js


BIN
templates/dist/spa/js/19.b1cca1b7.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/19.4e20799a.js


BIN
templates/dist/spa/js/20.4730a8aa.js.gz


BIN
templates/dist/spa/js/20.5869c533.js.gz


BIN
templates/dist/spa/js/21.43a20687.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/20.5869c533.js


BIN
templates/dist/spa/js/21.6eee568e.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/21.43a20687.js


BIN
templates/dist/spa/js/22.ea906fdf.js.gz


BIN
templates/dist/spa/js/23.7d6e0d41.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/22.f4d41e96.js


BIN
templates/dist/spa/js/22.f4d41e96.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/23.7d6e0d41.js


BIN
templates/dist/spa/js/24.31e1a220.js.gz


BIN
templates/dist/spa/js/24.cb0c46eb.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/24.cb0c46eb.js


BIN
templates/dist/spa/js/25.91acd9c1.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/25.2de0f4a3.js


BIN
templates/dist/spa/js/25.2de0f4a3.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/26.7d399143.js


BIN
templates/dist/spa/js/26.7d399143.js.gz


BIN
templates/dist/spa/js/28.8d0422a3.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/27.02dd357d.js


BIN
templates/dist/spa/js/27.02dd357d.js.gz


BIN
templates/dist/spa/js/29.1bae39fb.js.gz


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
templates/dist/spa/js/28.8d0422a3.js


BIN
templates/dist/spa/js/29.64a81974.js.gz


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
templates/dist/spa/js/3.5b010f7e.js


+ 0 - 0
templates/dist/spa/js/3.5b010f7e.js.gz


Некоторые файлы не были показаны из-за большого количества измененных файлов