Browse Source

操作记录

flower_bs 1 tháng trước cách đây
mục cha
commit
1b036572b8
100 tập tin đã thay đổi với 2457 bổ sung300 xóa
  1. 84 1
      backup/views.py
  2. 544 37
      bin/views.py
  3. 340 162
      bound/views.py
  4. 733 12
      container/views.py
  5. 200 42
      erp/views.py
  6. 1 0
      greaterwms/urls.py
  7. 217 0
      operation_log/README.md
  8. 20 0
      operation_log/filter.py
  9. 13 0
      operation_log/serializers.py
  10. 12 0
      operation_log/urls.py
  11. 40 6
      operation_log/views.py
  12. 1 1
      reportcenter/files.py
  13. 62 0
      reportcenter/views.py
  14. 102 3
      staff/views.py
  15. 55 5
      stock/views.py
  16. 0 1
      templates/dist/spa/css/13.8b9d1b53.css
  17. 1 0
      templates/dist/spa/css/13.b352291e.css
  18. 1 0
      templates/dist/spa/css/33.103b121d.css
  19. 0 0
      templates/dist/spa/css/34.0d4c4716.css
  20. 0 1
      templates/dist/spa/css/34.c527e777.css
  21. 1 0
      templates/dist/spa/css/35.5557cd4a.css
  22. 0 0
      templates/dist/spa/css/36.8f3f6188.css
  23. 0 0
      templates/dist/spa/css/37.44ddcebd.css
  24. 0 0
      templates/dist/spa/css/38.2ac1dad1.css
  25. 0 0
      templates/dist/spa/css/39.12670fd1.css
  26. 0 0
      templates/dist/spa/css/40.9478c981.css
  27. 0 0
      templates/dist/spa/css/41.c4652654.css
  28. 0 0
      templates/dist/spa/css/42.7a23b7fb.css
  29. 0 0
      templates/dist/spa/css/43.2594d0b9.css
  30. 0 0
      templates/dist/spa/css/44.0faa4aeb.css
  31. 0 1
      templates/dist/spa/css/7.8879267a.css
  32. 1 0
      templates/dist/spa/css/7.b5042fa0.css
  33. 1 1
      templates/dist/spa/index.html
  34. 0 1
      templates/dist/spa/js/13.ac106c63.js
  35. BIN
      templates/dist/spa/js/13.ac106c63.js.gz
  36. 1 0
      templates/dist/spa/js/13.eb7c38b1.js
  37. BIN
      templates/dist/spa/js/13.eb7c38b1.js.gz
  38. 1 0
      templates/dist/spa/js/33.212de067.js
  39. BIN
      templates/dist/spa/js/33.212de067.js.gz
  40. 0 1
      templates/dist/spa/js/34.c66dd14f.js
  41. BIN
      templates/dist/spa/js/34.c66dd14f.js.gz
  42. 1 1
      templates/dist/spa/js/33.e044124a.js
  43. 1 0
      templates/dist/spa/js/35.08b4d310.js
  44. BIN
      templates/dist/spa/js/35.08b4d310.js.gz
  45. 0 1
      templates/dist/spa/js/35.191d823c.js
  46. BIN
      templates/dist/spa/js/35.191d823c.js.gz
  47. 1 0
      templates/dist/spa/js/36.0ef8719a.js
  48. BIN
      templates/dist/spa/js/36.0ef8719a.js.gz
  49. BIN
      templates/dist/spa/js/37.54c90e2b.js.gz
  50. 1 1
      templates/dist/spa/js/36.04984167.js
  51. BIN
      templates/dist/spa/js/36.04984167.js.gz
  52. 1 1
      templates/dist/spa/js/37.54c90e2b.js
  53. BIN
      templates/dist/spa/js/38.51ed54fd.js.gz
  54. 1 1
      templates/dist/spa/js/38.96100c54.js
  55. BIN
      templates/dist/spa/js/38.96100c54.js.gz
  56. 1 1
      templates/dist/spa/js/39.5872b98c.js
  57. 1 1
      templates/dist/spa/js/40.d194e156.js
  58. 1 1
      templates/dist/spa/js/41.eed3ba0d.js
  59. 1 1
      templates/dist/spa/js/42.135aa88c.js
  60. 1 1
      templates/dist/spa/js/43.ac48efb6.js
  61. BIN
      templates/dist/spa/js/44.65975fc6.js.gz
  62. 1 1
      templates/dist/spa/js/44.65975fc6.js
  63. BIN
      templates/dist/spa/js/45.85676583.js.gz
  64. 1 1
      templates/dist/spa/js/45.8feb3608.js
  65. BIN
      templates/dist/spa/js/47.8ee118cc.js.gz
  66. 1 1
      templates/dist/spa/js/46.b3afcac9.js
  67. 1 1
      templates/dist/spa/js/47.8ee118cc.js
  68. BIN
      templates/dist/spa/js/48.f5f2c346.js.gz
  69. 1 1
      templates/dist/spa/js/48.72e06ecf.js
  70. BIN
      templates/dist/spa/js/49.d3deb20b.js.gz
  71. BIN
      templates/dist/spa/js/50.89d8c121.js.gz
  72. 1 1
      templates/dist/spa/js/49.d3deb20b.js
  73. BIN
      templates/dist/spa/js/50.b2e076a3.js.gz
  74. 1 1
      templates/dist/spa/js/50.89d8c121.js
  75. BIN
      templates/dist/spa/js/51.f4758ae8.js.gz
  76. 1 1
      templates/dist/spa/js/51.67372365.js
  77. BIN
      templates/dist/spa/js/51.67372365.js.gz
  78. BIN
      templates/dist/spa/js/52.c0c678a9.js.gz
  79. 1 1
      templates/dist/spa/js/52.c0c678a9.js
  80. BIN
      templates/dist/spa/js/53.4bd72b33.js.gz
  81. BIN
      templates/dist/spa/js/53.d09f7623.js.gz
  82. 1 1
      templates/dist/spa/js/53.d09f7623.js
  83. BIN
      templates/dist/spa/js/54.13366a89.js.gz
  84. BIN
      templates/dist/spa/js/54.f7392283.js.gz
  85. BIN
      templates/dist/spa/js/55.7ff76385.js.gz
  86. 1 1
      templates/dist/spa/js/54.f7392283.js
  87. BIN
      templates/dist/spa/js/55.f93ef6f0.js.gz
  88. 1 1
      templates/dist/spa/js/55.7ff76385.js
  89. BIN
      templates/dist/spa/js/56.8954c039.js.gz
  90. BIN
      templates/dist/spa/js/56.c76fed61.js.gz
  91. 1 1
      templates/dist/spa/js/56.c76fed61.js
  92. BIN
      templates/dist/spa/js/57.21c754a8.js.gz
  93. BIN
      templates/dist/spa/js/57.79b8851a.js.gz
  94. 1 1
      templates/dist/spa/js/57.79b8851a.js
  95. BIN
      templates/dist/spa/js/58.b42c1c9b.js.gz
  96. BIN
      templates/dist/spa/js/58.d42ca317.js.gz
  97. 1 1
      templates/dist/spa/js/58.d42ca317.js
  98. BIN
      templates/dist/spa/js/59.9471f3f5.js.gz
  99. BIN
      templates/dist/spa/js/59.bf3b4865.js.gz
  100. 0 0
      templates/dist/spa/js/59.bf3b4865.js

+ 84 - 1
backup/views.py

@@ -9,6 +9,9 @@ from datetime import datetime
 from apscheduler.schedulers.background import BackgroundScheduler
 from django.conf import settings
 import math
+from operation_log.views import log_success_operation, log_failure_operation
+from operation_log.models import OperationLog
+from django.utils import timezone
 
 logger = logging.getLogger(__name__)
 
@@ -20,6 +23,21 @@ def scheduled_backup():
     try:
         backup_path = perform_base_backup()
         logger.info(f"定时备份完成: {backup_path}")
+        # 记录操作日志(定时任务没有request对象,直接创建日志)
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时备份数据库完成,备份路径: {backup_path}",
+                operation_level="other",
+                operation_result="success",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/scheduled",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"定时备份日志记录失败: {str(log_error)}")
         # 更新托盘分类任务(如果存在)
         try:
             from container.utils import update_container_categories_task,reconcile_material_history
