import datetime import logging import os import traceback from collections import Counter import xlrd3 from django.apps import apps from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.db.models import Q, Count from django.http import FileResponse from django_filters.rest_framework import DjangoFilterBackend from rest_framework.decorators import action from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.generics import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework_jwt.authentication import JSONWebTokenAuthentication from ChaCeRndTrans.basic import CCAIResponse from ChaCeRndTrans.code import SERVER_ERROR, BAD, OK, NOT_FOUND from ChaCeRndTrans.settings import MEDIA_ROOT, SHOW_UPLOAD_PATH, FILE_PATH from rbac.models import Company from staff.models import Staff, Dept from staff.serializers.Serializer import DeptSerializer, DeptModifySerializer from utils.custom import CustomViewBase, RbacPermission, CommonPagination, req_operate_by_user, asyncDeleteFile, \ MyCustomError logger = logging.getLogger('error') class DeptCustomView(CustomViewBase): ''' 部门管理,增删改查 ''' perms_map = ( {"*": "admin"}, {"*": "comadmin"}, {"*": "dept_all"}, {"get": "dept_list"}, {"post": "dept_create"}, {"put": "dept_edit"}, {"delete": "dept_delete"} ) queryset = Dept.objects.all() serializer_class = DeptSerializer pagination_class = CommonPagination filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) search_fields = ("name",) ordering_fields = ("id",) authentication_classes = (JSONWebTokenAuthentication,) permission_classes = (RbacPermission,) # 关联部门的模型 modelNameList = {'Equipment': "设备", 'Building': "建筑物", 'Inassets': "无形资产", 'Longterm': "长期待摊", 'Rentequip': "租赁设备", 'Rentbuild': "租赁建筑"} remark = [{'app': 'asset', 'model': modelName, 'info': info} for modelName, info in modelNameList.items()] remark.append({'app': 'staff', 'model': 'Staff', 'info': '员工'}) def get_serializer_class(self): # 根据请求类型动态变更serializer if self.action == "create": return DeptSerializer elif self.action == "list": return DeptSerializer return DeptModifySerializer def get_queryset(self): # query = self.request.query_params.get('q', None) # q为实际的搜索词 # field = self.request.query_params.get('field', None) # field为具体字段 companyMid = self.request.query_params.get('companyMid', None) # field为具体字段 queryset = Dept.objects.all() # if query and field: # # 根据传递的字段决定搜索的字段 # if field == 'subjectCode': # queryset = queryset.filter(subjectCode__icontains=query) # elif field == 'subjectName': # queryset = queryset.filter(subjectName__icontains=query) if companyMid: queryset = queryset.filter(companyMid=companyMid) return queryset def list(self, request, format=None): """ 列表 """ pagination = {} try: params = request.GET keyword = params.get('keyword') # 关键词 is_rnd = params.get('is_rnd') # 是否研发部 companyMid = params.get('companyMid') page_size = params.get('size') page = params.get('page') sort = params.get('sort') param = [] order_by = 'N.id desc' if sort: order_by = sort if page is None: page = 1 if page_size is None: page_size = 10 start_index = (int(page) - 1) * int(page_size) where = 'WHERE 1=1 ' if not companyMid: return CCAIResponse('公司信息缺失', BAD) if companyMid: where += 'AND N.companyMid = %s ' param.append(companyMid) if is_rnd: where += 'AND N.is_rnd = %s ' param.append(is_rnd) if keyword: where += 'AND N.name like %s ' param.append('%' + keyword + '%') sql = 'SELECT N.id, name, pid_id, (SELECT name from chace_rnd.staff_dept where id = N.pid_id) AS parentName, ' \ '(SELECT COUNT(id) FROM chace_rnd.staff_staff where dept = N.id) AS count, is_rnd, ' \ 'CreateBy, UpdateBy, CreateByUid, UpdateByUid, CreateDateTime, UpdateDateTime, companyMid ' \ 'FROM chace_rnd.staff_dept AS N ' \ '%s ORDER BY %s LIMIT %s,%s ' % (where, order_by, start_index, page_size) query_rows = Dept.objects.raw(sql, param) count_sql = """ select N.id, count(N.id) as count from chace_rnd.staff_dept AS N %s """ % where count_result = Dept.objects.raw(count_sql, param) count = 0 if len(count_result) > 0: count = count_result[0].count rows = [] for item in query_rows: item.__dict__.pop('_state') item.__dict__['CreateDateTime'] = item.__dict__['CreateDateTime'].strftime('%Y-%m-%d') item.__dict__['UpdateDateTime'] = item.__dict__['UpdateDateTime'].strftime('%Y-%m-%d') rows.append(item.__dict__) pagination = { "page": page, "page_size": page_size } return CCAIResponse(rows, count=count, pagination=pagination) except Exception as e: logger.error("user: %s, get dept list failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("获取部门管理列表失败", SERVER_ERROR) def create(self, request, *args, **kwargs): try: data = req_operate_by_user(request) companyMid = request.query_params.get('companyMid', None) if not companyMid: return CCAIResponse("Missing company information", BAD) is_com_exist = Company.objects.filter(MainId=companyMid).exists() if is_com_exist: data['companyMid'] = companyMid serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) # headers = self.get_success_headers(serializer.data) return CCAIResponse(data="success") else: return CCAIResponse("Company information error", BAD) except MyCustomError as e: return CCAIResponse(e.message, BAD) except Exception as e: logger.error("user: %s, staff create failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("create failed", SERVER_ERROR) def update(self, request, *args, **kwargs): from rest_framework.exceptions import ValidationError try: companyMid = request.query_params.get('companyMid', None) data = req_operate_by_user(request) data['companyMid'] = companyMid partial = kwargs.pop('partial', False) # True:所有字段全部更新, False:仅更新提供的字段 instance = self.get_object() children_ids = get_all_children(instance.id) param_pid = data.get('pid') if param_pid and (param_pid in children_ids or param_pid == instance.id): return CCAIResponse("不可选自身与自身的下级部门作为上级部门", BAD) serializer = self.get_serializer(instance, data=data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) if getattr(instance, '_prefetched_objects_cache', None): # If 'prefetch_related' has been applied to a queryset, we need to # forcibly invalidate the prefetch cache on the instance. instance._prefetched_objects_cache = {} return CCAIResponse(data="success") except ValidationError as e: logger.error( "user: %s, update dept cause:can not set self pid \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("can not set self as parent", BAD) except MyCustomError as e: return CCAIResponse(e.message, BAD) except Exception as e: logger.error("user: %s, update dept failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("update dept failed", SERVER_ERROR) def destroy(self, request, *args, **kwargs): try: instance = self.get_object() # 查询是否存在关联 is_association = self.check_association(deptId=instance.id) if is_association: return CCAIResponse(is_association, BAD) self.perform_destroy(instance) return CCAIResponse(data="delete resource success") except Exception as e: logger.error("user: %s, delete dept failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("delete dept failed", SERVER_ERROR) def check_association(self, deptId): """ 检查部门关联的状态,遍历查询所有与部门关联的表 return False-不存在关联 True-存在关联 """ try: isExist = False # 不存在关联 info = None code = None # 存在关联的人员或设备的编码 for item in self.remark: # 遍历查询是否存在关联,一旦存在关联就不给删除 Model = apps.get_model(app_label=item.get('app'), model_name=item.get('model')) if not isExist: if 'staff' == item.get('app'): isExist = Model.objects.filter(dept=deptId).first() else: isExist = Model.objects.filter(departmentId=deptId).first() if isExist: info = item.get('info') code = getattr(isExist, 'code' if 'staff' != item.get('app') else 'idCode') else: break if isExist: return f'{info}:{code} 与部门存在关联!' else: return False except Exception as e: logger.error("check staff association failed: \n%s" % traceback.format_exc()) raise Exception('检查员工关联状态失败') @action(methods=['delete'], detail=False) def multiple_delete(self, request, *args, **kwargs): try: delete_id = request.query_params.get('ids', None) if not delete_id: return CCAIResponse("参数不对啊!", NOT_FOUND) for i in delete_id.split(','): # 查询是否存在关联 is_association = self.check_association(deptId=i) if not is_association: # 不存在关联,才删除 get_object_or_404(self.queryset, pk=int(i)).delete() else: # 存在关联,不删除,跳过 return CCAIResponse(is_association, BAD) return CCAIResponse("批量删除成功", OK) except Exception as e: logger.error("multiple delete dept failed: \n%s" % traceback.format_exc()) return CCAIResponse("批量删除失败", SERVER_ERROR) @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated], url_name='download', url_path='download') def download(self, request, *args, **kwargs): """导入模板下载""" try: file = open(MEDIA_ROOT + '/部门批量导入模板.xlsx', 'rb') response = FileResponse(file) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = 'attachment;filename="部门批量导入模板.xlsx"' return response except Exception as e: logger.error("user: %s, get dept template file failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("下载部门批量导入模板失败", SERVER_ERROR) @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], url_path="import_excel", url_name="import_excel") def import_excel(self, request): """导入excel表数据""" try: params = request.data companyMid = request.query_params.get('companyMid', None) file_name = params.get('file_name') if not companyMid or not file_name: return CCAIResponse('Missing params', BAD) file_paths = file_name.split(",") if len(file_paths) >= 2: return CCAIResponse("部门数据暂不支持同时多文件上传", BAD) excel_file = file_paths[0].replace(SHOW_UPLOAD_PATH, FILE_PATH) with xlrd3.open_workbook(excel_file) as data: sheets = list(data.sheets()) if len(sheets) > 1: return CCAIResponse("部门数据暂不支持同时多表格上传", BAD) table = sheets[0] # 只读取第一张表 rows = table.nrows check_value = table.row_values(0) if (check_value[0] != '部门名称' and check_value[1] != '是否研发部门' and check_value[2] != '上级部门') or (len(check_value) != 3): return CCAIResponse("文件不符合模板标准", SERVER_ERROR) # 查询该公司已存在的部门记录 dept_tree_list = [] db_depts = self.get_queryset().order_by('pid') deptId_dept_map = {d.id: d for d in db_depts} db_depts_name = {x.name: deptid for deptid, x in deptId_dept_map.items()} for d in db_depts: if not d.pid: dept_tree_list.append(d) dept_col_data = [str(dname).strip() for dname in table.col_values(0)[1:] if dname and str(dname).strip()] # 获取部门列数据 deptCounter = Counter(dept_col_data) error_dept_name_set = [item for item, count in deptCounter.items() if count > 1] if error_dept_name_set: return CCAIResponse(f"部门名称列存在重复{','.join(error_dept_name_set)}", BAD) dept_parent_col_data = [str(pName).strip() for pName in table.col_values(2)[1:] if pName and str(pName).strip()] # 获取上级部门列数据 error_dept_name_set = set(db_depts_name.keys()) - (set(db_depts_name.keys()) - set(dept_col_data)) if error_dept_name_set: return CCAIResponse(f"部门已存在:{','.join(error_dept_name_set)}", BAD) error_dept_name_set = set(dept_parent_col_data) - (set(db_depts_name.keys()) | set(dept_col_data)) if error_dept_name_set: return CCAIResponse(f"未知部门:{','.join(error_dept_name_set)}", BAD) # 数据库事务 with transaction.atomic(): try: for row in range(1, rows): row_values = table.row_values(row) # 每一行的数据 # 校验必填项 if not row_values[0] or str(row_values[0]).strip() == '': raise MyCustomError("部门名称必填!") if len(str(row_values[0]).strip()) > 20: raise MyCustomError("部门名称过长,请控制在20字符内!") is_rnd = False if row_values[1]: is_rnd_str = str(row_values[1]).strip() if is_rnd_str and is_rnd_str not in ['是', '否', '不是']: raise MyCustomError(f"部门研发属性:{str(row_values[1])}错误,请在「'是', '否', '不是'」中选择!") if '是' == is_rnd_str: is_rnd = True parent_dept_name = None parent_id = None if row_values[2] and str(row_values[2]).strip(): parent_dept_name = str(row_values[2]).strip() if parent_dept_name: if parent_dept_name == str(row_values[0]).strip(): raise MyCustomError(f"部门:{parent_dept_name},不可以以自身为上级") parent_id = db_depts_name.get(parent_dept_name) dept = Dept() dept.name = str(row_values[0]).strip() dept.is_rnd = is_rnd dept.CreateBy = request.user.name dept.CreateByUid = request.user.id dept.CreateDateTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') dept.companyMid = companyMid if parent_dept_name: dept.parent_dept_name = parent_dept_name if parent_id: dept.pid = deptId_dept_map.get(parent_id) dept.save() deptId_dept_map[dept.id] = dept db_depts_name[dept.name] = dept.id if not parent_dept_name: dept_tree_list.append(dept) # 查看是否存在未设置pid的部门 for deptId, d in deptId_dept_map.items(): if hasattr(d, 'parent_dept_name') and not d.pid: # if d.pid.id not in deptId_dept_map: if d.parent_dept_name not in db_depts_name: raise MyCustomError("数据有误!") d.pid = deptId_dept_map.get(db_depts_name.get(d.parent_dept_name)) d.save() if check_circular_association(request, list(deptId_dept_map.values())): raise MyCustomError("存在循环上下级关系!") except MyCustomError as e: transaction.set_rollback(True) return CCAIResponse(e.message, BAD) # 异步删除文件 try: if os.path.exists(MEDIA_ROOT + '/' + excel_file): # 如果文件存在 # 删除文件,可使用以下两种方法。 os.remove(MEDIA_ROOT + '/' + excel_file) except Exception as e: logger.error( "user: %s, export ccw user xlsx file failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse(data="success") except Exception as e: logger.error("user: %s, import dept file failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("导入部门记录失败", SERVER_ERROR) @classmethod def changeCount(cls, method, deptId): """ 改变部门关联数量 method: 1-新增关联 2-取消关联 return: True操作成功 False操作失败 """ if not deptId or not method: return False try: dept = Dept.objects.get(id=deptId) except Dept.DoesNotExist: logger.error("dept do not exist: \n%s" % (traceback.format_exc(),)) return False else: try: if 1 == method: # 新增关联 dept.relCount += 1 elif 2 == method: if dept.relCount > 0: # 大于0才取消关联 dept.relCount -= 1 else: return False dept.save() return True except Exception as e: logger.error("change dept relCount failed: \n%s" % (traceback.format_exc(),)) return False @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], url_path="getDeptMap", url_name="getDeptMap") def getDeptMap(self, request): try: params = request.GET companyMid = params.get('companyMid') if not companyMid: return CCAIResponse("Missing company information", BAD) if companyMid: query = Q(companyMid=companyMid) dept = Dept.objects.filter(query).values('id', 'name') return CCAIResponse(dept) except Exception as e: logger.error("user: %s, getDeptMap failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("getDeptMap failed", SERVER_ERROR) @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], url_path="tree", url_name="tree") def tree(self, request, format=None): """ 树状列表 """ params = request.GET companyMid = params.get('companyMid') if not companyMid: return CCAIResponse("Missing company information", BAD) # 统计员工表不同部门人数 sql = 'SELECT id, dept, COUNT(id) AS count FROM chace_rnd.staff_staff WHERE companyMid = %s and status = 0 GROUP BY dept ' result = Staff.objects.raw(sql, [companyMid]) # result = Staff.objects.filter(companyMid=companyMid, status=0).values('dept').annotate(count=Count('id')) deptCountDict = {item.dept: item.count for item in result} queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) serializer = self.get_serializer(queryset, many=True) tree_dict = {} tree_data = [] try: # 初始赋值字典与count for item in serializer.data: if item['id'] in deptCountDict: item['count'] = deptCountDict[item['id']] else: item['count'] = 0 tree_dict[item["id"]] = item # 设置树状结构 for i in tree_dict: if tree_dict[i]["pid"]: pid = tree_dict[i]["pid"] parent = tree_dict[pid] parent.setdefault("children", []).append(tree_dict[i]) else: tree_data.append(tree_dict[i]) # 统计部门人数 1:递归 2:多层遍历 for dept in tree_data: # 第一层部门 if 'children' in dept: # 若存在下级部门,调用方法递归累加最终数量 self.getCountFromTree(dept) results = tree_data except KeyError: results = serializer.data if page is not None: return self.get_paginated_response(results) return CCAIResponse(results) def getCountFromTree(self, parentDept): """ (递归)设置并返回当前本部门的下级部门的总人数 """ sum = 0 # 子部门人数统计 for dept in parentDept['children']: if 'children' in dept: # 存在下级部门,递归调用,设置当前部门人数,并返回上级,本部门人数 deptCount = self.getCountFromTree(dept) sum += deptCount else: # 最底层部门不需要设置,只需要累计到上层 sum += dept['count'] parentDept['count'] += sum # 本部门 + 子级部门 return parentDept['count'] # def getCountFromTree(self, parentDept): # """ # (模拟栈)设置并返回当前本部门的下级部门的总人数 # """ # stack = [parentDept] # 使用栈来模拟递归的过程 # # results = [] # 用于保存每次调用的结果 # while stack: # parentDept = stack.pop() # sum = 0 # 子部门人数统计 # for dept in parentDept['children']: # if 'children' in dept: # 存在下级部门,递归调用,设置当前部门人数,并返回上级,本部门人数 # stack.append(dept) # # deptCount = self.getCountFromTree(dept) # # sum += deptCount # else: # 最底层部门不需要设置,只需要累计到上层 # sum += dept['count'] # parentDept['count'] += sum # 本部门 + 子级部门 # # results.append(parentDept['count']) def get_all_children(deptId, max_depth=50, current_depth=0): # 如果设置了最大深度并且当前深度已经达到或超过最大深度,则返回空列表 if current_depth >= max_depth: return [] children = [] sub_depts = Dept.objects.filter(pid=deptId) for sub_dept in sub_depts: children.append(sub_dept.id) # 在递归调用时增加当前深度 children.extend(get_all_children(sub_dept.id, max_depth, current_depth + 1)) return children def check_circular_association(request, dept_list): """ 检查部门循环上下级, 存在循环问题会返回True,否则返回False deptId_dept_map: [ Node1, Node2, ... ] """ try: if not dept_list or 0 == len(dept_list): return False for node in dept_list: nodeList = [node, ] n = 0 while node.pid: n += 1 if n >= 50: return True if node.pid in nodeList: return True node = node.pid nodeList.append(node) return False except Exception as e: logger.error("user: %s, check_circular_association failed: \n%s" % (request.user.id, traceback.format_exc())) return CCAIResponse("check_circular_association failed", SERVER_ERROR)