Files
gzth/yuthon_hr_attendance/models/go_approval.py
T
2026-07-16 10:44:45 +08:00

509 lines
24 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
from datetime import datetime, time, timedelta
from odoo.exceptions import UserError
from odoo.exceptions import ValidationError, UserError
import calendar
from dateutil.relativedelta import relativedelta
class GoApprovalsLine(models.Model):
_name = 'go.approvals.line'
class GoApprovals(models.Model):
_name = 'go.approval'
_description = "外出审批"
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin', 'soong.workflow.mixin']
_rec_name = 'name'
name = fields.Char(string="标题", compute='_compute_name', store=True, readonly=False)
code = fields.Char(string="编号", default=lambda self: self.env['ir.sequence'].next_by_code('overtime_approval'))
users_id = fields.Many2one('res.users', string="申请人", default=lambda self: self.env.user.id)
department_id = fields.Many2one(related='users_id.employee_id.department_id', string="部门")
company_id = fields.Many2one(related='users_id.employee_id.company_id', string="公司")
reason_application = fields.Char(string="事由")
is_issues = fields.Boolean(string='是否上会')
issues_result = fields.Html(string="会议结果")
employee_ids = fields.Many2many('hr.employee', string="外出人", default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)]))
documents_ids = fields.Many2many('ir.attachment', string="附件")
details = fields.Text(string="外出说明", default="1.可通过填写天数和小时,直接获得结束时间\n 2.一份外出单,可同时选择多人一次审批")
related_issues_id = fields.Many2one('issue.library', string="关联议题")
date_start = fields.Datetime(string="开始时间", default=fields.Datetime.now)
date_end = fields.Datetime(string="结束时间", default=fields.Datetime.now)
meal = fields.Selection([('yes', '是'), ('no', '否')], string="误餐费", default=None)
days = fields.Float(string="天数", compute='_compute_days', store=True, readonly=False)
hours = fields.Float(string="小时", compute='_compute_hours', readonly=False)
urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度',
default='normal')
active = fields.Boolean('Active', default=True, tracking=True)
work_end = fields.Boolean(string="结束")
is_day = fields.Boolean(string="按天")
is_hour = fields.Boolean(string="按小时")
start_day_selection = fields.Selection([('half', '上半天'),('afternoon', '下半天')])
end_day_selection = fields.Selection([('half', '上半天'), ('afternoon', '下半天')])
start_date = fields.Date(string="开始日期", default=fields.Date.today)
end_date = fields.Date(string="结束日期", default=fields.Date.today)
is_this_department = fields.Boolean(string='是否是本部门', default=True)
department_ids = fields.Many2many('hr.department', string="多部门")
@api.constrains('is_day', 'is_hour')
def _check_is_day_or_hour(self):
for record in self:
if not record.is_day and not record.is_hour:
raise ValidationError("请至少选择【按天】或【按小时】其中一项!")
@api.constrains('start_date', 'end_date', 'date_start', 'date_end', 'employee_ids', 'is_day', 'is_hour')
def _check_date_overlap(self):
for rec in self:
if not rec.employee_ids:
continue
for employee in rec.employee_ids:
# 按天模式校验:日期区间重叠
if rec.is_day and rec.start_date and rec.end_date:
domain_day = [
('id', '!=', rec.id),
('employee_ids', 'in', employee.id),
('is_day', '=', True),
('start_date', '<=', rec.end_date),
('end_date', '>=', rec.start_date),
]
day_records = self.env['go.approval'].search(domain_day).ids
if day_records:
workflow = self.env['soong.workflow.record'].search([
('res_model', '=', 'go.approval'),
('res_id', 'in', day_records),
('reclaim_state', '=', False),
('approve_state', 'in', ['pending', 'end']),
])
if workflow:
raise ValidationError(
f"员工【{employee.name}】在 {rec.start_date} 至 {rec.end_date} 期间已有按天外出申请(审批中/已结束),不可重复创建。"
)
# 按小时模式校验:时间区间重叠
if rec.is_hour and rec.date_start and rec.date_end:
domain_hour = [
('id', '!=', rec.id),
('employee_ids', 'in', employee.id),
('is_hour', '=', True),
('date_start', '<=', rec.date_end),
('date_end', '>=', rec.date_start),
]
hour_records = self.env['go.approval'].search(domain_hour).ids
if hour_records:
workflow = self.env['soong.workflow.record'].search([
('res_model', '=', 'go.approval'),
('res_id', 'in', hour_records),
('reclaim_state', '=', False),
('approve_state', 'in', ['pending', 'end']),
])
if workflow:
raise ValidationError(
f"员工【{employee.name}】在 {rec.date_start} 至 {rec.date_end} 期间已有按小时外出申请(审批中/已结束),不可重复创建。"
)
def _get_default_name_title(self):
return self._description if self._description else self._name
def btn_exec_action(self):
"""重写转交方法,在转交时校验时间段冲突"""
cx = self.env.context.copy() or {}
btn_key = cx.get('btn_key', '')
# 只在转交时做校验
btn_rec = self.env['soong.workflow.node.button'].search([('btn_key', '=', btn_key)], limit=1)
if btn_rec and btn_rec.name == '转交':
if self.start_date and self.end_date and self.employee_ids:
for employee in self.employee_ids:
candidate_ids = self.env['go.approval'].search([
('id', '!=', self.id),
('employee_ids', 'in', employee.id),
('start_date', '<=', self.end_date),
('end_date', '>=', self.start_date),
]).ids
if candidate_ids:
valid_workflow = self.env['soong.workflow.record'].search([
('res_model', '=', 'go.approval'),
('res_id', 'in', candidate_ids),
('reclaim_state', '=', False),
('approve_state', 'in', ['pending', 'end']),
])
if valid_workflow:
raise ValidationError(
f"员工【{employee.name}】在 {self.start_date} 至 {self.end_date} 期间已有外出申请(审批中或已结束),相同时间段内不能重复创建。"
)
return super().btn_exec_action()
name_title = fields.Char(default=_get_default_name_title)
self_users_ids = fields.Many2many('res.users', string="当前经办人", default=lambda self: [(6, 0, [self.env.user.id])])
@api.onchange('is_this_department')
def _onchange_is_this_department(self):
self.department_ids = [(5, 0, 0)]
if self.is_this_department and self.users_id:
employee = self.env['hr.employee'].search([('user_id', '=', self.users_id.id)], limit=1)
main_department = employee.department_id
all_departments = main_department.search([
('id', 'child_of', main_department.id)
])
self.department_ids = [(6, 0, all_departments.ids)]
@api.onchange('start_day_selection', 'end_day_selection')
def _onchange_selection_fields(self):
for record in self:
if record.start_date == record.end_date:
if record.start_day_selection == 'afternoon' and record.end_day_selection == 'half':
raise ValidationError("当开始日期为下半天时,结束选择日期只能下半天。")
@api.onchange('date_start', 'date_end')
def _onchange_date_range(self):
if self.date_start and self.date_end:
start = fields.Datetime.from_string(self.date_start)
end = fields.Datetime.from_string(self.date_end)
if start > end:
raise ValidationError("开始时间必须小于结束时间,请重新设置。")
@api.onchange('is_issues')
def _onchange_is_issues(self):
if self.is_issues:
issue_id = self.env['issue.library'].create({'name': self.name, 'is_invisible_workflow': True})
self.related_issues_id = issue_id.id
self.env['issue.library.approval.line'].create({
'issue_library_id': issue_id.id,
'workflow_name': self.name,
'approval_type': self.workflow_id.name,
'res_model': self._name,
'res_id': self.id,
'code': self.code,
'approval_date': fields.Datetime.now(),
})
@api.depends('employee_ids', 'days', 'hours', 'start_date', 'end_date', 'date_start', 'date_end',
'is_day', 'is_hour', 'start_day_selection', 'end_day_selection')
def _compute_name(self):
for rec in self:
employee_list = [employee.name for employee in rec.employee_ids]
employees_str = ", ".join(employee_list)
base_name = f"{employees_str}外出"
date_info = ""
if rec.is_hour:
# 按小时计算逻辑
if rec.date_start and rec.date_end:
start_date = fields.Date.from_string(rec.date_start)
end_date = fields.Date.from_string(rec.date_end)
start_str = start_date.strftime('%Y-%m-%d')
end_str = end_date.strftime('%Y-%m-%d')
if start_date == end_date:
date_info = f"{start_str}共:{rec.hours}小时"
else:
date_info = f"{start_str}至{end_str}共:{rec.hours}小时"
elif rec.is_day:
if rec.start_date and rec.end_date:
start_str = rec.start_date.strftime('%Y-%m-%d')
end_str = rec.end_date.strftime('%Y-%m-%d')
full_days = (rec.end_date - rec.start_date).days
day_count = 0
if not rec.start_day_selection and not rec.end_day_selection:
day_count1 = full_days
else:
if rec.start_day_selection == 'half' and rec.end_day_selection == 'afternoon':
day_count = 1
elif rec.start_day_selection == 'half' and rec.end_day_selection == 'half':
day_count = 0.5
else:
day_count = 0.5
day_count1 = full_days + day_count
if start_str == end_str:
date_part = start_str
else:
date_part = f"{start_str}至{end_str}"
date_info = f"{date_part}共:{day_count1}天"
if date_info:
rec.name = f"{base_name}{date_info}"
else:
rec.name = base_name
@api.depends('date_start', 'date_end')
def _compute_hours(self):
"""根据开始时间和结束时间计算小时数"""
for record in self:
if record.date_start and record.date_end:
start = fields.Datetime.from_string(record.date_start)
end = fields.Datetime.from_string(record.date_end)
if end > start:
record.hours = round((end - start).total_seconds() / 3600, 2)
else:
record.hours = 0
else:
record.hours = 0
# @api.onchange('start_date', 'end_date') # 问题:onchange 新建时不触发,days 初始为 0
# def _onchange_days(self):
# for record in self:
# if record.start_date and record.end_date:
# delta = record.end_date - record.start_date
# record.days = delta.days + 1 if delta.days >= 0 else 0
# else:
# record.days = 0
@api.depends('start_date', 'end_date', 'is_day', 'is_hour', 'date_start', 'date_end')
def _compute_days(self):
"""根据模式计算天数:按小时模式用 (date_end - date_start)秒数 / 3600 / 8;按天模式用日期差+1"""
for record in self:
if record.is_hour:
# 按小时模式:直接用 Datetime 字段计算,不依赖 hours 存储值
if record.date_start and record.date_end:
start = fields.Datetime.from_string(record.date_start)
end = fields.Datetime.from_string(record.date_end)
if end > start:
total_hours = (end - start).total_seconds() / 3600
record.days = round(total_hours / 8, 2)
else:
record.days = 0
else:
record.days = 0
else:
if record.start_date and record.end_date:
delta = record.end_date - record.start_date
record.days = delta.days + 1 if delta.days >= 0 else 0
else:
record.days = 0
@api.onchange('is_day')
def _onchange_is_day(self):
if self.is_day:
self.is_hour = False
@api.onchange('is_hour')
def _onchange_is_hour(self):
if self.is_hour:
self.is_day = False
@api.onchange('days')
def _onchange_calculate_end_date(self):
"""根据开始日期和天数计算结束日期"""
if self.start_date and self.days:
# 计算结束日期
self.end_date = self.start_date + timedelta(days=self.days) - timedelta(days=1)
@api.onchange('hours')
def _onchange_calculate_end_datetime(self):
"""根据开始时间、天数和小时计算结束时间"""
if self.date_start and self.hours:
# 计算时间增量
delta = timedelta(hours=self.hours or 0)
# 计算结束时间
self.date_end = self.date_start + delta
def end_approval(self):
workflow_record_id = self.env['soong.workflow.record'].search([
('code', '=', self.code)
], limit=1)
# 2. 拆分跨月日期区间
if self.is_day == True:
start_date = self.start_date
end_date = self.end_date
else:
start_date = self.date_start
end_date = self.date_end
date_ranges = self._split_date_by_month(start_date, end_date)
# 3. 遍历每个月的日期区间处理
for date_range in date_ranges:
month_first = date_range['month_first']
month_start = date_range['month_start']
month_end = date_range['month_end']
# 3.1 查询当月的vacation_reports记录
reports_id = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', self.users_id.employee_ids.id),
('vacation_date', '=', month_first)
], limit=1)
if not reports_id:
reports_id = self.env['yuthon.vacation.reports'].create({
'employee_id': self.users_id.employee_ids.id,
'vacation_date': month_first,
})
reports_id.vacation_state = 'not_sent'
# 如果当月考勤确认单已确认,则将数据加到下个月的确认单
if reports_id and reports_id.vacation_state == 'confirmed':
next_month_first = month_first + relativedelta(months=1)
reports_id = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', self.users_id.employee_ids.id),
('vacation_date', '=', next_month_first)
], limit=1)
if not reports_id:
reports_id = self.env['yuthon.vacation.reports'].create({
'employee_id': self.users_id.employee_ids.id,
'vacation_date': next_month_first,
})
reports_id.vacation_state = 'not_sent'
if not reports_id:
continue
# 3.2 计算当月天数
month_days = self._compute_duration_day(month_start, month_end)
if month_days <= 0:
continue
# 检查当月是否已存在相同记录
existing_meal_fee = self.env['vacation.meal.line'].search([
('vacation_id', '=', reports_id.id),
('workflow_record_id', '=', workflow_record_id.id if workflow_record_id else False),
('update_type', '=', 'add'),
('text', '=', '审批'),
('update_lines', '=', month_days)
], limit=1)
if not existing_meal_fee and self.meal == 'yes':
meal_fee_value = (0, 0, {
'vacation_id': reports_id.id,
'update_type': 'add',
'workflow_record_id': workflow_record_id.id if workflow_record_id else False,
'update_date': fields.Datetime.now(),
'update_lines': month_days,
'text': '审批'
})
reports_id.write({'meal_fee_ids': [meal_fee_value]})
for employee in self.employee_ids:
if self.is_day:
start = self.start_date
end = self.end_date
date_field = 'current_data'
elif self.is_hour:
start = self.date_start.date() if self.date_start else None
end = self.date_end.date() if self.date_end else None
date_field = 'current_data'
else:
return True
if not (start and end):
return True
attendance_records = self.env['hr.attendance'].search([
('employee_id', '=', employee.id),
(date_field, '>=', start),
(date_field, '<=', end)
])
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
if attendance_records:
attendance_records.write({
'is_workflow': True,
'workflow_id':workflow_id,
'is_clock_card': True,
'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'absenteeism_card': False
})
else:
current_date = start
while current_date <= end:
self.env['hr.attendance'].create({
'is_workflow': True,
'workflow_id': workflow_id,
'employee_id': employee.id,
date_field: current_date,
'is_clock_card': True,
'check_in': False,
'check_out': False,
'is_no_card': False,
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'absenteeism_card': False
})
# 日期递增
current_date += timedelta(days=1)
def _compute_duration_day(self,start_date, end_date):
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),
('resource_id', '=', False),
])
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]
days = 0 # 初始化工作日计数
current_date = start_date
while current_date <= end_date:
current_date_str = current_date.strftime('%m月%d日')
current_date_str = self.remove_leading_zero(current_date_str)
is_weekend = current_date.weekday() in [5, 6]
if current_date_str in holidays_list:
# 如果是法定节假日,不计为工作日
pass
elif current_date_str in shift_days_list:
# 如果是调休日,无论是否周末都算作工作日
days += 1
elif not is_weekend:
# 如果不是周末且不是节假日,算作工作日
days += 1
current_date += timedelta(days=1)
return days
def _split_date_by_month(self, start_date, end_date):
"""
拆分跨月日期为每月的起止区间
:param start_date: 开始日期(date对象)
:param end_date: 结束日期(date对象)
:return: 列表,每个元素为(当月1日, 当月开始日, 当月结束日)
"""
date_ranges = []
current_date = start_date
while current_date <= end_date:
# 获取当月最后一天
last_day = calendar.monthrange(current_date.year, current_date.month)[1]
month_end = current_date.replace(day=last_day)
actual_end = min(month_end, end_date)
actual_start = current_date
month_first = current_date.replace(day=1)
date_ranges.append({
'month_first': month_first,
'month_start': actual_start,
'month_end': actual_end
})
if current_date.month == 12:
next_month = current_date.replace(year=current_date.year + 1, month=1, day=1)
else:
next_month = current_date.replace(month=current_date.month + 1, day=1)
current_date = next_month
return date_ranges
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}日'