@@ -30,6 +48,21 @@ def scheduled_backup():
             logger.warning("更新托盘分类模块未找到,跳过更新")
     except Exception as e:
         logger.error(f"定时备份失败: {str(e)}")
+        # 记录失败日志
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时备份数据库失败: {str(e)}",
+                operation_level="other",
+                operation_result="failure",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/scheduled",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"定时备份失败日志记录失败: {str(log_error)}")
 
 # 启动定时备份(每6小时执行一次)
 if not scheduler.running:
@@ -120,7 +153,16 @@ def trigger_backup(request):
     """手动触发备份的API接口"""
     try:
         backup_path = perform_base_backup()
-   
+        # 记录成功日志
+        try:
+            log_success_operation(
+                request=request,
+                operation_content=f"手动触发数据库备份,备份路径: {backup_path}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"备份成功日志记录失败: {str(log_error)}")
 
         return JsonResponse({
             'status': 'success',
@@ -128,6 +170,16 @@ def trigger_backup(request):
             'path': backup_path
         })
     except Exception as e:
+        # 记录失败日志
+        try:
+            log_failure_operation(
+                request=request,
+                operation_content=f"手动触发数据库备份失败: {str(e)}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"备份失败日志记录失败: {str(log_error)}")
         return JsonResponse({
             'status': 'error',
             'message': str(e)
@@ -175,6 +227,16 @@ def restore_to_point(request):
        
         
         if not base_backup or not os.path.exists(base_backup):
+            # 记录失败日志(无效路径)
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"执行数据库恢复失败: 无效的基础备份路径 - {base_backup}",
+                    operation_level="other",
+                    module_name="系统备份"
+                )
+            except Exception as log_error:
+                logger.error(f"恢复失败日志记录失败: {str(log_error)}")
             return JsonResponse({
                 'status': 'error',
                 'message': '无效的基础备份路径'
@@ -193,12 +255,33 @@ def restore_to_point(request):
         scheduler.resume_job('db_backup_job')
         logger.info("定时备份任务已恢复")
         
+        # 记录成功日志
+        try:
+            log_success_operation(
+                request=request,
+                operation_content=f"执行数据库时间点恢复,恢复路径: {base_backup}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"恢复成功日志记录失败: {str(log_error)}")
+        
         return JsonResponse({
             'status': 'success',
             'message': f'已成功恢复到{base_backup}'
         })
     except Exception as e:
         logger.error(f"时间点恢复失败: {str(e)}")
+        # 记录失败日志
+        try:
+            log_failure_operation(
+                request=request,
+                operation_content=f"执行数据库时间点恢复失败: {str(e)}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"恢复失败日志记录失败: {str(log_error)}")
         # 确保恢复定时备份任务
         if scheduler.get_job('db_backup_job') and scheduler.get_job('db_backup_job').next_run_time is None:
             scheduler.resume_job('db_backup_job')

+ 544 - 37
bin/views.py

@@ -27,6 +27,7 @@ import json
 from collections import defaultdict
 logger = logging.getLogger(__name__)
 from operation_log.views import log_operation,log_failure_operation,log_success_operation
+from operation_log.models import OperationLog
 # 库位分配
 # 入库规则函数
 # 逻辑根据批次下的托盘数目来找满足区间范围的库位,按照优先级排序,
@@ -96,11 +97,66 @@ class locationViewSet(viewsets.ModelViewSet):
     def get_serializer_class(self):
         if self.action == 'list':
             return LocationListSerializer
+        elif self.action == 'create':
+            return LocationPostSerializer
         elif self.action == 'update':
             return LocationPostSerializer
         elif self.action =='retrieve':
             return LocationListSerializer
 
+    def create(self, request, *args, **kwargs):
+        """创建库位"""
+        serializer = self.get_serializer(data=request.data)
+        try:
+            serializer.is_valid(raise_exception=True)
+            instance = serializer.save()
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建库位成功,库位编码: {instance.location_code}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=instance.id,
+                module_name="库位"
+            )
+            headers = self.get_success_headers(serializer.data)
+            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建库位失败: {str(e)}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="库位"
+            )
+            raise
+
+    def destroy(self, request, *args, **kwargs):
+        """删除库位"""
+        instance = self.get_object()
+        location_code = instance.location_code
+        object_id = instance.id
+        try:
+            self.perform_destroy(instance)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"删除库位成功,库位编码: {location_code}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"删除库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            raise
+
     def update(self, request, *args, **kwargs):
         qs = self.get_object()
         data = self.request.data
@@ -307,12 +363,29 @@ class locationGroupViewSet(viewsets.ModelViewSet):
         id = self.get_project()
         if self.request.auth:
             if id is None:
+                log_operation(
+                    request=self.request,
+                    operation_content="查看库位组列表",
+                    operation_level="view",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    module_name="库位"
+                )
                 return LocationGroupModel.objects.filter()  
             else:
+                log_operation(
+                    request=self.request,
+                    operation_content=f"查看库位组详情 ID:{id}",
+                    operation_level="view",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=id,
+                    module_name="库位"
+                )
                 return LocationGroupModel.objects.filter(id=id)            
         else:
             return LocationGroupModel.objects.none()                           
-    def get_serializer_class(self):                                             
+    def get_serializer_class(self):
+        if self.action == 'create':
+            return LocationGroupPostSerializer                                             
         if self.action == 'list':
             return LocationGroupListSerializer          
 
@@ -320,7 +393,63 @@ class locationGroupViewSet(viewsets.ModelViewSet):
             return LocationGroupPostSerializer
 
         elif self.action =='retrieve':
-            return LocationGroupListSerializer      
+            return LocationGroupListSerializer
+
+    def create(self, request, *args, **kwargs):
+        """创建库位组"""
+        data = self.request.data.copy()
+        order_month = str(timezone.now().strftime('%Y%m'))
+        data['month'] = order_month
+        serializer = LocationGroupPostSerializer(data=data)
+        try:
+            serializer.is_valid(raise_exception=True)
+            instance = serializer.save()
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建库位组成功,库位组编码: {instance.group_code}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=instance.id,
+                module_name="库位"
+            )
+            headers = self.get_success_headers(serializer.data)
+            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建库位组失败: {str(e)}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="库位"
+            )
+            raise
+
+    def destroy(self, request, *args, **kwargs):
+        """删除库位组"""
+        instance = self.get_object()
+        group_code = instance.group_code
+        object_id = instance.id
+        try:
+            self.perform_destroy(instance)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"删除库位组成功,库位组编码: {group_code}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"删除库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            raise
     
     def update(self, request, *args, **kwargs):
         data = self.request.data
@@ -332,7 +461,30 @@ class locationGroupViewSet(viewsets.ModelViewSet):
         if group_obj:
             data['id'] = group_obj.id
             logger.info(f"库位组 {group_code} 已存在")
-            
+            # 更新现有库位组
+            serializer = LocationGroupPostSerializer(group_obj, data=data)
+            try:
+                serializer.is_valid(raise_exception=True)
+                serializer.save()
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新库位组成功,库位组编码: {group_code}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=group_obj.id,
+                    module_name="库位"
+                )
+                return Response(serializer.data, status=status.HTTP_200_OK)
+            except Exception as e:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"更新库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=group_obj.id,
+                    module_name="库位"
+                )
+                raise
         else:
             logger.info(f"库位组 {group_code} 不存在,创建库位组对象")
             serializer_list = LocationGroupPostSerializer(data=data)
@@ -340,14 +492,14 @@ class locationGroupViewSet(viewsets.ModelViewSet):
             serializer_list.save()
             log_success_operation(
                 request=self.request,
-                operation_content=f"创建库位组成功,库位组 {group_code} 已创建",
+                operation_content=f"创建库位组成功,库位组编码: {group_code}",
                 operation_level="new",  
                 operator=self.request.auth.name if self.request.auth else None,
                 object_id=serializer_list.data.get('id'),
                 module_name="库位"
             )
             data['id'] = serializer_list.data.get('id')
-        return Response(data, status=status.HTTP_201_CREATED)
+            return Response(data, status=status.HTTP_201_CREATED)
 
 class LocationAllocation:
     # 入库规则函数
@@ -450,11 +602,12 @@ class LocationAllocation:
             return None
 
     
-    def update_location_container_link(self,location_code,container_code):
+    def update_location_container_link(self,location_code,container_code, request=None):
         """
         更新库位和托盘的关联关系
         :param location_code: 库位编码
         :param container_code: 托盘编码
+        :param request: 请求对象(可选)
         :return:
         """
         try:
@@ -473,15 +626,72 @@ class LocationAllocation:
                 )
                 location_container_link.save()
                 print(f"更新库位和托盘的关联关系成功!")
