独角鲸同步合作方公司数据项目
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
26 KiB

10 months ago
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)