diff --git a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py index 615e75e5d..3f728c7f6 100644 --- a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py +++ b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py @@ -96,133 +96,121 @@ class HrAttendanceDetail(models.Model): } @api.model + def _get_attendance_template(self, employee, current_data): + """根据员工与打卡日期获取对应的班次模板,找不到返回 None。 + + 逻辑与 compute_attendance_detail 中一致:先按排班(planning.slot)匹配, + 再按部门层级+角色兜底,最后回退到「集团行政」。 + """ + slot_start = datetime.combine(current_data, datetime.min.time()) - timedelta(hours=8) + slot_end = datetime.combine(current_data + timedelta(days=1), datetime.min.time()) - timedelta(hours=8) + planning_slots = self.env['planning.slot'].search([ + ('resource_id', '=', employee.resource_id.id), + ('start_datetime', '>=', slot_start), + ('start_datetime', '<', slot_end), + ], limit=1) + planning_name = planning_slots.planning_name if planning_slots else None + + def get_all_upper_depts(dept): + dept_ids = [dept.id] + if dept.parent_id: + dept_ids.extend(get_all_upper_depts(dept.parent_id)) + return dept_ids + + all_dept_ids = get_all_upper_depts(employee.department_id) + + template_domain = [] + if planning_name: + template_domain.append(('planning_name', '=', planning_name)) + template_domain.append(('department_id', 'in', all_dept_ids)) + template = self.env['planning.slot.template'].search(template_domain, limit=1) + # 模板里找不到这个人则置空,继续往下兜底 + if template and employee.id not in template.role_id.resource_ids.employee_id.ids: + template = None + if not template: + role_templates = [ + {'role_name': '厨卫(集团)', 'template': None}, + {'role_name': '项目运营岗', 'template': None} + ] + for item in role_templates: + item['template'] = self.env['planning.slot.template'].search( + [('role_id.name', '=', item['role_name'])]) + for item in role_templates: + if item['template']: + employee_ids = item['template'].role_id.resource_ids.employee_id.ids or [] + if employee.id in list(employee_ids): + template = item['template'] + break + if not template: + template = self.env['planning.slot.template'].search( + [('role_id.name', '=', '集团行政')], limit=1) + return template + + def _send_att_notify(self, employee_id, message, record, vals, test_users=None): + """发送企业微信考勤通知并写入 remark 日志。 + + :param test_users: 命中该名单的员工改用管理员测试号发送(保留原调试逻辑) + """ + test_users = test_users or [] + if employee_id.name in test_users: + self.sudo().env["wecom.apps"].sync_send_message( + category='att', user_id='18562027762', + message=message + f'管理员测试管理员测试{vals}', + ) + else: + self.sudo().env["wecom.apps"].sync_send_message( + category='att', user_id=employee_id.wecom_userid, + message=message, + ) + record.write({'remark': message}) + _logger.info(message) + def create(self, vals): record = super(HrAttendanceDetail, self).create(vals) # 批量导入时跳过企业微信通知 if self.env.context.get('skip_iot_notify'): return record if record.employee_id.name not in ['Administrator', '张丽芳']: - resource_user_ids = record.employee_id.resource_calendar_id.resource_user_ids.ids - is_resource_user = True if record.employee_id.id in resource_user_ids else False employee_id = record.employee_id - checkin_datetime = record.checkin_time + checkin_datetime = record.checkin_time # UTC 时间 + resource_user_ids = employee_id.resource_calendar_id.resource_user_ids.ids + is_resource_user = employee_id.id in resource_user_ids + # 上一条记录存在且时间差 < 60秒 → 不重复提醒 last_record = self.search([ ('employee_id', '=', employee_id.id), ('source', '=', 'iot'), ('checkin_time', '<', checkin_datetime) ], order='checkin_time desc', limit=1) - # 如果上一条记录存在,并且时间差 < 60秒 → 不发提醒 - skip_notify = False - if last_record: - time_diff = checkin_datetime - last_record.checkin_time - if time_diff.total_seconds() < 60: - skip_notify = True # 小于1分钟,跳过提醒 - if record.employee_id and record.checkin_time and record.source == 'iot' and not is_resource_user and not skip_notify: - current_data = record.checkin_time.date() + timedelta(hours=8) - day_start = checkin_datetime.replace(hour=0, minute=0, second=0, microsecond=0) - day_end = checkin_datetime.replace(hour=23, minute=59, second=59, microsecond=99) - daily_iot_records = self.search([ - ('employee_id', '=', employee_id.id), - ('source', '=', 'iot'), - ('checkin_time', '>=', day_start), - ('checkin_time', '<=', day_end) - ], order='checkin_time asc') - if daily_iot_records: - message = f"打卡成功!今日上班时间:{record.checkin_time + timedelta(hours=8)}: {employee_id.name},祝您工作愉快!" - self.sudo().env["wecom.apps"].sync_send_message( - category='att', user_id=employee_id.wecom_userid, - message=message, - ) - # if record.employee_id.name == '蒋彬': - # self.sudo().env["wecom.apps"].sync_send_message( - # category='att', user_id='18562027762', - # message=message + f'管理员测试管理员测试{vals}', - # ) - self.write({'remark': message}) - _logger.info(message) + skip_notify = bool(last_record) and (checkin_datetime - last_record.checkin_time).total_seconds() < 60 + if employee_id and checkin_datetime and record.source == 'iot' and not is_resource_user and not skip_notify: + # 本地时间表示(UTC+8),用于匹配班次模板与标准上下班时间 + current_data = checkin_datetime.date() + timedelta(hours=8) + template = self._get_attendance_template(employee_id, current_data) + if not template: + # 找不到班次模板,默认按上班打卡提醒 + message = f"打卡成功!今日上班时间:{checkin_datetime + timedelta(hours=8)}: {employee_id.name},祝您工作愉快!" + self._send_att_notify(employee_id, message, record, vals) else: - resource_user_ids = employee_id.resource_calendar_id.resource_user_ids.ids - is_resource_user = True if employee_id.id in resource_user_ids else False - # 哺乳人员 + sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_data) + # 哺乳人员:下午班次下班时间提前 1 小时 suckling_employees = employee_id.resource_calendar_id.breastfeeding_holiday_ids.mapped('employee_id') - def get_all_upper_depts(dept): - """递归获取部门及其所有上级部门的ID列表""" - dept_ids = [dept.id] - if dept.parent_id: - dept_ids.extend(get_all_upper_depts(dept.parent_id)) - return dept_ids - - all_dept_ids = get_all_upper_depts(employee_id.department_id) - - if not is_resource_user: - slot_start = datetime.combine(current_data, datetime.min.time()) - timedelta(hours=8) - slot_end = datetime.combine(current_data + timedelta(days=1), datetime.min.time()) - timedelta( - hours=8) - # 查找员工对应的班次 - planning_slots = self.env['planning.slot'].search([ - ('resource_id', '=', employee_id.resource_id.id), - ('start_datetime', '>=', slot_start), - ('start_datetime', '<', slot_end), - ], limit=1) - planning_name = planning_slots.planning_name if planning_slots else None - template_domain = [] - if planning_name: - template_domain.append(('planning_name', '=', planning_name)) - template_domain.append(('department_id', 'in', all_dept_ids)) - template = self.env['planning.slot.template'].search(template_domain, limit=1) - # 如果模板里面找不到这个人 则还是往下判断(模板为空) - if employee_id.id not in template.role_id.resource_ids.employee_id.ids: - template = None - if not template: - role_templates = [ - {'role_name': '厨卫(集团)', 'template': None}, - {'role_name': '项目运营岗', 'template': None} - ] - - for item in role_templates: - item['template'] = self.env['planning.slot.template'].search( - [('role_id.name', '=', item['role_name'])], ) - for item in role_templates: - if item['template']: - employee_ids = item['template'].role_id.resource_ids.employee_id.ids or [] - employee_list = list(employee_ids) - if employee_id.id in employee_list: - template = item['template'] - break - if not template: - template = self.env['planning.slot.template'].search( - [('role_id.name', '=', '集团行政')], - limit=1 - ) - sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_data) - # 判断是否是哺乳人员 - if employee_id in suckling_employees: - # 判断是否是哺乳人员并调整标准打卡时间 - current_date = fields.Date.to_date(current_data) if current_data else None - if current_date: - # 查找当天的哺乳假记录(匹配员工+日期范围) - breastfeeding_record = self.env['breastfeeding.holiday'].search([ - ('employee_id', '=', employee_id.id), - ('calendar_start_time', '<=', current_date), - ('calendar_end_time', '>=', current_date), - ], limit=1) - if breastfeeding_record.time_slot_selection == 'afternoon': - sch_check_out_time = sch_check_out_time - timedelta(hours=1) - - if checkin_datetime > sch_check_out_time: - message = f"打卡成功!今日下班时间:{record.checkin_time + timedelta(hours=8)}: {employee_id.name},祝您归途平安!" - if record.employee_id.name not in ['林伟东']: - self.sudo().env["wecom.apps"].sync_send_message( - category='att', user_id=employee_id.wecom_userid, - message=message, - ) - else: - self.sudo().env["wecom.apps"].sync_send_message( - category='att', user_id='18562027762', - message=message + f'管理员测试管理员测试{vals}', - ) - self.write({'remark': message}) - _logger.info(message) + if employee_id in suckling_employees: + current_date = fields.Date.to_date(current_data) + if current_date: + breastfeeding_record = self.env['breastfeeding.holiday'].search([ + ('employee_id', '=', employee_id.id), + ('calendar_start_time', '<=', current_date), + ('calendar_end_time', '>=', current_date), + ], limit=1) + if breastfeeding_record.time_slot_selection == 'afternoon': + sch_check_out_time = sch_check_out_time - timedelta(hours=1) + # 以标准下班时间为界:晚于下班时间算「下班」,否则算「上班」 + if checkin_datetime > sch_check_out_time: + message = f"打卡成功!今日下班时间:{checkin_datetime + timedelta(hours=8)}: {employee_id.name},祝您归途平安!" + self._send_att_notify(employee_id, message, record, vals, test_users=['林伟东']) + else: + message = f"打卡成功!今日上班时间:{checkin_datetime + timedelta(hours=8)}: {employee_id.name},祝您工作愉快!" + self._send_att_notify(employee_id, message, record, vals) if record.employee_id and record.checkin_time: self._sync_to_hr_attendance(record)