Browse Source

库存状态表

flower_bs 2 months ago
parent
commit
fcd0737249

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 db.sqlite3
+postgres_service_manager.log
 __pycache__/
 *.py[cod]
 *$py.class

+ 21 - 20
container/utils.py

@@ -135,7 +135,6 @@ def reconcile_material_history():
     """从后往前修复物料变动历史记录"""
     from container.models import MaterialStatistics, MaterialChangeHistory
 
-
     all_materials = MaterialStatistics.objects.all()
     
     for material in all_materials:
@@ -152,25 +151,27 @@ def reconcile_material_history():
         
         # 从后往前处理每个历史记录
         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
+            if record.count_time <= 5:
+                # 最后一个记录:使用当前库存作为期末数量
+                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.count_time = record.count_time + 1
+                record.save()
+                
+                # 更新当前数量为当前记录的期初数量(用于下一个记录)
+                current_quantity = record.opening_quantity
         
         # 验证第一个记录的期初数量是否合理
         first_record = history_records.last()

+ 22 - 19
data_base/test_move copy.py

@@ -42,25 +42,28 @@ def reconcile_material_history():
         
         # 从后往前处理每个历史记录
         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
+            if record.count_time >= 1:
+                # 最后一个记录:使用当前库存作为期末数量
+                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.count_time =  1
+                record.save()
+                
+                # 更新当前数量为当前记录的期初数量(用于下一个记录)
+                current_quantity = record.opening_quantity
+
         
         # 验证第一个记录的期初数量是否合理
         first_record = history_records.last()

+ 1 - 0
reportcenter/urls.py

