From df19c6f30f7675d292bdebfbab1eafe4d124cd41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?=
<9664676+pengyuthon@user.noreply.gitee.com>
Date: Fri, 13 Mar 2026 11:24:41 +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 | 81 +++++++++--------
yuthon_hr_attendance/__manifest__.py | 1 +
yuthon_hr_attendance/models/__init__.py | 1 +
.../models/overtime_carryover.py | 26 ++++++
.../models/vacation_overtime_active.py | 86 ++++++++++++++++---
.../models/yuthon_vacation_reports.py | 63 +++++++++++---
.../security/ir.model.access.csv | 4 +-
.../views/overtime_carryover_views.xml | 23 +++++
.../views/yuthon_vacation_reports_views.xml | 3 +-
yuthon_work_brand/views/hr_menus.xml | 6 ++
10 files changed, 228 insertions(+), 66 deletions(-)
create mode 100644 yuthon_hr_attendance/models/overtime_carryover.py
create mode 100644 yuthon_hr_attendance/views/overtime_carryover_views.xml
diff --git a/yuthon_holiday/models/inherit_hr_leave.py b/yuthon_holiday/models/inherit_hr_leave.py
index 3e807fa8c..52d435412 100644
--- a/yuthon_holiday/models/inherit_hr_leave.py
+++ b/yuthon_holiday/models/inherit_hr_leave.py
@@ -474,6 +474,7 @@ class HolidaysRequestBatch(models.Model):
def _get_day_leaves(self, leave_type_name):
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
return [{
+ 'leave_id': leave.id,
'start': fields.Date.from_string(leave.request_date_from),
'end': fields.Date.from_string(leave.request_date_to),
'type_id': leave.holiday_status_id,
@@ -492,6 +493,7 @@ class HolidaysRequestBatch(models.Model):
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({
+ 'leave_id': line.id,
'date': fields.Date.from_string(line.request_date_from),
'type_id': line.holiday_status_id,
'workflow_id': workflow_id,
@@ -532,6 +534,7 @@ class HolidaysRequestBatch(models.Model):
:param leave_type_name: 假期类型名称
"""
employee_id = self.users_id.employee_ids.id if self.users_id.employee_ids else False
+ leave_id = leave_data.get('leave_id', False)
if 'date' in leave_data:
leave_start = leave_data['date']
leave_end = leave_data['date']
@@ -604,7 +607,7 @@ class HolidaysRequestBatch(models.Model):
)
# 4. 只有当两个报表中都没有该记录时,才创建新记录
if not existing_line_in_vacation_report and not existing_line_in_vacation_report1:
- report_id = self.env['vacation.over.line'].create({
+ over_id = self.env['vacation.over.line'].create({
'vacation_id': actual_vacation_report.id,
'employee_id': employee_id,
'logging_data': '666',
@@ -618,45 +621,15 @@ class HolidaysRequestBatch(models.Model):
'is_void': False,
'text': f"请假审批"
})
- report_id.update_line()
- report_id._compute_working_overtime()
-
- if self.env.user.company_id.name == '集团本部':
- domain = [
- ('employee_id', '=', employee_id),
- "|",
- ('is_initial_number', '=', True),
- '&', ('is_initial_number', '=', False),
- '&', ('update_type', '=', 'add'),
- '&', ('workflow_record_id', '!=', False),
- '&', ('start_time', '>=', datetime(2025, 12, 31, 23, 59, 59)),
- '&', ('vacation_id', '!=', False),
- ('employee_id.is_annual_salary', '!=', True)
- ]
- else:
- domain = [
- ('workflow_record_id', '!=', False),
- ('update_type', '=', 'add'),
- ('remaining_quantity', '!=', 0.0),
- ]
- overtime_records = self.env['vacation.over.line'].search(domain, order='start_time ASC')
- remaining_deduct = hours
- for record in overtime_records:
- if remaining_deduct <= 0:
- break
- current_remaining = record.remaining_quantity # 剩余数
- deduct_amount = min(remaining_deduct, current_remaining)
- if remaining_deduct < current_remaining:
- carry_state = 'partial'
- else:
- carry_state = 'completed'
- record.write({
- 'carry_forward_date': fields.Date.to_string(period_start),
- 'carry_forward_quantity': record.carry_forward_quantity + deduct_amount,
- 'carry_forward_state': carry_state,
- 'logging_data': '113',
- })
- remaining_deduct -= deduct_amount
+ actual_vacation_report.update_line()
+ actual_vacation_report._compute_working_overtime()
+ if leave_id and over_id:
+ self._create_overtime_carryover(
+ over_line=over_id,
+ deduct_amount=interval_hours,
+ carry_date=period_start,
+ leave_id=leave_id
+ )
def _create_annual_leave_line(self, period_start, leave_data, hours, period):
"""
@@ -736,7 +709,7 @@ class HolidaysRequestBatch(models.Model):
limit=1
)
if not existing_line_in_vacation_report and not existing_line_in_vacation_report1:
- report_id = self.env['vacation.leave.line'].create({
+ self.env['vacation.leave.line'].create({
'vacation_id': actual_vacation_report.id,
'employee_id': employee_id,
'workflow_record_id': leave_data['workflow_id'],
@@ -747,7 +720,7 @@ class HolidaysRequestBatch(models.Model):
'allocation_type': 'vacation',
'text': f"请假审批"
})
- report_id.update_line()
+ actual_vacation_report.update_line()
def _create_other_leave_line(self, period_start, leave_data, days, period, leave_type_name):
"""
@@ -823,6 +796,30 @@ class HolidaysRequestBatch(models.Model):
'text': f"请假审批"
})
+ def _create_overtime_carryover(self, over_line, deduct_amount, carry_date, leave_id):
+ """
+ 封装创建加班结转明细的逻辑
+ """
+ # 1. 防重复校验:同一假单+同一假期行,只创建一条结转明细
+ if leave_id and over_line:
+ existing_carryover = self.env['overtime.carryover'].search([
+ ('leave_id', '=', leave_id),
+ ('over_line_id', '=', over_line.id),
+ ('carry_forward_quantity', '=', deduct_amount)
+ ], limit=1)
+ if existing_carryover:
+ return
+
+ carry_forward_date = fields.Date.to_date(carry_date) if carry_date else fields.Date.today()
+ self.env['overtime.carryover'].create({
+ 'over_line_id': over_line.id,
+ 'leave_id': leave_id,
+ 'carry_forward_date': carry_forward_date,
+ 'carry_forward_quantity': deduct_amount,
+ 'carryover_type': 'leave',
+ 'text': '',
+ })
+
def update_attendance(self, employee_id, start_date, end_date, code):
"""
更新考勤
diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index 0bd3dd7ec..f9c517d3b 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -72,6 +72,7 @@
'views/all_attendance_menu.xml',
'views/overtime_active_line_views.xml',
'views/vacation_meal_active_views.xml',
+ 'views/overtime_carryover_views.xml',
'views/overtime_approval_inherit_header.xml',
'wizard/hr_attendance_month_wizard_views.xml',
'wizard/yuthon_vacation_import_wizard_views.xml',
diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py
index 444e9c856..c6979f47f 100644
--- a/yuthon_hr_attendance/models/__init__.py
+++ b/yuthon_hr_attendance/models/__init__.py
@@ -40,6 +40,7 @@ from . import yuthon_overtime_line
from . import overtime_active_line
from . import vacation_meal_active
from . import duty_adjustment_schedule
+from . import overtime_carryover
# from odoo.tools import SQL
# import odoo
# odoo.models.READ_GROUP_AGGREGATE.update({
diff --git a/yuthon_hr_attendance/models/overtime_carryover.py b/yuthon_hr_attendance/models/overtime_carryover.py
new file mode 100644
index 000000000..d5979efd6
--- /dev/null
+++ b/yuthon_hr_attendance/models/overtime_carryover.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+
+class OvertimeCarryover(models.Model):
+ _name = 'overtime.carryover'
+ _description = '加班结转明细'
+
+ over_line_id = fields.Many2one('vacation.over.line', string='加班记录')
+ carry_forward_date = fields.Date(string="结转日期")
+ carry_forward_quantity = fields.Float(string="结转时长")
+ carryover_type = fields.Selection([('fee', '加班费结算'), ('leave', '假单结算')], string='加班补偿方式',
+ default='leave')
+ text = fields.Text(string='备注')
+ leave_id = fields.Many2one('hr.leave', string='假单')
+
+
+
+class OvertimeCarryoverLine(models.Model):
+ _name = 'overtime.carryover.line'
+
+class OvertimeUserLine(models.Model):
+ _name = 'overtime.carryover.wizard.line'
+
+
diff --git a/yuthon_hr_attendance/models/vacation_overtime_active.py b/yuthon_hr_attendance/models/vacation_overtime_active.py
index 6934c4ac6..8629b9866 100644
--- a/yuthon_hr_attendance/models/vacation_overtime_active.py
+++ b/yuthon_hr_attendance/models/vacation_overtime_active.py
@@ -31,22 +31,88 @@ class VacationOvertimeActive(models.Model):
remaining_quantity = fields.Float(string="剩余数")
select_revoke = fields.Selection([('to_do', '待撤销'), ('done', '已撤销')], string='状态')
-
def update_vacation(self):
+ """
+ 结转时长处理逻辑:
+ 1. 优先匹配 is_initial_number=True 的假期行记录
+ 2. 不足时匹配指定条件的 is_initial_number=False 记录(按start_time升序)
+ 3. 更新结转状态、剩余时长 + 填充对应记录的carry_forward_quantity/date
+ 4. 为每条修改的记录创建加班结转明细(overtime.carryover + overtime.carryover.line)
+ """
+ dec_2025_end = fields.Datetime.to_datetime('2025-12-31 23:59:59')
active_ids = self.env["vacation.overtime.active"].browse(
self._context.get('active_ids', self._context.get('active_id', []))
)
for record in active_ids:
- overtime_lines = self.env['vacation.over.line'].search([
- ('employee_id', '=', record.employee_id.id),
- ('overtime_methods', '=', 'fee')
- ])
- overtime_lines.write({
- 'workday': record.carry_forward_quantity,
- 'update_date': record.carry_forward_date,
- 'logging_data': 'qq',
- })
+ remaining_to_carry = record.carry_forward_quantity
+ initial_lines = self.env['vacation.over.line'].search([
+ ('is_initial_number', '=', True),
+ ('employee_id', '=', record.employee_id.id)
+ ])
+ # ========== 步骤1:处理初始记录 + 创建结转明细 ==========
+ for initial_line in initial_lines:
+ if remaining_to_carry <= 0:
+ break
+ line_duration = initial_line.duration or 0.0
+ actual_carry = min(line_duration, remaining_to_carry)
+
+ # 1. 更新初始记录的结转字段
+ initial_line.carry_forward_date = record.carry_forward_date
+ initial_line.carry_forward_quantity = actual_carry
+
+ if line_duration >= remaining_to_carry:
+ remaining_to_carry = 0.0
+ initial_line.carry_forward_state = 'partial'
+ else:
+ remaining_to_carry -= actual_carry
+ initial_line.carry_forward_state = 'completed'
+
+ # 核心新增:为当前初始记录创建加班结转明细
+ self._create_carryover_line(initial_line, record)
+
+ # ========== 步骤2:处理非初始记录 + 创建结转明细 ==========
+ if remaining_to_carry > 0:
+ non_initial_lines = self.env['vacation.over.line'].search([
+ ('remaining_quantity', '!=', 0.0),
+ ('employee_id', '=', record.employee_id.id),
+ ('is_initial_number', '=', False),
+ ('update_type', '=', 'add'),
+ ('workflow_record_id', '!=', False),
+ ('start_time', '>=', dec_2025_end),
+ ('vacation_id', '!=', False)
+ ], order='start_time ASC')
+ for non_line in non_initial_lines:
+ if remaining_to_carry <= 0:
+ break
+ line_remaining = non_line.remaining_quantity or 0.0
+ actual_carry = min(line_remaining, remaining_to_carry)
+
+ # 1. 更新非初始记录的结转字段
+ non_line.carry_forward_date = record.carry_forward_date
+ non_line.carry_forward_quantity = actual_carry
+
+ if line_remaining >= remaining_to_carry:
+ non_line.remaining_quantity -= actual_carry
+ remaining_to_carry = 0.0
+ non_line.carry_forward_state = 'partial'
+ else:
+ remaining_to_carry -= actual_carry
+ non_line.remaining_quantity = 0.0
+ non_line.carry_forward_state = 'completed'
+
+ # 核心新增:为当前非初始记录创建加班结转明细
+ self._create_carryover_line(non_line, record)
+
+ def _create_carryover_line(self, over_line, active_record):
+ self.env['overtime.carryover'].create({
+ 'over_line_id': over_line.id,
+ 'carry_forward_date': active_record.carry_forward_date,
+ 'carry_forward_quantity': over_line.carry_forward_quantity,
+ 'carryover_type': 'fee',
+ ''
+ 'text': '',
+ })
def all_unlink(self):
active_ids = self.env["vacation.overtime.active"].browse(
diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
index de1df46ab..68c0f2ee6 100644
--- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py
+++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
@@ -306,8 +306,7 @@ class YuthonVacationReports(models.Model):
for rec in over_ids:
inherit_lines = rec.overtime_ids.filtered(lambda l: l.text == '上月额度继承')
if inherit_lines:
- rec.is_initial_number = True
-
+ inherit_lines.is_initial_number = True
def all_update_line(self):
records = self.env["yuthon.vacation.reports"].browse(
@@ -2103,7 +2102,6 @@ class VacationOverline(models.Model):
_name = 'vacation.over.line'
_description = '加班明细'
-
active = fields.Boolean('Active', default=True, tracking=True)
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
@@ -2134,18 +2132,49 @@ class VacationOverline(models.Model):
workflow_id = fields.Many2one('soong.workflow', compute="_compute_overtime", string='审批类型', store=True)
start_time = fields.Datetime(string="开始时间", compute="_compute_overtime", store=True)
end_time = fields.Datetime(string="结束时间", compute="_compute_overtime", store=True)
- carry_forward_date = fields.Date(string="结转日期")
duration = fields.Float(string="时长", compute="_compute_overtime", store=True)
- carry_forward_quantity = fields.Float(string="结转时长")
- validity = fields.Char(string="有效期")
- carry_result = fields.Selection([('success', '成功'), ('failed', '失败')], string="结转结果")
- is_filing = fields.Boolean(string="归档")
- carry_forward_state = fields.Selection([('pending', '待结转'),('completed', '已结转'), ('partial', '部分结转')
- ],string='结转状态',
- default='pending',
- )
remaining_quantity = fields.Float(string='剩余数', compute="_compute_last_balance")
logging_data = fields.Char(string="日志日志日志")
+ validity = fields.Char(string="有效期")
+ is_filing = fields.Boolean(string="归档")
+
+ carry_result = fields.Selection([('success', '成功'), ('failed', '失败')], string="结转结果", default='success')
+ carry_forward_date = fields.Date(string="结转日期", compute='_compute_carry_forward_info', store=True)
+ carry_forward_quantity = fields.Float(string="结转时长", compute='_compute_carry_forward_info', store=True)
+ carry_forward_state = fields.Selection([
+ ('pending', '待结转'),
+ ('completed', '已结转'),
+ ('partial', '部分结转')
+ ], string='结转状态', compute='_compute_carry_forward_info', store=True, default='pending')
+ carryover_ids = fields.One2many('overtime.carryover', 'over_line_id', string='关联结转明细')
+
+ @api.depends('carryover_ids.carry_forward_quantity', 'carryover_ids.carry_forward_date', 'remaining_quantity')
+ def _compute_carry_forward_info(self):
+ """
+ 从overtime.carryover明细行自动计算:
+ 1. 结转日期:取最新的结转明细日期
+ 2. 结转时长:汇总所有关联明细的结转时长
+ 3. 结转状态:按剩余时长+结转时长计算(复用原有逻辑)
+ """
+ for rec in self:
+ rec.carry_forward_date = False
+ rec.carry_forward_quantity = 0.0
+ rec.carry_forward_state = 'pending'
+ if rec.carryover_ids:
+ # 结转时长:汇总所有明细的结转时长
+ total_carry = sum(rec.carryover_ids.mapped('carry_forward_quantity'))
+ rec.carry_forward_quantity = total_carry
+ # 结转日期:取最新的结转明细日期(无则取最早)
+ carry_dates = rec.carryover_ids.mapped('carry_forward_date')
+ if carry_dates:
+ rec.carry_forward_date = max(carry_dates)
+ current_remaining = rec.remaining_quantity or 0.0
+ if total_carry <= 0:
+ rec.carry_forward_state = 'pending'
+ elif total_carry >= current_remaining:
+ rec.carry_forward_state = 'completed'
+ else:
+ rec.carry_forward_state = 'partial'
def open_record1(self):
tree_id = self.env.ref("yuthon_hr_attendance.view_yuthon_vacation_overtime_tree").id
@@ -2160,6 +2189,16 @@ class VacationOverline(models.Model):
],
}
+ def open_overtime_carryover(self):
+ return {
+ "type": "ir.actions.act_window",
+ "name": "结转明细",
+ "res_model": "overtime.carryover",
+ "view_mode": "tree",
+ "target": "new",
+ 'domain': [('over_line_id', '=', self.id)],
+ }
+
@api.model
def confirm(self):
# 1. 构建筛选域
diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv
index b553f270a..1673f675b 100644
--- a/yuthon_hr_attendance/security/ir.model.access.csv
+++ b/yuthon_hr_attendance/security/ir.model.access.csv
@@ -96,4 +96,6 @@ access_export_date_wizard,export.date.wizard,model_export_date_wizard,base.group
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
-access_duty_adjustment_schedule,duty.adjustment.schedule,model_duty_adjustment_schedule,base.group_user,1,1,1,1
\ No newline at end of file
+access_duty_adjustment_schedule,duty.adjustment.schedule,model_duty_adjustment_schedule,base.group_user,1,1,1,1
+access_overtime_carryover,overtime.carryover,model_overtime_carryover,base.group_user,1,1,1,1
+access_overtime_carryover_line,overtime.carryover.line,model_overtime_carryover_line,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/overtime_carryover_views.xml b/yuthon_hr_attendance/views/overtime_carryover_views.xml
new file mode 100644
index 000000000..320d526a2
--- /dev/null
+++ b/yuthon_hr_attendance/views/overtime_carryover_views.xml
@@ -0,0 +1,23 @@
+
+
+
+ overtime.carryover.tree
+ overtime.carryover
+
+
+
+
+
+
+
+
+
+
+
+
+ 结转明细
+ overtime.carryover
+ tree
+
+
+
diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
index 133ad0484..854713d4b 100644
--- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
+++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
@@ -694,7 +694,7 @@
vacation.over.line.overtime.tree
vacation.over.line
-
+
@@ -711,6 +711,7 @@
+
diff --git a/yuthon_work_brand/views/hr_menus.xml b/yuthon_work_brand/views/hr_menus.xml
index 156e16703..d41df883b 100644
--- a/yuthon_work_brand/views/hr_menus.xml
+++ b/yuthon_work_brand/views/hr_menus.xml
@@ -170,6 +170,12 @@
action="yuthon_hr_attendance.view_yuthon_vacation_reports_action"
sequence="14"/>
+
+