@@ -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]
|
||||
|
||||
@@ -441,14 +441,34 @@ 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:
|
||||
self._create_workflow_entrust()
|
||||
|
||||
for leave in self.leave_ids | self.hour_leave_ids:
|
||||
leave.sudo().write({'state': 'validate'})
|
||||
|
||||
self._process_specific_leaves('年假', is_annual_leave=True)
|
||||
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
|
||||
)
|
||||
entrust_vals = {
|
||||
[('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,
|
||||
@@ -456,457 +476,325 @@ class HolidaysRequestBatch(models.Model):
|
||||
'start_date': self.start_date,
|
||||
'end_date': self.end_date,
|
||||
'entrust_notice': True,
|
||||
}
|
||||
self.env['soong.workflow.entrust'].create(entrust_vals)
|
||||
|
||||
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'})
|
||||
|
||||
"""提交请假申请,区分年假和其他假期处理"""
|
||||
self._process_specific_leaves('年假', is_annual_leave=True)
|
||||
other_leave_types = [
|
||||
'产假', '事假', '法定假', '调休', '补休', '值班调休',
|
||||
'婚假', '陪产假', '计生假', '丧假', '探亲假', '哺乳假',
|
||||
'家长会假', '病假(普通)', '病假(住院期)', '育儿假',
|
||||
'病假(出院恢复期)', '考试假'
|
||||
]
|
||||
for leave_type in other_leave_types:
|
||||
self._process_specific_leaves(leave_type, is_annual_leave=False)
|
||||
return 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
|
||||
'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_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({
|
||||
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': hours / 8
|
||||
})
|
||||
return hourly_records
|
||||
'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', '=', vacation_date),
|
||||
], limit=1)
|
||||
if not report:
|
||||
if not create:
|
||||
return Reports
|
||||
report = Reports.create({
|
||||
'employee_id': employee_id,
|
||||
'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)
|
||||
first_day = fields.Date.today().replace(day=1)
|
||||
vacation_report = self.env['yuthon.vacation.reports'].search([
|
||||
|
||||
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', '=', 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:
|
||||
('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
|
||||
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,
|
||||
'employee_id': employee_id,
|
||||
'logging_data': '666',
|
||||
'workflow_record_id': leave_data.get('workflow_id', False),
|
||||
'update_type': 'reduce',
|
||||
'workday': interval_hours,
|
||||
|
||||
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,
|
||||
'logging_data': '666',
|
||||
}
|
||||
else:
|
||||
extra_vals = {'lines': bucket['amount'], 'holiday_type': holiday_type.id}
|
||||
|
||||
line = self._upsert_leave_line(
|
||||
line_model, bucket['report'], leave_data, extra_vals, amount_field)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
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 _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 的
|
||||
「已确认且已计入则跳过」兜底,不会重复计)。
|
||||
"""
|
||||
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()
|
||||
|
||||
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),
|
||||
})
|
||||
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': f"请假审批"
|
||||
'text': '请假审批',
|
||||
})
|
||||
actual_vacation_report.update_line()
|
||||
actual_vacation_report._compute_working_overtime()
|
||||
created = True
|
||||
return self.env[line_model].create(vals)
|
||||
|
||||
if self.users_id.employee_ids.company_id.name == '集团本部':
|
||||
dec_2025_end = fields.Datetime.to_datetime('2025-12-31 23:59:59')
|
||||
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()
|
||||
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([
|
||||
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),
|
||||
'|',
|
||||
('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:
|
||||
|
||||
remaining = hours
|
||||
for carry_line in initial_lines + non_initial_lines:
|
||||
if remaining <= 0:
|
||||
break
|
||||
current_remaining = carry_line.remaining_quantity
|
||||
deduct_amount = min(remaining_deduct, current_remaining)
|
||||
if leave_id and deduct_amount > 0:
|
||||
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_amount,
|
||||
carry_date=period_start,
|
||||
deduct_amount=deduct,
|
||||
carry_date=carry_date,
|
||||
)
|
||||
remaining_deduct -= deduct_amount
|
||||
remaining -= deduct
|
||||
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
|
||||
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)
|
||||
|
||||
# 兜底报表(当月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:
|
||||
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()
|
||||
|
||||
def _create_other_leave_line(self, period_start, leave_data, hours, period, leave_type_name):
|
||||
"""
|
||||
创建其他假减记录(跨月自动拆分)
|
||||
: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 = []
|
||||
|
||||
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)
|
||||
|
||||
# 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 _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):
|
||||
"""
|
||||
更新考勤
|
||||
|
||||
Reference in New Issue
Block a user