# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.tools.safe_eval import safe_eval from odoo.tools.misc import frozendict from odoo.exceptions import UserError from datetime import datetime, date, time, timedelta from odoo.exceptions import UserError, ValidationError from lxml import etree import logging import ast _logger = logging.getLogger(__name__) class SoongWorkflowMixin(models.AbstractModel): _name = "soong.workflow.mixin" _description = "Soong Workflow Mixin" @api.model def read(self, fields=None, load='_classic_read'): for rec in self: if rec.log_ids: current_user = self.env.user target_logs = rec.log_ids.filtered(lambda l: l.done_by.id == current_user.id and l.approval_state == '0') if target_logs: target_logs.write({'approval_state': '1'}) return super(SoongWorkflowMixin, self).read(fields=fields, load=load) # @api.model # def write(self, vals): # res = super(SoongWorkflowMixin, self).write(vals) # workflow_records = self.env['soong.workflow.record'].search([('name', '=', self.name)]) # for record in workflow_records: # for field, value in vals.items(): # if hasattr(record, field): # setattr(record, field, value) # return res def action_is_modify(self): self.is_modify = False @api.model def _compute_default_state(self): # Objects wkf_obj = self.env["soong.workflow"] rec_model = self._name # Get Flow Start wkf_recs = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)]) for wkf_rec in wkf_recs: for node in wkf_rec.node_ids: if node.flow_start: return node.node_name @api.model def _load_state(self): # Objects wkf_obj = self.env["soong.workflow"] # Variables rec_model = self._name ret = [] # Get Status wkf_recs = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)]) for rec in wkf_recs: for node in rec.node_ids: ret.append((node.node_name, node.name)) return ret state = fields.Selection( selection="_load_state", string="Status", default=_compute_default_state, track_visibility="onchange", store=True ) is_modify = fields.Boolean(string="可修改") workflow_id = fields.Many2one( comodel_name="soong.workflow", string="Workflow Ref.", compute="_compute_workflow_id", ) approval_users_ids = fields.One2many("soong.approval.users", "res_id", domain=lambda self: [('res_model', '=', self._name)], auto_join=True, string="审批明细") approval_sign_ids = fields.Many2many('res.users', store=False, compute="_compute_approval_sign_ids", string="加签人员") is_import = fields.Boolean(string='是否导入') is_back = fields.Boolean(string='是否允许收回', compute='_compute_is_back', store=False,) is_active = fields.Boolean(string='是否允许归档', compute='_compute_is_active', store=False,) is_confirm = fields.Boolean(string="未确认", compute='_compute_is_confirm', store=False,) is_end = fields.Boolean(string="未结束", compute='_compute_is_confirm', store=False,) is_done = fields.Boolean(string="已办", compute='_compute_is_done', store=False,) is_send = fields.Boolean(string="是否分发", compute='_compute_is_send', store=False,) is_show_add_sign = fields.Boolean(string="显示加签按钮", compute='_compute_is_show_add_sign') is_show_transfer = fields.Boolean(string="显示加签确认按钮", compute='_compute_is_show_transfer') approval_result = fields.Selection([('pass', '通过'), ('fail', '不通过')], string="审批结果") def _compute_is_show_add_sign(self): for record in self: record.is_show_add_sign = True domain_hide_01 = [ ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('name', '=', '加签'), ('approval_state', 'in', ['0', '1']), ] has_01 = record.log_ids.search(domain_hide_01) domain_hide_done = [ ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('name', '=', '加签'), ('approval_state', '=', '2'), ('done_by', '=', self.env.user.id), ] has_done = record.log_ids.search(domain_hide_done) # 如果有日志,取【第一个节点】的经办人 == 当前登录人 → 隐藏 has_first_user = False if record.log_ids: first_log = record.log_ids.sorted('sequence')[:1] if first_log and first_log.done_by == self.env.user: has_first_user = True # :判断当前登录人是否在 log_ids2 中 has_log_ids2_user = False if record.log_ids2: has_log_ids2_user = self.env.user.id in record.log_ids2.mapped('done_by.id') # 任意一个条件满足 → 隐藏 if has_01 or has_done or has_first_user or has_log_ids2_user: record.is_show_add_sign = False def _compute_is_show_transfer(self): for record in self: record.is_show_transfer = False domain_has_sign = [ ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('name', '=', '加签'), ('approval_state', 'in', ['0', '1']), ('done_by', '=', self.env.user.id), ] has_sign_ok = record.log_ids.search(domain_has_sign) if has_sign_ok: record.is_show_transfer = True def _compute_is_send(self): for record in self: record.is_send = False log_ids_domain = [ ('done_by', '=', self.env.user.id), ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('approval_state', 'in', ['0', '1']), ('name', '=', '分发') ] lg1 = record.log_ids.search(log_ids_domain) if lg1: record.is_send = True def _compute_is_back(self): for record in self: record.is_back = False current_uid = self.env.uid user_matched_logs = record.log_ids.filtered( lambda log: log.done_by.id == current_uid ).sorted(key=lambda log: log.create_date) if not user_matched_logs: continue # 取当前登录人对应日志的最后一条(最新的) current_user_last_log = user_matched_logs[-1] all_sorted_logs = record.log_ids.sorted(key=lambda log: log.create_date) all_sorted_list = record.log_ids.sorted(key=lambda log: log.create_date).ids # 找到当前日志在所有日志中的索引 try: current_log_index = all_sorted_list.index(current_user_last_log.id) except ValueError: current_log_index = -1 if current_log_index != -1 and (current_log_index + 1) < len(all_sorted_list): next_log = all_sorted_logs[current_log_index + 1] if next_log.approval_state == "0": record.is_back = True same_node_logs = record.log_ids.filtered(lambda log: log.name == next_log.name) if same_node_logs and any(log.approval_state in ['1', '2'] for log in same_node_logs): record.is_back = False if record._name in ['yuthon.official.incoming.normal', 'yuthon.sub.enterprises']: current_user_in_logs2 = any(log.done_by.id == self.env.uid for log in record.log_ids2) organizer_user_in_logs = any(log.done_by.id == self.env.user.id for log in record.log_ids.filtered(lambda l: l.name == '承办')) all_handling_logs_approval_0 = all(log.approval_state == '0' for log in record.log_ids.filtered(lambda l: l.name == '承办')) all_logs2_approval_0 = all(log.approval_state == '0' for log in record.log_ids2) if all_logs2_approval_0 and all_handling_logs_approval_0 and not current_user_in_logs2 and not organizer_user_in_logs: record.is_back = True else: record.is_back = False def _compute_is_active(self): _logger.info("_compute_is_active----" + str(fields.datetime.now())) current_user_id = self.env.user.id self.is_active = False if self.log_ids: if self.log_ids[-1].name == '归档' and self.log_ids[-1].done_by.id == current_user_id and self.log_ids[-1].approval_state in ['0', '1']: self.is_active = True _logger.info("_compute_is_active----" + str(fields.datetime.now())) def _compute_is_done(self): _logger.info("_compute_is_done循环开始----" + str(fields.datetime.now())) for record in self: record.is_done = False log_ids_domain = [ ('done_by', '=', self.env.user.id), ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ] lg1 = record.log_ids.search(log_ids_domain) if lg1: all_approved = all(log.approval_state == '2' for log in lg1) if all_approved: record.is_done = True workflow_record_id = self.env["soong.workflow.record"].search( [("res_model", "=", self._name), ("res_id", "=", self.id), ('reclaim_state', '=', False)]) if workflow_record_id: if workflow_record_id.approve_state == 'end': record.is_done = True _logger.info("_compute_is_done循环结束----" + str(fields.datetime.now())) def _compute_is_confirm(self): for record in self: record.is_confirm = False record.is_end = False target_models = ['yuthon.official.incoming.normal', 'yuthon.sub.enterprises'] if record._name in target_models: chairman_logs = record.log_ids.filtered( lambda log: '董事长' in (log.name or '') and log.approval_state in ['0', '1'] and log.done_by.id == self.env.user.id ) if chairman_logs: record.is_end = True continue log_ids_domain = [ ('done_by', '=', self.env.user.id), ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('approval_state', 'in', ['0', '1']), ('name', '=', '承办') ] log_ids2_domain = [ ('done_by', '=', self.env.user.id), ('workflow_id', '=', record.workflow_id.id), ('res_id', '=', record.id), ('approval_state', 'in', ['0', '1']) ] lg1 = record.log_ids.search(log_ids_domain) lg2 = record.log_ids2.search(log_ids2_domain) lg3 = record.log_ids.search(log_ids2_domain) if lg1 or lg2: record.is_confirm = True if not record.is_confirm: record.is_end = True if not lg2 and not lg3: record.is_end = False if record.log_ids: if record.log_ids[-1].name == '归档': record.is_end = False @api.depends("node_id", "log_ids") def _compute_approval_sign_ids(self): for record in self: record.approval_sign_ids = record.log_ids.filtered(lambda r: r.node_btn_name == "2" and r.node_id == record.node_id).done_by.ids @api.depends("state") def _compute_workflow_id(self): # Objects wkf_obj = self.env["soong.workflow"] for rec in self: # Variables rec_model = rec._name # Get Workflow rec.workflow_id = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)]) node_id = fields.Many2one( comodel_name="soong.workflow.node", string="Workflow Node Ref.", compute="_compute_node_id", ) @api.depends("state") def _compute_node_id(self): # Objects wkf_obj = self.env["soong.workflow"] for rec in self: # Variables rec_model = rec._name # Get Workflow Node rec.node_id = wkf_obj.search( [("model_id", "=", rec_model), ('is_enable', '=', True)] ).node_ids.filtered(lambda n: n.node_name == rec.state) log_ids = fields.One2many(comodel_name="soong.workflow.log", inverse_name="res_id", string=" ", domain=lambda self: [("model", "=", self._name)], auto_join=True) log_ids2 = fields.One2many(comodel_name="soong.workflow.log2", inverse_name="res_id", string=" ", domain=lambda self: [("model", "=", self._name)], auto_join=True) # log_ids3 = fields.One2many(comodel_name="soong.workflow.log3", inverse_name="res_id", string=" ", # domain=lambda self: [("model", "=", self._name)], auto_join=True) soong_record_ids = fields.One2many(comodel_name="soong.workflow.line", inverse_name="res_id", string=" ",) project_ids = fields.One2many(comodel_name="project.project.line", inverse_name="res_id", string=" ",) def get_logs_for_print(self, applicant_first=False): """ 返回用于打印的审批日志列表: - 按与界面相同的顺序(sequence 升序) - 同名节点只保留最新一条 - 可选:申请人置顶 """ self.ensure_one() logs = self.log_ids.sorted('sequence') seen = set() ordered = [] # 逆序去重,保留最新 for log in reversed(logs): key = log.name or '' if key in seen: continue seen.add(key) ordered.append(log) ordered.reverse() # 恢复原有顺序(去重后) if applicant_first: applicants = [l for l in ordered if (l.name or '') == '申请人'] others = [l for l in ordered if (l.name or '') != '申请人'] ordered = applicants + others return ordered # @api.model_create_multi # def create(self, vals_list): # res = super().create(vals_list) # target_role = self.env['soong.user.role'].search( # [('name', '=', '考勤校对'), ('company_id', '=', res.env.user.employee_id.company_id.id)], # limit=1 # ) # target_role_id = target_role.id if target_role else None # # for record in res: # button_ids = record.sudo().workflow_id.node_ids.button_ids.search( # [('workflow_id', '=', record.workflow_id.id)]) # # 获取当前用户的主角色和辅助角色ID列表 # employee = record.env.user.employee_id # current_user_role_ids = [] # if employee.user_role_id: # current_user_role_ids.append(employee.user_role_id.id) # if employee.user_role_ids: # current_user_role_ids.extend(employee.user_role_ids.ids) # if target_role_id in current_user_role_ids: # continue # for button_id in button_ids: # if button_id.node_id.is_end: # continue # allowed_departments = button_id.node_id.department_id # if allowed_departments and employee.department_id.id not in allowed_departments.ids: # continue # allowed_role_ids = [] # if button_id.users_role_id: # allowed_role_ids.append(button_id.users_role_id.id) # elif button_id.users_role_group_ids: # allowed_role_ids.extend(button_id.users_role_group_ids.user_role_ids.ids) # else: # continue # if current_user_role_ids and any(role_id in allowed_role_ids for role_id in current_user_role_ids): # record.state = button_id.node_id.node_name # return res def _create_update_logs(self, btn_rec): log_obj = self.env["soong.workflow.log"] if btn_rec.name not in ['转交', '分发', '加签', '加签确认', '退回', '收回', '结束', '归档', '确认']: record_obj = log_obj.search( [("model", "=", self._name), ("res_id", "=", self.id), ("node_id", "=", btn_rec.node_id.id), ("workflow_id", "=", btn_rec.workflow_id.id), ("node_btn_id", "=", btn_rec.id), ("node_btn_name", "=", "0") ], order="lase_click_date DESC", limit=1) if not record_obj: record_obj = log_obj.create({ "model": self._name, "res_id": self.id, "node_id": btn_rec.node_id.id, "workflow_id": btn_rec.workflow_id.id, "node_btn_id": btn_rec.id, "done_by": self.env.uid, "node_btn_name": "0", "lase_click_date": fields.Datetime.now(), }) record_obj.write({"node_btn_id": btn_rec.id, "done_by": self.env.uid, "approval_state": "2"}) record_obj = log_obj.search([ ("model", "=", self._name), ("res_id", "=", self.id), ("node_id", "=", btn_rec.node_id.id), ("workflow_id", "=", btn_rec.workflow_id.id), ], order="lase_click_date DESC", limit=1 ) return record_obj def btn_exec_action(self): """ Got invoked when a workflow button is called. :return: button action return value. """ self.ensure_one() # Variables cx = self.env.context.copy() or {} cx.update({"active_id": self.id, "active_ids": self.ids}) wkf_btn_obj = self.env["soong.workflow.node.button"] btn_rec = wkf_btn_obj.search([("btn_key", "=", cx.get("btn_key", False))]) # # 优化弹窗 if btn_rec: log_obj = self._create_update_logs(btn_rec) # # 判断加签 # if btn_rec.name in ["加签"]: # # 弹窗 进行填写 人员 # wizard = self.env.ref("soong_workflow.view_soong_workflow_apostille_wizard") # return { # "name": _("添加加签人员"), # "type": "ir.actions.act_window", # "view_mode": "form", # "res_model": "soong.workflow.apostille.wizard", # "views": [(wizard.id, "form")], # "view_id": wizard.id, # "target": "new", # "context": { # "default_res_id": self.id, # "default_res_model": self._name, # "default_log_ids": log_obj.ids, # "default_node_btn_id": btn_rec.id, # }, # } # if btn_rec.name in ["加签确认"]: # # 弹窗 进行填写 人员 # wizard = self.env.ref("soong_workflow.view_soong_workflow_apostille_ok_wizard") # return { # "name": _("添加加签人员"), # "type": "ir.actions.act_window", # "view_mode": "form", # "res_model": "soong.workflow.apostille.ok.wizard", # "views": [(wizard.id, "form")], # "view_id": wizard.id, # "target": "new", # "context": { # "default_res_id": self.id, # "default_res_model": self._name, # "default_log_ids": log_obj.ids, # "default_node_btn_id": btn_rec.id, # }, # } # if btn_rec.name not in ["加签", "加签确认"]: # # 判断当前是否所有加签都已经结束 # apostille_log_obj = self.sudo().env["soong.workflow.log"].search( # [ # ("model", "=", self._name), # ("res_id", "=", self.id), # ("node_id", "=", btn_rec.node_id.id), # ("workflow_id", "=", btn_rec.workflow_id.id), # ("node_btn_name", "=", "2"), # ("approval_state", "!=", "2") # ] # ) # if apostille_log_obj: # raise UserError("当前加签人员还有没有审批完成的!") # 继续正常流程 # if btn_rec.has_comment: return self.with_context(cx)._add_comment(log_obj, btn_rec) # if btn_rec.name == '收回': # return { # 'name': '收回弹窗', # 'type': 'ir.actions.act_window', # 'view_mode': 'form', # 'res_model': 'soong.bounced.wizard', # "target": "new", # 'context': { # 'default_res_id': self.id, # "default_res_model": self._name, # } # } # else: # btn_rec.with_context(cx).log() # return btn_rec.with_context(cx).run() def _add_comment(self, logs, btn_rec): trans_link_ids = [] wizard = self.env.ref("soong_workflow.view_soong_workflow_comment_wizard") workflow_record_id = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id), ('reclaim_state', '=', False)]) # 状态列表 node_list = btn_rec.workflow_id.node_ids.sorted('sequence') if self.name_title != '采购申请单': state_list = [i.node_name for i in btn_rec.workflow_id.node_ids.sorted('sequence')] else: state_list = [i.node_name for i in btn_rec.workflow_id.node_ids.sorted('sequence')] + ['purchase'] # 当前节点状态 state = self.state if self.state == 'draft': state = '询价' self_state = state_list.index(state) # 当前节点 if btn_rec.name not in ['结束', '归档']: current_node = node_list[state_list.index(state)] workflow_record_id = self.env["soong.workflow.record"].search( [("res_model", "=", self._name), ("res_id", "=", self.id)]) self.env['trans.link'].search([('workflow_record_id', '=', workflow_record_id.id)]).unlink() # 退回节点根据日志进行判断 if btn_rec.name == '退回': approved_logs = self.log_ids.filtered(lambda log: log.approval_state == '2') for ret in approved_logs: if not self.env['trans.link'].search([('workflow_record_id', '=', workflow_record_id.id), ('name', '=', ret.name), ('link_type', '=', 'return')]): self.env['trans.link'].create({ 'sequence': ret.sequence, 'workflow_record_id': workflow_record_id.id, 'node_button_id': btn_rec.id, 'name': ret.name, 'link_type': 'return', 'record_id': self.id, }) if btn_rec.name == '转交': required_fields = [ field.name for field in current_node.field_ids if field.required and field.name ] missing_fields = [] if required_fields: for field_name in required_fields: field_value = getattr(self, field_name.name, None) if not field_value: missing_fields.append(field_name.field_description) if missing_fields: raise UserError(f"转交失败!必填字段未填写:{', '.join(missing_fields)}") skipped_node_names = [] # 因没有可用按钮而跳过的节点 node_list = list(btn_rec.workflow_id.node_ids.sorted('sequence')) index = self_state actual_path_nodes = [] #跟踪整个工作流路径,只保留实际会执行的节点 while index < len(node_list): current_node = node_list[index] actual_path_nodes.append(current_node) if not current_node.out_link_ids: index = 20 # 防止死循环 break current_department = self.department_id if btn_rec.link_id in current_node.out_link_ids and len(current_node.out_link_ids) == 1: matched_link = btn_rec.link_id next_node = matched_link.node_to matched_key = next_node.name else: if len(current_node.out_link_ids) > 1: # 筛选与当前节点部门匹配的后续节点 valid_nodes = [] for link in current_node.out_link_ids: if self.name_title in ['汇审意见表', '信息发布申请表',]: category_names = self.information_categories_ids.mapped('name') target_name = link.name.split('->')[-1].strip() if '工会活动' in category_names: if target_name == '工会主席': target_node = link.node_to valid_nodes.append((target_node, link)) else: if target_name == '复核': target_node = link.node_to valid_nodes.append((target_node, link)) if target_name == '承办部门意见': target_node = link.node_to valid_nodes.append((target_node, link)) if '经营业务(财务专用)'in category_names: if target_name == '财务部': target_node = link.node_to valid_nodes.append((target_node, link)) else: target_node = link.node_to if target_node.department_id and any( dept in current_department or current_department in self.get_all_sub_departments(dept) for dept in target_node.department_id ): valid_nodes.append((target_node, link)) continue # ===================== 新增:部门为空 → 走【角色/角色组】匹配 ===================== if not target_node.department_id: if hasattr(self, 'user_id') and self.user_id: applicant_user = self.user_id elif hasattr(self, 'users_id') and self.users_id: applicant_user = self.users_id applicant_roles = set() if applicant_user and applicant_user.employee_id: if applicant_user.employee_id.user_role_id: applicant_roles.add(applicant_user.employee_id.user_role_id.id) if applicant_user.employee_id.user_role_ids: applicant_roles.update(applicant_user.employee_id.user_role_ids.ids) button = None if link.node_to.button_ids: transfer_btn = link.node_to.button_ids.filtered(lambda r: r.name == '转交') if transfer_btn: for btn in transfer_btn: if btn.is_review_invisible: try: domain = eval(btn.invisible_expression) expression_result = self.search(domain) has_self_in_result = self.id in expression_result.ids if has_self_in_result: button = btn break except: continue else: button = btn break if not button: end_btn = link.node_to.button_ids.filtered(lambda r: r.name == '结束') if end_btn: button = end_btn[0] if button: valid_nodes.append((target_node, link)) continue # 尝试父部门匹配 if not valid_nodes and current_department and current_department.parent_id: parent_department = current_department.parent_id for link in current_node.out_link_ids: target_node = link.node_to if target_node.department_id and any( dept in parent_department or parent_department in self.get_all_sub_departments(dept) for dept in target_node.department_id ): valid_nodes.append((target_node, link)) # 如果两种匹配都失败,将所有节点加入有效列表 if not valid_nodes: for link in current_node.out_link_ids: valid_nodes.append((link.node_to, link)) final_valid_nodes = [] for node, link in valid_nodes: transfer_buttons = [btn for btn in node.button_ids if btn.name == '转交'] end_buttons = [btn for btn in node.button_ids if btn.name == '结束'] has_visible_transfer_btn = False has_visible_end_btn = False # 判断转交按钮可见性 if transfer_buttons: for btn in transfer_buttons: if btn.is_review_invisible: try: domain = eval(btn.invisible_expression) expression_result = self.search(domain) if self.id in expression_result.ids: has_visible_transfer_btn = True break except: continue else: has_visible_transfer_btn = True break # 判断结束按钮可见性 if end_buttons: for btn in end_buttons: if btn.is_review_invisible: try: domain = eval(btn.invisible_expression) expression_result = self.search(domain) if self.id in expression_result.ids: has_visible_end_btn = True break except: continue else: has_visible_end_btn = True break # 两个按钮都不可见 → 跳过 if not has_visible_transfer_btn and not has_visible_end_btn: skipped_node_names.append(node.name) _logger.info(f"跳过节点: {node.name},转交按钮和结束按钮均不可见") else: final_valid_nodes.append((node, link)) # 选择第一个匹配的节点 next_node, matched_link = valid_nodes[0] matched_key = next_node.name else: matched_link = current_node.out_link_ids[0] next_node = matched_link.node_to matched_key = next_node.name transfer_buttons = [ btn for btn in next_node.button_ids if btn.name == '转交' ] end_buttons = [ btn for btn in next_node.button_ids if btn.name == '结束' ] has_visible_transfer_btn = False has_visible_end_btn = False # ========== 第一步:判断转交按钮(transfer_buttons)是否可见 ========== if transfer_buttons: for btn in transfer_buttons: if btn.is_review_invisible: # 解析隐藏表达式,判断当前记录是否在表达式结果中 domain = eval(btn.invisible_expression) expression_result = self.search(domain) has_self_in_result = self.id in expression_result.ids # 转交按钮可见条件:表达式结果包含当前记录 if has_self_in_result: has_visible_transfer_btn = True break # 找到可见按钮,终止循环 else: # 无隐藏控制,转交按钮默认可见 has_visible_transfer_btn = True break # 找到可见按钮,终止循环 # ========== 第二步:判断结束按钮(end_buttons)是否可见 ========== if end_buttons: for btn in end_buttons: if btn.is_review_invisible: # 解析隐藏表达式,判断当前记录是否在表达式结果中 domain = eval(btn.invisible_expression) expression_result = self.search(domain) has_self_in_result = self.id in expression_result.ids # 结束按钮可见条件:表达式结果包含当前记录 if has_self_in_result: has_visible_end_btn = True break # 找到可见按钮,终止循环 else: # 无隐藏控制,结束按钮默认可见 has_visible_end_btn = True break # 找到可见按钮,终止循环 if not has_visible_transfer_btn and not has_visible_end_btn: skipped_node_names.append(next_node.name) _logger.info(f"跳过节点: {next_node.name},转交按钮和结束按钮均不可见") # 更新索引 index = state_list.index(matched_key) if index >= len(node_list): break #根据实际路径创建trans.link记录并生成domain actual_path_links = [] self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id)]) for node in actual_path_nodes[1:]: workflow_record_id = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id)]) if node.name not in skipped_node_names: link = self.env['trans.link'].create({ 'sequence': node.sequence, 'workflow_record_id': workflow_record_id.id, 'node_button_id': btn_rec.id, 'name': node.name, 'link_type': 'trans', 'record_id': self.id, }) actual_path_links.append(link) if actual_path_links: trans_link_ids = [link.id for link in actual_path_links] trans_domain = [('id', 'in', trans_link_ids)] else: trans_domain = [] is_supervising = False if self.name_title in ['行政发文']: is_supervising = self.is_supervising comment_field_map = { '转交': 'transfer_comment_id', '退回': 'return_comment_id', '确认': 'confirm_comment_id', '结束': 'end_comment_id' } comment_note_id = False if btn_rec.name in comment_field_map: target_field = comment_field_map[btn_rec.name] if hasattr(btn_rec.node_id, target_field) and btn_rec.node_id[target_field]: comment_note_id = btn_rec.node_id[target_field].id trans_link_id = self.env['trans.link'].search(trans_domain, limit=1).sorted('sequence').id if btn_rec.name == '转交' else False return_link_id = self.env['trans.link'].search( [('link_type', '=', 'return'), ('record_id', '=', self.id)], limit=1 ).sorted('sequence')[:1].id if btn_rec.name == '退回' else False all_links = self.env['trans.link'].browse(trans_link_ids) invalid_links = self.env['trans.link'] current_link = self.env['trans.link'].browse(trans_link_id) allowed_link_ids = [] default_trans_link_id = trans_link_id if btn_rec.name == '转交' and trans_link_ids: if current_link and all_links: sorted_all_links = all_links.sorted('sequence') for idx, link in enumerate(sorted_all_links): approvers = self._get_approvers_for_link(link, self) if len(approvers) == 1: current_approver = approvers[0] node_id = self.workflow_id.node_ids.search([ ('name', '=', link.name), ('workflow_id', '=', self.workflow_id.id) ], limit=1) if node_id: # 下一个相同审批人跳过 if node_id.same_user_skip_type == 'next': if idx + 1 < len(sorted_all_links): next_link = sorted_all_links[idx + 1] next_approvers = self._get_approvers_for_link(next_link, self) if len(next_approvers) == 1 and next_approvers[0] == current_approver: invalid_links |= next_link # 上一个相同审批人跳过 if node_id.same_user_skip_type == 'prev': if idx - 1 >= 0: prev_link = sorted_all_links[idx - 1] prev_approvers = self._get_approvers_for_link(prev_link, self) if len(prev_approvers) == 1 and prev_approvers[0] == current_approver: invalid_links |= prev_link allowed_link_ids = [lid for lid in trans_link_ids if lid not in invalid_links.ids] default_trans_link_id = allowed_link_ids[0] if allowed_link_ids else trans_link_id return { "name": _("评论"), "type": "ir.actions.act_window", "view_mode": "form", "res_model": "soong.workflow.comment.wizard", "views": [(wizard.id, "form")], "view_id": wizard.id, "target": "new", "context": { "default_return_link_id": return_link_id, "default_res_id": self.id, 'default_comment_note_id': comment_note_id, "default_code": self.code, "default_workflow_record_id": workflow_record_id.id, 'default_illustrate':btn_rec.node_id.illustrate, "default_res_model": self._name, "default_node_btn_id": btn_rec.id, "default_is_supervising": is_supervising, "default_trans_link_id": default_trans_link_id, "all_workflow_links": allowed_link_ids, "default_invalid_link_ids": invalid_links.ids, }, } def get_all_sub_departments(self, department, level=0, print_tree=True): all_departments = department.child_ids # 打印当前部门的子部门 if print_tree and department.child_ids: indent = " " * level # 使用空格缩进表示层级 print(f"{indent}{department.name} 的子部门:") for child in department.child_ids: child_indent = " " * (level + 1) print(f"{child_indent}- {child.name} (ID: {child.id})") # 递归获取每个子部门的子部门 for child in department.child_ids: child_sub_departments = self.get_all_sub_departments(child, level + 1, print_tree) all_departments |= child_sub_departments return all_departments @api.constrains("state") def _update_wkf_records(self): for rec in self: record_obj = self.env["soong.workflow.record"].search([("res_model", "=", rec._name), ("res_id", "=", rec.id)]) record_id = self.env[rec._name].search([('id', '=', rec.id)]) current_user_id = self.env.user.id if self.state in ['申请人', '拟办', '草稿', 'draft']: if not record_obj: values = { "code": record_id.code, "res_model": rec._name, "department_id": rec.department_id.id, "res_id": rec.id, "workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id, "done_by": self.env.uid, "users_ids": [(4, current_user_id)], "employee_ids": self.env.user.employee_id, "write_date2": fields.Datetime.now(), } # 查找匹配的字段 matched_fields = self.find_fields_by_string(record_id) # 提取"事由"或"正文"字段的值 text_fields = [f for f in matched_fields if '事由' in f['field_string'] or '正文' in f['field_string']] if text_fields: # 优先使用"事由"字段,没有则使用第一个"正文"字段 reason_field = next((f for f in text_fields if '事由' in f['field_string']), text_fields[0]) values["text"] = getattr(rec, reason_field['field_name'], '') # 提取"附件"字段的值 attachment_fields = [f for f in matched_fields if '附件' in f['field_string']] if attachment_fields and attachment_fields[0]['field_type'] in ['many2many', 'one2many']: # 处理附件关系字段 attachment_field = attachment_fields[0] attachment_ids = getattr(rec, attachment_field['field_name'], []) if attachment_ids: values["documents_ids"] = [(6, 0, attachment_ids.ids)] # 创建工作流记录 self.env["soong.workflow.record"].create(values) if record_obj: # 获取上一节点的结束时间(即log_ids最后一条记录的lase_click_date) node_update_time = False try: if hasattr(rec, 'log_ids') and rec.log_ids: # 按ID排序获取最后一条日志 sorted_logs = rec.log_ids.sorted(lambda log: log.id) if sorted_logs and sorted_logs[-1].lase_click_date: node_update_time = sorted_logs[-1].lase_click_date # 补丁:如果审批状态已经是结束,但还没拿到时间,则尝试用当前时间作为办结时间 if not node_update_time and record_obj.approve_state == 'end': node_update_time = fields.Datetime.now() except Exception as e: _logger.debug(f"Failed to get node_update_time: {e}") # 更新记录 update_vals = { "workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id, "done_by": self.env.uid, "write_date2": fields.Datetime.now(), } if node_update_time: update_vals["node_update_time"] = node_update_time record_obj.update(update_vals) def find_fields_by_string(self,record_id): """ 根据模型名称和记录ID查找字段,其string属性包含特定关键词 :param record_id: 记录id :return: 匹配的字段信息列表 """ # 定义要查找的关键词 keywords = ['正文', '事由', '附件'] # 获取模型的所有字段 fields = record_id.fields_get() # 存储匹配的字段信息 matched_fields = [] # 遍历所有字段,检查string属性 for field_name, field_attrs in fields.items(): field_string = field_attrs.get('string', '') # 检查字段的string是否包含任何关键词 if any(keyword in field_string for keyword in keywords): matched_fields.append({ 'field_name': field_name, 'field_string': field_string, 'field_type': field_attrs.get('type'), }) return matched_fields def unlink(self): self.mapped("log_ids").unlink() self.mapped("log_ids2").unlink() if self.env.user.name != 'Administrator': if len(self.log_ids) > 1: raise UserError("该流程不允许删除,如果要调整请联系管理员") record_objs = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "in", self.ids)]) record_objs.unlink() return super(SoongWorkflowMixin, self).unlink() def action_open_diagram(self): self.ensure_one() current_node_id = self.workflow_id.node_ids.filtered( lambda n: n.node_name == self.state ) highlight_color = self.workflow_id.current_stage_highlight_color return { "type": "ir.actions.client", "tag": "open_diagram_plus_dialog", "params": { "act_window": { "name": _("Workflow: %(name)s") % {"name": self.display_name}, "res_model": "soong.workflow", "type": "ir.actions.act_window", "res_id": self.workflow_id.id, "target": "new", "dialog_size": "medium", "context": { "highlight_node_id": current_node_id.id, "highlight_node_color": highlight_color, "diagram_readonly": True, }, "views": [(False, "diagram_plus")], } } } # return { # "name": _("Workflow: %(name)s") # % { # "name": self.display_name, # }, # "res_model": "soong.workflow", # "type": "ir.actions.act_window", # "res_id": self.workflow_id.id, # "target": "new", # "dialog_size": "medium", # "context": { # "highlight_node_id": current_node_id.id, # "highlight_node_color": highlight_color, # "diagram_readonly": True, # }, # "views": [(False, "diagram_plus")], # } @api.model def get_view(self, view_id=None, view_type="form", **options): res = super().get_view(view_id, view_type, **options) active_id = self._context.get('active_id') if '