Files
gzth/yuthon_hr_attendance/models/hr_attendance.py
T
2026-02-28 11:55:48 +08:00

1348 lines
63 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 -*-
from datetime import datetime, time
from re import search
from odoo import models, fields, api, exceptions, _
from datetime import timedelta, date
from dateutil.relativedelta import relativedelta
from odoo.tools import format_datetime, format_duration
import logging
from odoo.exceptions import UserError, ValidationError
import calendar
_logger = logging.getLogger(__name__)
class HrAttendance(models.Model):
_inherit = "hr.attendance"
_description = "Attendance"
_rec_name = 'current_data'
is_clock_card = fields.Boolean(string="外勤")
is_no_card = fields.Boolean(string="缺卡", group_operator='count_true')
is_early_card = fields.Boolean(string="早退", group_operator='count_true')
is_late_card = fields.Boolean(string="迟到", group_operator='count_true')
is_abnormal = fields.Boolean(string="异常", group_operator='count_true')
is_expatriation = fields.Boolean(string="外派", group_operator='count_true')
is_approval_no = fields.Boolean()
is_approval_early = fields.Boolean()
is_approval_late = fields.Boolean()
is_approval_abnormal = fields.Boolean()
user_id = fields.Many2one(related='employee_id.user_id', store=True)
absenteeism_card = fields.Boolean(string="旷工", group_operator='count_true')
is_approval_abs = fields.Boolean()
work_time_type = fields.Selection([('close_day', '休息日'), ('work_day', '工作日')], string="工作时间类型")
company_id = fields.Many2one(related='employee_id.company_id', string='公司', index=True, store=True)
on_card_location = fields.Char(string="上班打卡地点")
down_card_location = fields.Char(string="下班打卡地点")
out_card_time = fields.Datetime(string="外出打卡时间")
out_card_location = fields.Char(string="外出打卡地点")
department_id = fields.Many2one(related='employee_id.department_id', string="部门", store=True)
work_hours = fields.Float(string="工作时长")
current_data = fields.Date(string='打卡日期', tracking=True)
week = fields.Selection([('one', '一'), ('two', '二'), ('three', '三'),
('four', '四'), ('five', '五'), ('six', '六'),
('seven', '日')], string='星期', compute='_compute_week', store=True)
abnormal = fields.Integer(string="异常计算")
shift_id = fields.Many2one('planning.slot', string="班次", store=True)
shift_name = fields.Char(string="班次", store=True)
is_week = fields.Boolean(string="是否为周末")
is_holiday = fields.Boolean(string="是否为节假日")
noon_duty = fields.Datetime(string="中值")
night_duty = fields.Datetime(string="晚值")
is_approval = fields.Boolean(string="是否手动修正", default=False)
is_check_in_red = fields.Boolean(string="是否上班打卡异常")
is_check_out_red = fields.Boolean(string="是否下班打卡异常")
is_warning = fields.Boolean(compute='_compute_is_warning', string="警告", store=True)
approval_check_in = fields.Datetime(string="修正前上班打卡时间")
approval_check_out = fields.Datetime(string="修正前下班打卡时间")
is_workflow = fields.Boolean(string="是否是流程", default=False)
workflow_id = fields.Many2one('soong.workflow.record', string="流程")
is_month = fields.Boolean(string="是否为本月数据", compute='_compute_month_status', store=True)
is_last_month = fields.Boolean(string="是否为上月数据", compute='_compute_month_status', store=True)
correct_result = fields.Text(string="修正原因")
def update_workflow_id(self):
attendance_ids = self.env['hr.attendance'].search([('workflow_id', '=', True)])
for attendance in attendance_ids:
attendance.is_no_card = False
attendance.is_early_card = False
attendance.is_late_card = False
attendance.is_abnormal = False
attendance.absenteeism_card = False
@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)], limit=1)
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)], limit=1)
if reminder_id:
reminder_id.state = 'done'
def one_time(self):
for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))):
if att.current_data:
att.current_data_month = f'{att.current_data.year}-{att.current_data.month}'
def task_remind(self):
all_employees = self.env['hr.employee'].search([('name', 'in', ['Administrator', '叶华锋'])])
for employee in all_employees:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='att', user_id=employee.wecom_userid,
description="记得打卡", title="测试提醒",btntxt="详细信息", )
def create_attendance_data_holiday(self):
"""
定时任务create_attendance_data_holiday
:return:
"""
today = fields.Date.today() + timedelta(hours=8)
today_str = fields.Date.to_string(today)
existing_employees = self.env['hr.employee'].search([
('name', 'not in', ['Administrator'])
])
existing_records = self.search([
('employee_id', 'in', existing_employees.ids),
('current_data', '=', today_str)
])
existing_employee_ids = set()
for record in existing_records:
existing_employee_ids.add(record.employee_id.id)
for employee in existing_employees:
if employee.id not in existing_employee_ids:
att = self.create({
'employee_id': employee.id,
'current_data': today_str,
'check_in': False,
'check_out': False,
'is_no_card': False,
'is_abnormal': False
})
att.compute_attend()
def state_fix(self):
for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))):
att.write({'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'is_warning': False,
'absenteeism_card': False})
@api.depends('is_no_card', 'is_early_card', 'is_late_card', 'is_abnormal', 'absenteeism_card')
def _compute_is_warning(self):
for record in self:
record.is_warning = (
record.is_no_card or
record.is_early_card or
record.is_late_card or
record.is_abnormal or
record.absenteeism_card
)
def button_workflow(self):
if self.workflow_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_id.res_model}',
'res_id': self.workflow_id.res_id,
'view_mode': 'form',
}
def compute_attendance_ids(self):
for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))):
if att.employee_id.active == False:
att.write({
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'is_no_card': False,
'absenteeism_card': False,
'is_warning': False,
})
else:
att.compute_attend()
def compute_attend(self):
current_data = fields.Datetime.to_datetime(self.current_data) + timedelta(hours=8)
self.env["hr.attendance.detail"].compute_attendance_detail(self.employee_id, current_data.date(), is_manual=True)
@api.depends('current_data')
def _compute_month_status(self):
today = fields.Date.today()
# 获取本月第一天和最后一天
first_day_this_month = today.replace(day=1)
last_day_this_month = today.replace(day=calendar.monthrange(today.year, today.month)[1])
# 获取上月第一天和最后一天
first_day_last_month = first_day_this_month - relativedelta(months=1)
last_day_last_month = first_day_this_month - relativedelta(days=1)
for record in self:
if not record.current_data:
record.is_month = False
record.is_last_month = False
continue
# 判断是否为本月
record.is_month = (first_day_this_month <= record.current_data <= last_day_this_month)
# 判断是否为上月
record.is_last_month = (first_day_last_month <= record.current_data <= last_day_last_month)
def update_att(self):
valid_employees = self.sudo().env['hr.employee'].search([
('name', '=', '廖志强'),
])
# 2. 定义日期范围(2025-11-01 至 2025-11-11)
start_date = fields.Date.from_string("2025-12-31")
end_date = fields.Date.from_string("2025-01-01")
current_date = start_date
while current_date <= end_date:
for employee in valid_employees:
existing_attendance = self.env["hr.attendance"].search([
('employee_id', '=', employee.id),
('current_data', '=', current_date)
], limit=1)
if not existing_attendance:
self.env["hr.attendance.detail"].compute_attendance_detail(
employee,
current_date,
is_manual=True
)
current_date += timedelta(days=1)
@api.constrains('check_in', 'check_out')
def _check_validity_check_in_check_out(self):
""" verifies if check_in is earlier than check_out. """
for attendance in self:
if attendance.check_in and attendance.check_out:
if attendance.check_out < attendance.check_in:
print('------------------------------修改源码后期优化')
# raise exceptions.ValidationError(_('"Check Out" time cannot be earlier than "Check In" time.'))
@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)
# ))
def attendance_abnormal_remind(self):
current_utc_time = fields.Datetime.now()
target_time = current_utc_time + timedelta(hours=8)
target_hour = target_time.hour
target_minute = target_time.minute
# 判断是否在9:55 - 10:05之间
is_in_time_range = False
if target_hour == 9 and target_minute >= 55:
is_in_time_range = True
elif target_hour == 10 and target_minute <= 5:
is_in_time_range = True
if not is_in_time_range:
return
today = date.today()
current_year = today.year
current_month = today.month
if current_month == 1:
last_month = 12
last_month_year = current_year - 1
else:
last_month = current_month - 1
last_month_year = current_year
last_month_first_day = date(last_month_year, last_month, 1)
last_month_last_day = date(current_year, current_month, 1) - timedelta(days=1)
yesterday = today - timedelta(days=1) # 昨天
attendance_records = self.env['hr.attendance'].search([
('current_data', '>=', last_month_first_day),
('current_data', '<=', yesterday),
('shift_name', '!=', '休息'),
])
employee_abnormal = {}
for rec in attendance_records:
emp = rec.employee_id
emp_id = emp.id
resource_user_ids = emp.resource_calendar_id.resource_user_ids.ids if emp.resource_calendar_id else []
if emp_id in resource_user_ids:
continue
if emp_id not in employee_abnormal:
employee_abnormal[emp_id] = {
'employee': emp,
'last_month_abnormal': [], # 上月异常列表
'current_month_abnormal': [] # 本月异常列表
}
if rec.is_abnormal:
att_date = rec.current_data
date_str = att_date.strftime('%m-%d')
shift_name = rec.shift_name
single_abnormal = f"{date_str} {shift_name}"
if att_date <= last_month_last_day:
employee_abnormal[emp_id]['last_month_abnormal'].append(single_abnormal)
else:
employee_abnormal[emp_id]['current_month_abnormal'].append(single_abnormal)
# 4. 统一处理每个员工的提醒(合并上月+本月异常)
for emp_id, data in employee_abnormal.items():
emp = data['employee']
last_month_abnormal = data['last_month_abnormal']
current_month_abnormal = data['current_month_abnormal']
vacation_report = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', emp_id),
('vacation_date', '=', last_month_first_day),
], limit=1)
# 无需要提醒的异常则跳过:上月异常已确认 且 本月无异常
if (not last_month_abnormal or (vacation_report and vacation_report.vacation_state == 'confirmed')) and not current_month_abnormal:
continue
has_last_month_abnormal = bool(
last_month_abnormal and (not vacation_report or vacation_report.vacation_state != 'confirmed'))
has_current_month_abnormal = bool(current_month_abnormal)
if has_last_month_abnormal and has_current_month_abnormal:
title = '【上月+本月考勤异常提醒】'
elif has_last_month_abnormal:
title = '【上月考勤异常提醒】'
elif has_current_month_abnormal:
title = '【本月考勤异常提醒】'
else:
continue
url = f'https://phone.thtzjt.com/phone/AttendanceSummary/{fields.date.today().year}/{fields.date.today().month}/{emp.user_id.id}'
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att',
user_id=emp.wecom_userid,
title=f'【{title}】',
description='考勤异常请尽快处理',
exception_alerts = True,
url=url, btntxt="查看详情")
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att',
user_id='18562027762',
title='【考勤异常提醒】',
exception_alerts=True,
description='考勤异常请尽快处理',
url=url, btntxt="查看详情")
def missing_card_judgment(self):
today = fields.Date.today()
employee_list = []
role_ids = self.env['planning.role'].search([
('name', 'not in', ['厨卫(集团)', '项目运营岗'])
])
for role in role_ids:
for resource_id in role.resource_ids:
if resource_id.employee_id.id not in employee_list:
employee_list.append(resource_id.employee_id.id)
attend_records = self.env['hr.attendance'].search([
('current_data', '=', today),
('employee_id', 'not in', employee_list),
('is_expatriation', '=', False),
('is_clock_card', '=', False),
('is_workflow', '=', False),
('is_approval', '=', False)
])
all_employees = self.env['hr.employee'].search([
('id', 'not in', employee_list)
])
# 判断是否为工作日
is_workday = self._process_shift_days(today)
for employee in all_employees:
# 白名单 和 外派 不做处理
resource_user_ids = employee.resource_calendar_id.resource_user_ids.ids
if employee.id not in resource_user_ids:
# 查找该员工当天的考勤记录
employee_record = attend_records.filtered(
lambda r: r.employee_id.id == employee.id
)
# 工作日打卡情况判断
if is_workday:
if employee_record:
is_no_card = (not employee_record.check_in or not employee_record.check_out)
else:
is_no_card = True
else:
is_no_card = False
if employee_record:
# 更新现有记录的缺卡状态
employee_record.write({'is_no_card': is_no_card})
if is_no_card == True:
employee_record.write({'absenteeism_card': True,
'is_abnormal': True
})
def _process_shift_days(self, current_data):
"""
判断是否是工作日 调休日 双休
"""
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)
])
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}日'
@api.depends('employee_id', 'check_in', 'check_out')
def _compute_display_name(self):
for attendance in self:
is_early_card = '早退' if attendance.is_early_card else ''
is_late_card = '迟到' if attendance.is_late_card else ''
if not attendance.check_out:
attendance.display_name = _(
str(attendance.current_data) + " 上午:%s" + is_late_card + " -下午缺卡",
format_datetime(self.env, attendance.check_in, dt_format="HH:mrm"),
)
elif not attendance.check_in:
attendance.display_name = _(
str(attendance.current_data) + " 上午:缺卡-下午%s" + is_early_card,
format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
)
else:
attendance.display_name = _(
str(attendance.current_data) + " 上午:%s" + is_late_card + "-下午:%s" + is_early_card,
format_datetime(self.env, attendance.check_in, dt_format="HH:mm"),
format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
)
def add_comment(self):
self.ensure_one()
menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的异常')])
for rec in self:
current_date = datetime.today()
month_start = current_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
month_end = (current_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + relativedelta(months=1)) - timedelta(seconds=1)
domain = [
('employee_id.user_id', '=', rec.employee_id.user_id.id),
('check_in', '>=', month_start),
('check_in', '<=', month_end),
('week', 'not in', ['seven', 'six'])
]
is_no_card = 0
is_early_card = 0
is_late_card = 0
for i in self.search(domain):
if i.is_no_card:
is_no_card += 1
if i.is_no_card:
is_early_card += 1
if i.is_no_card:
is_late_card += 1
url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=hr.attendance&view_type=from&menu_id={menu_id.id}'
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att', user_id=rec.employee_id.wecom_userid,
title="打卡异常提醒", description="本月迟到: {}\n本月缺卡: {}\n本月早退: {}\n".format(is_late_card, is_no_card, is_early_card),
url=url, btntxt="详细信息")
def update_state(self):
attendance_ids = self.env["hr.attendance"].browse(
self._context.get('active_ids', self._context.get('active_id'))
)
for rec in attendance_ids:
rec.write({'is_no_card': False,
'is_early_card': False,
'shift_name': False,
'is_holiday': False,
'is_week': False,
'is_late_card': False,
'is_abnormal': False,
'is_warning': False,
'absenteeism_card': False
})
def button_controls_wizard(self):
return {
"type": "ir.actions.act_window",
"name": "操作弹窗",
"res_model": "attendance.controls.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_attendance_id": self.id
}
}
def revoke(self):
self.check_in = self.approval_check_in
self.correct_result = None
self.check_out = self.approval_check_out
self.is_no_card = self.is_approval_no
self.is_early_card = self.is_approval_early
self.is_late_card = self.is_approval_late
self.is_abnormal = self.is_approval_abnormal
self.absenteeism_card = self.is_approval_abs
self.is_approval = False
if self.is_check_in_red == True:
self.is_check_in_red = False
if self.is_check_out_red == True:
self.is_check_out_red = False
def button_correct_wizard(self):
self.approval_check_in = self.check_in
self.approval_check_out = self.check_out
self.is_approval_no = self.is_no_card
self.is_approval_early = self.is_early_card
self.is_approval_late = self.is_late_card
self.is_approval_abnormal = self.is_abnormal
self.is_approval_abs = self.absenteeism_card
return {
"type": "ir.actions.act_window",
"name": "修正弹窗",
"res_model": "attendance.correct.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_attendance_ids": [(6, 0, [self.id])]
}
}
def all_button_correct_wizard(self):
self.approval_check_in = self.check_in
self.approval_check_out = self.check_out
self.is_approval_no = self.is_no_card
self.is_approval_early = self.is_early_card
self.is_approval_late = self.is_late_card
self.is_approval_abnormal = self.is_abnormal
self.is_approval_abs = self.absenteeism_card
return {
"type": "ir.actions.act_window",
"name": "修正弹窗",
"res_model": "attendance.correct.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_attendance_ids": [(6, 0, [self.id])]
}
}
def button_compensatory_leave(self):
attendance_ids = self.env["hr.attendance"].browse(
self._context.get('active_ids', self._context.get('active_id'))
)
for record in attendance_ids:
record.approval_check_in = record.check_in
record.approval_check_out = record.check_out
record.is_approval_no = record.is_no_card
record.is_approval_early = record.is_early_card
record.is_approval_late = record.is_late_card
record.is_approval_abnormal = record.is_abnormal
record.is_approval_abs = record.absenteeism_card
return {
"type": "ir.actions.act_window",
"name": "修正弹窗",
"res_model": "attendance.correct.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_attendance_ids": [(6, 0, attendance_ids.ids)]
}
}
def ensure_september_records(self):
"""
确保选择期间每位员工至少有一条记录
若没有则创建一条默认记录
"""
start_date = fields.Date.from_string('2026-01-01')
end_date = fields.Date.from_string('2026-01-31')
existing_employees = self.env['hr.employee'].search([('name', 'not in', ['Administrator'])])
if not existing_employees:
return
# 计算日期范围内的所有日期
current_date = start_date
date_list = []
while current_date <= end_date:
date_list.append(current_date)
current_date += timedelta(days=1)
existing_records = self.search([
('employee_id', 'in', existing_employees.ids),
('current_data', '>=', fields.Date.to_string(start_date)),
('current_data', '<=', fields.Date.to_string(end_date + timedelta(days=1)))
])
existing_map = {}
for record in existing_records:
if not record.current_data:
continue
key = (record.employee_id.id, fields.Date.to_string(record.current_data))
existing_map[key] = True
# 检查每个员工在每个日期是否有记录
for employee in existing_employees:
for date in date_list:
date_str = fields.Date.to_string(date)
key = (employee.id, date_str)
# 如果没有记录,则添加到创建列表
if key not in existing_map:
new_record = self.create({
'employee_id': employee.id,
'current_data': date_str,
'check_in': False,
'check_out': False,
'is_no_card': False,
'is_abnormal': False
})
new_record.compute_attend()
def button_source(self):
employee_id = self.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)
current_date = self.current_data
slot_start = datetime.combine(current_date, datetime.min.time())
slot_end = datetime.combine(current_date + timedelta(days=1), datetime.min.time())
# 查找员工对应的班次
planning_slots = self.env['planning.slot'].search([
('employee_id', '=', employee_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 not template:
template = self.env['planning.slot.template'].search([('role_id.name', '=', '集团行政')], limit=1)
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
next_day = current_date + timedelta(days=1)
start_datetime = datetime.combine(current_date, datetime.min.time()) - timedelta(hours=8)
check_out_end = self.float_to_datetime(current_date, template.check_out_end)
if template.check_out_start == 0.0:
template.check_out_start = 24.0
if template.check_out_end < template.check_out_start:
check_out_end = self.float_to_datetime(next_day, template.check_out_end)
domain = [
('employee_id', '=', employee_id.id),
('checkin_time', '>=', start_datetime),
('checkin_time', '<=', check_out_end - timedelta(hours=8))
]
if '行政' in template.display_name:
domain = [
('employee_id', '=', employee_id.id),
('checkin_time', '>=', current_date),
('checkin_time', '<', next_day)
]
attendance_records = self.env['hr.attendance.detail'].search(domain)
if attendance_records:
action = {
'name': '考勤记录列表',
'type': 'ir.actions.act_window',
'res_model': 'hr.attendance.detail',
'view_mode': 'tree,form',
'target': 'new',
'domain': domain,
}
return action
else:
_logger.info(f"当前过滤条件为开始日期-----{start_datetime}结束日期------{check_out_end - timedelta(hours=8)}")
raise ValidationError("该员工当天没有考勤记录!")
def float_to_datetime(self, base_date, hour_float):
hours = int(hour_float)
minutes = int((hour_float - hours) * 60)
return datetime.combine(base_date, datetime.min.time()) + timedelta(hours=hours, minutes=minutes)
def button_card_approval(self):
form_id = self.env.ref('yuthon_hr_attendance.card_approval_form').id
if self.is_approval:
raise ValidationError("该补卡已提交申请!")
else:
self.is_approval = True
return {
"type": "ir.actions.act_window",
"name": "补卡申请",
"res_model": "card.approval",
"views": [[form_id, "form"]],
"context": {
'default_attendance_id': self.id,
'default_users_id': self.employee_id.user_id.id,
'default_shift_id':self.shift_id.id
}
}
def button_duty_approval(self):
form_id = self.env.ref('yuthon_hr_attendance.go_approval_form').id
return {
"type": "ir.actions.act_window",
"name": "外出申请",
"res_model": "go.approval",
"views": [[form_id, "form"]],
"context": {
'default_is_day': True,
'default_start_date': self.current_data,
'default_end_date': self.current_data,
'default_users_id': self.employee_id.user_id.id,
}
}
def button_batch_correct(self):
attendance_ids = self.env["hr.attendance"].browse(
self._context.get('active_ids', self._context.get('active_id'))
)
attendance_ids.fixes()
def fixes(self):
for record in self:
record.approval_check_in = record.check_in
record.approval_check_out = record.check_out
record.is_approval_no = record.is_no_card
record.is_approval_early = record.is_early_card
record.is_approval_late = record.is_late_card
record.is_approval_abnormal = record.is_abnormal
record.is_approval_abs = record.absenteeism_card
values = {
'is_approval': True,
'is_abnormal': False,
'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_check_in_red': False,
'is_check_out_red': False,
'absenteeism_card': False,
}
if not record.shift_id:
year = record.current_data.year
month = record.current_data.month
day = record.current_data.day
current_date = datetime.today()
calendar_attendances = record.employee_id.resource_id.calendar_id.attendance_ids
if not calendar_attendances:
continue
min_hour_from = calendar_attendances[0].hour_from
min_hour = int(min_hour_from)
min_minute = int((min_hour_from - min_hour) * 60)
will_datetime_start = current_date.replace(
year=year, month=month, day=day,
hour=min_hour, minute=min_minute,
second=0, microsecond=0
) - timedelta(hours=8)
max_hour_to = calendar_attendances[-1].hour_to
max_hour = int(max_hour_to)
max_minute = int((max_hour_to - max_hour) * 60)
will_datetime_end = current_date.replace(
year=year, month=month, day=day,
hour=max_hour, minute=max_minute,
second=0, microsecond=0
) - timedelta(hours=8)
if not record.check_in:
values['check_in'] = will_datetime_start
values['is_check_in_red'] = True
values['is_no_card'] = False
elif record.check_in > will_datetime_start:
values['check_in'] = will_datetime_start
values['is_check_in_red'] = True
values['is_late_card'] = False
if not record.check_out:
values['check_out'] = will_datetime_end
values['is_check_out_red'] = True
values['is_no_card'] = False
elif record.check_out < will_datetime_end:
values['check_out'] = will_datetime_end
values['is_check_out_red'] = True
values['is_early_card'] = False
else:
shift = record.shift_id
shift_template = shift.template_id
start_time = shift_template.start_time
end_time = shift_template.end_time
end_time2 = shift_template.end_time2
start_hour = int(start_time)
start_minute = int((start_time - start_hour) * 60)
end_hour = int(end_time)
end_minute = int((end_time - end_hour) * 60)
end2_hour = int(end_time2)
end2_minute = int((end_time2 - end2_hour) * 60)
shift_date = shift.start_datetime.date()
normal_check_in = datetime.combine(shift_date, datetime.min.time()).replace(
hour=start_hour, minute=start_minute, second=0, microsecond=0
) - timedelta(hours=8)
normal_check_out = datetime.combine(shift_date, datetime.min.time()).replace(
hour=end_hour, minute=end_minute, second=0, microsecond=0
) - timedelta(hours=8)
if shift_template.day2 == '次日' and shift_template.planning_name != '中夜':
normal_check_out = datetime.combine(shift_date, datetime.min.time()).replace(
hour=end2_hour, minute=end2_minute, second=0, microsecond=0
) - timedelta(hours=8) + timedelta(days=1)
if not record.check_in:
values['check_in'] = normal_check_in
values['is_check_in_red'] = True
values['is_no_card'] = False
elif record.check_in > normal_check_in:
values['check_in'] = normal_check_in
values['is_check_in_red'] = True
values['is_late_card'] = False
if not record.check_out:
values['check_out'] = normal_check_out
values['is_check_out_red'] = True
values['is_no_card'] = False
elif record.check_out < normal_check_out:
values['check_out'] = normal_check_out
values['is_check_out_red'] = True
values['is_early_card'] = False
record.write(values)
@api.depends('current_data')
def _compute_week(self):
for record in self:
if record.current_data:
weekday = (record.current_data + timedelta(hours=8)).weekday()
weekdays_cn = {0: 'one', 1: 'two', 2: 'three',
3: 'four', 4: 'five', 5: 'six',
6: 'seven'}
record.week = weekdays_cn[weekday]
else:
record.week = False
def _common_card_remind(self, dept_name=None, planning_name=None, is_check_in=True, method_name=None, is_executive=False):
"""通用打卡提醒函数"""
if not method_name:
return
current_date = fields.Date.today() + timedelta(hours=8)
cron_code = f'model.{method_name}()'
cron = self.env['ir.cron'].search([('code', 'like', cron_code)], limit=1)
next_call = cron.nextcall + timedelta(hours=8)
start_window = next_call - timedelta(minutes=5)
end_window = next_call + timedelta(minutes=5)
if not (start_window <= fields.Datetime.now() + timedelta(hours=8) <= end_window):
return
if is_executive:
domain = [('current_data', '=', current_date)]
attendances = self.env['hr.attendance'].search(domain)
if self._process_shift_days(current_date): # 如果是工作日则提醒
attended_employee_ids = attendances.mapped('employee_id')
for employee in attended_employee_ids:
if is_check_in:
is_card = self.env['hr.attendance'].search([('employee_id', '=', employee.id), ('check_in', '!=', False), ('current_data', '=', current_date)], limit=1)
if not is_card:
self.card_in_comment(employee, '行政班次')
else:
is_card = self.env['hr.attendance'].search([('employee_id', '=', employee.id), ('check_out', '!=', False), ('current_data', '=', current_date)], limit=1)
if not is_card:
self.card_out_comment(employee, '行政班次')
else:
slot_start = datetime.combine(current_date, datetime.min.time())
slot_end = datetime.combine(current_date + timedelta(days=1), datetime.min.time())
domain = [
('template_id.planning_name', '=', planning_name),
('start_datetime', '>=', slot_start),
('start_datetime', '<', slot_end),
('role_id.name', '=', dept_name),
]
planning_slots = self.env['planning.slot'].search(domain)
for slot in planning_slots:
if is_check_in:
is_card = self.env['hr.attendance'].search([('employee_id', '=', slot.employee_id.id), ('check_in', '!=', False), ('current_data', '=', current_date)], limit=1)
if not is_card:
self.card_in_comment(slot.employee_id, planning_name)
else:
is_card = self.env['hr.attendance'].search([('employee_id', '=', slot.employee_id.id), ('check_out', '!=', False), ('current_data', '=', current_date)], limit=1)
if not is_card:
self.card_out_comment(slot.employee_id, planning_name)
# 保安部 - 早班
def ba_card_white_up(self):
self._common_card_remind(dept_name='保安部', planning_name='早班', is_check_in=True,
method_name='ba_card_white_up')
def ba_card_white_down(self):
self._common_card_remind(dept_name='保安部', planning_name='早班', is_check_in=False,
method_name='ba_card_white_down')
# 保安部 - 中班
def ba_card_mid_up(self):
self._common_card_remind(dept_name='保安部', planning_name='中班', is_check_in=True,
method_name='ba_card_mid_up')
def ba_card_mid_down(self):
self._common_card_remind(dept_name='保安部', planning_name='中班', is_check_in=False,
method_name='ba_card_mid_down')
# 保安部 - 夜班
def ba_card_night_up(self):
self._common_card_remind(dept_name='保安部', planning_name='夜班', is_check_in=True,
method_name='ba_card_night_up')
def ba_card_night_down(self):
self._common_card_remind(dept_name='保安部', planning_name='夜班', is_check_in=False,
method_name='ba_card_night_down')
# 保安部 - 旧晚(旧区府晚班)
def ba_card_old_night_up(self):
self._common_card_remind(dept_name='保安部', planning_name='旧晚(旧区府晚班)', is_check_in=True,
method_name='ba_card_old_night_up')
def ba_card_old_night_down(self):
self._common_card_remind(dept_name='保安部', planning_name='旧晚(旧区府晚班)', is_check_in=False,
method_name='ba_card_old_night_down')
# 保安部 - 旧白(旧区府白班)
def ba_card_old_white_up(self):
self._common_card_remind(dept_name='保安部', planning_name='旧白(旧区府白班)', is_check_in=True,
method_name='ba_card_old_white_up')
def ba_card_old_white_down(self):
self._common_card_remind(dept_name='保安部', planning_name='旧白(旧区府白班)', is_check_in=False,
method_name='ba_card_old_white_down')
# 工程部 - 白班
def gc_white_in_remind(self):
self._common_card_remind(dept_name='工程部', planning_name='白班', is_check_in=True,
method_name='gc_white_in_remind')
def gc_white_out_remind(self):
self._common_card_remind(dept_name='工程部', planning_name='白班', is_check_in=False,
method_name='gc_white_out_remind')
# 工程部 - 白值
def gc_white_remind_in(self):
self._common_card_remind(dept_name='工程部', planning_name='白值', is_check_in=True,
method_name='gc_white_remind_in')
def gc_white_remind_out(self):
self._common_card_remind(dept_name='工程部', planning_name='白值', is_check_in=False,
method_name='gc_white_remind_out')
# 工程部 - 中夜
def gc_night_remind_in(self):
self._common_card_remind(dept_name='工程部', planning_name='中夜', is_check_in=True,
method_name='gc_night_remind_in')
def gc_night_remind_out(self):
self._common_card_remind(dept_name='工程部', planning_name='中夜', is_check_in=False,
method_name='gc_night_remind_out')
# 工程部 - 旧晚(旧区府晚班)
def card_old_night_remind_in(self):
self._common_card_remind(dept_name='工程部', planning_name='旧晚(旧区府晚班)', is_check_in=True,
method_name='card_old_night_remind_in')
def card_old_night_remind_out(self):
self._common_card_remind(dept_name='工程部', planning_name='旧晚(旧区府晚班)', is_check_in=False,
method_name='card_old_night_remind_out')
# 工程部 - 旧白(旧区府白班)
def card_old_day_remind_in(self):
self._common_card_remind(dept_name='工程部', planning_name='旧白(旧区府白班)', is_check_in=True,
method_name='card_old_day_remind_in')
def card_old_day_remind_out(self):
self._common_card_remind(dept_name='工程部', planning_name='旧白(旧区府白班)', is_check_in=False,
method_name='card_old_day_remind_out')
# 粮储-白班
def card_lc_remind_in(self):
self._common_card_remind(dept_name='联安粮库', planning_name='白班', is_check_in=True,
method_name='card_lc_remind_in')
def card_lc_remind_out(self):
self._common_card_remind(dept_name='联安粮库', planning_name='白班', is_check_in=False,
method_name='card_lc_remind_out')
# 粮储-值班(白值)
def card_lc_shift_remind_in(self):
self._common_card_remind(dept_name='联安粮库', planning_name='白值', is_check_in=True,
method_name='card_lc_shift_remind_in')
def card_lc_shift_remind_out(self):
self._common_card_remind(dept_name='联安粮库', planning_name='白值', is_check_in=False,
method_name='card_lc_shift_remind_out')
# 保安计划行政
def card_lc_shift_remind_on_administrative(self):
self._common_card_remind(dept_name='保安部', planning_name='保安计划行政', is_check_in=True,
method_name='card_lc_shift_remind_on_administrative')
def card_lc_shift_remind_out_administrative(self):
self._common_card_remind(dept_name='保安部', planning_name='保安计划行政', is_check_in=False,
method_name='card_lc_shift_remind_out_administrative')
# 行政班次
def executive_day_remind_in(self):
self._common_card_remind(planning_name='行政班次', is_check_in=True,
method_name='executive_day_remind_in', is_executive=True)
def executive_day_remind_out(self):
self._common_card_remind(planning_name='行政班次', is_check_in=False,
method_name='executive_day_remind_out', is_executive=True)
def card_in_comment(self, employee_id, planning_name):
url = '#小程序://天投云/XttsPpKg3MEpY7h'
content_item = [{"key": "班次", "value": planning_name}, {"key": "类型", "value": "上班"}]
try:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='att', user_id=employee_id.wecom_userid,
description="记得打卡", title="打卡提醒",
url=url, btntxt="详细信息", content_item=content_item,)
except Exception as e:
_logger.error(f"上班打卡提醒发送失败:员工={employee_id.name}, 错误={e}")
def card_out_comment(self, employee_id, planning_name):
url = '#小程序://天投云/XttsPpKg3MEpY7h'
content_item = [{"key": "班次", "value": planning_name}, {"key": "类型", "value": "下班"}]
try:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='att', user_id=employee_id.wecom_userid,
description="记得打卡", title="打卡提醒",
url=url, btntxt="详细信息", content_item=content_item, )
except Exception as e:
_logger.error(f"下班打卡提醒发送失败:员工={employee_id.name}, 错误={e}")
def test_card(self, user_id):
content_item = [{"key": "班次", "value": "测试行政"}, {"key": "类型", "value": "下班"}]
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='att', user_id=str(user_id),
description="记得打卡", title="打卡提醒",
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')))
for attendance in attendance_ids:
attendance.set_shift_name()
def set_shift_name(self):
"""抽取的统一班次名称设置逻辑"""
for record in self:
# 白名单不做处理
if record.shift_id or record.employee_id.id in record.employee_id.resource_calendar_id.resource_user_ids.ids:
continue # 已有班次记录,无需自动设置
shift_days_list = []
is_shift_replacement = False
is_holiday = False
is_weekend = False
# 判断排班员工
planning_ids = self.env['planning.role'].search([('name', 'in', ['保安部', '工程部', '联安粮库'])])
related_resources = set()
for role in planning_ids:
if role.resource_ids:
related_resources.update(role.resource_ids.ids)
related_resources_set = self.env['resource.resource'].browse(related_resources)
if record.employee_id.resource_id in related_resources_set:
record.shift_name = '休息'
record.is_week = True
record.write({
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'is_warning': False,
'is_no_card': False,
'absenteeism_card': False,
})
continue
if record.current_data:
current_year = record.current_data.year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
leave_records = self.env['resource.calendar.leaves'].search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)
])
# 检查是否为节假日
for leave in leave_records:
if leave.date_from and leave.date_to:
from_date = fields.Date.to_date(leave.date_from)
to_date = fields.Date.to_date(leave.date_to)
if from_date <= record.current_data <= to_date:
is_holiday = True # 标记为节假日
# 收集调班补班日期
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(','))
# 检查是否为调班补班日
target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
if target_day_str in shift_days_list:
is_shift_replacement = True
# 检查是否为周末
weekday = record.current_data.isoweekday()
if weekday in [6, 7]:
is_weekend = True
if is_shift_replacement:
record.shift_name = '行政班次'
elif is_weekend or is_holiday:
record.shift_name = '休息'
else:
record.shift_name = '行政班次'
if record.is_expatriation:
record.shift_name = '外派'
if record.shift_name == "休息" and not is_holiday:
record.is_week = is_weekend
else:
record.is_week = False
record.is_holiday = is_holiday
@api.model
def create(self, vals):
record = super(HrAttendance, self).create(vals)
# 未来日期不处理班次(保持原逻辑)
if 'current_data' in vals:
current_data = fields.Date.to_date(vals['current_data'])
today = fields.Date.today()
if current_data > today:
return record
record.set_shift_name()
return record
def write(self, vals):
result = super(HrAttendance, self).write(vals)
# 当更新考勤日期或班次时,重新计算班次名称
if 'current_data' in vals or 'shift_id' in vals:
self.set_shift_name()
for record in self:
self._update_vacation_report(record)
return result
def unlink(self):
for record in self:
self._update_vacation_report(record)
result = super(HrAttendance, self).unlink()
return result
def _update_vacation_report(self, record):
"""更新相关的假期报表日志"""
if record.current_data and record.employee_id:
current_date = record.current_data.replace(day=1)
report = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', record.employee_id.id),
('vacation_date', '=', current_date)
], limit=1)
if report:
report.compute_attendance_fields()
def button_all(self):
attendance_ids = self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id')))
for record in attendance_ids:
if not record.shift_id:
shift_days_list = []
is_shift_replacement = False
is_leave_day = False
is_weekend = False
if record.current_data:
# 查询休假记录
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
leave_records = self.env['resource.calendar.leaves'].search(
[("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)])
# 检查是否存在匹配的休假日期
for leave in leave_records:
if leave.date_from and leave.date_to:
# 提取休假记录的月日范围
from_date = fields.Date.to_date(leave.date_from) + timedelta(days=1)
to_date = fields.Date.to_date(leave.date_to) + timedelta(days=1)
if from_date <= record.current_data <= to_date:
is_leave_day = True
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(',')) # 调班补班
target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
if target_day_str in shift_days_list:
is_shift_replacement = True
weekday = record.current_data.isoweekday()
if weekday in [6, 7]:
is_weekend = True
if is_leave_day:
record.shift_name = ''
elif is_shift_replacement:
record.shift_name = '行政班次'
elif is_weekend:
record.shift_name = ''
else:
record.shift_name = '行政班次'
# 定时任务主方法
def action_update_attendance(self):
pass #todo 待过
def update_attendance(self, employee, current_date):
current_date_str = fields.Date.to_string(current_date)
leave_domain = [
('employee_id', '=', employee.id),
('date_from', '<=', fields.Datetime.to_string(datetime.combine(current_date, datetime.max.time()))),
('date_to', '>=', fields.Datetime.to_string(datetime.combine(current_date, datetime.min.time()))),
('state', '=', 'validate')
]
leave_records = self.env['hr.leave'].search(leave_domain)
if leave_records:
for leave in leave_records:
if leave.batch_id:
main_leave_record = leave.batch_id
elif leave.hour_batch_id:
main_leave_record = leave.hour_batch_id
workflow = self.env['soong.workflow.record'].search([
('code', '=', main_leave_record.code)
], limit=1)
self.write({
'workflow_id': workflow.id,
'is_workflow': True,
'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'absenteeism_card': False
})
else:
self.write({
'workflow_id': False,
'is_workflow': False,
})
go_approval_domain = [
('employee_ids', 'in', employee.id),
('start_date', '<=', current_date_str),
('end_date', '>=', current_date_str),
('work_end', '=', True),
]
go_approval_record = self.env['go.approval'].search(go_approval_domain, limit=1)
if go_approval_record:
workflow = self.env['soong.workflow.record'].search([
('code', '=', go_approval_record.code)
], limit=1)
if workflow and workflow.approve_state == 'end':
self.write({
'workflow_id': workflow.id,
'is_workflow': True,
'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'absenteeism_card': False
})
class card_remind_form(models.Model):
_name = 'card.remind.form'
_description = '提醒日志'
name = fields.Char(string='姓名')
start_time = fields.Datetime(string='时间')
shift = fields.Char(string='班次')
department = fields.Char(string='部门')
remarks = fields.Text(string='备注')
class MedicalRulesSetting(models.Model):
_name = 'medical.rules.setting' # 数据库keyerror错误