Files
gzth/yuthon_hr_attendance/models/yuthon_vacation_reports.py
T
2026-04-23 11:41:31 +08:00

3289 lines
156 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 datetime import datetime
import json
from odoo.tools import date_utils
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"
month_id = fields.Many2one('base.month', string='月份', compute="_compute_display_date", store=True)
year_id = fields.Many2one('base.year', string='年份', compute="_compute_display_date", store=True)
employee_id = fields.Many2one('hr.employee', string='员工')
employee_number = fields.Char(string='序号', related='employee_id.number', store=True, readonly=True)
entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", store=True)
number_card = fields.Integer(string='补卡次数', compute="_compute_number_card")
remaining_number_card = fields.Integer(string='剩余补卡次数', default=3, compute="_compute_number_card", store=True)
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)) # 固定值
dynamic_previous_year = fields.Float(string="上年年假剩余d", digits=(16, 3)) # 动态值
dynamic_remainder_year = fields.Float(string="本年年假剩余d", digits=(16, 3)) # 动态值
valid_date = fields.Date(string="有效期至")
year_distribution = fields.Float(string="本年发放d", digits=(16, 3))
used_this_year = fields.Float(string="本年已用d", digits=(16, 3))
remainder_year = fields.Float(string="剩余(年假)d", digits=(16, 3), compute="_compute_remainder_year")
last_holiday = fields.Float(string="上年公休日d", digits=(16, 3)) # 固定值
last_workday = fields.Float(string="上年工作日d", digits=(16, 3)) # 固定值
valid_date1 = fields.Date(string="有效期至")
year_holiday = fields.Float(string="本年公休日d", digits=(16, 3))
year_workday = fields.Float(string="本年工作日d", digits=(16, 3))
year_sum = fields.Float(string="本年调休合计d", digits=(16, 3), compute="_compute_year_sum")
previous_year1 = fields.Float(string="上年剩余(补休)d", digits=(16, 3)) # 固定值
dynamic_previous_year1 = fields.Float(string="上年补休剩余d", digits=(16, 3)) # 动态值
this_year1 = fields.Float(string="本年补休剩余d", digits=(16, 3)) # 动态值
dynamic_previous_year1_hour = fields.Float(
string="上年补休剩余h",
digits=(16, 1),
compute='_compute_hour_fields',
)
this_year1_hour = fields.Float(
string="本年补休剩余h",
digits=(16, 1),
compute='_compute_hour_fields',
)
used_this_year1 = fields.Float(string="本年已休d", digits=(16, 3))
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='加班明细')
# 加班明细合计字段
overtime_workday_total = fields.Float(string='工作日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_legal_day_total = fields.Float(string='法定节假日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_public_holidays_total = fields.Float(string='公休日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_grand_total = fields.Float(string='总合计', compute='_compute_overtime_totals', digits=(16, 1))
meal_fee_ids = fields.One2many('vacation.meal.line', 'vacation_id', string='误餐明细')
medical_fee_ids = fields.One2many('vacation.medical.line', 'vacation_id', string='体检费明细')
parenting_line_ids = fields.One2many('vacation.parenting.line', 'vacation_id', string='育儿假明细')
other_fee_ids = fields.One2many('vacation.other.line', 'vacation_id', string='其他明细')
duty_ids = fields.One2many('duty.adjustment.schedule', 'vacation_id', string='值班调休明细', compute='_compute_duty_ids')
vacation_date = fields.Date(string='日期', default=lambda self: fields.Date.today().replace(day=1))
show_date = fields.Date(string='日期', compute='_compute_display_date', store=True)
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='剩余年假d', compute='_compute_leave', store=True, digits=(16, 3), group_operator=None)
working_overtime = fields.Float(string='剩余补休d', compute='_compute_working_overtime', store=True,
digits=(16, 3), group_operator=None)
remain_missed_meals = fields.Float(string="剩余误餐次数", compute='_compute_meal_fee', store=True, group_operator=None)
medical_fee = fields.Float(string="体检费", compute='_compute_medical_fee', store=True, group_operator=None)
duty = fields.Float(string="值班", compute='_compute_duty_fee', store=True, group_operator=None)
duty_off_leave = fields.Float(string='值班调休', compute='_compute_duty_fee', store=True, group_operator=None)
compensatory_leave = fields.Float(string="调休d", compute='_compute_other_leave', store=True, group_operator=None)
leave_of_absence = fields.Float(string="事假d", digits=(16, 3), compute='_compute_other_leave', store=True,
group_operator=None)
statutory_leave = fields.Float(string="加班时(法定)d", compute='_compute_other_leave', store=True, group_operator=None, digits=(16, 3))
cop_leave = fields.Float(string='补休d', digits=(16, 3), store=True, compute='_compute_working_overtime',
group_operator=None)
marriage_leave = fields.Float(string='婚假d', compute='_compute_other_leave', store=True, group_operator=None)
maternity_leave = fields.Float(string='产假d', compute='_compute_other_leave', store=True, group_operator=None)
paternity_leave = fields.Float(string='陪产假d', compute='_compute_other_leave', store=True, group_operator=None)
family_planning_leave = fields.Float(string='计生假d', compute='_compute_other_leave', store=True, group_operator=None)
bereavement_leave = fields.Float(string='丧假d', compute='_compute_other_leave', store=True, group_operator=None)
home_visit_leave = fields.Float(string='探亲假d', compute='_compute_other_leave', store=True, group_operator=None)
parents_meeting_leave = fields.Float(string='家长会假d', compute='_compute_other_leave', store=True, group_operator=None)
general_sick_leave = fields.Float(string='病假(普通)d', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
hospital_sick_leave = fields.Float(string='病假(住院期)d', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
recovery_sick_leave = fields.Float(string='病假(出院恢复期)d', digits=(16, 3), compute='_compute_other_leave', store=True, group_operator=None)
parental_leave = fields.Float(string='育儿假d', digits=(16, 3), compute='_compute_parental_leave', store=True, group_operator=None)
remaining_parental_leave = fields.Float(string='剩余育儿假d', digits=(16, 3), compute='_compute_parental_leave', store=True, group_operator=None)
exam_leave = fields.Integer(string='考试假d', 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)
expatriation = fields.Float(string='外派', compute='compute_attendance_fields', store=True, group_operator=None)
go_out = 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')
@api.depends('dynamic_previous_year1', 'this_year1')
def _compute_hour_fields(self):
for record in self:
record.dynamic_previous_year1_hour = record.dynamic_previous_year1 * 8
record.this_year1_hour = record.this_year1 * 8
@api.depends('employee_id', 'vacation_date')
def _compute_duty_ids(self):
duty_schedule_env = self.env['duty.adjustment.schedule']
planning_slot_env = self.env['planning.slot']
# 新增:客服值班计划模型
customer_planning_env = self.env['yuthon.customer.planning']
for record in self:
if not record.employee_id or not record.vacation_date:
record.duty_ids = False
continue
emp_id = record.employee_id.id
year = record.vacation_date.year
month = record.vacation_date.month
month_start = date(year, month, 1)
last_day = calendar.monthrange(year, month)[1]
month_end = date(year, month, last_day)
# ====================== 原有逻辑 ======================
# 1. 删除非调休的 duty 记录
all_duty_records = duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])
non_adjusted_duties = all_duty_records.filtered(lambda d: not d.is_adjustment)
if non_adjusted_duties:
non_adjusted_duties.unlink()
# 2. 从 planning.slot 白值生成值班记录
white_duty_slots = planning_slot_env.search([
('employee_id', '=', emp_id),
('planning_name', '=', '白值'),
('start_datetime', '>=', datetime.combine(month_start, datetime.min.time())),
('start_datetime', '<=', datetime.combine(month_end, datetime.max.time()))
])
# 已存在的日期
existing_duty_dates = {d.duty_date for d in duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])}
create_vals = []
for slot in white_duty_slots:
if not slot.start_datetime:
continue
duty_date = slot.start_datetime.date()
if duty_date in existing_duty_dates:
continue
create_vals.append({
'employee_id': emp_id,
'vacation_id': record.id,
'duty_date': duty_date,
'is_adjustment': False
})
# 从客服值班计划取日期
customer_planning_records = customer_planning_env.search([
('date', '>=', month_start),
('date', '<=', month_end),
('employee_ids', 'in', emp_id),
])
for plan in customer_planning_records:
duty_date = plan.date
if duty_date in existing_duty_dates:
continue
create_vals.append({
'employee_id': emp_id,
'vacation_id': record.id,
'duty_date': duty_date,
'is_adjustment': False
})
if create_vals:
duty_schedule_env.create(create_vals)
record.duty_ids = duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])
@api.depends('duty_ids.is_adjustment')
def _compute_duty_fee(self):
for record in self:
total_duty_count = len(record.duty_ids)
duty_off_count = len([line for line in record.duty_ids if line.is_adjustment])
record.duty_off_leave = duty_off_count
record.duty = max(total_duty_count - duty_off_count, 0)
@api.depends('vacation_date', 'employee_id')
def _compute_number_card(self):
for record in self:
count = 0
if not record.employee_id or not record.vacation_date:
record.number_card = count
continue
target_date = record.vacation_date
month_start = target_date.replace(day=1)
month_end = month_start + relativedelta(months=1)
attendance_model = self.env['hr.attendance']
employee_attendances = attendance_model.search([
('employee_id', '=', record.employee_id.id),
('check_in', '>=', month_start),
('check_in', '<', month_end),
('workflow_id', '!=', False)
])
if employee_attendances:
count = len([
att for att in employee_attendances
if att.workflow_id.res_model == 'card.approval'
])
record.number_card = count
record.remaining_number_card = 3 - record.number_card
@api.model
def cleanup_march_2026(self):
reports_ids = self.env['yuthon.vacation.reports'].search([('display_date', '=', '2026年3月')])
# 按员工分组
employee_groups = {}
for report in reports_ids:
if report.employee_id.id not in employee_groups:
employee_groups[report.employee_id.id] = []
employee_groups[report.employee_id.id].append(report.id)
# 对每个员工,保留第一条,删除其他
to_delete = []
for employee_id, report_list in employee_groups.items():
if len(report_list) > 1:
# 保留第一个,删除后面的
to_delete.extend(report_list[1:])
@api.model
def leave_line(self):
today = date.today()
start_date = date(today.year, today.month, 1)
next_month = today.month + 1 if today.month < 12 else 1
next_year = today.year if today.month < 12 else today.year + 1
end_date = date(next_year, next_month, 1) - timedelta(days=1)
return {
'type': 'ir.actions.act_window',
'name': '请假情况',
"views": [(False, "tree")],
'res_model': 'hr.leave',
'domain': [
('state', '=', 'validate'),
('is_filing', '=', False),
('company_id', 'in', self.env.companies.ids)
],
'target': 'current',
'context': {'tree_view_ref': "yuthon_hr_attendance.view_hr_leave_statement_tree",
'search_view_ref': "yuthon_hr_attendance.view_hr_leave_statement_search"}
}
def import_initial_data(self):
return {
'type': 'ir.actions.act_window',
'name': '导入初始数据',
'res_model': 'yuthon.vacation.import.wizard',
'view_mode': 'form',
'target': 'new',
}
def update_vacation_task(self):
today = date.today()
current_month_first = today.replace(day=1)
vacation_ids = self.search([('vacation_date', '=', current_month_first)])
for vacation in vacation_ids:
vacation.update_line()
def update_is_visible(self):
over_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in over_ids:
rec.previous_year = 0
rec.previous_year1 = 0
def all_update_line(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
for rec in records:
rec.update_line()
def update_line(self):
if self.vacation_date == fields.Date.from_string('2026-01-01'):
return
self.manage_reject()
# ========== 第一步:处理年假逻辑 ==========
for rec in self:
self_leave = rec.leave_ids.filtered(lambda o: not o.workflow_record_id and o.text == '上月额度继承')
self_leave.update_lines = (rec.dynamic_previous_year + rec.dynamic_remainder_year) * 8
self_overtime = rec.overtime_ids.filtered(lambda o: not o.workflow_record_id and o.text == '上月额度继承')
self_overtime.workday = (rec.dynamic_previous_year1 + rec.this_year1) * 8
self_meal = rec.meal_fee_ids.filtered(lambda o: not o.workflow_record_id and o.text == '上月额度继承')
self_meal.update_lines = rec.remain_missed_meals
add_overtime_lines = rec.overtime_ids.filtered(
lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
# 计算补休新增总天数
workday_overtime_add = sum((line.workday or 0) for line in add_overtime_lines) / 8
holidays_overtime_add = sum((line.public_holidays or 0) for line in add_overtime_lines) / 8
rec.this_year1 += workday_overtime_add + holidays_overtime_add
rec.year_workday += workday_overtime_add
rec.year_holiday += holidays_overtime_add
# 计算年假新增总天数
add_leave_lines = rec.leave_ids.filtered(
lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
dynamic_remainder_year = sum((line.update_lines or 0) for line in add_leave_lines) / 8
rec.dynamic_remainder_year += dynamic_remainder_year
valid_leave_lines = rec.leave_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'reduce' and l.is_void != True
)
total_leave_deduct = sum(line.update_lines for line in valid_leave_lines if line.update_lines > 0)/8
remaining_deduct = total_leave_deduct
# 1. 先扣减上年年假剩余(dynamic_previous_year)
if rec.dynamic_previous_year > 0:
deduct_from_previous = min(rec.dynamic_previous_year, remaining_deduct)
rec.dynamic_previous_year -= deduct_from_previous
rec.used_this_year += deduct_from_previous # 累加到本年已用
remaining_deduct -= deduct_from_previous
# 2. 剩余部分扣减本年年假剩余(dynamic_remainder_year)
if remaining_deduct > 0 and rec.dynamic_remainder_year > 0:
deduct_from_current = min(rec.dynamic_remainder_year, remaining_deduct)
rec.dynamic_remainder_year -= deduct_from_current
rec.used_this_year += deduct_from_current # 累加到本年已用
remaining_deduct -= deduct_from_current
# ========== 第二步:处理补休逻辑 ==========
for rec in self:
reduce_overtime_lines = rec.overtime_ids.filtered(
lambda o: o.update_type == 'reduce' and (o.workflow_record_id or o.overtime_methods == 'fee') and o.is_void != True
)
# 计算补休扣减总天数
total_overtime_deduct = sum(
(line.workday or 0) + (line.public_holidays or 0)
for line in reduce_overtime_lines
if (line.workday or 0) + (line.public_holidays or 0) > 0
)/8
if total_overtime_deduct > 0:
remaining_overtime_deduct = total_overtime_deduct
# 1. 先扣减上年补休剩余(dynamic_previous_year1)
if rec.dynamic_previous_year1 > 0:
deduct_from_previous1 = min(rec.dynamic_previous_year1, remaining_overtime_deduct)
rec.dynamic_previous_year1 -= deduct_from_previous1
rec.used_this_year1 += deduct_from_previous1 # 累加到本年已休
remaining_overtime_deduct -= deduct_from_previous1
# 2. 剩余部分扣减本年补休剩余(this_year1)
if remaining_overtime_deduct > 0 and rec.this_year1 > 0:
deduct_from_current1 = min(rec.this_year1, remaining_overtime_deduct)
rec.this_year1 -= deduct_from_current1
rec.used_this_year1 += deduct_from_current1 # 累加到本年已休
remaining_overtime_deduct -= deduct_from_current1
rec._compute_leave()
rec._compute_meal_fee()
rec._compute_working_overtime()
def update_vacation(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
for rec in records:
rec.update_vacation1()
def update_vacation1(self):
"""
只限每年的1月的考勤数据 !!!!!!!!!!!!!!!!!!!!!!!!!!
1. 年假逻辑:上年剩余=上年额度继承÷8 → 扣减减少类明细 → 本年剩余=年度分配+增加类明细-剩余扣减
2. 补休逻辑:上年剩余=上月额度继承÷8 → 本年剩余=增加类明细总和 → 扣减减少类(先扣上年,再扣本年)
3. 有效期:集团本部空/天河物管2月底/天河粮储(年假8底/补休6底)
"""
for rec in self:
if rec.vacation_date != date(date.today().year , 1, 1):
raise UserError(f"该更新适用于每年1月考勤数据更新")
# 修正年假明细leave_ids中的text
leave_lines_with_last_month = rec.leave_ids.filtered(lambda l: '上月' in (l.text or ''))
if leave_lines_with_last_month:
leave_lines_with_last_month.write({'text': '上月额度继承'})
# 修正补休明细overtime_ids中的text
over_lines_with_last_month = rec.overtime_ids.filtered(lambda l: '上月' in (l.text or ''))
if over_lines_with_last_month:
over_lines_with_last_month.write({'text': '上月额度继承'})
company_name = rec.employee_id.company_id.name if rec.employee_id else ""
current_year = date.today().year # 当前年份
next_year = current_year + 1 # 下一年
vacation_valid_date = None # 年假有效期
overtime_valid_date = None # 补休有效期
inherit_valid_date = None # 上月额度继承有效期
allocation_valid_date = None # 年假分配有效期
add_valid_date = None # 增加类明细有效期
# 1. 集团本部:所有有效期为空
if company_name == "集团本部":
vacation_valid_date = None
overtime_valid_date = None
inherit_valid_date = None
add_valid_date = None
allocation_valid_date = None
# 2. 天河物管:默认2月底
elif "天河物管" in company_name:
# 主表有效期:当年2月底
vacation_valid_date = date(current_year, 2, 28)
overtime_valid_date = date(current_year, 2, 28)
# 明细行:上月额度继承=当年2月底,年假分配/增加类=明年2月底
inherit_valid_date = date(current_year, 2, 28)
allocation_valid_date = date(next_year, 2, 28)
add_valid_date = date(next_year, 2, 28)
# 3. 天河粮储:年假8月底,补休6月底
elif "天河粮储" in company_name:
# 主表有效期:年假=当年8月底,补休=当年6月底
vacation_valid_date = date(current_year, 8, 31)
overtime_valid_date = date(current_year, 6, 30)
# 明细行:上月额度继承=对应主表有效期,年假分配/增加类=明年对应月底
inherit_valid_date = {
'leave': date(current_year, 8, 31), # 年假继承额度=8月底
'overtime': date(current_year, 6, 30) # 补休继承额度=6月底
}
allocation_valid_date = date(next_year, 8, 31) # 年假分配=明年8月底
add_valid_date = {
'leave': date(next_year, 8, 31), # 年假增加=明年8月底
'overtime': date(next_year, 6, 30) # 补休增加=明年6月底
}
# 更新主表有效期字段
rec.write({
'valid_date': vacation_valid_date,
'valid_date1': overtime_valid_date,
})
# ---------------------- 更新年假明细(leave_ids)有效期 ----------------------
for leave_line in rec.leave_ids:
line_valid_date = None
if company_name == "集团本部":
line_valid_date = None
elif "天河物管" in company_name:
if leave_line.text == "上月额度继承":
line_valid_date = inherit_valid_date # 当年2月底
elif leave_line.text == "年假分配":
line_valid_date = allocation_valid_date # 明年2月底
elif leave_line.update_type == "add": # 增加类
line_valid_date = add_valid_date # 明年2月底
elif "天河粮储" in company_name:
if leave_line.text == "上月额度继承":
line_valid_date = inherit_valid_date['leave'] # 当年8月底
elif leave_line.text == "年假分配":
line_valid_date = allocation_valid_date # 明年8月底
elif leave_line.update_type == "add":
line_valid_date = add_valid_date['leave'] # 明年8月底
leave_line.write({'valid_date': line_valid_date})
# ---------------------- 修正:补休明细(overtime_ids)公司名称判断(物管→天河物管,粮储→天河粮储) ----------------------
for over_line in rec.overtime_ids:
line_valid_date = None
if company_name == "集团本部":
line_valid_date = None
elif "天河物管" in company_name: # 修正:从"物管"改为"天河物管"
if over_line.text == "上月额度继承":
line_valid_date = inherit_valid_date
elif over_line.update_type == "add":
line_valid_date = add_valid_date
elif "天河粮储" in company_name: # 修正:从"粮储"改为"天河粮储"
if over_line.text == "上月额度继承":
line_valid_date = inherit_valid_date['overtime'] # 当年6月底
elif over_line.update_type == "add":
line_valid_date = add_valid_date['overtime'] # 明年6月底
over_line.write({'valid_date': line_valid_date})
rec.dynamic_previous_year = 0.0
rec.dynamic_remainder_year = 0.0
# 2. 取leave_ids中text="上年额度继承"的值 ÷8 作为上年剩余初始值
inherit_leave_lines = rec.leave_ids.filtered(lambda l: l.text == '上月额度继承')
if inherit_leave_lines:
inherit_update_lines = inherit_leave_lines.update_lines or 0.0
rec.dynamic_previous_year = inherit_update_lines / 8
rec.previous_year = inherit_update_lines / 8
# 3. 筛选leave_ids中workflow_record_id有值且update_type="减少"的明细,计算总减少额
leave_decrease_lines = rec.leave_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'reduce'
)
total_leave_decrease = sum(l.update_lines for l in leave_decrease_lines if l.update_lines) / 8
# 4. 优先扣减上年剩余,剩余扣减年度分配
remaining_decrease = total_leave_decrease
if rec.dynamic_previous_year > 0:
if rec.dynamic_previous_year >= remaining_decrease:
rec.dynamic_previous_year -= remaining_decrease
remaining_decrease = 0.0
else:
remaining_decrease -= rec.dynamic_previous_year
rec.dynamic_previous_year = 0.0
# 5. 计算本年剩余:年度分配 + 增加类明细 - 剩余扣减
# 5.1 取leave_ids中text="年假分配"的年度分配值
annual_allocation_lines = rec.leave_ids.filtered(lambda l: l.text == '年假分配')
total_annual_allocation = annual_allocation_lines.update_lines / 8 if annual_allocation_lines else 0.0
# 5.2 筛选leave_ids中workflow_record_id有值且update_type="增加"的明细,计算总增加额
leave_increase_lines = rec.leave_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'add'
)
total_leave_increase = sum(l.amount for l in leave_increase_lines if l.amount) / 8
# 5.3 本年剩余 = 年度分配 + 增加额 - 剩余扣减(兜底非负)
rec.dynamic_remainder_year = max(0.0, total_annual_allocation + total_leave_increase - remaining_decrease)
# ---------------------- 第二步:处理补休额度计算 ----------------------
rec.dynamic_previous_year1 = 0.0
rec.this_year1 = 0.0
inherit_overtime_lines = rec.overtime_ids.filtered(lambda l: l.text == '上月额度继承')
if inherit_overtime_lines:
inherit_over_update_lines = inherit_overtime_lines.workday + inherit_overtime_lines.public_holidays
rec.dynamic_previous_year1 = inherit_over_update_lines / 8 # 初始补休上年剩余
rec.last_workday = inherit_overtime_lines.workday / 8
rec.last_holiday = inherit_overtime_lines.public_holidays / 8
# 3. 筛选overtime_ids中workflow_record_id有值且update_type="增加"的明细,计算总增加额
over_increase_lines = rec.overtime_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'add'
)
total_over_increase = sum(l.workday + l.public_holidays for l in over_increase_lines if l.workday or l.public_holidays) / 8
rec.this_year1 = total_over_increase # 本年剩余初始值=增加总额
rec.year_workday = sum(l.workday for l in over_increase_lines if l.workday) / 8
rec.year_holiday = sum(l.public_holidays for l in over_increase_lines if l.public_holidays) / 8
# 4. 筛选overtime_ids中workflow_record_id有值且update_type="减少"的明细,计算总减少额
over_decrease_lines = rec.overtime_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'reduce'
)
rec.used_this_year1 = sum(l.workday + l.public_holidays for l in over_decrease_lines if l.workday or l.public_holidays) / 8
total_over_decrease = sum(l.workday + l.public_holidays for l in over_decrease_lines if l.workday or l.public_holidays) / 8
# 5. 扣减减少额:先扣补休上年剩余,再扣本年剩余(兜底非负)
remaining_over_decrease = total_over_decrease
if rec.dynamic_previous_year1 > 0:
if rec.dynamic_previous_year1 >= remaining_over_decrease:
rec.dynamic_previous_year1 -= remaining_over_decrease
remaining_over_decrease = 0.0
else:
remaining_over_decrease -= rec.dynamic_previous_year1
rec.dynamic_previous_year1 = 0.0
# 扣减本年剩余
rec.this_year1 = max(0.0, rec.this_year1 - remaining_over_decrease)
@api.depends('other_fee_ids', 'other_fee_ids.holiday_type', 'other_fee_ids.lines',
'other_fee_ids.is_void', 'other_fee_ids.update_type', 'parenting_line_ids',
'parenting_line_ids.update_date', 'parenting_line_ids.end_date', 'parenting_line_ids.lines')
def _compute_parental_leave(self):
today = date.today()
for rec in self:
used_amount = 0.0
deduct_fee_lines = rec.other_fee_ids.filtered(
lambda l:
l.holiday_type.name == '育儿假'
and l.update_type == 'reduce'
and not l.is_void
and l.lines > 0
)
if deduct_fee_lines:
used_amount = sum(line.lines for line in deduct_fee_lines)
rec.parental_leave = used_amount
total_amount = 0.0
valid_parenting_lines = rec.parenting_line_ids.filtered(
lambda l:
l.update_date <= today
and (l.end_date >= today if l.end_date else True)
)
if valid_parenting_lines:
total_amount = sum(line.lines for line in valid_parenting_lines)
rec.remaining_parental_leave = max(total_amount - used_amount, 0.0)
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,
'logging_data': 'r',
})
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('year_distribution', 'used_this_year')
def _compute_remainder_year(self):
for record in self:
record.remainder_year = record.year_distribution + record.previous_year - 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.previous_year1 = record.last_holiday + record.last_workday
@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_prev_month(self):
"""跳转到上一个月的假期记录"""
self.ensure_one()
prev_date = (self.vacation_date - relativedelta(months=1)).replace(day=1)
prev_record = self.search([
('employee_id', '=', self.employee_id.id),
('vacation_date', '=', prev_date),
], limit=1)
if not prev_record:
raise UserError('上一个月没有对应的假期记录')
action = self.env['ir.actions.actions']._for_xml_id('yuthon_hr_attendance.view_yuthon_vacation_reports_action')
action.update({
'res_id': prev_record.id,
'views': [(False, 'form')],
'view_mode': 'form',
'target': 'current',
})
return action
def action_next_month(self):
"""跳转到下一个月的假期记录"""
self.ensure_one()
next_date = (self.vacation_date + relativedelta(months=1)).replace(day=1)
next_record = self.search([
('employee_id', '=', self.employee_id.id),
('vacation_date', '=', next_date),
], limit=1)
if not next_record:
raise UserError('下一个月没有对应的假期记录')
action = self.env['ir.actions.actions']._for_xml_id('yuthon_hr_attendance.view_yuthon_vacation_reports_action')
action.update({
'res_id': next_record.id,
'views': [(False, 'form')],
'view_mode': 'form',
'target': 'current',
})
return action
def action_open(self):
self.ensure_one()
target_date = self.vacation_date.replace(day=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('有加班单未结束,请先结束加班批次')
def update_show_date(self):
records = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id', self.ids))
)
for record in records:
record._compute_display_date()
def update_working_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:
record.remaining_number_card = 4
@api.depends('vacation_date')
def _compute_display_date(self):
for rec in self:
rec.show_date = rec.vacation_date
rec.display_date = (rec.vacation_date).strftime('%Y年%m月')
month_id = rec.env['base.month'].search([('name', '=', int(rec.vacation_date.month))])
rec.month_id = month_id.id
year_id = rec.env['base.year'].search([('name', '=', int(rec.vacation_date.year))])
rec.year_id = year_id.id
@api.depends('employee_id', 'vacation_date')
def compute_attendance_fields(self):
for rec in self:
# 初始化基础变量
is_late = is_early = is_no = is_work = is_abnormal = 0
probation_days = regular_days = expatriation_days = 0
actual_attendance = leave_total_days = 0
is_retired = rec.employee_id.user_state in ('leave', 'withdraw')
# 核心:外出天数统计(按workflow_id去重)
go_out_days = 0.0 # 外出总天数
processed_workflow_ids = set() # 已处理的workflow_id(去重)
# 外派相关变量
is_outside = exp_has_return = False
exp_start_date = exp_end_return = exp_end_date = None
if rec.employee_id and rec.vacation_date and not is_retired:
# 考勤月份时间范围
year, month = rec.vacation_date.year, rec.vacation_date.month
month_start = date(year, month, 1)
month_end = date(year, month, calendar.monthrange(year, month)[1])
entry_date = rec.employee_id.entry_date
days_in_month = calendar.monthrange(year, month)[1]
# 1. 查询外派/返回单
employee = rec.employee_id
exp_approval = self.env['expatriation.approval'].sudo().search([
('expatriates_ids', 'in', [employee.id]),
('start_date', '<=', month_end), ('work_end', '=', True)
], limit=1)
if exp_approval:
is_outside = True
exp_start_date = exp_approval.start_date
# 查询返回单
back_approval = self.env['back.approval'].sudo().search([
('expatriates_id', '=', employee.id),
('end_date', '>=', month_start), ('end_date', '<=', month_end), ('work_end', '=', True)
], limit=1)
if back_approval:
exp_has_return = True
exp_end_return = back_approval.end_date
exp_end_date = min(date.today(), exp_end_return)
else:
exp_end_date = min(date.today(), month_end)
# 2. 计算外派天数
if is_outside and exp_start_date and exp_end_date:
actual_exp_start = max(exp_start_date, month_start)
expatriation_days = rec._create_leave_record(employee, actual_exp_start, exp_end_date)
# 3. 确定考勤统计起始日(适配外派/入职)
start_day = 1 if (not entry_date or month_start > entry_date) else min(entry_date.day, days_in_month)
if is_outside and exp_has_return and exp_end_return:
actual_start_day = exp_end_return.day + 1 if exp_end_return.month == month else start_day
actual_start_day = max(actual_start_day, start_day)
elif is_outside and not exp_has_return:
actual_start_day = days_in_month + 1 # 无返回单不统计
else:
actual_start_day = start_day
# 4. 遍历当月考勤日统计
for day in range(actual_start_day, days_in_month + 1):
current_date = date(year, month, day)
# 跳过外派日期
if is_outside and exp_start_date and exp_end_date and (
exp_start_date <= current_date <= exp_end_date):
continue
# 查询当日考勤记录
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 or att.workflow_id.workflow_id.name == '外出') and att.shift_name != '休息' and not att.is_abnormal]
leave_days = 0
processed_leave_codes = set()
# 遍历考勤记录统计
for att in attendances:
# ========== 核心:外出天数统计(按workflow_id去重) ==========
if (att.workflow_id and att.workflow_id.workflow_id
and att.workflow_id.workflow_id.name == '外出'
and att.workflow_id.id not in processed_workflow_ids):
# 通过model+res_id获取外出记录
leave_model = self.env[att.workflow_id.res_model].sudo()
leave_record = leave_model.browse(att.workflow_id.res_id)
if leave_record:
processed_workflow_ids.add(att.workflow_id.id)
# 区分按天/按小时计算
if leave_record.is_day:
go_out_days += float(leave_record.days or 0)
elif leave_record.is_hour:
go_out_days += float(leave_record.hours or 0) / 8
# 原有请假天数统计
if att.workflow_id and att.workflow_id.workflow_id.name == '请假':
leave_record = self.env[att.workflow_id.res_model].sudo().browse(att.workflow_id.res_id)
if leave_record.code and leave_record.code in processed_leave_codes:
continue
if leave_record.days:
leave_days += leave_record.days
processed_leave_codes.add(leave_record.code)
# 计算当日实到天数
total_leave_days = leave_days
leave_total_days += leave_days
if valid_attendances:
actual_attendance += max(1 - total_leave_days, 0)
# 统计考勤异常次数
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
# 5. 计算应到天数(适配外派)
contract = employee.contract_id
if is_outside:
if exp_has_return and exp_end_return:
work_days = rec._create_leave_record(employee, exp_end_return + timedelta(days=1), month_end)
else:
work_days = 0
else:
start_date = max(contract.date_start, month_start) if (
contract and contract.date_start) else month_start
end_date = min(contract.date_end, month_end) if (contract and contract.date_end) else month_end
work_days = rec._create_leave_record(employee, start_date, end_date)
# 6. 计算试用期/转正天数
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:
prob_end = min(probation_contract.end_date, month_end)
prob_start = max(probation_contract.start_date, month_start)
if prob_start <= prob_end:
probation_days = rec._create_leave_record(employee, prob_start, prob_end)
# 转正天数
reg_start = prob_end + timedelta(days=1)
reg_end = month_end
if reg_start <= reg_end:
regular_days = rec._create_leave_record(employee, reg_start, reg_end)
# 赋值考勤字段
rec.go_out = go_out_days # 外出天数最终赋值
rec.should_attendance = work_days
rec.test_days = probation_days
rec.right_days = regular_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, 0)
rec.att_mount = actual_attendance
rec.expatriation = expatriation_days
else:
# 重置字段(员工为空/离职/无考勤月份)
rec.test_days = rec.right_days = rec.actual_attendance = rec.should_attendance = 0
rec.is_late_card = rec.is_early_card = rec.is_no_card = rec.is_work = rec.is_abnormal = 0
rec.att_mount = rec.expatriation = rec.go_out = 0.0
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,
'logging_data': '444',
'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",
}
def update_holiday(self):
# 获取当前激活的记录
vacation_ids = self.env["yuthon.vacation.reports"].browse(
self._context.get('active_ids', self._context.get('active_id')))
current_year = datetime.now().year
for vacation in vacation_ids:
# 1. 处理上年年假剩余 (年假, 时间:上上年3月1日 到 上年2月28/29日)
if vacation.dynamic_previous_year > 0:
start_date = datetime(current_year - 2, 3, 1)
end_date = datetime(current_year - 1, 2, 28)
# 检查是否是闰年,
if (current_year - 1) % 4 == 0 and (current_year - 1) % 100 != 0 or (current_year - 1) % 400 == 0:
end_date = datetime(current_year - 1, 2, 29)
self._create_leave_allocation(
vacation=vacation,
leave_days=vacation.dynamic_previous_year,
allocation_type='accrual',
leave_name='年假',
start_date=start_date,
end_date=end_date
)
# 2. 处理本年年假剩余 (本年3月1日 到 明年2月28/29日)
if vacation.dynamic_remainder_year > 0:
start_date = datetime(current_year, 3, 1)
end_date = datetime(current_year + 1, 2, 28)
# 检查闰年
if (current_year + 1) % 4 == 0 and (current_year + 1) % 100 != 0 or (current_year + 1) % 400 == 0:
end_date = datetime(current_year + 1, 2, 29)
self._create_leave_allocation(
vacation=vacation,
leave_days=vacation.dynamic_remainder_year,
allocation_type='accrual',
leave_name='年假',
start_date=start_date,
end_date=end_date
)
# 3. 处理上年补休剩余 (休假, 时间:上年3月1日 到 上年2月28/29日)
if vacation.dynamic_previous_year1 > 0:
start_date = datetime(current_year - 2, 3, 1)
end_date = datetime(current_year - 1, 2, 28)
# 检查闰年
if (current_year - 1) % 4 == 0 and (current_year - 1) % 100 != 0 or (current_year - 1) % 400 == 0:
end_date = datetime(current_year - 1, 2, 29)
self._create_leave_allocation(
vacation=vacation,
leave_days=vacation.dynamic_previous_year1,
allocation_type='regular',
leave_name='补休',
start_date=start_date,
end_date=end_date
)
# 4. 处理本年补休剩余 ( 休假, 时间:本年3月1日 到 明年2月28/29日)
if vacation.this_year1 > 0:
start_date = datetime(current_year, 3, 1)
end_date = datetime(current_year + 1, 2, 28)
if (current_year + 1) % 4 == 0 and (current_year + 1) % 100 != 0 or (current_year + 1) % 400 == 0:
end_date = datetime(current_year + 1, 2, 29)
self._create_leave_allocation(
vacation=vacation,
leave_days=vacation.this_year1,
allocation_type='regular',
leave_name='补休',
start_date=start_date,
end_date=end_date
)
def _create_leave_allocation(self, vacation, leave_days, allocation_type, leave_name, start_date, end_date):
"""
封装创建hr.leave.allocation的逻辑
:param vacation: 当前yuthon.vacation.reports记录
:param leave_days: 假期天数
:param allocation_type: 分配类型 (accrual/regular)
:param leave_name: 假期名称 (年假/补休)
:param start_date: 开始日期
:param end_date: 结束日期
"""
accrual_plan_id = self.env['hr.leave.accrual.plan'].search(
[('name', 'ilike', '天河物管')], limit=1
)
if leave_name == '年假':
leave_type = self.env['hr.leave.type'].search([('display_name', 'ilike', leave_name), ('company_id.name', '=', '天河物管')], limit=1)
else:
leave_type = self.env['hr.leave.type'].search([('name', '=', leave_name)])
vals = {
'name': leave_name,
'number_of_days': leave_days,
'holiday_status_id': leave_type.id,
'allocation_type': allocation_type,
'date_from': fields.Date.to_string(start_date),
'date_to': fields.Date.to_string(end_date),
'notes': leave_name,
}
if allocation_type == 'accrual' and accrual_plan_id:
vals['accrual_plan_id'] = accrual_plan_id.id
vals['employee_id'] = vacation.employee_id.id
self.env['hr.leave.allocation'].create(vals)
@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',
'婚假': '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',
'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
if actual_start_date > end_date:
return 0
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)
])
exclude_days = set()
include_shift_days = set()
for leave in calendar_leaves:
# 法定节假日
if leave.holidays:
exclude_days.update([self.remove_leading_zero(d) for d in leave.holidays.split(',')])
# 调休休息日
if leave.alternate_rest_day:
exclude_days.update([self.remove_leading_zero(d) for d in leave.alternate_rest_day.split(',')])
# 调班日
if leave.shift_replacement_day:
include_shift_days.update([self.remove_leading_zero(d) for d in leave.shift_replacement_day.split(',')])
total_days = 0
current_date = actual_start_date
while current_date <= end_date:
current_date_str = self.remove_leading_zero(current_date.strftime('%m月%d日'))
is_weekend = current_date.weekday() in [5, 6]
is_exclude = current_date_str in exclude_days
is_shift_day = current_date_str in include_shift_days
if not is_exclude:
if not is_weekend:
total_days += 1
elif is_shift_day:
total_days += 1
current_date += timedelta(days=1)
return total_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.model
def put_vacation(self):
today = fields.Date.today()
first_day = today.replace(day=1)
last_month_first = first_day - relativedelta(months=1)
report_objs = self.env['yuthon.vacation.reports'].search([
('vacation_date', '=', last_month_first),
('vacation_state', '=', 'not_sent'),
])
for report in report_objs:
resource_user_ids = report.employee_id.resource_calendar_id.resource_user_ids.ids
if report.employee_id.id in resource_user_ids:
report.write({'vacation_state': 'confirmed'})
else:
report.button_put()
def att_button_put_task(self):
today = fields.Date.today()
first_day = today.replace(day=1)
current_date = first_day
while True:
if self._process_shift_days(current_date):
first_workday = current_date
break
current_date += timedelta(days=1)
if today != first_workday:
return
last_month_first = first_day - relativedelta(months=1)
report_objs = self.env['yuthon.vacation.reports'].search([
('vacation_date', '=', last_month_first),
('vacation_state', '=', 'not_sent'),
])
for report in report_objs:
resource_user_ids = report.employee_id.resource_calendar_id.resource_user_ids.ids
if report.employee_id.id in resource_user_ids:
report.write({'vacation_state': 'confirmed'})
else:
report.button_put()
def button_put(self):
we_id = self.employee_id.wecom_userid
if we_id:
url = f'https://phone.thtzjt.com/phone/Notarize/{self.id}/{self.employee_id.user_id.id}'
year = self.vacation_date.year
month = self.vacation_date.month
data = f"你{year}年{month}月的考勤确认单已经生成,请确认"
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att_ok', user_id=we_id,
title="考勤确认单", description=data, url=url,
btntxt="详细信息")
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att_ok', user_id="18562027762",
title="考勤确认单", description=data, url=url,
btntxt="详细信息")
self.write({'vacation_state': 'pending'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': '确认通知已发送',
'sticky': False,
}
}
from datetime import datetime
# 放在 yuthon.vacation.reports 模型类中
def send_vacation_confirm_reminder(self):
today = datetime.today()
# 1. 过滤条件:待确认 + 当月20号及之前
domain = [
('vacation_state', '=', 'pending'),
]
# 2. 每月20号之后不执行
if today.day > 20:
return
# 3. 批量处理符合条件的记录
records = self.sudo().search(domain)
for record in records:
we_id = record.employee_id.wecom_userid
if not we_id:
continue # 无企业微信ID跳过
# 复用原按钮的消息逻辑
url = f'https://phone.thtzjt.com/phone/Notarize/{record.id}/{record.employee_id.user_id.id}'
year = record.vacation_date.year
month = record.vacation_date.month
data = f"你{year}年{month}月的考勤确认单已经生成,请确认"
# 发送给员工
record.env["wecom.apps"].sync_send_message_textcard(
category='att_ok',
user_id=we_id,
title="考勤确认单",
description=data,
url=url,
btntxt="详细信息"
)
return
def _process_shift_days(self, current_data):
"""
判断是否是工作日 调休日 双休
"""
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)
])
holidays_list = []
shift_days_list = []
# 提取节假日和调休日
for leave in calendar_leaves:
if leave.holidays:
holidays_list.extend(leave.holidays.split(','))
if leave.alternate_rest_day:
holidays_list.extend(leave.alternate_rest_day.split(','))
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(','))
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
result = f"{current_data.month}月{current_data.day}日"
if result in shift_days_list:
return True
elif (result in holidays_list) or (current_data.weekday() in [5, 6]):
return False
else:
return True
def all_put(self):
vacation_ids = self.env["yuthon.vacation.reports"].browse(self._context.get('active_ids', self._context.get('active_id')))
for rec in vacation_ids:
we_id = rec.employee_id.wecom_userid
year = rec.vacation_date.year
month = rec.vacation_date.month
if we_id:
url = f'https://phone.thtzjt.com/phone/Notarize/{rec.id}/{rec.employee_id.user_id.id}'
data = f"你{year}年{month}月的考勤确认单已经生成,请确认"
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att_ok', user_id=we_id, title="考勤确认单", description=data, url=url, btntxt="详细信息")
# self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att_ok', user_id="18562027762", title="考勤确认单", description=data, url=url, btntxt="详细信息")
rec.write({'vacation_state': 'pending'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': '已发送提醒消息',
'sticky': False,
}
}
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.vacation_state = 'confirmed'
def manage_reject(self):
'''
继承核心逻辑
:return:
'''
target_calendar_names = [
'行政考勤-本部',
'行政考勤-物管',
'行政考勤-粮储'
]
target_calendars = self.env['resource.calendar'].search([
('name', 'in', target_calendar_names)
])
target_employee_ids = set()
if target_calendars:
for calendar in target_calendars:
employee_ids = calendar.resource_user_ids.ids
target_employee_ids.update(employee_ids)
for rec in self:
employee = rec.employee_id
vacation_date = rec.vacation_date
unpaid_leaves = self.env['hr.leave'].search([
('employee_id', '=', employee.id),
('holiday_status_id.name', '=', ['产假']),
('state', '=', 'validate')
])
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)),
], limit=1)
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)
current_date = rec.vacation_date
if last_month_record:
last_month_record._compute_parental_leave()
dynamic_previous_year_val = 0.0 if (
rec.valid_date and rec.valid_date < current_date) else last_month_record.dynamic_previous_year
dynamic_previous_year1_val = 0.0 if (
rec.valid_date1 and rec.valid_date1 < current_date) else last_month_record.dynamic_previous_year1
inherit_vals = {
'remain_missed_meals': last_month_record.remain_missed_meals,
'leave': last_month_record.leave,
'dynamic_previous_year': dynamic_previous_year_val,
'dynamic_remainder_year': last_month_record.dynamic_remainder_year,
'dynamic_previous_year1': dynamic_previous_year1_val,
'this_year1': last_month_record.this_year1,
'working_overtime': last_month_record.working_overtime,
'medical_fee': last_month_record.medical_fee,
'previous_year': last_month_record.previous_year,
'remaining_parental_leave': last_month_record.remaining_parental_leave,
'year_distribution': last_month_record.year_distribution,
'used_this_year': last_month_record.used_this_year,
'last_holiday': last_month_record.last_holiday,
'last_workday': last_month_record.last_workday,
'year_holiday': last_month_record.year_holiday,
'year_workday': last_month_record.year_workday,
'previous_year1': last_month_record.previous_year1,
'used_this_year1': last_month_record.used_this_year1,
}
if not rec.valid_date:
inherit_vals['valid_date'] = last_month_record.valid_date
if not rec.valid_date1:
inherit_vals['valid_date1'] = last_month_record.valid_date1
rec.write(inherit_vals)
if unpaid_leaves:
for leave in unpaid_leaves:
leave_start = leave.date_from.date()
leave_end = leave.date_to.date()
current_month_start = vacation_date
current_month_end = (current_month_start + relativedelta(months=1)) - timedelta(days=1)
real_start = max(leave_start, current_month_start)
real_end = min(leave_end, current_month_end)
if real_start > real_end:
continue
# 计算当前月的天数
days = self._compute_duration_day(leave.holiday_status_id, real_start, real_end)
# 取 workflow
if leave.batch_id:
main_leave_record = leave.batch_id
elif leave.hour_batch_id:
main_leave_record = leave.hour_batch_id
else:
main_leave_record = leave
workflow = self.env['soong.workflow.record'].search([
('code', '=', main_leave_record.code)
], limit=1)
# 防重
existing_line = self.env['vacation.other.line'].search([
('vacation_id', '=', rec.id),
('employee_id', '=', employee.id),
('text', '=', '请假审批'),
('workflow_record_id', '=', workflow.id if workflow else False),
], limit=1)
# 创建
if not existing_line and days > 0:
self.env['vacation.other.line'].create({
'vacation_id': rec.id,
'employee_id': employee.id,
'workflow_record_id': workflow.id if workflow else False,
'update_type': 'reduce',
'update_date': fields.Date.today(),
'lines': days,
'text': '请假审批',
'is_void': False,
})
if rec.remain_missed_meals:
self._update_meal_lines(rec, [rec], rec.remain_missed_meals)
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)
if rec.employee_id.id not in target_employee_ids:
rec.remaining_number_card = 3
else:
rec.remaining_number_card = 0
if rec.remaining_parental_leave and last_month_record.parenting_line_ids:
update_date = last_month_record.parenting_line_ids[0].update_date
end_date = last_month_record.parenting_line_ids[0].end_date
if end_date:
date_condition = update_date <= rec.vacation_date <= end_date
else:
date_condition = rec.vacation_date >= update_date
if date_condition:
self._update_parental_leave_lines(rec, [rec], rec.remaining_parental_leave, update_date, end_date)
@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,
})
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('overtime_ids.workday', 'overtime_ids.legal_day', 'overtime_ids.public_holidays', 'overtime_ids.update_type', 'overtime_ids.is_void')
def _compute_overtime_totals(self):
"""计算加班明细的合计:工作日、法定节假日、公休日的合计,以及总合计
根据update_type判断是增加还是减少
"""
for record in self:
workday_total = 0.0
legal_day_total = 0.0
public_holidays_total = 0.0
for line in record.overtime_ids:
if line.is_void:
continue
if line.update_type == 'add':
workday_total += line.workday or 0
legal_day_total += line.legal_day or 0
public_holidays_total += line.public_holidays or 0
elif line.update_type == 'reduce':
workday_total -= line.workday or 0
legal_day_total -= line.legal_day or 0
public_holidays_total -= line.public_holidays or 0
record.overtime_workday_total = workday_total
record.overtime_legal_day_total = legal_day_total
record.overtime_public_holidays_total = public_holidays_total
record.overtime_grand_total = workday_total + legal_day_total + public_holidays_total
def _compute_duration_day(self, holiday_status_id, start_date, end_date):
"""
计算请假时长
:param holiday_status_id: 请假类型
:param start_date: 开始时间
:param end_date: 结束时间
:return:天
"""
employee_id = self.employee_id
use_planning = False
if employee_id and employee_id.resource_id:
roles = self.env['planning.role'].sudo().search([
('name', 'in', ['保安部', '工程部', '联安粮储'])
])
all_resources = roles.mapped('resource_ids')
if employee_id.resource_id in all_resources:
use_planning = True
if use_planning:
if not start_date or not end_date:
return 0
slots = self.env['planning.slot'].sudo().search([
('resource_id', '=', employee_id.resource_id.id),
('start_datetime', '>=', datetime.combine(start_date, datetime.min.time())),
('end_datetime', '<=', datetime.combine(end_date, datetime.max.time())),
])
work_days = set()
for slot in slots:
day = slot.start_datetime.date()
if start_date <= day <= end_date:
work_days.add(day)
return len(work_days)
else:
# 如果是自然日:
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":
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)
else:
delta = end_date - start_date
days = delta.days + 1
return days
def action_overtime_change(self):
"""打开加班变更向导"""
self.ensure_one()
return {
'name': '加班变更',
'type': 'ir.actions.act_window',
'res_model': 'overtime.change.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'yuthon.vacation.reports',
'active_id': self.id,
},
}
@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.remain_missed_meals = 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.vacation_state = 'confirmed'
def monthly_report(self):
today = fields.Date.today()
current_month_first = today.replace(day=1)
from dateutil.relativedelta import relativedelta
next_month_first = current_month_first + relativedelta(months=1)
target_months = [current_month_first, next_month_first]
employees = self.env['hr.employee'].search([
('user_state', '=', 'in')
])
for employee in employees:
resource_user_ids = employee.resource_calendar_id.resource_user_ids.ids
is_resource_user = employee.id in resource_user_ids
for target_month in target_months:
existing_record = self.search([
('employee_id', '=', employee.id),
('vacation_date', '=', target_month)
], limit=1)
if not existing_record:
report_id = self.create({
'employee_id': employee.id,
'vacation_date': target_month,
})
report_id.update_line()
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),
'|',
('text', '=', '上月额度继承'),
('text', '=', '年假分配')
], limit=1)
if not existing_line:
self.env['vacation.leave.line'].create({
'vacation_id': report.id,
'valid_date': report.valid_date1 + relativedelta(years=1) if report.valid_date1 else None,
'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),
('text', '=', '上月额度继承'),
], limit=1)
if not existing_line:
if report.valid_date1 and report.dynamic_previous_year1 and not report.this_year1:
valid_date = report.valid_date1
elif report.valid_date1 and report.dynamic_previous_year1 and report.this_year1:
valid_date = report.valid_date1 + relativedelta(years=1)
else:
valid_date = False
self.env['vacation.over.line'].create({
'vacation_id': report.id,
'employee_id': report.employee_id.id,
'valid_date': valid_date,
'update_type': 'add',
'update_date': fields.Date.today(),
'workday': number_of_hours_display,
'legal_day': 0,
'public_holidays': 0,
'meal_number': 0,
'text': f"上月额度继承",
'logging_data': '333',
})
def _update_parental_leave_lines(self, report, allocations, number_of_hours_display, update_date, end_date):
for alloc in allocations:
existing_line = self.env['vacation.parenting.line'].search([
('vacation_id', '=', report.id),
('text', '=', '上月额度继承'),
], limit=1)
if existing_line:
continue
self.env['vacation.parenting.line'].sudo().create({
'vacation_id': report.id,
'employee_id': report.employee_id.id,
'update_date': update_date,
'end_date': end_date,
'lines': number_of_hours_display,
'text': f"上月额度继承",
})
def _update_meal_lines(self, report, allocations, number_of_hours_display):
for alloc in allocations:
existing_line = self.env['vacation.meal.line'].search([
('vacation_id', '=', report.id),
('employee_id', '=', report.employee_id.id),
('text', '=', '上月额度继承'),
], limit=1)
if existing_line:
existing_line.write({
'update_lines': number_of_hours_display,
})
else:
self.env['vacation.meal.line'].sudo().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,
'text': f"上月额度继承",
})
def log(self):
return {
"type": "ir.actions.act_window",
"res_model": 'yuthon.vacation.reports',
"res_id": self.id,
"name": "详情",
"view_mode": "form",
}
@api.model
def last_month(self):
"""
上月数据处理:
- 2025年之前的记录:仅筛选is_initial_number = True
- 2025年及之后的记录:按原有公司维度的复杂条件筛选
"""
base_line = self.env['yuthon.overtime.line'].search([], order='create_date asc', limit=1)
if not base_line:
return
base_year = int(base_line.select_year)
base_month = int(base_line.select_month)
base_date = date(base_year, base_month, 1)
self.env['yuthon.overtime.line'].search([]).unlink()
current_date = base_date - timedelta(days=1)
last_month_first = (base_date - timedelta(days=1)).replace(day=1)
dec_2025_end = datetime(2025, 12, 31, 23, 59, 59)
if self.env.user.company_id.name == '集团本部':
domain = [
'|',
('is_initial_number', '=', True),
'&', ('is_initial_number', '=', False),
'&', ('update_type', '=', 'add'),
'&', ('workflow_record_id', '!=', False),
'&', ('start_time', '>=', dec_2025_end),
'&', ('vacation_id', '!=', False),
('start_time', '<=', current_date),
('carry_forward_state', '!=', 'completed'),
('employee_id.is_annual_salary', '!=', True)
]
all_records = self.env['vacation.over.line'].search(domain)
employee_summary = {}
for line in all_records:
line.select_month = str(last_month_first.month)
line.select_year = str(last_month_first.year)
emp_id = line.employee_id.id
if not emp_id:
continue
if emp_id not in employee_summary:
report_id = self.env['yuthon.vacation.reports'].search(
[
('vacation_date', '=', last_month_first),
('employee_id', '=', emp_id),
],
limit=1 # 避免多条记录,只取第一条
)
vacation_sum = report_id.dynamic_previous_year1 * 8 + report_id.this_year1 * 8 if report_id else 0.0
active_id = self.env['vacation.overtime.active'].search(
[('employee_id', '=', emp_id),
('select_month', '=', str(last_month_first.month)),
('select_year', '=', str(last_month_first.year)),
('select_revoke', '!=', 'done'),
])
if active_id:
vacation_sum -= active_id.carry_forward_quantity
employee_summary[emp_id] = {
'employee_id': emp_id,
'valid_date': line.valid_date,
'vacation_sum': vacation_sum,
'last_balance': report_id.dynamic_previous_year1 * 8 if report_id else 0.0,
'remainder_year': 0.0,
'carry_forward_quantity': 0.0,
'carry_forward_date': line.carry_forward_date,
'select_month': str(last_month_first.month),
'select_year': str(last_month_first.year),
}
summary = employee_summary[emp_id]
summary['carry_forward_quantity'] += line.carry_forward_quantity or 0.0
create_vals = []
for emp_id, vals in employee_summary.items():
create_vals.append(vals)
if create_vals:
self.env['yuthon.overtime.line'].create(create_vals)
return {
"name": _("加班明细"),
"type": "ir.actions.act_window",
"views": [(False, "tree")],
"res_model": "yuthon.overtime.line",
"target": "current",
}
@api.model
def next_month(self):
base_line = self.env['yuthon.overtime.line'].search([], order='create_date asc', limit=1)
base_year = int(base_line.select_year)
base_month = int(base_line.select_month)
base_date = date(base_year, base_month, 1)
self.env['yuthon.overtime.line'].search([]).unlink()
last_month_first = ((base_date + relativedelta(months=2)).replace(day=1) - timedelta(days=1)).replace(day=1)
current_date = (base_date + relativedelta(months=2)).replace(day=1) - timedelta(days=1)
dec_2025_end = datetime(2025, 12, 31, 23, 59, 59)
if self.env.user.company_id.name == '集团本部':
domain = [
'|',
('is_initial_number', '=', True),
'&', ('is_initial_number', '=', False),
'&', ('update_type', '=', 'add'),
'&', ('workflow_record_id', '!=', False),
'&', ('start_time', '>=', dec_2025_end),
'&', ('vacation_id', '!=', False),
('start_time', '<=', current_date),
('carry_forward_state', '!=', 'completed'),
('employee_id.is_annual_salary', '!=', True)
]
all_records = self.env['vacation.over.line'].search(domain)
employee_summary = {}
for line in all_records:
line.select_month = str(last_month_first.month)
line.select_year = str(last_month_first.year)
emp_id = line.employee_id.id
if not emp_id:
continue
if emp_id not in employee_summary:
report_id = self.env['yuthon.vacation.reports'].search(
[
('vacation_date', '=', last_month_first),
('employee_id', '=', emp_id),
],
limit=1 # 避免多条记录,只取第一条
)
vacation_sum = report_id.dynamic_previous_year1 * 8 + report_id.this_year1 * 8 if report_id else 0.0
active_id = self.env['vacation.overtime.active'].search(
[('employee_id', '=', emp_id),
('select_month', '=', str(last_month_first.month)),
('select_year', '=', str(last_month_first.year)),
('select_revoke', '!=', 'done'),
])
if active_id:
vacation_sum -= active_id.carry_forward_quantity
employee_summary[emp_id] = {
'employee_id': emp_id,
'valid_date': line.valid_date,
'vacation_sum': vacation_sum,
'last_balance': report_id.dynamic_previous_year1 * 8 if report_id else 0.0,
'remainder_year': 0.0,
'carry_forward_quantity': 0.0,
'carry_forward_date': line.carry_forward_date,
'select_month': str(last_month_first.month),
'select_year': str(last_month_first.year),
}
summary = employee_summary[emp_id]
summary['carry_forward_quantity'] += line.carry_forward_quantity or 0.0
create_vals = []
for emp_id, vals in employee_summary.items():
create_vals.append(vals)
if create_vals:
self.env['yuthon.overtime.line'].create(create_vals)
return {
"name": _("加班明细"),
"type": "ir.actions.act_window",
"views": [(False, "tree")],
"res_model": "yuthon.overtime.line",
"target": "current",
}
@api.model
def overtime_line(self):
# 1. 清空历史数据(先删除所有旧记录)
self.env['yuthon.overtime.line'].search([]).unlink()
# 2. 定义时间范围
today = date.today()
last_month_first = (today - relativedelta(months=1)).replace(day=1) # 上月1号
current_month_first = today.replace(day=1) # 本月1号
current_month_first_dt = datetime.combine(current_month_first, datetime.min.time())
dec_2025_end = datetime(2025, 12, 31, 23, 59, 59)
# 3. 仅处理集团本部的数据
if self.env.user.company_id.name == '集团本部':
# 数据筛选条件
domain = [
'|',
('is_initial_number', '=', True),
'&', ('is_initial_number', '=', False),
'&', ('update_type', '=', 'add'),
'&', ('workflow_record_id', '!=', False),
'&', ('start_time', '>=', dec_2025_end),
'&', ('vacation_id', '!=', False),
('start_time', '<', current_month_first_dt),
('employee_id.is_annual_salary', '!=', True)
]
all_records = self.env['vacation.over.line'].search(domain)
employee_summary = {}
for line in all_records:
line.select_month = str(last_month_first.month)
line.select_year = str(last_month_first.year)
emp_id = line.employee_id.id
if not emp_id:
continue
if emp_id not in employee_summary:
report_id = self.env['yuthon.vacation.reports'].search(
[
('vacation_date', '=', last_month_first),
('employee_id', '=', emp_id),
],
limit=1 # 避免多条记录,只取第一条
)
vacation_sum = report_id.dynamic_previous_year1 * 8 + report_id.this_year1 * 8 if report_id else 0.0
active_id = self.env['vacation.overtime.active'].search(
[('employee_id', '=', emp_id),
('select_month', '=', str(last_month_first.month)),
('select_year', '=', str(last_month_first.year)),
('select_revoke', '!=', 'done'),
])
if active_id:
vacation_sum -= active_id.carry_forward_quantity
employee_summary[emp_id] = {
'employee_id': emp_id,
'valid_date': line.valid_date,
'vacation_sum':vacation_sum,
'last_balance': report_id.dynamic_previous_year1 * 8 if report_id else 0.0,
'remainder_year': 0.0,
'carry_forward_quantity': 0.0,
'carry_forward_date': line.carry_forward_date,
'select_month': str(last_month_first.month),
'select_year': str(last_month_first.year),
}
summary = employee_summary[emp_id]
summary['carry_forward_quantity'] += line.carry_forward_quantity or 0.0
create_vals = []
for emp_id, vals in employee_summary.items():
create_vals.append(vals)
if create_vals:
self.env['yuthon.overtime.line'].create(create_vals)
return {
"name": _("加班明细"),
"type": "ir.actions.act_window",
"views": [(False, "tree")],
"res_model": "yuthon.overtime.line",
"target": "current",
}
# 误餐上月
@api.model
def last_meal_month(self):
yuthon_meal_env = self.env['yuthon.meal.line']
last_record = yuthon_meal_env.search([], limit=1, order='id desc')
if last_record:
current_year = int(last_record.select_year)
current_month = int(last_record.select_month)
else:
now = datetime.now()
current_year = now.year
current_month = now.month
if current_month == 1:
last_year = current_year - 1
last_month = 12
else:
last_year = current_year
last_month = current_month - 1
end_date = date(last_year, last_month, 1) + relativedelta(months=1) - relativedelta(days=1)
domain = [
('start_time', '>=', datetime(2026, 1, 1)),
('meal', '=', 'yes'),
('vacation_id', '!=', False),
('workflow_id', '!=', False),
('update_type', '>=', 'add'),
('vacation_date', '<=', end_date),
('company_id', 'in', self.env.companies.ids),
('text', '!=', '上月额度继承')
]
meal_lines = self.env['vacation.meal.line'].search(domain)
yuthon_meal_env.search([]).unlink()
# 批量创建
create_vals_list = []
for line in meal_lines:
create_vals = {
'meal_name': line.meal_name,
'meal_id': line.id,
'employee_id': line.employee_id.id,
'entry_date': line.employee_id.entry_date,
'select_year': str(last_year),
'select_month': str(last_month),
'workflow_record_id': line.workflow_record_id.id,
'start_time': line.start_time,
'end_time': line.end_time,
'duration': line.duration,
'meal': line.meal,
'is_filing':line.is_filing,
'update_lines': line.update_lines,
'reasons': line.reasons,
}
create_vals_list.append(create_vals)
if create_vals_list:
yuthon_meal_env.create(create_vals_list)
return {
'type': 'ir.actions.act_window',
'name': f'误餐情况',
"views": [(False, "tree")],
'res_model': 'yuthon.meal.line',
'domain': [],
'target': 'current',
'context': {
'group_by': 'is_filing',
}
}
# 误餐本月
@api.model
def meal_line(self):
today = date.today()
end_date = date(today.year, today.month, 1) + relativedelta(months=1) - relativedelta(days=1)
domain = [
('start_time', '>=', datetime(2026, 1, 1)),
('meal', '=', 'yes'),
('vacation_id', '!=', False),
('workflow_id', '!=', False),
('update_type', '>=', 'add'),
('vacation_date', '<=', end_date),
('company_id', 'in', self.env.companies.ids),
('text', '!=', '上月额度继承')
]
meal_lines = self.env['vacation.meal.line'].search(domain)
yuthon_meal_env = self.env['yuthon.meal.line']
yuthon_meal_env.search([]).unlink()
create_vals_list = []
for line in meal_lines:
create_vals = {
'meal_id': line.id,
'meal_name': line.meal_name,
'employee_id': line.employee_id.id,
'entry_date': line.employee_id.entry_date,
'select_month': str(today.month),
'select_year': str(today.year),
'workflow_record_id': line.workflow_record_id.id,
'start_time': line.start_time,
'end_time': line.end_time,
'duration': line.duration,
'update_lines': line.update_lines,
'is_filing': line.is_filing,
'meal': line.meal,
'reasons': line.reasons,
}
create_vals_list.append(create_vals)
if create_vals_list:
yuthon_meal_env.create(create_vals_list)
return {
'type': 'ir.actions.act_window',
'name': f'误餐情况',
"views": [(False, "tree")],
'res_model': 'yuthon.meal.line',
'domain': [],
'target': 'current',
'context': {
'group_by': 'is_filing',
}
}
@api.model
def next_meal_month(self):
# 核心:从已生成数据取年月
yuthon_meal_env = self.env['yuthon.meal.line']
last_record = yuthon_meal_env.search([], limit=1, order='id desc')
if last_record:
current_year = int(last_record.select_year)
current_month = int(last_record.select_month)
else:
now = datetime.now()
current_year = now.year
current_month = now.month
# 计算:下月
if current_month == 12:
next_year = current_year + 1
next_month = 1
else:
next_year = current_year
next_month = current_month + 1
# 下月最后一天
end_date = date(next_year, next_month, 1) + relativedelta(months=1) - relativedelta(days=1)
active_records = self.env['vacation.meal.active'].search([('select_revoke', '=', 'to_do')])
domain = [
('start_time', '>=', datetime(2026, 1, 1)),
('meal', '=', 'yes'),
('vacation_id', '!=', False),
('workflow_id', '!=', False),
('update_type', '>=', 'add'),
('vacation_date', '<=', end_date),
('company_id', 'in', self.env.companies.ids),
('text', '!=', '上月额度继承')
]
meal_lines = self.env['vacation.meal.line'].search(domain)
# 清空
yuthon_meal_env.search([]).unlink()
# 批量创建
create_vals_list = []
for line in meal_lines:
create_vals = {
'meal_id': line.id,
'meal_name': line.meal_name,
'employee_id': line.employee_id.id,
'entry_date': line.employee_id.entry_date,
'select_year': str(next_year), # 用下一年月
'select_month': str(next_month),
'workflow_record_id': line.workflow_record_id.id,
'start_time': line.start_time,
'end_time': line.end_time,
'duration': line.duration,
'update_lines': line.update_lines,
'is_filing': line.is_filing,
'meal': line.meal,
'reasons': line.reasons,
}
create_vals_list.append(create_vals)
if create_vals_list:
yuthon_meal_env.create(create_vals_list)
return {
'type': 'ir.actions.act_window',
'name': f'误餐情况',
"views": [(False, "tree")],
'res_model': 'yuthon.meal.line',
'domain': [],
'target': 'current',
'context': {
'group_by': 'is_filing',
}
}
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='流程')
leave_id = fields.Many2one('hr.leave', string='假单')
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
update_date = fields.Date(string='更新时间')
update_lines = fields.Float(string='更新额度h', digits=(16, 1))
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
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 = '加班明细'
active = fields.Boolean('Active', default=True, tracking=True)
select_month = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
('9', '九月'), ('10', '十月'), ('11', '十一月'),
('12', '十二月')], string='月份',
default=lambda self: str(datetime.now().month))
select_year = fields.Selection(selection=[(str(i), str(i)) for i in range(2024, 2060)], string='年份',
default=lambda self: str(datetime.now().year))
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
employee_number = fields.Char(string='序号', related='employee_id.number', store=True)
entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", 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='流程')
leave_id = fields.Many2one('hr.leave', string='假单')
is_visible = fields.Boolean(string='是否隐藏')
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
workday = fields.Float(string='工作日h', digits=(16, 1))
last_balance = fields.Float(string='上年结余', compute='_compute_last_balance', store=True)
legal_day = fields.Float(string='法定节假日h', digits=(16, 1))
public_holidays = fields.Float(string='公休日h', digits=(16, 1))
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)
reason_application = fields.Text(string="事由", compute="_compute_overtime", store=True)
meal = fields.Selection([('yes', '是'), ('no', '否')], string="误餐费", compute="_compute_overtime", store=True)
workflow_id = fields.Many2one('soong.workflow', compute="_compute_overtime", string='审批类型', store=True)
start_time = fields.Datetime(string="开始时间", compute="_compute_overtime", store=True)
end_time = fields.Datetime(string="结束时间", compute="_compute_overtime", store=True)
duration = fields.Float(string="时长", compute="_compute_overtime", store=True)
remaining_quantity = fields.Float(string='剩余数', compute="_compute_carry_forward_info", store=True)
remaining_quantity1 = fields.Float(string='剩余数', compute="_compute_carry_forward_info1") # 动态值根据选择的年月过滤无效额度
logging_data = fields.Char(string="日志日志日志")
validity = fields.Char(string="有效期")
is_filing = fields.Boolean(string="归档")
carry_result = fields.Selection([('success', '成功'), ('failed', '失败')], string="结转结果", default='success')
carry_forward_date = fields.Date(string="结转日期", compute='_compute_carry_forward_info', store=True)
carry_forward_quantity = fields.Float(string="结转时长", compute='_compute_carry_forward_info', store=True)
carry_forward_quantity1 = fields.Float(string="结转时长", compute='_compute_carry_forward_info1') # 动态值根据选择的年月过滤无效额度
carry_forward_state = fields.Selection([
('pending', '待结转'),
('completed', '已结转'),
('partial', '部分结转')
], string='结转状态', compute='_compute_carry_forward_info', store=True, default='pending')
carry_forward_state1 = fields.Selection([
('pending', '待结转'),
('completed', '已结转'),
('partial', '部分结转')
], string='结转状态', compute='_compute_carry_forward_info1') # 动态值根据选择的年月过滤无效额度
carryover_ids = fields.One2many('overtime.carryover', 'over_line_id', string='关联结转明细')
@api.depends('carryover_ids.carry_forward_quantity', 'carryover_ids.carry_forward_date',
'carryover_ids.is_void', 'select_year', 'select_month')
def _compute_carry_forward_info1(self):
"""
动态计算(按选择年月过滤):
1. carry_forward_quantity1 = 符合条件的结转时长合计
2. remaining_quantity1 = 总时长 - 符合条件结转时长
3. carry_forward_state1 = 动态结转状态
"""
for rec in self:
rec.carry_forward_quantity1 = 0.0
rec.remaining_quantity1 = rec.duration or 0.0
rec.carry_forward_state1 = 'pending' # 默认未结转
if not rec.select_year or not rec.select_month:
continue
year = int(rec.select_year)
month = int(rec.select_month)
last_day_of_selected = fields.Date.end_of(
fields.Date.from_string(f'{year}-{month:02d}-01'),
'month'
)
# 过滤:未作废 + 日期 <= 选择年月
filtered_carries = rec.carryover_ids.filtered(lambda c:
not c.is_void
and c.carry_forward_date
and c.carry_forward_date <= last_day_of_selected
)
total_filtered = sum(filtered_carries.mapped('carry_forward_quantity'))
rec.carry_forward_quantity1 = total_filtered
rec.remaining_quantity1 = (rec.duration or 0.0) - total_filtered
if total_filtered <= 0:
rec.carry_forward_state1 = 'pending'
elif total_filtered >= (rec.duration or 0.0):
rec.carry_forward_state1 = 'completed'
else:
rec.carry_forward_state1 = 'partial'
@api.depends('carryover_ids.carry_forward_quantity', 'carryover_ids.carry_forward_date', 'remaining_quantity')
def _compute_carry_forward_info(self):
"""
从overtime.carryover明细行自动计算:
1. 结转日期:取最新的结转明细日期
2. 结转时长:汇总所有关联明细的结转时长
3. 结转状态:按剩余时长+结转时长计算(复用原有逻辑)
"""
for rec in self:
rec.carry_forward_date = False
rec.carry_forward_quantity = 0.0
rec.carry_forward_state = 'pending'
rec.remaining_quantity = rec.duration
if rec.carryover_ids:
valid_carryover_ids = rec.carryover_ids.filtered(lambda c: not c.is_void)
# 仅汇总未作废明细的结转时长
total_carry = sum(valid_carryover_ids.mapped('carry_forward_quantity'))
# 结转日期:取最新的结转明细日期(无则取最早)
carry_dates = valid_carryover_ids.mapped('carry_forward_date')
if carry_dates:
rec.carry_forward_date = max(carry_dates)
current_remaining = rec.remaining_quantity or 0.0
if total_carry <= 0:
rec.carry_forward_state = 'pending'
elif total_carry >= current_remaining:
rec.carry_forward_state = 'completed'
else:
rec.carry_forward_state = 'partial'
rec.carry_forward_quantity = total_carry
rec.remaining_quantity = rec.duration - rec.carry_forward_quantity
def open_record1(self):
tree_id = self.env.ref("yuthon_hr_attendance.view_yuthon_vacation_overtime_tree").id
return {
'type': 'ir.actions.act_window',
'name': 'Overtime Record',
'res_model': 'vacation.meal.line',
'view_mode': 'tree',
'views': [[tree_id, 'tree']],
'domain': [('employee_id', '=', self.employee_id.id),
('carry_forward_state', '=', 'pending')
],
}
def open_overtime_carryover(self):
return {
"type": "ir.actions.act_window",
"name": "结转明细",
"res_model": "overtime.carryover",
"view_mode": "tree",
"target": "current",
'domain': [('over_line_id', '=', self.id)],
}
def open_overtime_carryover1(self):
domain = [('over_line_id', '=', self.id)]
if self.select_year and self.select_month:
last_day = fields.Date.end_of(
fields.Date.from_string(f'{int(self.select_year)}-{int(self.select_month):02d}-01'),
'month'
)
domain.append(('carry_forward_date', '<=', last_day))
carryover_ids = self.env['overtime.carryover'].search([
('over_line_id', '=', self.id),
('is_void', '=', False),
('carry_forward_date', '<=', last_day),
], order='carry_forward_date asc')
if carryover_ids:
total_balance = self.duration
current_remaining = total_balance
for carry in carryover_ids:
carry.sudo().write({
'last_month': current_remaining,
'this_month': current_remaining - carry.carry_forward_quantity,
})
current_remaining = carry.this_month
return {
"type": "ir.actions.act_window",
"name": "结转明细",
"res_model": "overtime.carryover",
"view_mode": "tree",
"target": "current",
'domain': domain, # 用动态拼接的过滤条件
}
@api.model
def confirm(self):
# 1. 构建筛选域
domain = [
'|',
['is_initial_number', '=', True],
'&',
['is_initial_number', '=', False],
'&',
['update_type', '=', 'add'],
'&',
['workflow_record_id', '!=', False],
'!',
'&',
['start_time', '>=', '2025-12-31 00:00:00'],
['start_time', '<', '2026-01-01 00:00:00']
]
filtered_records = self.search(domain)
if not filtered_records:
return
valid_records = filtered_records.filtered(
lambda r: r.carry_forward_quantity and r.carry_forward_quantity != 0.0
)
if not valid_records:
return
active_model = self.env['vacation.overtime.active']
for rec in valid_records:
if rec.remaining_quantity == 0:
rec.is_filing = True
match_domain = [
('employee_id', '=', rec.employee_id.id if rec.employee_id else False),
('workflow_record_id', '=', rec.workflow_record_id.id if rec.workflow_record_id else False),
('carry_forward_date', '=', rec.carry_forward_date or fields.Date.today()),
('overtime_name', '=', rec.overtime_name or '')
]
existing_active = active_model.search(match_domain, limit=1)
active_vals = {
'employee_id': rec.employee_id.id if rec.employee_id else False,
'workflow_record_id': rec.workflow_record_id.id if rec.workflow_record_id else False,
'overtime_name': rec.overtime_name or '',
'remaining_quantity': rec.remaining_quantity,
'carry_forward_date': rec.carry_forward_date or fields.Date.today(),
'carry_forward_quantity': rec.carry_forward_quantity,
'carry_result': 'success'
}
if existing_active:
existing_active.write(active_vals)
else:
active_model.create(active_vals)
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_overtime()
rec._compute_employee_id()
rec._compute_carry_forward_info()
@api.depends('vacation_id', 'carry_forward_quantity', 'duration')
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 + rec.public_holidays
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:
if res_model == 'overtime.approval':
overtime_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
if overtime_id:
rec.meal = overtime_id.meal
rec.reason_application = overtime_id.reason_application
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
rec.workflow_id = rec.workflow_record_id.workflow_id.id or 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',
}
@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(VacationOverline, self).create(vals)
# def unlink(self):
# """删除记录时同步扣减员工加班时长"""
# self._sync_to_employee(reverse=True)
# return super(VacationOverline, self).unlink()
def action_export_overtime_excel(self):
"""导出加班明细 Excel 报表 - 打开向导选择日期"""
# 获取当前选中的记录 ID(来自前端 JS 传的参数)
active_ids = self.ids if self else []
if not active_ids:
# 如果没传 ID,就导出当前筛选条件下的全部记录
domain = self.env.context.get('search_default_domain') or []
active_ids = self.search(domain).ids
return {
'name': '打印薪资',
'type': 'ir.actions.act_window',
'res_model': 'export.date.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_export_type': 'overtime',
'default_model_name': 'vacation.over.line',
'default_active_ids': json.dumps(active_ids),
}
}
def action_export_overtime_detail_excel(self):
"""导出加班详情 Excel 报表 - 打开向导选择日期"""
# 获取当前选中的记录 ID
active_ids = self.ids if self else []
if not active_ids:
domain = self.env.context.get('search_default_domain') or []
active_ids = self.search(domain).ids
return {
'name': '打印薪资',
'type': 'ir.actions.act_window',
'res_model': 'export.date.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_export_type': 'overtime_detail',
'default_model_name': 'vacation.over.line',
'default_active_ids': json.dumps(active_ids),
}
}
class VacationMealLine(models.Model):
_name = 'vacation.meal.line'
_description = '误餐明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
is_filing = fields.Boolean(string="归档")
employee_number = fields.Char(string='序号', related='employee_id.number', store=True)
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", 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='流程')
workflow_id = fields.Many2one(related="workflow_record_id.workflow_id", 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)
start_time = fields.Datetime(string="开始时间", compute="_compute_overtime", store=True)
end_time = fields.Datetime(string="结束时间", compute="_compute_overtime", store=True)
duration = fields.Float(string="时长", compute="_compute_overtime", store=True)
reasons = fields.Text(string="申请事由", compute="_compute_overtime", store=True)
meal = fields.Selection([('yes', '是'), ('no', '否')], compute="_compute_overtime", string='是否误餐', default=None)
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()
def confirm(self):
valid_leaves = self.env['vacation.meal.line'].browse(
self._context.get('active_ids', self._context.get('active_id')))
emp_month_summary = {}
for rec in valid_leaves:
rec_year = rec.start_time.year
rec_month = rec.start_time.month
rec.write({'is_filing': True})
emp_id = rec.employee_id.id
active_exists = self.env['vacation.meal.active'].search([
('line_id', '=', rec.id),
('employee_id', '=', emp_id),
('carry_forward_state', '=', 'completed')
], limit=1)
if not active_exists:
meal_active_vals = {
'line_id': rec.id,
'meal_name': rec.meal_name,
'employee_id': emp_id,
'workflow_id': rec.workflow_id.id if rec.workflow_id else False,
'start_time': rec.start_time,
'end_time': rec.end_time,
'duration': rec.duration,
'update_lines': rec.update_lines,
'reasons': rec.reasons,
'meal': rec.meal,
'carry_forward_date': fields.Date.today(),
'carry_forward_state': 'completed',
'select_revoke': 'to_do'
}
self.env['vacation.meal.active'].create(meal_active_vals)
summary_key = (emp_id, rec_year, rec_month)
if summary_key not in emp_month_summary:
emp_month_summary[summary_key] = 0.0
emp_month_summary[summary_key] += rec.update_lines or 0.0
# 4. 第二步:遍历汇总结果,为每个【员工+年月】创建一条vacation.over.line
for (emp_id, rec_year, rec_month), total_amount in emp_month_summary.items():
if total_amount <= 1e-6:
continue
report_id = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', emp_id),
('vacation_date', '=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
], limit=1)
if report_id:
future_reps = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', emp_id),
('vacation_date', '>=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
], order='vacation_date asc')
for rep in future_reps:
rep.update_line()
over_exists = self.env['vacation.over.line'].search([
('vacation_id', '=', report_id.id),
('employee_id', '=', emp_id),
('update_type', '=', 'reduce'),
('text', '=', f'{rec_month}月份误餐结转'),
], limit=1)
if not over_exists:
self.env['vacation.meal.line'].create([{
'vacation_id': report_id.id,
'employee_id': emp_id,
'update_type': 'reduce',
'update_lines': total_amount,
'update_date': fields.Date.today(),
'is_void': False,
'text': f'{rec_month}月份误餐结转',
}])
@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
rec.meal = 'yes'
res_id = rec.workflow_record_id.res_id
if res_model and res_id and res_model in self.env and rec.update_type == 'add':
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 ''
rec.start_time = overtime_id.start_time
rec.end_time = overtime_id.end_time
rec.duration = overtime_id.duration or ''
rec.reasons = overtime_id.reason_application or ''
rec.meal = overtime_id.meal
else:
go_approval_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
if go_approval_id:
if go_approval_id.is_day:
start_date = go_approval_id.start_date or ''
start_time = datetime.combine(start_date, time(1, 0, 0))
end_time = datetime.combine(go_approval_id.end_date, time(10, 0, 0))
duration = ((end_time - start_time).days + 1) * 8
else:
start_date = go_approval_id.date_start.date() or ''
start_time = go_approval_id.date_start
end_time = go_approval_id.date_end
duration = (end_time - start_time).total_seconds() / 3600
rec.duration = duration or ''
rec.reasons = go_approval_id.reason_application
rec.start_date = start_date
rec.start_time = start_time
rec.end_time = end_time
rec.meal = go_approval_id.meal
rec.workflow_id = rec.workflow_record_id.workflow_id.id
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 _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 VacationParentingLine(models.Model):
_name = 'vacation.parenting.line'
_description = '育儿明细'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工')
update_date = fields.Date(string='发放日期')
end_date = fields.Date(string='失效日期')
lines = fields.Float(string='额度')
text = fields.Text(string='备注')
is_confirm = fields.Boolean(string="是否确认")
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="假期类型")
leave_id = fields.Many2one('hr.leave', 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()