# -*- 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 lxml import etree import logging _logger = logging.getLogger(__name__) class SoongWorkflowMixin(models.AbstractModel): _name = "soong.workflow.mixin" _description = "Soong Workflow Mixin" @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', '=', 'yes')]) 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', '=', 'yes')]) 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", ) approve_state = fields.Selection( [ ("start", "Start"), ("pending", "Pending"), ("end", "End"), ], default="start", string="Approve Status", compute="_compute_node_id", ) 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', string="加签人员") @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', '=', 'yes')]) 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', '=', 'yes')] ).node_ids.filtered(lambda n: n.node_name == rec.state) if rec.node_id.flow_start: rec.approve_state = "start" elif rec.node_id.flow_end: rec.approve_state = "end" else: rec.approve_state = "pending" log_ids = fields.One2many( comodel_name="soong.workflow.log", inverse_name="res_id", string="Logs", domain=lambda self: [("model", "=", self._name)], auto_join=True, ) def _create_update_logs(self, btn_rec): log_obj = self.env["soong.workflow.log"] record_obj = log_obj.search( [ ("model", "=", self._name), ("res_id", "=", self.id), ("node_btn_id", "=", btn_rec.id), ] ) if not record_obj: record_obj = log_obj.create( { "model": self._name, "res_id": self.id, "node_btn_id": btn_rec.id, "done_by": self.env.uid, "lase_click_date": fields.Datetime.now(), } ) record_obj.update({"done_by": self.env.uid, "lase_click_date": fields.Datetime.now()}) 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.has_comment: return self.with_context(cx)._add_comment(log_obj, btn_rec) return btn_rec.with_context(cx).run() def _add_comment(self, logs, btn_rec): wizard = self.env.ref("soong_workflow.view_soong_workflow_comment_wizard") # 状态列表 state_list = ['拟办' if i.node_name == 'start' else i.node_name for i in btn_rec.workflow_id.node_ids] # 当前节点状态 self_state = state_list.index('拟办' if self.state == 'start' else self.state) # 下一个节点 next_state = btn_rec.workflow_id.node_ids[self_state + 1].button_ids[0] self.env['trans.link'].search([]).unlink() if btn_rec.name == '转交' and len(btn_rec.node_id.out_link_ids) > 1: for state in btn_rec.node_id.out_link_ids: self.env['trans.link'].create({ 'node_button_id': btn_rec.id, 'name': state.name.split('-> ')[1], 'link_type': 'trans', 'record_id': self.id, }) elif btn_rec.name == '退回': for state in state_list[0:self_state]: self.env['trans.link'].create({ 'node_button_id': btn_rec.id, 'name': state, 'link_type': 'return', 'record_id': self.id, }) # 不自动匹配 employee_user_role_ids = self.env['hr.employee'].search([('user_role_id', '=', next_state.users_role_id.id)]) # 自动匹配 employee_user_role_id = self.env['hr.employee'].search([('user_role_id', '=', next_state.users_role_id.id), ('department_id', '=', self.department_id.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_res_id": self.id, "default_res_model": self._name, "default_log_ids": logs.ids, "default_is_automatic": next_state.is_automatic, "default_employee_ids": employee_user_role_ids.ids if not next_state.is_automatic else employee_user_role_id.ids, "default_node_btn_id": btn_rec.id, "default_node_link_len": '转交2' if len(btn_rec.node_id.out_link_ids) > 1 and btn_rec.name == '转交' else '', }, } @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)]) current_user_id = self.env.user.id if not record_obj: record_obj = self.env["soong.workflow.record"].create({ "res_model": rec._name, "res_id": rec.id, "workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id, "users_ids": [(4, current_user_id)], }) record_obj.update({"workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id, "users_ids": [(4, current_user_id)], }) def unlink(self): self.mapped("log_ids").unlink() record_objs = self.env["soong.workflow.record"].search( [("res_model", "=", self._name), ("res_id", "in", self.ids)] ) record_objs.unlink() return super().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): # """ Set the correct label for `unit_amount`, depending on company UoM """ # arch, view = super()._get_view(view_id, view_type, **options) # for node in arch.xpath("//header"): # node.append(etree.XML("""booooooo""")) # return arch, view @api.model def get_view(self, view_id=None, view_type="form", **options): res = super().get_view(view_id, view_type, **options) if 'create="false"' not in res['arch']: if view_type in ["tree", "form"]: params = {} if "field" in res["arch"]: res = self._remove_default_header(view_type, res) # res = self._remove_default_modifiers(view_type, res) res = self._load_buttons_view(view_type, res) res = self._load_button_box_view(view_type, res) res = self._load_state_view(view_type, res) # res = self._load_one2many_views(view_type, res) res = self._update_fields_view(view_type, res) # res = self._add_state_tree_view(view_type, res) res = self._add_soong_workflow_log(view_type, res, params) res = self._load_sign_view(view_type, res) return res # 收回 def action_withdraw(self): for record in self: employee_id = self.env['hr.employee'] state_list = [] for state in record.workflow_id.node_ids: if state.name == '拟办': state_list.append('start') elif state.name == '结束': state_list.append('end') else: state_list.append(state.name) self_node_id = record.workflow_id.node_ids.search([('name', '=', self.state)]) transmitted = self_node_id.button_ids.search([('name', '=', '转交'), ('node_id', '=', self_node_id.id)]) if transmitted.btn_hide: if transmitted.review_type == 'user_role': user_ids_list = employee_id.search(['|', ('user_role_id', '=', transmitted.users_role_id.id), ('user_role_ids', 'in', [transmitted.users_role_id.id])]).ids if self.env.user.employee_id.id in user_ids_list: raise UserError('当前节点不允许收回') else: state_index = state_list.index(self.state) self.write({'state': state_list[state_index-1]}) else: raise UserError('请配置节点角色') else: raise UserError('当前节点不允许收回') def _remove_default_header(self, view_type, res): """ Removes default header element from form view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ model = self._name wkf_obj = self.env["soong.workflow"] wkf_rec = wkf_obj.search([("model_id", "=", model), ('is_enable', '=', True)]) if not wkf_rec.remove_default_btn_until_end: return res # Variables arch = etree.fromstring(res["arch"]) # Find Default Header Element & EDIT it if view_type == "form": header_el = arch.xpath("//form/header") header_el = header_el[0] if header_el else False if header_el: for button in header_el.iter("button"): btn_invisible = "state != 'end'" if "invisible" in button.attrib: btn_invisible = btn_invisible + " or " + button.attrib["invisible"] button.set("invisible", btn_invisible) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _remove_default_modifiers(self, view_type, res): """ Removed fields default modifiers & attributes in form view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ # Variables model = self._name wkf_obj = self.env["soong.workflow"] wkf_rec = wkf_obj.search([("model_id", "=", model), ('is_enable', '=', True)]) arch = etree.fromstring(res["arch"]) # Validation if not wkf_rec.remove_default_attrs_mod: return res fields_name = wkf_rec.node_ids.field_ids.name.mapped("name") # Find Field & Remove Modifiers if view_type == "form": for field in arch.iter("field"): field_name = field.attrib.get("name", False) if not field_name or field_name not in fields_name: continue for attr in field.attrib.keys(): if attr != "name": field.attrib.pop(attr) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _load_buttons_view(self, view_type, res): """ Adds buttons to state bar in form view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ # Variables this = self model = self._name wkf_obj = self.env["soong.workflow"] arch = etree.fromstring(res["arch"]) # Helper Functions def _check_condition_code(btn): locals_dict = { "env": this.env, "model": this.env[model], "obj": None, "user": this.env["res.users"].browse(this.env.uid), "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": this.env["soong.workflow"], "warning": this.warning, "syslog": this.syslog, } try: safe_eval( btn.reviewer_expression, locals_dict=locals_dict, mode="exec", nocopy=True, ) user = "user" in locals_dict and locals_dict["user"] or False if user and self.env.user in user: return True return False except Warning as ex: raise ex except: raise UserError(_("Wrong python condition defined.")) # Find header & ADD button if view_type == "form": # Get Some Elements form_el = arch.xpath("//form") form_el = form_el[0] if form_el else False header_el = arch.xpath("//form/header") header_el = header_el[0] if header_el else False # Create Header Element If not Exists if header_el is False: header_el = etree.Element("header") form_el.insert(0, header_el) # Construct Buttons & Fields Tags wkf_rec = wkf_obj.search([("model_id", "=", model), ('is_enable', '=', True)]) for node in wkf_rec.node_ids: for button in node.button_ids: # Check Condition Code if button.btn_hide: if button.review_type == "individual": if self.env.user != button.reviewer_id: continue elif button.review_type == "group": if self.env.user not in button.reviewer_group_id.users: continue # elif button.review_type == 'user_role': # user_role_id = self.env['res.users'].search( # [('employee_id.user_role_id', '=', button.users_role_id.id), # ('employee_id.department_id', '=', button.users_role_id.department_id.id)]) # if self.env.user not in user_role_id: # continue else: if not _check_condition_code(button): continue # Add Button to View btn_exec_action_el = etree.SubElement(header_el, "button") btn_exec_action_el.set("name", "btn_exec_action") btn_exec_action_el.set("string", button.name) btn_exec_action_el.set("type", "object") if button.is_highlight: btn_exec_action_el.set("class", "oe_highlight") if button.has_icon: btn_exec_action_el.set("icon", button.icon) btn_exec_action_el.set( "invisible", "state != '%s'" % node.node_name ) btn_exec_action_el.set( "context", "{'btn_key':'%s','active_model':'%s'}" % (button.btn_key, self._name), ) # setup_modifiers(btn_exec_action_el) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _load_button_box_view(self, view_type, res): """ Adds button_box in form view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ arch = etree.fromstring(res["arch"]) # Find header & ADD button if view_type == "form": # Get Some Elements form_sheet_el = arch.xpath("//form/sheet") form_sheet_el = form_sheet_el[0] if form_sheet_el else False button_box_el = arch.xpath("//form/sheet/div[hasclass('oe_button_box')]") button_box_el = button_box_el[0] if button_box_el else False # Create button_box Element If not Exists if button_box_el is False: button_box_el = etree.Element("div") button_box_el.set("class", "oe_button_box") button_box_el.set("name", "button_box") if form_sheet_el: form_sheet_el.insert(0, button_box_el) # Add Button to View btn_exec_action_el = etree.SubElement(button_box_el, "button") btn_exec_action_el.set("name", "action_open_diagram") btn_exec_action_el.set("type", "object") btn_exec_action_el.set("string", _("View Flow")) btn_exec_action_el.set("class", "oe_stat_button") btn_exec_action_el.set("icon", "fa-code-fork") res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _load_state_view(self, view_type, res): """ Adds state bar to form view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ # Variables wkf_obj = self.env["soong.workflow"] model = self._name visible_seq = [] arch = etree.fromstring(res["arch"]) wkf_rec = wkf_obj.search([("model_id", "=", model), ('is_enable', '=', True)]) # Find header & ADD state if view_type == "form": # Get Header Element header_el = arch.xpath("//form/header") header_el = header_el[0] if header_el else False # Create State Element If not Exists if header_el is not False: state_el = etree.Element("field") state_el.set("name", "state") state_el.set("widget", "statusbar") # Loop all nodes for node in wkf_rec.node_ids: # Read Visible States if node.is_visible: visible_seq.append(node.node_name) # Set Attributes & Setup Modifiers for State Field if visible_seq: state_el.set("statusbar_visible", ",".join(visible_seq)) header_el.append(state_el) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _load_one2many_views(self, view_type, res): # Variables # wkf_obj = self.env["soong.workflow"] # model = self._name arch = etree.fromstring(res["arch"]) # wkf_rec = wkf_obj.search([("model_id", "=", model)]) if view_type == "form": # Locate the group element group_el = arch.xpath("//sheet") if group_el: group_el = group_el[0] else: # If no group element exists, create one and add it to the arch group_el = etree.Element("sheet") # Insert the group element at the beginning or end of the arch arch.insert(0, group_el) # Locate or create the notebook element within the group notebook_el = group_el.xpath(".//notebook") if notebook_el: notebook_el = notebook_el[0] else: # Create a notebook element if it doesn't exist notebook_el = etree.Element("notebook") group_el.append(notebook_el) # Create a new page element page_el = etree.Element("page") page_el.set("string", "审批明细") # Set a label for the new page # Create the field element for the one2many field field_el = etree.Element("field") field_el.set("name", "approval_users_ids") field_el_context = {'tree_view_ref': 'soong_workflow.soong_approval_users_search', 'default_res_model_id': self.env['ir.model']._get(self._name).id} field_el.set("context", str(field_el_context)) # Append the field element to the new page page_el.append(field_el) # Append the new page to the notebook notebook_el.append(page_el) # Convert the modified XML tree back to a string res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _load_sign_view(self, view_type, res): arch = etree.fromstring(res["arch"]) if view_type == "form": sheet_el = arch.xpath("//sheet") if sheet_el: sheet_el = sheet_el[0] else: sheet_el = etree.Element("sheet") arch.insert(0, sheet_el) notebook_el = sheet_el.xpath(".//notebook") if notebook_el: notebook_el = notebook_el[0] else: notebook_el = etree.Element("notebook") sheet_el.append(notebook_el) page_el = etree.Element("page") page_el.set("string", "加签人员") # 设置页面标签 page_el.set("invisible", "1") # todo domain field_m2m_el = etree.Element("field") field_m2m_el.set("name", "approval_sign_ids") field_m2m_el.set("widget", "many2many_tags") page_el.append(field_m2m_el) notebook_el.append(page_el) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _update_fields_view(self, view_type, res): """ Updates fields attributes. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ # Objects wkf_obj = self.env["soong.workflow"] user_obj = self.env["res.users"] # Variables model = self._name uid = self._uid wkf_rec = wkf_obj.search([("model_id", "=", model), ('is_enable', '=', True)]) arch = etree.fromstring(res["arch"]) # Helper Functions def _get_external_id(group): arr = [] ext_ids = group._get_external_ids() for ext_id in ext_ids: arr.append(ext_ids[ext_id][0]) return arr if view_type == "form": # Read fields of view for field in arch.iter("field"): field_name = field.attrib.get("name", False) if not field_name: continue # Get Fields Instance field_inst = arch.xpath("//field[@name='%s']" % str(field_name)) field_inst = field_inst[0] if field_inst else False # Scope Variables readonly_arr = [] required_arr = [] invisible_arr = [] if field_inst is not False: # Loop all nodes for node in wkf_rec.node_ids: # Loop Other Nodes for field_attrs in node.field_ids: # Record all states for each attribute if ( field_inst is not False and field_attrs.name.name == field_inst.attrib["name"] ): flag_show = True if field_attrs.readonly: readonly_arr.append(node.node_name) if field_attrs.required: required_arr.append(node.node_name) if field_attrs.invisible: invisible_arr.append(node.node_name) # Check Users & Groups if field_attrs.user_ids: user_rec = user_obj.browse(uid) if user_rec not in field_attrs.user_ids: flag_show = False if field_attrs.group_ids: has_group = False user_rec = user_obj.browse(uid) ext_ids = _get_external_id(field_attrs.group_ids) for ext_id in ext_ids: has_group = user_rec.has_group(ext_id) if not has_group: flag_show = False if not flag_show and ( field_attrs.group_ids or field_attrs.user_ids ): invisible_arr.append(node.node_name) # Set Attributes if readonly_arr: field_inst.set("readonly", "state in %s" % readonly_arr) if required_arr: field_inst.set("required", "state in %s" % required_arr) if invisible_arr: field_inst.set("invisible", "state in %s" % invisible_arr) res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _add_state_tree_view(self, view_type, res): """ Adds state field to tree view. :param view_type: Type of view now rendering. :param res: View resource data. :return: Updated view resource. """ # Variables model = self._name model_obj = self.env[model] arch = etree.fromstring(res["arch"]) # Add State Field to Tree View if not Exists if view_type == "tree": # Get Header Element tree_el = arch.xpath("//tree") tree_el = tree_el[0] if tree_el else False # Get State Element node_el = arch.xpath("//field[@name='node_id']") node_el = node_el[0] if node_el else False if node_el is False: node_el = etree.Element("field") node_el.set("name", "node_id") tree_el.append(node_el) approve_state_el = arch.xpath("//field[@name='approve_state']") approve_state_el = approve_state_el[0] if approve_state_el else False if approve_state_el is False: approve_state_el = etree.Element("field") approve_state_el.set("name", "approve_state") tree_el.append(approve_state_el) tree_el.set("decoration-muted", "approve_state == 'end'") tree_el.set("decoration-warning", "approve_state != 'end'") res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") return res def _add_soong_workflow_log(self, view_type, res, params): View = self.env["ir.ui.view"] doc = etree.XML(res["arch"]) all_models = res["models"].copy() for node in doc.xpath("/form/sheet"): # _add_tier_validation_reviews process str_element = self.env["ir.qweb"]._render( "soong_workflow.soong_workflow_log", params ) new_node = etree.fromstring(str_element) new_arch, new_models = View.postprocess_and_fields(new_node, self._name) for model in new_models: if model in all_models: continue if model not in res["models"]: all_models[model] = new_models[model] else: all_models[model] = res["models"][model] new_node = etree.fromstring(new_arch) node.append(new_node) res["arch"] = etree.tostring(doc) res["models"] = frozendict(all_models) return res def warning(self, msg): if not isinstance(msg, str): msg = str(msg) raise UserError(msg) def syslog(self, msg): if not isinstance(msg, str): msg = str(msg) _logger.info(msg)