From 2696cf04871b51cb831b83a6a065c44218e0a6a5 Mon Sep 17 00:00:00 2001 From: zhou <123@> Date: Thu, 7 Aug 2025 14:57:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_holiday/models/inherit_hr_leave.py | 278 ++++++++++++------ .../data/mothly_report_time.xml | 13 + .../models/hr_attendance_detail_wechat.py | 28 ++ .../models/yuthon_vacation_reports.py | 32 +- .../views/yuthon_vacation_reports_views.xml | 2 +- .../views/yuthon_department_change_viwes.xml | 6 +- .../views/property_bank_statements_views.xml | 2 +- .../wizard/property_reconciliation_wizard.py | 9 +- 8 files changed, 263 insertions(+), 107 deletions(-) diff --git a/yuthon_holiday/models/inherit_hr_leave.py b/yuthon_holiday/models/inherit_hr_leave.py index 33ca19c9f..c7e1d3ff5 100644 --- a/yuthon_holiday/models/inherit_hr_leave.py +++ b/yuthon_holiday/models/inherit_hr_leave.py @@ -9,6 +9,7 @@ from odoo.tools.misc import format_date from collections import namedtuple, defaultdict from datetime import date import calendar +import logging class HrLeave(models.Model): @@ -438,6 +439,8 @@ class HrLeave(models.Model): day = day.lstrip('0') return f'{month}月{day}日' +_logger = logging.getLogger(__name__) + class HolidaysRequestBatch(models.Model): _name = 'hr.leave.batch' _description = '请假审批' @@ -506,8 +509,7 @@ class HolidaysRequestBatch(models.Model): # 提交 def action_submit(self): """提交请假申请,区分年假和其他假期处理""" - # self.leave_ids.action_validate() - # self.hour_leave_ids.action_validate() + _logger.info(f"开始处理请假申请:{self.code},申请人:{self.users_id.employee_id.name}") self._process_specific_leaves('年假', is_annual_leave=True) other_leave_types = [ @@ -518,6 +520,8 @@ class HolidaysRequestBatch(models.Model): ] for leave_type in other_leave_types: self._process_specific_leaves(leave_type, is_annual_leave=False) + + _logger.info(f"请假申请:{self.code} 处理完成") return True def _process_specific_leaves(self, leave_type_name, is_annual_leave): @@ -526,129 +530,209 @@ class HolidaysRequestBatch(models.Model): :param leave_type_name: 假期类型名称 :param is_annual_leave: 是否为年假(True/False) """ + _logger.info(f"开始处理假期类型:{leave_type_name},是否为年假:{is_annual_leave},申请编号:{self.code}") + # 收集天假和小时假记录 day_leaves = self._get_day_leaves(leave_type_name) hourly_leaves = self._get_hourly_leaves(leave_type_name) + _logger.debug(f"假期类型:{leave_type_name},天假记录数:{len(day_leaves)},小时假记录数:{len(hourly_leaves)}") + # 处理天假 - 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']) + for idx, leave in enumerate(day_leaves): + try: + # 跨月拆分逻辑(仅非年假) + date_ranges = self._split_by_month(leave['start'], leave['end']) if not is_annual_leave else [ + {'start': leave['start'], 'end': leave['end']} + ] + _logger.debug( + f"天假记录 {idx + 1} 拆分后日期段数:{len(date_ranges)},原始起止:{leave['start']}至{leave['end']}") + + for period in date_ranges: + days = self._compute_duration_day(leave['type_id'], period['start'], period['end']) + _logger.debug(f"日期段 {period['start']}至{period['end']} 计算天数:{days}") + + 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) + except Exception as e: + _logger.error(f"处理天假记录 {idx + 1} 失败(类型:{leave_type_name}):{str(e)}", exc_info=True) + + # 处理小时假 + for idx, leave in enumerate(hourly_leaves): + try: + leave_date = leave['date'] + days = leave['days'] + _logger.debug(f"小时假记录 {idx + 1} 日期:{leave_date},转换天数:{days}") + if days > 0: if is_annual_leave: - self._create_annual_leave_line(period['start'], leave, days, period) + self._create_annual_leave_line(leave_date, leave, days, + {'start': leave_date, 'end': leave_date}) 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) + self._create_other_leave_line(leave_date, leave, days, {'start': leave_date, 'end': leave_date}, + leave_type_name) + except Exception as e: + _logger.error(f"处理小时假记录 {idx + 1} 失败(类型:{leave_type_name}):{str(e)}", exc_info=True) + + _logger.info(f"假期类型:{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), - ('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) + try: + 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.other.line'].sudo().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', - 'lines': days, - 'text': f"请假审批" - }) + _logger.debug( + f"创建非年假明细(类型:{leave_type_name}),关联假期记录:{vacation_report.id if vacation_report else '无'},天数:{days}") + + record = self.env['vacation.other.line'].sudo().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', + 'lines': days, + 'text': f"请假审批" + }) + _logger.info(f"成功创建非年假明细记录:{record.id}(类型:{leave_type_name})") + except Exception as e: + _logger.error(f"创建非年假明细失败(类型:{leave_type_name}):{str(e)}", exc_info=True) def _get_day_leaves(self, leave_type_name): - workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id - return [{ + try: + workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id + records = [{ '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 - ] + 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 + ] + _logger.debug(f"收集天假记录(类型:{leave_type_name}):{len(records)}条") + return records + except Exception as e: + _logger.error(f"收集天假记录失败(类型:{leave_type_name}):{str(e)}", exc_info=True) + return [] 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 + try: + 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 + days = hours / 8 + hourly_records.append({ + 'date': fields.Date.from_string(line.request_date_from), + 'type_id': line.holiday_status_id, + 'workflow_id': workflow_id, + 'days': days + }) + _logger.debug(f"收集小时假记录(类型:{leave_type_name}):{len(hourly_records)}条") + return hourly_records + except Exception as e: + _logger.error(f"收集小时假记录失败(类型:{leave_type_name}):{str(e)}", exc_info=True) + return [] 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}) + try: + 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}) + _logger.debug(f"无需跨月拆分:{start_date}至{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}) + + _logger.debug(f"跨月拆分完成:原始{start_date}至{end_date},拆分为{len(date_ranges)}段") return date_ranges + except Exception as e: + _logger.error(f"跨月拆分失败({start_date}至{end_date}):{str(e)}", exc_info=True) + return [] - 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 + def _compute_duration_day(self, holiday_type_id, start_date, end_date): + try: + if not start_date or not end_date or start_date > end_date: + _logger.warning(f"无效日期范围:{start_date}至{end_date},返回0天") + return 0.0 - date_ranges.append({'start': date(end_date.year, end_date.month, 1), 'end': end_date}) - return date_ranges + holiday = self.env['hr.leave.type'].browse(holiday_type_id.id) + total_days = (end_date - start_date).days + 1 + + if holiday.work_entry_type_id: + work_days = 0 + current_date = start_date + while current_date <= end_date: + if current_date.isoweekday() not in [6, 7]: + work_days += 1 + current_date += timedelta(days=1) + _logger.debug(f"工作日计算:{start_date}至{end_date},共{work_days}天(排除周末)") + return work_days + + _logger.debug(f"自然日计算:{start_date}至{end_date},共{total_days}天") + return total_days + except Exception as e: + _logger.error(f"计算天数失败({start_date}至{end_date}):{str(e)}", exc_info=True) + return 0.0 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, - 'is_void': False, - 'allocation_type': 'vacation', - 'text': f"请假审批" - }) + try: + 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) + + _logger.debug(f"创建年假明细,关联假期记录:{vacation_report.id if vacation_report else '无'},天数:{days}") + + record = 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, + 'is_void': False, + 'allocation_type': 'vacation', + 'text': f"请假审批" + }) + _logger.info(f"成功创建年假明细记录:{record.id}") + except Exception as e: + _logger.error(f"创建年假明细失败:{str(e)}", exc_info=True) def _compute_duration_day(self, holiday_status_id, start_date, end_date): diff --git a/yuthon_hr_attendance/data/mothly_report_time.xml b/yuthon_hr_attendance/data/mothly_report_time.xml index a762fe747..817b2719c 100644 --- a/yuthon_hr_attendance/data/mothly_report_time.xml +++ b/yuthon_hr_attendance/data/mothly_report_time.xml @@ -14,5 +14,18 @@ -1 + + 定时任务:更新我的考勤确认单实到数据 + + code + model.monthly_day_report() + + + 1 + days + + -1 + + diff --git a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py index 32d02347e..7df8c635c 100644 --- a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py +++ b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py @@ -8,6 +8,7 @@ import logging import traceback from odoo import api, fields, models, tools, _ from dateutil.relativedelta import relativedelta +from datetime import date _logger = logging.getLogger(__name__) @@ -81,6 +82,33 @@ class HrAttendanceDetail(models.Model): "context": {'default_attendance_detail_id': self.id} } + @api.model + def create(self, vals): + record = super(HrAttendanceDetail, self).create(vals) + if record.employee_id and record.checkin_time and record.checkin_type != '未打卡': + self._update_actual_attendance(record) + return record + + def _update_actual_attendance(self, record): + """更新假期日志中的实到天数""" + checkin_date = fields.Date.from_string(record.checkin_time) + vacation_report = self.env['yuthon.vacation.reports'].search([ + ('employee_id', '=', record.employee_id.id), + ('vacation_date', '>=', fields.Date.to_string(date(checkin_date.year, checkin_date.month, 1))), + ('vacation_date', '<=', fields.Date.to_string(date(checkin_date.year, checkin_date.month,calendar.monthrange(checkin_date.year, checkin_date.month)[1])))], limit=1) + + existing_checkins = self.search([ + ('employee_id', '=', record.employee_id.id), + ('checkin_time', '>=', fields.Datetime.to_string(datetime.combine(checkin_date, time.min))), + ('checkin_time', '<=', fields.Datetime.to_string(datetime.combine(checkin_date, time.max))), + ('id', '!=', record.id) + ]) + + # 如果当天没有其他打卡记录,则实到天数+1 + if not existing_checkins: + vacation_report.actual_attendance = (vacation_report.actual_attendance or 0) + 1 + + # 传参按一个人打卡一次传参 # @api.model_create_multi # def create(self, vals_list): diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index 2e55da2f6..9b20082d2 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -114,6 +114,36 @@ class YuthonVacationReports(models.Model): rec.is_abnormal = is_abnormal rec.actual_attendance = actual_attendance + + def _compute_attendance_fields1(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_days = 0 + 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 + + rec.should_attendance = leave_days + 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): @@ -270,7 +300,7 @@ class YuthonVacationReports(models.Model): def disposable_report(self): year = 2025 - months = range(1, 8) # 1到7月 + 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") diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml index 97ae7de78..ce9fc5ee4 100644 --- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml +++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml @@ -437,7 +437,7 @@ code - action = model.batch_compute_leave_and_overtime() + action = model._compute_attendance_fields1() details.tree.tree 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 2ec0af638..a51b9f1a4 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 - - - + + + diff --git a/yuthon_property/views/property_bank_statements_views.xml b/yuthon_property/views/property_bank_statements_views.xml index 2daf97cd2..1bb673217 100644 --- a/yuthon_property/views/property_bank_statements_views.xml +++ b/yuthon_property/views/property_bank_statements_views.xml @@ -84,7 +84,7 @@ 对账 property.bank.statements tree,search - [('bank_state', '=', 'not_reconciled'), ('is_company', '=', True)] + [('bank_state', 'in', ['not_reconciled', 'registered']), ('is_company', '=', True)] diff --git a/yuthon_property_approval/wizard/property_reconciliation_wizard.py b/yuthon_property_approval/wizard/property_reconciliation_wizard.py index d77b7feca..706615c23 100644 --- a/yuthon_property_approval/wizard/property_reconciliation_wizard.py +++ b/yuthon_property_approval/wizard/property_reconciliation_wizard.py @@ -100,6 +100,8 @@ class PropertyReconciliationWizard(models.Model): penalty_amount = late_days * round( line.rent * float(line.contract_id.percentage_liquidated_damages.name), 2 ) + start_date_str = fields.Date.to_string(next_month_first) + end_date_str = self.actual_date if penalty_amount > 0: # 检查是否已存在相同条件的违约金行 is_duplicate = any( @@ -109,7 +111,6 @@ class PropertyReconciliationWizard(models.Model): and pl.collection_type == 'penalty' for pl in existing_penalty_lines ) - if not is_duplicate: # 创建新的违约金记录 penalty_line_vals = { @@ -120,7 +121,7 @@ class PropertyReconciliationWizard(models.Model): 'rent': penalty_amount, 'amount': 0, 'collection_type': 'penalty', - 'remake': f'违约金 ({late_days}天)', + 'remake': f'违约金({start_date_str}至{end_date_str},共{late_days}天)', 'sequence': int(line.sequence) + 1, } new_lines.append((0, 0, penalty_line_vals)) @@ -185,7 +186,7 @@ class PropertyReconciliationWizard(models.Model): receivable.write({ 'is_reconciliation': is_reconciliation, 'delivered1': line.amount, - 'payout_date': rec.bank_id.start_time.date(), + 'payout_date': self.actual_date, 'remark': line.remake, 'reconciliation_time': fields.Datetime.now(), 'receivable_log': f'当前登录人{rec.env.user.name}于{fields.Datetime.now()}对户名为{rec.bank_id.name_account}进行对账操作,对账应收金额为{rec.receivable_total},实际金额为{rec.actual_total}' @@ -209,7 +210,7 @@ class PropertyReconciliationWizard(models.Model): 'property_id': line.property_id.id, 'contract_id': line.contract_id.id, 'delivered1': line.amount, - 'payout_date': rec.bank_id.start_time.date() if is_reconciliation else None, + 'payout_date': self.actual_date, 'original_amount': line.rent, 'remark': line.remake, 'reconciliation_time': fields.Datetime.now(),