@@ -5,6 +5,7 @@ urlpatterns = [
     path(r'file/', views.FileListDownloadView.as_view({"get": "list"}), name="flowfile"),
 
     path(r'MaterialChangeHistory/', views.MaterialChangeHistoryViewSet.as_view({"get": "list","post": "summary" }), name="management"),
+    path(r'flows_statements/', views.MaterialChangeHistoryViewSet.as_view({"post": "summary_complex" }), 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"),

+ 163 - 36
reportcenter/views.py

@@ -59,20 +59,21 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
             return self.http_method_not_allowed(request=self.request)
         
     
-    def summary(self, request):
+    def summary_complex(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', [])
+        # 获取请求参数 "POST /reportcenter/MaterialChangeHistory/?page=1&page_size=11&goods_code__icontains=UZ 
+
+        start_time = request.query_params.get('start_time') if request.query_params.get('start_time') else None
+        end_time = request.quary_params.get('end_time') if request.query_params.get('end_time') else None
+        material_ids_code = request.query_params.get('goods_code__icontains') if request.query_params.get('goods_code__icontains') else None
         
         # 记录查询时间范围
         query_time_range = {}
@@ -105,43 +106,45 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         )
         
         # 如果有物料ID过滤
-        if material_ids:
-            queryset = queryset.filter(material_stats_id__in=material_ids)
+        if material_ids_code:
+            queryset = queryset.filter(goods_code__icontains=material_ids_code)
+            print(f"过滤物料编码包含: {material_ids_code},剩余条目数: {queryset.count()}")
         
-                # 获取期初数量(时间段开始时的库存)
+        # 获取期初数量(时间段开始时的库存)    
         opening_subquery = MaterialChangeHistory.objects.filter(
-            material_stats_id=OuterRef('material_stats_id'),
+            goods_code=OuterRef('goods_code'),
             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)
+        if material_ids_code:
+            opening_data = opening_data.filter(goods_code__icontains=material_ids_code)
         
-        opening_data = opening_data.values('material_stats_id').annotate(
+        opening_data = opening_data.values('goods_code').annotate(
             opening_quantity=Subquery(opening_subquery)
         )
         
         # 获取期末数量(时间段结束时的库存)
         closing_subquery = MaterialChangeHistory.objects.filter(
-            material_stats_id=OuterRef('material_stats_id'),
+            goods_code=OuterRef('goods_code'),  
             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)
+        if material_ids_code:
+            closing_data = closing_data.filter(goods_code__icontains=material_ids_code)
         
-        closing_data = closing_data.values('material_stats_id').annotate(
+        closing_data = closing_data.values('goods_code').annotate(
             closing_quantity=Subquery(closing_subquery)
         )
         
         # 计算期间出入库总量
-        period_data = queryset.values('material_stats_id').annotate(
+        period_data = queryset.values('goods_code').annotate(
             total_in=Sum('in_quantity'),
             total_out=Sum('out_quantity')
         )
@@ -149,10 +152,10 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         # 构建结果字典
         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,
+            material_code = item['goods_code']
+            goods=MaterialChangeHistory.objects.filter(goods_code=material_code).first()
+            result.setdefault(material_code, {
+                'material_code': material_code,
                 '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',
@@ -166,14 +169,14 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
             })
         
         for item in closing_data:
-            material_id = item['material_stats_id']
-            goods = MaterialChangeHistory.objects.filter(material_stats_id=material_id).first()
+            material_code = item['goods_code']
+            goods=MaterialChangeHistory.objects.filter(goods_code=material_code).first()
 
-            if material_id in result:
-                result[material_id]['closing_quantity'] = item['closing_quantity'] or Decimal('0')
+            if material_code in result:
+                result[material_code]['closing_quantity'] = item['closing_quantity'] or Decimal('0')
             else:
-                result[material_id] = {
-                    'material_id': material_id,
+                result[material_code] = {
+                    'material_code': material_code,
                     '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',
@@ -187,14 +190,14 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
                 }
         
         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')
+            material_code = item['goods_code']
+            goods=MaterialChangeHistory.objects.filter(goods_code=material_code).first()
+            if material_code in result:
+                result[material_code]['total_in'] = item['total_in'] or Decimal('0')
+                result[material_code]['total_out'] = item['total_out'] or Decimal('0')
             else:
-                result[material_id] = {
-                    'material_id': material_id,
+                result[material_code] = {
+                    'material_code': material_code,
                     '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',
@@ -208,7 +211,7 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
                 }
         
         # 计算净含量变化和理论变化量,并进行一致性校验
-        for material_id, data in result.items():
+        for material_code, data in result.items():
             # 计算净含量变化(期末 - 期初)
             net_change = data['closing_quantity'] - data['opening_quantity']
             data['net_change'] = net_change
@@ -239,6 +242,130 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         }
         
         return Response(response_data)
+    
+    def summary(self, request):
+        """
+        获取物料库存变动汇总数据(支持分页)
+        """
+        # 获取请求参数
+        start_time = request.query_params.get('start_time') or None
+        end_time = request.query_params.get('end_time') or None
+        material_ids_code = request.query_params.get('goods_code__icontains') or None
+        
+        # 记录查询时间范围
+        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_code:
+            queryset = queryset.filter(goods_code__icontains=material_ids_code)
+        
+        # 获取每个物料的期初和期末数量(直接从过滤集中获取)
+        material_codes = queryset.values_list('goods_code', flat=True).distinct()
+        opening_closing_data = {}
+        
+        for code in material_codes:
+            # 获取该物料在时间段内的第一条记录(最早记录)
+            first_record = queryset.filter(goods_code=code).order_by('change_time').first()
+            # 获取该物料在时间段内的最后一条记录(最晚记录)
+            last_record = queryset.filter(goods_code=code).order_by('-change_time').first()
+            
+            # 期初数量 = 第一条记录的期初数量
+            # 期末数量 = 最后一条记录的期末数量
+            opening_closing_data[code] = {
+                'opening_quantity': first_record.opening_quantity,
+                'closing_quantity': last_record.closing_quantity
+            }
+        
+        # 计算期间出入库总量
+        period_data = queryset.values('goods_code').annotate(
+            total_in=Sum('in_quantity'),
+            total_out=Sum('out_quantity')
+        )
+        
+        # 构建结果字典
+        result = {}
+        for item in period_data:
+            material_code = item['goods_code']
+            goods = MaterialChangeHistory.objects.filter(goods_code=material_code).first()
+            
+            # 获取该物料的期初和期末数量
+            oc_data = opening_closing_data.get(material_code, {
+                'opening_quantity': Decimal('0'),
+                'closing_quantity': Decimal('0')
+            })
+            
+            result[material_code] = {
+                'material_code': material_code,
+                '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': oc_data['opening_quantity'],
+                'closing_quantity': oc_data['closing_quantity'],
+                '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_code, 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')
+            data['is_consistent'] = abs(net_change - theoretical_change) <= tolerance
+        
+        # 转换为列表格式
+        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)

+ 10 - 9
templates/src/pages/dashboard/dashboard.vue

@@ -13,6 +13,15 @@
                 exact
               />
             </transition>
+            <transition appear enter-active-class="animated zoomIn">
+              <q-route-tab
+                name="flows"
+                :label="'分拣统计'"
+                icon="img:statics/dashboard/statement.png"
+                :to="{ name: 'flows' }"
+                exact
+              />
+            </transition>
             <transition appear enter-active-class="animated zoomIn">
               <q-route-tab
                 name="MaterialChangeHistory"
@@ -49,15 +58,7 @@
                 exact
               />
             </transition>
-            <transition appear enter-active-class="animated zoomIn">
-              <q-route-tab
-                name="batch_summary"
-                :label="$t('dashboards.batch_summary')"
-                icon="img:statics/dashboard/statement.png"
-                :to="{ name: 'batch_summary' }"
-                exact
-              />
-            </transition>
+            
             <transition appear enter-active-class="animated zoomIn">
               <q-route-tab
                 name="inventory_inquiry"

templates/src/pages/dashboard/predeliverystock.vue → templates/src/pages/dashboard/flows.vue


+ 968 - 0
templates/src/pages/dashboard/flows_complex.vue

@@ -0,0 +1,968 @@
+<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="['check_status', 'log_type'].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="$t('downloadasnlist')"
+              icon="cloud_download"
+              @click="downloadlistData()"
+            >
+            </q-btn>
+            <!-- <q-btn :label="'日志'" icon="logout" @click="getlog()"> </q-btn> -->
+          </q-btn-group>
+
+          <q-space />
+
+          <div class="flex items-center">
+            <div class="q-mr-md">{{ $t("download_center.createTime") }}</div>
+            <q-input
+              readonly
+              outlined
+              dense
+              v-model="createDate2"
+              :placeholder="interval"
+            >
+              <template v-slot:append>
+                <q-icon name="event" class="cursor-pointer">
+                  <q-popup-proxy
+                    ref="qDateProxy"
+                    transition-show="scale"
+                    transition-hide="scale"
+                  >
+                    <q-date v-model="createDate1" range>
+                      <div class="row items-center justify-end q-gutter-sm">
+                        <q-btn
+                          :label="$t('index.cancel')"
+                          color="primary"
+                          flat
+                          v-close-popup
+                        />
+                        <q-btn
+                          :label="$t('index.clear')"
+                          color="primary"
+                          @click="
+                            createDate2 = '';
+                            createDate1 = '';
+                          "
+                          v-close-popup
+                        />
+                      </div>
+                    </q-date>
+                  </q-popup-proxy>
+                </q-icon>
+              </template>
+            </q-input>
+            <q-btn-group push class="q-ml-md"> </q-btn-group>
+          </div>
+        </template>
+
+        <template v-slot:body="props">
+          <q-tr :props="props" :style="getRowStyle(props.row)">
+            <q-td
+              v-for="col in columns.filter((c) => c.name !== 'expand')"
+              :key="col.name"
+              :props="props"
+            >
+              <span v-if="col.name === 'check_status'">
+                {{ checkStatusToText(props.row[col.field]) }}
+              </span>
+              <span v-else-if="col.name === 'goods_qty'">
+                {{ props.row.goods_in_qty - props.row.goods_out_qty }}
+              </span>
+              <span v-else-if="col.name === 'log_type'">
+                {{ logtypeToText(props.row[col.field]) }}
+              </span>
+              <span
+                v-else-if="
+                  ['check_status', 'goods_qty'].indexOf(col.name) === -1
+                "
+              >
+                {{ col.field ? props.row[col.field] : props.row[col.name] }}
+              </span>
+            </q-td>
+          </q-tr>
+
+          <!-- 第二级:时间轴 -->
+          <q-tr
+            v-show="props.row.expand"
+            :props="props"
+            class="expanded-row"
+            :style="getRowStyle(props.row)"
+          >
+            <template>
+              <q-td colspan="100%">
+                <div class="q-pa-md timeline-wrapper">
+                  <q-timeline
+                    color="#e0e0e0"
+                    v-if="props.row.containers?.length"
+                  >
+                    <q-timeline-entry
+                      v-for="(container, index) in props.row.containers"
+                      :key="index"
+                      class="custom-node"
+                    >
+                      <template v-slot:title>
+                        <span>
+                          <div>
+                            托盘 {{ container.container_code }}
+                            <span class="text-caption">
+                              操作时间:{{ container.change_time }}
+                            </span>
+                          </div>
+                        </span>
+                      </template>
+
+                      <template v-slot:subtitle>
+                        <div class="text-caption">
+                          批次: {{ container.batch }} | 物料编码:
+                          {{ container.goods_code }} |{{ container.goods_desc }}
+                          |
+                          <br />
+                          批次计划入库数:
+                          {{ container.batch_goods_qty }} | 批次总组盘数:
+                          {{ container.batch_goods_in_qty }} | 批次总出库数:
+                          {{ container.batch_goods_out_qty }} | 在库数量:
+                          {{ container.batch_goods_in_location_qty }} |
+                        </div>
+                      </template>
+
+                      <div class="timeline-content">
+                        <div class="row">
+                          <div class="col-6">
+                            <div class="text-caption">
+                              操作类型: {{ logtypeToText(container.log_type) }}
+                            </div>
+                            <div class="text-caption">
+                              操作人: {{ container.creater }}
+                            </div>
+                          </div>
+                          <div class="col-6">
+                            数量变化:<br />
+                            <span v-if="container.log_type === 'delete'">
+                              {{ container.old_goods_qty }} →
+                              {{ container.new_goods_qty }}
+                            </span>
+                            <span v-else-if="container.log_type === 'out'">
+                              出库数量:{{ container.old_goods_out_qty }} →
+                              {{ container.new_goods_out_qty }}
+                            </span>
+                            <span
+                              v-else-if="container.log_type === 'cancel_out'"
+                            >
+                              出库数量:{{ container.old_goods_out_qty }} →
+                              {{ container.new_goods_out_qty }}
+                            </span>
+                            <span v-else-if="container.log_type === 'create'">
+                              入库数量:{{ container.old_goods_qty }} →
+                              {{ container.new_goods_in_qty }}
+                            </span>
+
+                            <span v-else-if="container.log_type === 'update'">
+                              入库数量:{{ container.old_goods_qty }} →
+                              {{ container.new_goods_qty }}
+                              <br />
+                              出库数量:{{ container.old_goods_out_qty }} →
+                              {{ container.new_goods_out_qty }}
+                            </span>
+
+                            <!-- <div class="text-caption">
+                              状态:
+                              <span v-if="container.old_status !== null">
+                                {{ getStatusText(container.old_status) }} →
+                                {{ getStatusText(container.new_status) }}
+                              </span>
+                            </div> -->
+                          </div>
+                        </div>
+                      </div>
+                    </q-timeline-entry>
+                  </q-timeline>
+                  <div v-else class="text-caption text-grey">
+                    暂无容器操作记录
+                  </div>
+                </div>
+              </q-td>
+            </template>
+          </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 } from 'boot/axios_request'
+import { date, exportFile, LocalStorage } from 'quasar'
+import containercard from 'components/containercard.vue'
+
+export default {
+  name: 'PageTask',
+  components: {
+    containercard
+  },
+  data () {
+    return {
+      createDate1: '',
+      createDate2: '',
+      date_range: '',
+      proxyDate: '',
+      date: '',
+      goods_code: '',
+      goods_desc: '',
+      openid: '',
+      login_name: '',
+      authin: '0',
+      searchUrl: '',
+      pathname: 'reportcenter/flows_statements/',
+      pathfilename: 'reportcenter/MaterialChangeHistory/file/',
+      pathname_previous: '',
+      pathname_next: '',
+      separator: 'cell',
+      loading: false,
+      height: '',
+      viewForm: false,
+      editDialog: false,
+
+      table_list: [],
+      columns: [
+        // {
+        //   name: 'change_time',
+        //   label: '操作时间',
+        //   field: 'change_time',
+        //   align: 'center'
+        // },
+        // {
+        //   name: 'change_type',
+        //   label: '变更类型',
+        //   field: 'change_type',
+        //   align: 'center'
+        // },
+        {
+          name: 'goods_code',
+          label: '存货编码',
+          field: 'goods_code',
+          align: 'center'
+        },
+
+        {
+          name: 'goods_desc',
+          label: '存货名称',
+          field: 'goods_desc',
+          align: 'center'
+        },
+        {
+          name: 'opening_quantity',
+          label: '期初数量',
+          field: 'opening_quantity',
+          align: 'center'
+        },
+        {
+          name: 'closing_quantity',
+          label: '期末数量',
+          field: 'closing_quantity',
+          align: 'center'
+        },
+        {
+          name: 'net_change',
+          label: '期间变化',
+          field: 'net_change',
+          align: 'center'
+        },
+        {
+          name: 'total_in',
+          label: '入库数量',
+          field: 'total_in',
+          align: 'center'
+        },
+        {
+          name: 'total_out',
+          label: '出库数量',
+          field: 'total_out',
+          align: 'center'
+        },
+        {
+          name: 'theoretical_change',
+          label: '出入库差异',
+          field: 'theoretical_change',
+          align: 'center'
+        },
+        {
+          name: 'is_consistent',
+          label: '对比结果',
+          field: 'is_consistent',
+          align: 'center'
+        },
+        {
+          name: 'goods_unit',
+          label: '计量单位',
+          field: 'goods_unit',
+          align: 'center'
+        }
+      ],
+      filter: '',
+      pagination: {
+        page: 1,
+        rowsPerPage: 11
+      },
+      current: 1,
+      max: 0,
+      total: 0,
+      paginationIpt: 1,
+      containers: {},
+      timer: null,
+      showInventoryDetails: false,
+      select_container_number: 0,
+      select_container_code: 0,
+      filterModels: {
+        bound_department: null
+      },
+      editForm: {
+        id: '',
+        bound_number: '',
+        goods_code: '',
+        goods_desc: '',
+        goods_qty: '',
+        goods_unit: '',
+        goods_package: '',
+        goods_in_qty: '',
+        goods_out_qty: '',
+        goods_std: '',
+        check_status: '',
+        check_user: '默认质检人',
+        change_time: '',
+        note: '无'
+      },
+      activeSearchField: '',
+      activeSearchLabel: '',
+      filterdata: {}
+    }
+  },
+  computed: {
+    interval () {
+      return (
+        this.$t('download_center.start') +
+        ' - ' +
+        this.$t('download_center.end')
+      )
+    }
+  },
+  methods: {
+    downloadlistData () {
+      this.getfileList()
+    },
+    getfileList () {
+      var _this = this
+      _this.loading = true
+      const params = {
+        change_time__range: _this.date_range
+      }
+      const queryParams = new URLSearchParams({
+        ...params
+      })
+      console.log(queryParams)
+      // 过滤空值参数
+      Array.from(queryParams.entries()).forEach(([key, value]) => {
+        if (value === '' || value === null || value === undefined) {
+          queryParams.delete(key)
+        }
+      })
+      console.log(`${_this.pathfilename}?${queryParams}`)
+      postauth(`${_this.pathfilename}?${queryParams}`)
+        .then((res) => {
+          var timeStamp = Date.now()
+          var formattedString = date.formatDate(timeStamp, 'YYYYMMDDHHmmss')
+          const status = exportFile(
+            _this.pathfilename + 'list' + formattedString + '.csv',
+            '\uFEFF' + res,
+            'text/csv'
+          )
+          if (status !== true) {
+            _this.$q.notify({
+              message: 'Browser denied file download...',
+              color: 'negative',
+              icon: 'warning'
+            })
+          }
+        })
+        .catch((err) => {
+          _this.$q.notify({
+            message: err.detail,
+            icon: 'close',
+            color: 'negative'
+          })
+        })
+        .finally(() => {
+          _this.loading = false
+        })
+    },
+    handleEditRow (row) {
+      this.editForm = { ...row } // 复制当前行的数据到表单
+      this.editForm.note = this.editForm.note || '无' // 防止note为空
+      this.editForm.check_user = this.editForm.check_user || '默认质检人' // 防止check_user为空
+      console.log(this.editForm)
+      this.editDialog = true // 打开对话框
+    },
+    saveEditRow () {
+      const _this = this
+      putauth(`bound/batch/${_this.editForm.id}/`, _this.editForm) // 假设修改API是这样的
+        .then((res) => {
+          _this.editDialog = false // 关闭对话框
+
+          if (res.status_code !== 400) {
+            _this.$q.notify({ message: '修改成功', color: 'positive' })
+            _this.getSearchList() // 刷新列表
+          } else {
+            // 错误信息的键值映射到中文字段名称
+            const errorFieldMap = {
+              note: '备注',
+              check_user: '质检人'
+            }
+
+            // 遍历 res 对象的属性,查找错误信息
+            let errorMessage = '修改失败'
+            for (const key in res) {
+              if (Array.isArray(res[key]) && res[key].length > 0) {
+                const fieldLabel = errorFieldMap[key] || key
+                errorMessage = `${fieldLabel}: ${res[key].join(' ')}`
+                break
+              }
+            }
+            console.error('修改失败', errorMessage)
+            _this.$q.notify({ message: errorMessage, color: 'negative' })
+          }
+        })
+        .catch((error) => {
+          console.error('修改失败', error)
+          _this.$q.notify({
+            message: '发生未知错误,请联系管理员',
+            color: 'negative'
+          })
+        })
+    },
+
+    checkStatusToText (check_status) {
+      const statusTexts = {
+        0: '待质检',
+        1: '质检合格',
+        2: '质检问题'
+      }
+
+      return statusTexts[check_status] || '未知状态'
+    },
+    logtypeToText (log) {
+      const logtypeTexts = {
+        create: '批次入库',
+        out: '批次出库',
+        delete: '批次删除',
+        update: '批次更新',
+        cancel_out: '取消出库'
+      }
+
+      return logtypeTexts[log] || '未知状态'
+    },
+    getRowStyle (row) {
+      // 根据check_status值返回不同的背景色
+      const statusColors = {
+        0: '#fff9c4', // 更浅的黄色 - 待质检
+        true: '#c8e6c9', // 更浅的绿色 - 质检合格
+        false: '#ffcdd2' // 更浅的红色 - 质检问题
+      }
+
+      const color = statusColors[row.is_consistent] || ''
+      return color ? { backgroundColor: color } : {}
+    },
+    // 处理过滤变化
+    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
+
+        case 'check_status':
+          return [
+            { label: '待质检', value: 0 },
+            { label: '质检合格', value: 1 },
+            { label: '质检问题', value: 2 }
+          ]
+
+        case 'log_type':
+          return [
+            { label: '批次入库', value: 'create' },
+            { label: '批次出库', value: 'out' },
+            { label: '批次删除', value: 'delete' },
+            { label: '批次更新', value: 'update' }
+          ]
+        default:
+          return []
+      }
+    },
+    getStatusText (status) {
+      const statusMap = {
+        0: '待入库',
+        1: '在库',
+        2: '待出库',
+        3: '已出库',
+        4: '异常'
+      }
+      return statusMap[status] || '未知状态'
+    },
+    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('container/batchdetaillog/containerlog/?batchlog_id=' + row.id)
+          .then((res) => {
+            // 将数据存储到当前行的 containers 属性
+
+            _this.$set(row, 'containers', res)
+            console.log('当前的', row.containers)
+          })
+          .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)
+        }
+      })
+
+      postauth(`${_this.pathname}?${queryParams}`)
+        .then((res) => {
+          _this.table_list = res.results.map((item) => ({
+            ...item,
+            expand: false,
+            containers: [
+              // {
+              //   id: 0,
+              //   container_code: 0,
+              //   current_location: '0',
+              //   goods_qty: 0,
+              //   class: 0
+              // }
+            ],
+            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,
+        change_time__range: this.date_range,
+        ...filterParams, // 添加过滤条件
+        ...this.filterdata // 添加其他过滤条件
+      })
+    },
+
+    getListPrevious () {
+      var _this = this
+      if (LocalStorage.has('auth')) {
+        postauth(_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')) {
+        postauth(_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/?' + 'change_time__range=' + this.date_range
+        } else {
+          this.createDate2 = `${val}`
+          this.dateArray = val.split('/')
+          this.searchUrl =
+            this.pathname +
+            '?' +
+            'change_time__year=' +
+            this.dateArray[0] +
+            '&' +
+            'change_time__month=' +
+            this.dateArray[1] +
+            '&' +
+            'change_time__day=' +
+            this.dateArray[2]
+          // this.downloadhUrl = this.pathname + 'filelist/?' + 'change_time__year=' + this.dateArray[0] + '&' + 'change_time__month=' + this.dateArray[1] + '&' + 'change_time__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>

+ 9 - 0
templates/src/router/routes.js

@@ -97,6 +97,15 @@ const routes = [
             path: 'flows_statements',
             name: 'flows_statements',
             component: () => import('pages/dashboard/flows_statements.vue')
+          },{
+            path: 'flows',
+            name: 'flows',
+            component: () => import('pages/dashboard/flows.vue')
+          },
+          {
+            path: 'flows_complex',
+            name: 'flows_complex',
+            component: () => import('pages/dashboard/flows_complex.vue')
           },
           {
             path: 'batchlog',