diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py index 61fbfea87..a4bdefbb8 100644 --- a/yuthon_hr_attendance/models/hr_attendance.py +++ b/yuthon_hr_attendance/models/hr_attendance.py @@ -80,9 +80,40 @@ class HrAttendance(models.Model): def compute_attendance_ids(self): for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))): - now_time = fields.Datetime.to_datetime(att.current_data) + timedelta(hours=8, days=1) - start_time = now_time - timedelta(days=1) - self.env["hr.attendance.detail"].get_hr_attendance_time(att.employee_id, now_time, start_time) + current_data = fields.Date.today() + timedelta(hours=8) + self.env["hr.attendance.detail"].compute_attendance_detail(att.employee_id, current_data) + + @api.constrains('check_in', 'check_out', 'employee_id') + def _check_validity(self): + """重写验证方法,移除未签退记录的检查""" + for attendance in self: + last_attendance_before_check_in = self.env['hr.attendance'].search([ + ('employee_id', '=', attendance.employee_id.id), + ('check_in', '<=', attendance.check_in), + ('id', '!=', attendance.id), + ], order='check_in desc', limit=1) + + if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out > attendance.check_in: + raise exceptions.ValidationError(_( + "Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s", + empl_name=attendance.employee_id.name, + datetime=format_datetime(self.env, attendance.check_in, dt_format=False) + )) + if attendance.check_out: + last_attendance_before_check_out = self.env['hr.attendance'].search([ + ('employee_id', '=', attendance.employee_id.id), + ('check_in', '<', attendance.check_out), + ('id', '!=', attendance.id), + ], order='check_in desc', limit=1) + if last_attendance_before_check_out and last_attendance_before_check_in != last_attendance_before_check_out: + raise exceptions.ValidationError(_( + "Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s", + empl_name=attendance.employee_id.name, + datetime=format_datetime(self.env, last_attendance_before_check_out.check_in, dt_format=False) + )) + + + @api.depends('current_data') def _compute_month_status(self): @@ -111,27 +142,6 @@ class HrAttendance(models.Model): else: record.company_id = False - def write(self, vals): - if 'check_out' in vals: - for record in self: - employee_id = record.employee_id.id - current_date = record.current_data - local_start = datetime.combine(current_date, datetime.min.time()) - local_end = local_start + timedelta(days=1) - utc_start = local_start - timedelta(hours=8) - utc_end = local_end - timedelta(hours=8) - domain = [ - ("exception_type", "!=", "未打卡"), - ('employee_id', '=', employee_id), - ('checkin_time', '>=', fields.Datetime.to_string(utc_start)), - ('checkin_time', '<', fields.Datetime.to_string(utc_end)) - ] - detail_records = self.env['hr.attendance.detail'].search(domain) - if detail_records: - max_checkin_time = max(detail_records.mapped('checkin_time')) - vals['check_out'] = max_checkin_time - return super(HrAttendance, self).write(vals) - # @api.model # def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False, lazy=True): # result = super().web_read_group(domain, fields, groupby, limit, offset, orderby, lazy) @@ -141,6 +151,7 @@ class HrAttendance(models.Model): # if fields_dict[field].get('type') == 'boolean': # group[field] = len([r for r in self.search_read(group['__domain'], fields=[field]) if r.get(field)]) # return result + @api.constrains('check_in', 'check_out') def _check_validity_check_in_check_out(self): """ verifies if check_in is earlier than check_out. """ diff --git a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py index a5010f2ba..1aafc35d1 100644 --- a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py +++ b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py @@ -126,9 +126,184 @@ class HrAttendanceDetail(models.Model): start_time = now_time - timedelta(days=2) self.env["hr.attendance.detail"].get_hr_attendance_time(record.employee_id, now_time, start_time) + def compute_attendance_detail(self, employee, current_data): + """重新计算逻辑 + :param current_data: 打卡日期 + :param employee_id: 员工 + """ + # 是否在白名单 + resource_user_ids = employee.resource_calendar_id.resource_user_ids.ids + is_resource_user = True if employee.id in resource_user_ids else False + + if not is_resource_user: + template = self.env['planning.slot.template'].search([('department_id', '=', employee.department_id.id)], limit=1) + if not template: + template = self.env['planning.slot.template'].search([('role_id.name', '=', '集团行政')], limit=1) + next_day = current_data + timedelta(days=1) + start_datetime = datetime.combine(current_data, datetime.min.time()) - timedelta(hours=8) + # 根据day2判断是否需要包含次日记录 + if template.day2 == '次日': + end_datetime = datetime.combine(next_day, datetime.max.time()) - timedelta(hours=8) + else: + end_datetime = datetime.combine(current_data, datetime.max.time()) - timedelta(hours=8) + # 筛选当前员工当天的打卡记录 + domain = [ + ('employee_id', '=', employee.id), + ('checkin_time', '>=', start_datetime), + ('checkin_time', '<=', end_datetime) + ] + attendance_records = self.search(domain) + checkin_times = [rec.checkin_time + timedelta(hours=8) for rec in attendance_records if rec.checkin_time] + + check_in_start = self.float_to_datetime(current_data, template.check_in_start) + check_in_end = self.float_to_datetime(current_data, template.check_in_end) + + # 3. 确定有效上下班时间 + # 上班时间:在上班范围内的最小打卡时间 + valid_checkins = [ + t for t in checkin_times + if check_in_start <= t <= check_in_end + ] + check_in_time = min(valid_checkins) - timedelta(hours=8) if valid_checkins else None + check_out_start = self.float_to_datetime(current_data, template.check_out_start) + if template.day2 == '次日': + check_out_end = self.float_to_datetime(next_day, template.check_out_end) + else: + check_out_end = self.float_to_datetime(current_data, template.check_out_end) + # 筛选在下班时间范围内的打卡记录 + valid_checkouts = [t for t in checkin_times if check_out_start <= t <= check_out_end] + # 取最大的有效下班时间 + check_out_time = max(valid_checkouts) - timedelta(hours=8) if valid_checkouts else None + attendance = self.env['hr.attendance'].search( + [('employee_id', '=', employee.id), + ('current_data', '=', current_data), + ], + ) + # 获取正常上下班时间 + sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_data) + is_no_card, is_early_card, is_late_card, is_abnormal, is_warning = self.state_judgment(check_in_time, check_out_time, sch_check_in_time, sch_check_out_time, current_data) + if attendance: + attendance.write({ + 'check_in': check_in_time, + 'check_out': check_out_time, + 'is_no_card': is_no_card, + 'is_early_card': is_early_card, + 'is_late_card': is_late_card, + 'is_abnormal': is_abnormal, + 'is_warning': is_warning, + }) + else: + self.env['hr.attendance'].create({ + 'employee_id': employee.id, + 'current_data': current_data, + 'check_in': check_in_time, + 'check_out': check_out_time, + 'sch_check_in_time': sch_check_in_time, + 'sch_check_out_time': sch_check_out_time, + 'is_no_card': is_no_card, + 'is_early_card': is_early_card, + 'is_late_card': is_late_card, + 'is_abnormal': is_abnormal, + 'is_warning': is_warning, + }) + + def normal_clock_in_time(self, template, current_data): + """ + :param template: 班次模板 + :param current_data: 打卡日期 + :return: 正常开始上班时间和下班时间 + """ + check_in_start = template.start_time + check_out_end = template.end_time + date = current_data + start_hour = int(check_in_start) + start_minute = int((check_in_start - start_hour) * 60) + end_hour = int(check_out_end) + end_minute = int((check_out_end - end_hour) * 60) + start_datetime = datetime.combine(date, datetime.min.time()).replace( + hour=start_hour, minute=start_minute, second=0, microsecond=0 + ) + end_datetime = datetime.combine(date, datetime.min.time()).replace( + hour=end_hour, minute=end_minute, second=0, microsecond=0 + ) + return start_datetime, end_datetime + + def float_to_datetime(self, base_date, hour_float): + """ + 将小时浮点数(如9.5表示9:30)转换为具体datetime + :param base_date: 基准日期(date对象) + :param hour_float: 小时浮点数 + :return: datetime对象 + """ + hours = int(hour_float) + minutes = int((hour_float - hours) * 60) + return datetime.combine(base_date, datetime.min.time()) + timedelta(hours=hours, minutes=minutes) + + def state_judgment(self, check_in, check_out, start_datetime, end_datetime, current_data): + """ + 判断打卡状态 + :param check_in: 签到时间 + :param check_out: 签出时间 + :param start_datetime: 标准上班打卡时间 + :param end_datetime: 标准下班打卡时间 + :param current_data: 打卡日期 + :return: + """ + is_no_card = False + is_early_card = False + is_late_card = False + is_abnormal = False + is_warning = False + # 判断是否工作日 + if self._process_shift_days(current_data): + if not check_in or not check_out: + is_no_card = True + if check_in and start_datetime and check_in > start_datetime: + is_late_card = True + if check_out and end_datetime and check_out < end_datetime: + is_early_card = True + if is_no_card or is_early_card or is_late_card: + is_abnormal = True + is_warning = True + return is_no_card, is_early_card, is_late_card, is_abnormal, is_warning + + def _process_shift_days(self, current_data): + """ + 判断是否是工作日 调休日 双休 + """ + calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")]) + holidays_list = [] + shift_days_list = [] + # 提取节假日和调休日 + for leave in calendar_leaves: + if leave.holidays: + holidays_list.extend(leave.holidays.split(',')) + if leave.alternate_rest_day: + holidays_list.extend(leave.alternate_rest_day.split(',')) + if leave.shift_replacement_day: + shift_days_list.extend(leave.shift_replacement_day.split(',')) + + holidays_list = [self.remove_leading_zero(date) for date in holidays_list] + shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list] + + if (current_data in holidays_list) or (current_data.weekday() in [5, 6]): + return False + elif current_data in shift_days_list: + return True + else: + return True + + def remove_leading_zero(self, date_str): + month, day = date_str.split('月') + month = month.lstrip('0') + day = day.rstrip('日') + day = day.lstrip('0') + return f'{month}月{day}日' - # 传参按一个人打卡一次传参 + + + # 传参按一个人打卡一, shift_days_list次传参 # @api.model_create_multi # def create(self, vals_list): # res = super(HrAttendanceDetail, self).create(vals_list)