962 lines
39 KiB
Python
Executable File
962 lines
39 KiB
Python
Executable File
|
||
from django.conf import settings
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
from rest_framework import serializers
|
||
from rest_framework.decorators import action
|
||
from rest_framework.exceptions import ParseError
|
||
from rest_framework.mixins import ListModelMixin
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework.response import Response
|
||
|
||
from apps.hrm.errors import NO_NEED_LEVEL_REMARK
|
||
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
|
||
NotWorkRemarkFilterSet)
|
||
from apps.hrm.models import (Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation,
|
||
EmpNeed, EmpJoin, Leave, EmployeeTransfer, Probation, EmpPersonInfo, EmpContract)
|
||
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
|
||
ClockRecordListSerializer,
|
||
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
|
||
EmployeeNotWorkRemarkSerializer,EmpPersonInfoSerializer,
|
||
EmployeeSerializer,
|
||
ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
|
||
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
|
||
ResignationSerializer, EmpNeedSerializer, LeaveSerializer, TransferSerializer, ProbationSerializer, EmpContractSerializer)
|
||
from apps.hrm.services import HrmService
|
||
|
||
from apps.third.dahua import dhClient
|
||
from apps.third.tapis import dhapis
|
||
from openpyxl import load_workbook
|
||
from django.db import transaction
|
||
from django.core.exceptions import ValidationError
|
||
from datetime import datetime
|
||
from apps.utils.export import export_excel
|
||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, RetrieveModelMixin
|
||
from apps.wf.models import Ticket, Workflow, State
|
||
from apps.wf.mixins import TicketMixin
|
||
from apps.wf.services import WfService
|
||
from apps.system.models import Post, Dept
|
||
from django.db.models import DateField
|
||
from datetime import datetime, date
|
||
from openpyxl.utils.datetime import from_excel
|
||
import os
|
||
import re
|
||
epTypeOptions = {'employee': '正式员工', 'remployee': '相关方',
|
||
'visitor': '访客', 'driver': '货车司机'}
|
||
epStateOptions = {10: '在职', 20: '离职', 30: '退休'}
|
||
crOptions = {10: '上班打卡', 20: '下班打卡', 30: ''}
|
||
crEoptions = {10: '在岗时间短', 20: '在岗时间长', 30: '缺卡', 40: '加班'}
|
||
|
||
HEAD_MAP = {
|
||
"所属部门": "belong_dept",
|
||
"车间": "workshop",
|
||
"职务": "position",
|
||
"职务聘任日期": "office_date",
|
||
"类别": "type",
|
||
"性别": "gender",
|
||
"身份证号码:姓名": "name",
|
||
"身份证号码:身份证号": "id_number",
|
||
"银行卡号码": "bank_card",
|
||
"入职日期": "start_date",
|
||
"合同到期日": "contract_end_date",
|
||
"转正日期": "regular_date",
|
||
"出生日期": "birthday",
|
||
"年龄": "age",
|
||
"工龄": "work_years",
|
||
"政治面貌": "partisan",
|
||
"入党时间": "join_partisan_date",
|
||
"民族": "nation",
|
||
"婚姻状况": "marriage",
|
||
"户口性质": "hukou_type",
|
||
"籍贯": "birthplace",
|
||
"户籍地址": "hukou_address",
|
||
"现住地址": "address",
|
||
"枣庄市职称": "zhuanzhi",
|
||
"枣庄市职称证书:图片_1": "zhuanzhi_img_1",
|
||
"枣庄市职称证书:图片_2": "zhuanzhi_img_2",
|
||
"获得枣庄市职称日期": "zhuanzhi_date",
|
||
"总院/集团职称": "zyjt_zhuanzhi",
|
||
"总院/集团职称证书:图片_1": "zyjt_zhuanzhi_img_1",
|
||
"获得职称日期": "zyjt_zhuanzhi_date",
|
||
"职业技能等级": "skill_rank",
|
||
"获得技能等级证书日期": "skill_rank_date",
|
||
"全日制最高学历": "full_edu",
|
||
"全日制最高学历学校名称": "full_edu_school",
|
||
"全日制最高学历所学专业": "full_edu_major",
|
||
"全日制最高学历入学时间-毕业时间": "full_edu_time",
|
||
"非全日制最高学历": "part_edu",
|
||
"非全日制最高学历学校名称": "part_edu_school",
|
||
"非全日制最高学历所学专业": "part_edu_major",
|
||
"非全日制最高学历入学时间-毕业时间": "part_edu_time",
|
||
"本人联系电话": "phone",
|
||
"紧急联系人姓名": "emergency_contact",
|
||
"紧急联系人电话": "emergency_phone",
|
||
"所获荣誉": "honor",
|
||
"首次缴纳社保日期": "first_social_security_date",
|
||
"是否为退役军人": "is_veteran",
|
||
"个人邮箱":"email",
|
||
}
|
||
|
||
def cell_value(cell):
|
||
val = cell.value
|
||
if isinstance(val, datetime):
|
||
return val.date()
|
||
return val.strip() if isinstance(val, str) else val
|
||
|
||
class EmployeeViewSet(CustomModelViewSet):
|
||
"""
|
||
人员管理
|
||
"""
|
||
queryset = Employee.objects.exclude(user__is_superuser=True)
|
||
select_related_fields = ['user']
|
||
filterset_class = EmployeeFilterSet
|
||
serializer_class = EmployeeSerializer
|
||
retrieve_serializer_class = EmployeeDetailSerializer
|
||
create_serializer_class = EmployeeCreateUpdateSerializer
|
||
update_serializer_class = EmployeeCreateUpdateSerializer
|
||
partial_update_serializer_class = EmployeeCreateUpdateSerializer
|
||
search_fields = ['name', 'number',
|
||
'user__username', 'id_number', 'id', 'phone']
|
||
ordering = ['-pk']
|
||
|
||
# def filter_queryset(self, queryset):
|
||
# if not self.detail:
|
||
# self.request.query_params._mutable = True
|
||
# self.request.query_params.setdefault('type', 'employee')
|
||
# return super().filter_queryset(queryset)
|
||
|
||
@action(methods=['get'], detail=False, perms_map={'get': '*'},
|
||
serializer_class=serializers.Serializer)
|
||
def info(self, request, pk=None):
|
||
"""个人信息
|
||
|
||
个人信息
|
||
"""
|
||
user = request.user
|
||
ep, _ = Employee.objects.get_or_create(user=user,
|
||
defaults={
|
||
"user": user,
|
||
"name": user.name,
|
||
"phone": user.phone,
|
||
"belong_dept": user.belong_dept,
|
||
"post": user.post,
|
||
"type": user.type
|
||
})
|
||
return Response(EmployeeSerializer(instance=ep).data)
|
||
|
||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated],
|
||
serializer_class=EmployeeImproveSerializer)
|
||
@transaction.atomic
|
||
def improve_info(self, request, *args, **kwargs):
|
||
"""完善个人信息
|
||
|
||
完善个人信息
|
||
"""
|
||
user = request.user
|
||
ep = user.employee
|
||
serializer = EmployeeImproveSerializer(instance=ep, data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
vdata = serializer.validated_data
|
||
if vdata.get('photo', None):
|
||
dhClient.request(**dhapis['person_img_upload'],
|
||
file_path_rela=vdata['photo'])
|
||
serializer.save()
|
||
if ep.type == 'remployee':
|
||
from apps.rpm.services import sync_to_rep
|
||
sync_to_rep(ep)
|
||
elif ep.type in ['visitor', 'driver']:
|
||
from apps.vm.services import sync_to_visitor
|
||
sync_to_visitor(ep)
|
||
return Response()
|
||
|
||
@action(methods=['post'], detail=True, perms_map={'post': 'employee.notworkremark'},
|
||
serializer_class=EmployeeNotWorkRemarkSerializer)
|
||
def not_work_remark(self, request, pk=None):
|
||
"""
|
||
填写离岗说明
|
||
"""
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
obj = self.get_object()
|
||
if not obj.is_atwork:
|
||
remark = request.data.get('not_work_remark', '')
|
||
obj.not_work_remark = remark
|
||
obj.save()
|
||
now = timezone.now()
|
||
instance, created = NotWorkRemark.objects.get_or_create(
|
||
not_work_date=now.date(),
|
||
user=obj.user,
|
||
defaults={
|
||
"not_work_date": now.date(),
|
||
"user": obj.user,
|
||
"remark": remark,
|
||
"create_by": request.user,
|
||
}
|
||
)
|
||
if not created:
|
||
instance.remark = remark
|
||
instance.update_by = request.user
|
||
instance.save()
|
||
return Response()
|
||
raise ParseError(**NO_NEED_LEVEL_REMARK)
|
||
|
||
@transaction.atomic
|
||
@action(methods=['post'], detail=True, perms_map={'post': 'employee.channel_authority'},
|
||
serializer_class=serializers.Serializer)
|
||
def channel_authority(self, request, pk=None):
|
||
"""重新门禁授权
|
||
|
||
重新门禁授权
|
||
"""
|
||
obj = self.get_object()
|
||
if obj.third_info.get('dh_face_card', None):
|
||
HrmService.door_auth(ep=obj)
|
||
else:
|
||
raise ParseError('该员工缺少主卡')
|
||
# objs = Employee.objects.filter(pk__in=vdata['pks'], third_info__dh_face_card__isnull=False)
|
||
# infos = objs.values('third_info')
|
||
# cards = []
|
||
# for i in infos:
|
||
# if isinstance(i['third_info'], dict) and 'dh_face_card' in i['third_info']:
|
||
# cards.append(i['third_info']['dh_face_card'])
|
||
# details = []
|
||
# for i in vdata['channels']:
|
||
# details.append({
|
||
# "privilegeType": 1,
|
||
# "resouceCode": i
|
||
# })
|
||
# if cards and details:
|
||
# json_data = {
|
||
# "cardNumbers": cards,
|
||
# "timeQuantumId": 1,
|
||
# "cardPrivilegeDetails": details
|
||
# }
|
||
# dhClient.request(**dhapis['card_door_authority'], json=json_data)
|
||
# for i in objs:
|
||
# i.third_info['dh_channels'] = vdata['channels']
|
||
# Employee.objects.bulk_update(objs, fields=['third_info'])
|
||
return Response()
|
||
|
||
@transaction.atomic
|
||
@action(methods=['post'], detail=False, perms_map={'post': 'employee.face_bind_1'},
|
||
serializer_class=serializers.Serializer)
|
||
def face_bind_1(self, request, pk=None):
|
||
"""同步人脸库1
|
||
|
||
全部人脸库
|
||
"""
|
||
# 获取设备
|
||
# json_data = {
|
||
# "pageNum": 1,
|
||
# "pageSize": 1000,
|
||
# "ownerCodes": ['001'],
|
||
# "showChildNodeData": 1,
|
||
# # "isOnline":1
|
||
# }
|
||
# _, res = dhClient.request(**dhapis['dev_page'], json=json_data)
|
||
# devs = []
|
||
# if res['pageData']:
|
||
# for i in res['pageData']:
|
||
# devs.append(i['deviceCode'])
|
||
# 编辑人像库
|
||
json_data = {
|
||
"groupid": settings.DAHUA_FACEGROUPID_1,
|
||
"groupname": "全体人员",
|
||
"groupdetail": "全体人员",
|
||
"grouptype": 3,
|
||
"deviceCodeList": [settings.DAHUA_IVSS_CODE],
|
||
"syncState": 0
|
||
}
|
||
dhClient.request(**dhapis['face_group_update'], json=json_data)
|
||
# 人像绑定
|
||
dhClient.face_bind()
|
||
return Response()
|
||
|
||
@action(methods=['get'], detail=False, perms_map={'get': 'employee.face_bind_1'},
|
||
serializer_class=serializers.Serializer)
|
||
def face_status_1(self, request, pk=None):
|
||
"""人像下发状态
|
||
|
||
人像下发状态
|
||
"""
|
||
params = {'id': settings.DAHUA_FACEGROUPID_1}
|
||
_, res = dhClient.request(**dhapis['face_group_info'], params=params)
|
||
return Response(res)
|
||
|
||
@action(methods=['get'], detail=False, perms_map={'get': '*'},
|
||
serializer_class=serializers.Serializer)
|
||
def export_excel(self, request, pk=None):
|
||
"""导出excel
|
||
导出excel
|
||
"""
|
||
# 导出列定义: (表头, 取值函数)。覆盖人员的全部业务字段。
|
||
def _cell(v):
|
||
return '' if v is None else v
|
||
columns = [
|
||
('人员类型', lambda i: epTypeOptions.get(i['type'], i['type'])),
|
||
('姓名', lambda i: i['name']),
|
||
('性别', lambda i: i['gender']),
|
||
('人员编号', lambda i: i['number']),
|
||
('手机号', lambda i: i['phone']),
|
||
('个人邮箱', lambda i: i['email']),
|
||
('身份证号', lambda i: i['id_number']),
|
||
('所属部门', lambda i: i.get('belong_dept_name', '')),
|
||
('所属岗位', lambda i: i.get('post_name', '')),
|
||
('车间', lambda i: i['workshop']),
|
||
('职务', lambda i: i['position']),
|
||
('职务聘任日期', lambda i: i['office_date']),
|
||
('在职状态', lambda i: epStateOptions.get(i['job_state'], i['job_state'])),
|
||
('入职日期', lambda i: i['start_date']),
|
||
('转正日期', lambda i: i['regular_date']),
|
||
('合同到期日', lambda i: i['contract_end_date']),
|
||
('离职日期', lambda i: i['end_date']),
|
||
('银行卡号', lambda i: i['bank_card']),
|
||
('出生年月日', lambda i: i['birthday']),
|
||
('年龄', lambda i: i['age']),
|
||
('工龄', lambda i: i['work_years']),
|
||
('学历', lambda i: i['qualification']),
|
||
('政治面貌', lambda i: i['partisan']),
|
||
('入党时间', lambda i: i['join_partisan_date']),
|
||
('民族', lambda i: i['nation']),
|
||
('婚姻状况', lambda i: i['marriage']),
|
||
('户口性质', lambda i: i['hukou_type']),
|
||
('籍贯', lambda i: i['birthplace']),
|
||
('户籍地址', lambda i: i['hukou_address']),
|
||
('现住地址', lambda i: i['address']),
|
||
('枣庄市职称', lambda i: i['zhuanzhi']),
|
||
('获得枣庄市职称日期', lambda i: i['zhuanzhi_date']),
|
||
('总院/集团职称', lambda i: i['zyjt_zhuanzhi']),
|
||
('获得职称日期', lambda i: i['zyjt_zhuanzhi_date']),
|
||
('职业技能等级', lambda i: i['skill_rank']),
|
||
('获得技能等级证书日期', lambda i: i['skill_rank_date']),
|
||
('全日制最高学历', lambda i: i['full_edu']),
|
||
('全日制最高学历学校名称', lambda i: i['full_edu_school']),
|
||
('全日制最高学历专业', lambda i: i['full_edu_major']),
|
||
('全日制最高学历毕业时间', lambda i: i['full_edu_time']),
|
||
('非全日制最高学历', lambda i: i['part_edu']),
|
||
('非全日制最高学历学校名称', lambda i: i['part_edu_school']),
|
||
('非全日制最高学历专业', lambda i: i['part_edu_major']),
|
||
('非全日制最高学历毕业时间', lambda i: i['part_edu_time']),
|
||
('紧急联系人姓名', lambda i: i['emergency_contact']),
|
||
('紧急联系人电话', lambda i: i['emergency_phone']),
|
||
('所获荣誉', lambda i: i['honor']),
|
||
('首次缴纳社保日期', lambda i: i['first_social_security_date']),
|
||
('是否为退役军人', lambda i: '是' if i['is_veteran'] else '否'),
|
||
('定位卡号', lambda i: i['blt_'].get('code', '') if i.get('blt_') else ''),
|
||
('创建时间', lambda i: (i.get('create_time') or '')[:19].replace('T', ' ')),
|
||
]
|
||
field_data = [c[0] for c in columns]
|
||
queryset = self.filter_queryset(self.get_queryset()).select_related(
|
||
'belong_dept', 'post', 'blt')
|
||
odata = EmployeeSerializer(queryset, many=True).data
|
||
# 处理数据
|
||
data = []
|
||
for i in odata:
|
||
data.append([_cell(getter(i)) for _, getter in columns])
|
||
return Response({'path': export_excel(field_data, data, '人员信息')})
|
||
|
||
@action(methods=['post'], detail=False, perms_map={'post': 'employee.import_excel'},
|
||
serializer_class=serializers.Serializer)
|
||
def import_excel(self, request, pk=None):
|
||
import logging
|
||
myLogger = logging.getLogger('log')
|
||
"""导入excel"""
|
||
file_path = request.data.get('file_path')
|
||
relative_path = file_path.lstrip('/')
|
||
abs_path = os.path.join(settings.BASE_DIR, relative_path)
|
||
wb = load_workbook(abs_path, data_only=True)
|
||
sheet = wb.active
|
||
|
||
rows = list(sheet.iter_rows())
|
||
if len(rows) < 2:
|
||
raise ParseError('Excel 没有数据')
|
||
|
||
# 获取表头
|
||
headers = [cell_value(c) for c in rows[0]]
|
||
field_index = {}
|
||
for idx, header in enumerate(headers):
|
||
if header in HEAD_MAP:
|
||
field_index[HEAD_MAP[header]] = idx
|
||
|
||
# 外键缓存
|
||
dept_map = {d.name: d.id for d in Dept.objects.all()}
|
||
TYPE_MAPPING = {
|
||
'光芯': 'employee',
|
||
'外包': 'remployee'
|
||
}
|
||
success = 0
|
||
errors = []
|
||
|
||
# 获取模型字段
|
||
model_fields = {f.name: f for f in Employee._meta.fields}
|
||
|
||
from django.db.models import CharField, TextField
|
||
_ABSENT = object() # 区分“列没出现”与“列为空”
|
||
_KEEP = object() # 该字段不可清空时,保留原值
|
||
|
||
def empty_value(field_name):
|
||
"""全量覆盖时,空单元格应写入的“清空值”;不可清空的字段返回 _KEEP。"""
|
||
if field_name == 'belong_dept_id':
|
||
return None # 部门 FK 可空
|
||
mf = model_fields.get(field_name)
|
||
if mf is None:
|
||
return _KEEP
|
||
if getattr(mf, 'null', False):
|
||
return None
|
||
if isinstance(mf, (CharField, TextField)) and getattr(mf, 'blank', False):
|
||
return ''
|
||
return _KEEP # 非空且不可为空(如布尔/性别/类别)-> 保留原值
|
||
|
||
for row_num, row in enumerate(rows[1:], start=2):
|
||
try:
|
||
data = {}
|
||
for field, idx in field_index.items():
|
||
raw_value = cell_value(row[idx])
|
||
model_field = model_fields.get(field)
|
||
value = convert_field_value(model_field, raw_value, row_num)
|
||
data[field] = value
|
||
|
||
# 移除可能的id字段
|
||
if 'id' in data:
|
||
del data['id']
|
||
|
||
# 获取唯一标识
|
||
id_number = data.get("id_number")
|
||
name = data.get("name")
|
||
|
||
if not name:
|
||
raise ParseError(f'第{row_num}行,姓名为空')
|
||
|
||
myLogger.info(f"处理第{row_num}行:{name} - {id_number}")
|
||
# 处理人员类型
|
||
if 'type' in data and data['type']:
|
||
excel_type = data['type']
|
||
if excel_type in TYPE_MAPPING:
|
||
data['type'] = TYPE_MAPPING[excel_type]
|
||
else:
|
||
raise ParseError(f'第{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
|
||
# 处理部门外键:填了校验并赋值;列存在但为空 -> 清空(新增场景的必填在创建处校验)
|
||
dept_raw = data.pop('belong_dept', _ABSENT)
|
||
if dept_raw is not _ABSENT:
|
||
if dept_raw:
|
||
if dept_raw not in dept_map:
|
||
raise ParseError(f'第{row_num}行,部门"{dept_raw}"不存在')
|
||
data['belong_dept_id'] = dept_map[dept_raw]
|
||
else:
|
||
data['belong_dept_id'] = None
|
||
|
||
# 数据验证
|
||
if data.get('phone'):
|
||
if not re.match(r'^1[3-9]\d{9}$', data['phone']):
|
||
raise ParseError(f'第{row_num}行,手机号格式不正确')
|
||
|
||
if data.get('id_number'):
|
||
if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']):
|
||
raise ParseError(f'第{row_num}行,身份证号格式不正确')
|
||
|
||
# 按姓名匹配并更新:姓名唯一则直接更新(身份证号可为空);
|
||
# 存在重名则必须用身份证号定位具体记录,否则报错。
|
||
try:
|
||
with transaction.atomic():
|
||
name_matches = list(Employee.objects.filter(name=name))
|
||
if len(name_matches) <= 1:
|
||
existing = name_matches[0] if name_matches else None
|
||
# 同名但身份证号不一致,视为不同人员,新建
|
||
if existing and id_number and existing.id_number \
|
||
and existing.id_number != id_number:
|
||
existing = None
|
||
else:
|
||
# 存在重名,必须用身份证号区分
|
||
if not id_number:
|
||
raise ParseError(f'第{row_num}行,姓名"{name}"存在重名,必须填写身份证号')
|
||
existing = next(
|
||
(e for e in name_matches if e.id_number == id_number), None)
|
||
|
||
if existing:
|
||
# 全量覆盖:以本次 Excel 为准。填了值的列覆盖原值;
|
||
# 空单元格清空原值(不可为空的字段如布尔/性别/类别则保留原值)。
|
||
updated_fields = []
|
||
for field_name, value in data.items():
|
||
if value in [None, '']:
|
||
value = empty_value(field_name)
|
||
if value is _KEEP:
|
||
continue
|
||
if getattr(existing, field_name, None) != value:
|
||
setattr(existing, field_name, value)
|
||
updated_fields.append(field_name)
|
||
if updated_fields:
|
||
existing.save(update_fields=updated_fields + ['update_time'])
|
||
myLogger.info(f"✅ 第{row_num}行更新成功:{name},更新字段:{updated_fields}")
|
||
else:
|
||
myLogger.info(f"⏭️ 第{row_num}行无变化:{name}")
|
||
created = False
|
||
else:
|
||
# 新增人员时所属部门必填
|
||
if not data.get('belong_dept_id'):
|
||
raise ParseError(f'第{row_num}行,新增人员时所属部门不能为空')
|
||
# 新增时丢弃空值,交给模型默认值/允许为空
|
||
create_data = {k: v for k, v in data.items() if v is not None}
|
||
Employee.objects.create(**create_data)
|
||
created = True
|
||
except Exception as e:
|
||
raise
|
||
if created:
|
||
myLogger.info(f"✅ 第{row_num}行新增成功:{name}")
|
||
|
||
success += 1
|
||
|
||
except Exception as e:
|
||
error_msg = f'第{row_num}行处理失败:{str(e)}'
|
||
myLogger.error(f"❌ {error_msg}")
|
||
errors.append({'row': row_num, 'error': str(e)})
|
||
|
||
myLogger.info(f"\n处理完成:成功{success}条,失败{len(errors)}条")
|
||
|
||
return Response({'success': success, 'errors': errors})
|
||
|
||
|
||
def convert_field_value(model_field, value, row_num):
|
||
"""
|
||
根据模型字段类型自动转换 Excel 值
|
||
"""
|
||
from django.db.models import DateField, BooleanField, IntegerField, FloatField, CharField
|
||
if value in [None, ""]:
|
||
return None
|
||
|
||
# ===== 日期字段 =====
|
||
if isinstance(model_field, CharField):
|
||
sval = str(value)
|
||
if isinstance(value, str):
|
||
if 'e' in value.lower():
|
||
try:
|
||
sval = str(int(float(value)))
|
||
except ValueError:
|
||
pass
|
||
if '.' in value:
|
||
try:
|
||
sval = str(int(float(value)))
|
||
except ValueError:
|
||
pass
|
||
# 超长字段给出明确提示,避免数据库报 value too long
|
||
if model_field.max_length and len(sval) > model_field.max_length:
|
||
raise ParseError(
|
||
f'第{row_num}行,字段 {model_field.verbose_name} 长度超过 {model_field.max_length} 个字符(当前 {len(sval)})')
|
||
return sval
|
||
|
||
if isinstance(model_field, DateField):
|
||
|
||
# 已经是 date
|
||
if isinstance(value, date):
|
||
return value
|
||
|
||
# datetime
|
||
if isinstance(value, datetime):
|
||
return value.date()
|
||
|
||
# Excel 数字日期
|
||
if isinstance(value, (int, float)):
|
||
try:
|
||
return from_excel(value).date()
|
||
except Exception:
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 日期格式错误')
|
||
|
||
# 字符串日期
|
||
if isinstance(value, str):
|
||
value = value.strip()
|
||
formats = [
|
||
"%Y-%m-%d",
|
||
"%Y/%m/%d",
|
||
"%Y.%m.%d",
|
||
"%Y%m%d",
|
||
"%Y年%m月%d日",
|
||
]
|
||
for fmt in formats:
|
||
try:
|
||
return datetime.strptime(value, fmt).date()
|
||
except ValueError:
|
||
continue
|
||
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 日期格式不正确')
|
||
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 日期类型错误')
|
||
|
||
# ===== 布尔字段 =====
|
||
if isinstance(model_field, BooleanField):
|
||
|
||
if value in [True, 1, '1']:
|
||
return True
|
||
if value in [False, 0, '0']:
|
||
return False
|
||
|
||
if isinstance(value, str):
|
||
value = value.strip()
|
||
if value in ['是', 'Y', 'y', 'yes', 'YES', 'True', 'true']:
|
||
return True
|
||
if value in ['否', 'N', 'n', 'no', 'NO', 'False', 'false']:
|
||
return False
|
||
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 只能填写 是 或 否')
|
||
|
||
# ===== 整数字段 =====
|
||
if isinstance(model_field, IntegerField):
|
||
try:
|
||
return int(value)
|
||
except:
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 必须为整数')
|
||
|
||
# ===== 浮点字段 =====
|
||
if isinstance(model_field, FloatField):
|
||
try:
|
||
return float(value)
|
||
except:
|
||
raise ParseError(f'第{row_num}行,字段 {model_field.name} 必须为数字')
|
||
return value
|
||
|
||
class AttendanceViewSet(CustomModelViewSet):
|
||
"""
|
||
list: 到岗记录
|
||
|
||
到岗记录
|
||
"""
|
||
queryset = Attendance.objects.all()
|
||
serializer_class = AttendanceSerializer
|
||
select_related_fields = ['user',
|
||
'user__belong_dept', 'user__post', 'shift', 'team']
|
||
filterset_fields = ['user',
|
||
'user__belong_dept', 'user__post', 'state', 'work_date', 'user__name', 'user__phone']
|
||
search_fields = ['user__name', 'user__phone']
|
||
ordering = ['-work_date', 'shift', '-create_time']
|
||
ordering_fields = ['work_date', 'shift', 'create_time']
|
||
|
||
|
||
class ClockRecordViewSet(BulkCreateModelMixin, ListModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||
"""
|
||
打卡记录
|
||
"""
|
||
perms_map = {'get': '*', 'post': 'clockrecord.create',
|
||
'delete': 'clockrecord.delete'}
|
||
queryset = ClockRecord.objects.all()
|
||
select_related_fields = ['employee']
|
||
search_fields = ['employee__name', 'employee__number', 'employee__phone']
|
||
serializer_class = ClockRecordListSerializer
|
||
create_serializer_class = ClockRecordCreateSerializer
|
||
filterset_class = ClockRecordFilterSet
|
||
ordering = ['-create_time']
|
||
|
||
@action(methods=['get'], detail=False, perms_map={'get': '*'},
|
||
serializer_class=serializers.Serializer)
|
||
def export_excel(self, request, pk=None):
|
||
"""导出excel
|
||
导出excel
|
||
"""
|
||
field_data = ['人员类型', '人员', '编号', '身份证号',
|
||
'所属部门', '触发形式', '打卡时间', '打卡推测', '异常推测']
|
||
queryset = self.filter_queryset(self.get_queryset())
|
||
odata = ClockRecordListSerializer(queryset, many=True).data
|
||
# 处理数据
|
||
data = []
|
||
for i in odata:
|
||
data.append(
|
||
[epTypeOptions[i['employee_']['type']],
|
||
i['employee_']['name'],
|
||
i['employee_']['number'],
|
||
i['employee_']['id_number'],
|
||
i['employee_'].get('belong_dept_name', ''),
|
||
i['detail'].get('deviceName', None),
|
||
i['create_time'],
|
||
crOptions[i['type']],
|
||
crEoptions[i['exception_type']] if i['exception_type'] else '']
|
||
)
|
||
return Response({'path': export_excel(field_data, data, '打卡记录')})
|
||
|
||
@action(methods=['post'], detail=False, perms_map={'post': '*'},
|
||
serializer_class=serializers.Serializer, logging_methods=[])
|
||
def dahua(self, request):
|
||
"""
|
||
大华刷脸分页带my_info
|
||
|
||
大华刷脸分页带my_info
|
||
"""
|
||
request.data.update({
|
||
"openType": "61",
|
||
})
|
||
_, res = dhClient.request(**dhapis['swipe_list'], json=request.data)
|
||
ids = []
|
||
if res.get('pageData', None):
|
||
for i in res['pageData']:
|
||
ids.append(i['id'])
|
||
crs_info = ClockRecordSimpleSerializer(
|
||
instance=ClockRecord.objects.filter(detail__id__in=ids), many=True).data
|
||
crs_dict = {}
|
||
for i in crs_info:
|
||
crs_dict[i['detail']['id']] = i
|
||
for i in res['pageData']:
|
||
i['my_info'] = {}
|
||
if i['id'] in crs_dict:
|
||
i['my_info'] = crs_dict[i['id']]
|
||
return Response(res)
|
||
|
||
# @action(methods=['post'], detail=False, perms_map={'post': '*'},
|
||
# serializer_class=CorrectSerializer)
|
||
# def correct_swip(self, request, pk=None):
|
||
# """
|
||
# 重跑一段时间的打卡记录
|
||
|
||
# 重跑一段时间的打卡记录
|
||
# """
|
||
# sr = CorrectSerializer(data=request.data)
|
||
# sr.is_valid(raise_exception=True)
|
||
# vdata = sr.validated_data
|
||
# from apps.hrm.tasks import correct_swip_task
|
||
# correct_swip_task.delay(vdata['start_time'], vdata['end_time'])
|
||
# return Response()
|
||
|
||
# @action(methods=['post'], detail=False, perms_map={'post': '*'},
|
||
# serializer_class=CorrectSerializer)
|
||
# def correct_enter_or_exit(self, request, pk=None):
|
||
# """
|
||
# 变更一段时间日志刷脸类型
|
||
|
||
# 变更一段时间日志刷脸类型
|
||
# """
|
||
# from apps.monitor.models import DrfRequestLog
|
||
# sr = CorrectSerializer(data=request.data)
|
||
# sr.is_valid(raise_exception=True)
|
||
# vdata = sr.validated_data
|
||
# for i in DrfRequestLog.objects.filter(path='/api/third/dahua/c_swip/', data__contains='办公楼考勤面板'
|
||
# , create_time__gte=vdata['start_time']
|
||
# , create_time__lte=vdata['end_time']).filter(data__contains = "'enterOrExit': 1" ):
|
||
# data = i.data
|
||
# i.data = data.replace("'enterOrExit': 1", "'enterOrExit': 3")
|
||
# i.save()
|
||
# return Response()
|
||
|
||
|
||
class NotWorkRemarkViewSet(ListModelMixin, CustomGenericViewSet):
|
||
"""
|
||
离岗说明
|
||
"""
|
||
perms_map = {'get': '*'}
|
||
queryset = NotWorkRemark.objects.all()
|
||
select_related_fields = ['user']
|
||
serializer_class = NotWorkRemarkListSerializer
|
||
filterset_class = NotWorkRemarkFilterSet
|
||
ordering = ['-pk']
|
||
|
||
|
||
class CertificateViewSet(CustomModelViewSet):
|
||
queryset = Certificate.objects.all()
|
||
create_serializer_class = CertificateCreateUpdateSerializer
|
||
update_serializer_class = CertificateCreateUpdateSerializer
|
||
serializer_class = CertificateSerializer
|
||
filterset_class = CertificateFilterSet
|
||
search_fields = ['name', 'number', 'employee__name']
|
||
|
||
def perform_create(self, serializer):
|
||
ins: Certificate = serializer.save()
|
||
ins.get_state(need_update=True)
|
||
|
||
def perform_update(self, serializer):
|
||
ins: Certificate = serializer.save()
|
||
ins.get_state(need_update=True)
|
||
|
||
|
||
class ResignationViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
离职记录
|
||
"""
|
||
select_related_fields = ['employee', 'employee__belong_dept', 'employee__post']
|
||
queryset = Resignation.objects.all()
|
||
serializer_class = ResignationSerializer
|
||
search_fields = ["employee__name"]
|
||
workflow_key = "wf_resignation"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"employee_name": instance.employee.name}
|
||
|
||
@staticmethod
|
||
def update_handle_date(ticket: Ticket, transition, new_ticket_data: dict):
|
||
handle_date = new_ticket_data.get("handle_date", None)
|
||
if handle_date:
|
||
resignation = Resignation.objects.get(ticket=ticket)
|
||
resignation.handle_date = handle_date
|
||
resignation.save()
|
||
else:
|
||
raise ParseError("请填写办理离职的交接日期")
|
||
|
||
@staticmethod
|
||
def make_job_off(ticket: Ticket, transition, new_ticket_data: dict):
|
||
resignation = Resignation.objects.get(ticket=ticket)
|
||
emp = resignation.employee
|
||
emp.job_state = Employee.JOB_OFF
|
||
emp.save(update_fields=['job_state'])
|
||
user = emp.user
|
||
user.is_deleted = True
|
||
user.save(update_fields=['is_deleted'])
|
||
|
||
|
||
class EmpNeedViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工需求
|
||
"""
|
||
queryset = EmpNeed.objects.all()
|
||
serializer_class = EmpNeedSerializer
|
||
filterset_fields = ['dept_need']
|
||
search_fields = ["dept_need__name", "post_need"]
|
||
workflow_key = "wf_empneed"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"post_need": instance.post_need}
|
||
|
||
|
||
class EmpJoinViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工入职
|
||
"""
|
||
queryset = EmpJoin.objects.all()
|
||
serializer_class = EmpJoinSerializer
|
||
workflow_key = "wf_empjoin"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"dept_name": instance.dept_need.name if instance.dept_need else None}
|
||
|
||
@staticmethod
|
||
def approve(ticket: Ticket, transition, new_ticket_data: dict):
|
||
person = new_ticket_data.get("person", None)
|
||
EmpJoin.objects.filter(ticket=ticket).update(person=person)
|
||
if not person:
|
||
raise ParseError("请选择人员")
|
||
serializer = EmpPersonInfoSerializer(data=person, many=True)
|
||
serializer.is_valid(raise_exception=True)
|
||
serializer.save()
|
||
|
||
|
||
class LeaveViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工请假
|
||
"""
|
||
select_related_fields = [
|
||
'employee',
|
||
'employee__belong_dept',
|
||
'ticket',
|
||
]
|
||
queryset = Leave.objects.all()
|
||
serializer_class = LeaveSerializer
|
||
filterset_fields = ['leave_type', 'employee__belong_dept']
|
||
search_fields = ["employee__name", "leave_type"]
|
||
workflow_key = "wf_leave"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"hour": instance.hour if instance.hour else None}
|
||
|
||
|
||
class EmpPersonInfoViewSet(CustomModelViewSet, EuModelViewSet):
|
||
"""
|
||
入职人员信息
|
||
"""
|
||
queryset = EmpPersonInfo.objects.all()
|
||
serializer_class = EmpPersonInfoSerializer
|
||
filterset_fields = ['post']
|
||
search_fields = ['name', 'post']
|
||
|
||
|
||
class TransferViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工岗位调动
|
||
"""
|
||
select_related_fields = [
|
||
'employee',
|
||
'employee__belong_dept',
|
||
'employee__post',
|
||
'ticket',
|
||
]
|
||
queryset = EmployeeTransfer.objects.all()
|
||
serializer_class = TransferSerializer
|
||
filterset_fields = ['employee__belong_dept']
|
||
search_fields = ["employee__name", "employee__post", "employee__belong_dept"]
|
||
workflow_key = "wf_transfer"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"name": instance.employee.name if instance.employee.name else None}
|
||
|
||
class ProbationViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工转正
|
||
"""
|
||
select_related_fields = [
|
||
'empperson',
|
||
'ticket',
|
||
'reg_dept',
|
||
'reg_post'
|
||
]
|
||
queryset = Probation.objects.all()
|
||
serializer_class = ProbationSerializer
|
||
filterset_fields = ['empperson__name', 'reg_dept']
|
||
search_fields = ["empperson__name", "reg_post__name", "reg_dept__name"]
|
||
workflow_key = "wf_probation"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"name": instance.empperson.name if instance.empperson.name else None}
|
||
|
||
|
||
class EmpContractViewSet(TicketMixin, EuModelViewSet):
|
||
"""
|
||
员工合同
|
||
"""
|
||
queryset = EmpContract.objects.all()
|
||
serializer_class = EmpContractSerializer
|
||
filterset_fields = ['employee', 'employee__belong_dept','employee__name', 'employee__post']
|
||
search_fields = ["employee__name", "employee__post", "employee__belong_dept"]
|
||
workflow_key = "wf_contract"
|
||
|
||
def gen_other_ticket_data(self, instance):
|
||
return {"name": instance.employee.name if instance.employee.name else None}
|
||
|
||
def perform_create(self, serializer):
|
||
result = serializer.save()
|
||
is_batch = isinstance(result, list)
|
||
instances = result if is_batch else [result]
|
||
handler = self.request.user
|
||
|
||
workflow_key = self.get_workflow_key(instances[0])
|
||
if not workflow_key:
|
||
raise ParseError('工作流异常:必须赋值workflow_key')
|
||
try:
|
||
wf = Workflow.objects.get(key=workflow_key)
|
||
except Exception as e:
|
||
raise ParseError(f'工作流{workflow_key}异常:{e}')
|
||
|
||
# 批量时创建一个工单,所有记录共享
|
||
if is_batch:
|
||
names = [ins.employee.name for ins in instances if ins.employee]
|
||
ticket_data = {
|
||
"t_model": "EmpContract",
|
||
"t_id": str(instances[0].id),
|
||
"batch": True,
|
||
"count": len(instances),
|
||
"name": f"批量合同变更({len(instances)}人)",
|
||
"employees": [
|
||
{"name": ins.employee.name, "id": str(ins.id)} for ins in instances if ins.employee
|
||
],
|
||
}
|
||
else:
|
||
ticket_data = self.gen_ticket_data(instances[0])
|
||
|
||
ticket = WfService.handle_ticket(ticket=None, transition=None, workflow=wf, new_ticket_data=ticket_data,
|
||
handler=handler, oinfo={})
|
||
for ins in instances:
|
||
ins.ticket = ticket
|
||
ins.save(update_fields=['ticket'])
|
||
|
||
if self.ticket_auto_submit_on_create:
|
||
source_state = WfService.get_workflow_start_state(wf)
|
||
transitions = WfService.get_state_transitions(source_state)
|
||
if transitions.count() == 1:
|
||
transition = transitions.first()
|
||
WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
|
||
handler=handler, oinfo={})
|
||
else:
|
||
raise ParseError(f'工作流{workflow_key}异常:有多个或无后续状态;不可处理')
|
||
|