浏览代码

恢复变更历史

flower_bs 1 月之前
父节点
当前提交
8102f11f48

+ 2 - 1
backup/views.py

@@ -22,8 +22,9 @@ def scheduled_backup():
         logger.info(f"定时备份完成: {backup_path}")
         # 更新托盘分类任务(如果存在)
         try:
-            from container.utils import update_container_categories_task
+            from container.utils import update_container_categories_task,reconcile_material_history
             update_container_categories_task()
+            reconcile_material_history()
             logger.info(f"定时更新托盘分类完成")
         except ImportError:
             logger.warning("更新托盘分类模块未找到,跳过更新")

+ 18 - 0
container/migrations/0034_materialchangehistory_count_time.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2025-09-23 22:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0033_alter_containerwcsmodel_tasknumber'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='materialchangehistory',
+            name='count_time',
+            field=models.IntegerField(blank=True, default=0, null=True, verbose_name='统计次数'),
+        ),
+    ]

+ 5 - 13
container/models.py

@@ -469,15 +469,7 @@ def update_material_history(sender, instance, created, **kwargs):
     if created:
         # 创建物料变动历史记录
         create_material_history(instance, update=True)
-    
-    else:
-        # 更新物料变动历史记录
-        MaterialChangeHistory_obj =MaterialChangeHistory.objects.get(batch_log=instance)
-        MaterialChangeHistory_obj.in_quantity = instance.goods_in_qty or Decimal('0')
-        MaterialChangeHistory_obj.out_quantity = instance.goods_out_qty or Decimal('0')
-        MaterialChangeHistory_obj.change_type = instance.log_type
-        MaterialChangeHistory_obj.closing_quantity = MaterialChangeHistory_obj.opening_quantity + MaterialChangeHistory_obj.in_quantity - MaterialChangeHistory_obj.out_quantity
-        MaterialChangeHistory_obj.save()
+
 
 def create_material_history( instance, update):
     """为每条批次日志创建物料变动历史记录"""
@@ -623,9 +615,9 @@ def handle_container_detail_log(sender, instance, created, **kwargs):
             # 队列满时同步处理当前日志
             aggregate_to_batch_log(instance)
             count_day_in_out(instance)
-            if created:
-                # 创建物料变动历史记录
-                create_material_history(instance, update=True)
+            # if created:
+            #     # 创建物料变动历史记录
+            #     create_material_history(instance, update=True)
 
 @receiver(post_save, sender=ContainerDetailModel)
 def container_detail_post_save(sender, instance, created, **kwargs):
@@ -652,7 +644,7 @@ class MaterialChangeHistory(models.Model):
     
     # 与批次日志建立多对一关系
     batch_log = models.ForeignKey(batchLogModel,on_delete=models.CASCADE, related_name='material_history', verbose_name="关联批次日志",primary_key=False)
-
+    count_time = models.IntegerField(verbose_name='统计次数',default=0,null=True, blank=True)
     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)

+ 54 - 2
container/utils.py

@@ -127,6 +127,58 @@ 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
+    logger.info(f"后台任务更新托盘分类: 成功 {result['updated']}, 失败 {len(result['errors'])}")
+    return result
+
+def reconcile_material_history():
+    """从后往前修复物料变动历史记录"""
+    from container.models import MaterialStatistics, MaterialChangeHistory
+
+
+    all_materials = MaterialStatistics.objects.all()
+    
+    for material in all_materials:
+        # 获取该物料的所有变动历史记录(按时间倒序)
+        history_records = MaterialChangeHistory.objects.filter(
+            goods_code = material.goods_code
+        ).order_by('-change_time')
+        
+        if not history_records.exists():
+            continue
+            
+        # 从物料统计中获取当前库存作为最后一个记录的期末数量
+        current_quantity = material.total_quantity
+        
+        # 从后往前处理每个历史记录
+        for i, record in enumerate(history_records):
+            # 最后一个记录:使用当前库存作为期末数量
+            if i == 0:
+                record.closing_quantity = current_quantity
+            else:
+                # 前一个记录的期末数量就是当前记录的期初数量
+                record.closing_quantity = history_records[i-1].opening_quantity
+            
+            # 计算期初数量(期末 + 出库 - 入库)
+            record.opening_quantity = (
+                record.closing_quantity 
+                + record.out_quantity 
+                - record.in_quantity
+            )
+            
+            # 更新记录
+            record.save()
+            
+            # 更新当前数量为当前记录的期初数量(用于下一个记录)
+            current_quantity = record.opening_quantity
+        
+        # 验证第一个记录的期初数量是否合理
+        first_record = history_records.last()
+        if first_record.opening_quantity < 0:
+            logger.info(f"警告:物料 {material.goods_code} 的期初库存为负值: {first_record.opening_quantity}")
+        
+        # # 更新物料统计的总库存(应该与最后一个记录的期末数量一致)
+        # material.total_quantity = history_records.first().closing_quantity
+        # material.save()
+    
+    logger.info("物料变动历史记录修复完成")

+ 105 - 0
data_base/test_move copy.py

