Files
gzth/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py
T
2025-04-19 21:38:53 +08:00

1115 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
import os
import base64
import pytz
import calendar
from datetime import datetime, timedelta, time
import logging
import traceback
from odoo import api, fields, models, tools, _
from dateutil.relativedelta import relativedelta
_logger = logging.getLogger(__name__)
class HrAttendanceDetailImage(models.Model):
_name = "hr.attendance.detail.image"
_description = "打卡原始抓取数据图片"
attendance_detail_id = fields.Many2one('hr.attendance.detail', string="原始打卡记录", ondelete="cascade",
readonly=True)
media_url = fields.Char(string='附件地址')
media_file = fields.Binary(string='附件文件', compute="_compute_media_file")
@api.depends('media_url')
def _compute_media_file(self):
for record in self:
record.media_file = None
wx_work_auth = self.env['auth.oauth.provider'].search([('name', '=', 'Wechat Card')], limit=1)
if record.media_url:
image_path = os.path.join(wx_work_auth.wx_work_image_path, record.media_url)
record.media_file = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
class HrAttendanceDetail(models.Model):
_name = "hr.attendance.detail"
_description = "打卡原始抓取数据"
_order = 'checkin_time desc'
name = fields.Char(related='employee_id.name')
employee_id = fields.Many2one('hr.employee', string="员工", readonly=True)
department_id = fields.Many2one('hr.department', string="部门", related="employee_id.department_id", readonly=True)
userid = fields.Char(string='userid')
groupname = fields.Char(string='打卡规则名称')
checkin_type = fields.Char(string='打卡类型')
exception_type = fields.Char(string='异常类型')
checkin_time = fields.Datetime(string='打卡时间')
location_title = fields.Char(string='打卡地点')
location_detail = fields.Char(string='打卡地点详情')
wifiname = fields.Char(string='打卡wifi名称')
notes = fields.Char(string='打卡备注')
wifimac = fields.Char(string='打卡的MAC地址')
mediaids = fields.Char(string='打卡的附件media_id')
lat = fields.Integer(string='位置打卡地点纬度')
lng = fields.Integer(string='位置打卡地点经度')
deviceid = fields.Char(string='打卡设备id')
sch_checkin_time = fields.Datetime(string='标准打卡时间')
groupid = fields.Integer(string='规则id')
schedule_id = fields.Char(string='班次id')
timeline_id = fields.Integer(string='时段id')
attendance_detail_image_ids = fields.One2many(string="打卡记录图片", comodel_name='hr.attendance.detail.image',
inverse_name='attendance_detail_id')
hr_attendance_id = fields.Many2one('hr.attendance', string="出勤记录", ondelete="cascade", readonly=True)
def action_show_details(self):
self.ensure_one()
view = self.env.ref("yuthon_wechat.view_hr_attendance_detail_image_tree")
return {
"name": _("图片详情"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "hr.attendance.detail.image",
"views": [(view.id, "tree")],
"view_id": view.id,
"target": "new",
"res_id": self.id,
"domain": [('attendance_detail_id', '=', self.id)],
"context": {'default_attendance_detail_id': self.id}
}
def _get_wx_attendance_user_list(self):
"""
获取企业微信的关联用户
:return: 用户列表
"""
user_obj = self.env['res.users'].search([('name', 'not in', ['Administrator', '李鹏宇'])])
user_list = []
for user in user_obj:
if user.oauth_uid and user.employee_id.contract_id:
user_list.append(user.oauth_uid)
return user_list
@staticmethod
def _get_wx_attendance_data(userid_limit, now_time, start_time):
"""
获取企业微信的打卡信息
:param wx_work_checkin_api: 企业微信api
:param userid_limit: 用户列表
:return:
"""
# 先按照天抓取,时间戳
# now_time = datetime.now() + timedelta(hours=8)
# start_time = now_time - timedelta(days=30)
start_time_stamp = int(start_time.timestamp())
end_time_stamp = int(now_time.timestamp())
# 整理请求信息
data = {
"opencheckindatatype": 3,
"starttime": start_time_stamp,
"endtime": end_time_stamp,
"need_photo": 1,
"useridlist": userid_limit
}
# 重写一个类处理result获取想要的结果
return data
@staticmethod
def _check_wx_attendance_data(response, userid_limit):
"""
检查请求返回的信息是否正确
:param response: 请求返回信息
:param userid_limit: 用户列表
:return: 检查结果
"""
# 对返回信息进行整理
if response.status_code != 200:
_logger.error('抓取出勤数据请求失败,异常:{0},用户列表{1}'.format(str(response.json()), str(userid_limit)))
return []
result = response.json()
if result.get('errcode') != 0:
_logger.error(
'获取外出打卡数据失败,异常:{0},用户列表{1}'.format(str(result.get('errmsg')), str(userid_limit)))
return []
checkin_data = result.get('checkindata', [])
if not checkin_data:
_logger.error('没有找到外出打卡记录,异常:{0},用户列表{1}'.format(str(response.json()), str(userid_limit)))
return []
return checkin_data
def _check_wx_attendance_time(self, data, userid_limit):
"""
检查保存之前的数据是否准确,并且分析出想要的打卡时间
:param data: 原始数据
:param userid_limit: 用户列表
:return: 分析后的打卡数据
"""
userid = data.get("userid", "")
checkin_time_stamp = data.get("checkin_time", "")
sch_checkin_time_stamp = data.get("sch_checkin_time", None)
if not userid or not checkin_time_stamp:
_logger.error('没有抓取到用户出勤信息,异常:{0},用户列表{1}'.format(str(data), str(userid_limit)))
return False
checkin_time = datetime.fromtimestamp(checkin_time_stamp) if checkin_time_stamp else None
sch_checkin_time = datetime.fromtimestamp(sch_checkin_time_stamp) if sch_checkin_time_stamp else None
return checkin_time, sch_checkin_time
def _check_wx_attendance_media(self, data):
"""
分析返回来的数据获取图片原图
:param data: 原始数据
:param userid_limit: 用户列表
:return: 获取后的图片
"""
media_list = []
userid = data.get("userid", "")
mediaids_list = data.get("mediaids", [])
if not mediaids_list:
return media_list
for media_id in mediaids_list:
data = {"media_id": media_id}
try:
result, response = self.sudo().env["wecom.apps"].sync_media_get(data)
if result:
filename = response.headers['Content-Disposition'].split('; ')[2].replace('filename=', '').replace('"', '')
wx_work_auth = self.env['auth.oauth.provider'].search([('name', '=', 'Wechat Card')], limit=1)
wx_work_image = os.path.join(wx_work_auth.wx_work_image_path, str(filename))
with open(wx_work_image, 'wb') as f:f.write(response.content)
media_list.append((0, 0, {'media_url': filename,}))
else:
_logger.error('检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1}'.format(str(response), str(data)))
except Exception as e:
_logger.error('请求原始图片失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data),str(traceback.format_exc())))
return media_list
def _save_wx_attendance_detail_data(self, data, checkin_time, sch_checkin_time):
"""
保存相关的企业微信的原始打卡信息
:param data: 请求返回后的处理信息
:param checkin_time: 打卡时间
:param sch_checkin_time: 应打卡时间
:param media_list: 图片列表
:return: 保存结果
"""
userid = data.get("userid", "")
# 查询是否存在数据
if userid and checkin_time and self.env['hr.attendance.detail'].sudo().search_count(
[("userid", "=", userid), ("checkin_time", "=", checkin_time)]) <= 0:
res_user_obj = self.env['res.users'].sudo().search([("oauth_uid", "=", userid)], limit=1)
hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", res_user_obj.id)], limit=1)
if not res_user_obj or not hr_employee_obj:
_logger.error('检查保存之前的数据异常,异常:无关联的员工数据,数据信息{0}'.format(str(data)))
return False
attendance_detail_obj = self.env['hr.attendance.detail'].sudo().create({
'employee_id': hr_employee_obj.id,
"userid": userid,
"groupname": data.get("groupname", ""),
"checkin_type": data.get("checkin_type", ""),
"exception_type": data.get("exception_type", ""),
"checkin_time": checkin_time,
"location_title": data.get("location_title", ""),
"location_detail": data.get("location_detail", ""),
"wifiname": data.get("wifiname", ""),
"notes": data.get("notes", ""),
"wifimac": data.get("wifimac", ""),
"mediaids": str(data.get("mediaids", "")),
"lat": data.get("lat", 0),
"lng": data.get("lng", 0),
"deviceid": data.get("deviceid", ""),
"sch_checkin_time": sch_checkin_time,
"groupid": data.get("groupid", 0),
"schedule_id": data.get("schedule_id", ""),
"timeline_id": data.get("timeline_id", 0),
# "attendance_detail_image_ids": media_list
})
# 分析返回来的数据获取图片原图
try:
media_list = self._check_wx_attendance_media(data)
attendance_detail_obj.attendance_detail_image_ids = media_list
except Exception as e:
_logger.error('检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data),
str(traceback.format_exc())))
def get_hr_attendance_time(self):
"""
获取企业微信打卡数据的核心逻辑
:return: 处理结果
"""
# 从用户列表里获取企业微信的用户
user_list = self._get_wx_attendance_user_list()
now_time = datetime.now() + timedelta(hours=8)
start_time = now_time - timedelta(days=30)
# 企业微信获取打卡数据,要求用户分组100,分批获取打卡记录
for userid_limit in self.utils_list_page(user_list, 100):
# 获取企业微信的打卡信息
try:
attendance_data = self._get_wx_attendance_data(userid_limit, now_time, start_time)
attendance_result, response = self.sudo().env["wecom.apps"].sync_checkin_data(attendance_data)
except Exception as e:
_logger.error('获取企业微信的打卡信息失败,异常:{0},用户列表{1},异常详情:{2}'.format(str(e), str(userid_limit), str(traceback.format_exc())))
continue
# 检查请求返回的信息是否正确
try:
checkin_data = self._check_wx_attendance_data(response, userid_limit)
except Exception as e:
_logger.error('检查请求返回的信息失败,异常:{0},用户列表{1},异常详情:{2}'.format(str(e), str(userid_limit), str(traceback.format_exc())))
continue
# 将原始数据存储
for data in checkin_data:
# 检查保存之前的数据是否准确,并且分析出想要的打卡时间
try:
checkin_time, sch_checkin_time = self._check_wx_attendance_time(data, userid_limit)
except Exception as e:
_logger.error(
'检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e),
str(data),
str(traceback.format_exc())))
continue
# 保存相关的企业微信的原始打卡信息
try:
self._save_wx_attendance_detail_data(data, checkin_time, sch_checkin_time)
except Exception as e:
_logger.error(
'保存相关的企业微信的原始打卡信息失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data), str(traceback.format_exc())))
# 将取回来的数据进行组织整理写入到考勤表中
employee_ids = self.env['hr.employee'].search([('use_user_type', '=', 'labor')])
employee_list = [emp.name for emp in employee_ids]
user_obj = self.env['res.users'].search([('name', 'not in', ['Administrator', '李鹏宇'] + employee_list)])
# 暂时先用for循环,如果后期加大频率,需改成并发
total_users = len(user_obj)
remaining_users = total_users
for user in user_obj:
# 判断是否是微信用户
if user.oauth_uid and user.employee_ids.contract_id:
_logger.info("正在梳理考勤人员:{} id:{},全部人数:{},剩余人数:{},所属公司:{}".format(user.name, user.id, total_users,remaining_users, user.employee_ids.company_id.name))
remaining_users -= 1
# 查询当前用户区间内的所有打卡数据没有的创建
for current_date in self.divide_time_range_fixed(start_time, now_time):
# 普通用户写入打卡数据
self._do_save_wx_attendance_data(user, current_date)
# 计划用户的打开时间
self._do_plan_save_wx_attendance_data(user, current_date)
return True
def _do_save_wx_attendance_data(self, user, current_date):
"""
开始 整理考勤数据
:param user:
:param current_date:
:return:
"""
# 同步整理当天出勤数据
attendance_obj = self.env['hr.attendance'].search([("userid", "=", user.oauth_uid), ("current_data", "=", current_date)], limit=1)
hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", user.id)], limit=1)
current_datetime_start = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.min.time())
current_datetime_end = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.max.time())
if not attendance_obj:
# 判断是否是 普通打卡
if not hr_employee_obj.default_planning_role_id:
attendance_obj = self.env['hr.attendance'].sudo().create({
'employee_id': hr_employee_obj.id,
"userid": user.oauth_uid,
"current_data": current_date,
"attendance_type": "wx_work"
})
if not hr_employee_obj.default_planning_role_id:
if attendance_obj.is_approval:
# 已经审批修复无需再次进行更新数据
return True
self._do_not_planning_role(user, current_date, attendance_obj)
# # 查询出当前抓取微信的当前时间的所有数据
# attendance_detail_obj = self.env['hr.attendance.detail'].sudo().search(
# [("userid", "=", user.oauth_uid),
# ("checkin_time", ">=", current_datetime_start - timedelta(hours=8)),
# ("checkin_time", "<=", current_datetime_end - timedelta(hours=8))])
# # 关联 打卡详情
# attendance_detail_obj.hr_attendance_id = attendance_obj.id
return True
def _do_plan_save_wx_attendance_data(self, user, current_date):
"""
开始 整理 计划排版的 考勤数据
:param user:
:param current_date:
:return:
"""
# 同步整理当天出勤数据
hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", user.id)], limit=1)
current_datetime_start = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.min.time())
current_datetime_end = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.max.time())
# 判断是否是 排版打卡
if hr_employee_obj.default_planning_role_id:
planning_slot_ids = self.env['planning.slot'].search(
[('resource_id', '=', hr_employee_obj.resource_id.id),
('start_datetime', '>=', current_datetime_start - timedelta(hours=8)),
('start_datetime', '<=', current_datetime_end - timedelta(hours=8))])
for planning_slot_id in planning_slot_ids:
# 查询其中的一个排版信息是否生成了打卡记录
attendance_obj = self.env['hr.attendance'].search(
[("userid", "=", user.oauth_uid), ("current_data", "=", current_date), ("shift_id", "=", planning_slot_id.id)
], limit=1)
if not attendance_obj:
self.env['hr.attendance'].sudo().create({
'employee_id': hr_employee_obj.id,
"userid": user.oauth_uid,
"current_data": current_date,
"shift_id": planning_slot_id.id,
"attendance_type": "wx_work"
})
if attendance_obj.is_approval:
# 已经审批修复无需再次进行更新数据
return True
self._do_planning_role(user, current_date, attendance_obj, planning_slot_id)
return True
def _do_not_planning_role(self, user, current_date, attendance_obj):
"""
正常班次的 上班下班判断
:param user: 用户集合
:param current_date: 循环判断的时间
:param attendance_obj: 判断的时刻的考勤集合
:return:
"""
current_datetime_start = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.min.time())
current_datetime_end = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.max.time())
hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", user.id)], limit=1)
# 白名单
resource_user_ids = hr_employee_obj.resource_calendar_id.resource_user_ids.ids
is_resource_user = True if hr_employee_obj.id in resource_user_ids else False
# 判断是否是星期
tz = hr_employee_obj.resource_calendar_id.tz
current_year = current_date.split('-')[0]
current_month = current_date.split('-')[1]
current_day = current_date.split('-')[2]
current_week = calendar.weekday(int(current_year), int(current_month), int(current_day))
work_time_result = self.get_employee_work_time(hr_employee_obj, current_week, current_date, tz)
# # 说明今天休息
# if not work_time_result:
# # 如果有人打开那么进行记录
#
# attendance_obj.sudo().write({
# "is_late_card": False,
# "is_early_card": False,
# "is_no_card": False,
# "is_abnormal": False,
# })
# return True
# 今天正常上班
# go_from, go_to, off_from, off_to, current_date = work_time_result
# 获取考勤日历的上班下班时间
min_hour_from = self.env['resource.calendar'].search([])[0].attendance_ids[0].hour_from
min_hour = int(min_hour_from)
min_minute = int(min_hour_from * 10 % min_hour_from)
max_hour_to = self.env['resource.calendar'].search([])[0].attendance_ids[-1].hour_to
max_hour = int(max_hour_to)
max_minute = int(max_hour_to * 10 % max_hour_to)
will_datetime_start = current_datetime_start.replace(hour=min_hour, minute=min_minute) - timedelta(hours=8)
will_datetime_end = current_datetime_end.replace(hour=max_hour, minute=max_minute) - timedelta(hours=8)
# 判断打卡情况排除未打卡情况
# 6:00 - 17:00 // 10.00 - // 5.00。
# 改为区间打卡
check_in_start = datetime.strptime(str(current_date) + " " + "06:00", "%Y-%m-%d %H:%M") - timedelta(hours=8)
check_in_end = datetime.strptime(str(current_date) + " " + "17:00", "%Y-%m-%d %H:%M") - timedelta(hours=8)
check_out_start = datetime.strptime(str(current_date) + " " + "10:00", "%Y-%m-%d %H:%M") - timedelta(hours=8)
check_out_end = datetime.strptime(str(current_date) + " " + "05:00", "%Y-%m-%d %H:%M") + relativedelta(days=1) - timedelta(hours=8)
# 说明今天休息
if not work_time_result:
# 如果有人打卡那么进行记录 相当于白名单
# 上班
attendance_detail_obj_in = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_in_start),
("checkin_time", "<=", check_in_end)])
if len(attendance_detail_obj_in) > 0:
# 关联 打卡详情
attendance_detail_obj_in.hr_attendance_id = attendance_obj.id
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_in]
if checkin_time_list:
# 最早 时间
min_time = max(checkin_time_list)
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_early_card": False,
"is_no_card": False,
"is_abnormal": False,
})
# 下班
attendance_detail_obj_out = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_out_start),
("checkin_time", "<=", check_out_end),
])
if len(attendance_detail_obj_out) > 0:
attendance_detail_obj_out.hr_attendance_id = attendance_obj.id
checkout_time_list = [obj.checkin_time for obj in attendance_detail_obj_out]
if checkout_time_list:
# 最早 时间
max_time = max(checkout_time_list)
# # 没有上班卡
# if not attendance_obj.check_in:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": False,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# elif attendance_obj.check_in and attendance_obj.check_in < max_time:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_late_card": False,
"is_early_card": False,
"is_no_card": False,
"is_abnormal": False,
})
attendance_obj.sudo().write({
"is_late_card": False,
"is_early_card": False,
"is_no_card": False,
"is_abnormal": False,
})
return True
# 上班
attendance_detail_obj_in = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_in_start),
("checkin_time", "<=", check_in_end)])
# 关联 打卡详情
attendance_detail_obj_in.hr_attendance_id = attendance_obj.id
# 白名单用户直接正常
if is_resource_user:
if len(attendance_detail_obj_in) > 0:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_in]
# 最早 时间
min_time = min(checkin_time_list)
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
else:
# 没有打卡记录说明缺卡
if len(attendance_detail_obj_in) <= 0:
attendance_obj.sudo().write({"is_no_card": True, "is_abnormal": False, "absenteeism_card": False})
else:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_in]
if not checkin_time_list:
attendance_obj.sudo().write({"is_no_card": True, "is_abnormal": False, "absenteeism_card": False})
else:
# 最早 时间
min_time = min(checkin_time_list)
# 查询最早时间上班时间 查询最晚下班时间
# 判断时间 在10分钟内说明是 异常
if will_datetime_start + timedelta(minutes=10) > min_time > will_datetime_start + timedelta(minutes=1):
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": True,
"absenteeism_card": False,
})
elif will_datetime_start + timedelta(hours=1) > min_time > will_datetime_start + timedelta(minutes=1):
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": True,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
elif min_time > will_datetime_start + timedelta(hours=1) and min_time > will_datetime_start + timedelta(minutes=1):
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": True,
})
else:
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
# 下班
attendance_detail_obj_out = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_out_start),
("checkin_time", "<=", check_out_end),
])
# 关联 打卡详情
attendance_detail_obj_out.hr_attendance_id = attendance_obj.id
# 白名单用户直接正常
if is_resource_user:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_out]
if checkin_time_list:
# 最早 时间
max_time = max(checkin_time_list)
# # 如果上班没有打卡
# if not attendance_obj.check_in:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": False,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# # 如果上班时间早于下班时间说明
# elif attendance_obj.check_in and attendance_obj.check_in < max_time:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
else:
if len(attendance_detail_obj_out) <= 0:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
# 打了一次卡记录说明缺卡
else:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_out]
if not checkin_time_list:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
else:
# 最早 时间
max_time = max(checkin_time_list)
# # 如果上班没有打卡
# if not attendance_obj.check_in:
# if will_datetime_start + timedelta(minutes=10) > max_time > will_datetime_start + timedelta(minutes=1):
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": True,
# "absenteeism_card": False,
# })
# elif will_datetime_start + timedelta(hours=1) > max_time > will_datetime_start + timedelta(minutes=1):
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": True,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# elif max_time > will_datetime_start + timedelta(hours=1) and max_time > will_datetime_start + timedelta(minutes=1):
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": True,
# })
# else:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# 如果上班时间早于下班时间说明下班的卡没打
if attendance_obj.check_in:
if attendance_obj.check_in >= max_time:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
else:
if will_datetime_end - timedelta(minutes=10) < max_time < will_datetime_end - timedelta(minutes=1):
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
elif will_datetime_end - timedelta(hours=1) < max_time < will_datetime_end - timedelta(minutes=1):
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": True,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
elif max_time < will_datetime_end - timedelta(hours=1):
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True,
})
else:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
return True
def _do_planning_role(self, user, current_date, attendance_obj, planning_id):
"""
带有计划的班次判断
:param user: 用户集合
:param current_date: 循环判断的时间
:param attendance_obj: 判断的时刻的考勤集合
:param planning_id:
:return:
"""
# 获取模板信息
template_id = planning_id.template_id
# check_in_start = fields.Datetime(string="上班开始")
check_in_start = template_id.check_in_start
# check_in_end = fields.Datetime(string="上班结束")
check_in_end = template_id.check_in_end
# check_out_start = fields.Datetime(string="下班开始")
check_out_start = template_id.check_out_start
# check_out_end = fields.Datetime(string="下班结束")
check_out_end = template_id.check_out_end
# 这里需要将 浮点型时间转为 时间型
if check_in_start:
check_in_start = self.str_to_time(current_date, check_in_start)
if check_in_end:
check_in_end = self.str_to_time(current_date, check_in_end)
if check_out_start:
check_out_start = self.str_to_time(current_date, check_out_start)
if check_out_end:
check_out_end = self.str_to_time(current_date, check_out_end)
current_datetime_start = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.min.time())
current_datetime_end = datetime.combine(datetime.strptime(current_date, '%Y-%m-%d'), datetime.max.time())
# 如果没有设定区间那么默认当天最早时间和最晚时间 到 应该上下班时间
if not check_in_start:
check_in_start = current_datetime_start
if not check_in_end:
check_in_end = planning_id.start_datetime + timedelta(hours=8)
if not check_out_start:
check_out_start = planning_id.end_datetime + timedelta(hours=8)
if not check_out_end:
check_out_end = current_datetime_end
# 如果结束时间小于开始时间说明 是第二天的时间
if check_in_end < check_in_start:
check_in_end = check_in_end + relativedelta(days=1)
if check_out_end < check_out_start:
check_out_end = check_out_end + relativedelta(days=1)
# 转换时区
check_in_start = check_in_start - timedelta(hours=8)
check_in_end = check_in_end - timedelta(hours=8)
check_out_start = check_out_start - timedelta(hours=8)
check_out_end = check_out_end - timedelta(hours=8)
# 判断打卡情况排除未打卡情况
# 上班
attendance_detail_obj_in = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_in_start),
("checkin_time", "<=", check_in_end),
])
# 关联 打卡详情
attendance_detail_obj_in.hr_attendance_id = attendance_obj.id
will_datetime_start = planning_id.start_datetime
will_datetime_end = planning_id.end_datetime
if len(attendance_detail_obj_in) <= 0:
attendance_obj.sudo().write({"is_no_card": True, "is_abnormal": False, "absenteeism_card": False})
# 打了一次卡记录说明缺卡
else:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_in]
if not checkin_time_list:
attendance_obj.sudo().write({"is_no_card": True, "is_abnormal": False, "absenteeism_card": False})
else:
# 最早 时间
min_time = min(checkin_time_list)
# attendance_obj.sudo().write({
# "sch_check_in_time": planning_id.start_datetime,
# "check_in": min_time,
# "is_late_card": min_time > planning_id.start_datetime,
# "is_no_card": False,
# "is_abnormal": False,
# })
if will_datetime_start + timedelta(minutes=10) > min_time > will_datetime_start:
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": True,
"absenteeism_card": False,
})
elif will_datetime_start + timedelta(hours=1) > min_time > will_datetime_start:
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": True,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
elif min_time > will_datetime_start + timedelta(hours=1) and min_time > will_datetime_start:
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": True,
})
else:
attendance_obj.sudo().write({
"sch_check_in_time": will_datetime_start,
"check_in": min_time,
"is_late_card": False,
"is_no_card": False,
"is_abnormal": False,
"absenteeism_card": False,
})
# 下班
attendance_detail_obj_out = self.env['hr.attendance.detail'].sudo().search(
[("exception_type", "!=", "未打卡"),
("userid", "=", user.oauth_uid),
("checkin_time", ">=", check_out_start),
("checkin_time", "<=", check_out_end),
])
# 关联 打卡详情
attendance_detail_obj_out.hr_attendance_id = attendance_obj.id
if len(attendance_detail_obj_out) <= 0:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
# 打了一次卡记录说明缺卡
else:
# 多次打卡时间的集合
checkin_time_list = [obj.checkin_time for obj in attendance_detail_obj_out]
if not checkin_time_list:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
else:
# 最早 时间
max_time = max(checkin_time_list)
# 如果上班没有打卡
# if not attendance_obj.check_in:
# # attendance_obj.sudo().write({
# # "sch_check_in_time": planning_id.start_datetime,
# # "check_in": max_time,
# # "is_late_card": max_time > planning_id.start_datetime,
# # "is_no_card": True,
# # "is_abnormal": True,
# # })
#
# if will_datetime_start + timedelta(minutes=10) > max_time > will_datetime_start:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": True,
# "absenteeism_card": False,
# })
# elif will_datetime_start + timedelta(hours=1) > max_time > will_datetime_start:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": True,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# elif max_time > will_datetime_start + timedelta(hours=1) and max_time > will_datetime_start:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": True,
# })
# else:
# attendance_obj.sudo().write({
# "sch_check_in_time": will_datetime_start,
# "check_in": max_time,
# "is_late_card": False,
# "is_no_card": True,
# "is_abnormal": False,
# "absenteeism_card": False,
# })
# 如果上班时间早于下班时间说明下班的卡没打
if attendance_obj.check_in:
if attendance_obj.check_in >= max_time:
attendance_obj.sudo().write({
"is_no_card": True,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
else:
# attendance_obj.sudo().write({
# "sch_check_out_time": planning_id.end_datetime,
# "check_out": max_time,
# "is_early_card": max_time < planning_id.end_datetime,
# "is_no_card": False,
# "is_abnormal": False,
# })
if will_datetime_end - timedelta(minutes=10) < max_time < will_datetime_end:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
elif will_datetime_end - timedelta(hours=1) < max_time < will_datetime_end:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": True,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
elif max_time < will_datetime_end - timedelta(hours=1):
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True,
})
else:
attendance_obj.sudo().write({
"sch_check_out_time": will_datetime_end,
"check_out": max_time,
"is_early_card": False,
"is_no_card": True if attendance_obj.check_in and attendance_obj.is_no_card else False,
"is_abnormal": True if attendance_obj.check_in and attendance_obj.is_abnormal else False,
"absenteeism_card": True if attendance_obj.check_in and attendance_obj.absenteeism_card else False,
})
return True
@staticmethod
def divide_time_range_fixed(start_time, end_time):
time_ranges = [start_time.strftime("%Y-%m-%d")]
while start_time + relativedelta(days=1) <= end_time:
current_date = start_time + relativedelta(days=1)
time_ranges.append(current_date.strftime("%Y-%m-%d"))
start_time = current_date
return time_ranges
@staticmethod
def str_to_time(current_data, data):
"""
%d:%d 的 str类型的时间转换成时间
:param current_data: 当前日期
:param data: str类型时间
:return: 转换后的时间
"""
if data >= 24:
data = 23.99
from_time = "%d:%d" % (int(data), round((data - int(data)) * 60))
from_times = str(current_data) + " " + from_time
from_ti = datetime.strptime(from_times, "%Y-%m-%d %H:%M")
return from_ti
@staticmethod
def utils_list_page(data, limit):
"""
列表平分
:param data: list
:param limit: 每组多少个
:return: 平分结果
"""
for start in range(0, len(data), limit):
if start + limit > len(data):
end = len(data)
else:
end = start + limit
yield data[start:end]
def get_employee_work_time(self, employee, current_week, current_date, tz):
"""
获取员工的当天是否应该出勤以及出勤时间
:param employee: 员工
:param current_week: 当天周
:param current_date: 当前天
:param tz: 时区
:return: 出勤时间
"""
# 获取员工的工作信息
resource_calendar = employee.resource_calendar_id
# 获取工作表
attendance_ids = resource_calendar.attendance_ids
# 获取公休息日表
global_leave = resource_calendar.global_leave_ids
# 有没有工作规则
if not attendance_ids:
return False
# 判断是否是调休,如果是直接返回当天上班
if self.get_leave_rest_day(global_leave, current_date):
return True
# 是不是工作时间
attendance_data = resource_calendar.attendance_ids.filtered(
lambda s: s.dayofweek == str(current_week) and s.day_period in ["morning", "afternoon"])
if not attendance_data:
return False
# 获取工作时间
work_time = self.get_work_time(attendance_data, current_date, tz)
if not work_time:
return False
go_from, go_to, off_from, off_to, current_date = work_time
# 早上和下午上班 判断当天 是不是公休日
if self.get_leave_day(global_leave, go_from, tz) and self.get_leave_day(global_leave, off_from, tz):
return False
return go_from, go_to, off_from, off_to, current_date
def get_work_time(self, attendance_data, current_date, tz):
"""
当前时间的工作时间段
:param attendance_data:
:param current_date:
:param tz:
:return:
"""
work_time_list = []
for work_time in attendance_data:
work_time_list.append(self.time_to_utc_time(self.str_to_time(current_date, work_time.hour_from), tz))
work_time_list.append(self.time_to_utc_time(self.str_to_time(current_date, work_time.hour_to), tz))
work_time_sorted = sorted(work_time_list)
go_from, go_to, off_from, off_to = work_time_sorted
return go_from, go_to, off_from, off_to, current_date
@staticmethod
def time_to_utc_time(data, tz):
"""
本地时间转换成tz时间
:param data: 时间
:param tz: 时区
:return: 转换时间
"""
time_timezone = pytz.timezone(tz).localize(data, is_dst=False)
time_to_utc = time_timezone.astimezone(pytz.timezone("utc"))
utc_time_str = time_to_utc.strftime("%Y-%m-%d %H:%M:%S")
utc_time = datetime.strptime(utc_time_str, "%Y-%m-%d %H:%M:%S")
return utc_time
def get_leave_rest_day(self, global_leave, current_time):
"""
判断是否是公休日的调休日
:param global_leave:
:param current_time:
:param tz:
:return:
"""
if not global_leave:
return False
for leave in global_leave:
if leave.alternate_rest_day:
for rest_day in str(leave.alternate_rest_day).split(","):
# rest_day_month = str(rest_day).split("月")[0]
rest_day_month = datetime.strptime(rest_day, "%m月%d日").strftime("%m")
# rest_day_day = str(str(rest_day).split("月")[1].replace("月", "")).split("日")[0]
rest_day_day = datetime.strptime(rest_day, "%m月%d日").strftime("%d")
current_time_month = current_time.split('-')[1]
current_time_day = current_time.split('-')[2]
if int(rest_day_month) == int(current_time_month) and int(rest_day_day) == int(current_time_day):
return True
return False
def get_leave_day(self, global_leave, current_time, tz):
"""
判断是否是公休日
:param global_leave:
:param current_time:
:param tz:
:return:
"""
if not global_leave:
return False
for leave in global_leave:
if self.utc_time_to_tz(tz, leave.date_from) <= current_time.strftime("%Y-%m-%d %H:%M:%S") <= self.utc_time_to_tz(tz, leave.date_to):
return True
return False
@staticmethod
def utc_time_to_tz(tz, chin):
"""
utc时间的时区转换tz时间
:param tz: 时区
:param chin: 时间
:return: 转换时间
"""
time_timezone = pytz.timezone('utc').localize(chin, is_dst=False)
time_to_utc = time_timezone.astimezone(pytz.timezone(tz))
utc_time_str = time_to_utc.strftime("%Y-%m-%d %H:%M:%S")
return utc_time_str