diff --git a/yuthon_holiday/models/inherit_hr_leave.py b/yuthon_holiday/models/inherit_hr_leave.py index ddcf3af07..cdc39d3ea 100644 --- a/yuthon_holiday/models/inherit_hr_leave.py +++ b/yuthon_holiday/models/inherit_hr_leave.py @@ -7,6 +7,8 @@ from odoo.exceptions import UserError, ValidationError import re from odoo.tools.misc import format_date from collections import namedtuple, defaultdict +from datetime import date +import calendar class HrLeave(models.Model): @@ -198,13 +200,11 @@ class HrLeave(models.Model): def _process_annual_leave(self, allocation, employees): """处理年假逻辑""" days = allocation.number_of_days * 8 - for employee in employees: # 更新员工年假余额 employee.write({ 'user_year': employee.user_year + days }) - reports_id = self.env['yuthon.vacation.reports'].search([ ('employee_id', '=', employee.id) ], limit=1) @@ -212,7 +212,6 @@ class HrLeave(models.Model): reports_id = self.env['yuthon.vacation.reports'].create({ 'employee_id': employee.id, }) - self.env['vacation.leave.line'].create({ 'vacation_id': reports_id.id, 'update_type': 'reduce', @@ -291,7 +290,6 @@ class HrLeave(models.Model): for holiday in self: # 如果是自然日: if holiday.holiday_status_id.type_of_work == "1": - calendar = self.env['resource.calendar'].sudo().search([("name", "=", "行政考勤自然日")], limit=1) if holiday.request_date_from and holiday.request_date_to: days = (holiday.request_date_to - holiday.request_date_from).days + 1 hours = days * 8 @@ -300,7 +298,6 @@ class HrLeave(models.Model): hours = 0.0 holiday.number_of_hours = hours holiday.number_of_days = days - if holiday.holiday_status_id.type_of_work == "0": calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")]) holidays_list = [] # 假期 @@ -370,16 +367,8 @@ class HrLeave(models.Model): if is_weekend: days -= 1 current_date += timedelta(days=1) - holiday.number_of_days = days - def remove_leading_zero(self, date_str): - month, day = date_str.split('月') - month = month.lstrip('0') - day = day.rstrip('日') - day = day.lstrip('0') - return f'{month}月{day}日' - def _compute_request_unit_hours(self): if not self.env.context.get('no_change_request_unit_hours'): super()._compute_request_unit_hours() @@ -509,52 +498,222 @@ class HolidaysRequestBatch(models.Model): # 提交 def action_submit(self): - self.leave_ids.action_validate() - self.hour_leave_ids.action_validate() - days = 0 - leave_list = [] - reports_id = self.env['yuthon.vacation.reports'].search([ + """提交请假申请,区分年假和其他假期处理""" + # self.leave_ids.action_validate() + # self.hour_leave_ids.action_validate() + + self._process_specific_leaves('年假', is_annual_leave=True) + other_leave_types = [ + '产假', '事假', '法定假', '调休', '补休', '值班调休', + '婚假', '陪产假', '计生假', '丧假', '探亲假', + '家长会假', '病假(普通)', '病假(住院期)', + '病假(出院恢复期)', '考试假' + ] + for leave_type in other_leave_types: + self._process_specific_leaves(leave_type, is_annual_leave=False) + return True + + def _process_specific_leaves(self, leave_type_name, is_annual_leave): + """ + 处理指定类型假期 + :param leave_type_name: 假期类型名称 + :param is_annual_leave: 是否为年假(True/False) + """ + # 收集天假和小时假记录 + day_leaves = self._get_day_leaves(leave_type_name) + hourly_leaves = self._get_hourly_leaves(leave_type_name) + + # 处理天假 + for leave in day_leaves: + date_ranges = self._split_by_month(leave['start'], leave['end']) if not is_annual_leave else [ + {'start': leave['start'], 'end': leave['end']}] + for period in date_ranges: + days = self._compute_duration_day(leave['type_id'], period['start'], period['end']) + if days > 0: + if is_annual_leave: + self._create_annual_leave_line(period['start'], leave, days, period) + else: + self._create_other_leave_line(period['start'], leave, days, period, leave_type_name) + # 处理小时假 + for leave in hourly_leaves: + leave_date = leave['date'] + days = leave['days'] + if days > 0: + if is_annual_leave: + self._create_annual_leave_line(leave_date, leave, days, {'start': leave_date, 'end': leave_date}) + else: + self._create_other_leave_line(leave_date, leave, days,{'start': leave_date, 'end': leave_date}, leave_type_name) + + def _create_other_leave_line(self, period_start, leave_data, days, period, leave_type_name): + """为非年假类型创建vacation.other.line记录""" + # 查找关联的假期额度记录 + report_month = date(period_start.year, period_start.month, 1) + vacation_report = self.env['yuthon.vacation.reports'].search([ ('employee_id', '=', self.users_id.employee_id.id), - ]) - workflow_record_id = self.env['soong.workflow.record'].search([ - ('code', '=', self.code) - ]) + ('vacation_date', '>=', fields.Date.to_string(report_month)), + ('vacation_date', '<=', fields.Date.to_string( + date(period_start.year, period_start.month, + calendar.monthrange(period_start.year, period_start.month)[1])) + ) + ], limit=1) - for line in self.hour_leave_ids: - if line.holiday_status_id.name == '年假': - match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '') - days += float(match.group(1)) / 8 if match else 0 - for leave in self.leave_ids: - if leave.holiday_status_id.name == '年假': - days += leave.number_of_days - - leave_list.append((0, 0, { - 'vacation_id': reports_id.id, + self.env['vacation.other.line'].create({ + 'vacation_id': vacation_report.id if vacation_report else False, + 'holiday_type': leave_data['type_id'].id, + 'employee_id': self.users_id.employee_id.id, + 'workflow_record_id': leave_data['workflow_id'], + 'update_date': fields.Date.today(), 'update_type': 'reduce', - 'workflow_record_id':workflow_record_id.id, - 'update_date': fields.Datetime.now(), + 'lines': days, + 'text': f"请假审批" + }) + + def _get_day_leaves(self, leave_type_name): + workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id + return [{ + 'start': fields.Date.from_string(leave.request_date_from), + 'end': fields.Date.from_string(leave.request_date_to), + 'type_id': leave.holiday_status_id, + 'workflow_id': workflow_id} + for leave in self.leave_ids + if leave.holiday_status_id.name == leave_type_name + and leave.request_date_from + and leave.request_date_to + ] + + def _get_hourly_leaves(self, leave_type_name): + workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id + hourly_records = [] + for line in self.hour_leave_ids: + if line.holiday_status_id.name == leave_type_name and line.request_date_from: + hours_match = re.search(r'(\d+(?:\.\d+)?)', line.number_of_hours_text or '') + hours = float(hours_match.group(1)) if hours_match else 0 + hourly_records.append({ + 'date': fields.Date.from_string(line.request_date_from), + 'type_id': line.holiday_status_id, + 'workflow_id': workflow_id, + 'days': hours / 8 # 转换为天数 + }) + return hourly_records + + def _split_by_month(self, start_date, end_date): + # 跨月拆分逻逻辑 + date_ranges = [] + if start_date.month == end_date.month and start_date.year == end_date.year: + date_ranges.append({'start': start_date, 'end': end_date}) + return date_ranges + + first_month_end = date(start_date.year, start_date.month, + calendar.monthrange(start_date.year, start_date.month)[1]) + date_ranges.append({'start': start_date, 'end': first_month_end}) + current_month = start_date.month + 1 + current_year = start_date.year + while (current_year < end_date.year) or (current_year == end_date.year and current_month < end_date.month): + if current_month > 12: + current_month = 1 + current_year += 1 + month_end = date(current_year, current_month, calendar.monthrange(current_year, current_month)[1]) + date_ranges.append({'start': date(current_year, current_month, 1), 'end': month_end}) + current_month += 1 + + date_ranges.append({'start': date(end_date.year, end_date.month, 1), 'end': end_date}) + return date_ranges + + def _create_annual_leave_line(self, period_start, leave_data, days, period): + """更新年假记录""" + report_month = date(period_start.year, period_start.month, 1) + vacation_report = self.env['yuthon.vacation.reports'].search([ + ('employee_id', '=', self.users_id.employee_id.id), + ('vacation_date', '>=', fields.Date.to_string(report_month)), + ('vacation_date', '<=', fields.Date.to_string( + date(period_start.year, period_start.month, + calendar.monthrange(period_start.year, period_start.month)[1])) + ) + ], limit=1) + # 创建年假明细记录 + self.env['vacation.leave.line'].create({ + 'vacation_id': vacation_report.id if vacation_report else False, + 'employee_id': self.users_id.employee_id.id, + 'workflow_record_id': leave_data['workflow_id'], + 'update_type': 'reduce', + 'update_date': fields.Date.today(), 'update_lines': days, - 'text': '审批' - })) - reports_id.write({'leave_ids': leave_list}) + 'is_void': False, + 'allocation_type': 'vacation', + 'text': f"请假审批w" + }) + + + def _compute_duration_day(self, holiday_status_id, start_date, end_date): + """ + 计算请假时长 + :param holiday_status_id: 请假类型 + :param start_date: 开始时间 + :param end_date: 结束时间 + :return:天 + """ + # 如果是自然日: + 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: + calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")]) + 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 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.constrains('is_day', 'is_hour', 'leave_ids', 'hour_leave_ids') def _check_leave_type_and_details(self): """检查请假类型和明细行是否符合要求""" for record in self: - # 验证 is_day 和 is_hour 不能同时为空 if not record.is_day and not record.is_hour: raise ValidationError(_("请选择按天请假或按小时请假!")) - # 验证 leave_ids 和 hour_leave_ids 不能同时为空 if not record.leave_ids and not record.hour_leave_ids: raise ValidationError(_("请至少添加一条按天或按小时的请假记录!")) - # 验证按天请假时必须选择按天请假 if record.leave_ids and not record.is_day: raise ValidationError(_("添加了按天请假记录,必须选择'按天请假'选项!")) - # 验证按小时请假时必须选择按小时请假 if record.hour_leave_ids and not record.is_hour: raise ValidationError(_("添加了按小时请假记录,必须选择'按小时请假'选项!")) - # 原有的小时数验证逻辑(仅在按小时请假时有小时记录时执行) if record.hour_leave_ids and not record.leave_ids: number_hours = 0 for line in record.hour_leave_ids: diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index 9a8273f38..2e55da2f6 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -26,26 +26,27 @@ class YuthonVacationReports(models.Model): working_overtime = fields.Float(string='加班时', compute='_compute_working_overtime', store=True, digits=(16, 3)) meal_fee = fields.Integer(string="误餐次数", compute='_compute_meal_fee', store=True) medical_fee = fields.Float(string="体检费", compute='_compute_medical_fee', store=True) - compensatory_leave = fields.Integer(string="调休") - leave_of_absence = fields.Integer(string="事假", digits=(16, 3)) - statutory_leave = fields.Integer(string="法定假") - cop_leave = fields.Float(string='补休', digits=(16, 3)) - duty_off_leave = fields.Integer(string='值班调休') - marriage_leave = fields.Integer(string='婚假') - maternity_leave = fields.Integer(string='产假') - paternity_leave = fields.Integer(string='陪产假') - family_planning_leave = fields.Integer(string='计生假') - bereavement_leave = fields.Float(string='丧假') - home_visit_leave = fields.Integer(string='探亲假') - parents_meeting_leave = fields.Integer(string='家长会假') - general_sick_leave = fields.Float(string='病假(普通)') - hospital_sick_leave = fields.Float(string='病假(住院期)') - recovery_sick_leave = fields.Float(string='病假(出院恢复期)') - exam_leave = fields.Integer(string='考试假') + compensatory_leave = fields.Integer(string="调休", compute='_compute_other_leave', store=True) + leave_of_absence = fields.Float(string="事假", digits=(16, 3), compute='_compute_other_leave', store=True) + statutory_leave = fields.Integer(string="法定假", compute='_compute_other_leave', store=True) + cop_leave = fields.Float(string='补休', digits=(16, 3), compute='_compute_other_leave', store=True) + duty_off_leave = fields.Integer(string='值班调休', compute='_compute_other_leave', store=True) + marriage_leave = fields.Integer(string='婚假', compute='_compute_other_leave', store=True) + maternity_leave = fields.Integer(string='产假', compute='_compute_other_leave', store=True) + paternity_leave = fields.Integer(string='陪产假', compute='_compute_other_leave', store=True) + family_planning_leave = fields.Integer(string='计生假', compute='_compute_other_leave', store=True) + bereavement_leave = fields.Float(string='丧假', compute='_compute_other_leave', store=True) + home_visit_leave = fields.Integer(string='探亲假', compute='_compute_other_leave', store=True) + parents_meeting_leave = fields.Integer(string='家长会假', compute='_compute_other_leave', store=True) + general_sick_leave = fields.Float(string='病假(普通)', digits=(16, 3), compute='_compute_other_leave', store=True) + hospital_sick_leave = fields.Float(string='病假(住院期)', digits=(16, 3), compute='_compute_other_leave', store=True) + recovery_sick_leave = fields.Float(string='病假(出院恢复期)', digits=(16, 3), compute='_compute_other_leave', store=True) + exam_leave = fields.Integer(string='考试假', compute='_compute_other_leave', store=True) right_days = fields.Float(string='转正天数') internship_days = fields.Float(string='实习天数') - should_attendance = fields.Integer(string="应到") - actual_attendance = fields.Integer(string="实到") + + should_attendance = fields.Integer(string="应到", compute='_compute_attendance_fields', store=True) + actual_attendance = fields.Integer(string="实到", compute='_compute_attendance_fields', store=True) is_late_card = fields.Integer(string="迟到", compute='_compute_attendance_fields', store=True) is_early_card = fields.Integer(string="早退", compute='_compute_attendance_fields', store=True) @@ -72,17 +73,27 @@ class YuthonVacationReports(models.Model): is_early = 0 is_no = 0 is_work = 0 + leave_days = 0 is_abnormal = False + actual_attendance = 0 if rec.employee_id and rec.vacation_date: year = rec.vacation_date.year month = rec.vacation_date.month days_in_month = calendar.monthrange(year, month)[1] + month_start = date(year, month, 1) + month_end = date(year, month, days_in_month) + leave_days = rec._create_leave_record(month_start, month_end) for day in range(1, days_in_month + 1): current_date = date(year, month, day) attendances = self.env["hr.attendance"].sudo().search([ ('employee_id', '=', rec.employee_id.id), ("current_data", '=', current_date) ]) + if attendances: + has_valid_check = any(att.check_in or att.check_out for att in attendances) + if has_valid_check: + actual_attendance += 1 + for att in attendances: if att.is_late_card: is_late += 1 @@ -95,11 +106,98 @@ class YuthonVacationReports(models.Model): if att.is_abnormal: is_abnormal = True + rec.should_attendance = leave_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 = actual_attendance + + @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', + '补休': 'cop_leave', + '值班调休': 'duty_off_leave', + '婚假': 'marriage_leave', + '产假': 'maternity_leave', + '陪产假': 'paternity_leave', + '计生假': 'family_planning_leave', + '丧假': 'bereavement_leave', + '探亲假': 'home_visit_leave', + '家长会假': 'parents_meeting_leave', + '病假(普通)': 'general_sick_leave', + '病假(住院期)': 'hospital_sick_leave', + '病假(出院恢复期)': 'recovery_sick_leave', + '考试假': 'exam_leave' + } + # 初始化所有假期字段为0 + for field in leave_mapping.values(): + setattr(rec, field, 0) + # 按假期类型分组累加天数 + for line in valid_lines: + leave_type_name = line.holiday_type.name or '' + if leave_type_name in leave_mapping: + field_name = leave_mapping[leave_type_name] + current_value = getattr(rec, field_name) or 0 + setattr(rec, field_name, current_value + line.lines) + int_fields = [ + 'compensatory_leave', 'statutory_leave', 'duty_off_leave', + 'marriage_leave', 'maternity_leave', 'paternity_leave', + 'family_planning_leave', 'home_visit_leave', + 'parents_meeting_leave', 'exam_leave' + ] + for field in int_fields: + setattr(rec, field, getattr(rec, field) or 0) + + def _create_leave_record(self, start_date, end_date): + calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")]) + 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 + + def remove_leading_zero(self, date_str): + month, day = date_str.split('月') + month = month.lstrip('0') + day = day.rstrip('日') + day = day.lstrip('0') + return f'{month}月{day}日' def button_put(self): menu_id = self.env['ir.ui.menu'].search([('name', '=', '考勤确认'), ('parent_id', '!=', False)]) @@ -264,7 +362,6 @@ class YuthonVacationReports(models.Model): leave_hours += line_value elif line.update_type == 'reduce': leave_hours -= line_value - record.leave = leave_hours / 8 @api.depends( diff --git a/yuthon_hr_employee_approval/views/yuthon_department_change_viwes.xml b/yuthon_hr_employee_approval/views/yuthon_department_change_viwes.xml index 5313a4a6c..2ec0af638 100644 --- a/yuthon_hr_employee_approval/views/yuthon_department_change_viwes.xml +++ b/yuthon_hr_employee_approval/views/yuthon_department_change_viwes.xml @@ -5,9 +5,9 @@ yuthon.department.change - - - + + +