diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py index 0da7f5b45..43ffecd98 100644 --- a/yuthon_hr_attendance/__manifest__.py +++ b/yuthon_hr_attendance/__manifest__.py @@ -44,6 +44,7 @@ 'views/leave_approval.xml', 'views/overtime_page_views.xml', 'views/remove_holiday_approval.xml', + 'views/message_reminder_tasks.xml', 'views/yuthon_special_approval_views.xml', 'views/card_approval.xml', 'views/duty_approval.xml', diff --git a/yuthon_hr_attendance/data/hr_attendance_time.xml b/yuthon_hr_attendance/data/hr_attendance_time.xml index 170df5358..0ee88f794 100644 --- a/yuthon_hr_attendance/data/hr_attendance_time.xml +++ b/yuthon_hr_attendance/data/hr_attendance_time.xml @@ -14,5 +14,44 @@ -1 + + 定时任务:生成消息提醒任务 + + code + model.get_reminder_tasks() + + + 1 + days + + -1 + + + + 定时任务:执行消息提醒任务 + + code + model.reminder_card() + + + 1 + minutes + + -1 + + + + 定时任务:删除3天前消息提醒任务 + + code + model.cron_delete_3days_ago_records() + + + 1 + days + + -1 + + \ No newline at end of file diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py index 763239374..70115ca77 100644 --- a/yuthon_hr_attendance/models/__init__.py +++ b/yuthon_hr_attendance/models/__init__.py @@ -31,6 +31,7 @@ from . import expatriation_approval from . import meal_approval from . import back_approval from . import staff_dining_report +from . import message_reminder_tasks # from odoo.tools import SQL # import odoo diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py index 9d6d5f671..da589dcd0 100644 --- a/yuthon_hr_attendance/models/hr_attendance.py +++ b/yuthon_hr_attendance/models/hr_attendance.py @@ -63,6 +63,26 @@ class HrAttendance(models.Model): is_last_month = fields.Boolean(string="是否为上月数据", compute='_compute_month_status', store=True) correct_result = fields.Text(string="修正原因") + @api.constrains("check_in") + def _check_in_out(self): + reminder_id = self.env['message.reminder.tasks'].search([('name', '=', self.employee_id.name), ('state', '=', 'todo'), + ('type', '=', 'work'), + ('current_data', '=', self.current_data)]) + if reminder_id: + reminder_id.state = 'done' + + @api.constrains("check_out") + def _check_out_in(self): + if self.shift_name in ['夜班', '旧晚(旧区府晚班)']: + current_data = self.current_data + timedelta(days=1) + else: + current_data = self.current_data + reminder_id = self.env['message.reminder.tasks'].search([('name', '=', self.employee_id.name), ('state', '=', 'todo'), + ('type', '=', 'offWork'), + ('current_data', '=', current_data)]) + if reminder_id: + reminder_id.state = 'done' + def task_remind(self): all_employees = self.env['hr.employee'].search([('name', 'in', ['Administrator', '叶华锋'])]) for employee in all_employees: @@ -1001,6 +1021,21 @@ class HrAttendance(models.Model): content_item=content_item, ) + def reminder_card(self): + current_date = (fields.Date.today() + relativedelta(hours=8)).date() + now = fields.datetime.now() + timedelta(hours=8) + reminder_ids = self.env['message.reminder.tasks'].search( + [('state', '=', 'todo'), ('current_data', '=', current_date)]) + for task in reminder_ids: + hour1 = (task.reminder_time + timedelta(hours=8)).hour + minute1 = (task.reminder_time + timedelta(hours=8)).minute + if minute1 == now.minute and hour1 == now.hour: + employee_id = self.env['hr.employee'].search([('name', '=', task.name)]) + if task.type == 'work': + self.card_in_comment(employee_id, task.shift_name) + else: + self.card_out_comment(employee_id, task.shift_name) + def update_abnormal(self): attendance_ids = self.env["hr.attendance"].browse( self._context.get('active_ids', self._context.get('active_id'))) diff --git a/yuthon_hr_attendance/models/message_reminder_tasks.py b/yuthon_hr_attendance/models/message_reminder_tasks.py new file mode 100644 index 000000000..cc2cf25e3 --- /dev/null +++ b/yuthon_hr_attendance/models/message_reminder_tasks.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, tools, _ +from datetime import datetime, time, timedelta +from odoo.exceptions import UserError + + +class MessageReminderTasks(models.Model): + _name = 'message.reminder.tasks' + _description = '消息提醒任务' + + name = fields.Char(string="姓名") + department = fields.Char(string="部门") + company = fields.Char(string="公司") + shift_name = fields.Char(string="班次") + current_data = fields.Date(string='打卡日期') + state = fields.Selection([('todo', '待提醒'), ('done', '已提醒')], string='状态', default='todo') + type = fields.Selection([('work', '上班'), ('offWork', '下班')], string='类型') + reminder_time = fields.Datetime(string="提醒时间") + remake = fields.Char(string="备注") + holiday = fields.Boolean(string="假期") + + def get_reminder_tasks(self): + current_date = (fields.Date.today() + relativedelta(hours=8)).date() + target_company_names = ['天河物管', '天河粮储', '集团本部'] + target_companies = self.env['res.company'].search([('name', 'in', target_company_names)]) + target_calendars = self.env['resource.calendar'].search([ + ('company_id', 'in', target_companies.ids) + ]) + # 获取日历关联的白名单员工 + whitelist_employees = target_calendars.mapped('resource_user_ids').ids + condition1_employees = self.env['hr.employee'].search([ + ('id', 'not in', whitelist_employees), + ('name', '!=', 'Administrator'), + ('user_state', '=', 'in'), + ('active', '=', True) + ]) + target_role_names = ['保安部', '工程部', '联安粮库'] + target_roles = self.env['planning.role'].search([ + ('name', 'in', target_role_names) + ]) + default_reminder_types = ['work', 'offWork'] + default_reminder_times = [ + datetime.combine(current_date, datetime.strptime('00:55', '%H:%M').time()), # 时差 + datetime.combine(current_date, datetime.strptime('10:00', '%H:%M').time()) + ] + condition2_employee_ids = [] + if target_roles: + for role in target_roles: + resource_employees = role.resource_ids.mapped('employee_id') + condition2_employee_ids.extend(resource_employees) + condition2_employee_ids = list(set(condition2_employee_ids)) + # 哺乳人员 + breastfeeding_employees = target_calendars.mapped('breastfeeding_holiday_ids.employee_id') + breastfeeding_employee_ids = breastfeeding_employees.ids if breastfeeding_employees else [] + # 获取不在白名单且是行政班次的人员 + final_employees = condition1_employees.filtered(lambda emp: emp not in condition2_employee_ids) + task_vals_list = [] + is_workday = self._process_shift_days(current_date) + if is_workday: + for employee in final_employees: + emp_name = employee.name + emp_department = employee.department_id.name + emp_company = employee.company_id.name + is_lactating = employee.id in breastfeeding_employee_ids + if is_lactating: + # 哺乳人员 + reminder_times = self.get_reminder_lactating_personnel(employee, current_date) + reminder_types = ['work', 'offWork'] + shift_name = '行政班次(哺乳)' + else: + reminder_times = default_reminder_times + reminder_types = default_reminder_types + shift_name = '行政班次' + + for reminder_time, reminder_type in zip(reminder_times, reminder_types): + task_vals = { + 'name': emp_name, + 'current_data': reminder_time.date(), + 'department': emp_department, + 'company': emp_company, + 'shift_name': shift_name, + 'type': reminder_type, + 'reminder_time': fields.Datetime.to_string(reminder_time), + } + task_vals_list.append(task_vals) + + # 计划行政 + slot_start = datetime.combine(current_date, datetime.min.time()) - timedelta(hours=8) + slot_end = datetime.combine(current_date + timedelta(days=1), datetime.min.time()) - timedelta(hours=8) + # 查找员工对应的班次 + for employee in condition2_employee_ids: + reminder_plan_times = [] + reminder_plan_types = [] + planning_slot = self.env['planning.slot'].search([ + ('resource_id.name', '=', employee.name), + ('start_datetime', '>=', slot_start), + ('start_datetime', '<', slot_end), + ], limit=1) + template = self.env['planning.slot.template'].search([('planning_name', '=', planning_slot.planning_name)], limit=1) + if template: + sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_date) + + is_lactating = employee.id in breastfeeding_employee_ids + if is_lactating: + reminder_plan_times = self.get_reminder_lactating_personnel(employee.id, current_date) + reminder_plan_types = ['work', 'offWork'] + shift_name = f"{template.planning_name}(哺乳)" + else: + reminder_plan_times.append(sch_check_in_time - timedelta(minutes=5)) + reminder_plan_types.append('work') + reminder_plan_times.append(sch_check_out_time) + reminder_plan_types.append('offWork') + shift_name = template.planning_name + + for reminder_time, reminder_type in zip(reminder_plan_times, reminder_plan_types): + task_plan_vals = { + 'name': employee.name, + 'department': employee.department_id.name, + 'company': employee.company_id.name, + 'shift_name': shift_name, + 'current_data': (reminder_time + relativedelta(hours=8)).date(), + 'type': reminder_type, + 'reminder_time': fields.Datetime.to_string(reminder_time), + } + task_vals_list.append(task_plan_vals) + + self.env['message.reminder.tasks'].create(task_vals_list) + + + def get_reminder_lactating_personnel(self, employee, current_data): + # 查找当天的哺乳假记录(匹配员工+日期范围) + breastfeeding_record = self.env['breastfeeding.holiday'].search([ + ('employee_id', '=', employee.id), + ('calendar_start_time', '<=', current_data), + ('calendar_end_time', '>=', current_data), + ], limit=1) # 取当天有效的第一条记录 + def float_to_datetime(float_time, base_date): + """将float时间(如8.5=8:30)转换为datetime""" + if not float_time or not base_date: + return None + hours = int(float_time) + minutes = int((float_time - hours) * 60) + return fields.Datetime.to_datetime(f"{base_date} {hours:02d}:{minutes:02d}:00") + + sch_check_in_time = float_to_datetime(breastfeeding_record.check_in, current_data) - timedelta(hours=8) + sch_check_out_time = float_to_datetime(breastfeeding_record.check_out, current_data) - timedelta(hours=8) + return [ + sch_check_in_time - timedelta(minutes=5), + sch_check_out_time + ] + + def normal_clock_in_time(self, template, current_data): + """ + :param template: 班次模板 + :param current_data: 打卡日期 + :return: 正常开始上班时间和下班时间 + """ + if template.day2 == '次日': + check_out_end = template.end_time2 + else: + check_out_end = template.end_time + check_in_start = template.start_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 + ) - timedelta(hours=8) + if template.day2 == '次日': + end_datetime = datetime.combine(date, datetime.min.time()).replace( + hour=end_hour, minute=end_minute, second=0, microsecond=0 + ) - timedelta(hours=8) + timedelta(days=1) + else: + end_datetime = datetime.combine(date, datetime.min.time()).replace( + hour=end_hour, minute=end_minute, second=0, microsecond=0 + ) - timedelta(hours=8) + + if template.planning_name == '中夜': + end_datetime = datetime.combine(date, datetime.min.time()).replace( + hour=22, minute=0, second=0, microsecond=0 + ) - timedelta(hours=8) + + return start_datetime, end_datetime + + def cron_delete_3days_ago_records(self): + three_days_ago = fields.Datetime.now() - timedelta(days=3) + old_records = self.search([ + ('create_date', '<=', three_days_ago) + ]) + if old_records: + old_records.unlink() + + 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] + result = f"{current_data.month}月{current_data.day}日" + if result in shift_days_list: + return True + elif (result in holidays_list) or (current_data.weekday() in [5, 6]): + return False + 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}日' \ No newline at end of file diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv index 12b42ed7b..065825cfd 100644 --- a/yuthon_hr_attendance/security/ir.model.access.csv +++ b/yuthon_hr_attendance/security/ir.model.access.csv @@ -78,4 +78,5 @@ access_back_approval,back.approval,model_back_approval,base.group_user,1,1,1,1 access_staff_dining_report,staff.dining.report,model_staff_dining_report,base.group_user,1,1,1,1 access_vacation_confirm_wizard,vacation.confirm.wizard,model_vacation_confirm_wizard,base.group_user,1,1,1,1 -access_vacation_put_wizard,vacation.put.wizard,model_vacation_put_wizard,base.group_user,1,1,1,1 \ No newline at end of file +access_vacation_put_wizard,vacation.put.wizard,model_vacation_put_wizard,base.group_user,1,1,1,1 +access_message_reminder_tasks,message.reminder.tasks,model_message_reminder_tasks,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/yuthon_hr_attendance/views/message_reminder_tasks.xml b/yuthon_hr_attendance/views/message_reminder_tasks.xml new file mode 100644 index 000000000..7a1d31a57 --- /dev/null +++ b/yuthon_hr_attendance/views/message_reminder_tasks.xml @@ -0,0 +1,32 @@ + + + message.reminder.tasks.tree + message.reminder.tasks + + + + + + + + + + + + + + + + + 行政班次任务 + message.reminder.tasks + tree + [('shift_name','in', ['行政班次', '行政班次(哺乳)'])] + + + 计划班次任务 + message.reminder.tasks + tree + [('shift_name','not in', ['行政班次', '行政班次(哺乳)'])] + + \ No newline at end of file diff --git a/yuthon_work_brand/views/hr_menus.xml b/yuthon_work_brand/views/hr_menus.xml index 279d78039..a57d192c5 100644 --- a/yuthon_work_brand/views/hr_menus.xml +++ b/yuthon_work_brand/views/hr_menus.xml @@ -247,26 +247,36 @@ parent="attendance_resources_setting_root_board" sequence="2" action="yuthon_base.act_view_ir_cron_action"/> + + + sequence="13"/> + sequence="50"/>