@@ -0,0 +1,105 @@
+import os
+import django
+import sys
+from decimal import Decimal
+from django.db import transaction
+from django.db.models import F
+import unittest
+from datetime import datetime, timedelta
+
+def setup_django():
+    """设置Django环境"""
+    project_path = "D:/code/vue/greater_wms"
+    sys.path.append(project_path)
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'greaterwms.settings')
+    django.setup()
+    print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Django环境已设置")
+
+
+
+def reconcile_material_history():
+    """从后往前修复物料变动历史记录"""
+    from container.models import MaterialStatistics, MaterialChangeHistory
+    
+    # 获取所有物料统计
+    all_materials = MaterialStatistics.objects.all()
+    
+    for material in all_materials:
+        # 获取该物料的所有变动历史记录(按时间倒序)
+        print(f"处理物料 {material.goods_code} 的变动历史记录...")
+        print(f"获取物料 {material.goods_code} 的所有变动历史记录...")
+        history_records = MaterialChangeHistory.objects.filter(
+            goods_code = material.goods_code
+        ).order_by('-change_time')
+        
+        if not history_records.exists():
+            continue
+        
+        print(f"总计获取到 {history_records.count()} 条{material.goods_code}历史记录")
+
+        # 从物料统计中获取当前库存作为最后一个记录的期末数量
+        current_quantity = material.total_quantity
+        
+        # 从后往前处理每个历史记录
+        for i, record in enumerate(history_records):
+            # 最后一个记录:使用当前库存作为期末数量
+            if i == 0:
+                record.closing_quantity = current_quantity
+            else:
+                # 前一个记录的期末数量就是当前记录的期初数量
+                record.closing_quantity = history_records[i-1].opening_quantity
+            
+            # 计算期初数量(期末 + 出库 - 入库)
+            record.opening_quantity = (
+                record.closing_quantity 
+                + record.out_quantity 
+                - record.in_quantity
+            )
+            print(f"更新记录 {record.id},条目时间{record.change_time}: 期初 {record.opening_quantity}, 期末 {record.closing_quantity}, 入库 {record.in_quantity}, 出库 {record.out_quantity}")
+            # 更新记录
+            record.save()
+            
+            # 更新当前数量为当前记录的期初数量(用于下一个记录)
+            current_quantity = record.opening_quantity
+        
+        # 验证第一个记录的期初数量是否合理
+        first_record = history_records.last()
+        if first_record.opening_quantity < 0:
+            print(f"警告:物料 {material.goods_code} 的期初库存为负值: {first_record.opening_quantity}")
+        
+        # # 更新物料统计的总库存(应该与最后一个记录的期末数量一致)
+        # material.total_quantity = history_records.first().closing_quantity
+        # material.save()
+    
+    print("物料变动历史记录修复完成")
+
+
+
+
+
+def main():
+    """主测试函数"""
+    setup_django()
+    
+    print("\n===== 开始物料历史记录修复测试 =====")
+    
+    try:
+
+        
+
+        reconcile_material_history()
+        
+
+    
+    except Exception as e:
+        print(f"\n❌ 测试异常: {e}")
+        import traceback
+        traceback.print_exc()
+    
+    finally:
+        # 清理测试数据
+
+        print("\n===== 测试结束 =====")
+
+if __name__ == "__main__":
+    main()

文件差异内容过多而无法显示
+ 0 - 1189
migration_log_20250923_132621.txt


+ 0 - 10
migration_log_20250923_133057.txt

@@ -1,10 +0,0 @@
-迁移日志文件: migration_log_20250923_133057.txt
-连接成功: SQLite(db.sqlite3) → PostgreSQL(wmsdb)
-
-是否在迁移前清空 PostgreSQL 数据库?
-1. 清空所有数据 (确保无重复)
-2. 不清空 (可能产生重复数据)
-3. 退出
-
-警告: 此操作将清空 PostgreSQL 数据库中的所有数据!
-日志文件已保存: migration_log_20250923_133057.txt

文件差异内容过多而无法显示
+ 0 - 1189
migration_log_20250923_133221.txt


+ 1 - 1
reportcenter/urls.py

