Files
gzth/yuthon_hr_attendance/models/yuthon_vacation_reports.py
T
2026-01-24 11:34:58 +08:00

1509 lines
70 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.
from datetime import datetime, timedelta, time
from email.policy import default
from operator import index
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import calendar
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError
class YuthonVacationReports(models.Model):
_name = 'yuthon.vacation.reports'
_description = '假期额度记录'
_rec_name = "employee_id"
_order = "vacation_date desc"
employee_id = fields.Many2one('hr.employee', string='员工')
salary_group_id = fields.Many2one(related='employee_id.salary_group_id', string="工资单分组", store=True)
department_id = fields.Many2one(related='employee_id.department_id', string="部门", store=True)
open_start_time = fields.Date(related='employee_id.open_start_time',string="参加工作时间")
company_old = fields.Char(related='employee_id.company_old', string="工作年限")
previous_year = fields.Float(string="上年剩余", digits=(16, 3))
valid_date = fields.Date(string="有效期至")
year_distribution = fields.Float(string="本年发放", digits=(16, 3))
used_this_year = fields.Float(string="本年已用", digits=(16, 3))
remainder_year = fields.Float(string="本年剩余(年假)", digits=(16, 3), compute="_compute_remainder_year")
last_holiday = fields.Float(string="上年公休日", digits=(16, 3))
last_workday = fields.Float(string="上年工作日", digits=(16, 3))
valid_date1 = fields.Date(string="有效期至")
year_holiday = fields.Float(string="本年公休日", digits=(16, 3))
year_workday = fields.Float(string="本年工作日", digits=(16, 3))
year_sum = fields.Float(string="本年调休合计", digits=(16, 3), compute="_compute_year_sum")
used_this_year1 = fields.Float(string="本年已休", digits=(16, 3))
remainder_year1 = fields.Float(string="剩余(补休)", digits=(16, 3), compute="_compute_year_sum")
company_id = fields.Many2one(related="employee_id.company_id", string="公司", store=True)
late_allowed = fields.Float(related="employee_id.late_allowed", string="允许迟到次数")
leave_ids = fields.One2many('vacation.leave.line', 'vacation_id', string='年假明细')
overtime_ids = fields.One2many('vacation.over.line', 'vacation_id', string='加班明细')
meal_fee_ids = fields.One2many('vacation.meal.line', 'vacation_id', string='误餐明细')
medical_fee_ids = fields.One2many('vacation.medical.line', 'vacation_id', string='体检费明细')
other_fee_ids = fields.One2many('vacation.other.line', 'vacation_id', string='其他明细')
vacation_date = fields.Date(string='日期', default=lambda self: fields.Date.today().replace(day=1))
display_date = fields.Char(string='日期', compute='_compute_display_date')
done_leave = fields.Float(string='年假', compute='_compute_leave', store=True, digits=(16, 3), group_operator=None)
done_working_overtime = fields.Float(string='加班时', compute='_compute_working_overtime', digits=(16, 3),
store=True, group_operator=None)
leave = fields.Float(string='剩余年假', compute='_compute_leave', store=True, digits=(16, 3), group_operator=None)
working_overtime = fields.Float(string='剩余补休', compute='_compute_working_overtime', store=True,
digits=(16, 3), group_operator=None)
meal_fee = fields.Integer(string="误餐次数", compute='_compute_meal_fee', store=True, group_operator=None)
medical_fee = fields.Float(string="体检费", compute='_compute_medical_fee', store=True, group_operator=None)
compensatory_leave = fields.Integer(string="调休", compute='_compute_other_leave', store=True, group_operator=None)
leave_of_absence = fields.Float(string="事假", digits=(16, 3), compute='_compute_other_leave', store=True,
group_operator=None)
statutory_leave = fields.Integer(string="加班时(法定)", compute='_compute_other_leave', store=True, group_operator=None, digits=(16, 3))
cop_leave = fields.Float(string='补休', digits=(16, 3), store=True, compute='_compute_working_overtime',
group_operator=None)
duty_off_leave = fields.Integer(string='值班调休', compute='_compute_other_leave', store=True, group_operator=None)
marriage_leave = fields.Integer(string='婚假', compute='_compute_other_leave', store=True, group_operator=None)
maternity_leave = fields.Integer(string='产假', compute='_compute_other_leave', store=True, group_operator=None)
paternity_leave = fields.Integer(string='陪产假', compute='_compute_other_leave', store=True, group_operator=None)
family_planning_leave = fields.Integer(string='计生假', compute='_compute_other_leave', store=True, group_operator=None)
bereavement_leave = fields.Float(string='丧假', compute='_compute_other_leave', store=True, group_operator=None)
home_visit_leave = fields.Integer(string='探亲假', compute='_compute_other_leave', store=True, group_operator=None)
parents_meeting_leave = fields.Integer(string='家长会假', compute='_compute_other_leave', store=True, group_operator=None)
general_sick_leave = fields.Float(string='病假(普通)', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
hospital_sick_leave = fields.Float(string='病假(住院期)', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
recovery_sick_leave = fields.Float(string='病假(出院恢复期)', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
exam_leave = fields.Integer(string='考试假', compute='_compute_other_leave', store=True, group_operator=None)
right_days = fields.Float(string='转正天数', group_operator=None, compute='compute_attendance_fields')
internship_days = fields.Float(string='实习天数', group_operator=None)
test_days = fields.Float(string='试用天数', group_operator=None, compute='compute_attendance_fields')
should_attendance = fields.Float(string="应到", store=True, group_operator=None, compute='compute_attendance_fields')
actual_attendance = fields.Float(string="实到", compute='compute_attendance_fields', store=True,group_operator=None)
is_late_card = fields.Integer(string="迟到", compute='compute_attendance_fields', store=True, group_operator=None)
is_early_card = fields.Integer(string="早退", compute='compute_attendance_fields', store=True, group_operator=None)
is_no_card = fields.Integer(string="缺卡", compute='compute_attendance_fields', store=True, group_operator=None)
is_work = fields.Integer(string='旷工', compute='compute_attendance_fields', store=True, group_operator=None)
is_abnormal = fields.Integer(string="异常", compute='compute_attendance_fields', store=True, group_operator=None)
att_mount = fields.Integer(string="出勤天数", compute='compute_attendance_fields', store=True, group_operator=None)
attendance_message_log_ids = fields.One2many('attendance.message.log', 'vacation_id')
admin_confirms = fields.Boolean(string="是否管理员确认")
is_filing = fields.Boolean(string="归档")
vacation_state = fields.Selection(
selection=[
('not_sent', "未发送"),
('confirmed', "已确认"),
('pending', "待确认"),
],
string="状态",
readonly=True, copy=False, index=True,
tracking=3,
default='not_sent')
def update_vacation_state(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
records.write({'vacation_state': 'not_sent'})
def update_is_confirm(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
overtime_records = self.env['vacation.over.line'].search([
('vacation_id', 'in', records.ids),
('text', '=', '上月额度继承'),
])
if overtime_records:
overtime_records.write({
'is_initial_number': True
})
def write(self, vals):
result = super(YuthonVacationReports, self).write(vals)
if 'vacation_state' in vals and vals['vacation_state'] == 'confirmed':
for record in self:
if record.leave_ids:
record.leave_ids.write({'is_confirm': True})
if record.overtime_ids:
record.overtime_ids.write({'is_confirm': True})
if record.meal_fee_ids:
record.meal_fee_ids.write({'is_confirm': True})
if record.medical_fee_ids:
record.medical_fee_ids.write({'is_confirm': True})
if record.other_fee_ids:
record.other_fee_ids.write({'is_confirm': True})
return result
@api.depends('previous_year', 'year_distribution', 'used_this_year')
def _compute_remainder_year(self):
for record in self:
record.remainder_year = record.previous_year + record.year_distribution - record.used_this_year
@api.depends('last_holiday', 'last_workday', 'year_holiday', 'year_workday', 'used_this_year1')
def _compute_year_sum(self):
for record in self:
record.year_sum = record.last_holiday + record.last_workday + record.year_holiday + record.year_workday
record.remainder_year1 = record.year_sum - record.used_this_year1
@api.constrains('employee_id', 'vacation_date')
def _check_duplicate_monthly_record(self):
"""检查是否存在同一员工当月的重复记录"""
for record in self:
duplicate_records = self.search([
('employee_id', '=', record.employee_id.id),
('vacation_date', '=', record.vacation_date),
('id', '!=', record.id)
])
if duplicate_records:
raise ValidationError(
f"员工 {record.employee_id.name} 在 {record.vacation_date.strftime('%Y年%m月')} "
f"已存在假期额度记录,不能重复创建!"
)
def action_open(self):
self.ensure_one()
target_date = self.vacation_date.replace(day=1) - relativedelta(months=1)
if target_date.month == 12:
next_month = 1
next_year = target_date.year + 1
else:
next_month = target_date.month + 1
next_year = target_date.year
last_day = fields.Date.from_string(
f"{next_year}-{next_month}-01"
) - timedelta(days=1)
first_day_str = fields.Date.to_string(target_date)
last_day_str = fields.Date.to_string(last_day)
action = self.env["ir.actions.actions"]._for_xml_id("yuthon_hr_attendance.action_attendance_company_form")
action.update({
'domain': [
('employee_id', '=', self.employee_id.id),
('current_data', '>=', first_day_str),
('current_data', '<=', last_day_str)
]
})
return action
def confirm_state(self):
for rec in self:
current_user = self.env.user
is_admin = current_user.has_group('base.group_system') or current_user.has_group('base.group_admin')
if not is_admin:
if rec.vacation_date.month == 1:
last_month_year = rec.vacation_date.year - 1
last_month = 12
else:
last_month_year = rec.vacation_date.year
last_month = rec.vacation_date.month - 1
last_day_of_last_month = calendar.monthrange(last_month_year, last_month)[1]
last_month_start = fields.Date.from_string(f'{last_month_year}-{last_month:02d}-01')
last_month_end = fields.Date.from_string(f'{last_month_year}-{last_month:02d}-{last_day_of_last_month}')
leave_batch_domain = [
('users_id', '=', current_user.id),
('start_date', '>=', last_month_start),
('end_date', '<=', last_month_end),
('work_end', '!=', True)
]
invalid_leave_batches = self.env['hr.leave.batch'].search(leave_batch_domain)
if invalid_leave_batches:
raise UserError('有假单未结束, 请先结束假期批次')
current_employee_id = self.env['hr.employee'].search([('user_id', '=', current_user.id)], limit=1).id
overtime_domain = [
('employee_ids', 'in', current_employee_id),
('start_time', '>=', last_month_start),
('start_time', '<=', last_month_end),
('work_end', '!=', True)
]
invalid_overtimes = self.env['overtime.approval'].search(overtime_domain)
if invalid_overtimes:
raise UserError('有加班单未结束,请先结束加班批次')
rec.manage_reject()
@api.depends('vacation_date')
def _compute_display_date(self):
for rec in self:
rec.display_date = rec.vacation_date.strftime('%Y年%m月')
@api.depends('employee_id', 'vacation_date')
def compute_attendance_fields(self):
for rec in self:
is_late = 0
is_early = 0
is_no = 0
is_work = 0
leave_days = 0
test_days = 0
statutory_days = 0
is_abnormal = 0
right_days = 0
actual_attendance = 0 # 实到天数 = 应到天数 - 请假总天数 - 异常天数 + 法定节假日
total_leave_days = 0 # 请假工作流总天数(含小时折算)
is_retired = rec.employee_id.user_state in ('leave', 'withdraw')
if rec.employee_id and rec.vacation_date and not is_retired:
year = rec.vacation_date.year
month = rec.vacation_date.month
month_start = date(year, month, 1)
entry_date = rec.employee_id.entry_date
days_in_month = calendar.monthrange(year, month)[1]
if not entry_date or month_start > entry_date:
start_day = 1
else:
start_day = min(entry_date.day, days_in_month)
actual_days_range = range(start_day, days_in_month + 1)
for day in actual_days_range:
current_date = date(year, month, day)
attendances = self.env["hr.attendance"].sudo().search([
('employee_id', '=', rec.employee_id.id),
("current_data", '=', current_date)
])
if attendances:
valid_attendances = [
att for att in attendances
if (att.check_in or att.check_out) # 有有效打卡
and att.shift_name != '休息'
]
if valid_attendances:
actual_attendance += 1
for att in attendances:
if att.is_late_card:
is_late += 1
if att.is_early_card:
is_early += 1
if att.is_no_card:
is_no += 1
if att.absenteeism_card:
is_work += 1
if att.is_abnormal:
is_abnormal += 1
for att in attendances:
if att.workflow_id and att.workflow_id.workflow_id.name == '请假':
res_model = att.workflow_id.res_model
res_id = att.workflow_id.res_id
if res_model and res_id:
related_model = self.env[res_model].sudo().browse(res_id)
if related_model.exists():
if related_model.days:
total_leave_days += float(related_model.days)
# 小时数折算为天数(1天=8小时)
elif related_model.hours:
total_leave_days += float(related_model.hours) / 8
actual_attendance = actual_attendance - total_leave_days - is_abnormal
employee = rec.employee_id
contract = employee.contract_id
is_outside = employee.user_role_id.name == '外派'
month_start = date(year, month, 1)
month_end = date(year, month, calendar.monthrange(year, month)[1])
# 计算start_date:外派且无合同时用month_start,否则取合同开始日与month_start的最大值
start_date = month_start if (is_outside and not contract) else max(contract.date_start, month_start)
# 计算end_date:外派且无合同时用month_end;有合同且有结束日则取最小值;其他情况用month_end
end_date = month_end if (is_outside and not contract) else (min(contract.date_end, month_end) if contract.date_end else month_end)
leave_days, statutory_days = rec._create_leave_record(rec.employee_id, start_date, end_date)
probation_contract = self.env['hr.contract'].search([
('employee_id', '=', rec.employee_id.id),
('is_probation', '=', True),
('end_date', '>=', month_start),
], limit=1)
if probation_contract:
end_date = min(probation_contract.end_date, month_end)
start_date = max(probation_contract.start_date, month_start)
work_days, statutory_days = rec._create_leave_record(rec.employee_id, start_date, end_date)
test_days = work_days + statutory_days
right_days = (leave_days + statutory_days) - (work_days + statutory_days)
else:
test_days = 0
right_days = 0
rec.should_attendance = leave_days + statutory_days
rec.test_days = test_days
rec.right_days = right_days
rec.is_late_card = is_late
rec.is_early_card = is_early
rec.is_no_card = is_no
rec.is_work = is_work
rec.is_abnormal = is_abnormal
rec.actual_attendance = max(actual_attendance + statutory_days, 0)
rec.att_mount = actual_attendance
def update_is_confirm1(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
for rec in records:
leave_line_domain = [
('vacation_id', '=', rec.id),
('text', '=', "上月额度继承"),
('employee_id', '=', rec.employee_id.id)
]
existing_leave_line = self.env['vacation.leave.line'].search(leave_line_domain, limit=1)
if existing_leave_line:
existing_leave_line.unlink()
if rec.year_distribution:
self.env['vacation.leave.line'].create({
'vacation_id': rec.id,
'employee_id': rec.employee_id.id,
'update_type': 'add',
'update_date': fields.Date.today(),
'update_lines': rec.year_distribution * 8,
'is_void': False,
'allocation_type': 'allocation',
'text': "上月额度继承"
})
over_line_domain = [
('vacation_id', '=', rec.id),
('employee_id', '=', rec.employee_id.id)
]
existing_over_lines = self.env['vacation.over.line'].search(over_line_domain)
if existing_over_lines:
existing_over_lines.unlink()
if rec.year_holiday or rec.year_workday:
over_line_create_vals = {
'vacation_id': rec.id,
'employee_id': rec.employee_id.id,
'update_type': 'add',
'workday': rec.year_workday * 8,
'public_holidays': rec.year_holiday * 8,
'legal_day': 0.0,
'text': "上月额度继承"
}
self.env['vacation.over.line'].create(over_line_create_vals)
def _compute_attendance_fields1(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
august_start = fields.Date.from_string('2025-10-01')
for rec in records:
rec.write({
'vacation_date': august_start,
})
def update_data(self):
return {
"type": "ir.actions.act_window",
"name": "导入",
"res_model": "vacation.reports.wizard",
"view_mode": "form",
"target": "new",
}
@api.depends('other_fee_ids', 'other_fee_ids.holiday_type', 'other_fee_ids.lines',
'other_fee_ids.is_void', 'other_fee_ids.update_type')
def _compute_other_leave(self):
for rec in self:
# 获取所有未作废且类型为减少的有效假期记录
valid_lines = rec.other_fee_ids.filtered(
lambda line: not line.is_void and line.update_type == 'reduce'
)
leave_mapping = {
'调休': 'compensatory_leave',
'事假': 'leave_of_absence',
'法定假': 'statutory_leave',
'值班调休': 'duty_off_leave',
'婚假': 'marriage_leave',
'产假': 'maternity_leave',
'陪产假': 'paternity_leave',
'计生假': 'family_planning_leave',
'丧假': 'bereavement_leave',
'探亲假': 'home_visit_leave',
'家长会假': 'parents_meeting_leave',
'病假(普通)': 'general_sick_leave',
'病假(住院期)': 'hospital_sick_leave',
'病假(出院恢复期)': 'recovery_sick_leave',
'考试假': 'exam_leave'
}
# 初始化所有假期字段为0
for field in leave_mapping.values():
setattr(rec, field, 0)
# 按假期类型分组累加天数
for line in valid_lines:
leave_type_name = line.holiday_type.name or ''
if leave_type_name in leave_mapping:
field_name = leave_mapping[leave_type_name]
current_value = getattr(rec, field_name) or 0
setattr(rec, field_name, current_value + line.lines)
int_fields = [
'compensatory_leave', 'statutory_leave', 'duty_off_leave',
'marriage_leave', 'maternity_leave', 'paternity_leave',
'family_planning_leave', 'home_visit_leave',
'parents_meeting_leave', 'exam_leave'
]
for field in int_fields:
setattr(rec, field, getattr(rec, field) or 0)
def _create_leave_record(self, employee_id, start_date, end_date):
entry_date = employee_id.entry_date
# 调整日期范围:如果开始日期早于入职日期,则从入职日期开始计算
actual_start_date = max(start_date, entry_date) if entry_date else start_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)
])
holidays_list = [] # 节假日列表
statutory_list = [] # 法定节假日列表
shift_days_list = [] # 调休日列表
# 提取节假日和调休日
for leave in calendar_leaves:
if leave.holidays:
holidays_list.extend(leave.holidays.split(','))
statutory_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(','))
# 去除日期中的前导零(如"01月05日"→"1月5日")
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]
statutory_list = [self.remove_leading_zero(date) for date in statutory_list]
work_days = 0 # 工作日计数
statutory_days = 0 # 法定节假日计数
current_date = actual_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]
is_statutory = current_date_str in statutory_list
is_holiday = current_date_str in holidays_list
is_shift_day = current_date_str in shift_days_list
if is_shift_day or (not is_weekend and not is_holiday):
work_days += 1
if is_statutory:
statutory_days += 1
current_date += timedelta(days=1)
return work_days, statutory_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}日'
def button_put(self):
url = f'https://oa.thtzjt.com/webh5/#/vacation?id={self.id}'
we_id = self.env['hr.employee'].search([('id', '=', self.employee_id.id)]).wecom_userid
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='default', user_id=we_id, title="考勤确认",
description=self.employee_id.name + '请确认你的考勤:',
url=url, btntxt="详细信息")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': '确认通知已发送',
'sticky': False,
}
}
def all_put(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
vacation_list = []
for rec in vacation_ids:
vacation_list.append(rec.id)
return {
'type': 'ir.actions.act_window',
'name': '批量发送确认弹窗',
'res_model': 'vacation.put.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_vacation_ids': [(6, 0, [vacation_list])]
}
}
def manage_confirmation(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in vacation_ids:
rec.manage_reject()
def manage_reject(self):
'''
继承核心逻辑
:return:
'''
for rec in self:
last_month_last = rec.vacation_date - timedelta(days=1)
last_month_first = last_month_last.replace(day=1)
last_month_record = self.search([
('employee_id', '=', rec.employee_id.id),
('vacation_date', '>=', fields.Date.to_string(last_month_first)),
('vacation_date', '<=', fields.Date.to_string(last_month_last)),
('vacation_state', '=', 'confirmed')
], limit=1)
rec.vacation_state = 'confirmed'
current_year = rec.vacation_date.year
last_month_year = last_month_last.year
is_cross_year = current_year != last_month_year
if is_cross_year:
inherit_vals = {
'previous_year': last_month_record.remainder_year,
'last_workday': last_month_record.year_workday,
'last_holiday': last_month_record.year_holiday,
'year_holiday': 0.0,
'year_workday': 0.0,
}
rec.write(inherit_vals)
if last_month_record:
inherit_vals = {
'leave': last_month_record.leave,
'working_overtime': last_month_record.working_overtime,
'medical_fee': last_month_record.medical_fee,
}
rec.write(inherit_vals)
if rec.leave:
leave_hours = rec.leave * 8
self._update_leave_lines(rec, [rec], leave_hours)
if rec.working_overtime:
overtime_hours = rec.working_overtime * 8
self._update_over_lines(rec, [rec], overtime_hours)
@api.depends('medical_fee_ids.update_type', 'medical_fee_ids.lines', 'medical_fee_ids.is_void')
def _compute_medical_fee(self):
for record in self:
total = 0.0
valid_lines = record.medical_fee_ids.filtered(lambda l: not l.is_void)
for line in valid_lines:
if line.update_type == 'add':
total += line.lines
elif line.update_type == 'reduce':
total -= line.lines
record.medical_fee = max(total, 0.0)
def disposable_report(self):
year = 2025
months = range(1, 8)
employees = self.env['hr.employee'].search([])
for month in months:
report_date = fields.Date.from_string(f"{year}-{month:02d}-01")
for employee in employees:
existing = self.search([
('employee_id', '=', employee.id),
('vacation_date', '=', report_date)
], limit=1)
if not existing:
self.create({
'employee_id': employee.id,
'vacation_date': report_date,
})
def overtime_allocation(self):
form_id = self.env.ref('hr_holidays.hr_leave_allocation_view_form_manager').id
holiday_status_id = self.env['hr.leave.type'].search([('name', '=', '补休')])
return {
'name': '补休时分配',
'view_mode': 'form',
'res_model': 'hr.leave.allocation',
'type': 'ir.actions.act_window',
'views': [(form_id, 'form')],
'context': {
'default_holiday_status_id': holiday_status_id.id,
'default_employee_ids': [(6, 0, [self.employee_id.id])],
'default_allocation_type': 'regular',
'default_name': '补休增加',
}}
def batch_compute_leave_and_overtime(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
for record in records:
leave_hours = 0.0
for line in record.leave_ids:
if not line.is_void:
line_value = line.update_lines or 0
if line.update_type == 'add':
leave_hours += line_value
elif line.update_type == 'reduce':
leave_hours -= line_value
record.leave = leave_hours / 8
overtime_hours = 0.0
for line in record.overtime_ids:
if not line.is_void:
line_total = (line.workday or 0) + (line.legal_day or 0) + (line.public_holidays or 0)
if line.update_type == 'add':
overtime_hours += line_total
elif line.update_type == 'reduce':
overtime_hours -= line_total
record.working_overtime = overtime_hours / 8
def vacation_delete(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in vacation_ids:
for leave in record.leave_ids:
leave.unlink()
for overtime in record.overtime_ids:
overtime.unlink()
for meal_fee in record.meal_fee_ids:
meal_fee.unlink()
for other in record.other_fee_ids:
other.unlink()
record.write({
'leave': 0.0,
'working_overtime': 0.0,
'meal_fee': 0,
})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': '删除成功',
'sticky': False,
}
}
@api.depends('leave_ids.update_lines', 'leave_ids.update_type', 'leave_ids.is_void')
def _compute_leave(self):
for record in self:
done_leave_hours = 0.0
leave_hours = 0.0
for line in record.leave_ids:
if not line.is_void:
line_value = line.update_lines or 0
if line.update_type == 'add':
leave_hours += line_value
elif line.update_type == 'reduce':
leave_hours -= line_value
done_leave_hours += line_value
record.leave = max(leave_hours / 8, 0)
record.done_leave = max(done_leave_hours / 8, 0)
@api.depends(
'overtime_ids.workday',
'overtime_ids.legal_day',
'overtime_ids.public_holidays',
'overtime_ids.update_type',
'overtime_ids.is_void',
'overtime_ids.workflow_record_id',
'other_fee_ids.holiday_type',
'other_fee_ids.lines'
)
def _compute_working_overtime(self):
for record in self:
total_add1 = 0.0
total_add2 = 0.0
total_add = 0.0
legal_total = 0.0
for line in record.overtime_ids:
if line.is_void:
continue
line_total = (line.workday or 0) + (line.public_holidays or 0)
legal_total += (line.legal_day or 0)
if line.update_type == 'add':
total_add += line_total
elif line.update_type == 'reduce':
total_add -= line_total
if line.workflow_record_id and line.update_type == 'add' and line.overtime_methods == 'leave':
total_add1 += line_total
if line.workflow_record_id and line.update_type == 'reduce':
total_add2 += line_total
adjust_holiday_days = 0.0
for other_line in record.other_fee_ids:
if other_line.holiday_type and other_line.holiday_type.name == '调休':
adjust_holiday_days += other_line.lines or 0
record.working_overtime = max(total_add / 8, 0) + adjust_holiday_days
record.done_working_overtime = total_add1 / 8
record.statutory_leave = legal_total/8
record.cop_leave = total_add2 / 8
@api.depends('meal_fee_ids.update_lines', 'meal_fee_ids.update_type', 'meal_fee_ids.is_void')
def _compute_meal_fee(self):
for record in self:
meal_total = 0
for line in record.meal_fee_ids:
if not line.is_void:
if line.update_type == 'add':
meal_total += int(line.update_lines or 0)
elif line.update_type == 'reduce':
meal_total -= int(line.update_lines or 0)
record.meal_fee = meal_total
def all_confirm(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
vacation_list = []
for rec in vacation_ids:
vacation_list.append(rec.id)
return {
'name': '批量确认弹窗',
'type': 'ir.actions.act_window',
'res_model': 'vacation.confirm.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_vacation_ids': [(6, 0, vacation_list)]},
}
def all_carryover(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
vacation_list = []
for rec in vacation_ids:
vacation_list.append(rec.id)
return {
'name': '加班结转',
'type': 'ir.actions.act_window',
'res_model': 'pay.carryover.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_vacation_ids': [(6, 0, vacation_list)]},
}
# 自动确认考勤台账(上个月)
def automatic_confirmation(self):
current_date = fields.Date.today() - relativedelta(months=1)
vacation_ids = self.env["yuthon.vacation.reports"].search([
('vacation_date', '=', current_date.replace(day=1)),
('vacation_state', '!=', 'confirmed')
])
for rec in vacation_ids:
month_start = current_date.replace(day=1)
month_end = (month_start + relativedelta(months=1) - relativedelta(days=1))
attendance_exception = self.env['hr.attendance'].search([
('employee_id', '=', rec.employee_id.id),
('current_data', '>=', month_start),
('current_data', '<=', month_end),
('is_abnormal', '=', True)
], limit=1)
# 如果考勤异常,则不自动确认
if not attendance_exception:
rec.manage_reject()
def monthly_report(self):
today = fields.Date.today()
employees = self.env['hr.employee'].search([('contract_id.state', '=', 'open'), ('name', '!=', 'Administrator')])
current_month_first = today.replace(day=1)
for employee in employees:
resource_user_ids = employee.resource_calendar_id.resource_user_ids.ids
is_resource_user = employee.id in resource_user_ids
existing_record = self.search([
('employee_id', '=', employee.id),
('vacation_date', '=', current_month_first)
], limit=1)
if not existing_record and not is_resource_user:
report_id = self.create({
'employee_id': employee.id,
'vacation_date': current_month_first,
})
report_id.manage_reject()
report_id.vacation_state = 'not_sent'
def _update_leave_lines(self, report, allocations, number_of_hours_display):
for alloc in allocations:
existing_line = self.env['vacation.leave.line'].search([
('vacation_id', '=', report.id),
('employee_id', '=', report.employee_id.id),
'|',
('text', '=', '上月额度继承'),
('text', '=', '年假分配')
], limit=1)
if existing_line:
continue
self.env['vacation.leave.line'].create({
'vacation_id': report.id,
'employee_id': report.employee_id.id,
'update_type': 'add',
'update_date': fields.Date.today(),
'update_lines': number_of_hours_display,
'is_void': False,
'allocation_type': 'allocation',
'text': f"上月额度继承"
})
def _update_over_lines(self, report, allocations, number_of_hours_display):
"""更新补休明细行"""
for alloc in allocations:
existing_line = self.env['vacation.over.line'].search([
('vacation_id', '=', report.id),
('employee_id', '=', report.employee_id.id),
('text', '=', '上月额度继承'),
], limit=1)
if existing_line:
continue
self.env['vacation.over.line'].create({
'vacation_id': report.id,
'employee_id': report.employee_id.id,
'update_type': 'add',
'update_date': fields.Date.today(),
'workday': number_of_hours_display,
'legal_day': 0,
'public_holidays': 0,
'meal_number': 0,
'text': f"上月额度继承"
})
def log(self):
return {
"type": "ir.actions.act_window",
"res_model": 'yuthon.vacation.reports',
"res_id": self.id,
"name": "详情",
"view_mode": "form",
}
class VacationLeaveLine(models.Model):
_name = 'vacation.leave.line'
_description = '年假明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工')
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
update_date = fields.Date(string='更新时间')
update_lines = fields.Float(string='更新额度')
is_void = fields.Boolean(string='是否作废')
allocation_type = fields.Selection([('vacation', '休假'), ('allocation', '分配')], string='分配类型')
valid_date = fields.Date(string='有效期至')
adjust_date = fields.Date(string='调整至')
text = fields.Text(string='备注')
leave_date = fields.Date(string='请假日期')
is_confirm = fields.Boolean(string="是否确认")
def open_record(self):
if self.workflow_record_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_record_id.res_model}',
'res_id': self.workflow_record_id.res_id,
'view_mode': 'form',
}
def action_void(self):
self.is_void = True
@api.model
def create(self, vals):
if vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
if vacation_main.vacation_state == 'confirmed':
vals['is_confirm'] = True
if vals.get('update_type') == 'reduce' and vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
deduct_amount = vals.get('update_lines', 0.0) / 8
if vacation_main:
used_this_year_new = vacation_main.used_this_year + deduct_amount
vacation_main.write({'used_this_year': used_this_year_new})
#处理跨年场景:查找下月1号的假期记录并扣减previous_year
if vacation_main.vacation_date:
current_vacation_date = vacation_main.vacation_date
next_month_first_day = current_vacation_date + relativedelta(months=+1, day=1)
# 判断是否跨年
is_cross_year = current_vacation_date.year != next_month_first_day.year
if is_cross_year:
next_month_vacation = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', vacation_main.employee_id.id),
('vacation_date', '=', next_month_first_day),
('vacation_state', 'in', ['not_sent', 'pending']),
], limit=1)
if next_month_vacation:
previous_year_new = next_month_vacation.previous_year- deduct_amount
previous_year_new = max(previous_year_new, 0.0)
next_month_vacation.write({'previous_year': previous_year_new})
return super(VacationLeaveLine, self).create(vals)
def _sync_to_employee(self, reverse=False):
"""将年假数据同步到关联员工"
:param reverse: 是否反向操作(用于删除场景)
"""
sign = -1 if reverse else 1
employee = self.vacation_id.employee_id
if self.allocation_type == 'vacation':
employee.write({
'user_year': employee.user_year + self.update_lines * sign,
})
else:
employee.write({
'quota_issued': employee.quota_issued + self.update_lines * sign,
})
def unlink(self):
"""删除记录时同步扣减员工加班时长"""
self._sync_to_employee(reverse=True)
return super(VacationLeaveLine, self).unlink()
class VacationOverline(models.Model):
_name = 'vacation.over.line'
_description = '加班明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
department_id = fields.Many2one('hr.department', related='employee_id.department_id', string="部门")
company_id = fields.Many2one('res.company', related='employee_id.company_id', string="公司")
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
is_visible = fields.Boolean(string='是否隐藏')
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
workday = fields.Float(string='工作日')
last_balance = fields.Float(string='上年结余', compute='_compute_last_balance', store=True)
legal_day = fields.Float(string='法定节假日')
public_holidays = fields.Float(string='公休日')
meal_number = fields.Integer(string='误餐次数')
update_date = fields.Date(string='更新时间')
is_void = fields.Boolean(string='是否作废')
valid_date = fields.Date(string='有效期至')
adjust_date = fields.Date(string='调整至')
leave_date = fields.Date(string='请假日期')
overtime_methods = fields.Selection([('leave', '补休'), ('fee', '加班费结算')], string='加班补偿方式', default='leave')
text = fields.Text(string='备注')
is_confirm = fields.Boolean(string="是否确认")
is_initial_number = fields.Boolean(string="初始额度")
overtime_name = fields.Char(string="标题", compute="_compute_overtime", store=True)
start_time = fields.Datetime(string="开始时间", compute="_compute_overtime", store=True)
end_time = fields.Datetime(string="结束时间", compute="_compute_overtime", store=True)
carry_forward_date = fields.Date(string="结转日期")
duration = fields.Float(string="时长", compute="_compute_overtime", store=True)
carry_forward_quantity = fields.Float(string="结转时长")
validity = fields.Char(string="有效期")
carry_result = fields.Selection([('success', '成功'), ('failed', '失败')], string="结转结果")
is_filing = fields.Boolean(string="归档")
carry_forward_state = fields.Selection([('pending', '待结转'),('completed', '已结转'), ('partial', '部分结转')
],string='结转状态',
default='pending',
)
remaining_quantity = fields.Float(string='剩余数', compute="_compute_remaining_quantity", store=True)
@api.model
def confirm(self):
domain = [
'|',
['is_initial_number', '=', True],
'&',
['is_initial_number', '=', False],
'&',
['update_type', '=', 'add'],
'&',
['workflow_record_id', '!=', False],
'!',
'&',
['start_time', '>=', '2025-12-01 00:00:00'],
['start_time', '<', '2026-01-01 00:00:00']
]
filtered_records = self.search(domain)
if not filtered_records:
return
records_to_update = filtered_records.filtered(
lambda r: fields.float_compare(r.duration, r.carry_forward_quantity) == 0
)
if records_to_update:
records_to_update.write({'is_filing': True})
def update_is_visible(self):
over_ids = self.env["vacation.over.line"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in over_ids:
rec.is_visible = True
def update_line(self):
over_ids = self.env["vacation.over.line"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in over_ids:
rec._compute_remaining_quantity()
rec._compute_overtime()
rec._compute_employee_id()
rec._compute_last_balance()
@api.depends('duration', 'carry_forward_quantity')
def _compute_remaining_quantity(self):
for rec in self:
rec.remaining_quantity = rec.duration - rec.carry_forward_quantity
@api.depends('vacation_id')
def _compute_last_balance(self):
for rec in self:
if rec.vacation_id:
rec.last_balance = rec.vacation_id.last_holiday + rec.vacation_id.last_workday
else:
rec.last_balance = 0
@api.depends('vacation_id')
def _compute_employee_id(self):
for rec in self:
if rec.vacation_id:
rec.employee_id = rec.vacation_id.employee_id.id
elif not rec.employee_id:
rec.employee_id = False
@api.depends('workflow_record_id', 'is_initial_number', 'workday')
def _compute_overtime(self):
for rec in self:
rec.overtime_name = ''
rec.start_time = False
rec.end_time = False
rec.duration = 0
if rec.is_initial_number:
rec.duration = rec.workday or 0
rec.overtime_name = '2025年12月结余初始数'
rec.start_time = datetime(2025, 12, 1, 1, 0, 0)
rec.end_time = datetime(2025, 12, 1, 9, 0, 0)
if rec.vacation_id and rec.vacation_id.valid_date1:
current_date = fields.Date.today()
if current_date > rec.vacation_id.valid_date1:
rec._compute_last_balance()
rec.duration = max(0, rec.duration - rec.last_balance)
elif rec.workflow_record_id and rec.update_type == 'add':
res_model = rec.workflow_record_id.res_model
res_id = rec.workflow_record_id.res_id
if res_model and res_id and res_model in self.env:
overtime_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
if overtime_id:
rec.overtime_name = overtime_id.name or ''
rec.start_time = overtime_id.start_time or ''
rec.end_time = overtime_id.end_time or ''
rec.duration = overtime_id.duration or 0
def action_void(self):
self.is_void = True
def open_record(self):
if self.workflow_record_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_record_id.res_model}',
'res_id': self.workflow_record_id.res_id,
'view_mode': 'form',
}
@api.model
def create(self, vals):
if vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
if vacation_main.vacation_state == 'confirmed':
vals['is_confirm'] = True
if vals.get('update_type') == 'reduce' and vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
current_vacation_date = vacation_main.vacation_date
next_month_first_day = current_vacation_date + relativedelta(months=+1, day=1)
deduct_workday = vals.get('workday', 0.0)
deduct_public = vals.get('public_holidays', 0.0)
deduct_workday_convert = deduct_workday / 8
deduct_public_convert = deduct_public / 8
deduct_total = deduct_workday_convert + deduct_public_convert
if vacation_main:
vacation_used_vals = {
'used_this_year1': (vacation_main.used_this_year1 or 0.0) + deduct_total
}
vacation_main.write(vacation_used_vals)
is_cross_year = False
# 计算下月1号并判断是否跨年
if vacation_main.vacation_date:
is_cross_year = current_vacation_date.year != next_month_first_day.year
# 跨年场景
if is_cross_year:
# 查找下月1号的未确认假期记录并扣减上年额度
next_month_vacation = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', vacation_main.employee_id.id),
('vacation_date', '=', next_month_first_day),
('vacation_state', 'in', ['not_sent', 'pending']),
], limit=1)
if next_month_vacation:
next_month_deduct_vals = {
'last_workday': next_month_vacation.last_workday - deduct_workday_convert,
'last_holiday': next_month_vacation.last_holiday - deduct_public_convert,
}
next_month_vacation.write(next_month_deduct_vals)
if vals.get('update_type') == 'add' and vals.get('vacation_id'):
current_text = vals.get('text', '').strip()
if current_text != '上月额度继承':
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
current_vacation_date = vacation_main.vacation_date
next_month_first_day = current_vacation_date + relativedelta(months=+1, day=1)
add_workday = vals.get('workday', 0.0)
add_public = vals.get('public_holidays', 0.0)
next_month_vacation = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', vacation_main.employee_id.id),
('vacation_date', '=', next_month_first_day),
('vacation_state', 'in', ['not_sent', 'pending']),
], limit=1)
if vacation_main:
vacation_add_vals = {
'year_workday': vacation_main.year_workday + add_workday / 8,
'year_holiday': vacation_main.year_holiday + add_public / 8,
}
vacation_main.write(vacation_add_vals)
next_month_vacation = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', vacation_main.employee_id.id),
('vacation_date', '=', next_month_first_day),
('vacation_state', 'in', ['not_sent', 'pending']),
], limit=1)
if next_month_vacation:
total_add_days = add_workday + add_public
inherit_vacation = self.env['vacation.over.line'].search([
('vacation_id', '=', next_month_vacation.id),
('text', '=', '上月额度继承'),
], limit=1)
if inherit_vacation:
inherit_vacation.write({
'year_workday': next_month_vacation.year_workday + add_workday / 8,
'year_holiday': next_month_vacation.year_holiday + add_public / 8,
'workday': inherit_vacation.workday + total_add_days
})
if vacation_main.vacation_date:
current_vacation_date = vacation_main.vacation_date
next_month_first_day = current_vacation_date + relativedelta(months=+1, day=1)
# 必须是跨年才写入上年工作日
is_cross_year = current_vacation_date.year != next_month_first_day.year
if is_cross_year:
next_month_deduct_vals = {
'last_workday': next_month_vacation.last_workday + add_workday / 8,
'last_holiday': next_month_vacation.last_holiday + add_public / 8,
}
next_month_vacation.write(next_month_deduct_vals)
next_month_vacation.previous_year = next_month_vacation.last_workday/8 + next_month_vacation.last_holiday / 8,
return super(VacationOverline, self).create(vals)
# def unlink(self):
# """删除记录时同步扣减员工加班时长"""
# self._sync_to_employee(reverse=True)
# return super(VacationOverline, self).unlink()
class VacationMealLine(models.Model):
_name = 'vacation.meal.line'
_description = '误餐明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
is_filing = fields.Boolean(string="归档")
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
department_id = fields.Many2one('hr.department', related='employee_id.department_id', string="部门", store=True)
company_id = fields.Many2one('res.company', related='employee_id.company_id', string="公司", store=True)
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
vacation_date = fields.Date(related='vacation_id.vacation_date', string='误餐日期')
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
meal_name = fields.Char(string="标题", compute="_compute_overtime", store=True)
start_date = fields.Date(string="日期", compute="_compute_overtime", store=True)
update_date = fields.Date(string='更新时间')
update_lines = fields.Float(string='更新额度')
is_soon = fields.Boolean(string="流程修改")
is_void = fields.Boolean(string='是否作废', default=False)
valid_date = fields.Date(string='有效期至')
adjust_date = fields.Date(string='调整至')
is_manual = fields.Boolean(string="手动修改")
text = fields.Text(string='备注')
is_confirm = fields.Boolean(string="是否确认")
def update_line(self):
meal_ids = self.env["vacation.meal.line"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in meal_ids:
rec._compute_employee_id()
rec._compute_overtime()
@api.model
def confirm(self):
now = datetime.now()
current_month = now.month
current_year = now.year
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 = datetime(last_month_year, last_month, 1)
current_month_first_day = datetime(current_year, current_month, 1)
valid_leaves = self.search([
('start_date', '>=', fields.Datetime.to_string(last_month_first_day)),
('start_date', '<', fields.Datetime.to_string(current_month_first_day)),
('id', 'in', self.ids)
])
if valid_leaves:
valid_leaves.write({'is_filing': True})
@api.depends('workflow_record_id')
def _compute_overtime(self):
for rec in self:
rec.start_date = False
res_model = rec.workflow_record_id.res_model
res_id = rec.workflow_record_id.res_id
if res_model and res_id and res_model in self.env:
if res_model == 'overtime.approval':
overtime_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
if overtime_id:
rec.start_date = overtime_id.start_time.date() or ''
else:
go_approval_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
if go_approval_id:
rec.start_date = go_approval_id.date_start.date() or ''
rec.meal_name = rec.workflow_record_id.name or ''
@api.depends('vacation_id')
def _compute_employee_id(self):
for rec in self:
if rec.vacation_id:
rec.employee_id = rec.vacation_id.employee_id.id
elif not rec.employee_id:
rec.employee_id = False
def action_void(self):
self.is_void = True
def open_record(self):
if self.workflow_record_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_record_id.res_model}',
'res_id': self.workflow_record_id.res_id,
'view_mode': 'form',
}
def create(self, vals):
vals = vals[0]
if vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
if vacation_main.vacation_state == 'confirmed':
vals['is_confirm'] = True
record = super(VacationMealLine, self).create(vals)
record._sync_to_employee()
return record
def _sync_to_employee(self, reverse=False):
"""将数据同步到关联员工
:param reverse: 是否反向操作(用于删除场景)
"""
for record in self:
employee = record.vacation_id.employee_id
if not employee:
continue
# 根据操作类型和反向标志计算餐补变化量
sign = -1 if reverse else 1
delta = int(record.update_lines) * sign
if record.update_type == 'add':
employee.write({'meal_number': employee.meal_number + delta})
elif record.update_type == 'reduce':
employee.write({'meal_number': employee.meal_number - delta})
def unlink(self):
self._sync_to_employee(reverse=True)
return super(VacationMealLine, self).unlink()
class VacationMedicalLine(models.Model):
_name = 'vacation.medical.line'
_description = '体检费明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工')
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
update_type = fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
update_date = fields.Date(string='更新时间')
valid_date = fields.Date(string='有效期至')
adjust_date = fields.Date(string='调整至')
lines = fields.Float(string='额度')
is_void = fields.Boolean(string='是否作废', default=False)
text = fields.Text(string='备注')
is_confirm = fields.Boolean(string="是否确认")
def action_void(self):
self.is_void = True
def open_record(self):
if self.workflow_record_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_record_id.res_model}',
'res_id': self.workflow_record_id.res_id,
'view_mode': 'form',
}
@api.model
def create(self, vals):
# 调用父类方法创建记录
if vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
if vacation_main.vacation_state == 'confirmed':
vals['is_confirm'] = True
record = super(VacationMedicalLine, self).create(vals)
if record.employee_id and record.valid_date:
employee = record.employee_id
valid_date = record.valid_date
medical_rule = self.env['yuthon.medical.rule'].search([
('company_id', '=', employee.company_id.id),
('start_date', '<=', valid_date),
('end_date', '>=', valid_date),
('is_enable', '=', True)
], limit=1)
age_diff = max(0, employee.old - medical_rule.starting_age)
increase_times = age_diff // medical_rule.increase_interval_years
base_amount = medical_rule.base_amount
if employee.gender == 'female':
# 将离异、丧偶、合法同居视为未婚
if employee.marital in ['single', 'divorced', 'widower', 'cohabitant']:
base_amount += medical_rule.allowance_unmarried_women
else:
base_amount += medical_rule.allowance_unmarried_women + medical_rule.allowance_married_women
final_amount = base_amount + (increase_times * medical_rule.increment_amount)
final_amount = min(final_amount, medical_rule.cap)
record.write({'lines': final_amount})
return record
def create_leave_record(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)
])
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
class VacationOtherLine(models.Model):
_name = 'vacation.other.line'
_description = '其他'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
holiday_type = fields.Many2one('hr.leave.type', string="假期类型")
employee_id = fields.Many2one('hr.employee', string='员工')
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
update_date = fields.Date(string='更新时间')
update_type = fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
leave_date = fields.Date(string='请假日期')
is_void = fields.Boolean(string='是否作废', default=False)
lines = fields.Float(string='天数')
text = fields.Text(string='备注')
is_confirm = fields.Boolean(string="是否确认")
def action_void(self):
self.is_void = True
@api.model
def create(self, vals):
if vals.get('vacation_id'):
vacation_main = self.env['yuthon.vacation.reports'].browse(vals['vacation_id'])
if vacation_main.vacation_state == 'confirmed':
vals['is_confirm'] = True
return super(VacationOtherLine, self).create(vals)
def open_record(self):
if self.workflow_record_id:
return {
"name": "流程详情",
'type': 'ir.actions.act_window',
'res_model': f'{self.workflow_record_id.res_model}',
'res_id': self.workflow_record_id.res_id,
'view_mode': 'form',
}
class AttendanceMessageLog(models.Model):
_name = 'attendance.message.log'
_description = '消息提醒日志'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工')
department_id = fields.Many2one(related='employee_id.department_id', string="部门", index=True)
company_id = fields.Many2one(related='employee_id.company_id', string="公司")
service_address = fields.Char(string='服务地址')
update_date = fields.Datetime(string='提醒时间')
shift_id = fields.Many2one('planning.slot', string="班次", compute="_compute_shift_id", store=True)
content = fields.Text(string='内容')
notice_type = fields.Selection([('attendance', '出勤'), ('schedule', '日程'), ('announcement', '公告'), ('attendance_confirmation', '考勤确认单'), ('salary_slip', '薪资条'),
('process', '流程'), ('other', '其他')], string='类型')
message_url = fields.Char(string="url")
@api.depends('employee_id')
def _compute_shift_id(self):
for rec in self:
att_id = rec.env['hr.attendance'].search([('employee_id', '=', rec.employee_id.id),
('current_data', '=', rec.update_date.date())
], limit=1)
if att_id:
rec.shift_id = att_id.shift_id.id
else:
rec.shift_id = False
class DetailsTree(models.Model):
_name = 'details.tree'
_description = '明细表'
code = fields.Char()
name = fields.Char()