Files
gzth/soong_workflow/models/soong_workflow_mixin.py
T
李鹏宇 1972349dcf !7969 添加功能
Merge pull request !7969 from 李鹏宇/dev_zhou
2026-01-14 07:43:52 +00:00

1330 lines
62 KiB
Python

# -*- 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
@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="加签人员")
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,)
def _compute_is_back(self):
for record in self:
record.is_back = False
current_state = record.state
if not current_state or not record.log_ids:
continue
# 判断当前节点的状态
matched_logs = record.log_ids.filtered(lambda log: log.name == current_state)
if matched_logs:
if matched_logs[-1].approval_state != '0':
record.is_back = True
if record._name == 'yuthon.official.incoming.normal':
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):
current_user_id = self.env.user.id
if hasattr(self, 'user_id') and self.user_id:
query_user_id = self.user_id.id
elif hasattr(self, 'users_id') and self.users_id:
query_user_id = self.users_id.id
else:
query_user_id = self.env.user.id
if self.log_ids:
if query_user_id == current_user_id and self.work_end and self.log_ids[-1].approval_state in ['0', '1']:
self.is_active = True
else:
self.is_active = False
else:
self.is_active = False
def _compute_is_confirm(self):
for record in self:
record.is_confirm = True
record.is_end = False
domain = [('done_by', '=', self.env.user.id), ('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id), ('approval_state', 'in', ['0', '1']), ('name', '!=', '归档')]
lg2 = record.log_ids2.search(domain)
lg1 = record.log_ids.search(domain)
if lg2 or lg1:
record.is_confirm = False
if not lg1 and not lg2 and record.is_active == True:
record.is_end = True
if record.log_ids:
if record.log_ids[-1].name == '归档' and record.log_ids[-1].approval_state == '2':
record.is_end = True
@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=" ",)
# @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")
# 状态列表
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)]
self.env['trans.link'].search([]).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([('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 == '转交':
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))
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))
# 尝试父部门匹配
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.name_title in ['行政发文']:
is_supervising = self.is_supervising
if btn_rec.node_id.comment_id:
comment_note_id = btn_rec.node_id.comment_id.id
else:
comment_note_id = False
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_comment_note_id': comment_note_id,
"default_code": self.code,
"default_res_model": self._name,
"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 ['申请人', '拟办', '草稿', '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:
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,
"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 '<form 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)
if self.env.user.has_group("soong_workflow.group_soong_workflow_manager"):
res = self._add_soong_workflow_log_manager(view_type, res, params)
else:
res = self._add_soong_workflow_log(view_type, res, params)
res = self._load_sign_view(view_type, res)
# res = self._load_workflow_approver_view(view_type, res)
return res
# 收回
def action_withdraw(self):
pass
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:
if self.env.user.has_group("soong_workflow.group_soong_workflow_manager"):
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)
invisible_condition = "state != '%s'" % node.node_name
# if model == 'purchase.order':
# invisible_condition = invisible_condition.replace("询价", "draft")
btn_exec_action_el.set("invisible", invisible_condition)
btn_exec_action_el.set("context", "{'btn_key':'%s','active_model':'%s'}" % (button.btn_key, self._name), )
else:
is_apostille = False
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 == "apostille":
is_apostille = True
elif button.review_type == "expression":
if not _check_condition_code(button):
continue
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")
id_el = etree.Element("field")
id_el.set("name", "id")
id_el.set("invisible", "1")
if button.is_highlight:
btn_exec_action_el.set("class", "oe_highlight")
if button.has_icon:
btn_exec_action_el.set("icon", button.icon)
# 接下来是隐藏的配置
invisible = "not id or state != '{0}' ".format(str(node.node_name))
if button.review_type == 'user_role':
# 构建角色条件
role_domain = []
if button.users_role_id:
role_domain = [
'|',
('employee_id.user_role_id', '=', button.users_role_id.id),
('employee_id.user_role_ids', 'in', button.users_role_id.id)
]
if button.users_role_group_ids:
role_ids = button.users_role_group_ids.user_role_ids.ids
role_domain = [
'|',
('employee_id.user_role_id', 'in', role_ids),
('employee_id.user_role_ids', 'in', role_ids)
]
# 如果有角色条件,添加到可见性条件中
if role_domain:
allowed_user_ids = self.env['res.users'].search(role_domain).ids
invisible += f" or uid not in {allowed_user_ids}"
# 如果有隐藏条件的话,启用隐藏
if button.is_review_invisible and button.invisible_expression:
domain = eval(button.invisible_expression)
btn_invisible = self.sudo().env[button.workflow_id.model_id.model].search(domain).ids
invisible += " or id not in {0} ".format(str(btn_invisible))
if button.name == '确认':
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += " or not log_ids or is_confirm"
if button.name == '结束':
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += "or is_end"
if button.name == "退回":
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += " or not log_ids "
if button.name == "分发":
invisible += " or is_send"
if button.name == "收回":
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += " or not log_ids or not is_back"
if button.name == '归档':
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += "or not is_active"
if model == 'purchase.order':
invisible = invisible.replace("询价", "draft")
btn_exec_action_el.set("invisible", invisible)
btn_exec_action_el.set("context", "{'btn_key':'%s','active_model':'%s'}" % (button.btn_key, self._name),)
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
end_el = etree.Element("field")
end_el.set("name", "is_end")
end_el.set("invisible", "1")
header_el.append(end_el)
confirm_el = etree.Element("field")
confirm_el.set("name", "is_confirm")
confirm_el.set("invisible", "1")
header_el.append(confirm_el)
active_el = etree.Element("field")
active_el.set("name", "is_active")
active_el.set("invisible", "1")
header_el.append(active_el)
if header_el is not False:
state_el = etree.Element("field")
state_el.set("name", "state")
state_el.set("widget", "statusbar")
state_el.set("invisible", "1")
# 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 _load_workflow_approver_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", "workflow_approver_ids")
# 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.name.name in ["code", "name_title"]:
field_inst.set("readonly", "1")
readonly_arr.append(node.node_name)
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)
if readonly_arr:
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
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)
if self.env.user.has_group("soong_workflow.group_soong_workflow_manager"):
field_inst.set("readonly", "0")
current_field_name = field_inst.attrib.get("name")
if current_field_name in ["code", "name_title"]:
field_inst.set("readonly", "1")
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_manager(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_manager", 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 _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)
class SoongWorkflowLine(models.Model):
_name = "soong.workflow.line"
model = fields.Char(string="Related Document Model", index=True)
res_id = fields.Integer(string="Related Document ID", index=True)
workflow_record_id = fields.Many2one("soong.workflow.record", string="名称")
code = fields.Char(related="workflow_record_id.code", string="流水号")
approvals_group_id = fields.Many2one(related="workflow_record_id.approvals_group_id", string="审批组")
create_uid = fields.Many2one(related="workflow_record_id.create_uid", string="创建人")
write_date2 = fields.Datetime(related="workflow_record_id.write_date2", string="最后更新日期")
def unlink(self):
if not self.env.context.get('confirm_delete'):
# 提示用户确认删除
message = _("确定要删除这些记录吗?此操作不可撤销。")
return {
'name': _('确认删除'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'workflow.line.wizard',
'target': 'new',
'context': {
'active_model': self._name,
'active_ids': self.ids,
'delete_message': message,
}
}
# 继续执行删除操作
return super(SoongWorkflowLine, self).unlink()
# return {
# "type": "ir.actions.act_window",
# "name": "二次确认",
# "res_model": "workflow.line.wizard",
# "view_mode": "form",
# "target": "new",
# "context": {
# "default_workflow_id": self.workflow_record_id.id,
# "default_workflow_line_id": self.id
# },
# }
def open_record(self):
return {
"type": "ir.actions.act_window",
"res_model": self.workflow_record_id.res_model,
"res_id": self.workflow_record_id.res_id,
"name": self.workflow_record_id.name,
"view_mode": "form",
}
class ProjectProjectLine(models.Model):
_name = "project.project.line"
model = fields.Char(string="Related Document Model", index=True)
res_id = fields.Integer(string="Related Document ID", index=True)
project_id = fields.Many2one("project.project", string="项目")
user_id = fields.Many2one(related="project_id.user_id", string="项目管理员")
date_start = fields.Date(related="project_id.date_start", string="日期")