3937 lines
186 KiB
Python
3937 lines
186 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 datetime import datetime
|
||
import json
|
||
from odoo.tools import date_utils
|
||
from dateutil.relativedelta import relativedelta
|
||
from odoo.exceptions import UserError, ValidationError
|
||
|
||
# 与 inherit_hr_leave.action_submit 中的 OTHER_LEAVE_TYPES 保持一致:
|
||
# 排除「年假」「补休」(它们分别落到 vacation.leave.line / vacation.over.line),其余都进 vacation.other.line。
|
||
# 注:原 update_line 中专门的「产假」分支(unpaid_leaves)已移除,产假统一由此处自愈逻辑处理,
|
||
# 去重键为 (vacation_id, leave_id),与提交侧 _create_other_leave_line 一致,不会重复。
|
||
# 如 action_submit 的类型列表有调整,需同步此处。
|
||
OTHER_LEAVE_SYNC_TYPES = [
|
||
'产假', '事假', '法定假', '调休', '值班调休', '婚假', '陪产假',
|
||
'计生假', '丧假', '探亲假', '哺乳假', '家长会假', '病假(普通)',
|
||
'病假(住院期)', '育儿假', '病假(出院恢复期)', '考试假',
|
||
]
|
||
|
||
|
||
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(string="允许迟到次数", compute="_compute_late_allowed_fields", store=False)
|
||
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))
|
||
can_next_month = fields.Boolean(string='可切换下月', compute='_compute_can_next_month')
|
||
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, 1),
|
||
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')
|
||
|
||
last_reminder_date = fields.Date(string="最后提醒日期", copy=False)
|
||
|
||
@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')
|
||
@api.depends('employee_id', 'vacation_date')
|
||
def _compute_late_allowed_fields(self):
|
||
for record in self:
|
||
# 每月发放:天河物管员工 3 次允许迟到次数,其余公司 0
|
||
if record.employee_id and record.employee_id.company_id.id == 3:
|
||
grant = 3
|
||
else:
|
||
grant = 0
|
||
# 当月已使用的宽容迟到次数(is_late_allowed=True 的考勤记录数)
|
||
used = 0
|
||
if record.employee_id and record.vacation_date:
|
||
month_start = record.vacation_date.replace(day=1)
|
||
month_end = month_start + relativedelta(months=1)
|
||
used = self.env['hr.attendance'].search_count([
|
||
('employee_id', '=', record.employee_id.id),
|
||
('current_data', '>=', month_start),
|
||
('current_data', '<', month_end),
|
||
('is_late_allowed', '=', True),
|
||
])
|
||
# 允许迟到次数(剩余)= 发放 - 已用
|
||
record.late_allowed = max(0, grant - used)
|
||
|
||
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:])
|
||
|
||
def batch_delete_bu_xiu_july_2026(self):
|
||
"""
|
||
批量删除用户在 tree 视图中勾选的 yuthon.vacation.reports 记录对应的
|
||
vacation.over.line(加班明细)中满足以下条件的记录:
|
||
1. 明细行属于 加班 页签(即在 overtime_ids 中)
|
||
2. workflow_record_id 字段为空(未关联审批记录)
|
||
3. 备注(text) == "补休"
|
||
"""
|
||
# 1. 只处理用户在 tree 视图中勾选的记录(self 即为勾选的记录集)
|
||
if not self:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': _('提示'),
|
||
'message': _('请先在列表中勾选要处理的记录'),
|
||
'type': 'warning',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
# 2. 收集所有需要删除的 vacation.over.line 记录 ID
|
||
# 筛选条件:
|
||
# - 加班页签(overtime_ids,自动满足)
|
||
# - workflow_record_id 字段为空
|
||
# - 备注(text) == "补休"
|
||
to_delete_ids = []
|
||
for report in self:
|
||
for over_line in report.overtime_ids:
|
||
if not over_line.workflow_record_id and over_line.text == '补休':
|
||
to_delete_ids.append(over_line.id)
|
||
if not to_delete_ids:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': _('提示'),
|
||
'message': _('勾选的记录中没有满足条件(加班页签 + workflow_record_id 为空 + 备注为"补休")的明细'),
|
||
'type': 'warning',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
# 3. 批量删除(使用 sudo 避免权限问题)
|
||
delete_count = len(to_delete_ids)
|
||
self.env['vacation.over.line'].sudo().browse(to_delete_ids).unlink()
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': _('删除成功'),
|
||
'message': _('已成功删除 %d 条满足条件的加班明细(加班页签 + workflow_record_id 为空 + 备注为"补休")') % delete_count,
|
||
'type': 'success',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
|
||
@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_parenting = rec.parenting_line_ids.filtered(lambda o:o.text == '上月额度继承')
|
||
if not self_parenting:
|
||
# 更新育儿假
|
||
self.employee_id.update_parental_leave()
|
||
|
||
# 如果上个月加班是0 删除明细
|
||
if self_overtime.workday == 0:
|
||
self_overtime.unlink()
|
||
|
||
# 如果上个月误餐费是0 删除明细
|
||
if rec.remain_missed_meals <= 0:
|
||
self_meal_fee = rec.meal_fee_ids.filtered(lambda o: o.text == '上月额度继承')
|
||
if self_meal_fee and self_meal_fee.update_lines:
|
||
self_meal_fee.unlink()
|
||
|
||
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 = 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_parental_leave()
|
||
rec._compute_working_overtime()
|
||
|
||
# ========== 第三步:自愈 - 补建未计入的「其他类型」请假明细 ==========
|
||
# 针对产假等长假期:请假跨度覆盖未来月份,而 yuthon.vacation.reports 只提前一个月生成,
|
||
# 未来月份的报表在对应月份真正到来、重新计算(update_line)时,需把尚未计入的请假补建到明细行。
|
||
# 年假/补休有各自明细模型(vacation.leave.line / vacation.over.line),不在此处理。
|
||
for rec in self:
|
||
rep_month_start = rec.vacation_date
|
||
if not rep_month_start or not rec.employee_id:
|
||
continue
|
||
# 该月最后一天
|
||
if rep_month_start.month == 12:
|
||
rep_month_end = date(rep_month_start.year + 1, 1, 1) - timedelta(days=1)
|
||
else:
|
||
rep_month_end = date(rep_month_start.year, rep_month_start.month + 1, 1) - timedelta(days=1)
|
||
# 找覆盖本月、属于「其他类型」的请假明细(hr.leave.detail,batch_id 非空)
|
||
leaves = self.env['hr.leave'].search([
|
||
('employee_id', '=', rec.employee_id.id),
|
||
('holiday_status_id.name', 'in', OTHER_LEAVE_SYNC_TYPES),
|
||
('state', '=', 'validate'),
|
||
('request_date_from', '<=', fields.Date.to_string(rep_month_end)),
|
||
('request_date_to', '>=', fields.Date.to_string(rep_month_start)),
|
||
('batch_id', '!=', False),
|
||
])
|
||
for leave in leaves:
|
||
# 去重:同一报表 + 同一明细请假 已存在则跳过(避免与提交侧重复创建)
|
||
if self.env['vacation.other.line'].search([
|
||
('vacation_id', '=', rec.id),
|
||
('leave_id', '=', leave.id),
|
||
], limit=1):
|
||
continue
|
||
# 计算本月区间内的天数(与提交侧 _create_other_leave_line 的算法一致)
|
||
seg_start = max(fields.Date.from_string(leave.request_date_from), rep_month_start)
|
||
seg_end = min(fields.Date.from_string(leave.request_date_to), rep_month_end)
|
||
batch = leave.batch_id
|
||
days = self._compute_duration_day(leave.holiday_status_id, seg_start, seg_end)
|
||
if not days or days <= 0:
|
||
continue
|
||
workflow_id = False
|
||
if batch and batch.code:
|
||
workflow_id = self.env['soong.workflow.record'].search([('code', '=', batch.code)], limit=1).id
|
||
self.env['vacation.other.line'].create({
|
||
'leave_id': leave.id,
|
||
'vacation_id': rec.id,
|
||
'employee_id': rec.employee_id.id,
|
||
'workflow_record_id': workflow_id,
|
||
'update_type': 'reduce',
|
||
'lines': days,
|
||
'holiday_type': leave.holiday_status_id.id,
|
||
'update_date': fields.Date.today(),
|
||
'is_void': False,
|
||
'text': '请假审批',
|
||
})
|
||
|
||
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 rec.employee_id.company_id.id == 3:
|
||
# 主表有效期:当年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. 天河粮储:年假6月底,补休6月底
|
||
elif "天河粮储" in company_name:
|
||
# 主表有效期:年假=当年6月底,补休=当年6月底
|
||
vacation_valid_date = date(current_year, 6, 30)
|
||
overtime_valid_date = date(current_year, 6, 30)
|
||
# 明细行:上月额度继承=对应主表有效期,年假分配/增加类=明年对应月底
|
||
inherit_valid_date = {
|
||
'leave': date(current_year, 6, 30), # 年假继承额度=6月底
|
||
'overtime': date(current_year, 6, 30) # 补休继承额度=6月底
|
||
}
|
||
allocation_valid_date = date(next_year, 6, 30) # 年假分配=明年6月底
|
||
add_valid_date = {
|
||
'leave': date(next_year, 6, 30), # 年假增加=明年6月底
|
||
'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 rec.employee_id.company_id.id == 3:
|
||
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 rec.employee_id.company_id.id == 3: # 修正:从"物管"改为"天河物管"
|
||
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"已存在假期额度记录,不能重复创建!"
|
||
)
|
||
|
||
@api.depends('vacation_date')
|
||
def _compute_can_next_month(self):
|
||
today = fields.Date.today()
|
||
for record in self:
|
||
next_month_date = (record.vacation_date + relativedelta(months=1)).replace(day=1)
|
||
record.can_next_month = next_month_date <= today
|
||
|
||
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()
|
||
if not self.can_next_month:
|
||
raise UserError('当前时间尚未进入下个月,无法切换!')
|
||
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('有加班单未结束,请先结束加班批次')
|
||
|
||
if rec.overtime_ids:
|
||
for overtime in rec.overtime_ids:
|
||
overtime.confirm_date = fields.Date.today()
|
||
if rec.meal_fee_ids:
|
||
for meal_fee in rec.meal_fee_ids:
|
||
meal_fee.confirm_date = fields.Date.today()
|
||
|
||
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 = False
|
||
employee = rec.employee_id
|
||
vacation_date = rec.vacation_date # 每月1号
|
||
if employee and vacation_date:
|
||
if employee.user_state in ('leave', 'withdraw'):
|
||
contract = employee.contract_id
|
||
leave_date = False
|
||
# 优先取合同中止日期
|
||
if contract and contract.end_date:
|
||
leave_date = contract.end_date
|
||
if leave_date and leave_date >= vacation_date:
|
||
is_retired = False
|
||
else:
|
||
is_retired = True
|
||
|
||
go_out_days = 0.0
|
||
processed_workflow_ids = set()
|
||
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. 计算应到天数(适配外派)
|
||
# 开始时间:取员工最早的合同(含已中止)的 date_start
|
||
# 结束时间:取运行中(state='open')合同的 date_end
|
||
earliest_contract = self.env['hr.contract'].search([
|
||
('employee_id', '=', employee.id),
|
||
], order='date_start asc, id asc', limit=1)
|
||
running_contract = self.env['hr.contract'].search([
|
||
('employee_id', '=', employee.id),
|
||
('state', '=', 'open'),
|
||
], order='date_end desc, id desc', limit=1)
|
||
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(earliest_contract.date_start, month_start) if (
|
||
earliest_contract and earliest_contract.date_start) else month_start
|
||
end_date = min(running_contract.date_end, month_end) if (
|
||
running_contract and running_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', '=', 3)], 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_annual_leave_allocation(self):
|
||
"""
|
||
根据勾选记录创建年假分配记录
|
||
逻辑:
|
||
1. 检查hr.leave.allocation中是否已有该员工的年假记录
|
||
2. 如果有,方法结束
|
||
3. 如果没有,根据公司类型和字段值创建年假分配记录
|
||
- 物管公司:图二(dynamic_remainder_year)有值创建2026-03-01到2027-02-28,图三(dynamic_previous_year)有值创建2025-03-01到2026-02-28
|
||
- 天河粮储公司:图二有值创建2026-09-01到2027-08-31,图三有值创建2025-09-01到2026-08-31
|
||
"""
|
||
records = self.env["yuthon.vacation.reports"].browse(
|
||
self._context.get('active_ids', self._context.get('active_id', self.ids))
|
||
)
|
||
|
||
for record in records:
|
||
employee = record.employee_id
|
||
company_name = employee.company_id.name if employee else ""
|
||
|
||
# 检查是否已有年假记录
|
||
existing_allocation = self.env['hr.leave.allocation'].search([
|
||
('employee_id', '=', employee.id),
|
||
('holiday_status_id.name', '=', '年假'),
|
||
('state', 'not in', ['refuse', 'cancel'])
|
||
], limit=1)
|
||
|
||
if existing_allocation:
|
||
continue # 已有年假记录,跳过
|
||
|
||
# 获取年假类型(按公司过滤)
|
||
holiday_status = self.env['hr.leave.type'].search([
|
||
('name', '=', '年假'),
|
||
('company_id', '=', employee.company_id.id)
|
||
], limit=1)
|
||
|
||
if not holiday_status:
|
||
continue # 没有找到对应的年假类型
|
||
|
||
# 根据公司类型创建年假分配
|
||
if "物管" in company_name:
|
||
# 物管公司:图二字段有值创建2026-03-01到2027-02-28
|
||
if record.dynamic_remainder_year > 0:
|
||
self._create_allocation_record(
|
||
employee=employee,
|
||
holiday_status=holiday_status,
|
||
leave_days=record.dynamic_remainder_year,
|
||
start_date=date(2026, 3, 1),
|
||
end_date=date(2027, 2, 28)
|
||
)
|
||
|
||
# 图三字段有值创建2025-03-01到2026-02-28
|
||
if record.dynamic_previous_year > 0:
|
||
self._create_allocation_record(
|
||
employee=employee,
|
||
holiday_status=holiday_status,
|
||
leave_days=record.dynamic_previous_year,
|
||
start_date=date(2025, 3, 1),
|
||
end_date=date(2026, 2, 28)
|
||
)
|
||
|
||
elif "天河粮储" in company_name:
|
||
# 天河粮储公司:图二字段有值创建2026-09-01到2027-08-31
|
||
if record.dynamic_remainder_year > 0:
|
||
self._create_allocation_record(
|
||
employee=employee,
|
||
holiday_status=holiday_status,
|
||
leave_days=record.dynamic_remainder_year,
|
||
start_date=date(2026, 9, 1),
|
||
end_date=date(2027, 8, 31)
|
||
)
|
||
|
||
# 图三字段有值创建2025-09-01到2026-08-31
|
||
if record.dynamic_previous_year > 0:
|
||
self._create_allocation_record(
|
||
employee=employee,
|
||
holiday_status=holiday_status,
|
||
leave_days=record.dynamic_previous_year,
|
||
start_date=date(2025, 9, 1),
|
||
end_date=date(2026, 8, 31)
|
||
)
|
||
|
||
def _create_allocation_record(self, employee, holiday_status, leave_days, start_date, end_date):
|
||
"""
|
||
创建年假分配记录
|
||
优化:分配类别选择应计分配,应计方案选择对应公司的方案,创建后自动触发验证
|
||
"""
|
||
# 获取对应公司的应计方案
|
||
company_name = employee.company_id.name if employee else ""
|
||
accrual_plan = self.env['hr.leave.accrual.plan'].search([
|
||
('name', 'ilike', company_name)
|
||
], limit=1)
|
||
|
||
vals = {
|
||
'name': '年假',
|
||
'number_of_days': leave_days,
|
||
'holiday_status_id': holiday_status.id,
|
||
'allocation_type': 'accrual', # 应计分配
|
||
'accrual_plan_id': accrual_plan.id if accrual_plan else False, # 对应公司的应计方案
|
||
'date_from': fields.Date.to_string(start_date),
|
||
'date_to': fields.Date.to_string(end_date),
|
||
'notes': '自动创建的年假分配',
|
||
'employee_id': employee.id,
|
||
'holiday_type': 'employee',
|
||
}
|
||
# 创建记录并自动触发验证
|
||
allocation = self.env['hr.leave.allocation'].create(vals)
|
||
allocation.action_validate()
|
||
|
||
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),
|
||
('resource_id', '=', False),
|
||
])
|
||
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, active_ids=None):
|
||
"""一键发送考勤确认单通知。
|
||
|
||
如果前端勾选了记录,则发送这些未确认的记录(未发送/待确认均可重发);
|
||
否则默认发送上月未发送的记录。
|
||
"""
|
||
today = fields.Date.today()
|
||
first_day = today.replace(day=1)
|
||
last_month_first = first_day - relativedelta(months=1)
|
||
if active_ids:
|
||
# 前端有勾选记录时,按勾选记录处理
|
||
report_objs = self.env['yuthon.vacation.reports'].browse(active_ids)
|
||
else:
|
||
# 没有勾选时,保持原逻辑:发送上月未发送的记录
|
||
report_objs = self.env['yuthon.vacation.reports'].search([
|
||
('vacation_date', '=', last_month_first),
|
||
('vacation_state', '=', 'not_sent'),
|
||
])
|
||
processed_employees = set()
|
||
send_count = 0
|
||
for report in report_objs:
|
||
# 已确认的记录不需要再发
|
||
if report.vacation_state == 'confirmed':
|
||
continue
|
||
if report.employee_id.id in processed_employees:
|
||
# 同员工已发送过通知,多余记录只改状态不重复发
|
||
report.write({'vacation_state': 'pending'})
|
||
continue
|
||
processed_employees.add(report.employee_id.id)
|
||
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()
|
||
send_count += 1
|
||
# 给管理员发统计消息
|
||
if send_count > 0:
|
||
if active_ids:
|
||
admin_msg = f"考勤确认单一键发送完成:共发送{send_count}条通知"
|
||
else:
|
||
year = last_month_first.year
|
||
month = last_month_first.month
|
||
admin_msg = f"考勤确认单一键发送完成:{year}年{month}月,共发送{send_count}条通知"
|
||
self.sudo().env["wecom.apps"].sync_send_message(category='att_ok', user_id='18562027762', message=admin_msg)
|
||
# 返回通知 action,避免前端 doAction(undefined) 报错
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'type': 'success',
|
||
'message': f'已发送{send_count}条通知',
|
||
'sticky': False,
|
||
}
|
||
}
|
||
|
||
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'),
|
||
])
|
||
processed_employees = set()
|
||
send_count = 0
|
||
for report in report_objs:
|
||
if report.employee_id.id in processed_employees:
|
||
report.write({'vacation_state': 'confirmed'})
|
||
continue
|
||
processed_employees.add(report.employee_id.id)
|
||
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()
|
||
send_count += 1
|
||
# 给管理员发统计消息
|
||
if send_count > 0:
|
||
year = last_month_first.year
|
||
month = last_month_first.month
|
||
admin_msg = f"考勤确认单定时发送完成:{year}年{month}月,共发送{send_count}条通知"
|
||
self.sudo().env["wecom.apps"].sync_send_message(category='att_ok', user_id='18562027762', message=admin_msg)
|
||
|
||
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.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号之后不执行
|
||
if today.day > 20:
|
||
return
|
||
|
||
# 2. 只提醒上月的考勤确认单
|
||
first_day = today.replace(day=1)
|
||
last_month_first = first_day - relativedelta(months=1)
|
||
domain = [
|
||
('vacation_state', '=', 'pending'),
|
||
('vacation_date', '=', last_month_first),
|
||
]
|
||
|
||
# 3. 批量处理,同员工只发一次,且2天内不重复提醒
|
||
records = self.sudo().search(domain)
|
||
processed_employees = set()
|
||
send_count = 0
|
||
today_date = fields.Date.today()
|
||
for record in records:
|
||
if record.employee_id.id in processed_employees:
|
||
continue
|
||
processed_employees.add(record.employee_id.id)
|
||
|
||
# 2天内已提醒过的跳过
|
||
if record.last_reminder_date and (today_date - record.last_reminder_date).days < 2:
|
||
continue
|
||
|
||
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="详细信息")
|
||
record.last_reminder_date = today_date
|
||
send_count += 1
|
||
# 给管理员发统计消息
|
||
if send_count > 0:
|
||
year = last_month_first.year
|
||
month = last_month_first.month
|
||
admin_msg = f"考勤确认单待确认提醒发送完成:{year}年{month}月,共发送{send_count}条通知"
|
||
self.sudo().env["wecom.apps"].sync_send_message(category='att_ok', user_id='18562027762', message=admin_msg)
|
||
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),
|
||
('resource_id', '=', False),
|
||
])
|
||
holidays_list = []
|
||
shift_days_list = []
|
||
# 提取节假日和调休日
|
||
for leave in calendar_leaves:
|
||
if leave.holidays:
|
||
holidays_list.extend(leave.holidays.split(','))
|
||
if leave.alternate_rest_day:
|
||
holidays_list.extend(leave.alternate_rest_day.split(','))
|
||
if leave.shift_replacement_day:
|
||
shift_days_list.extend(leave.shift_replacement_day.split(','))
|
||
|
||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
|
||
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 _process_shift_days1(self, current_data):
|
||
"""
|
||
只判断:是否为法定节假日
|
||
返回:True = 法定节假日
|
||
False = 非法定节假日
|
||
"""
|
||
current_year = current_data.year
|
||
query_start = datetime(current_year, 1, 1)
|
||
query_end = datetime(current_year, 12, 31, 23, 59, 59)
|
||
# 只取法定节假日配置
|
||
calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
|
||
("calendar_id.name", "=", "行政考勤工作日"),
|
||
("date_from", ">=", query_start),
|
||
("date_from", "<=", query_end),
|
||
('resource_id', '=', False),
|
||
])
|
||
|
||
holidays_list = []
|
||
for leave in calendar_leaves:
|
||
if leave.holidays:
|
||
holidays_list.extend(leave.holidays.split(','))
|
||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||
current_date_str = f"{current_data.month}月{current_data.day}日"
|
||
return current_date_str in holidays_list
|
||
|
||
def all_put(self):
|
||
vacation_ids = self.env["yuthon.vacation.reports"].browse(self._context.get('active_ids', self._context.get('active_id')))
|
||
processed_employees = set()
|
||
send_count = 0
|
||
for rec in vacation_ids:
|
||
# 已发送/已确认的不再重复发送
|
||
if rec.vacation_state in ('pending', 'confirmed'):
|
||
continue
|
||
if rec.employee_id.id in processed_employees:
|
||
# 同员工已发送过通知,多余记录只改状态不重复发消息
|
||
rec.write({'vacation_state': 'pending'})
|
||
continue
|
||
processed_employees.add(rec.employee_id.id)
|
||
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="详细信息")
|
||
send_count += 1
|
||
rec.write({'vacation_state': 'pending'})
|
||
# 给管理员发统计消息
|
||
if send_count > 0:
|
||
admin_msg = f"考勤确认单批量发送完成:共发送{send_count}条通知"
|
||
self.sudo().env["wecom.apps"].sync_send_message(category='att_ok', user_id='18562027762', message=admin_msg)
|
||
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'
|
||
if rec.overtime_ids:
|
||
for overtime in rec.overtime_ids:
|
||
overtime.confirm_date = fields.Date.today()
|
||
if rec.meal_fee_ids:
|
||
for meal_fee in rec.meal_fee_ids:
|
||
meal_fee.confirm_date = fields.Date.today()
|
||
|
||
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
|
||
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 rec.remain_missed_meals:
|
||
self._update_meal_lines(rec, [rec], rec.remain_missed_meals)
|
||
if rec.leave != 0:
|
||
leave_hours = rec.leave * 8
|
||
self._update_leave_lines(rec, [rec], leave_hours)
|
||
if rec.working_overtime != 0:
|
||
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)
|
||
else:
|
||
# 没有上月考勤记录,继承数据全部置0
|
||
# valid_date / valid_date1 根据公司名设置(参照 update_vacation1 逻辑)
|
||
company_name = rec.employee_id.company_id.name if rec.employee_id else ""
|
||
valid_year = rec.vacation_date.year
|
||
vacation_valid_date = None
|
||
overtime_valid_date = None
|
||
if company_name == "集团本部":
|
||
vacation_valid_date = None
|
||
overtime_valid_date = None
|
||
elif rec.employee_id.company_id.id == 3:
|
||
vacation_valid_date = date(valid_year, 2, 28)
|
||
overtime_valid_date = date(valid_year, 2, 28)
|
||
elif "天河粮储" in company_name:
|
||
vacation_valid_date = date(valid_year, 6, 30)
|
||
overtime_valid_date = date(valid_year, 6, 30)
|
||
rec.write({
|
||
'remain_missed_meals': 0.0,
|
||
'leave': 0.0,
|
||
'dynamic_previous_year': 0.0,
|
||
'dynamic_remainder_year': 0.0,
|
||
'dynamic_previous_year1': 0.0,
|
||
'this_year1': 0.0,
|
||
'working_overtime': 0.0,
|
||
'medical_fee': 0.0,
|
||
'previous_year': 0.0,
|
||
'remaining_parental_leave': 0.0,
|
||
'year_distribution': 0.0,
|
||
'used_this_year': 0.0,
|
||
'last_holiday': 0.0,
|
||
'last_workday': 0.0,
|
||
'year_holiday': 0.0,
|
||
'year_workday': 0.0,
|
||
'previous_year1': 0.0,
|
||
'used_this_year1': 0.0,
|
||
'valid_date': vacation_valid_date,
|
||
'valid_date1': overtime_valid_date,
|
||
})
|
||
if rec.employee_id.id not in target_employee_ids:
|
||
rec.remaining_number_card = 3
|
||
else:
|
||
rec.remaining_number_card = 0
|
||
|
||
@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 cron_auto_recompute_vacation(self):
|
||
"""定时任务:每月1号自动批量重新计算上月假期额度"""
|
||
today = fields.Date.today()
|
||
last_month_first = today.replace(day=1) - timedelta(days=1)
|
||
last_month_first = last_month_first.replace(day=1)
|
||
|
||
records = self.search([('vacation_date', '=', last_month_first)])
|
||
if records:
|
||
records.with_context(active_ids=records.ids).batch_compute_leave_and_overtime()
|
||
|
||
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',
|
||
'employee_id',
|
||
'vacation_date'
|
||
)
|
||
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
|
||
shift_total_hours = 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
|
||
# ===================== 原有逻辑 结束 =====================
|
||
|
||
# ===================== 新增:排班加班逻辑(含你的新规则) =====================
|
||
employee = record.employee_id
|
||
vacation_date = record.vacation_date
|
||
|
||
if employee and vacation_date:
|
||
# 1. 获取【当前员工最新一条排班】
|
||
shift_planning = self.env['yuthon.shift.planning'].search([], order='start_date desc', limit=1)
|
||
rest_weekday = None
|
||
if shift_planning:
|
||
# 2. 查5个排班表,取固定休息日
|
||
shift_models = [
|
||
'yuthon.shift.planning.a.line',
|
||
'yuthon.shift.planning.b.line',
|
||
'yuthon.shift.planning.c.line',
|
||
'yuthon.shift.ever.white.line',
|
||
'yuthon.shift.ever.noon.line',
|
||
|
||
]
|
||
rest_code = False
|
||
for model in shift_models:
|
||
line = self.env[model].search([
|
||
('shift_planning_id', '=', shift_planning.id),
|
||
('employee_id', '=', employee.id)
|
||
], limit=1)
|
||
if line:
|
||
rest_code = line.regular_holidays
|
||
break
|
||
|
||
week_map = {'one': 0, 'two': 1, 'three': 2, 'four': 3, 'five': 4, 'six': 5, 'seven': 6}
|
||
rest_weekday = week_map.get(rest_code)
|
||
|
||
# 3. 遍历当月所有天
|
||
dt = fields.Date.from_string(vacation_date)
|
||
year, month = dt.year, dt.month
|
||
_, days = calendar.monthrange(year, month)
|
||
|
||
for day in range(1, days + 1):
|
||
curr_date = datetime(year, month, day).date()
|
||
date_str = fields.Date.to_string(curr_date)
|
||
prev_date_str = fields.Date.to_string(curr_date - timedelta(days=1))
|
||
|
||
slot = self.env['planning.slot'].search([
|
||
('employee_id', '=', employee.id),
|
||
('start_datetime', '<=', date_str + ' 16:00:00'),
|
||
('end_datetime', '>=', prev_date_str + ' 16:00:00'),
|
||
], limit=1)
|
||
plan_name = slot.display_name or '' # 修正:用 name 不是 plan_name
|
||
|
||
# 日期类型判断
|
||
is_legal_holiday = self._process_shift_days1(curr_date)
|
||
|
||
# ===================== 规则1:固定休息日前一天 =====================
|
||
if rest_weekday is not None and curr_date.weekday() == rest_weekday - 1:
|
||
if not slot:
|
||
continue
|
||
work_hours = 0.0
|
||
if plan_name in ('旧白', '旧晚'):
|
||
work_hours = 12.0 if is_legal_holiday else 4.0
|
||
elif plan_name in ('旧白固加', '旧晚固加'):
|
||
work_hours = 12.0
|
||
else:
|
||
# 不属于以上班次 → 固定算8小时
|
||
work_hours = 8.0
|
||
|
||
# 计入加班
|
||
if is_legal_holiday:
|
||
legal_total += work_hours
|
||
else:
|
||
total_add += work_hours
|
||
total_add1 += work_hours
|
||
|
||
if slot:
|
||
work_hours = 0.0
|
||
is_special_shift = any(key in plan_name for key in ['旧白', '旧晚', '旧固加白', '旧固加晚', '全天班', '固加早', '固加夜', '固加中'])
|
||
if is_special_shift:
|
||
if '旧固加白' in plan_name or '旧固加晚' in plan_name:
|
||
work_hours = 12.0
|
||
elif '全天班' in plan_name:
|
||
work_hours = 24.0 if is_legal_holiday else 8.0
|
||
elif '旧白' in plan_name or '旧晚' in plan_name:
|
||
work_hours = 12.0 if is_legal_holiday else 4.0
|
||
elif '固加早' in plan_name or '固加夜' in plan_name or '固加中' in plan_name:
|
||
work_hours = 8.0
|
||
else:
|
||
if is_legal_holiday:
|
||
work_hours = 8.0
|
||
|
||
if is_legal_holiday:
|
||
legal_total += work_hours
|
||
else:
|
||
total_add += work_hours
|
||
total_add1 += work_hours
|
||
shift_total_hours += work_hours
|
||
|
||
record.working_overtime = (total_add / 8) + adjust_holiday_days
|
||
record.this_year1 += shift_total_hours / 8
|
||
record.done_working_overtime = total_add1
|
||
record.statutory_leave = legal_total
|
||
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),
|
||
('resource_id', '=', False),
|
||
])
|
||
holidays_list = []
|
||
shift_days_list = []
|
||
# 提取节假日和调休日
|
||
for leave in calendar_leaves:
|
||
if leave.holidays:
|
||
holidays_list.extend(leave.holidays.split(','))
|
||
if leave.alternate_rest_day:
|
||
holidays_list.extend(leave.alternate_rest_day.split(','))
|
||
if leave.shift_replacement_day:
|
||
shift_days_list.extend(leave.shift_replacement_day.split(','))
|
||
|
||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
|
||
days = 0 # 初始化工作日计数
|
||
current_date = start_date
|
||
while current_date <= end_date:
|
||
current_date_str = current_date.strftime('%m月%d日')
|
||
current_date_str = self.remove_leading_zero(current_date_str)
|
||
is_weekend = current_date.weekday() in [5, 6]
|
||
if current_date_str in holidays_list:
|
||
# 如果是法定节假日,不计为工作日
|
||
pass
|
||
elif current_date_str in shift_days_list:
|
||
# 如果是调休日,无论是否周末都算作工作日
|
||
days += 1
|
||
elif not is_weekend:
|
||
# 如果不是周末且不是节假日,算作工作日
|
||
days += 1
|
||
current_date += timedelta(days=1)
|
||
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:
|
||
existing_line.write({
|
||
'lines': number_of_hours_display,
|
||
})
|
||
else:
|
||
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.replace(day=today.day), datetime.max.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(current_month_first.month)
|
||
line.select_year = str(current_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', '=', current_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(current_month_first.month)),
|
||
('select_year', '=', str(current_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',
|
||
}
|
||
}
|
||
|
||
def _has_pending_hr_workflow(self, employee_id, month_date):
|
||
"""
|
||
检查员工在指定月份是否存在未办结的人力资源相关流程(请假、加班、外出等)
|
||
检查员工在指定月份是否存在未办结的人力资源相关流程(请假、加班、外出等)
|
||
搜索条件保留 users_ids 包含当前用户(不管是自己的流程还是需要审批的),
|
||
找到后再判断员工是否是流程的当事人(请假人/加班人/外出人/补卡人),
|
||
如果员工只是审批人(盖章),不算未办结,不阻止确认。
|
||
:param employee_id: hr.employee.id
|
||
:param month_date: 月份任一天(用于计算当月起止)
|
||
:return: (bool, str) 是否有未办结流程, 提示文字
|
||
"""
|
||
month_start = month_date.replace(day=1)
|
||
month_end = (month_start + relativedelta(months=1) - relativedelta(days=1))
|
||
|
||
employee = self.env['hr.employee'].sudo().browse(employee_id)
|
||
user = employee.user_id
|
||
if not user:
|
||
return False, ''
|
||
|
||
# 查找当月考勤分组下 users_ids 包含当前用户的未办结流程
|
||
pending_records = self.env['soong.workflow.record'].sudo().search([
|
||
('users_ids', 'in', user.id),
|
||
('approvals_group_id.name', '=', '考勤'),
|
||
('reclaim_state', '=', False),
|
||
('approve_state', '=', 'pending'),
|
||
('workflow_happen_date', '>=', month_start),
|
||
('workflow_happen_date', '<=', month_end),
|
||
])
|
||
pending_codes = []
|
||
for record in pending_records:
|
||
workflow_name = record.workflow_id.name
|
||
# 获取业务记录
|
||
biz_record = self.env[record.res_model].sudo().browse(record.res_id)
|
||
if not biz_record.exists():
|
||
continue
|
||
|
||
is_party = False
|
||
# 根据流程类型判断当事人字段
|
||
if workflow_name == '请假':
|
||
if hasattr(biz_record, 'users_id') and biz_record.users_id:
|
||
if biz_record.users_id == employee.user_id:
|
||
is_party = True
|
||
elif workflow_name == '加班':
|
||
if hasattr(biz_record, 'employee_ids') and biz_record.employee_ids:
|
||
if employee in biz_record.employee_ids:
|
||
is_party = True
|
||
elif workflow_name == '外出':
|
||
if hasattr(biz_record, 'employee_ids') and biz_record.employee_ids:
|
||
if employee in biz_record.employee_ids:
|
||
is_party = True
|
||
elif workflow_name == '补卡':
|
||
if hasattr(biz_record, 'users_id') and biz_record.users_id:
|
||
if biz_record.users_id == employee.user_id:
|
||
is_party = True
|
||
else:
|
||
# 其他考勤类型:如果员工是业务记录的 users_id 或在 employee_ids 中也算当事人
|
||
if hasattr(biz_record, 'users_id') and biz_record.users_id:
|
||
if biz_record.users_id == employee.user_id:
|
||
is_party = True
|
||
if hasattr(biz_record, 'employee_ids') and biz_record.employee_ids:
|
||
if employee in biz_record.employee_ids:
|
||
is_party = True
|
||
|
||
if is_party:
|
||
pending_codes.append(record.code)
|
||
|
||
if pending_codes:
|
||
codes_str = '、'.join(pending_codes)
|
||
return True, f'该月考勤相关流程尚未完结,暂不允许确认(流水号:{codes_str})'
|
||
|
||
return False, ''
|
||
|
||
class VacationLeaveLine(models.Model):
|
||
_name = 'vacation.leave.line'
|
||
_description = '年假明细'
|
||
|
||
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
|
||
allocation_id = fields.Many2one('hr.leave.allocation', 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)
|
||
confirm_date = fields.Date(string='确认日期')
|
||
allocation_id = fields.Many2one('hr.leave.allocation', string='关联假期分配')
|
||
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='误餐日期')
|
||
confirm_date = fields.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')))
|
||
# 检查确认日期,没有则提示未确认
|
||
for rec in valid_leaves:
|
||
if not rec.confirm_date:
|
||
raise UserError("未确认,请勿归档")
|
||
|
||
emp_month_summary = {}
|
||
for rec in valid_leaves:
|
||
rec_year = rec.confirm_date.year
|
||
rec_month = rec.confirm_date.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
|
||
|
||
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),
|
||
('resource_id', '=', False),
|
||
])
|
||
holidays_list = []
|
||
shift_days_list = []
|
||
# 提取节假日和调休日
|
||
for leave in calendar_leaves:
|
||
if leave.holidays:
|
||
holidays_list.extend(leave.holidays.split(','))
|
||
if leave.alternate_rest_day:
|
||
holidays_list.extend(leave.alternate_rest_day.split(','))
|
||
if leave.shift_replacement_day:
|
||
shift_days_list.extend(leave.shift_replacement_day.split(','))
|
||
|
||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
|
||
|
||
days = 0 # 初始化工作日计数
|
||
current_date = start_date
|
||
|
||
while current_date <= end_date:
|
||
current_date_str = current_date.strftime('%m月%d日')
|
||
current_date_str = self.remove_leading_zero(current_date_str)
|
||
is_weekend = current_date.weekday() in [5, 6]
|
||
|
||
if current_date_str in holidays_list:
|
||
# 如果是法定节假日,不计为工作日
|
||
pass
|
||
elif current_date_str in shift_days_list:
|
||
# 如果是调休日,无论是否周末都算作工作日
|
||
days += 1
|
||
elif not is_weekend:
|
||
# 如果不是周末且不是节假日,算作工作日
|
||
days += 1
|
||
|
||
current_date += timedelta(days=1)
|
||
|
||
return days
|
||
|
||
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 = '消息提醒日志'
|
||
_order = 'update_date desc'
|
||
|
||
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")
|
||
msg_id = fields.Char(string='消息ID')
|
||
category = fields.Char(string='消息类别')
|
||
|
||
def action_recall_message(self):
|
||
"""撤回企微消息(调用企微back_message接口)"""
|
||
self.ensure_one()
|
||
if not self.msg_id:
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '撤回失败',
|
||
'message': '消息ID为空,无法撤回',
|
||
'type': 'warning',
|
||
}
|
||
}
|
||
category = self.category or 'att'
|
||
self.env['wecom.apps'].back_message(self.msg_id, category=category)
|
||
self.msg_id = '已撤回'
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '撤回成功',
|
||
'message': f'消息已撤回(msg_id={self.msg_id})',
|
||
'type': 'success',
|
||
}
|
||
}
|
||
|
||
def action_clean_old_logs(self):
|
||
"""清理7天前的消息提醒日志记录(定时任务和手动按钮共用)"""
|
||
# 计算截止日期:当前时间往前推7天
|
||
deadline = fields.Datetime.now() - timedelta(days=7)
|
||
# 搜索所有提醒时间早于截止日期的记录
|
||
old_logs = self.search([('update_date', '<', deadline)])
|
||
count = len(old_logs)
|
||
# 批量删除
|
||
old_logs.sudo().unlink()
|
||
# 返回清理结果通知(定时任务调用时自动忽略返回值)
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'display_notification',
|
||
'params': {
|
||
'title': '清理完成',
|
||
'message': f'已删除 {count} 条7天前的日志记录',
|
||
'type': 'success',
|
||
}
|
||
}
|
||
|
||
@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()
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|