+                # 记录操作日志
+                try:
+                    if request:
+                        log_success_operation(
+                            request=request,
+                            operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="other",
+                            module_name="库位分配"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="other",
+                            operation_result="success",
+                            module_name="库位分配"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
                 return True
             # 3. 更新库位和托盘的关联关系
             else:
                 LocationContainerLink.objects.filter(location=location).update(location=location, container=container)
                 print(f"更新库位和托盘的关联关系成功!")
+                # 记录操作日志
+                try:
+                    if request:
+                        log_success_operation(
+                            request=request,
+                            operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="update",
+                            module_name="库位分配"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="update",
+                            operation_result="success",
+                            module_name="库位分配"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
                 return True
         except Exception as e:       
             logger.error(f"更新库位和托盘的关联关系失败:{str(e)}")
             print(f"更新库位和托盘的关联关系失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False 
     def update_container_detail_status(self,container_code,status):
         try:
@@ -536,11 +746,12 @@ class LocationAllocation:
             logger.error(f"更新库位组的批次失败:{str(e)}")
             print(f"更新库位组的批次失败:{str(e)}")
             return False    
-    def update_location_status(self,location_code,status):
+    def update_location_status(self,location_code,status, request=None):
         """
         更新库位状态
         :param location_code: 库位编码
         :param status: 库位状态
+        :param request: 请求对象(可选)
         :return:
         """
         try:
@@ -552,22 +763,65 @@ class LocationAllocation:
                 print(f"库位获取失败!")
                 return False
             # 2. 更新库位状态
+            old_status = location.status
             location.status = status
             location.save()
             print(f"更新库位状态成功!")
+            # 记录操作日志(状态变更敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位状态更新日志失败: {str(log_error)}")
             return True
         except Exception as e:       
             logger.error(f"更新库位状态失败:{str(e)}")
             print(f"更新库位状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False    
     
-    def update_group_status_reserved(self,location_group_list):
+    def update_group_status_reserved(self,location_group_list, request=None):
         """
-        更新库位组状态
+        更新库位组状态为预留
         :param location_group_list: 库位组对象列表
+        :param request: 请求对象(可选)
         :return:
         """
         try:
+            reserved_groups = []
             for location_group in location_group_list:
                 # 1. 获取库位组
                 if not location_group:
@@ -582,13 +836,54 @@ class LocationAllocation:
                     print(f"库位组 {location_group} 不存在")
                     return False
                 # 3. 更新库位组状态
+                old_status = location_group_item.status
                 location_group_item.status = 'reserved'
                 location_group_item.save()
+                reserved_groups.append(f"{location_group_item.group_code}({old_status}->reserved)")
+
+            # 记录预留库位组操作日志(敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
+                        operation_level="update",
+                        operation_result="success",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录预留库位组日志失败: {str(log_error)}")
 
             return True
         except Exception as e:       
             logger.error(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"预留库位组失败,错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"预留库位组失败,错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False    
     
     def update_location_group_status(self, location_code):
@@ -636,11 +931,12 @@ class LocationAllocation:
         except Exception as e:       
             logger.error(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
-    def update_batch_status(self,container_code,status):
+    def update_batch_status(self,container_code,status, request=None):
         """
         更新批次状态
-        :param batch_id: 批次id
+        :param container_code: 托盘编码
         :param status: 批次状态
+        :param request: 请求对象(可选)
         :return:
         """
         try:
@@ -664,14 +960,55 @@ class LocationAllocation:
         
             # 3. 更新批次状态
             batch = container_detail.batch
+            old_status = batch.status
             batch.status = status
             batch.save()
 
             print(f"更新批次状态成功!")
+            # 记录操作日志(批次状态变更敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        object_id=str(batch.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(batch.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录批次状态更新日志失败: {str(log_error)}")
             return True
         except Exception as e:       
             logger.error(f"更新批次状态失败:{str(e)}")
             print(f"更新批次状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False  
         
     # def update_batch_goods_in_location_qty(self,container_code,taskworking):
@@ -816,21 +1153,52 @@ class LocationAllocation:
                 self.update_current_finish_task(container_code,current_task)
                 return location_list[0]
     
-    def get_location_type(self, container_code):
+    def get_location_type(self, container_code, request=None):
         """
         智能库位分配核心算法
         :param container_code: 托盘码
+        :param request: 请求对象(可选)
         :return: 库位类型分配方案
         """
         try:
             batch = self.get_batch(container_code)
             if not batch:
                 logger.error("批次信息获取失败")
+                # 记录失败日志
+                try:
+                    if request:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
+                            operation_level="other",
+                            module_name="库位分配算法"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
+                            operation_level="other",
+                            operation_result="failure",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录算法失败日志失败: {str(log_error)}")
                 return None
 
             # 检查已有分配方案
             existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
             if existing_solution:
+                # 记录使用已有分配方案
+                try:
+                    if request:
+                        log_operation(
+                            request=request,
+                            operation_content=f"使用已有库位分配方案,托盘编码: {container_code}, 批次号: {batch}",
+                            operation_level="view",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录使用已有方案日志失败: {str(log_error)}")
                 return existing_solution.layer_pre_type
 
             # 获取关键参数
@@ -914,6 +1282,25 @@ class LocationAllocation:
             
             if not allocation:
                 logger.error("无法生成有效分配方案")
+                # 记录失败日志
+                try:
+                    if request:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
+                            operation_level="other",
+                            module_name="库位分配算法"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
+                            operation_level="other",
+                            operation_result="failure",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录算法失败日志失败: {str(log_error)}")
                 return None
 
             # 保存分配方案
@@ -938,11 +1325,50 @@ class LocationAllocation:
             solution.save()
             solution_pressure.save()
 
+            # 记录算法执行成功日志(关键操作)
+            try:
+                allocation_summary = f"批次号: {batch}, 托盘数: {total_pallets}, 分配方案: {len(allocation[0])}个库位组, 压力分布: L1={allocation[1][0]}, L2={allocation[1][1]}, L3={allocation[1][2]}"
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
+                        operation_level="other",
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
+                        operation_level="other",
+                        operation_result="success",
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录算法成功日志失败: {str(log_error)}")
 
             return [loc.split('_')[1] for loc in allocation[0]]
 
         except Exception as e:
             logger.error(f"分配算法异常:{str(e)}")
+            # 记录异常日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="other",
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="other",
+                        operation_result="failure",
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录算法异常日志失败: {str(log_error)}")
             return None
 
 
@@ -1071,11 +1497,12 @@ class LocationAllocation:
 
 
     
-    def get_location_by_status(self,container_code,start_location):
+    def get_location_by_status(self,container_code,start_location, request=None):
         """
         根据库位状态获取库位
-        :param location_type: 库位类型
+        :param container_code: 托盘编码
         :param start_location: 起始库位 if in1 优先考虑left_priority, if in2 优先考虑right_priority 就是获取库位组列表之后进行排序
+        :param request: 请求对象(可选)
         :return: 库位列表
         """
         # 1. 获取批次状态 1 为已组盘 2 为部分入库 3 为全部入库
@@ -1086,20 +1513,35 @@ class LocationAllocation:
             print(f"[1]第一次入库")
 
             # 重新获取最新数据
-            self.get_location_type(container_code) 
+            self.get_location_type(container_code, request) 
             
             location_type_list = json.loads(alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_pre_type)            
             location_list = self.get_location_by_type(location_type_list,start_location,container_code)
             # 预定这些库组
-            self.update_group_status_reserved(location_list)
+            self.update_group_status_reserved(location_list, request)
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
-            # if not location_list[location_min_index]:
-            #     # 库位已满,返回None
-            #     return None
-            # else:
-                
-            #     return location_list[location_min_index]
+            # 记录库位分配成功日志
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        operation_result="success",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位分配日志失败: {str(log_error)}")
             return location_min_value
       
         elif status == 2:
@@ -1108,28 +1550,93 @@ class LocationAllocation:
             location_list = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_solution_type
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
+            # 记录库位分配成功日志
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        operation_result="success",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位分配日志失败: {str(log_error)}")
             return location_min_value
 
 
 
 
 
-    def release_location(self, location_code):
-            """释放库位并更新关联数据"""
-            try:
-                location = LocationModel.objects.get(location_code=location_code)
-                links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
-                if not links:
-                    logger.error(f"库位{location_code}未关联托盘")
-                    return True
-                print(f"释放库位: {location_code}, 关联托盘: {links.container_id}")
-                # 解除关联并标记为非活跃
-                links.is_active = False
-                links.save()
+    def release_location(self, location_code, request=None):
+        """释放库位并更新关联数据"""
+        try:
+            location = LocationModel.objects.get(location_code=location_code)
+            links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
+            if not links:
+                logger.error(f"库位{location_code}未关联托盘")
                 return True
-            except Exception as e:
-                logger.error(f"释放库位失败: {str(e)}")
-                return False      
+            container_id = links.container_id
+            print(f"释放库位: {location_code}, 关联托盘: {container_id}")
+            # 解除关联并标记为非活跃
+            links.is_active = False
+            links.save()
+            # 更新库位状态为可用
+            location.status = 'available'
+            location.save()
+            # 记录释放库位操作日志(敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
+                        operation_level="update",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录释放库位日志失败: {str(log_error)}")
+            return True
+        except Exception as e:
+            logger.error(f"释放库位失败: {str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
+            return False      
   
       
   

+ 340 - 162
bound/views.py

@@ -84,36 +84,58 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                 "msg": "Success Create",
             	"data": data
         }
-    
+        log_operation(
+            request=self.request,
+            operation_content=f"查询出库需求列表,出库单ID:{data.get('bound_list_id')}",
+            operation_level="view",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="出库需求"
+        )
         return Response(return_data,status=200,headers={})
 
   
     def create(self, request, *args, **kwargs):
         data = self.request.data
-
-        data['openid'] = self.request.auth.openid
-        data['create_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
-        data['update_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
-        data['working'] = True
-        bound_list_obj = BoundListModel.objects.get(id=data['bound_list_id'])
-
-        OutBoundDemand_obj =OutBoundDemandModel.objects.create(
-            bound_list=bound_list_obj, 
-            goods_code=data['goods_code'], 
-            goods_desc=data['goods_desc'], 
-            goods_std=data['goods_std'], 
-            goods_unit=data['goods_unit'], 
-            goods_qty=data['goods_out_qty'],
-            out_type = data['out_type'],
-            creater=data['creater'], 
-            create_time=data['create_time'],
-            update_time=data['update_time'], 
-            working=data['working']
+        try:
+            data['openid'] = self.request.auth.openid
+            data['create_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            data['update_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            data['working'] = True
+            bound_list_obj = BoundListModel.objects.get(id=data['bound_list_id'])
+
+            OutBoundDemand_obj =OutBoundDemandModel.objects.create(
+                bound_list=bound_list_obj, 
+                goods_code=data['goods_code'], 
+                goods_desc=data['goods_desc'], 
+                goods_std=data['goods_std'], 
+                goods_unit=data['goods_unit'], 
+                goods_qty=data['goods_out_qty'],
+                out_type = data['out_type'],
+                creater=data['creater'], 
+                create_time=data['create_time'],
+                update_time=data['update_time'], 
+                working=data['working']
+                )
+            return_data = OutBoundDemandModelSerializer(OutBoundDemand_obj).data
+            headers = self.get_success_headers(return_data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建出库需求 ID:{OutBoundDemand_obj.id},创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=OutBoundDemand_obj.id,
+                module_name="出库需求"
             )
-        return_data = OutBoundDemandModelSerializer(OutBoundDemand_obj).data
-        headers = self.get_success_headers(return_data)
-
-        return Response(return_data, status=200, headers=headers)
+            return Response(return_data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建出库需求失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库需求"
+            )
+            raise
     
     def batch_demanded_list(self, request):
         data =self.request.data
@@ -124,7 +146,13 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
             "msg": "Success Create",
             "data": data
         }
-
+        log_operation(
+            request=self.request,
+            operation_content=f"查询出库批次列表,出库单ID:{data.get('bound_list_id')}",
+            operation_level="view",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="出库批次"
+        )
         return Response(return_data,status=200,headers={})
 
     def distribute(self, request):
@@ -134,6 +162,13 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                 bound_list_id, demands,out_type, creater= self.validate_distribute_request(request)
 
                 if OutBatchModel.objects.filter(bound_list_id=bound_list_id, is_delete=False).exists():
+                    log_operation(
+                        request=self.request,
+                        operation_content=f"出库分配失败,出库单ID:{bound_list_id} 已分配,请勿重复分配",
+                        operation_level="other",
+                        operator=self.request.auth.name if self.request.auth else None,
+                        module_name="出库分配"
+                    )
                     return_data = {
                         "code": 200,
                         "msg": "Success Create",
@@ -144,10 +179,32 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                     return Response(return_data, status=200, headers={})
                 aggregated_demands = self.aggregate_demands(demands)
                 result = self.process_all_goods(aggregated_demands, request, out_type, creater,bound_list_id)
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"出库分配成功,出库单ID:{bound_list_id},分配结果:{result}",
+                    operation_level="other",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=bound_list_id,
+                    module_name="出库分配"
+                )
                 return self.build_success_response(result)
         except APIException as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"出库分配失败,出库单ID:{request.data.get('bound_list_id')},错误:{e.detail}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库分配"
+            )
             return self.build_error_response(e.detail, 200)
         except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"出库分配异常,出库单ID:{request.data.get('bound_list_id')},错误:{str(e)}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库分配"
+            )
             return self.build_error_response(str(e), 200)
 
     # 验证层方法
@@ -457,35 +514,55 @@ class BoundListViewSet(viewsets.ModelViewSet):
 
         serializer = self.get_serializer(data=data)
         serializer.is_valid(raise_exception=True)
-        serializer.save()
-
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"创建入库单 ID:{serializer.data['id']},创建内容:{data}",
-            operation_level="create",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库单"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建入库单 ID:{serializer.data['id']},创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库单"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建入库单失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库单"
+            )
+            raise
     
     def update(self, request, pk):
         qs = self.get_object()
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库单 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库单"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库单 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库单"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库单失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库单"
+            )
+            raise
 
 
     def destroy(self, request, pk):
@@ -623,6 +700,13 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
                 return Response(serializer.data, status=200, headers=headers)
         except Exception as e:
             print(e)
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建入库批次失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             raise APIException({"detail": "{}".format(e)})
     
     def add_batch_log(self, data, log_type, goods_qty):
@@ -658,19 +742,30 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
     def update(self, request, pk):
         qs = self.get_object()
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库批次 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库批次"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库批次 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库批次"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库批次失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库批次"
+            )
+            raise
 
     def destroy(self, request, pk):
         qs = self.get_object()
@@ -707,11 +802,25 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
         batch_in_qty = request.data.get('batch_in_qty')
         
         if not batch_number:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"批次进出库失败,批次号不能为空",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             return Response({"code": 200,"message": "批次号不能为空","data": "批次进出库数目:{batch_in_qty}"}, status=200)
             
         try:
             batch_obj = BoundBatchModel.objects.get(bound_number=batch_number, is_delete=False)
         except BoundBatchModel.DoesNotExist:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"批次进出库失败,批次号不存在:{batch_number}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             return Response({"code": 200,"message": "批次号不存在","data": "批次进出库数目:{batch_in_qty}"}, status=200)
 
 
@@ -808,6 +917,13 @@ class BatchFileDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='batch_剩余量_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        log_operation(
+            request=self.request,
+            operation_content=f"下载批次剩余量文件",
+            operation_level="download",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="批次文件下载"
+        )
         return response
 
    
@@ -868,6 +984,14 @@ class BoundDetailViewSet(viewsets.ModelViewSet):
             detail_obj = BoundDetailModel.objects.get(bound_list=data['bound_list'], bound_batch=data['bound_batch'], is_delete=False)
             serializer = self.get_serializer(detail_obj, many=False)
             headers = self.get_success_headers(serializer.data)
+            log_operation(
+                request=self.request,
+                operation_content=f"查询入库明细,入库单ID:{data['bound_list']}, 批次ID:{data['bound_batch']}",
+                operation_level="view",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=detail_obj.id,
+                module_name="入库明细"
+            )
             return Response(serializer.data, status=200, headers=headers)
         else:
             serializer = self.get_serializer(data=data)
@@ -889,19 +1013,30 @@ class BoundDetailViewSet(viewsets.ModelViewSet):
     def update(self, request, pk):
         qs = self.get_object()
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库明细 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库明细"
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库明细 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库明细"
             )
-        return Response(serializer.data, status=200, headers=headers)
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库明细失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库明细"
+            )
+            raise
 
     def destroy(self, request, pk):
         qs = self.get_object()
@@ -975,59 +1110,81 @@ class OutBoundDetailViewSet(viewsets.ModelViewSet):
 
     def create(self, request, *args, **kwargs):
         data = self.request.data
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
+        try:
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+
+            data['bound_batch_number'] = OutBatchModel.objects.get(id=data['bound_batch']).batch_number.id
+            # 验证并保存数据
+            data['detail_code'] = f"DC-{data['bound_list']:02}{data['bound_batch']:02}"
+            # print(data['detail_code'])
+            if OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).exists():
+                # 这里追加数目
+                detail_obj = OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).first()
+                OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).update(goods_qty=F('goods_qty')+data['goods_qty'])
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新出库明细 ID:{detail_obj.id},数量追加:{data['goods_qty']}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=detail_obj.id,
+                    module_name="出库明细"
+                )
+                return Response({"detail": "数据存在,数量追加"}, status=200)
+                # raise APIException({"detail": "Data exists"})
+            else:
+                serializer = self.get_serializer(data=data)
+                serializer.is_valid(raise_exception=True)
+                serializer.save()
 
