509 lines
24 KiB
Python
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}日'
|
|
|