diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py index 10eeab9cb..0bd3dd7ec 100644 --- a/yuthon_hr_attendance/__manifest__.py +++ b/yuthon_hr_attendance/__manifest__.py @@ -30,6 +30,7 @@ 'views/inherit_planning_role_views.xml', 'views/inherit_planning_slot_views.xml', 'views/attendance_message_log.xml', + 'views/duty_adjustment_schedule_views.xml', 'views/inherit_planning_slot_template_views.xml', 'views/yuthon_customer_approval_views.xml', 'views/yuthon_shift_planning_views.xml', diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py index 98a88c0cb..444e9c856 100644 --- a/yuthon_hr_attendance/models/__init__.py +++ b/yuthon_hr_attendance/models/__init__.py @@ -39,6 +39,7 @@ from . import vacation_overtime_active from . import yuthon_overtime_line from . import overtime_active_line from . import vacation_meal_active +from . import duty_adjustment_schedule # from odoo.tools import SQL # import odoo # odoo.models.READ_GROUP_AGGREGATE.update({ diff --git a/yuthon_hr_attendance/models/duty_adjustment_schedule.py b/yuthon_hr_attendance/models/duty_adjustment_schedule.py new file mode 100644 index 000000000..a27f015fd --- /dev/null +++ b/yuthon_hr_attendance/models/duty_adjustment_schedule.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools, _ +from datetime import datetime, time, timedelta +from odoo.exceptions import UserError +from datetime import date, timedelta + + +class DutyAdjustmentSchedule(models.Model): + _name = 'duty.adjustment.schedule' + _description = '值班调休表' + + vacation_id = fields.Many2one('yuthon.vacation.reports', string="关联假期额度日志") + employee_id = fields.Many2one('hr.employee', string="员工") + is_adjustment = fields.Boolean(string="已调休") + hide_selection = fields.Selection([('legal_date', '法定日'), ('adjust_holidays', '调班日'), ('workday', '工作日')], string="", compute="_compute_is_hide") + is_hide = fields.Boolean(string="隐藏", compute='_compute_is_hide') + company_id = fields.Many2one(related='employee_id.company_id', string="公司") + duty_date = fields.Date(string="值班日期") + week_day = fields.Char(string='星期', compute='_compute_week_day', store=True) + + @api.depends('duty_date') + def _compute_is_hide(self): + """ + 最终规则(周末/调休日不隐藏): + - 法定日 → 隐藏(is_hide=True),原因=法定日 + - 调班日 → 隐藏(is_hide=True),原因=调班日 + - 平常工作日 → 隐藏(is_hide=True),原因=工作日 + - 调休日/周末 → 不隐藏(is_hide=False),无原因 + """ + for record in self: + record.is_hide = False + record.hide_selection = False + if not record.duty_date: + continue + date_type = record._process_shift_days(record.duty_date) + if date_type == 'legal_date': + record.is_hide = True + record.hide_selection = 'legal_date' + elif date_type == 'adjust_holidays': + record.is_hide = True + record.hide_selection = 'adjust_holidays' + elif date_type == 'workday': + record.is_hide = True + record.hide_selection = 'workday' + + @api.depends('duty_date') + def _compute_week_day(self): + week_map = {'0': '周一', '1': '周二', '2': '周三', '3': '周四', '4': '周五', '5': '周六', '6': '周日'} + for record in self: + if record.duty_date: + record.week_day = week_map[str(record.duty_date.weekday())] + else: + record.week_day = '' + + def compensation(self): + for record in self: + if record.vacation_id.vacation_state == 'confirmed': + raise UserError('该假期额度已确认,无法进行调休操作') + record.is_adjustment = True + over_record = self.env['vacation.over.line'].search([('employee_id', '=', record.employee_id.id), ('update_date', '=', record.duty_date), + ('vacation_id', '=', record.vacation_id.id)], limit=1) + if not over_record: + self.env['vacation.over.line'].create({ + 'employee_id': record.employee_id.id, + 'update_date': record.duty_date, + 'vacation_id': record.vacation_id.id, + 'valid_date': date(record.duty_date.year + 1, 6, 30), + 'workday': 8, + 'update_type': 'add', + 'text': '值班调休' + }) + record.vacation_id.update_line() + + def revoke(self): + for record in self: + record.is_adjustment = False + over_record = self.env['vacation.over.line'].search([('employee_id', '=', record.employee_id.id), ('update_date', '=', record.duty_date), + ('vacation_id', '=', record.vacation_id.id), ('text', '=', '值班调休')], limit=1) + if over_record: + over_record.unlink() + record.vacation_id.update_line() + + def _process_shift_days(self, current_data): + """ + 判断指定日期的类型: + - 法定日(holidays)→ 'legal_date' + - 调班日(shift_replacement_day)→ 'adjust_holidays' + - 平常工作日(1-5)→ 'workday' + - 调休日(alternate_rest_day)/周末 → None + """ + if not current_data: + return None + current_year = current_data.year + query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8) + query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8) + calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([ + ("calendar_id.name", "=", "行政考勤工作日"), + ("date_from", ">=", query_start), + ("date_from", "<=", query_end) + ]) + holidays_list = [] # 法定日 + shift_days_list = [] # 调班日(对应adjust_holidays) + adjust_days_list = [] # 调休日(不隐藏,无原因) + + for leave in calendar_leaves: + if leave.holidays: + holidays_list.extend(leave.holidays.split(',')) + if leave.shift_replacement_day: # 调班日 + shift_days_list.extend(leave.shift_replacement_day.split(',')) + if leave.alternate_rest_day: # 调休日(仅判断,不赋值原因) + adjust_days_list.extend(leave.alternate_rest_day.split(',')) + + holidays_list = [self.remove_leading_zero(d) for d in holidays_list if d] + shift_days_list = [self.remove_leading_zero(d) for d in shift_days_list if d] + current_date_str = f"{current_data.month}月{current_data.day}日" + + if current_date_str in holidays_list: + return 'legal_date' # 法定日 + elif current_date_str in shift_days_list: + return 'adjust_holidays' # 调班日 + elif current_data.weekday() in [0, 1, 2, 3, 4]: + return 'workday' # 平常工作日(周一到周五) + else: + return None # 调休日/周末(不隐藏,无原因) + + # 移除日期字符串中的前导零 + 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}日' + diff --git a/yuthon_hr_attendance/models/planning_template_inherit.py b/yuthon_hr_attendance/models/planning_template_inherit.py index 15628328c..34f412956 100644 --- a/yuthon_hr_attendance/models/planning_template_inherit.py +++ b/yuthon_hr_attendance/models/planning_template_inherit.py @@ -88,19 +88,6 @@ class PlanningSlotTemplate(models.Model): } return action - # @api.depends("planning_name") - # def _compute_display_name(self): - # super()._compute_display_name() - # for record in self: - # if record.planning_name and record.role_id: - # record.display_name = f"{record.role_id.name}-{record.planning_name}" - # elif record.planning_name: - # record.display_name = record.planning_name - # elif record.role_id: - # record.display_name = record.role_id.name - # else: - # record.display_name = "Unknown" - @api.depends('role_id','planning_name') def _compute_display_name(self): for shift_template in self: diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index 38ca0f8b0..ee1378ae4 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -57,6 +57,7 @@ class YuthonVacationReports(models.Model): medical_fee_ids = fields.One2many('vacation.medical.line', 'vacation_id', string='体检费明细') parenting_line_ids = fields.One2many('vacation.parenting.line', 'vacation_id', string='育儿假明细') other_fee_ids = fields.One2many('vacation.other.line', 'vacation_id', string='其他明细') + duty_ids = fields.One2many('duty.adjustment.schedule', 'vacation_id', string='值班调休明细', compute='_compute_duty_ids') vacation_date = fields.Date(string='日期', default=lambda self: fields.Date.today().replace(day=1)) show_date = fields.Date(string='日期', compute='_compute_display_date', store=True) display_date = fields.Char(string='日期', compute='_compute_display_date') @@ -68,13 +69,14 @@ class YuthonVacationReports(models.Model): 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) - duty_off_leave = fields.Float(string='值班调休d', compute='_compute_other_leave', store=True, 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) @@ -117,6 +119,77 @@ class YuthonVacationReports(models.Model): tracking=3, default='not_sent') + @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'] + + 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) + + 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() + + 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 + }) + # 批量创建(效率更高) + if create_vals: + duty_schedule_env.create(create_vals) + record.duty_ids = duty_schedule_env.search([ + ('vacation_id', '=', record.id), + ('duty_date', '>=', month_start), + ('duty_date', '<=', month_end) + ]) + + + @api.depends('duty_ids.is_adjustment') + def _compute_duty_fee(self): + for record in self: + total_duty_count = len(record.duty_ids) + duty_off_count = len([line for line in record.duty_ids if line.is_adjustment]) + record.duty_off_leave = duty_off_count + record.duty = max(total_duty_count - duty_off_count, 0) + @api.depends('vacation_date', 'employee_id') def _compute_number_card(self): for record in self: @@ -181,7 +254,6 @@ class YuthonVacationReports(models.Model): 'context': {'tree_view_ref': "yuthon_hr_attendance.view_hr_leave_statement_tree"}, } - def import_initial_data(self): return { 'type': 'ir.actions.act_window', @@ -312,7 +384,7 @@ class YuthonVacationReports(models.Model): rec.used_this_year1 += deduct_from_current1 # 累加到本年已休 remaining_overtime_deduct -= deduct_from_current1 - add_overtime_lines = rec.overtime_ids.filtered(lambda o: o.workflow_record_id and o.update_type == 'add' and o.is_void != True) + 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 @@ -1134,7 +1206,6 @@ class YuthonVacationReports(models.Model): '调休': 'compensatory_leave', '事假': 'leave_of_absence', '法定假': 'statutory_leave', - '值班调休': 'duty_off_leave', '婚假': 'marriage_leave', '产假': 'maternity_leave', '陪产假': 'paternity_leave', @@ -1158,7 +1229,7 @@ class YuthonVacationReports(models.Model): current_value = getattr(rec, field_name) or 0 setattr(rec, field_name, current_value + line.lines) int_fields = [ - 'compensatory_leave', 'statutory_leave', 'duty_off_leave', + 'compensatory_leave', 'statutory_leave', 'marriage_leave', 'maternity_leave', 'paternity_leave', 'family_planning_leave', 'home_visit_leave', 'parents_meeting_leave', 'exam_leave' diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv index 0b478949e..b553f270a 100644 --- a/yuthon_hr_attendance/security/ir.model.access.csv +++ b/yuthon_hr_attendance/security/ir.model.access.csv @@ -95,4 +95,5 @@ access_overtime_active_line,overtime.active.line,model_overtime_active_line,base access_export_date_wizard,export.date.wizard,model_export_date_wizard,base.group_user,1,1,1,1 access_vacation_meal_active,vacation.meal.active,model_vacation_meal_active,base.group_user,1,1,1,1 access_yuthon_vacation_import_wizard,yuthon.vacation.import.wizard,model_yuthon_vacation_import_wizard,base.group_user,1,1,1,1 -access_overtime_change_wizard,overtime.change.wizard,model_overtime_change_wizard,base.group_user,1,1,1,1 \ No newline at end of file +access_overtime_change_wizard,overtime.change.wizard,model_overtime_change_wizard,base.group_user,1,1,1,1 +access_duty_adjustment_schedule,duty.adjustment.schedule,model_duty_adjustment_schedule,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml b/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml new file mode 100644 index 000000000..455be456a --- /dev/null +++ b/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml @@ -0,0 +1,20 @@ + + + duty.adjustment.schedule.tree + duty.adjustment.schedule + + + + + + + + + + +