-        data['bound_batch_number'] = OutBatchModel.objects.get(id=data['bound_batch']).batch_number.id
-        # 验证并保存数据
-        data['detail_code'] = f"DC-{data['bound_list']:02}{data['bound_batch']:02}"
-        # print(data['detail_code'])
-        if OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).exists():
-            # 这里追加数目
-            OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).update(goods_qty=F('goods_qty')+data['goods_qty'])
-            log_success_operation(
+                # 返回响应
+                headers = self.get_success_headers(serializer.data)
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"创建出库明细 ID:{serializer.data['id']},创建内容:{data}",
+                    operation_level="create",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=serializer.data['id'],
+                    module_name="出库明细"
+                )
+                return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
                 request=self.request,
-                operation_content=f"更新出库明细 ID:{data['id']}:{data}",
-                operation_level="update",
+                operation_content=f"创建出库明细失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
                 operator=self.request.auth.name if self.request.auth else None,
-                object_id=data['id'],
                 module_name="出库明细"
             )
-            return Response({"detail": "数据存在,数量追加"}, status=200)
-            # raise APIException({"detail": "Data exists"})
-        else:
-            serializer = self.get_serializer(data=data)
+            raise
+
+    def update(self, request, pk):
+        qs = self.get_object()
+        data = self.request.data
+        try:
+            serializer = self.get_serializer(qs, data=data)
             serializer.is_valid(raise_exception=True)
             serializer.save()
