# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.tools.safe_eval import safe_eval from odoo.exceptions import ValidationError, UserError from datetime import datetime, date, time, timedelta import random import string import logging from lxml import etree _logger = logging.getLogger(__name__) CONDITION_CODE_TEMP = """# Available locals: # - time, date, datetime, timedelta: Python libraries. # - env: Odoo Environement. # - model: Model of the record on which the action is triggered. # - obj: Record on which the action is triggered if there is one, otherwise None. # - user, Current user object. # - workflow: Workflow engine. # - syslog : syslog(message), function to log debug information to Odoo logging file or console. # - warning: warning(message), Warning Exception to use with raise. # - To return an user, assign: user = env.user""" PYTHON_CODE_TEMP = """# Available locals: # - time, date, datetime, timedelta: Python libraries. # - env: Odoo Environement. # - model: Model of the record on which the action is triggered. # - obj: Record on which the action is triggered if there is one, otherwise None. # - user, Current user object. # - workflow: Workflow engine. # - syslog : syslog(message), function to log debug information to Odoo logging file or console. # - warning: warning(message), Warning Exception to use with raise. # To return an action, assign: action = {...} """ DEFAULT_BG_COLOR = "rgb(255, 255, 255,1)" DEFAULT_LABEL_COLOR = "rgb(81, 81, 81,1)" START_BG_COLOR = "rgba(120,120,120,1)" START_LABEL_COLOR = "rgba(255,255,255,1)" END_BG_COLOR = "rgba(120,120,120,1)" END_LABEL_COLOR = "rgba(255,255,255,1)" END_BG_COLOR_1 = "rgba(0,255,30,1)" class SoongWorkflowNode(models.Model): _name = "soong.workflow.node" _description = "Soong Workflow Nodes" _order = "sequence" _sql_constraints = [ ( "uniq_node_name", "unique(workflow_id,node_name)", _("Workflow and Technical Name must be unique."), ), ] sequence = fields.Integer(string="Sequence", default=10, help="Arrange node by defining sequence.") name = fields.Char(string="Name", translate=True, help="Enter string name of the node.") node_name = fields.Char(string="Technical Name", help="Generated technical name which used by backend code.", ) workflow_id = fields.Many2one(comodel_name="soong.workflow", string="Workflow Ref.", ondelete="cascade", required=True, ) model_id = fields.Many2one("ir.model", string="Model Ref.", domain="[('state','=','base')]", related="workflow_id.model_id", required=True, ) flow_start = fields.Boolean(string="Flow Start", help="Check it if this node is the starting node.") flow_end = fields.Boolean(string="Flow End", help="Check it if this node is the ending node.") is_visible = fields.Boolean(string="Appear in Statusbar", default=True, help="Control visiability of the node/state in view.", ) out_link_ids = fields.One2many(comodel_name="soong.workflow.link", inverse_name="node_from", string="Outgoing Transitions", ) in_link_ids = fields.One2many(comodel_name="soong.workflow.link", inverse_name="node_to", string="Incoming Transitions", ) field_ids = fields.One2many(comodel_name="soong.workflow.node.field", inverse_name="node_id", string="Fields", ) button_ids = fields.One2many(comodel_name="soong.workflow.node.button", inverse_name="node_id", string="Buttons", ) diagram_position = fields.Char(readonly=True, string="Diagram Position") bg_color = fields.Char(default=DEFAULT_BG_COLOR, string="Background Color") label_color = fields.Char(default=DEFAULT_LABEL_COLOR, string="Label Color") use_custom_colors = fields.Boolean(help="Select colors from the palette manually", string="Use Custom Colors") res_bg_color = fields.Char(compute="_compute_custom_colors", readonly=True, string="Backgroung Color") res_label_color = fields.Char(compute="_compute_custom_colors", readonly=True, string="Label Color") is_archive = fields.Boolean(string="归档") illustrate = fields.Text(string="说明") is_apostille = fields.Boolean(string="加签", default=False) is_end = fields.Boolean(string="结束", default=False) is_confirm = fields.Boolean(string="确认", default=False) department_id = fields.Many2many( 'hr.department', string='部门转入范围', relation="soong_workflow_node_transfer_department_rel", column1="node_id", column2="transfer_department_id" ) transfer_comment_id = fields.Many2one('soong.note', string="转交固定意见") return_comment_id = fields.Many2one('soong.note', string="退回固定意见") confirm_comment_id = fields.Many2one('soong.note', string="确认固定意见") end_comment_id = fields.Many2one('soong.note', string="结束固定意见") approval_concatenation = fields.Selection([('chuan', '串申'), ('bing', '并申')], string="审批形式", default="bing", help='并:任何人均给意见,最后一个完成的人转交到下一节点.' '串:都会收到消息,但任意一个人转交后,即到下一节点,其他人将不再显示,相当于收回.') same_user_skip_type = fields.Selection([ ('none', '不跳过'), ('next', '跳过下一个相同人员节点'), ('prev', '跳过上一个相同人员节点'), ('after', '跳过此节点之后的所有相同人员节点'), ], string='相同人员节点处理', default='none') skip_if_node_id = fields.Many2one( 'soong.workflow.node', string='前置节点', domain="[('workflow_id', '=', workflow_id), ('id', '!=', id)]", help="选择一个位于本节点之前的节点:若它在流转时仍出现在可选节点中(即它可以选择)," "则本节点会被自动跳过;本节点被跳过时,其上配置的「相同人员节点处理」不再触发。", ) is_mandatory_node = fields.Boolean(string="必经节点", default=False) @api.onchange("is_apostille") def onchange_is_apostille(self): for record in self: if record.is_apostille: sequence = max([r.sequence for r in record.button_ids] if record.button_ids else [1]) record.button_ids = [ (0, 0, { "name": _("加签"), # "workflow_id": record.workflow_id.ids[0] if record.workflow_id.ids else None, "node_id": record.id, "sequence": sequence + 1, "action_type": "apostille", "btn_hide": False # 对所有人显示 }), (0, 0, { "name": _("加签确认"), # "workflow_id": record.workflow_id.ids[0] if record.workflow_id.ids else None, "node_id": record.id, "sequence": sequence + 2, "action_type": "apostille", "btn_hide": True, # 仅对加签人员显示 "review_type": "apostille", }) ] else: remove_ids = record.button_ids.filtered(lambda r: r.name in ["加签", "加签确认"]) record.button_ids = [(3, remove_id.id) for remove_id in remove_ids] @api.onchange("is_end") def onchange_is_end(self): for record in self: current_buttons = record.button_ids if record.is_end: record.use_custom_colors = True record.bg_color = END_BG_COLOR_1 record.res_bg_color = END_BG_COLOR_1 end_btns = current_buttons.filtered(lambda r: r.name == _("结束")) archive_btns = current_buttons.filtered(lambda r: r.name == _("归档")) remove_commands = [(3, btn.id) for btn in end_btns + archive_btns] sequence = max([r.sequence for r in current_buttons] if current_buttons else [1]) add_end = (0, 0, { "name": _("结束"), "node_id": record.id, "sequence": sequence + 1, "action_type": "code", "btn_hide": False, "has_comment": True, "review_type": "end", }) add_archive = (0, 0, { "name": _("归档"), "node_id": record.id, "sequence": sequence + 2, "action_type": "code", "btn_hide": False, "has_comment": True, }) record.button_ids = remove_commands + [add_end, add_archive] else: record.use_custom_colors = True record.bg_color = DEFAULT_BG_COLOR record.res_bg_color = DEFAULT_BG_COLOR end_btns = current_buttons.filtered(lambda r: r.name == _("结束")) archive_btns = current_buttons.filtered(lambda r: r.name == _("归档")) record.button_ids = [(3, btn.id) for btn in end_btns + archive_btns] @api.onchange("is_confirm") def onchange_is_confirm(self): for record in self: if record.is_confirm: record.use_custom_colors = True sequence = max([r.sequence for r in record.button_ids] if record.button_ids else [1]) record.button_ids = [ (0, 0, { "name": _("确认"), # "workflow_id": record.workflow_id.ids[0] if record.workflow_id.ids else None, "node_id": record.id, "sequence": sequence + 1, "action_type": "confirm", "btn_hide": False, # 对所有人显示 "has_comment": True, # 对所有人显示 "review_type": "confirm", }), ] else: record.use_custom_colors = True record.bg_color = DEFAULT_BG_COLOR record.res_bg_color = DEFAULT_BG_COLOR remove_ids = record.button_ids.filtered(lambda r: r.name in ["确认"]) record.button_ids = [(3, remove_id.id) for remove_id in remove_ids] def btn_role_sync(self): for record in self: now_btn = record.button_ids.filtered(lambda r: r.name in ["转交"]) for btn in now_btn: other_btn = record.button_ids.filtered(lambda r: r.name not in ["转交", "收回", "加签", "加签确认", '确认', '归档']) other_btn.write({ "btn_hide": btn.btn_hide, "review_type": btn.review_type, "reviewer_id": btn.reviewer_id.id, "reviewer_group_id": btn.reviewer_group_id.id, "users_role_id": btn.users_role_id.id, "users_role_group_ids": btn.users_role_group_ids.ids, }) @api.model_create_multi def create(self, values): records = super().create(values) for record in records: if record.node_name != 'end': if not record.button_ids: if record.node_name == 'start': record.write({"button_ids": [ (0, 0, { "name": _("转交"), "workflow_id": record.workflow_id.id, "sequence": 1}), (0, 0, { "name": _("收回"), "workflow_id": record.workflow_id.id, "sequence": 2, "btn_hide": True, "review_type": "withdraw", "action_type": "code", "code": "obj.action_withdraw()", }) ]}) elif '分发' in record.node_name: record.write({"button_ids": [(0, 0, { "name": _("分发"), "workflow_id": record.workflow_id.id, "sequence": 1, "action_type": "code", "has_comment": True, })]}) else: record.write({"button_ids": [ (0, 0, { "name": _("转交"), "workflow_id": record.workflow_id.id, "sequence": 1, "action_type": "code", "has_comment": True}), (0, 0, { "name": _("收回"), "workflow_id": record.workflow_id.id, "sequence": 2, "btn_hide": True, "review_type": "withdraw", "action_type": "code", "code": "obj.action_withdraw()"}), (0, 0, { "name": _("退回"), "workflow_id": record.workflow_id.id, "sequence": 2, "action_type": "code", "has_comment": True}), (0, 0, { "name": _("确认"), "workflow_id": record.workflow_id.id, "sequence": 2, "action_type": "code", "has_comment": True}) ]}) # 运行时执行加载所有form,并且只读 model_id = self.env['soong.workflow'].search([('id', '=', record.workflow_id.id), ('is_enable', '=', True)]).model_id form_view = self.env["ir.ui.view"].search([("model", "=", model_id.model), ("type", "=", "form")], limit=1) if form_view: xml_tree = etree.fromstring(form_view.arch) field_names = set() for field_node in xml_tree.xpath("//field"): field_name = field_node.get("name") if field_name: field_names.add(field_name) for field in self.env["ir.model.fields"].search( [("model_id", "=", model_id.id), ("name", "in", list(field_names))]): self.field_ids.create({ "model_id": model_id.id, "node_id": record.id, "name": field.id, "readonly": True, }) return records @api.depends("bg_color", "label_color", "use_custom_colors", "flow_start", "flow_end") def _compute_custom_colors(self): for rec in self: if rec.use_custom_colors: rec.res_bg_color = rec.bg_color rec.res_label_color = rec.label_color elif rec.flow_start: rec.res_bg_color = START_BG_COLOR rec.res_label_color = START_LABEL_COLOR elif rec.flow_end: rec.res_bg_color = END_BG_COLOR rec.res_label_color = END_LABEL_COLOR else: rec.res_bg_color = DEFAULT_BG_COLOR rec.res_label_color = DEFAULT_LABEL_COLOR @api.constrains("flow_start", "flow_end") def _check_flow_start_end(self): for rec in self: if rec.flow_start and rec.flow_end: raise UserError(_("Start and end cannot be selected for the node")) @api.onchange("name") def _compute_node_name(self): for rec in self: if rec and rec.name: name = rec.name.lower().strip().replace(" ", "_") rec.node_name = name def btn_load_fields(self): # 加载form视图中的字段 for rec in self: rec.field_ids.unlink() form_view = self.env["ir.ui.view"].search([("model", "=", rec.model_id.model), ("type", "=", "form")], limit=1) if form_view: xml_tree = etree.fromstring(form_view.arch) field_names = set() for field_node in xml_tree.xpath("//field"): field_name = field_node.get("name") if field_name and field_name != "name_title": field_names.add(field_name) for field in self.env["ir.model.fields"].search( [("model_id", "=", rec.model_id.id), ("name", "in", list(field_names))]): rec.field_ids.create({ "model_id": rec.model_id.id, "node_id": rec.id, "name": field.id, }) else: raise UserError('该模型未找到form视图') def all_readonly_fields(self): self.field_ids.write({"readonly": True}) def all_required_fields(self): self.field_ids.write({"required": True}) def all_invisible_fields(self): self.field_ids.write({"invisible": True}) def all_false_fields(self): self.field_ids.write({"invisible": False, "required": False, "readonly": False}) class SoongWorkflowNodeButton(models.Model): _name = "soong.workflow.node.button" _description = "Soong Workflow Node Buttons" _order = "sequence" def _generate_key(self): return "".join( random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(10) ) name = fields.Char( string="Button String", translate=True, help="Enter button string name that will appear in the view.", ) sequence = fields.Integer( string="Sequence", default=10, help="Arrange buttons by defining sequence." ) is_highlight = fields.Boolean(string="Is Highlighted", default=True, help="Control highlighting of the button if needs user attention..", ) # is_high_red = fields.Boolean(string="红色突出") has_icon = fields.Boolean( string="Has Icon", help="Enable it to add icon to the button." ) icon = fields.Char(string="Icon", help="Enter icon name like: fa-print, it's recommended to use FontAwesome Icons.", ) has_comment = fields.Boolean(string="意见/弹窗", default=True) btn_key = fields.Char(string="Button Key", default=_generate_key) btn_hide = fields.Boolean( string="Hide Button if Condition isn't fulfilled", help="If condition is false the button will be hidden.", ) review_type = fields.Selection( string="审批类型", default="user_role", selection=[ ("individual", "Specific user"), ("group", "Any user in a specific group"), ("expression", "Python Expression"), ('user_role', '角色'), # ('invisible', '隐藏条件'), ('apostille', '加签确认'), ('end', "结束"), ('confirm', "确认"), ('withdraw', '收回'), ], ) is_review_invisible = fields.Boolean(string="使用范围", default=False) is_node_automatic = fields.Boolean('是否自动选择所有人员', help='默认否,选择是之后,前节点转交是会自动带出本节点的所有匹配的人员。', default=False) illustrate = fields.Text(string="说明") reviewer_id = fields.Many2one(comodel_name="res.users", string="Reviewer") reviewer_group_id = fields.Many2one(comodel_name="res.groups", string="Reviewer group") users_role_id = fields.Many2one(comodel_name="soong.user.role", string="主角色",) users_role_group_ids = fields.Many2many(comodel_name="soong.user.role.group", string="角色组") reviewer_expression = fields.Text( string="Review Expression", default=CONDITION_CODE_TEMP, help="Write Python code that defines the reviewer. " "The result of executing the expression must be a res.users " "recordset.", ) invisible_expression = fields.Char( string="隐藏条件", default="", ) # todo 找翻译文件改变状态 action_type = fields.Selection( [ ("link", "Trigger Link"), ("link_start_statu", "Trigger Link Start Statu"), ("link_action", "Trigger Link Action"), ("link_action_approve", "Trigger Link Action Approve"), ("link_action_refuse", "Trigger Link Action Refuse"), ("code", "Python Code"), ("action", "Server Action"), ("win_act", "Window Action"), ("apostille", "加签按钮"), ('end', "结束"), ('confirm', "确认"), ], string="Action Type", default="link", help="Choose type of action to be trigger by the button.", ) link_id = fields.Many2one(comodel_name="soong.workflow.link", string="Link") code = fields.Text(string="Python Code", default=PYTHON_CODE_TEMP) server_action_id = fields.Many2one( comodel_name="ir.actions.server", string="Server Action" ) win_act_id = fields.Many2one( comodel_name="ir.actions.act_window", string="Window Action" ) node_id = fields.Many2one( comodel_name="soong.workflow.node", string="Workflow Node Ref.", ondelete="cascade", required=True, ) workflow_id = fields.Many2one( "soong.workflow", string="Workflow Ref.", # required=True, store=True, related="node_id.workflow_id", ondelete="cascade", ) invisible_expression_model = fields.Char(related="workflow_id.model_id.model", store=True) is_automatic = fields.Boolean('发起人部门') is_automatic_company = fields.Boolean('发起人公司', default=True) is_automatic_group = fields.Boolean('发起人集团') is_logged_by = fields.Boolean('上一节点部门') is_logged_company = fields.Boolean('上一节点公司') is_logged_group = fields.Boolean('上一节点集团') is_dep = fields.Boolean('按指定部门') role_id = fields.Many2one(comodel_name="soong.user.role.group", string="角色") specify_field_id = fields.Many2one(comodel_name="ir.model.fields", string="指定字段") is_carbon_copy = fields.Boolean(string="抄送/传阅") department1_ids = fields.Many2many( "hr.department", string="抄送部门", relation="soong_workflow_node_department_rel", column1="node_id", column2="department_id" ) user_role_ids = fields.Many2many("soong.user.role", string="抄送角色", ) cc_users_role_group_ids = fields.Many2many( 'soong.user.role.group', 'soong_workflow_node_button_cc_groups_rel', 'button_id', 'group_id', string='抄送角色组' ) employee_ids = fields.Many2many('hr.employee', string='抄送/传阅人员') @api.onchange('department1_ids', 'user_role_ids', 'cc_users_role_group_ids', 'employee1_ids') def _onchange_sync_employee_ids(self): for record in self: final_employees = self.env['hr.employee'] role_group_roles = record.cc_users_role_group_ids.mapped('user_role_ids') all_target_roles = record.user_role_ids | role_group_roles if all_target_roles: role_employees = self.env['hr.employee'].search([ '|', ('user_role_id', 'in', all_target_roles.ids), ('user_role_ids', 'in', all_target_roles.ids) ]) final_employees |= role_employees if record.department1_ids: dept_employees = self.env['hr.employee'].search([ ('department_id', 'in', record.department1_ids.ids) ]) final_employees |= dept_employees record.employee_ids = final_employees if final_employees else [(5, 0, 0)] def update_user_id(self): button_ids = self.env["soong.workflow.node.button"].browse( self._context.get('active_ids', self._context.get('active_id'))) for button_id in button_ids: button_id.write({'reviewer_id': False}) @api.onchange('is_automatic') def _onchange_is_automatic(self): if self.is_automatic: self.is_automatic_company = False self.is_automatic_group = False self.is_logged_by = False self.is_logged_company = False self.specify_field_id = None self.is_dep = False self.is_logged_group = False @api.onchange('is_automatic_company') def _onchange_is_automatic_company(self): if self.is_automatic_company: self.is_automatic = False self.is_automatic_group = False self.is_logged_by = False self.is_dep = False self.specify_field_id = None self.is_logged_company = False self.is_logged_group = False @api.onchange('is_automatic_group') def _onchange_is_automatic_group(self): if self.is_automatic_group: self.is_automatic = False self.is_automatic_company = False self.is_logged_by = False self.is_dep = False self.specify_field_id = None self.is_logged_company = False self.is_logged_group = False @api.onchange('is_logged_by') def _onchange_is_logged_by(self): if self.is_logged_by: self.is_automatic = False self.is_automatic_company = False self.is_automatic_group = False self.specify_field_id = None self.is_dep = False self.is_logged_company = False self.is_logged_group = False @api.onchange('is_logged_company') def _onchange_is_logged_company(self): if self.is_logged_company: self.is_automatic = False self.is_automatic_company = False self.is_automatic_group = False self.is_logged_by = False self.is_dep = False self.specify_field_id = None self.is_logged_group = False @api.onchange('is_logged_group') def _onchange_is_logged_group(self): if self.is_logged_group: self.is_automatic = False self.is_automatic_company = False self.is_automatic_group = False self.is_logged_by = False self.is_dep = False self.specify_field_id = None self.is_logged_company = False @api.onchange('specify_field_id') def _onchange_specify_field_id(self): if self.specify_field_id: self.is_automatic = False self.is_automatic_company = False self.is_automatic_group = False self.is_logged_by = False self.is_dep = False self.is_logged_company = False self.is_logged_group = False @api.onchange('is_dep') def _onchange_is_dep(self): if self.is_dep: self.is_automatic = False self.is_automatic_company = False self.is_automatic_group = False self.is_logged_by = False self.specify_field_id = None self.is_logged_company = False self.is_logged_group = False @api.onchange('users_role_id', 'users_role_group_ids') def _onchange_role_selection(self): if self.users_role_id and self.users_role_group_ids: self.users_role_id = False self.users_role_group_ids = [(5, 0, 0)] return { 'warning': { 'title': _('输入错误'), 'message': _('只能选择主角色或者角色组中的一个,不能同时选择。') } } @api.onchange("node_id") def change_workflow(self): for rec in self: if isinstance(rec.id, int) and rec.node_id and rec.node_id.workflow_id: rec.workflow_id = rec.node_id.workflow_id.id elif self.env.context.get("default_node_id", 0): model_id = ( self.env["soong.workflow.node"] .sudo() .browse(self.env.context.get("default_node_id")) .model_id.id ) rec.workflow_id = ( self.env["soong.workflow"] .sudo() .search([("model_id", "=", model_id), ('is_enable', '=', True)]) ) @api.constrains("btn_key") def validation(self): for rec in self: # Check if there is no duplicate button key res = self.search_count( [ ("id", "!=", rec.id), ("btn_key", "=", rec.btn_key), ] ) if res: rec.btn_key = self._generate_key() def log(self): cx = self.env.context.copy() or {} query_user_id = self.env.user.id is_admin = self.env.user.has_group('soong_workflow.group_soong_workflow_manager') res_model = cx.get("active_model", False) res_id = cx.get("active_id", 0) record_id = self.env[res_model].search([('id', '=', res_id)]) log = self.env['soong.workflow.log'].search([("model", "=", res_model), ("res_id", "=", res_id)]) employee_user_role_ids = self._get_approvers_for_link(record_id) if is_admin and record_id: if hasattr(record_id, 'user_id') and record_id.user_id: query_user_id = record_id.user_id.id elif hasattr(record_id, 'users_id') and record_id.users_id: query_user_id = record_id.users_id.id if len(log) > 1 and is_admin: current_node_log = self.sudo().env["soong.workflow.log"].search( [("model", "=", res_model), ("res_id", "=", res_id), ("node_id", "=", self.node_id.id)]) else: current_node_log = self.sudo().env["soong.workflow.log"].search( [("model", "=", res_model), ("res_id", "=", res_id), ("done_by", "=", query_user_id), ("node_id", "=", self.node_id.id)]) # 创建或更新当前用户的审批记录 if not current_node_log: self.sudo().env["soong.workflow.log"].create({ "model": res_model, "res_id": res_id, "node_id": self.node_id.id, "workflow_id": self.workflow_id.id, "done_by": query_user_id, "node_btn_name": "0", "node_btn_id": self.id, "lase_click_date": fields.Datetime.now(), "approval_state": "2", # 已完成 "comment": '同意', }) else: current_node_log.write({ "comment": '同意', "approval_state": "2", # 已完成 "lase_click_date": fields.Datetime.now() }) expected_employees = employee_user_role_ids # 查找下一个节点 next_node_id = record_id.workflow_id.node_ids.search([ ('name', '=', self.link_id.node_to.name), ('workflow_id', '=', self.workflow_id.id) ]) completed_logs = self.sudo().env["soong.workflow.log"].search([ ("model", "=", res_model), ("res_id", "=", res_id), ("node_id", "=", self.node_id.id), ("node_btn_name", "=", "4"), ("approval_state", "!=", "2") ]) all_completed = len(completed_logs) == 0 if all_completed: for employee in expected_employees: # 检查该员工在下一节点是否已有记录 next_node_employee_log = self.sudo().env["soong.workflow.log"].search([ ("model", "=", res_model), ("res_id", "=",res_id), ("node_id", "=", next_node_id.id), ('node_btn_name', '!=', '1'), ("done_by", "=", employee.user_id.id) ]) if not next_node_employee_log: # 创建初步记录,状态为"待审批" self.sudo().env["soong.workflow.log"].create({ "model": res_model, "res_id":res_id, "node_id": next_node_id.id, "workflow_id": self.workflow_id.id, "done_by": employee.user_id.id, "node_btn_name": "4", "approval_state": "0", # 待审批 }) workflow_record_id = self.env['soong.workflow.record'].search([('code', '=', record_id.code)]) if employee_user_role_ids: workflow_record_id.users_ids = [(4, employee.user_id.id) for employee in employee_user_role_ids if employee.user_id] workflow_record_id.employee_ids = employee_user_role_ids workflow_record_id.approve_state = "pending" # 待审批 related_record = self.env[res_model].browse(res_id) if hasattr(related_record, 'self_users_ids'): user_ids = [employee.user_id.id for employee in employee_user_role_ids if employee.user_id] related_record.self_users_ids = [(6, 0, user_ids)] if user_ids else [(5,)] record_id.write({'state': self.link_id.node_to.name}) for employee in employee_user_role_ids: action_id = self.env['ir.actions.act_window'].search([('res_model', '=', res_model)])[0] # url = f'https://oa.thtzjt.com/web?login={employee.user_id.login}&oauth_uid={employee.wecom_userid}#id={record_id.id}&cids=2&action={action_id.id}&model={record_id._name}&view_type=form&menu_id=10104' # 生成目标URL(不使用login参数,避免session冲突) target_url = f'https://oa.thtzjt.com/web#id={record_id.id}&cids=2&action={action_id.id}&model={record_id._name}&view_type=form&menu_id=10104' # 使用OAuth登录链接,避免session冲突 url = self.sudo().env["wecom.apps"].generate_oauth_login_url(target_url) self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=employee.wecom_userid, title=record_id._description, description="标题: {}\n".format( record_id.name) + self.env.user.name + ' ' + str( fields.Datetime.now() + timedelta( hours=8)), url=url, btntxt="查看详情") def _get_approvers_for_link(self, record_id=None): node = self.workflow_id.node_ids.search([ ('name', '=', self.link_id.node_to.name), ('workflow_id', '=', self.workflow_id.id) ]) button = False if node.button_ids: # 1. 优先找“分发”按钮 distribute_btn = next((btn for btn in node.button_ids if btn.name == '分发'), None) if distribute_btn: button = distribute_btn else: # 2. 找不到“分发”再找“转交” transfer_btn = next((btn for btn in node.button_ids if btn.name == '转交'), None) if transfer_btn: button = transfer_btn else: # 3. 最后找“结束” button = next((btn for btn in node.button_ids if btn.name == '结束'), None) if not button: return self.env['hr.employee'] link_users_id = button.users_role_id link_users_group_ids = button.users_role_group_ids is_automatic = button.is_automatic is_automatic_company = button.is_automatic_company is_automatic_group = button.is_automatic_group is_logged_by = button.is_logged_by is_logged_company = button.is_logged_company is_logged_group = button.is_logged_group is_dep = button.is_dep is_field = button.specify_field_id if button.reviewer_id: employee = self.env['hr.employee'].search([ ('user_id', '=', button.reviewer_id.id), ], limit=1) return employee # 部门和公司筛选条件 department_domain = [] company_domain = [] user_role_domain = [] if record_id: # 获取当前用户和申请人的相关信息 current_user = self.env.user applicant = None # 获取申请人的用户对象 for field_name in ['user_id', 'users_id']: if hasattr(record_id, field_name): applicant = getattr(record_id, field_name) if applicant: break # 根据勾选的条件设置域条件(只勾选一个) if is_automatic and record_id.department_id: # 按当前申请人自动匹配-本部门(保留部门和公司条件) department_domain = ['|', ('department_id', '=', record_id.department_id.id), ('department_ids', 'in', record_id.department_id.id)] company_domain = [('company_id', '=', applicant.company_id.id)] elif is_automatic_company and applicant and hasattr(applicant, 'company_id'): # 按当前申请人自动匹配-本公司(仅保留公司条件) company_id = applicant.company_id.id company_domain = [('company_id', '=', company_id)] elif is_automatic_group and applicant and hasattr(applicant, 'company_id'): # 按当前申请人自动匹配-本集团(仅保留公司条件) parent_company = applicant.company_id.parent_id if parent_company: group_company_ids = self.env['res.company'].search([('id', 'child_of', parent_company.id)]).ids company_domain = [('company_id', 'in', group_company_ids)] elif is_dep and hasattr(record_id, 'department_id1') and record_id.department_id1: department_id1 = record_id.department_id1.id department_domain = ['|', ('department_id', '=', department_id1), ('department_ids', 'in', department_id1)] user_role_domain = ['|', ('user_role_id', '=', button.role_id.user_role_ids.ids), ('user_role_ids', 'in', button.role_id.user_role_ids.ids)] elif is_logged_by and current_user.department_id: # 按当前登录人自动匹配-本部门(保留部门和公司条件) department_domain = ['|', ('department_id', '=', current_user.department_id.id), ('department_ids', 'in', current_user.department_id.id)] company_domain = [('company_id', '=', current_user.company_id.id)] elif is_logged_company: # 按当前登录人自动匹配-本公司(仅保留公司条件) company_id = current_user.company_id.id company_domain = [('company_id', '=', company_id)] elif is_logged_group: # 按当前登录人自动匹配-本集团(仅保留公司条件) parent_company = current_user.company_id.parent_id if parent_company: group_company_ids = self.env['res.company'].search([('id', 'child_of', parent_company.id)]).ids company_domain = [('company_id', 'in', group_company_ids)] elif is_field: field_name = button.specify_field_id.name # 目前只会有入职 field_value = record_id[field_name] department_domain = ['|', ('department_id', '=', field_value.id), ('department_ids', 'in', field_value.id)] if link_users_id: role_domain = ['|', ('user_role_id', '=', link_users_id.id), ('user_role_ids', 'in', link_users_id.id)] if user_role_domain: role_domain = ['|'] + role_domain + user_role_domain domain = role_domain + company_domain + department_domain return self.env['hr.employee'].sudo().search(domain) elif link_users_group_ids: role_domain = ['|', ('user_role_id', 'in', link_users_group_ids.user_role_ids.ids), ('user_role_ids', 'in', link_users_group_ids.user_role_ids.ids)] if user_role_domain: role_domain = ['|'] + role_domain + user_role_domain domain = role_domain + company_domain + department_domain return self.env['hr.employee'].sudo().search(domain) # 特殊分发按钮 if '分发' in link.name and record_id and hasattr(record_id, 'users_id'): return record_id.users_id.employee_id return self.env['hr.employee'] def run(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse(cx.get("active_id", 0)), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } if rec.btn_hide: if rec.review_type == "individual": if rec.reviewer_id == self.env.user: result = True elif rec.review_type == "group": if self.env.user in self.reviewer_group_id.users: result = True else: try: safe_eval( rec.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: result = True except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % ( ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3], ) ) ) else: result = True if result: # Run Proper Action if rec.action_type == 'confirm': rec.action_type = 'code' func = getattr(self, "_run_%s" % rec.action_type) func_result = func() try: user_ids = [] button_ids = rec.link_id.node_to.button_ids for button_id in button_ids: # 根据每个按钮状态判断当前应该的查看人员 if button_id.review_type == "individual": user_ids.append(button_id.reviewer_id.id) elif button_id.review_type == "group": user_ids += button_id.reviewer_group_id.users.ids elif button_id.review_type == "user_role": user_role_id = self.env['res.users'].search( [('employee_id.user_role_id', '=', button_id.users_role_id.id)]) user_ids += user_role_id.ids # # 发送信息提醒 # partner_ids = self.sudo().env['res.users'].search([('id', 'in', user_ids)]).mapped('partner_id').ids # self.sudo().with_context(lang=self.env.lang)._message_auto_subscribe_notify(partner_ids, 'soong_workflow.message_yuthon_workflow_btn_approval_data') except Exception as e: pass return func_result def _run_win_act(self): # Variables cx = self.env.context.copy() or {} win_act_obj = self.env["ir.actions.act_window"] # Run Window Action for rec in self: action = win_act_obj.with_context(cx).browse(rec.win_act_id.id).read()[0] action["context"] = cx return action return False def _run_action(self): # Variables srv_act_obj = self.env["ir.actions.server"] # Run Server Action for rec in self: srv_act_rec = srv_act_obj.browse(rec.server_action_id.id) return srv_act_rec.run() def _run_code(self): # Variables cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } # Run Code for rec in self: try: safe_eval(rec.code, locals_dict=locals_dict, mode="exec", nocopy=True) action = "action" in locals_dict and locals_dict["action"] or False if action: return action except Warning as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) return True def _run_link(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } try: safe_eval( rec.link_id.condition_code, locals_dict=locals_dict, mode="exec", nocopy=True, ) result = "result" in locals_dict and locals_dict["result"] or False except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) if result: # Trigger link function return rec.link_id.trigger_link() def _run_link_start_statu(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } try: safe_eval( rec.link_id.condition_code, locals_dict=locals_dict, mode="exec", nocopy=True, ) result = "result" in locals_dict and locals_dict["result"] or False except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) if result: # todo 更改业务数据的状态,成品不建议这样玩 try: active_id = cx.get("active_id", 0) active_model = cx.get("active_model", False) obj = self.env[active_model].browse(active_id) if obj.exists(): obj.write({'statu': 'approval'}) except Exception as e: pass # Trigger link function return rec.link_id.trigger_link() def _run_link_action(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } try: safe_eval( rec.link_id.condition_code, locals_dict=locals_dict, mode="exec", nocopy=True, ) result = "result" in locals_dict and locals_dict["result"] or False except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) if result: win_act_obj = self.env["ir.actions.act_window"] # Run Window Action action = win_act_obj.with_context(cx).browse( self.env.ref('soong_workflow.soong_approval_notice_wizard_action').id).read()[0] action["context"] = cx action["context"].update({"default_approval_notice_state": '0'}) # Trigger link function return action def _run_link_action_approve(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } try: safe_eval( rec.link_id.condition_code, locals_dict=locals_dict, mode="exec", nocopy=True, ) result = "result" in locals_dict and locals_dict["result"] or False except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) if result: win_act_obj = self.env["ir.actions.act_window"] # Run Window Action action = win_act_obj.with_context(cx).browse( self.env.ref('soong_workflow.soong_approval_approve_notice_wizard_action').id).read()[0] action["context"] = cx action["context"].update({"default_approval_notice_state": '1'}) # Trigger link function return action def _run_link_action_refuse(self): for rec in self: # Check Condition Before Executing Action result = False cx = self.env.context.copy() or {} locals_dict = { "env": self.env, "model": self.env[cx.get("active_model", False)], "obj": self.env[cx.get("active_model", False)].browse( cx.get("active_id", 0) ), "user": self.env.user, "datetime": datetime, "time": time, "date": date, "timedelta": timedelta, "workflow": self.env["soong.workflow"], "warning": self.warning, "syslog": self.syslog, } try: safe_eval( rec.link_id.condition_code, locals_dict=locals_dict, mode="exec", nocopy=True, ) result = "result" in locals_dict and locals_dict["result"] or False except ValidationError as ex: raise ex except SyntaxError as ex: raise UserError( _( "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) ) ) if result: win_act_obj = self.env["ir.actions.act_window"] # Run Window Action action = win_act_obj.with_context(cx).browse( self.env.ref('soong_workflow.soong_approval_refuse_notice_wizard_action').id).read()[0] action["context"] = cx action["context"].update({"default_approval_notice_state": '2'}) # Trigger link function return action 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) class SoongWorkflowNodeField(models.Model): _name = "soong.workflow.node.field" _description = "Soong Workflow Node Fields" name = fields.Many2one(comodel_name="ir.model.fields", string="Field") model_id = fields.Many2one( comodel_name="ir.model", string="Model", domain="[('state','=','base')]" ) readonly = fields.Boolean(string="Readonly") required = fields.Boolean(string="Required") invisible = fields.Boolean(string="Invisible") group_ids = fields.Many2many(comodel_name="res.groups", string="Groups") user_ids = fields.Many2many(comodel_name="res.users", string="Users") node_id = fields.Many2one( comodel_name="soong.workflow.node", string="Node Ref.", ondelete="cascade", required=True, )