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