@@ -4,7 +4,7 @@ urlpatterns = [
     path(r'flow/', views.FlowsStatsViewSet.as_view({"get": "list",  }), name="management"),
     path(r'file/', views.FileListDownloadView.as_view({"get": "list"}), name="flowfile"),
 
-    path(r'MaterialChangeHistory/', views.MaterialChangeHistoryViewSet.as_view({"get": "list",  }), name="management"),
+    path(r'MaterialChangeHistory/', views.MaterialChangeHistoryViewSet.as_view({"get": "list","post": "summary" }), name="management"),
     path(r'MaterialChangeHistory/file/', views.MaterialChangeHistoryDownloadView.as_view({"get": "list"}), name="flowfile"),    
 
     path(r'batchLog/', views.batchLogViewSet.as_view({"get": "list",  }), name="management"),

+ 196 - 0
reportcenter/views.py

@@ -30,6 +30,18 @@ from container.models import MaterialChangeHistory,batchLogModel,ContainerDetail
     path(r'ContainerDetailLog/file/', views.ContainerDetailLogDownloadView.as_view({"get": "list"}), name="flowfile"),    
 
 """
+from django.utils import timezone
+from datetime import datetime, timedelta
+from rest_framework.decorators import action
+from rest_framework.response import Response
+from django.db.models import Q, Max, Min
+from decimal import Decimal
+from django.db.models import Sum
+from django.db.models import OuterRef, Subquery
+
+
+    
+
 
 class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
     filter_backends = [DjangoFilterBackend, OrderingFilter, ]
@@ -46,6 +58,188 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         else:
             return self.http_method_not_allowed(request=self.request)
         
+    
+    def summary(self, request):
+        """
+        获取物料库存变动汇总数据(支持分页)
+        请求参数(JSON):
+        - start_time: 开始时间(可选)
+        - end_time: 结束时间(可选)
+        - material_ids: 物料ID列表(可选)
+        - page: 页码(可选)
+        - page_size: 每页数量(可选)
+        """
+        # 获取请求参数
+        start_time = request.data.get('start_time')
+        end_time = request.data.get('end_time')
+        material_ids = request.data.get('material_ids', [])
+        
+        # 记录查询时间范围
+        query_time_range = {}
+        
+        # 如果没有提供时间段,使用当前月份
+        if not start_time or not end_time:
+            today = timezone.now().date()
+            start_time = datetime(today.year, today.month, 1)
+            end_time = start_time + timedelta(days=32)
+            end_time = datetime(end_time.year, end_time.month, 1) - timedelta(days=1)
+            end_time = datetime.combine(end_time, datetime.max.time())
+            query_time_range['default_time_range'] = True
+        else:
+            query_time_range['default_time_range'] = False
+        
+        # 转换为datetime对象
+        if isinstance(start_time, str):
+            start_time = datetime.fromisoformat(start_time)
+        if isinstance(end_time, str):
+            end_time = datetime.fromisoformat(end_time)
+        
+        # 存储查询时间范围用于返回
+        query_time_range['start_time'] = start_time.isoformat()
+        query_time_range['end_time'] = end_time.isoformat()
+        
+        # 创建基础查询集
+        queryset = MaterialChangeHistory.objects.filter(
+            change_time__gte=start_time,
+            change_time__lte=end_time
+        )
+        
+        # 如果有物料ID过滤
+        if material_ids:
+            queryset = queryset.filter(material_stats_id__in=material_ids)
+        
+                # 获取期初数量(时间段开始时的库存)
+        opening_subquery = MaterialChangeHistory.objects.filter(
+            material_stats_id=OuterRef('material_stats_id'),
+            change_time__lt=start_time
+        ).order_by('-change_time').values('closing_quantity')[:1]
+        
+        opening_data = MaterialChangeHistory.objects.filter(
+            change_time__lt=start_time
+        )
+        if material_ids:
+            opening_data = opening_data.filter(material_stats_id__in=material_ids)
+        
+        opening_data = opening_data.values('material_stats_id').annotate(
+            opening_quantity=Subquery(opening_subquery)
+        )
+        
+        # 获取期末数量(时间段结束时的库存)
+        closing_subquery = MaterialChangeHistory.objects.filter(
+            material_stats_id=OuterRef('material_stats_id'),
+            change_time__lte=end_time
+        ).order_by('-change_time').values('closing_quantity')[:1]
+        
+        closing_data = MaterialChangeHistory.objects.filter(
+            change_time__lte=end_time
+        )
+        if material_ids:
+            closing_data = closing_data.filter(material_stats_id__in=material_ids)
+        
+        closing_data = closing_data.values('material_stats_id').annotate(
+            closing_quantity=Subquery(closing_subquery)
+        )
+        
+        # 计算期间出入库总量
+        period_data = queryset.values('material_stats_id').annotate(
+            total_in=Sum('in_quantity'),
+            total_out=Sum('out_quantity')
+        )
+        
+        # 构建结果字典
+        result = {}
+        for item in opening_data:
+            material_id = item['material_stats_id']
+            goods=MaterialChangeHistory.objects.filter(material_stats_id=material_id).first()
+            result.setdefault(material_id, {
+                'material_id': material_id,
+                'goods_code': goods.goods_code if goods else 'N/A',
+                'goods_desc': goods.goods_desc if goods else 'N/A',
+                'goods_unit': goods.goods_unit if goods else 'N/A',
+                'opening_quantity': item['opening_quantity'] or Decimal('0'),
+                'closing_quantity': Decimal('0'),
+                'total_in': Decimal('0'),
+                'total_out': Decimal('0'),
+                'net_change': Decimal('0'),  # 新增:净含量变化
+                'theoretical_change': Decimal('0'),  # 新增:理论变化量
+                'is_consistent': True  # 新增:一致性校验
+            })
+        
+        for item in closing_data:
+            material_id = item['material_stats_id']
+            goods = MaterialChangeHistory.objects.filter(material_stats_id=material_id).first()
+
+            if material_id in result:
+                result[material_id]['closing_quantity'] = item['closing_quantity'] or Decimal('0')
+            else:
+                result[material_id] = {
+                    'material_id': material_id,
+                    'goods_code': goods.goods_code if goods else 'N/A',
+                    'goods_desc': goods.goods_desc if goods else 'N/A',
+                    'goods_unit': goods.goods_unit if goods else 'N/A',
+                    'opening_quantity': Decimal('0'),
+                    'closing_quantity': item['closing_quantity'] or Decimal('0'),
+                    'total_in': Decimal('0'),
+                    'total_out': Decimal('0'),
+                    'net_change': Decimal('0'),
+                    'theoretical_change': Decimal('0'),
+                    'is_consistent': True
+                }
+        
+        for item in period_data:
+            material_id = item['material_stats_id']
+            goods = MaterialChangeHistory.objects.filter(material_stats_id=material_id).first()
+            if material_id in result:
+                result[material_id]['total_in'] = item['total_in'] or Decimal('0')
+                result[material_id]['total_out'] = item['total_out'] or Decimal('0')
+            else:
+                result[material_id] = {
+                    'material_id': material_id,
+                    'goods_code': goods.goods_code if goods else 'N/A',
+                    'goods_desc': goods.goods_desc if goods else 'N/A',
+                    'goods_unit': goods.goods_unit if goods else 'N/A',
+                    'opening_quantity': Decimal('0'),
+                    'closing_quantity': Decimal('0'),
+                    'total_in': item['total_in'] or Decimal('0'),
+                    'total_out': item['total_out'] or Decimal('0'),
+                    'net_change': Decimal('0'),
+                    'theoretical_change': Decimal('0'),
+                    'is_consistent': True
+                }
+        
+        # 计算净含量变化和理论变化量,并进行一致性校验
+        for material_id, data in result.items():
+            # 计算净含量变化(期末 - 期初)
+            net_change = data['closing_quantity'] - data['opening_quantity']
+            data['net_change'] = net_change
+            
+            # 计算理论变化量(入库 - 出库)
+            theoretical_change = data['total_in'] - data['total_out']
+            data['theoretical_change'] = theoretical_change
+            
+            # 检查是否一致(允许小数点后3位的差异)
+            tolerance = Decimal('0.001')
+            is_consistent = abs(net_change - theoretical_change) <= tolerance
+            data['is_consistent'] = is_consistent
+        
+        # 转换为列表格式
+        result_list = list(result.values())
+        
+        # 应用分页
+        paginator = MyPageNumberPagination()
+        page = paginator.paginate_queryset(result_list, request)
+        
+        # 构建响应数据
+        response_data = {
+            'query_time_range': query_time_range,
+            'count': len(result_list),
+            'next': paginator.get_next_link(),
+            'previous': paginator.get_previous_link(),
+            'results': page
+        }
+        
+        return Response(response_data)
+        
 class MaterialChangeHistoryDownloadView(viewsets.ModelViewSet):
     renderer_classes = (MaterialChangeHistoryRenderCN, ) + tuple(api_settings.DEFAULT_RENDERER_CLASSES)
     filter_backends = [DjangoFilterBackend, OrderingFilter, ]
@@ -102,6 +296,8 @@ class MaterialChangeHistoryDownloadView(viewsets.ModelViewSet):
         response['Content-Disposition'] = "attachment; filename='MaterialChangeHistory_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         return response
 
+    
+
 class batchLogViewSet(viewsets.ModelViewSet):
     filter_backends = [DjangoFilterBackend, OrderingFilter, ]
     ordering_fields = ['id', "create_time", "update_time", ]

+ 4 - 4
templates/src/layouts/MainLayout.vue

@@ -308,13 +308,13 @@
           <q-separator />
           <q-item
             clickable
-            :to="{ name: 'MaterialChangeHistory' }"
-            @click="linkChange('MaterialChangeHistory')"
+            :to="{ name: 'flows_statements' }"
+            @click="linkChange('flows_statements')"
             v-ripple
             exact
-            :active="link === 'MaterialChangeHistory' && link !== ''"
+            :active="link === 'flows_statements' && link !== ''"
             :class="{
-              'my-menu-link': link === 'MaterialChangeHistory' && link !== '',
+              'my-menu-link': link === 'flows_statements' && link !== '',
             }"
           >
             <q-item-section avatar><q-icon name="auto_graph" /></q-item-section>

+ 1 - 1
templates/src/pages/count/count.vue

@@ -10,7 +10,7 @@
           <q-route-tab name="batch" :label="'批次状态'"  icon="img:statics/inbound/more.png" :to="{ name: 'batch' }" exact/>
         </transition>
         <transition appear enter-active-class="animated zoomIn">
-          <q-route-tab name="batchlog" :label="'批次流水'"  icon="img:statics/dashboard/in_and_out_statement.svg" :to="{ name: 'batchlog' }" exact/>
+          <q-route-tab name="countbatchlog" :label="'批次流水'"  icon="img:statics/dashboard/in_and_out_statement.svg" :to="{ name: 'countbatchlog' }" exact/>
         </transition>
         <transition appear enter-active-class="animated zoomIn">
           <q-route-tab name="detaillog" :label="'托盘日志'"  icon="img:statics/inbound/polist.png" :to="{ name: 'detaillog' }" exact/>

+ 15 - 6
templates/src/pages/dashboard/dashboard.vue

@@ -4,6 +4,15 @@
       <div class="q-pa-md">
         <div class="q-gutter-y-md" style="max-width: 100%">
           <q-tabs v-model="detaillink">
+            <transition appear enter-active-class="animated zoomIn">
+              <q-route-tab
+                name="status_statements"
+                :label="$t('dashboards.status_statements')"
+                icon="manage_search"
+                :to="{ name: 'flows_statements' }"
+                exact
+              />
+            </transition>
             <transition appear enter-active-class="animated zoomIn">
               <q-route-tab
                 name="MaterialChangeHistory"
@@ -68,12 +77,12 @@
 
 <script>
 export default {
-  name: "Pagedashboard",
-  data() {
+  name: 'Pagedashboard',
+  data () {
     return {
-      detaillink: "inboundAndOutbound",
-    };
+      detaillink: 'inboundAndOutbound'
+    }
   },
-  methods: {},
-};
+  methods: {}
+}
 </script>

文件差异内容过多而无法显示
+ 584 - 1115
templates/src/pages/dashboard/flows_statements.vue


+ 664 - 0
templates/src/pages/dashboard/predeliverystock.vue

@@ -0,0 +1,664 @@
+<template>
+  <div>
+    <transition appear enter-active-class="animated fadeIn">
+      <q-table
+        class="my-sticky-header-column-table shadow-24"
+        :data="table_list"
+        row-key="id"
+        :separator="separator"
+        :loading="loading"
+        :columns="columns"
+        hide-bottom
+        :pagination.sync="pagination"
+        no-data-label="No data"
+        no-results-label="No data you want"
+        :table-style="{ height: height }"
+        flat
+        bordered
+      >
+          <template v-slot:header-cell="props">
+          <q-th :props="props" @dblclick="handleHeaderDblClick(props.col)">
+            <!-- 为特定列添加下拉选择器 -->
+            <template v-if="['bound_department'].includes(props.col.name)">
+              <q-select
+                dense
+                outlined
+                v-model="filterModels[props.col.name]"
+                :options="getFilterOptions(props.col.name)"
+                option-label="label"
+                option-value="value"
+                emit-value
+                map-options
+                clearable
+                @input="handleFilterChange"
+                style="min-width: 120px"
+              >
+                <template v-slot:prepend>
+                  <span class="text-caption">{{ props.col.label }}</span>
+                </template>
+              </q-select>
+            </template>
+            <template v-else>
+              {{ props.col.label }}
+            </template>
+          </q-th>
+        </template>
+        <template v-slot:top>
+          <q-btn-group push>
+            <q-btn :label="$t('refresh')" icon="refresh" @click="reFresh()">
+              <q-tooltip
+                content-class="bg-amber text-black shadow-4"
+                :offset="[10, 10]"
+                content-style="font-size: 12px"
+                >{{ $t("refreshtip") }}</q-tooltip
+              >
+            </q-btn>
+            <!-- <q-btn :label="'日志'" icon="logout" @click="getlog()"> </q-btn> -->
+          </q-btn-group>
+
+          <q-space />
+
+          <div class="flex items-center">
+            <q-btn-group push class="q-ml-md"> </q-btn-group>
+            <q-input
+              outlined
+              rounded
+              dense
+              debounce="300"
+              color="primary"
+              v-model="filter"
+              :placeholder="$t('search')"
+              @input="getSearchList()"
+              @keyup.enter="getSearchList()"
+            >
+              <template v-slot:append>
+                <q-icon name="search" @click="getSearchList()" />
+              </template>
+            </q-input>
+          </div>
+        </template>
+
+        <template v-slot:body="props">
+          <q-tr :props="props">
+            <q-td auto-width>
+              <q-btn
+                size="sm"
+                round
+                :icon="props.row.expand ? 'remove' : 'ballot'"
+                @click="handle_row_expand(props.row)"
+              />
+            </q-td>
+            <q-td
+              v-for="col in columns.filter((c) => c.name !== 'expand')"
+              :key="col.name"
+              :props="props"
+            >
+              {{ col.field ? props.row[col.field] : props.row[col.name] }}
+            </q-td>
+          </q-tr>
+
+          <!-- 第二级:时间轴 -->
+          <q-tr v-show="props.row.expand" :props="props" class="expanded-row">
+            <q-td colspan="100%">
+              <div class="q-pa-md timeline-wrapper">
+                <q-timeline
+                  color="#e0e0e0"
+                  v-if="props.row.batch_items?.length"
+                >
+                  <q-timeline-entry
+                    v-for="(batch_item, index) in props.row.batch_items"
+                    :key="index"
+                    class="custom-node"
+                  >
+                    <template v-slot:title>
+                      <span>
+                        <div>批次 {{ batch_item.bound_number }}</div>
+                        <div class="row">
+                          <div class="col">
+                            <div class="custom-title">
+                              {{ batch_item.goods_desc }}
+                            </div>
+                          </div>
+                          <div class="col">
+                            <div class="custom-title">
+                              计划数量:{{ batch_item.goods_qty }}
+                            </div>
+                          </div>
+
+                          <div class="col">
+                            <div class="custom-title">
+                              入库数量:{{ batch_item.goods_in_qty }}
+                            </div>
+                          </div>
+                          <div class="col">
+                            <div class="custom-title">
+                              实际在库数量:{{
+                                batch_item.goods_in_location_qty
+                              }}
+                            </div>
+                          </div>
+
+                          <div class="col">
+                            <div class="custom-title">
+                              预定/已出库数量:{{
+                                batch_item.goods_out_qty
+                              }}
+                            </div>
+                          </div>
+                        </div>
+                      </span>
+                    </template>
+                  </q-timeline-entry>
+                </q-timeline>
+                <div v-else-if="props.row.loading" class="text-center q-pa-md">
+                  <q-spinner color="primary" size="2em" />
+                  <div class="q-mt-sm">正在加载信息...</div>
+                </div>
+              </div>
+            </q-td>
+          </q-tr>
+        </template>
+      </q-table>
+    </transition>
+    <template>
+      <div v-show="max !== 0" class="q-pa-lg flex flex-center">
+        <div>{{ total }}</div>
+        <q-pagination
+          v-model="current"
+          color="black"
+          :max="max"
+          :max-pages="6"
+          boundary-links
+          @click="
+            getSearchList(current);
+            paginationIpt = current;
+          "
+        />
+        <div>
+          <input
+            v-model="paginationIpt"
+            @blur="changePageEnter"
+            @keyup.enter="changePageEnter"
+            style="width: 60px; text-align: center"
+          />
+        </div>
+      </div>
+      <div v-show="max === 0" class="q-pa-lg flex flex-center">
+        <q-btn flat push color="dark" :label="$t('no_data')"></q-btn>
+      </div>
+    </template>
+  </div>
+</template>
+<router-view />
+
+<script>
+import { getauth, postauth, putauth, deleteauth } from 'boot/axios_request'
+import { filter } from 'jszip'
+import { date, LocalStorage } from 'quasar'
+
+export default {
+  name: 'PageTask',
+  data () {
+    return {
+      createDate1: '',
+      createDate2: '',
+      date_range: '',
+      proxyDate: '',
+      date: '',
+      goods_code: '',
+      goods_desc: '',
+      openid: '',
+      login_name: '',
+      authin: '0',
+      searchUrl: '',
+      pathname: 'bound/batch/count/',
+      pathname_previous: '',
+      pathname_next: '',
+      separator: 'cell',
+      loading: false,
+      height: '',
+      viewForm: false,
+
+      table_list: [],
+      columns: [
+        {
+          name: 'expand',
+          label: '',
+          align: 'left',
+          headerStyle: 'width: 50px'
+        },
+
+        {
+          name: 'goods_code',
+          label: '存货编码',
+          field: 'goods_code',
+          align: 'center'
+        },
+        {
+          name: 'goods_desc',
+          label: '存货名称',
+          field: 'goods_desc',
+          align: 'center'
+        },
+
+        {
+          name: 'total_quantity',
+          label: '在库数目',
+          field: 'total_quantity',
+          align: 'center'
+        },
+
+        {
+          name: 'goods_unit',
+          label: '单位',
+          field: 'goods_unit',
+          align: 'center',
+          headerStyle: 'width: 20px'
+        }
+      ],
+      filter: '',
+      pagination: {
+        page: 1,
+        rowsPerPage: 11
+      },
+      current: 1,
+      max: 0,
+      total: 0,
+      paginationIpt: 1,
+      containers: {},
+      timer: null,
+            filterModels: {
+        bound_department: null
+      },
+      activeSearchField: '',
+      activeSearchLabel: '',
+      filterdata: {},
+    }
+  },
+  computed: {
+    interval () {
+      return (
+        this.$t('download_center.start') +
+        ' - ' +
+        this.$t('download_center.end')
+      )
+    }
+  },
+  methods: {
+       // 处理过滤变化
+    handleFilterChange () {
+      this.pagination.page = 1
+      this.getSearchList(1)
+    },
+
+    getFilterOptions (columnName) {
+      switch (columnName) {
+        case 'type':
+          return [
+            { label: '生产入库', value: 1 },
+            { label: '采购入库', value: 2 },
+            { label: '其他入库', value: 3 },
+            { label: '调拨入库', value: 4 }
+          ]
+        case 'bound_status':
+          return [
+            { label: '待审核', value: 0 },
+            { label: '确认无误', value: 1 }
+          ]
+        case 'bound_department':
+          return this.bound_department_list
+        default:
+          return []
+      }
+    },
+    handleHeaderDblClick (column) {
+      // 排除不需要搜索的列
+      if (['detail', 'action'].includes(column.name)) return
+
+      this.activeSearchField = column.field
+      this.activeSearchLabel = column.label
+
+      // 弹出搜索对话框
+      this.$q
+        .dialog({
+          title: `搜索${column.label}`,
+          message: `请输入${column.label}的搜索条件`,
+          prompt: {
+            model: '',
+            type: 'text'
+          },
+          cancel: true,
+          persistent: true
+        })
+        .onOk((data) => {
+          // 执行搜索
+          this.executeColumnSearch(column.field, data)
+        })
+        .onCancel(() => {
+          this.activeSearchField = ''
+          this.activeSearchLabel = ''
+        })
+    },
+    // 执行列搜索
+    executeColumnSearch (field, value) {
+      // 构建搜索参数
+      if (
+        field === 'type' ||
+        field === 'audit_status' ||
+        field === 'save_status' ||
+        field === 'bound_status'
+      ) {
+        const searchParams = {
+          [field]: value
+        }
+        // 清除其他搜索条件
+        this.filter = ''
+        this.date_range = ''
+
+        // 执行搜索
+        this.getList({
+          ...searchParams,
+          page: 1
+        })
+        this.filterdata = searchParams
+        this.$q.notify({
+          message: `已搜索 ${this.activeSearchLabel} 含有 "${value}" 的结果`,
+          icon: 'search',
+          color: 'positive'
+        })
+
+        // 重置激活的搜索字段
+        this.activeSearchField = ''
+        this.activeSearchLabel = ''
+      } else {
+        const searchParams = {
+          [field + '__icontains']: value
+        }
+        // 清除其他搜索条件
+        this.filter = ''
+        this.date_range = ''
+
+        // 执行搜索
+        this.getList({
+          ...searchParams,
+          page: 1
+        })
+        this.filterdata = searchParams
+        this.$q.notify({
+          message: `已搜索 ${this.activeSearchLabel} 含有 "${value}" 的结果`,
+          icon: 'search',
+          color: 'positive'
+        })
+
+        // 重置激活的搜索字段
+        this.activeSearchField = ''
+        this.activeSearchLabel = ''
+      }
+    },
+    class_to_name (class_id) {
+      const class_map = {
+        1: '整盘',
+        2: '托盘组',
+        3: '零盘'
+      }
+      return class_map[class_id]
+    },
+    handle_row_expand (row) {
+      const _this = this
+      row.expand = !row.expand
+      if (row.expand) {
+        // 添加行级 loading 状态
+        _this.$set(row, 'loading', true)
+        getauth('bound/batch/count/' + row.id + '/', {})
+          .then((res) => {
+            _this.$set(row, 'batch_items', res.batch_items)
+            console.log('当前的', row.batch_items)
+          })
+          .catch((err) => {
+            _this.$q.notify({ message: err.detail, color: 'negative' })
+          })
+          .finally(() => {
+            row.loading = false // 关闭加载状态
+          })
+      }
+    },
+    getlog () {
+      // console.log(this.table_list)
+      console.log('当前loading状态:', this.loading)
+    },
+    getList (params = {}) {
+      var _this = this
+      _this.loading = true
+      // 合并基础参数
+      const baseParams = {
+        page: _this.current,
+        page_size: _this.pagination.rowsPerPage
+      }
+
+      // 创建URLSearchParams处理参数
+      const queryParams = new URLSearchParams({
+        ...baseParams,
+        ...params
+      })
+      console.log(queryParams)
+      // 过滤空值参数
+      Array.from(queryParams.entries()).forEach(([key, value]) => {
+        if (value === '' || value === null || value === undefined) {
+          queryParams.delete(key)
+        }
+      })
+
+      getauth(`${_this.pathname}?${queryParams}`)
+        .then((res) => {
+          _this.table_list = res.results.map((item) => ({
+            ...item,
+            expand: false,
+            batch_items: [],
+            loading: false
+          }))
+          _this.total = res.count
+          _this.max = Math.ceil(res.count / _this.pagination.rowsPerPage) || 0
+          _this.pathname_previous = res.previous
+          _this.pathname_next = res.next
+        })
+        .catch((err) => {
+          _this.$q.notify({
+            message: err.detail,
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+        .finally(() => {
+          _this.loading = false
+        })
+    },
+    changePageEnter () {
+      if (Number(this.paginationIpt) < 1) {
+        this.current = 1
+        this.paginationIpt = 1
+      } else if (Number(this.paginationIpt) > this.max) {
+        this.current = this.max
+        this.paginationIpt = this.max
+      } else {
+        this.current = Number(this.paginationIpt)
+      }
+      this.getSearchList(this.current)
+    },
+
+        getSearchList (page = 1) {
+      this.current = page
+      this.paginationIpt = page
+
+      // 构建过滤参数
+      const filterParams = {}
+      for (const [key, value] of Object.entries(this.filterModels)) {
+        if (value !== null && value !== '') {
+          filterParams[key] = value
+        }
+      }
+
+      this.getList({
+        number__icontains: this.filter,
+        document_date__range: this.date_range,
+        ...filterParams ,// 添加过滤条件
+        ...this.filterdata, // 添加其他过滤条件
+      })
+    },
+
+    getListPrevious () {
+      var _this = this
+      if (LocalStorage.has('auth')) {
+        getauth(_this.pathname_previous, {})
+          .then((res) => {
+            _this.table_list = res.results
+            _this.pathname_previous = res.previous
+            _this.pathname_next = res.next
+          })
+          .catch((err) => {
+            _this.$q.notify({
+              message: err.detail,
+              icon: 'close',
+              color: 'negative'
+            })
+          })
+      } else {
+      }
+    },
+    getListNext () {
+      var _this = this
+      if (LocalStorage.has('auth')) {
+        getauth(_this.pathname_next, {})
+          .then((res) => {
+            _this.table_list = res.results
+
+            _this.pathname_previous = res.previous
+            _this.pathname_next = res.next
+          })
+          .catch((err) => {
+            _this.$q.notify({
+              message: err.detail,
+              icon: 'close',
+              color: 'negative'
+            })
+          })
+      }
+    },
+    reFresh () {
+      var _this = this
+      this.filterdata = {}
+      this.filterModels = {
+        bound_department: null
+      }
+      _this.getSearchList()
+    },
+
+    updateProxy () {
+      var _this = this
+      _this.proxyDate = _this.date
+    }
+  },
+  created () {
+    var _this = this
+    if (LocalStorage.has('openid')) {
+      _this.openid = LocalStorage.getItem('openid')
+    } else {
+      _this.openid = ''
+      LocalStorage.set('openid', '')
+    }
+    if (LocalStorage.has('login_name')) {
+      _this.login_name = LocalStorage.getItem('login_name')
+    } else {
+      _this.login_name = ''
+      LocalStorage.set('login_name', '')
+    }
+    if (LocalStorage.has('auth')) {
+      const timeStamp = Date.now()
+      const formattedString = date.formatDate(timeStamp, 'YYYY/MM/DD')
+      _this.date = formattedString
+      console.log(_this.date)
+      _this.authin = '1'
+      _this.getList()
+    } else {
+      _this.authin = '0'
+    }
+  },
+  mounted () {
+    var _this = this
+    if (_this.$q.platform.is.electron) {
+      _this.height = String(_this.$q.screen.height - 290) + 'px'
+    } else {
+      _this.height = _this.$q.screen.height - 290 + '' + 'px'
+    }
+    // _this.timer = setInterval(() => {
+    //   _this.getlog()
+    // }, 1000)
+  },
+  updated () {},
+  destroyed () {},
+  // 在 watch 或方法中添加调试代码
+  watch: {
+    createDate1 (val) {
+      if (val) {
+        if (val.to) {
+          this.createDate2 = `${val.from} - ${val.to}`
+          this.date_range = `${val.from},${val.to} `
+
+          // this.downloadhUrl = this.pathname + 'filelist/?' + 'document_date__range=' + this.date_range
+        } else {
+          this.createDate2 = `${val}`
+          this.dateArray = val.split('/')
+          this.searchUrl =
+            this.pathname +
+            '?' +
+            'document_date__year=' +
+            this.dateArray[0] +
+            '&' +
+            'document_date__month=' +
+            this.dateArray[1] +
+            '&' +
+            'document_date__day=' +
+            this.dateArray[2]
+          // this.downloadhUrl = this.pathname + 'filelist/?' + 'document_date__year=' + this.dateArray[0] + '&' + 'document_date__month=' + this.dateArray[1] + '&' + 'document_date__day=' + this.dateArray[2]
+        }
+        this.date_range = this.date_range.replace(/\//g, '-')
+
+        this.getSearchList()
+        this.$refs.qDateProxy.hide()
+      } else {
+        this.createDate2 = ''
+        this.date_range = ''
+        this.getSearchList()
+      }
+    }
+  }
+}
+</script>
+<style scoped>
+/* 添加在 <style> 中 */
+.q-date__calendar-item--selected {
+  transition: all 0.3s ease;
+  background-color: #1976d2 !important;
+}
+
+.q-date__range {
+  background-color: rgba(25, 118, 210, 0.1);
+}
+
+.custom-title {
+  font-size: 0.9rem; /* 推荐使用相对单位 */
+  font-weight: 500;
+}
+/* 添加以下样式 */
+.custom-timeline {
+  --q-timeline-color: #e0e0e0; /* 覆盖时间轴线颜色变量 */
+}
+
+.custom-node .q-timeline__dot {
+  background: #485573 !important; /* 节点填充色 */
+  border: 2px solid #5c6b8c !important; /* 节点边框色 */
+}
+
+.custom-node .q-timeline__content {
+  color: #485573; /* 文字颜色 */
+}
+</style>

+ 3 - 3
templates/src/router/routes.js

@@ -98,7 +98,7 @@ const routes = [
             name: 'flows_statements',
             component: () => import('pages/dashboard/flows_statements.vue')
           },
-           {
+          {
             path: 'batchlog',
             name: 'batchlog',
             component: () => import('pages/dashboard/batchlog.vue')
@@ -217,8 +217,8 @@ const routes = [
             component: () => import('pages/count/batchoperatelog.vue')
           },
           {
-            path: 'batchlog',
-            name: 'batchlog',
+            path: 'countbatchlog',
+            name: 'countbatchlog',
             component: () => import('pages/count/batchlog.vue')
           },
           {