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"/>