diff --git a/soong_workflow/models/soong_workflow_mixin.py b/soong_workflow/models/soong_workflow_mixin.py index a8d77aeb7..25ff95d3e 100644 --- a/soong_workflow/models/soong_workflow_mixin.py +++ b/soong_workflow/models/soong_workflow_mixin.py @@ -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), ] diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py index 39ea26663..84e50beb2 100644 --- a/soong_workflow/models/soong_workflow_record.py +++ b/soong_workflow/models/soong_workflow_record.py @@ -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 diff --git a/soong_workflow/wizard/soong_workflow_comment_wizard.py b/soong_workflow/wizard/soong_workflow_comment_wizard.py index e3d822fa5..25a150dcc 100644 --- a/soong_workflow/wizard/soong_workflow_comment_wizard.py +++ b/soong_workflow/wizard/soong_workflow_comment_wizard.py @@ -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] diff --git a/yuthon_holiday/models/inherit_hr_leave.py b/yuthon_holiday/models/inherit_hr_leave.py index a245038b7..5d5fcbaab 100644 --- a/yuthon_holiday/models/inherit_hr_leave.py +++ b/yuthon_holiday/models/inherit_hr_leave.py @@ -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): """ 更新考勤