Merge pull request !12927 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-09-28 07:44:13 +00:00
committed by Gitee
4 changed files with 359 additions and 433 deletions
+14 -4
View File
@@ -1365,10 +1365,10 @@ class SoongWorkflowMixin(models.AbstractModel):
[('model', '=', 'entrust.all')], limit=1)
model_rec = self.env['ir.model'].sudo().search(
[('model', '=', model)], limit=1) if model else None
# ⚠️ is_enable 是 compute 非存储字段,不能进 search(会被忽略)
entrusts = self.env['soong.workflow.entrust'].sudo().search([
('is_enable', '=', True),
('entrust_user_id', 'in', list(allowed_user_ids)),
])
]).filtered(lambda e: e.is_enable)
for ent in entrusts:
ent_model_ids = ent.model_ids.ids
in_scope = (entrust_all and entrust_all.id in ent_model_ids) or (
@@ -1970,11 +1970,21 @@ class SoongWorkflowMixin(models.AbstractModel):
if is_automatic and record_id.department_id:
dept_id = record_id.department_id.id
# 按当前申请人自动匹配-本部门(保留部门和公司条件)
Dept = self.env['hr.department'].sudo()
# 命中范围 = 发起人部门 + 其全部**上级**部门。
# 被匹配人的「主部门 / 辅助部门」只要挂在这条线上任意一级就算命中,
# 即「辅助部门及其以下」都覆盖 —— 例:发起人=供应链(子公司),
# 被匹配人辅助部门=供应链(上级)→ 命中,不必再挂子公司那个部门。
dept_ids = Dept.search([('id', 'parent_of', dept_id)]).ids
# 申请人辅助部门按「该部门及以下」放宽
applicant_dept_ids = applicant.employee_ids.department_ids.ids
applicant_dept_ids = Dept.search([
('id', 'child_of', applicant_dept_ids)
]).ids if applicant_dept_ids else []
department_domain = [
'|', '|', '|',
('department_id', '=', dept_id),
('department_ids', 'in', [dept_id]),
('department_id', 'in', dept_ids),
('department_ids', 'in', dept_ids),
('department_id', 'in', applicant_dept_ids),
('department_ids', 'in', applicant_dept_ids),
]
@@ -236,10 +236,10 @@ class SoongWorkflowRecord(models.Model):
model = self.env['ir.model'].sudo().search([('model', '=', model_name)], limit=1)
if not model and not entrust_all:
return None
# ⚠️ is_enable 是 compute 非存储字段,不能进 search(会被忽略导致过期委托仍生效)
entrust = self.env['soong.workflow.entrust'].sudo().search([
('is_enable', '=', True),
('entrust_user_id', '=', current_uid),
], limit=1)
]).filtered(lambda e: e.is_enable)[:1]
if not entrust:
return None
model_ids = entrust.model_ids.ids
@@ -250,13 +250,10 @@ class SoongWorkflowCommentWizard(models.TransientModel):
model = self.env['ir.model'].sudo().search([('model', '=', model_name)], limit=1)
if not model and not entrust_all:
return substitution
entrusts = self.env['soong.workflow.entrust'].sudo().search([
('is_enable', '=', True),
('start_date', '<=', today),
'|',
('end_date', '>', today),
('end_date', '=', False)
])
# ⚠️ is_enable 是 compute 非存储字段,不能进 search(会被忽略导致过期委托仍生效),
# 统一在 Python 侧按 is_enable 过滤
entrusts = self.env['soong.workflow.entrust'].sudo().search([]).filtered(
lambda e: e.is_enable)
for ent in entrusts:
model_ids = ent.model_ids.ids
# 命中"全部流程"(entrust.all) 或 当前业务模型
@@ -273,6 +270,8 @@ class SoongWorkflowCommentWizard(models.TransientModel):
for record in self:
# 先清空
record_id = self.env[record.res_model].browse(record.res_id) if record.res_model and record.res_id else None
# 流程委托:当前模型(或"全部流程")且在有效期内的 {委托人 user_id: 被委托人 hr.employee}
entrust_substitution = self._get_entrust_substitution(record.res_model) if record.res_model else {}
ctx_links = self.env.context.get('all_workflow_links', [])
valid_links = self.env['trans.link'].browse(ctx_links) if isinstance(ctx_links, (list, tuple)) else ctx_links
@@ -332,6 +331,11 @@ class SoongWorkflowCommentWizard(models.TransientModel):
employee_user_role_ids = self.env['hr.employee'].sudo().search([
('user_id', 'in', user_ids.ids)
])
# 流程委托:印章管理员把流程委托给他人时,被委托人也要能被选中
for uid in user_ids.ids:
delegatee = entrust_substitution.get(uid)
if delegatee and delegatee not in employee_user_role_ids:
employee_user_role_ids |= delegatee
else:
employee_user_role_ids = record_id._get_approvers_for_link(link, record_id)
workflow_id = link.node_button_id.workflow_id
@@ -571,18 +575,38 @@ class SoongWorkflowCommentWizard(models.TransientModel):
user_stamp_map[user.id] = stamp
all_user_ids = []
all_employee_ids = []
delegator_user_ids = [] # 委托人(原印章管理员):待办已交给别人,但保留可见
created_keys = set() # (实际经办人, 章) 去重:同一人被委托到同一枚章时只建一条待办
for user, stamp in user_stamp_map.items():
employee = self.env['hr.employee'].sudo().search([('user_id', '=', user)], limit=1)
if employee and employee.id not in all_employee_ids:
all_employee_ids.append(employee.id)
all_user_ids.append(user)
# 流程委托:印章管理员把流程委托给他人时,待办落到被委托人身上
owner_employee = self.env['hr.employee'].sudo().search([('user_id', '=', user)], limit=1)
delegatee = entrust_substitution.get(user)
actual_employee = owner_employee
done_by_display = False
if delegatee and delegatee.user_id:
actual_employee = delegatee
done_by_display = '{}(委托人)'.format(actual_employee.name)
actual_user = actual_employee.user_id.id if (
actual_employee and actual_employee.user_id) else user
stamp_key = (actual_user, stamp.id)
if stamp_key in created_keys:
continue
created_keys.add(stamp_key)
if actual_employee and actual_employee.id not in all_employee_ids:
all_employee_ids.append(actual_employee.id)
if actual_user not in all_user_ids:
all_user_ids.append(actual_user)
if actual_user != user and user not in delegator_user_ids \
and user not in all_user_ids:
delegator_user_ids.append(user)
self.sudo().env["soong.workflow.log"].create({
"code": self.code,
"model": self.res_model,
"res_id": self.res_id,
"node_id": next_node_id.id,
"workflow_id": self.node_btn_id.workflow_id.id,
"done_by": user,
"done_by": actual_user,
"done_by_display": done_by_display or False,
"node_btn_name": "4",
"stamp_id": stamp.id,
"approval_state": "0",
@@ -593,7 +617,12 @@ class SoongWorkflowCommentWizard(models.TransientModel):
record_id.write({'state': self.trans_link_id.name})
workflow_record_id = self.env['soong.workflow.record'].sudo().search([('res_id', '=', record_id.id),
('res_model', '=', record_id._name)])
workflow_record_id.users_ids = [(4, uid) for uid in all_user_ids]
# 被委托人(实际经办) + 委托人(原印章管理员) 都可见
visible_uids = list(all_user_ids)
for uid in delegator_user_ids:
if uid not in visible_uids:
visible_uids.append(uid)
workflow_record_id.users_ids = [(4, uid) for uid in visible_uids]
workflow_record_id.employee_ids = all_employee_ids
workflow_record_id.approve_state = "pending"
action_id = self.env['ir.actions.act_window'].search([('res_model', '=', self.res_model)])[0]
+301 -414
View File
@@ -441,472 +441,360 @@ class HolidaysRequestBatch(models.Model):
else:
record.details = " "
# 除「年假」「补休」外,其余假期类型统一落到 vacation.other.line
# ⚠️ 与 yuthon_hr_attendance 的 OTHER_LEAVE_SYNC_TYPES 保持一致(那边靠它自愈补录)
OTHER_LEAVE_TYPES = [
'产假', '事假', '法定假', '调休', '补休', '值班调休',
'婚假', '陪产假', '计生假', '丧假', '探亲假', '哺乳假',
'家长会假', '病假(普通)', '病假(住院期)', '育儿假',
'病假(出院恢复期)', '考试假'
]
# 提交
def action_submit(self):
"""提交请假申请:明细单置为已批准,并逐类型写入考勤确认单"""
if self.is_entrusted:
leave_batch_model = self.env['ir.model'].search(
[('model', '=', 'hr.leave.batch')],
limit=1
)
entrust_vals = {
'model_ids': [(6, 0, [leave_batch_model.id])],
'entrust_user_id': self.users_id.id,
'entrust_user_id2': self.entrusted_agent_id.id,
'is_expiration_date': True,
'start_date': self.start_date,
'end_date': self.end_date,
'entrust_notice': True,
}
self.env['soong.workflow.entrust'].create(entrust_vals)
self._create_workflow_entrust()
if self.leave_ids:
for record in self.leave_ids:
record.sudo().write({'state': 'validate'})
if self.hour_leave_ids:
for record in self.hour_leave_ids:
record.sudo().write({'state': 'validate'})
for leave in self.leave_ids | self.hour_leave_ids:
leave.sudo().write({'state': 'validate'})
"""提交请假申请,区分年假和其他假期处理"""
self._process_specific_leaves('年假', is_annual_leave=True)
other_leave_types = [
'产假', '事假', '法定假', '调休', '补休', '值班调休',
'婚假', '陪产假', '计生假', '丧假', '探亲假', '哺乳假',
'家长会假', '病假(普通)', '病假(住院期)', '育儿假',
'病假(出院恢复期)', '考试假'
]
for leave_type in other_leave_types:
for leave_type in self.OTHER_LEAVE_TYPES:
self._process_specific_leaves(leave_type, is_annual_leave=False)
return True
def _create_workflow_entrust(self):
"""生成工作流委托"""
leave_batch_model = self.env['ir.model'].search(
[('model', '=', 'hr.leave.batch')], limit=1)
self.env['soong.workflow.entrust'].create({
'model_ids': [(6, 0, [leave_batch_model.id])],
'entrust_user_id': self.users_id.id,
'entrust_user_id2': self.entrusted_agent_id.id,
'is_expiration_date': True,
'start_date': self.start_date,
'end_date': self.end_date,
'entrust_notice': 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)
workflow_id = self._get_workflow_record_id()
# 处理天假
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:
if leave_type_name == '补休':
# 计算小时数(天数 * 8)
hours = self._compute_duration_day(leave['type_id'], period['start'], period['end']) * 8
if hours > 0:
self._create_overtime_leave_line(period_start=period['start'], leave_data=leave, hours=hours, period=period, leave_type_name=leave_type_name)
else:
days = self._compute_duration_day(leave['type_id'], period['start'], period['end'])
if days > 0:
if is_annual_leave:
# 年假以小时为单位(天数 * 8)
self._create_annual_leave_line(period_start=period['start'], leave_data=leave, hours=days * 8, period=period)
else:
self._create_other_leave_line(period_start=period['start'], leave_data=leave, hours=days * 8, period=period, leave_type_name=leave_type_name)
# 更新考勤
for leave in self._get_day_leaves(leave_type_name, workflow_id):
self._create_leave_lines(leave, leave['start'], leave['end'], leave_type_name, is_annual_leave)
self.update_attendance(self.users_id.employee_id, leave['start'], leave['end'], self.code)
# 处理小时假
for leave in hourly_leaves:
for leave in self._get_hourly_leaves(leave_type_name, workflow_id):
leave_date = leave['date']
hours = leave['days'] * 8
if hours > 0:
if is_annual_leave:
self._create_annual_leave_line(period_start=leave_date, leave_data=leave, hours=hours, period={'start': leave_date, 'end': leave_date})
elif leave_type_name == '补休':
self._create_overtime_leave_line(period_start=leave_date, leave_data=leave, hours=hours, period={'start': leave_date, 'end': leave_date}, leave_type_name=leave_type_name)
else:
# 其他类型小时假,使用天数
self._create_other_leave_line(period_start=leave_date, leave_data=leave, hours=hours, period={'start': leave_date, 'end': leave_date}, leave_type_name=leave_type_name)
# 更新考勤
self._create_leave_lines(leave, leave_date, leave_date, leave_type_name, is_annual_leave)
self.update_attendance(self.users_id.employee_id, leave_date, leave_date, self.code)
# ============ 公共取值 / 拆分 / 定位确认单 ============
def _get_day_leaves(self, leave_type_name):
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
def _get_workflow_record_id(self):
"""本单据对应的审批记录 id"""
return self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
def _get_employee_id(self):
"""请假人对应的员工 id"""
return self.users_id.employee_ids[:1].id or False
def _get_day_leaves(self, leave_type_name, workflow_id=None):
"""按天请假的明细(dict 列表)"""
workflow_id = workflow_id or self._get_workflow_record_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,
'workflow_id': workflow_id}
for leave in self.leave_ids
'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,
'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
]
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 = line.leave_hours
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,
'days': hours / 8
})
return hourly_records
def _get_hourly_leaves(self, leave_type_name, workflow_id=None):
"""按小时请假的明细(dict 列表),days = 小时数 / 8"""
workflow_id = workflow_id or self._get_workflow_record_id()
return [{
'leave_id': line.id,
'date': fields.Date.from_string(line.request_date_from),
'type_id': line.holiday_status_id,
'workflow_id': workflow_id,
'days': (line.leave_hours or 0.0) / 8,
} for line in self.hour_leave_ids
if line.holiday_status_id.name == leave_type_name
and line.request_date_from]
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
"""把 [start_date, end_date] 按自然月拆成若干区间
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_overtime_leave_line(self, period_start, leave_data, hours, period, leave_type_name):
:return: [{'start': date, 'end': date, 'period_start': 该月1号}, ...]
"""
创建补休请假扣减记录(跨月自动拆分,分月创建)
:param period_start: 原始周期开始时间(datetime.date)
:param leave_data: 请假数据字典(含workflow_id等)
:param hours: 总请假小时数(float)
:param period: 请假时间段 {'start': date, 'end': date}
: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']
else:
leave_start = leave_data['start'] if 'start' in leave_data else leave_data['end']
leave_end = leave_data['end']
month_intervals = []
current_start = leave_start
while current_start <= leave_end:
current_month = current_start.month
current_year = current_start.year
next_month = current_month + 1 if current_month < 12 else 1
next_year = current_year if current_month < 12 else current_year + 1
current_month_end = date(next_year, next_month, 1) - timedelta(days=1)
interval_end = min(current_month_end, leave_end)
month_intervals.append({
'start': current_start,
intervals = []
if not start_date or not end_date or start_date > end_date:
return intervals
current = start_date
while current <= end_date:
month_first = date(current.year, current.month, 1)
month_end = month_first + relativedelta(months=1) - timedelta(days=1)
interval_end = min(month_end, end_date)
intervals.append({
'start': current,
'end': interval_end,
'period_start': date(current_year, current_month, 1)
'period_start': month_first,
})
current_start = interval_end + timedelta(days=1)
holiday_status_id = self.env['hr.leave.type'].search([('name', '=', '补休')])
for interval in month_intervals:
interval_start = interval['start']
interval_end = interval['end']
if self.is_hour:
interval_hours = hours
else:
interval_hours = self._compute_duration_day(holiday_status_id, interval_start, interval_end) * 8
current = interval_end + timedelta(days=1)
return intervals
# 跳过小时数为0的无效区间
if interval_hours <= 1e-6:
continue
period_start = interval['period_start'] #
vacation_report1 = self.env['yuthon.vacation.reports'].search([
def _get_vacation_report(self, employee_id, period_start, create=True, max_roll=24):
"""按月份定位考勤确认单(跨月请假不再丢月份)
- 该月确认单不存在 → 按需创建(未来月份的确认单还没生成,不建就会丢数据)
- 该月确认单已确认 → 顺延到下个月,直到找到未确认的月份
"""
Reports = self.env['yuthon.vacation.reports']
if not employee_id or not period_start:
return Reports
vacation_date = period_start
for _ in range(max_roll):
report = Reports.search([
('employee_id', '=', employee_id),
('vacation_date', '=', period_start),
('vacation_date', '=', vacation_date),
], limit=1)
first_day = fields.Date.today().replace(day=1)
vacation_report = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '=', first_day),
], limit=1)
# 确定最终使用的报表
actual_vacation_report = vacation_report
if vacation_report1 and vacation_report1.vacation_state != 'confirmed':
actual_vacation_report = vacation_report1
if not actual_vacation_report:
continue
overtime_line_domain = [
('leave_id', '=', leave_id),
]
existing_line_in_vacation_report = False
if vacation_report:
existing_line_in_vacation_report = self.env['vacation.over.line'].search(
[('vacation_id', '=', vacation_report.id)] + overtime_line_domain,
limit=1
)
existing_line_in_vacation_report1 = False
if vacation_report1:
existing_line_in_vacation_report1 = self.env['vacation.over.line'].search(
[('vacation_id', '=', vacation_report1.id)] + overtime_line_domain,
limit=1
)
# 4. 只有当两个报表中都没有该记录时,才创建新记录
created = False
if not existing_line_in_vacation_report and not existing_line_in_vacation_report1:
over_line1 = self.env['vacation.over.line'].create({
'leave_id': leave_id,
'vacation_id': actual_vacation_report.id,
if not report:
if not create:
return Reports
report = Reports.create({
'employee_id': employee_id,
'logging_data': '666',
'workflow_record_id': leave_data.get('workflow_id', False),
'update_type': 'reduce',
'workday': interval_hours,
'vacation_date': vacation_date,
})
report.update_line()
report.vacation_state = 'not_sent'
return report
if report.vacation_state != 'confirmed':
return report
vacation_date += relativedelta(months=1)
return Reports
def _get_month_report(self, employee_id, period_start):
"""精确取该月的考勤确认单(不创建、不顺延),用于判断是否已确认/已计入"""
if not employee_id or not period_start:
return self.env['yuthon.vacation.reports']
return self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '=', period_start),
], limit=1)
def _refresh_reports_from(self, employee_id, from_date):
"""刷新该员工 from_date 及之后所有月份确认单的明细(余额向后结转)"""
if not employee_id or not from_date:
return
for report in self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '>=', from_date),
], order='vacation_date asc'):
report.update_line()
# ============ 写明细 ============
def _create_leave_lines(self, leave_data, start, end, leave_type_name, is_annual_leave):
"""把一段请假按自然月拆分,落到对应月份的考勤确认单
年假 → vacation.leave.line(小时);补休 → vacation.over.line(小时);
其余 → vacation.other.line(天)。
⚠️ 跨月且前面月份已确认时(如 8/25~9/2、8 月单已确认),前面那几天的假
会顺延并入后面的月份:先按「最终落地的确认单」把天数汇总,再一次性写入,
避免同一张单里被后一段覆盖掉。
"""
employee_id = self._get_employee_id()
if not employee_id or not start or not end:
return
leave_id = leave_data.get('leave_id')
holiday_type = leave_data.get('type_id') or self.env['hr.leave.type'].search(
[('name', '=', leave_type_name)], limit=1)
# 按小时请假的明细用 'date' 标记,直接取小时数,不按区间重算
is_hour_leave = 'date' in leave_data
in_hours = is_annual_leave or leave_type_name == '补休'
if is_annual_leave:
line_model, amount_field = 'vacation.leave.line', 'update_lines'
elif leave_type_name == '补休':
line_model, amount_field = 'vacation.over.line', 'workday'
else:
line_model, amount_field = 'vacation.other.line', 'lines'
# ---- 1) 按「最终落地的确认单」汇总天数 ----
buckets = {}
for interval in self._split_by_month(start, end):
if is_hour_leave:
amount = leave_data['days'] * 8 if in_hours else leave_data['days']
else:
amount = self._compute_duration_day(holiday_type, interval['start'], interval['end'])
if in_hours:
amount *= 8
if amount <= 1e-6:
continue
month_report = self._get_month_report(employee_id, interval['period_start'])
if month_report and month_report.vacation_state == 'confirmed' \
and self._find_leave_line(line_model, month_report, leave_id):
# 该月已确认且当时已计入本次请假 → 不再顺延,避免重复计
continue
report = self._get_vacation_report(employee_id, interval['period_start'])
if not report:
continue
bucket = buckets.setdefault(report.id, {
'report': report,
'amount': 0.0,
'period_start': interval['period_start'],
})
bucket['amount'] += amount
if interval['period_start'] < bucket['period_start']:
bucket['period_start'] = interval['period_start']
if not buckets:
return
# ---- 2) 幂等:清掉未确认月份里的旧行,重复提交不会翻倍(已确认的不动)----
self._remove_unconfirmed_leave_lines(line_model, leave_id)
# ---- 3) 写入(同一张单只写一行,天数为各月段之和)----
for bucket in buckets.values():
if is_annual_leave:
extra_vals = {'update_lines': bucket['amount'], 'allocation_type': 'vacation'}
elif leave_type_name == '补休':
extra_vals = {
'workday': bucket['amount'],
'legal_day': 0,
'public_holidays': 0,
'meal_number': 0,
'update_date': fields.Date.today(),
'is_void': False,
'text': f"请假审批"
})
actual_vacation_report.update_line()
actual_vacation_report._compute_working_overtime()
created = True
if self.users_id.employee_ids.company_id.name == '集团本部':
dec_2025_end = fields.Datetime.to_datetime('2025-12-31 23:59:59')
today = fields.Date.today()
initial_carry_lines = self.env['vacation.over.line'].search([
('employee_id', '=', employee_id),
('employee_id.is_annual_salary', '!=', True),
('update_type', '=', 'add'),
('carry_forward_state', 'in', ['pending', 'partial']),
('is_void', '=', False),
('is_initial_number', '=', True),
'|',
('valid_date', '=', False),
('valid_date', '>=', today)
], order='start_time ASC')
non_initial_carry_lines = self.env['vacation.over.line'].search([
('employee_id', '=', employee_id),
('employee_id.is_annual_salary', '!=', True),
('update_type', '=', 'add'),
('carry_forward_state', 'in', ['pending', 'partial']),
('is_void', '=', False),
('is_initial_number', '=', False),
('start_time', '>=', dec_2025_end),
('workflow_record_id', '!=', False),
('vacation_id', '!=', False),
'|',
('valid_date', '=', False),
('valid_date', '>=', today)
], order='start_time ASC')
carry_over_lines = initial_carry_lines + non_initial_carry_lines
remaining_deduct = interval_hours
for carry_line in carry_over_lines:
if remaining_deduct <= 0:
break
current_remaining = carry_line.remaining_quantity
deduct_amount = min(remaining_deduct, current_remaining)
if leave_id and deduct_amount > 0:
self._create_overtime_carryover(
over_line=carry_line,
over_line1=over_line1,
deduct_amount=deduct_amount,
carry_date=period_start,
)
remaining_deduct -= deduct_amount
carry_line._compute_carry_forward_info()
# ===================== 【新增】当前 + 后续月份全部更新 =====================
if created and actual_vacation_report:
future_reps = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '>=', actual_vacation_report.vacation_date)
], order='vacation_date asc')
for rep in future_reps:
rep.update_line()
def _create_annual_leave_line(self, period_start, leave_data, hours, period):
"""
创建年假扣减记录(跨月自动拆分,分月创建)
:param period_start: 原始周期开始时间(datetime.date)
:param leave_data: 请假数据字典(含workflow_id等)
:param hours: 总年假请假小时数(float)
:param period: 请假时间段 {'start': date, 'end': date}
:param leave_type_name: 假期类型名称(年假)
"""
employee_id = self.users_id.employee_ids.id if self.users_id.employee_ids else False
leave_start = period['start'] # 年假开始日期(date)
leave_end = period['end'] # 年假结束日期(date)
month_intervals = []
leave_id = leave_data.get('leave_id', False)
current_start = leave_start
while current_start <= leave_end:
current_month = current_start.month
current_year = current_start.year
next_month = current_month + 1 if current_month < 12 else 1
next_year = current_year if current_month < 12 else current_year + 1
current_month_end = date(next_year, next_month, 1) - timedelta(days=1)
interval_end = min(current_month_end, leave_end)
month_intervals.append({
'start': current_start,
'end': interval_end,
'period_start': date(current_year, current_month, 1)
})
current_start = interval_end + timedelta(days=1)
holiday_status_id = self.env['hr.leave.type'].search([('name', '=', '年假')], limit=1)
for interval in month_intervals:
interval_start = interval['start']
interval_end = interval['end']
if self.is_hour:
interval_hours = hours
'logging_data': '666',
}
else:
interval_hours = self._compute_duration_day(holiday_status_id, interval_start, interval_end) * 8
period_start = interval['period_start']
first_day = fields.Date.today().replace(day=1)
extra_vals = {'lines': bucket['amount'], 'holiday_type': holiday_type.id}
# 兜底报表(当月1号)
vacation_report = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '=', first_day),
], limit=1)
actual_vacation_report = vacation_report
# 优先匹配当月未确认的报表
vacation_report1 = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '=', period_start),
], limit=1)
if vacation_report1 and vacation_report1.vacation_state != 'confirmed':
actual_vacation_report = vacation_report1
if not actual_vacation_report:
line = self._upsert_leave_line(
line_model, bucket['report'], leave_data, extra_vals, amount_field)
if not line:
continue
leave_line_domain = [
('leave_id', '=', leave_id),
]
existing_line_in_vacation_report = False
if vacation_report:
existing_line_in_vacation_report = self.env['vacation.leave.line'].search(
[('vacation_id', '=', vacation_report.id)] + leave_line_domain,
limit=1
)
existing_line_in_vacation_report1 = False
if vacation_report1:
existing_line_in_vacation_report1 = self.env['vacation.leave.line'].search(
[('vacation_id', '=', vacation_report1.id)] + leave_line_domain,
limit=1
)
created = False
if not existing_line_in_vacation_report and not existing_line_in_vacation_report1:
self.env['vacation.leave.line'].create({
'leave_id': leave_id,
'vacation_id': actual_vacation_report.id,
'employee_id': employee_id,
'workflow_record_id': leave_data['workflow_id'],
'update_type': 'reduce',
'update_date': fields.Date.today(),
'update_lines': interval_hours,
'is_void': False,
'allocation_type': 'vacation',
'text': f"请假审批"
})
actual_vacation_report.update_line()
created = True
# ===================== 【新增】当前 + 后续月份全部更新 =====================
if created and actual_vacation_report:
future_reps = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '>=', actual_vacation_report.vacation_date)
], order='vacation_date asc')
for rep in future_reps:
rep.update_line()
self._refresh_reports_from(employee_id, bucket['report'].vacation_date)
if leave_type_name == '补休':
bucket['report']._compute_working_overtime()
self._deduct_overtime_carryover(
employee_id, leave_id, line, bucket['amount'], bucket['period_start'])
def _create_other_leave_line(self, period_start, leave_data, hours, period, leave_type_name):
def _find_leave_line(self, line_model, report, leave_id):
"""在指定确认单里找该假单已有的明细行"""
if not report or not leave_id:
return self.env[line_model]
return self.env[line_model].search([
('vacation_id', '=', report.id),
('leave_id', '=', leave_id),
], limit=1)
def _remove_unconfirmed_leave_lines(self, line_model, leave_id):
"""清掉该假单落在「未确认」月份里的旧明细行,保证重算/重复提交不翻倍
已确认月份的确认单不可再改,那里的行保持原样(由 _create_leave_lines 的
「已确认且已计入则跳过」兜底,不会重复计)。
"""
创建其他假减记录(跨月自动拆分)
:param period_start: 原始周期开始时间(datetime.date)
:param leave_data: 请假数据字典(含workflow_id等)
:param hours: 总请假天数(浮点型)
:param period: 请假时间段 {'start': date, 'end': date}
:param leave_type_name: 假期类型名称
"""
holiday_type = self.env['hr.leave.type'].search([('name', '=', leave_type_name)], limit=1)
employee_id = self.users_id.employee_ids.id if self.users_id.employee_ids else False
leave_start = period['start']
leave_end = period['end']
month_intervals = []
if not leave_id:
return
lines = self.env[line_model].sudo().search([
('leave_id', '=', leave_id),
('vacation_id.vacation_state', '!=', 'confirmed'),
])
if lines:
lines.unlink()
current_start = leave_start
while current_start <= leave_end:
current_month = current_start.month
current_year = current_start.year
next_month = current_month + 1 if current_month < 12 else 1
next_year = current_year if current_month < 12 else current_year + 1
current_month_end = date(next_year, next_month, 1) - timedelta(days=1)
interval_end = min(current_month_end, leave_end)
# 加入拆分后的区间列表
month_intervals.append({
'start': current_start,
'end': interval_end,
'period_start': date(current_year, current_month, 1)
def _upsert_leave_line(self, line_model, report, leave_data, extra_vals, amount_field=None):
"""在指定确认单里写一行请假明细(幂等)
已存在同一假单的行时:
- amount_field 为空 → 直接跳过,不重复写
- amount_field 有值 → 累加(兜底:跨月段并到同一张确认单时不会覆盖)
:return: 写入的明细行;跳过时返回空记录集
"""
existing = self._find_leave_line(line_model, report, leave_data.get('leave_id'))
if existing:
if not amount_field:
return self.env[line_model]
existing.write({
amount_field: (existing[amount_field] or 0.0) + (extra_vals.get(amount_field) or 0.0),
})
current_start = interval_end + timedelta(days=1)
return existing
vals = dict(extra_vals)
vals.update({
'leave_id': leave_data.get('leave_id'),
'vacation_id': report.id,
'employee_id': report.employee_id.id,
'workflow_record_id': leave_data.get('workflow_id', False),
'update_type': 'reduce',
'update_date': fields.Date.today(),
'is_void': False,
'text': '请假审批',
})
return self.env[line_model].create(vals)
# 3. 遍历拆分后的每个月区间,分别创建记录
for interval in month_intervals:
interval_start = interval['start']
interval_end = interval['end']
if self.is_hour:
interval_hours = hours / 8
else:
interval_hours = self._compute_duration_day(holiday_type, interval_start, interval_end)
period_start = interval['period_start']
if not employee_id:
continue
# 严格按请假所属月份定位报表(不再回退到当前月 first_day)。
# 产假等长假期会落到未来月份,而 yuthon.vacation.reports 只提前一个月生成,
# 未来月份报表尚未生成 → 按需生成(与 monthly_report 创建方式一致)。
vacation_report = self.env['yuthon.vacation.reports'].search([
('employee_id', '=', employee_id),
('vacation_date', '=', period_start),
], limit=1)
if not vacation_report:
vacation_report = self.env['yuthon.vacation.reports'].create({
'employee_id': employee_id,
'vacation_date': period_start,
})
vacation_report.update_line()
vacation_report.vacation_state = 'not_sent'
leave_id = leave_data.get('leave_id', False)
existing_leave_line = self.env['vacation.other.line'].search([
('vacation_id', '=', vacation_report.id),
('leave_id', '=', leave_id),
], limit=1)
if not existing_leave_line:
self.env['vacation.other.line'].create({
'leave_id': leave_id,
'vacation_id': vacation_report.id,
'employee_id': employee_id,
'workflow_record_id': leave_data.get('workflow_id', False),
'update_type': 'reduce',
'lines': interval_hours,
'holiday_type': holiday_type.id,
'update_date': fields.Date.today(),
'is_void': False,
'text': f"请假审批"
})
def _deduct_overtime_carryover(self, employee_id, leave_id, over_line1, hours, carry_date):
"""集团本部:补休请假优先冲抵可结转的加班额度"""
if self.users_id.employee_ids.company_id.name != '集团本部':
return
today = fields.Date.today()
dec_2025_end = fields.Datetime.to_datetime('2025-12-31 23:59:59')
base_domain = [
('employee_id', '=', employee_id),
('employee_id.is_annual_salary', '!=', True),
('update_type', '=', 'add'),
('carry_forward_state', 'in', ['pending', 'partial']),
('is_void', '=', False),
'|', ('valid_date', '=', False),
('valid_date', '>=', today),
]
initial_lines = self.env['vacation.over.line'].search(
base_domain + [('is_initial_number', '=', True)], order='start_time ASC')
non_initial_lines = self.env['vacation.over.line'].search(
base_domain + [
('is_initial_number', '=', False),
('start_time', '>=', dec_2025_end),
('workflow_record_id', '!=', False),
('vacation_id', '!=', False),
], order='start_time ASC')
remaining = hours
for carry_line in initial_lines + non_initial_lines:
if remaining <= 0:
break
deduct = min(remaining, max(0.0, carry_line.remaining_quantity or 0.0))
if leave_id and deduct > 0:
self._create_overtime_carryover(
over_line=carry_line,
over_line1=over_line1,
deduct_amount=deduct,
carry_date=carry_date,
)
remaining -= deduct
carry_line._compute_carry_forward_info()
def _create_overtime_carryover(self, over_line, deduct_amount, carry_date, over_line1):
"""
封装创建加班结转明细的逻辑
"""
carry_forward_date = (self.log_ids[-2].lase_click_date).date()
log = self.log_ids[-2] if len(self.log_ids) >= 2 else self.env['soong.workflow.log']
carry_forward_date = getattr(log, 'lase_click_date', False) or fields.Date.today()
self.env['overtime.carryover'].create({
'leave_id': self.id,
'over_line_id': over_line.id,
@@ -916,7 +804,6 @@ class HolidaysRequestBatch(models.Model):
'carryover_type': 'leave',
'text': '',
})
def update_attendance(self, employee_id, start_date, end_date, code):
"""
更新考勤