!4372 添加功能

Merge pull request !4372 from 李鹏宇/dev_zhou
This commit is contained in:
李鹏宇
2025-08-07 06:58:15 +00:00
committed by Gitee
8 changed files with 263 additions and 107 deletions
+181 -97
View File
@@ -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):
@@ -14,5 +14,18 @@
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
<record forcecreate="True" id="yuthon_vacation_start_end_time" model="ir.cron">
<field name="name">定时任务:更新我的考勤确认单实到数据</field>
<field name="model_id" ref="model_yuthon_vacation_reports"/>
<field name="state">code</field>
<field name="code">model.monthly_day_report()</field>
<field eval="False" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(DateTime.now()).strftime('%Y-%m-%d 00:00:00')"/>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
</data>
</odoo>
@@ -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):
@@ -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")
@@ -437,7 +437,7 @@
<field name="model_id" ref="yuthon_hr_attendance.model_yuthon_vacation_reports"/>
<field name="binding_model_id" ref="yuthon_hr_attendance.model_yuthon_vacation_reports"/>
<field name="state">code</field>
<field name="code">action = model.batch_compute_leave_and_overtime()</field>
<field name="code">action = model._compute_attendance_fields1()</field>
</record>
<record id="details_tree_view" model="ir.ui.view">
<field name="name">details.tree.tree</field>
@@ -5,9 +5,9 @@
<field name="model">yuthon.department.change</field>
<field name="arch" type="xml">
<tree string="部门变更审批">
<!-- <header>-->
<!-- <button name="approval_end" type="object" string="结束" dclass="oe_highlight"/>-->
<!-- </header>-->
<!-- <header>-->
<!-- <button name="approval_end" type="object" string="结束" dclass="oe_highlight"/>-->
<!-- </header>-->
<field name="code"/>
<field name="name"/>
</tree>
@@ -84,7 +84,7 @@
<field name="name">对账</field>
<field name="res_model">property.bank.statements</field>
<field name="view_mode">tree,search</field>
<field name="domain">[('bank_state', '=', 'not_reconciled'), ('is_company', '=', True)]</field>
<field name="domain">[('bank_state', 'in', ['not_reconciled', 'registered']), ('is_company', '=', True)]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_property.view_property_bank_statements_tree')})]"/>
</record>
@@ -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(),