# -*- 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" def read(self, fields=None, load='_classic_read'): if self.log_ids: if self.log_ids[-1].done_by.id == self.env.user.id and self.log_ids[-1].approval_state == '0': self.log_ids[-1].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 @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 ) 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="加签人员") @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) 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=" ",) @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: 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") # 状态列表 state_list = [i.node_name for i in btn_rec.workflow_id.node_ids.sorted('sequence')] # 当前节点状态 self_state = state_list.index(self.state) self.env['trans.link'].search([]).unlink() # 退回节点根据日志进行判断 if btn_rec.name == '退回': for ret in self.log_ids.search([('approval_state', '=', '2'), ('res_id', '=', self.id), ('node_btn_name', '=', '0')]): if not self.env['trans.link'].search([('name', '=', ret.name)]): self.env['trans.link'].create({ 'node_button_id': btn_rec.id, 'name': ret.name, 'link_type': 'return', 'record_id': self.id, }) if btn_rec.name == '转交': 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 len(current_node.out_link_ids) > 1: # 筛选与当前节点部门匹配的后续节点 valid_nodes = [] for link in current_node.out_link_ids: 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)) # 尝试父部门匹配 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)) # 选择第一个匹配的节点 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 = True 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 not end_buttons: if has_self_in_result: has_visible_transfer_btn = True break else: has_visible_transfer_btn = False else: has_visible_transfer_btn = True else: has_visible_transfer_btn = True if not has_visible_transfer_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 = [] for node in actual_path_nodes[1:]: if node.name not in skipped_node_names: link = self.env['trans.link'].create({ '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._module in ['yuthon_official']: is_supervising = self.is_supervising 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_res_id": self.id, "default_code": self.code, "default_res_model": self._name, "default_log_ids": logs.ids, "default_node_btn_id": btn_rec.id, "default_is_supervising": is_supervising, "default_trans_link_id": self.env['trans.link'].search(trans_domain, limit=1).id if btn_rec.name == '转交' else False, "all_workflow_links": trans_link_ids, "default_return_link_id": self.env['trans.link'].search( [('link_type', '=', 'return'), ('record_id', '=', self.id)], limit=1).id if btn_rec.name == '退回' else False, }, } 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 ['申请人', '拟办', '草稿']: 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: record_obj.update({"workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id, "done_by": self.env.uid, "write_date2": fields.Datetime.now(), }) 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 { "name": _("Workflow: %(name)s") % { "name": self.display_name, }, "res_model": "soong.workflow", "type": "ir.actions.act_window", "res_id": self.workflow_id.id, "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 '