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(),