-
-            # 返回响应
             headers = self.get_success_headers(serializer.data)
             log_success_operation(
                 request=self.request,
-                operation_content=f"创建出库明细 ID:{serializer.data['id']},创建内容:{data}",
-                operation_level="create",
+                operation_content=f"更新出库明细 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
                 operator=self.request.auth.name if self.request.auth else None,
                 object_id=serializer.data['id'],
                 module_name="出库明细"
             )
             return Response(serializer.data, status=200, headers=headers)
-
-    def update(self, request, pk):
-        qs = self.get_object()
-        data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新出库明细 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库明细"
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新出库明细失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="出库明细"
             )
-        return Response(serializer.data, status=200, headers=headers)
+            raise
 
     def destroy(self, request, pk):
         qs = self.get_object()
@@ -1102,64 +1259,85 @@ class OutBoundBatchViewSet(viewsets.ModelViewSet):
 
     def create(self, request, *args, **kwargs):
         data = self.request.data
-        batch_obj = BoundBatchModel.objects.get(bound_number=data['out_number'])
-        if batch_obj is None:
-            raise APIException({"detail": "批次不存在"})
+        try:
+            batch_obj = BoundBatchModel.objects.get(bound_number=data['out_number'])
+            if batch_obj is None:
+                raise APIException({"detail": "批次不存在"})
 
-        bound_obj = BoundListModel.objects.get(id=data['bound_list_id'])
-        data['bound_list'] = bound_obj.id
-        data['batch_number'] = batch_obj.id
-        data['out_date'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            bound_obj = BoundListModel.objects.get(id=data['bound_list_id'])
+            data['bound_list'] = bound_obj.id
+            data['batch_number'] = batch_obj.id
+            data['out_date'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
 
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
-        data['goods_total_weight'] = data['goods_weight']*data['goods_out_qty']
-        from decimal import Decimal
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+            data['goods_total_weight'] = data['goods_weight']*data['goods_out_qty']
+            from decimal import Decimal
 
-        #  data['goods_out_qty'] 是一个字符串或浮点数
-        data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
-        from decimal import Decimal
-        data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
-        data['goods_qty'] = batch_obj.goods_qty - batch_obj.goods_reserve_qty - data['goods_out_qty']
+            #  data['goods_out_qty'] 是一个字符串或浮点数
+            data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
+            from decimal import Decimal
+            data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
+            data['goods_qty'] = batch_obj.goods_qty - batch_obj.goods_reserve_qty - data['goods_out_qty']
 
-        data['status'] = 0  #现在处于出库申请状态
-   
-        serializer = self.get_serializer(data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        self.add_batch_log(serializer.data, 1, data['goods_out_qty'])
-        log_success_operation(
-            request=self.request,
-            operation_content=f"创建出库批次 ID:{serializer.data['id']},创建内容:{data}",
-            operation_level="create",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库批次"
-        )
-        
-        return Response(serializer.data, status=200, headers=headers)
+            data['status'] = 0  #现在处于出库申请状态
+       
+            serializer = self.get_serializer(data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            self.add_batch_log(serializer.data, 1, data['goods_out_qty'])
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建出库批次 ID:{serializer.data['id']},创建内容:{data}",
+                operation_level="create",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="出库批次"
+            )
+            
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建出库批次失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库批次"
+            )
+            raise
 
     def update(self, request, pk):        
         qs = self.get_object()
         data = self.request.data
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
-        data['goods_total_weight'] = data['goods_weight']*data['goods_qty']
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        self.add_batch_log(serializer.data, 1, data['goods_qty'])
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新出库批次 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库批次"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+            data['goods_total_weight'] = data['goods_weight']*data['goods_qty']
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            self.add_batch_log(serializer.data, 1, data['goods_qty'])
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新出库批次 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="出库批次"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新出库批次失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="出库批次"
+            )
+            raise
 
     def destroy(self, request, pk):
         qs = self.get_object()

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 733 - 12
container/views.py


+ 200 - 42
erp/views.py

@@ -24,6 +24,7 @@ from django.db import transaction
 from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchOperateLogModel
 from warehouse.models import ProductListModel
 from rest_framework.response import Response
+from operation_log.views import log_success_operation, log_failure_operation
 import json
 
 logger = logging.getLogger('wms.boundBill')
@@ -1961,19 +1962,40 @@ class InboundBills(viewsets.ModelViewSet):
         return Response(return_data,status=status.HTTP_200_OK)
     
     def destroy(self, request, pk):
-
         qs = self.get_object()
-        qs.is_delete = True
-        
-        MaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
-        qs.save()
-
-        # import datetime
-        # qs.save()
-        # qs.billId = f"404{qs.billId}{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.number = f"deleted_{qs.number}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.save()
-        return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        bill_number = qs.number
+        bill_id = qs.billId
+        try:
+            qs.is_delete = True
+            MaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
+            qs.save()
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"删除入库单 ID:{pk},单据编号: {bill_number}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP入库管理"
+                )
+            except Exception as e:
+                pass
+            return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"删除入库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP入库管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
     
     def update_inbound(self, request, *args, **kwargs):
         id = self.get_project()
