840 lines
40 KiB
Python
840 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
||
from datetime import datetime, timedelta, time
|
||
from email.policy import default
|
||
|
||
from odoo import models, fields, api, _
|
||
from odoo.exceptions import UserError, ValidationError
|
||
import re
|
||
from odoo.tools.misc import format_date
|
||
from collections import namedtuple, defaultdict
|
||
from datetime import date
|
||
import calendar
|
||
import logging
|
||
|
||
|
||
class HrLeave(models.Model):
|
||
_inherit = 'hr.leave'
|
||
number_of_days = fields.Float(string='天数', digits=(16, 3))
|
||
|
||
def _check_validity(self):
|
||
sorted_leaves = defaultdict(lambda: self.env['hr.leave'])
|
||
for leave in self:
|
||
sorted_leaves[(leave.holiday_status_id, leave.date_from.date())] |= leave
|
||
for (leave_type, date_from), leaves in sorted_leaves.items():
|
||
if leave_type.requires_allocation == 'no':
|
||
continue
|
||
employees = self.env['hr.employee']
|
||
for leave in leaves:
|
||
employees |= leave._get_employees_from_holiday_type()
|
||
leave_data = leave_type.get_allocation_data(employees, date_from)
|
||
if leave_type.allows_negative:
|
||
max_excess = leave_type.max_allowed_negative
|
||
for employee in employees:
|
||
if leave_data[employee] and leave_data[employee][0][1]['virtual_remaining_leaves'] < -max_excess:
|
||
raise ValidationError(_("There is no valid allocation to cover that request."))
|
||
continue
|
||
previous_leave_data = leave_type.with_context(ignored_leave_ids=leaves.ids).get_allocation_data(employees, date_from)
|
||
for employee in employees:
|
||
previous_emp_data = previous_leave_data[employee] and previous_leave_data[employee][0][1]['virtual_excess_data']
|
||
emp_data = leave_data[employee] and leave_data[employee][0][1]['virtual_excess_data']
|
||
if not previous_emp_data and not emp_data:
|
||
continue
|
||
# 这里加一个假期额度的判断
|
||
# if previous_emp_data != emp_data and len(emp_data) >= len(previous_emp_data):
|
||
# raise ValidationError("您的假期额度不足,无法申请该假期")
|
||
|
||
|
||
def action_leave_wizard(self):
|
||
return {
|
||
"type": "ir.actions.act_window",
|
||
"name": "请假单",
|
||
"res_model": "yuthon.leave.wizard",
|
||
"view_mode": "form",
|
||
"target": "new",
|
||
}
|
||
|
||
@api.model
|
||
def default_get(self, fields_list):
|
||
default = super().default_get(fields_list)
|
||
# 删除default的holiday_status_id键
|
||
if 'holiday_status_id' in default and 'holiday_status_id_default_request_unit' in self.env.context:
|
||
if self.env.context.get('holiday_status_id_default_request_unit') == 'hour':
|
||
search_domain = [
|
||
('request_unit', '=', 'hour'),
|
||
'|',
|
||
('requires_allocation', '=', 'no'),
|
||
'&',
|
||
('has_valid_allocation', '=', True),
|
||
'|',
|
||
('allows_negative', '=', True),
|
||
'&',
|
||
('virtual_remaining_leaves', '>', 0),
|
||
('allows_negative', '=', False),
|
||
]
|
||
else:
|
||
search_domain = [
|
||
('request_unit', '=', 'day'),
|
||
'|',
|
||
('requires_allocation', '=', 'no'),
|
||
'&',
|
||
('has_valid_allocation', '=', True),
|
||
'|',
|
||
('allows_negative', '=', True),
|
||
'&',
|
||
('virtual_remaining_leaves', '>', 0),
|
||
('allows_negative', '=', False),
|
||
]
|
||
lt = self.env['hr.leave.type'].search(search_domain, limit=1, order='sequence')
|
||
if lt:
|
||
default['holiday_status_id'] = lt.id
|
||
default['request_unit_custom'] = False
|
||
return default
|
||
|
||
batch_id = fields.Many2one(string='批量', comodel_name='hr.leave.batch', ondelete='cascade')
|
||
hour_batch_id = fields.Many2one(string='批量', comodel_name='hr.leave.batch', ondelete='cascade')
|
||
leave_documents_ids = fields.Many2many(related='holiday_status_id.leave_documents_id', string="假期详情说明", readonly=True)
|
||
details = fields.Text(string="假期流程说明")
|
||
request_hour_from_real = fields.Selection([
|
||
('0', '0:00'), ('0.5', '0:30'),
|
||
('1', '01:00'), ('1.5', '01:30'),
|
||
('2', '02:00'), ('2.5', '02:30'),
|
||
('3', '03:00'), ('3.5', '03:30'),
|
||
('4', '04:00'), ('4.5', '04:30'),
|
||
('5', '05:00'), ('5.5', '05:30'),
|
||
('6', '06:00'), ('6.5', '06:30'),
|
||
('7', '07:00'), ('7.5', '07:30'),
|
||
('8', '08:00'), ('8.5', '08:30'),
|
||
('9', '09:00'), ('9.5', '09:30'),
|
||
('10', '10:00'), ('10.5', '10:30'),
|
||
('11', '11:00'), ('11.5', '11:30'),
|
||
('12', '12:00'), ('12.5', '12:30'),
|
||
('13', '13:00'), ('13.5', '13:30'),
|
||
('14', '14:00'), ('14.5', '14:30'),
|
||
('15', '15:00'), ('15.5', '15:30'),
|
||
('16', '16:00'), ('16.5', '16:30'),
|
||
('17', '17:00'), ('17.5', '17:30'),
|
||
('18', '18:00'), ('18.5', '18:30'),
|
||
('19', '19:00'), ('19.5', '19:30'),
|
||
('20', '20:00'), ('20.5', '20:30'),
|
||
('21', '21:00'), ('21.5', '21:30'),
|
||
('22', '22:00'), ('22.5', '22:30'),
|
||
('23', '23:00'), ('23.5', '23:30')],
|
||
string='实际开始时间', compute='_compute_request_hour_from_real')
|
||
# custom_document_ids = fields.Many2many(
|
||
# string='文件',
|
||
# comodel_name='ir.attachment',
|
||
# relation='leave_custom_doc_rel',
|
||
# column1='leave_id',
|
||
# column2='attachment_id'
|
||
# )
|
||
request_hour_from = fields.Selection([
|
||
('0', '0:00'), ('0.5', '0:30'),
|
||
('1', '01:00'), ('1.5', '01:30'),
|
||
('2', '02:00'), ('2.5', '02:30'),
|
||
('3', '03:00'), ('3.5', '03:30'),
|
||
('4', '04:00'), ('4.5', '04:30'),
|
||
('5', '05:00'), ('5.5', '05:30'),
|
||
('6', '06:00'), ('6.5', '06:30'),
|
||
('7', '07:00'), ('7.5', '07:30'),
|
||
('8', '08:00'), ('8.5', '08:30'),
|
||
('9', '09:00'), ('9.5', '09:30'),
|
||
('10', '10:00'), ('10.5', '10:30'),
|
||
('11', '11:00'), ('11.5', '11:30'),
|
||
('12', '12:00'), ('12.5', '12:30'),
|
||
('13', '13:00'), ('13.5', '13:30'),
|
||
('14', '14:00'), ('14.5', '14:30'),
|
||
('15', '15:00'), ('15.5', '15:30'),
|
||
('16', '16:00'), ('16.5', '16:30'),
|
||
('17', '17:00'), ('17.5', '17:30'),
|
||
('18', '18:00'), ('18.5', '18:30'),
|
||
('19', '19:00'), ('19.5', '19:30'),
|
||
('20', '20:00'), ('20.5', '20:30'),
|
||
('21', '21:00'), ('21.5', '21:30'),
|
||
('22', '22:00'), ('22.5', '22:30'),
|
||
('23', '23:00'), ('23.5', '23:30')], string='开始时间')
|
||
|
||
request_hour_to = fields.Selection([
|
||
('0', '12:00'), ('0.5', '12:30'),
|
||
('1', '01:00'), ('1.5', '01:30'),
|
||
('2', '02:00'), ('2.5', '02:30'),
|
||
('3', '03:00'), ('3.5', '03:30'),
|
||
('4', '04:00'), ('4.5', '04:30'),
|
||
('5', '05:00'), ('5.5', '05:30'),
|
||
('6', '06:00'), ('6.5', '06:30'),
|
||
('7', '07:00'), ('7.5', '07:30'),
|
||
('8', '08:00'), ('8.5', '08:30'),
|
||
('9', '09:00'), ('9.5', '09:30'),
|
||
('10', '10:00'), ('10.5', '10:30'),
|
||
('11', '11:00'), ('11.5', '11:30'),
|
||
('12', '12:00'), ('12.5', '12:30'),
|
||
('13', '13:00'), ('13.5', '13:30'),
|
||
('14', '14:00'), ('14.5', '14:30'),
|
||
('15', '15:00'), ('15.5', '15:30'),
|
||
('16', '16:00'), ('16.5', '16:30'),
|
||
('17', '17:00'), ('17.5', '17:30'),
|
||
('18', '18:00'), ('18.5', '18:30'),
|
||
('19', '19:00'), ('19.5', '19:30'),
|
||
('20', '20:00'), ('20.5', '20:30'),
|
||
('21', '21:00'), ('21.5', '21:30'),
|
||
('22', '22:00'), ('22.5', '22:30'),
|
||
('23', '23:00'), ('23.5', '23:30')], string='结束时间')
|
||
|
||
def update_state(self):
|
||
leave_ids = self.env["hr.leave"].browse(
|
||
self._context.get('active_ids', self._context.get('active_id')))
|
||
for leave in leave_ids:
|
||
leave.state = 'draft'
|
||
|
||
@api.model
|
||
def create(self, vals):
|
||
allocation = super(HrLeave, self).create(vals)
|
||
if allocation.state != 'validate':
|
||
return allocation
|
||
employees = self.env['hr.employee'].browse(allocation.employee_id.id)
|
||
# 根据假期类型执行不同逻辑
|
||
if allocation.holiday_status_id.name == '年假':
|
||
self._process_annual_leave(allocation, employees)
|
||
elif allocation.holiday_status_id.name == '补休':
|
||
self._process_compensatory_leave(allocation, employees)
|
||
return allocation
|
||
|
||
def _process_annual_leave(self, allocation, employees):
|
||
"""处理年假逻辑"""
|
||
days = allocation.number_of_days * 8
|
||
for employee in employees:
|
||
# 更新员工年假余额
|
||
employee.write({
|
||
'user_year': employee.user_year + days
|
||
})
|
||
reports_id = self.env['yuthon.vacation.reports'].search([
|
||
('employee_id', '=', employee.id)
|
||
], limit=1)
|
||
if not reports_id:
|
||
reports_id = self.env['yuthon.vacation.reports'].create({
|
||
'employee_id': employee.id,
|
||
})
|
||
self.env['vacation.leave.line'].create({
|
||
'vacation_id': reports_id.id,
|
||
'update_type': 'reduce',
|
||
'update_date': fields.Datetime.now(),
|
||
'update_lines': days,
|
||
'text': '休假',
|
||
'allocation_type': 'vacation',
|
||
})
|
||
|
||
def _process_compensatory_leave(self, allocation, employees):
|
||
"""处理补休逻辑"""
|
||
for employee in employees:
|
||
reports_id = self.env['yuthon.vacation.reports'].search([
|
||
('employee_id', '=', employee.id)
|
||
], limit=1)
|
||
if not reports_id:
|
||
reports_id = self.env['yuthon.vacation.reports'].create({
|
||
'employee_id': employee.id,
|
||
})
|
||
self.env['vacation.over.line'].create({
|
||
'vacation_id': reports_id.id,
|
||
'employee_id': employee.id,
|
||
'update_type': 'reduce',
|
||
'workday': allocation.number_of_days * 8,
|
||
'legal_day': 0,
|
||
'public_holidays': 0,
|
||
'meal_number': 0,
|
||
'update_date': fields.Date.today(),
|
||
'is_void': False,
|
||
'text': f'补休',
|
||
})
|
||
|
||
def unlink(self):
|
||
for record in self:
|
||
if record.employee_id and record.holiday_status_id.name == '年假':
|
||
employee = record.employee_id
|
||
days = record.number_of_hours_display
|
||
lines_to_delete = self.env['vacation.leave.line'].search([
|
||
('vacation_id.employee_id', '=', employee.id),
|
||
('update_type', '=', 'reduce'),
|
||
('update_lines', '=', days),
|
||
('allocation_type', '=', 'vacation'),
|
||
])
|
||
if lines_to_delete:
|
||
lines_to_delete.is_void = True
|
||
employee.write({
|
||
'user_year': employee.user_year - days
|
||
})
|
||
|
||
elif record.employee_id and record.holiday_status_id.name == '补休':
|
||
employee = record.employee_id
|
||
days = record.number_of_hours_display
|
||
over_lines_to_delete = self.env['vacation.over.line'].search([
|
||
('employee_id', '=', employee.id),
|
||
('workday', '=', days),
|
||
('update_type', '=', 'reduce'),
|
||
('text', 'ilike', f'%{record.name}%'), # 通过名称模糊匹配
|
||
])
|
||
|
||
if over_lines_to_delete:
|
||
over_lines_to_delete.write({'is_void': True})
|
||
|
||
return super(HrLeave, self).unlink()
|
||
|
||
@api.depends('request_hour_from')
|
||
def _compute_request_hour_from_real(self):
|
||
for record in self:
|
||
if record.request_hour_from == '9':
|
||
record.request_hour_from_real = '8'
|
||
else:
|
||
record.request_hour_from_real = record.request_hour_from
|
||
|
||
@api.depends('date_from', 'date_to', 'resource_calendar_id', 'holiday_status_id.request_unit')
|
||
def _compute_duration(self):
|
||
super()._compute_duration()
|
||
for holiday in self:
|
||
# 如果是自然日:
|
||
if holiday.holiday_status_id.type_of_work == "1":
|
||
if holiday.request_date_from and holiday.request_date_to:
|
||
days = (holiday.request_date_to - holiday.request_date_from).days + 1
|
||
hours = days * 8
|
||
else:
|
||
days = 0
|
||
hours = 0.0
|
||
holiday.number_of_hours = hours
|
||
holiday.number_of_days = days
|
||
if holiday.holiday_status_id.type_of_work == "0":
|
||
calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")])
|
||
holidays_list = [] # 假期
|
||
shift_days_list = [] #调休
|
||
for leaves in calendar_leaves:
|
||
if leaves.holidays:
|
||
holidays_list.extend(leaves.holidays.split(',')) # 法定节假日
|
||
if leaves.shift_replacement_day:
|
||
shift_days_list.extend(leaves.shift_replacement_day.split(',')) #调班补班
|
||
if leaves.alternate_rest_day:
|
||
holidays_list.extend(leaves.alternate_rest_day.split(',')) #调休日
|
||
target_day = holiday.request_date_from
|
||
target_day_str = f"{target_day.month}月{target_day.day}日"
|
||
is_weekend = holiday.request_date_from.weekday() in [5, 6]
|
||
# 判断是否为假期
|
||
if target_day_str in holidays_list:
|
||
holiday.number_of_hours = 0
|
||
# 判断是否为调休日
|
||
elif target_day_str in shift_days_list:
|
||
if holiday.request_hour_from and holiday.request_hour_to:
|
||
# 调休日无论是否在周末都按正常逻辑计算
|
||
start_hour = float(holiday.request_hour_from)
|
||
end_hour = float(holiday.request_hour_to)
|
||
|
||
# 跨中午12点的情况
|
||
if start_hour < 12 and end_hour > 12:
|
||
hour = float(end_hour) - float(start_hour) - 1
|
||
holiday.number_of_hours = hour
|
||
# 9点到12点的情况
|
||
elif start_hour == 9 and end_hour == 12:
|
||
holiday.number_of_hours = 4
|
||
# 其他情况按正常计算
|
||
else:
|
||
hour = float(end_hour) - float(start_hour)
|
||
holiday.number_of_hours = hour
|
||
# 非假期且非调休日的情况
|
||
else:
|
||
# 如果是周末且不在调休列表中,小时数为0
|
||
if is_weekend:
|
||
holiday.number_of_hours = 0
|
||
# 普通工作日按正常逻辑计算
|
||
else:
|
||
if holiday.request_hour_from and holiday.request_hour_to:
|
||
start_hour = float(holiday.request_hour_from)
|
||
end_hour = float (holiday.request_hour_to)
|
||
if start_hour < 12 and end_hour > 12:
|
||
hour = float(end_hour) - float(start_hour) - 1
|
||
holiday.number_of_hours = hour
|
||
elif start_hour == 9 and end_hour == 12:
|
||
holiday.number_of_hours = 4
|
||
else:
|
||
hour = float(end_hour) - float(start_hour)
|
||
holiday.number_of_hours = hour
|
||
|
||
days = (holiday.request_date_to - holiday.request_date_from).days + 1
|
||
current_date = holiday.request_date_from
|
||
while current_date <= holiday.request_date_to:
|
||
current_date_str = current_date.strftime('%m月%d日')
|
||
current_date_str = holiday.remove_leading_zero(current_date_str)
|
||
is_weekend = current_date.weekday() in [5, 6]
|
||
if current_date_str in holidays_list:
|
||
days -= 1
|
||
elif current_date_str in shift_days_list:
|
||
if not is_weekend:
|
||
days += 1
|
||
else :
|
||
if is_weekend:
|
||
days -= 1
|
||
current_date += timedelta(days=1)
|
||
holiday.number_of_days = days
|
||
|
||
def _compute_request_unit_hours(self):
|
||
if not self.env.context.get('no_change_request_unit_hours'):
|
||
super()._compute_request_unit_hours()
|
||
|
||
@api.constrains('date_from', 'date_to', 'employee_id')
|
||
def _check_date(self):
|
||
if self.env.context.get('leave_skip_date_check', False):
|
||
return
|
||
|
||
all_employees = self.all_employee_ids
|
||
all_leaves = self.search([
|
||
('date_from', '<', max(self.mapped('date_to'))),
|
||
('date_to', '>', min(self.mapped('date_from'))),
|
||
('employee_id', 'in', all_employees.ids),
|
||
('id', 'not in', self.ids),
|
||
('state', 'not in', ['cancel', 'refuse']),
|
||
])
|
||
for holiday in self:
|
||
domain = [
|
||
('date_from', '<', holiday.date_to),
|
||
('date_to', '>', holiday.date_from),
|
||
('id', '!=', holiday.id),
|
||
('state', 'not in', ['cancel', 'refuse']),
|
||
]
|
||
|
||
employee_ids = (holiday.employee_id | holiday.employee_ids).ids
|
||
search_domain = domain + [('employee_id', 'in', employee_ids)]
|
||
conflicting_holidays = all_leaves.filtered_domain(search_domain)
|
||
|
||
if conflicting_holidays:
|
||
conflicting_holidays_list = []
|
||
# Do not display the name of the employee if the conflicting holidays have an employee_id.user_id equivalent to the user id
|
||
holidays_only_have_uid = bool(holiday.employee_id)
|
||
holiday_states = dict(conflicting_holidays.fields_get(allfields=['state'])['state']['selection'])
|
||
for conflicting_holiday in conflicting_holidays:
|
||
conflicting_holiday_data = {}
|
||
conflicting_holiday_data['employee_name'] = conflicting_holiday.employee_id.name
|
||
conflicting_holiday_data['date_from'] = format_date(self.env,
|
||
min(conflicting_holiday.mapped('date_from')))
|
||
conflicting_holiday_data['date_to'] = format_date(self.env,
|
||
min(conflicting_holiday.mapped('date_to')))
|
||
conflicting_holiday_data['state'] = holiday_states[conflicting_holiday.state]
|
||
if conflicting_holiday.employee_id.user_id.id != self.env.uid:
|
||
holidays_only_have_uid = False
|
||
if conflicting_holiday_data not in conflicting_holidays_list:
|
||
conflicting_holidays_list.append(conflicting_holiday_data)
|
||
if not conflicting_holidays_list:
|
||
return
|
||
conflicting_holidays_strings = []
|
||
if holidays_only_have_uid:
|
||
for conflicting_holiday_data in conflicting_holidays_list:
|
||
conflicting_holidays_string = _('%(date_from)s - %(date_to)s',
|
||
date_from=conflicting_holiday_data['date_from'],
|
||
date_to=conflicting_holiday_data['date_to'])
|
||
conflicting_holidays_strings.append(conflicting_holidays_string)
|
||
raise ValidationError(_("""\
|
||
您已有:「%s」 的假期申请待审批,请勿重复提交。如需调整,请先联系审批人或撤回原申请。
|
||
""",
|
||
"\n".join(conflicting_holidays_strings)))
|
||
|
||
super()._check_date()
|
||
|
||
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}日'
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
class HolidaysRequestBatch(models.Model):
|
||
_name = 'hr.leave.batch'
|
||
_description = '请假审批'
|
||
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin', 'soong.workflow.mixin']
|
||
|
||
code = fields.Char(string="编号", default=lambda self: self.env['ir.sequence'].next_by_code('yuthon_holiday_code'))
|
||
name = fields.Char(string='标题')
|
||
create_date = fields.Date(string='创建日期', default=fields.Datetime.today())
|
||
employee_id = fields.Many2one(related='users_id.employee_id', string="员工", store=True)
|
||
company_id = fields.Many2one(related='users_id.employee_id.company_id', string="公司", store=True)
|
||
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="部门")
|
||
user_role_id = fields.Many2one(related='users_id.employee_id.user_role_id', string="角色")
|
||
details = fields.Text(string="假期流程说明", compute='_compute_details')
|
||
days = fields.Float(string='天数')
|
||
hours = fields.Float(string='小时')
|
||
text = fields.Text(string='申请事由')
|
||
is_day = fields.Boolean(string='按天请假')
|
||
is_hour = fields.Boolean(string='按小时请假')
|
||
leave_ids = fields.One2many(string='按天请假详情', comodel_name='hr.leave', inverse_name='batch_id')
|
||
hour_leave_ids = fields.One2many(string='按小时请假详情', comodel_name='hr.leave', inverse_name='hour_batch_id')
|
||
is_issues = fields.Boolean(string='是否上会')
|
||
work_end = fields.Boolean(string="结束")
|
||
issues_result = fields.Html(string="会议结果")
|
||
leave_by_hour = fields.Boolean(string='请假单位(小时)', default=False)
|
||
leave_by_day = fields.Boolean(string='请假单位(天)', default=True)
|
||
urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度',
|
||
default='normal')
|
||
start_date = fields.Date(string="开始时间")
|
||
end_date = fields.Date(string="结束时间")
|
||
document_ids = fields.Many2many(
|
||
string='附件',
|
||
comodel_name='ir.attachment',
|
||
relation='leave_custom_doc_rel',
|
||
column1='leave_id',
|
||
column2='attachment_id'
|
||
)
|
||
holiday_allowance = fields.Char(string="可用额度", compute='_compute_holiday_allowance', store=True)
|
||
active = fields.Boolean('Active', default=True, tracking=True)
|
||
|
||
def _get_default_name_title(self):
|
||
return self._description if self._description else self._name
|
||
|
||
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_day')
|
||
def _onchange_is_day(self):
|
||
if not self.is_day:
|
||
self.leave_ids = [(5, 0, 0)] # 清空One2many字段
|
||
|
||
@api.onchange('is_hour')
|
||
def _onchange_is_hour(self):
|
||
if not self.is_hour:
|
||
self.hour_leave_ids = [(5, 0, 0)]
|
||
|
||
@api.depends("users_id")
|
||
def _compute_details(self):
|
||
for record in self:
|
||
holiday_id = record.env['yuthon.holiday.setting'].search([('department_ids', 'in', record.users_id.employee_id.department_id.id)], limit=1)
|
||
if holiday_id:
|
||
record.details = holiday_id.name
|
||
else:
|
||
record.details = " "
|
||
|
||
# 提交
|
||
def action_submit(self):
|
||
"""提交请假申请,区分年假和其他假期处理"""
|
||
self._process_specific_leaves('年假', is_annual_leave=True)
|
||
other_leave_types = [
|
||
'产假', '事假', '法定假', '调休', '补休', '值班调休',
|
||
'婚假', '陪产假', '计生假', '丧假', '探亲假',
|
||
'家长会假', '病假(普通)', '病假(住院期)',
|
||
'病假(出院恢复期)', '考试假'
|
||
]
|
||
for leave_type in other_leave_types:
|
||
self._process_specific_leaves(leave_type, is_annual_leave=False)
|
||
return True
|
||
|
||
def _process_specific_leaves(self, leave_type_name, is_annual_leave):
|
||
"""
|
||
处理指定类型假期
|
||
:param leave_type_name: 假期类型名称
|
||
:param is_annual_leave: 是否为年假(True/False)
|
||
"""
|
||
# 收集天假和小时假记录
|
||
day_leaves = self._get_day_leaves(leave_type_name)
|
||
hourly_leaves = self._get_hourly_leaves(leave_type_name)
|
||
|
||
# 处理天假
|
||
for leave in day_leaves:
|
||
date_ranges = self._split_by_month(leave['start'], leave['end']) if not is_annual_leave else [
|
||
{'start': leave['start'], 'end': leave['end']}]
|
||
for period in date_ranges:
|
||
if leave_type_name == '补休':
|
||
# 计算小时数(天数 * 8)
|
||
hours = self._compute_duration_day(leave['type_id'], period['start'], period['end']) * 8
|
||
if hours > 0:
|
||
self._create_overtime_leave_line(period_start=period['start'], leave_data=leave, hours=hours, period=period, leave_type_name=leave_type_name)
|
||
else:
|
||
days = self._compute_duration_day(leave['type_id'], period['start'], period['end'])
|
||
if days > 0:
|
||
if is_annual_leave:
|
||
# 年假以小时为单位(天数 * 8)
|
||
self._create_annual_leave_line(period_start=period['start'], leave_data=leave, hours=days * 8, period=period)
|
||
else:
|
||
self._create_other_leave_line(period_start=period['start'], leave_data=leave, days=days, period=period, leave_type_name=leave_type_name)
|
||
# 处理小时假
|
||
for leave in hourly_leaves:
|
||
leave_date = leave['date']
|
||
hours = leave['days'] * 8
|
||
if hours > 0:
|
||
if is_annual_leave:
|
||
self._create_annual_leave_line(period_start=leave_date, leave_data=leave, hours=hours, period={'start': leave_date, 'end': leave_date})
|
||
elif leave_type_name == '补休':
|
||
self._create_overtime_leave_line(period_start=leave_date, leave_data=leave, hours=hours, period={'start': leave_date, 'end': leave_date}, leave_type_name=leave_type_name)
|
||
else:
|
||
# 其他类型小时假,使用天数
|
||
self._create_other_leave_line(period_start=leave_date, leave_data=leave,days=leave['days'], period={'start': leave_date, 'end': leave_date}, leave_type_name=leave_type_name)
|
||
|
||
def _get_day_leaves(self, leave_type_name):
|
||
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
|
||
return [{
|
||
'start': fields.Date.from_string(leave.request_date_from),
|
||
'end': fields.Date.from_string(leave.request_date_to),
|
||
'type_id': leave.holiday_status_id,
|
||
'workflow_id': workflow_id}
|
||
for leave in self.leave_ids
|
||
if leave.holiday_status_id.name == leave_type_name
|
||
and leave.request_date_from
|
||
and leave.request_date_to
|
||
]
|
||
|
||
def _get_hourly_leaves(self, leave_type_name):
|
||
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
|
||
hourly_records = []
|
||
for line in self.hour_leave_ids:
|
||
if line.holiday_status_id.name == leave_type_name and line.request_date_from:
|
||
hours_match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '')
|
||
hours = float(hours_match.group(1)) if hours_match else 0
|
||
hourly_records.append({
|
||
'date': fields.Date.from_string(line.request_date_from),
|
||
'type_id': line.holiday_status_id,
|
||
'workflow_id': workflow_id,
|
||
'days': hours / 8
|
||
})
|
||
return hourly_records
|
||
|
||
def _split_by_month(self, start_date, end_date):
|
||
# 跨月拆分逻逻辑
|
||
date_ranges = []
|
||
if start_date.month == end_date.month and start_date.year == end_date.year:
|
||
date_ranges.append({'start': start_date, 'end': end_date})
|
||
return date_ranges
|
||
|
||
first_month_end = date(start_date.year, start_date.month,
|
||
calendar.monthrange(start_date.year, start_date.month)[1])
|
||
date_ranges.append({'start': start_date, 'end': first_month_end})
|
||
current_month = start_date.month + 1
|
||
current_year = start_date.year
|
||
while (current_year < end_date.year) or (current_year == end_date.year and current_month < end_date.month):
|
||
if current_month > 12:
|
||
current_month = 1
|
||
current_year += 1
|
||
month_end = date(current_year, current_month, calendar.monthrange(current_year, current_month)[1])
|
||
date_ranges.append({'start': date(current_year, current_month, 1), 'end': month_end})
|
||
current_month += 1
|
||
|
||
date_ranges.append({'start': date(end_date.year, end_date.month, 1), 'end': end_date})
|
||
return date_ranges
|
||
|
||
def _create_overtime_leave_line(self, period_start, leave_data, hours, period, leave_type_name):
|
||
"""为补休类型创建加班明细行(vacation.over.line),类型为减少,值赋给workday"""
|
||
report_month = date(period_start.year, period_start.month, 1)
|
||
vacation_report = self.env['yuthon.vacation.reports'].search([
|
||
('employee_id', '=', self.users_id.employee_ids.id),
|
||
('vacation_date', '>=', fields.Date.to_string(report_month)),
|
||
('vacation_date', '<=', fields.Date.to_string(
|
||
date(period_start.year, period_start.month,
|
||
calendar.monthrange(period_start.year, period_start.month)[1]))
|
||
)
|
||
], limit=1)
|
||
self.env['vacation.over.line'].create({
|
||
'vacation_id': vacation_report.id if vacation_report else False,
|
||
'employee_id': self.users_id.employee_ids.id,
|
||
'workflow_record_id': leave_data['workflow_id'],
|
||
'update_type': 'reduce',
|
||
'workday': hours,
|
||
'legal_day': 0,
|
||
'public_holidays': 0,
|
||
'meal_number': 0,
|
||
'update_date': fields.Date.today(),
|
||
'is_void': False,
|
||
'text': f"请假审批"
|
||
})
|
||
|
||
def _create_annual_leave_line(self, period_start, leave_data, hours, period):
|
||
"""更新年假记录,使用小时作为单位"""
|
||
report_month = date(period_start.year, period_start.month, 1)
|
||
vacation_report = self.env['yuthon.vacation.reports'].search([
|
||
('employee_id', '=', self.users_id.employee_ids.id),
|
||
('vacation_date', '>=', fields.Date.to_string(report_month)),
|
||
('vacation_date', '<=', fields.Date.to_string(
|
||
date(period_start.year, period_start.month,
|
||
calendar.monthrange(period_start.year, period_start.month)[1]))
|
||
)
|
||
], limit=1)
|
||
self.env['vacation.leave.line'].create({
|
||
'vacation_id': vacation_report.id if vacation_report else False,
|
||
'employee_id': self.users_id.employee_ids.id,
|
||
'workflow_record_id': leave_data['workflow_id'],
|
||
'update_type': 'reduce',
|
||
'update_date': fields.Date.today(),
|
||
'update_lines': hours,
|
||
'is_void': False,
|
||
'allocation_type': 'vacation',
|
||
'text': f"请假审批"
|
||
})
|
||
|
||
def _compute_duration_day(self, holiday_status_id, start_date, end_date):
|
||
"""
|
||
计算请假时长
|
||
:param holiday_status_id: 请假类型
|
||
:param start_date: 开始时间
|
||
:param end_date: 结束时间
|
||
:return:天
|
||
"""
|
||
# 如果是自然日:
|
||
if holiday_status_id.type_of_work == "1":
|
||
if start_date and end_date:
|
||
days = (end_date - start_date).days + 1
|
||
else:
|
||
days = 0
|
||
return days
|
||
if holiday_status_id.type_of_work == 0:
|
||
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]
|
||
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)
|
||
else:
|
||
delta = end_date - start_date
|
||
days = delta.days + 1
|
||
return days
|
||
|
||
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.constrains('is_day', 'is_hour', 'leave_ids', 'hour_leave_ids')
|
||
def _check_leave_type_and_details(self):
|
||
"""检查请假类型和明细行是否符合要求"""
|
||
for record in self:
|
||
if not record.is_day and not record.is_hour:
|
||
raise ValidationError(_("请选择按天请假或按小时请假!"))
|
||
if not record.leave_ids and not record.hour_leave_ids:
|
||
raise ValidationError(_("请至少添加一条按天或按小时的请假记录!"))
|
||
if record.leave_ids and not record.is_day:
|
||
raise ValidationError(_("添加了按天请假记录,必须选择'按天请假'选项!"))
|
||
if record.hour_leave_ids and not record.is_hour:
|
||
raise ValidationError(_("添加了按小时请假记录,必须选择'按小时请假'选项!"))
|
||
if record.hour_leave_ids and not record.leave_ids:
|
||
number_hours = 0
|
||
for line in record.hour_leave_ids:
|
||
if line.request_date_from and line.request_hour_to:
|
||
# 使用正则表达式提取小时数
|
||
match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '')
|
||
if match and float(match.group(1)) == 0:
|
||
raise ValidationError(_("请假小时数不能为0!"))
|
||
if record.company_id.name == '天河物管':
|
||
match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '')
|
||
if match:
|
||
number_hours += float(match.group(1))
|
||
# 天河物管的总小时数验证
|
||
if record.company_id.name == '天河物管' and number_hours:
|
||
valid_hours = [2, 4, 6, 8]
|
||
if number_hours not in valid_hours:
|
||
raise ValidationError(_("休假时长必须为 2、4、6 或 8 小时才可以保存!"))
|
||
|
||
@api.onchange('leave_ids', 'hour_leave_ids', 'employee_id')
|
||
def _onchange_leave_dates(self):
|
||
# 初始化变量
|
||
all_dates = []
|
||
total_days = 0
|
||
first_hour_leave = self.hour_leave_ids[0] if self.hour_leave_ids else None
|
||
|
||
# 收集所有日期并计算总天数
|
||
for leave in self.leave_ids:
|
||
if leave.request_date_from:
|
||
all_dates.append(leave.request_date_from)
|
||
if leave.request_date_to:
|
||
all_dates.append(leave.request_date_to)
|
||
total_days += leave.number_of_days_display if hasattr(leave, 'number_of_days_display') else 0
|
||
|
||
for line in self.hour_leave_ids:
|
||
if line.request_date_from:
|
||
all_dates.append(line.request_date_from)
|
||
|
||
# 设置开始和结束日期
|
||
self.start_date = min(all_dates) if all_dates else False
|
||
self.end_date = max(all_dates) if all_dates else False
|
||
|
||
# 计算小时请假的天数
|
||
hours_days = 0
|
||
for line in self.hour_leave_ids:
|
||
match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '')
|
||
hours_days += float(match.group(1)) / 8 if match else 0
|
||
|
||
self.days = total_days + hours_days
|
||
|
||
# 生成名称
|
||
if self.employee_id:
|
||
name_parts = [f"{self.employee_id.name}的"]
|
||
|
||
if self.start_date and self.end_date:
|
||
date_part = f"{self.start_date.strftime('%Y-%m-%d')}~{self.end_date.strftime('%Y-%m-%d')}"
|
||
name_parts.append(date_part)
|
||
|
||
if self.leave_ids:
|
||
name_parts.append(f"共计{int(total_days)}天")
|
||
|
||
if first_hour_leave and first_hour_leave.number_of_hours_text:
|
||
name_parts.append(first_hour_leave.number_of_hours_text)
|
||
|
||
self.name = " ".join(name_parts)
|
||
else:
|
||
self.name = ""
|
||
|
||
@api.depends('users_id')
|
||
def _compute_holiday_allowance(self):
|
||
for record in self:
|
||
reports_id = self.env['yuthon.vacation.reports'].search([('employee_id', '=', record.users_id.employee_id.id)], limit=1)
|
||
if reports_id and not record.log_ids:
|
||
leave_days = reports_id.leave
|
||
overtime_days = reports_id.working_overtime
|
||
record.holiday_allowance = f"年假{leave_days:.1f}天, 补休{overtime_days:.1f}天"
|
||
|
||
class HrLeaveLine(models.Model):
|
||
_name = 'hr.leave.line'
|
||
|
||
#
|
||
# # 计算开始时间与结束时间之间的时长
|
||
# def _get_leave_duration(self, start_time: datetime, end_time: datetime):
|
||
# """
|
||
# 公共假期计算
|
||
# 查询所有公共假期(resource.calendar.leaves)
|
||
# 通过开始时间与结束时间,去除公共假期,再去除双休,计算时长,
|
||
# """
|
||
# self.ensure_one()
|
||
# all_resource_calendar_leaves = self.env['resource.calendar.leaves'].search([('resource_id', '=', False)])
|
||
# # 创建一个集合存储假期的每一天
|
||
# holidays = []
|
||
# for holiday in all_resource_calendar_leaves:
|
||
# # 根据公共假期的开始时间与结束时间计算时间段
|
||
# current = holiday.date_from
|
||
# while current <= holiday.date_to:
|
||
# # 将当天0点的时间戳放入假期集合
|
||
# holidays.append(int(int(current.timest上午p()) + 8 * 3600) // 86400 * 86400 - 8 * 3600)
|
||
# current += timedelta(days=1)
|
||
# effective_days = 0
|
||
# current = start_time
|
||
# while current <= end_time:
|
||
# current_timest上午p = int(int(current.timest上午p()) + 8 * 3600) // 86400 * 86400 - 8 * 3600
|
||
# if current.weekday() < 5 and current_timest上午p not in holidays: # 0-4为工作日
|
||
# effective_days += 1
|
||
# current += timedelta(days=1)
|
||
#
|
||
# return effective_days
|