@@ -1982,12 +2004,50 @@ class InboundBills(viewsets.ModelViewSet):
         else:
             bill_obj = InboundBill.objects.filter(billId=id).first()
             if bill_obj:
-                serializer = InboundApplyPOSTSerializer(bill_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = InboundApplyPOSTSerializer(bill_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新入库单 ID:{id},单据编号: {bill_obj.number}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP入库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新入库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP入库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新入库单异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            module_name="ERP入库管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
 
@@ -2048,19 +2108,40 @@ class OutboundBills(viewsets.ModelViewSet):
         return Response(return_data,status=status.HTTP_200_OK)
     
     def destroy(self, request, pk):
-
         qs = self.get_object()
-        qs.is_delete = True
-        
-        OutMaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
-        qs.save()
-
-        # import datetime
-        # qs.save()
-        # qs.billId = f"404{qs.billId}{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.number = f"deleted_{qs.number}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.save()
-        return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        bill_number = qs.number
+        bill_id = qs.billId
+        try:
+            qs.is_delete = True
+            OutMaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
+            qs.save()
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"删除出库单 ID:{pk},单据编号: {bill_number}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP出库管理"
+                )
+            except Exception as e:
+                pass
+            return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"删除出库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP出库管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
     
     def update_outbound(self, request, *args, **kwargs):
         id = self.get_project()
@@ -2069,12 +2150,50 @@ class OutboundBills(viewsets.ModelViewSet):
         else:
             bill_obj = OutboundBill.objects.filter(billId=id).first()
             if bill_obj:
-                serializer = OutboundApplyPOSTSerializer(bill_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = OutboundApplyPOSTSerializer(bill_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新出库单 ID:{id},单据编号: {bill_obj.number}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP出库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新出库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP出库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新出库单异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            module_name="ERP出库管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
 
@@ -2125,12 +2244,51 @@ class Materials(viewsets.ModelViewSet):
         else:
             material_obj = MaterialDetail.objects.filter(id=id).first()
             if material_obj:
-                serializer = MaterialDetailPOSTSerializer(material_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = MaterialDetailPOSTSerializer(material_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新物料明细 ID:{id},物料编码: {material_obj.goods_code}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=id,
+                                module_name="ERP物料管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新物料明细失败 ID:{id},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=id,
+                                module_name="ERP物料管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新物料明细异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            object_id=id,
+                            module_name="ERP物料管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)        
 

+ 1 - 0
greaterwms/urls.py

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

+ 217 - 0
operation_log/README.md

@@ -0,0 +1,217 @@
+# 操作日志模块使用说明
+
+## 概述
+
+操作日志模块用于记录系统中的用户敏感操作,支持按操作级别进行分类,并提供前端界面查看操作历史。
+
+## 功能特性
+
+1. **操作级别分类**:
+   - `view`: 查看操作(蓝色)
+   - `update`: 更新操作(橙色)
+   - `new`: 新增操作(绿色)
+   - `delete`: 删除操作(红色)
+   - `download`: 下载操作(紫色)
+   - `login`: 登录操作(青色)
+   - `logout`: 登出操作(灰色)
+   - `other`: 其他操作(棕色)
+
+2. **操作结果记录**:
+   - `success`: 成功
+   - `failure`: 失败
+
+3. **详细信息记录**:
+   - 操作者信息
+   - IP地址
+   - 请求方法和路径
+   - 模块名称
+   - 操作对象ID
+
+## 使用方法
+
+### 在视图函数中记录操作日志
+
+#### 1. 导入日志记录函数
+
+```python
+from operation_log.views import log_operation, log_success_operation, log_failure_operation
+```
+
+#### 2. 记录成功操作
+
+```python
+def create(self, request, *args, **kwargs):
+    # ... 执行创建操作 ...
+    serializer.save()
+    
+    # 记录操作日志
+    log_success_operation(
+        request=self.request,
+        operation_content=f"创建新记录 ID:{serializer.data['id']}",
+        operation_level="new",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=serializer.data['id'],
+        module_name="模块名称"
+    )
+    
+    return Response(serializer.data, status=200)
+```
+
+#### 3. 记录更新操作
+
+```python
+def update(self, request, pk):
+    # ... 执行更新操作 ...
+    serializer.save()
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"更新记录 ID:{pk}:{request.data}",
+        operation_level="update",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return Response(serializer.data, status=200)
+```
+
+#### 4. 记录删除操作
+
+```python
+def destroy(self, request, pk):
+    # ... 执行删除操作 ...
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"删除记录 ID:{pk}",
+        operation_level="delete",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return Response(status=204)
+```
+
+#### 5. 记录失败操作
+
+```python
+def create(self, request, *args, **kwargs):
+    try:
+        # ... 执行操作 ...
+        serializer.save()
+        
+        log_success_operation(
+            request=self.request,
+            operation_content="创建记录成功",
+            operation_level="new",
+            module_name="模块名称"
+        )
+        
+        return Response(serializer.data, status=200)
+    except Exception as e:
+        # 记录失败操作
+        log_failure_operation(
+            request=self.request,
+            operation_content=f"创建记录失败: {str(e)}",
+            operation_level="new",
+            module_name="模块名称"
+        )
+        
+        raise APIException({"detail": str(e)})
+```
+
+#### 6. 记录查看操作
+
+```python
+def retrieve(self, request, pk):
+    obj = self.get_object()
+    
+    log_operation(
+        request=self.request,
+        operation_content=f"查看记录 ID:{pk}",
+        operation_level="view",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    serializer = self.get_serializer(obj)
+    return Response(serializer.data)
+```
+
+#### 7. 记录下载操作
+
+```python
+def download_file(self, request, pk):
+    # ... 执行下载操作 ...
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"下载文件 ID:{pk}",
+        operation_level="download",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return response
+```
+
+## API接口
+
+### 获取操作日志列表
+
+**GET** `/operation_log/`
+
+**查询参数**:
+- `page`: 页码(默认:1)
+- `page_size`: 每页数量(默认:11)
+- `operation_level`: 操作级别(可选值:view, update, new, delete, download, login, logout, other)
+- `operation_result`: 操作结果(可选值:success, failure)
+- `module_name`: 模块名称
+- `operator`: 操作者(支持模糊搜索)
+- `operation_content__icontains`: 操作内容(模糊搜索)
+- `operation_time__range`: 操作时间范围(格式:YYYY-MM-DD,YYYY-MM-DD)
+
+**示例**:
+```
+GET /operation_log/?operation_level=delete&operation_result=success&page=1
+```
+
+### 获取操作日志详情
+
+**GET** `/operation_log/{id}/`
+
+## 前端界面
+
+前端操作日志页面路径:`/operationlog`
+
+**功能**:
+1. 显示所有操作日志记录
+2. 按操作级别使用不同颜色标识
+3. 支持按操作级别、操作结果、模块名称筛选
+4. 支持按操作内容搜索
+5. 支持按时间范围筛选
+6. 支持分页查看
+
+## 操作级别颜色说明
+
+- **查看 (view)**: 蓝色
+- **更新 (update)**: 橙色
+- **新增 (new)**: 绿色
+- **删除 (delete)**: 红色
+- **下载 (download)**: 紫色
+- **登录 (login)**: 青色
+- **登出 (logout)**: 灰色
+- **其他 (other)**: 棕色
+
+## 注意事项
+
+1. 操作日志记录失败不会影响主要业务逻辑
+2. 操作日志只读,不能通过API创建、更新或删除
+3. 建议在关键操作的视图函数中添加日志记录
+4. 操作级别应该根据实际业务需求选择合适的值
+5. 建议为每个模块设置统一的 `module_name` 以便后续筛选和统计
+

+ 20 - 0
operation_log/filter.py

@@ -0,0 +1,20 @@
+from django_filters import FilterSet
+from .models import OperationLog
+
+
+class OperationLogFilter(FilterSet):
+    class Meta:
+        model = OperationLog
+        fields = {
+            "id": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "operator": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_content": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_level": ['exact', 'iexact', 'in'],
+            "operation_result": ['exact', 'iexact', 'in'],
+            "module_name": ['exact', 'iexact', 'contains', 'icontains'],
+            "object_id": ['exact', 'iexact', 'contains', 'icontains'],
+            "ip_address": ['exact', 'iexact'],
+            "request_method": ['exact', 'iexact', 'in'],
+            "request_path": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_time": ['year', 'month', 'day', 'week_day', 'gt', 'gte', 'lt', 'lte', 'range'],
+        }

+ 13 - 0
operation_log/serializers.py

@@ -0,0 +1,13 @@
+from rest_framework import serializers
+from .models import OperationLog
+
+
+class OperationLogSerializer(serializers.ModelSerializer):
+    operation_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M:%S')
+    operation_level_display = serializers.CharField(source='get_operation_level_display', read_only=True)
+    operation_result_display = serializers.CharField(source='get_operation_result_display', read_only=True)
+
+    class Meta:
+        model = OperationLog
+        fields = '__all__'
+        read_only_fields = ['id', 'operation_time']

+ 12 - 0
operation_log/urls.py

@@ -0,0 +1,12 @@
+from django.urls import path, re_path
+from . import views
+
+urlpatterns = [
+    # 操作日志列表查询接口(只读)
+    path(r'', views.OperationLogViewSet.as_view({"get": "list"}), name="operation_log"),
+    
+    # 操作日志详情查询接口
+    re_path(r'^(?P<pk>\d+)/$', views.OperationLogViewSet.as_view({
+        'get': 'retrieve'
+    }), name="operation_log_detail"),
+]

+ 40 - 6
operation_log/views.py

@@ -1,5 +1,11 @@
 from django.utils import timezone
 from .models import OperationLog
+from rest_framework import viewsets
+from rest_framework.filters import OrderingFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from utils.page import MyPageNumberPagination
+from .serializers import OperationLogSerializer
+from .filter import OperationLogFilter
 
 class OperationLogger:
     @staticmethod
@@ -60,15 +66,29 @@ class OperationLogger:
         if operator is not None:
             return str(operator)
         
-        # 尝试从request中获取用户信息
+        # 优先尝试从request.auth获取(系统的自定义认证方式)
+        if hasattr(request, 'auth') and request.auth:
+            # 如果auth对象有name属性,优先使用
+            if hasattr(request.auth, 'name') and request.auth.name:
+                return str(request.auth.name)
+            # 如果有openid属性
+            elif hasattr(request.auth, 'openid') and request.auth.openid:
+                return f"用户OpenID:{request.auth.openid}"
+            # 如果auth本身是字符串
+            elif isinstance(request.auth, str):
+                return request.auth
+        
+        # 尝试从request.user获取(标准Django认证)
         if hasattr(request, 'user') and request.user.is_authenticated:
             # 优先使用username,如果没有则使用其他标识
-            if hasattr(request.user, 'username'):
+            if hasattr(request.user, 'username') and request.user.username:
                 return request.user.username
             elif hasattr(request.user, 'get_username'):
-                return request.user.get_username()
-            else:
-                return f"用户ID:{request.user.id}" if hasattr(request.user, 'id') else "认证用户"
+                username = request.user.get_username()
+                if username:
+                    return username
+            if hasattr(request.user, 'id'):
+                return f"用户ID:{request.user.id}"
         
         # 如果都没有,返回未知用户
         return "未知用户"
@@ -138,4 +158,18 @@ def log_failure_operation(request, operation_content, operation_level, operator=
         operation_result='failure',
         module_name=module_name,
         object_id=object_id
-    )
+    )
+
+
+class OperationLogViewSet(viewsets.ReadOnlyModelViewSet):
+    """
+    操作日志 ViewSet
+    只读,不支持创建、更新、删除操作
+    """
+    queryset = OperationLog.objects.all()
+    serializer_class = OperationLogSerializer
+    pagination_class = MyPageNumberPagination
+    filter_backends = [DjangoFilterBackend, OrderingFilter]
+    filter_class = OperationLogFilter
+    ordering_fields = ['operation_time', 'id']
+    ordering = ['-operation_time']  # 默认按操作时间倒序排列

+ 1 - 1
reportcenter/files.py

@@ -85,7 +85,7 @@ def BatchList_cn_data_header_list():
         ('bound_number', '入库批次号'),
         ('goods_qty', '商品数量'),
         ('goods_in_qty', '组盘入库数量'),
-        ('goods_in_location_qty', '库位入库数量'),
+        ('goods_in_location_qty', '当前在库数目'),
         ('goods_out_qty', '出库数量'),
 
 

+ 62 - 0
reportcenter/views.py

@@ -39,6 +39,7 @@ 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
+from operation_log.views import log_success_operation, log_failure_operation
 
 
     
@@ -267,6 +268,17 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         response = HttpResponse(content_type='text/csv')
         response['Content-Disposition'] = 'attachment; filename="material_change_summary.csv"'
         
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载物料库存变动汇总文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
+        
         # 创建CSV写入器
         csv_writer = csv.writer(response)
         
@@ -598,6 +610,16 @@ class MaterialChangeHistoryDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='MaterialChangeHistory_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载物料变动历史文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
 
     
@@ -669,6 +691,16 @@ class batchLogDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='batchLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载批次日志文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
 
 class ContainerDetailLogViewSet(viewsets.ModelViewSet):
@@ -737,6 +769,16 @@ class ContainerDetailLogDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='ContainerDetailLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载托盘详情日志文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
 
 
@@ -797,6 +839,16 @@ class FileListDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='stocklist_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载库存流水文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
 
 class FlowsStatsViewSet(viewsets.ModelViewSet):
@@ -936,6 +988,16 @@ class BatchFileViewSet(viewsets.ModelViewSet):
             content_type="text/csv"
         )
         response['Content-Disposition'] = "attachment; filename='批次报表_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载批次列表文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
 
 class bigScreenModelViewSet(viewsets.ModelViewSet):

+ 102 - 3
staff/views.py

@@ -20,6 +20,7 @@ from utils.md5 import Md5
 import random
 from django.contrib.auth.models import User
 from .models import Role, Permission  # 新增角色和权限模型导入
+from operation_log.views import log_success_operation, log_failure_operation, log_operation
 
 class APIViewSet(viewsets.ModelViewSet):
     """
@@ -172,6 +173,17 @@ class APIViewSet(viewsets.ModelViewSet):
             serializer.is_valid(raise_exception=True)
             serializer.save()
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"创建员工:{data.get('staff_name', '未知')}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=serializer.data.get('id'),
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass  # 日志记录失败不影响业务
             return Response(serializer.data, status=200, headers=headers)
 
     def update(self, request, pk):
@@ -212,6 +224,17 @@ class APIViewSet(viewsets.ModelViewSet):
                     t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                 ))
         headers = self.get_success_headers(serializer.data)
+        try:
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新员工 ID:{pk}:{data.get('staff_name', '未知')}",
+                operation_level="update",
+                operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                object_id=pk,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return Response(serializer.data, status=200, headers=headers)
 
     def change_check_code(self, request, pk=None):
@@ -257,6 +280,17 @@ class APIViewSet(viewsets.ModelViewSet):
             # 若不存在则创建,保持行为幂等
             User.objects.create_user(username=str(target_staff.staff_name), password=str(new_code_int))
 
+        try:
+            log_success_operation(
+                request=self.request,
+                operation_content=f"修改员工验证码 ID:{target_staff.id},员工:{target_staff.staff_name}",
+                operation_level="update",
+                operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                object_id=target_staff.id,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return Response({"message": "验证码已更新", "id": target_staff.id, "check_code": target_staff.check_code})
 
     def partial_update(self, request, pk):
@@ -294,6 +328,17 @@ class APIViewSet(viewsets.ModelViewSet):
                         t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                     ))
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"部分更新员工 ID:{pk}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass
             return Response(serializer.data, status=200, headers=headers)
 
     def destroy(self, request, pk):
@@ -305,6 +350,17 @@ class APIViewSet(viewsets.ModelViewSet):
             qs.save()
             serializer = self.get_serializer(qs, many=False)
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"删除员工 ID:{pk},员工名:{qs.staff_name}",
+                    operation_level="delete",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass
             return Response(serializer.data, status=200, headers=headers)
 
 class RoleViewSet(viewsets.ModelViewSet):
@@ -415,6 +471,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
             {"primary_page": "staff", "path": "/staff/stafflist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafflist_check_code", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafftype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/staff/operationlog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/initializeupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/addupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadinbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
@@ -442,7 +499,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
                     "/warehouse/department", "/warehouse/boundcodetype", "/warehouse/boundtype", "/warehouse/boundbusiness", "/warehouse/status", "/warehouse/product",
-                    "/staff/stafflist", "/staff/stafflist_check_code", "/staff/stafftype"
+                    "/staff/stafflist", "/staff/stafflist_check_code", "/staff/stafftype", "/staff/operationlog"
                 ],
                 "component_access": "all"
             },
@@ -457,7 +514,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
-                    "/staff/stafflist", "/staff/stafftype"
+                    "/staff/stafflist", "/staff/stafftype", "/staff/operationlog"
                 ],
                 "component_access": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]
             },
@@ -471,6 +528,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/staff/operationlog",
                     "/dashboard/flows_statements", "/dashboard/flows"
                 ],
                 "component_access": ["view", "edit", "add", "download", "confirm"]
@@ -485,7 +543,8 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
-                    "/dashboard/flows_statements", "/dashboard/flows"
+                    "/dashboard/flows_statements", "/dashboard/flows",
+                    "/staff/operationlog",
                 ],
                 "component_access": ["view", "download"]
             }
@@ -574,6 +633,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
                         role.permissions.set(assigned_permissions)
                         permission_count += len(assigned_permissions)
             
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"恢复默认权限配置,创建权限数:{created_perm_count},更新角色数:{role_count}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except Exception as e:
+                pass
             return Response({
                 "message": "默认权限已恢复成功",
                 "created_permissions": created_perm_count,
@@ -581,6 +650,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
                 "permissions_assigned": permission_count
             })
         except Exception as e:
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"恢复默认权限配置失败:{str(e)}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except:
+                pass
             return Response({
                 "error": "恢复默认权限失败",
                 "detail": str(e)
@@ -653,6 +732,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
             if new_perms:
                 role.permissions.add(*new_perms)
             
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"更新角色权限:{pk},页面:{target_page}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except Exception as e:
+                pass
             return Response({"message": "Permissions updated successfully"})
         except Role.DoesNotExist:
             return Response({"error": "Role not found"}, status=404)
@@ -811,4 +900,14 @@ class FileDownloadView(viewsets.ModelViewSet):
         )
         response['Content-Disposition'] = "attachment; filename='staff_{}.csv'".format(
             str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载员工列表文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return response

+ 55 - 5
stock/views.py

@@ -16,6 +16,7 @@ from .files import FileListRenderCN, FileListRenderEN, FileBinListRenderCN, File
 from rest_framework.settings import api_settings
 from django.db import transaction  
 from rest_framework import status
+from operation_log.views import log_success_operation, log_failure_operation
 
 class stockshelfViewSet(viewsets.ModelViewSet):
 
@@ -94,7 +95,29 @@ class stockshelfViewSet(viewsets.ModelViewSet):
         try:
             with transaction.atomic():
                 shelflist.objects.bulk_create(instances, batch_size=1000)
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"批量创建货架位置,仓库: {warehouse_name}({warehouse_code}), 货架: {shelf_name}, 创建数量: {len(instances)}, 跳过数量: {(rows*cols*layers) - len(instances)}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    module_name="库存管理"
+                )
+            except Exception as e:
+                pass
         except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"批量创建货架位置失败,仓库: {warehouse_name}({warehouse_code}), 货架: {shelf_name}, 错误: {str(e)}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    module_name="库存管理"
+                )
+            except Exception as log_error:
+                pass
             return Response(
                 {"error": str(e)},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -112,11 +135,38 @@ class stockshelfViewSet(viewsets.ModelViewSet):
        
         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)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新货架位置 ID:{pk},仓库: {qs.warehouse_name}({qs.warehouse_code}), 货架: {qs.shelf_name}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="库存管理"
+                )
+            except Exception as e:
+                pass
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"更新货架位置失败 ID:{pk},错误: {str(e)}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="库存管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
 
     def retrieve(self, request, pk):
         qs = self.get_object()

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

@@ -1 +0,0 @@
-.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}

+ 1 - 0
templates/dist/spa/css/13.b352291e.css

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

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
templates/dist/spa/css/33.103b121d.css


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


+ 0 - 1
templates/dist/spa/css/34.c527e777.css

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

+ 1 - 0
templates/dist/spa/css/35.5557cd4a.css

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

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


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


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


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


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


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


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


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


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


+ 0 - 1
templates/dist/spa/css/7.8879267a.css

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

+ 1 - 0
templates/dist/spa/css/7.b5042fa0.css

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

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/index.html


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 1
templates/dist/spa/js/13.ac106c63.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
templates/dist/spa/js/13.eb7c38b1.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
templates/dist/spa/js/33.212de067.js


BIN
templates/dist/spa/js/33.212de067.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 1
templates/dist/spa/js/34.c66dd14f.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/33.e044124a.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
templates/dist/spa/js/35.08b4d310.js


BIN
templates/dist/spa/js/35.08b4d310.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 1
templates/dist/spa/js/35.191d823c.js


BIN
templates/dist/spa/js/35.191d823c.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
templates/dist/spa/js/36.0ef8719a.js


BIN
templates/dist/spa/js/36.0ef8719a.js.gz


BIN
templates/dist/spa/js/37.54c90e2b.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/36.04984167.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/37.54c90e2b.js


BIN
templates/dist/spa/js/38.51ed54fd.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/38.96100c54.js


BIN
templates/dist/spa/js/38.96100c54.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/39.5872b98c.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/40.d194e156.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/41.eed3ba0d.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/42.135aa88c.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/43.ac48efb6.js


BIN
templates/dist/spa/js/44.65975fc6.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/44.65975fc6.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/45.8feb3608.js


BIN
templates/dist/spa/js/47.8ee118cc.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/46.b3afcac9.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/47.8ee118cc.js


BIN
templates/dist/spa/js/48.f5f2c346.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/48.72e06ecf.js


BIN
templates/dist/spa/js/49.d3deb20b.js.gz


BIN
templates/dist/spa/js/50.89d8c121.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/49.d3deb20b.js


BIN
templates/dist/spa/js/50.b2e076a3.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/50.89d8c121.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/51.67372365.js


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


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/52.c0c678a9.js


BIN
templates/dist/spa/js/53.4bd72b33.js.gz


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/53.d09f7623.js


BIN
templates/dist/spa/js/54.13366a89.js.gz


BIN
templates/dist/spa/js/54.f7392283.js.gz


BIN
templates/dist/spa/js/55.7ff76385.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/54.f7392283.js


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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/55.7ff76385.js


BIN
templates/dist/spa/js/56.8954c039.js.gz


BIN
templates/dist/spa/js/56.c76fed61.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/56.c76fed61.js


BIN
templates/dist/spa/js/57.21c754a8.js.gz


BIN
templates/dist/spa/js/57.79b8851a.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/57.79b8851a.js


BIN
templates/dist/spa/js/58.b42c1c9b.js.gz


BIN
templates/dist/spa/js/58.d42ca317.js.gz


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
templates/dist/spa/js/58.d42ca317.js


BIN
templates/dist/spa/js/59.9471f3f5.js.gz


BIN
templates/dist/spa/js/59.bf3b4865.js.gz


+ 0 - 0
templates/dist/spa/js/59.bf3b4865.js


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác