Files
gzth/soong_workflow/models/soong_workflow_mixin.py
T
2026-08-19 17:12:29 +08:00

2031 lines
101 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)
def get_edit_button_state(self):
"""供前端判断编辑按钮是否显示,避免触发 read() 重写里的日志副作用。"""
self.ensure_one()
# 没有 self_users_ids / work_end 字段的模型不做限制
if not hasattr(self, 'self_users_ids') or not hasattr(self, 'work_end'):
return {'show_edit': True}
is_current_user = self.env.user.id in self.self_users_ids.ids
# is_modify=True 且当前用户是流程创建人时也允许编辑
is_modify_creator = False
if hasattr(self, 'is_modify') and self.is_modify:
workflow_record = self.env["soong.workflow.record"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id), ('reclaim_state', '=', False)], limit=1)
if workflow_record and workflow_record.create_uid.id == self.env.user.id:
is_modify_creator = True
show_edit = (is_current_user and not self.work_end) or is_modify_creator
return {
'show_edit': show_edit,
'self_users_ids': self.self_users_ids.ids,
'work_end': self.work_end,
'is_modify': hasattr(self, 'is_modify') and self.is_modify,
'is_modify_creator': is_modify_creator,
}
# @api.model
# def write(self, vals):
# res = super(SoongWorkflowMixin, self).write(vals)
# workflow_records = self.env['soong.workflow.record'].search([('name', '=', self.name)])
# for record in workflow_records:
# for field, value in vals.items():
# if hasattr(record, field):
# setattr(record, field, value)
# return res
def action_is_modify(self):
self.is_modify = False
@api.model
def _compute_default_state(self):
# Objects
wkf_obj = self.env["soong.workflow"]
rec_model = self._name
# Get Flow Start
wkf_recs = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)])
for wkf_rec in wkf_recs:
for node in wkf_rec.node_ids:
if node.flow_start:
return node.node_name
@api.model
def _load_state(self):
# Objects
wkf_obj = self.env["soong.workflow"]
# Variables
rec_model = self._name
ret = []
# Get Status
wkf_recs = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)])
for rec in wkf_recs:
for node in rec.node_ids:
ret.append((node.node_name, node.name))
return ret
state = fields.Selection(
selection="_load_state",
string="Status",
default=_compute_default_state,
track_visibility="onchange",
store=True
)
is_modify = fields.Boolean(string="可修改")
workflow_id = fields.Many2one(
comodel_name="soong.workflow",
string="Workflow Ref.",
compute="_compute_workflow_id",
)
approval_users_ids = fields.One2many("soong.approval.users", "res_id",
domain=lambda self: [('res_model', '=', self._name)], auto_join=True,
string="审批明细")
approval_sign_ids = fields.Many2many('res.users', store=False, compute="_compute_approval_sign_ids", string="加签人员")
is_import = fields.Boolean(string='是否导入')
is_back = fields.Boolean(string='是否允许收回', compute='_compute_is_back', store=False,)
is_active = fields.Boolean(string='是否允许归档', compute='_compute_is_active', store=False,)
is_confirm = fields.Boolean(string="未确认", compute='_compute_is_confirm', store=False,)
is_end = fields.Boolean(string="未结束", compute='_compute_is_confirm', store=False,)
is_done = fields.Boolean(string="已办", compute='_compute_is_done', store=False,)
is_send = fields.Boolean(string="是否分发", compute='_compute_is_send', store=False,)
is_show_add_sign = fields.Boolean(string="显示加签按钮", compute='_compute_is_show_add_sign')
is_show_transfer = fields.Boolean(string="显示加签确认按钮", compute='_compute_is_show_transfer')
approval_result = fields.Selection([('pass', '通过'), ('fail', '不通过')], string="审批结果")
def _compute_is_show_add_sign(self):
for record in self:
record.is_show_add_sign = True
domain_hide_01 = [
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('name', '=', '加签'),
('approval_state', 'in', ['0', '1']),
]
has_01 = record.log_ids.search(domain_hide_01)
domain_hide_done = [
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('name', '=', '加签'),
('approval_state', '=', '2'),
('done_by', '=', self.env.user.id),
]
has_done = record.log_ids.search(domain_hide_done)
has_user_approved = False
user_logs = record.log_ids.filtered(
lambda l: l.done_by == self.env.user and l.node_btn_name != '1'
)
if user_logs:
has_user_approved = all(log.approval_state == '2' for log in user_logs)
# 如果有日志,取【第一个节点】的经办人 == 当前登录人 → 隐藏 (归档待办的除外)
has_first_user = False
if record.log_ids:
archive_self_log = record.log_ids.filtered(
lambda l: l.name == '归档' and l.done_by == self.env.user and l.approval_state in ['0', '1'])
if not archive_self_log:
first_log = record.log_ids.sorted('sequence')[:1]
if not self.state:
has_first_user = True
else:
if first_log and first_log.done_by == self.env.user and self.state[0] == first_log.name:
has_first_user = True
# :判断当前登录人是否在 log_ids2 中
has_log_ids2_user = False
if record.log_ids2:
has_log_ids2_user = self.env.user.id in record.log_ids2.mapped('done_by.id')
# 任意一个条件满足 → 隐藏
if has_01 or has_done or has_first_user or has_log_ids2_user or has_user_approved:
record.is_show_add_sign = False
def _compute_is_show_transfer(self):
for record in self:
record.is_show_transfer = False
domain_has_sign = [
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('name', '=', '加签'),
('approval_state', 'in', ['0', '1']),
('done_by', '=', self.env.user.id),
]
has_sign_ok = record.log_ids.search(domain_has_sign)
if has_sign_ok:
record.is_show_transfer = True
def _compute_is_send(self):
for record in self:
record.is_send = False
log_ids_domain = [
('done_by', '=', self.env.user.id),
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('approval_state', 'in', ['0', '1']),
('name', '=', '分发')
]
lg1 = record.log_ids.search(log_ids_domain)
if lg1:
record.is_send = True
def _compute_is_back(self):
for record in self:
record.is_back = False
current_uid = self.env.uid
user_matched_logs = record.log_ids.filtered(
lambda log: log.done_by.id == current_uid
).sorted(key=lambda log: log.create_date)
if not user_matched_logs:
continue
# 取当前登录人对应日志的最后一条(最新的)
current_user_last_log = user_matched_logs[-1]
if current_user_last_log.name == '加签':
record.is_back = False
continue
all_sorted_logs = record.log_ids.sorted(key=lambda log: log.create_date)
all_sorted_list = record.log_ids.sorted(key=lambda log: log.create_date).ids
# 找到当前日志在所有日志中的索引
try:
current_log_index = all_sorted_list.index(current_user_last_log.id)
except ValueError:
current_log_index = -1
if current_log_index != -1 and (current_log_index + 1) < len(all_sorted_list):
next_log = all_sorted_logs[current_log_index + 1]
if next_log.approval_state == "0":
record.is_back = True
same_node_logs = record.log_ids.filtered(lambda log: log.name == next_log.name)
if next_log.name == '加签':
if same_node_logs and any(log.approval_state in ['1', '2'] for log in same_node_logs):
record.is_back = False
if record._name in ['yuthon.official.incoming.normal', 'yuthon.sub.enterprises']:
current_user_in_logs2 = any(log.done_by.id == self.env.uid for log in record.log_ids2)
organizer_user_in_logs = any(log.done_by.id == self.env.user.id for log in record.log_ids.filtered(lambda l: l.name == '承办'))
all_handling_logs_approval_0 = all(log.approval_state == '0' for log in record.log_ids.filtered(lambda l: l.name == '承办'))
all_logs2_approval_0 = all(log.approval_state == '0' for log in record.log_ids2)
if all_logs2_approval_0 and all_handling_logs_approval_0 and not current_user_in_logs2 and not organizer_user_in_logs:
record.is_back = True
else:
record.is_back = False
if self.is_show_transfer == True:
record.is_back = False
def _compute_is_active(self):
_logger.info("_compute_is_active----" + str(fields.datetime.now()))
current_user_id = self.env.user.id
self.is_active = False
if self.log_ids:
if self.log_ids[-1].name == '归档' and self.log_ids[-1].done_by.id == current_user_id and self.log_ids[-1].approval_state in ['0', '1']:
self.is_active = True
_logger.info("_compute_is_active----" + str(fields.datetime.now()))
def _compute_is_done(self):
_logger.info("_compute_is_done循环开始----" + str(fields.datetime.now()))
for record in self:
record.is_done = False
log_ids_domain = [
('done_by', '=', self.env.user.id),
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
]
lg1 = record.log_ids.search(log_ids_domain)
if lg1:
all_approved = all(log.approval_state == '2' for log in lg1)
if all_approved:
record.is_done = True
workflow_record_id = self.env["soong.workflow.record"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id), ('reclaim_state', '=', False)])
if workflow_record_id:
if workflow_record_id.approve_state == 'end':
record.is_done = True
_logger.info("_compute_is_done循环结束----" + str(fields.datetime.now()))
def _compute_is_confirm(self):
for record in self:
record.is_confirm = False
record.is_end = False
target_models = ['yuthon.official.incoming.normal', 'yuthon.sub.enterprises']
if record._name in target_models:
chairman_logs = record.log_ids.filtered(
lambda log: '董事长' in (log.name or '')
and log.approval_state in ['0', '1']
and log.done_by.id == self.env.user.id
)
if chairman_logs:
record.is_end = True
continue
log_ids_domain = [
('done_by', '=', self.env.user.id),
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('approval_state', 'in', ['0', '1']),
('name', '=', '承办')
]
log_ids2_domain = [
('done_by', '=', self.env.user.id),
('workflow_id', '=', record.workflow_id.id),
('res_id', '=', record.id),
('approval_state', 'in', ['0', '1'])
]
lg1 = record.log_ids.search(log_ids_domain)
lg2 = record.log_ids2.search(log_ids2_domain)
lg3 = record.log_ids.search(log_ids2_domain)
if lg1 or lg2:
record.is_confirm = True
if not record.is_confirm:
record.is_end = True
if not lg2 and not lg3:
record.is_end = False
if record.log_ids:
if record.log_ids[-1].name == '归档':
record.is_end = False
@api.depends("node_id", "log_ids")
def _compute_approval_sign_ids(self):
for record in self:
record.approval_sign_ids = record.log_ids.filtered(lambda r: r.node_btn_name == "2" and r.node_id == record.node_id).done_by.ids
@api.depends("state")
def _compute_workflow_id(self):
# Objects
wkf_obj = self.env["soong.workflow"]
for rec in self:
# Variables
rec_model = rec._name
# Get Workflow
rec.workflow_id = wkf_obj.search([("model_id", "=", rec_model), ('is_enable', '=', True)])
node_id = fields.Many2one(
comodel_name="soong.workflow.node",
string="Workflow Node Ref.",
compute="_compute_node_id",
)
@api.depends("state")
def _compute_node_id(self):
# Objects
wkf_obj = self.env["soong.workflow"]
for rec in self:
# Variables
rec_model = rec._name
# Get Workflow Node
rec.node_id = wkf_obj.search(
[("model_id", "=", rec_model), ('is_enable', '=', True)]
).node_ids.filtered(lambda n: n.node_name == rec.state)
log_ids = fields.One2many(comodel_name="soong.workflow.log", inverse_name="res_id", string=" ",
domain=lambda self: [("model", "=", self._name)], auto_join=True)
log_ids2 = fields.One2many(comodel_name="soong.workflow.log2", inverse_name="res_id", string=" ",
domain=lambda self: [("model", "=", self._name)], auto_join=True)
# log_ids3 = fields.One2many(comodel_name="soong.workflow.log3", inverse_name="res_id", string=" ",
# domain=lambda self: [("model", "=", self._name)], auto_join=True)
soong_record_ids = fields.One2many(comodel_name="soong.workflow.line", inverse_name="res_id", string=" ",)
project_ids = fields.One2many(comodel_name="project.project.line", inverse_name="res_id", string=" ",)
def get_logs_for_print(self, applicant_first=False):
"""
返回用于打印的审批日志列表:
- 按与界面相同的顺序(sequence 升序)
- 保留所有审批记录(包括重复节点,如申请人多次申请)
- 可选:申请人置顶
"""
self.ensure_one()
ordered = self.log_ids.sorted('sequence')
if applicant_first:
applicants = [l for l in ordered if (l.name or '') == '申请人']
others = [l for l in ordered if (l.name or '') != '申请人']
ordered = applicants + others
return ordered
# @api.model_create_multi
# def create(self, vals_list):
# res = super().create(vals_list)
# target_role = self.env['soong.user.role'].search(
# [('name', '=', '考勤校对'), ('company_id', '=', res.env.user.employee_id.company_id.id)],
# limit=1
# )
# target_role_id = target_role.id if target_role else None
#
# for record in res:
# button_ids = record.sudo().workflow_id.node_ids.button_ids.search(
# [('workflow_id', '=', record.workflow_id.id)])
# # 获取当前用户的主角色和辅助角色ID列表
# employee = record.env.user.employee_id
# current_user_role_ids = []
# if employee.user_role_id:
# current_user_role_ids.append(employee.user_role_id.id)
# if employee.user_role_ids:
# current_user_role_ids.extend(employee.user_role_ids.ids)
# if target_role_id in current_user_role_ids:
# continue
# for button_id in button_ids:
# if button_id.node_id.is_end:
# continue
# allowed_departments = button_id.node_id.department_id
# if allowed_departments and employee.department_id.id not in allowed_departments.ids:
# continue
# allowed_role_ids = []
# if button_id.users_role_id:
# allowed_role_ids.append(button_id.users_role_id.id)
# elif button_id.users_role_group_ids:
# allowed_role_ids.extend(button_id.users_role_group_ids.user_role_ids.ids)
# else:
# continue
# if current_user_role_ids and any(role_id in allowed_role_ids for role_id in current_user_role_ids):
# record.state = button_id.node_id.node_name
# return res
def _create_update_logs(self, btn_rec):
log_obj = self.env["soong.workflow.log"]
if btn_rec.name not in ['转交', '分发', '加签', '加签确认', '退回', '收回', '结束', '归档', '确认']:
record_obj = log_obj.search(
[("model", "=", self._name),
("res_id", "=", self.id),
("node_id", "=", btn_rec.node_id.id),
("workflow_id", "=", btn_rec.workflow_id.id),
("node_btn_id", "=", btn_rec.id),
("node_btn_name", "=", "0")
], order="lase_click_date DESC", limit=1)
if not record_obj:
record_obj = log_obj.create({
"model": self._name,
"res_id": self.id,
"node_id": btn_rec.node_id.id,
"workflow_id": btn_rec.workflow_id.id,
"node_btn_id": btn_rec.id,
"done_by": self.env.uid,
"node_btn_name": "0",
"lase_click_date": fields.Datetime.now(),
})
record_obj.write({"node_btn_id": btn_rec.id, "done_by": self.env.uid, "approval_state": "2"})
record_obj = log_obj.search([
("model", "=", self._name),
("res_id", "=", self.id),
("node_id", "=", btn_rec.node_id.id),
("workflow_id", "=", btn_rec.workflow_id.id),
], order="lase_click_date DESC", limit=1
)
return record_obj
def btn_exec_action(self):
"""
Got invoked when a workflow button is called.
:return: button action return value.
"""
self.ensure_one()
# Variables
cx = self.env.context.copy() or {}
cx.update({"active_id": self.id, "active_ids": self.ids})
wkf_btn_obj = self.env["soong.workflow.node.button"]
btn_rec = wkf_btn_obj.search([("btn_key", "=", cx.get("btn_key", False))])
# # 优化弹窗
if btn_rec:
log_obj = self._create_update_logs(btn_rec)
# # 判断加签
# if btn_rec.name in ["加签"]:
# # 弹窗 进行填写 人员
# wizard = self.env.ref("soong_workflow.view_soong_workflow_apostille_wizard")
# return {
# "name": _("添加加签人员"),
# "type": "ir.actions.act_window",
# "view_mode": "form",
# "res_model": "soong.workflow.apostille.wizard",
# "views": [(wizard.id, "form")],
# "view_id": wizard.id,
# "target": "new",
# "context": {
# "default_res_id": self.id,
# "default_res_model": self._name,
# "default_log_ids": log_obj.ids,
# "default_node_btn_id": btn_rec.id,
# },
# }
# if btn_rec.name in ["加签确认"]:
# # 弹窗 进行填写 人员
# wizard = self.env.ref("soong_workflow.view_soong_workflow_apostille_ok_wizard")
# return {
# "name": _("添加加签人员"),
# "type": "ir.actions.act_window",
# "view_mode": "form",
# "res_model": "soong.workflow.apostille.ok.wizard",
# "views": [(wizard.id, "form")],
# "view_id": wizard.id,
# "target": "new",
# "context": {
# "default_res_id": self.id,
# "default_res_model": self._name,
# "default_log_ids": log_obj.ids,
# "default_node_btn_id": btn_rec.id,
# },
# }
# if btn_rec.name not in ["加签", "加签确认"]:
# # 判断当前是否所有加签都已经结束
# apostille_log_obj = self.sudo().env["soong.workflow.log"].search(
# [
# ("model", "=", self._name),
# ("res_id", "=", self.id),
# ("node_id", "=", btn_rec.node_id.id),
# ("workflow_id", "=", btn_rec.workflow_id.id),
# ("node_btn_name", "=", "2"),
# ("approval_state", "!=", "2")
# ]
# )
# if apostille_log_obj:
# raise UserError("当前加签人员还有没有审批完成的!")
# 继续正常流程
# if btn_rec.has_comment:
return self.with_context(cx)._add_comment(log_obj, btn_rec)
# if btn_rec.name == '收回':
# return {
# 'name': '收回弹窗',
# 'type': 'ir.actions.act_window',
# 'view_mode': 'form',
# 'res_model': 'soong.bounced.wizard',
# "target": "new",
# 'context': {
# 'default_res_id': self.id,
# "default_res_model": self._name,
# }
# }
# else:
# btn_rec.with_context(cx).log()
# return btn_rec.with_context(cx).run()
def _add_comment(self, logs, btn_rec):
trans_link_ids = []
wizard = self.env.ref("soong_workflow.view_soong_workflow_comment_wizard")
workflow_record_id = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id), ('reclaim_state', '=', False)])
# 状态列表
node_list = btn_rec.workflow_id.node_ids.sorted('sequence')
if self.name_title != '采购申请单':
state = self.state
state_list = [i.node_name for i in btn_rec.workflow_id.node_ids.sorted('sequence')]
else:
if self.state == "purchase":
state = "总经理"
else:
state = self.state
state_list = [i.node_name for i in btn_rec.workflow_id.node_ids.sorted('sequence')] + ['purchase']
# 当前节点状态
if self.state == 'draft':
state = '询价'
self_state = state_list.index(state)
# 当前节点
if btn_rec.name not in ['结束', '归档']:
current_node = node_list[state_list.index(state)]
workflow_record_id = self.env["soong.workflow.record"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id)])
self.env['trans.link'].search([('workflow_record_id', '=', workflow_record_id.id)]).unlink()
# 退回节点根据日志进行判断
if btn_rec.name == '退回':
approved_logs = self.log_ids.filtered(lambda log: log.approval_state == '2')
for ret in approved_logs:
if not self.env['trans.link'].search([('workflow_record_id', '=', workflow_record_id.id), ('name', '=', ret.name), ('link_type', '=', 'return')]):
self.env['trans.link'].create({
'sequence': ret.sequence,
'workflow_record_id': workflow_record_id.id,
'node_button_id': btn_rec.id,
'name': ret.name,
'link_type': 'return',
'record_id': self.id,
})
if btn_rec.name in ['转交', '加签']:
required_fields = [
field.name for field in current_node.field_ids
if field.required and field.name
]
missing_fields = []
if required_fields:
for field_name in required_fields:
field_value = getattr(self, field_name.name, None)
if not field_value:
missing_fields.append(field_name.field_description)
if missing_fields:
raise UserError(f"转交失败!必填字段未填写:{', '.join(missing_fields)}")
skipped_node_names = [] # 因没有可用按钮而跳过的节点
node_list = list(btn_rec.workflow_id.node_ids.sorted('sequence'))
index = self_state
actual_path_nodes = []
#跟踪整个工作流路径,只保留实际会执行的节点
while index < len(node_list):
current_node = node_list[index]
actual_path_nodes.append(current_node)
if not current_node.out_link_ids:
index = 20 # 防止死循环
break
current_department = self.department_id
if btn_rec.link_id in current_node.out_link_ids and len(current_node.out_link_ids) == 1:
matched_link = btn_rec.link_id
next_node = matched_link.node_to
matched_key = next_node.name
else:
if len(current_node.out_link_ids) > 1:
# 筛选与当前节点部门匹配的后续节点
valid_nodes = []
for link in current_node.out_link_ids:
if self.name_title in ['汇审意见表', '信息发布申请表',]:
category_names = self.information_categories_ids.mapped('name')
target_name = link.name.split('->')[-1].strip()
if '工会活动' in category_names:
if target_name == '工会主席':
target_node = link.node_to
valid_nodes.append((target_node, link))
else:
if target_name == '复核':
target_node = link.node_to
valid_nodes.append((target_node, link))
if target_name == '承办部门意见':
target_node = link.node_to
valid_nodes.append((target_node, link))
if '经营业务(财务专用)'in category_names:
if target_name == '财务部':
target_node = link.node_to
valid_nodes.append((target_node, link))
else:
target_node = link.node_to
if target_node.department_id and any(
dept in current_department or
current_department in self.get_all_sub_departments(dept)
for dept in target_node.department_id
):
valid_nodes.append((target_node, link))
continue
# ===================== 新增:部门为空 → 走【角色/角色组】匹配 =====================
if not target_node.department_id:
if hasattr(self, 'user_id') and self.user_id:
applicant_user = self.user_id
elif hasattr(self, 'users_id') and self.users_id:
applicant_user = self.users_id
applicant_roles = set()
if applicant_user and applicant_user.employee_id:
if applicant_user.employee_id.user_role_id:
applicant_roles.add(applicant_user.employee_id.user_role_id.id)
if applicant_user.employee_id.user_role_ids:
applicant_roles.update(applicant_user.employee_id.user_role_ids.ids)
button = None
if link.node_to.button_ids:
transfer_btn = link.node_to.button_ids.filtered(lambda r: r.name == '转交')
if transfer_btn:
for btn in transfer_btn:
if btn.is_review_invisible:
try:
domain = eval(btn.invisible_expression)
expression_result = self.search(domain)
has_self_in_result = self.id in expression_result.ids
if has_self_in_result:
button = btn
break
except:
continue
else:
button = btn
break
if not button:
end_btn = link.node_to.button_ids.filtered(lambda r: r.name == '结束')
if end_btn:
button = end_btn[0]
if button:
valid_nodes.append((target_node, link))
continue
# 尝试父部门匹配
if not valid_nodes and current_department and current_department.parent_id:
parent_department = current_department.parent_id
for link in current_node.out_link_ids:
target_node = link.node_to
if target_node.department_id and any(
dept in parent_department or
parent_department in self.get_all_sub_departments(dept)
for dept in target_node.department_id
):
valid_nodes.append((target_node, link))
# 如果两种匹配都失败,将所有节点加入有效列表
if not valid_nodes:
for link in current_node.out_link_ids:
valid_nodes.append((link.node_to, link))
final_valid_nodes = []
for node, link in valid_nodes:
transfer_buttons = [btn for btn in node.button_ids if btn.name == '转交']
end_buttons = [btn for btn in node.button_ids if btn.name == '结束']
has_visible_transfer_btn = False
has_visible_end_btn = False
# 判断转交按钮可见性
if transfer_buttons:
for btn in transfer_buttons:
if btn.is_review_invisible:
try:
domain = eval(btn.invisible_expression)
expression_result = self.search(domain)
if self.id in expression_result.ids:
has_visible_transfer_btn = True
break
except:
continue
else:
has_visible_transfer_btn = True
break
# 判断结束按钮可见性
if end_buttons:
for btn in end_buttons:
if btn.is_review_invisible:
try:
domain = eval(btn.invisible_expression)
expression_result = self.search(domain)
if self.id in expression_result.ids:
has_visible_end_btn = True
break
except:
continue
else:
has_visible_end_btn = True
break
# 两个按钮都不可见 → 跳过
if not has_visible_transfer_btn and not has_visible_end_btn:
skipped_node_names.append(node.name)
_logger.info(f"跳过节点: {node.name},转交按钮和结束按钮均不可见")
else:
final_valid_nodes.append((node, link))
# 选择第一个匹配的节点
next_node, matched_link = final_valid_nodes[0] if final_valid_nodes else valid_nodes[0]
matched_key = next_node.name
else:
matched_link = current_node.out_link_ids[0]
next_node = matched_link.node_to
matched_key = next_node.name
transfer_buttons = [
btn for btn in next_node.button_ids
if btn.name == '转交'
]
end_buttons = [
btn for btn in next_node.button_ids
if btn.name == '结束'
]
has_visible_transfer_btn = False
has_visible_end_btn = False
# ========== 第一步:判断转交按钮(transfer_buttons)是否可见 ==========
if transfer_buttons:
for btn in transfer_buttons:
if btn.is_review_invisible:
# 解析隐藏表达式,判断当前记录是否在表达式结果中
domain = eval(btn.invisible_expression)
expression_result = self.search(domain)
has_self_in_result = self.id in expression_result.ids
# 转交按钮可见条件:表达式结果包含当前记录
if has_self_in_result:
has_visible_transfer_btn = True
break # 找到可见按钮,终止循环
else:
# 无隐藏控制,转交按钮默认可见
has_visible_transfer_btn = True
break # 找到可见按钮,终止循环
# ========== 第二步:判断结束按钮(end_buttons)是否可见 ==========
if end_buttons:
for btn in end_buttons:
if btn.is_review_invisible:
# 解析隐藏表达式,判断当前记录是否在表达式结果中
domain = eval(btn.invisible_expression)
expression_result = self.search(domain)
has_self_in_result = self.id in expression_result.ids
# 结束按钮可见条件:表达式结果包含当前记录
if has_self_in_result:
has_visible_end_btn = True
break # 找到可见按钮,终止循环
else:
# 无隐藏控制,结束按钮默认可见
has_visible_end_btn = True
break # 找到可见按钮,终止循环
if not has_visible_transfer_btn and not has_visible_end_btn:
skipped_node_names.append(next_node.name)
_logger.info(f"跳过节点: {next_node.name},转交按钮和结束按钮均不可见")
# 更新索引
index = state_list.index(matched_key)
if index >= len(node_list):
break
#根据实际路径创建trans.link记录并生成domain
actual_path_links = []
self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id)])
for node in actual_path_nodes[1:]:
workflow_record_id = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "=", self.id)])
if node.name not in skipped_node_names:
link = self.env['trans.link'].create({
'sequence': node.sequence,
'workflow_record_id': workflow_record_id.id,
'node_button_id': btn_rec.id,
'name': node.name,
'link_type': 'trans',
'record_id': self.id,
})
actual_path_links.append(link)
if actual_path_links:
trans_link_ids = [link.id for link in actual_path_links]
trans_domain = [('id', 'in', trans_link_ids)]
else:
trans_domain = []
is_supervising = False
if self.name_title in ['行政发文']:
is_supervising = self.is_supervising
comment_field_map = {
'转交': 'transfer_comment_id',
'退回': 'return_comment_id',
'确认': 'confirm_comment_id',
'结束': 'end_comment_id'
}
comment_note_id = False
if btn_rec.name in comment_field_map:
target_field = comment_field_map[btn_rec.name]
if hasattr(btn_rec.node_id, target_field) and btn_rec.node_id[target_field]:
comment_note_id = btn_rec.node_id[target_field].id
trans_link_id = self.env['trans.link'].search(trans_domain, limit=1).sorted('sequence').id if btn_rec.name == '转交' else False
return_link_id = self.env['trans.link'].search(
[('link_type', '=', 'return'), ('record_id', '=', self.id)],
limit=1
).sorted('sequence')[:1].id if btn_rec.name == '退回' else False
all_links = self.env['trans.link'].browse(trans_link_ids)
invalid_links = self.env['trans.link']
current_link = self.env['trans.link'].browse(trans_link_id)
allowed_link_ids = []
default_trans_link_id = trans_link_id
if btn_rec.name in ['转交', '加签'] and trans_link_ids:
if current_link and all_links:
sorted_all_links = all_links.sorted('sequence')
for idx, link in enumerate(sorted_all_links):
approvers = self._get_approvers_for_link(link, self)
if len(approvers) == 1:
current_approver = approvers[0]
node_id = self.workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', self.workflow_id.id)
], limit=1)
if node_id:
# 下一个相同审批人跳过
if node_id.same_user_skip_type == 'next':
if idx + 1 < len(sorted_all_links):
next_link = sorted_all_links[idx + 1]
next_approvers = self._get_approvers_for_link(next_link, self)
if len(next_approvers) == 1 and next_approvers[0] == current_approver:
invalid_links |= next_link
# 上一个相同审批人跳过
if node_id.same_user_skip_type == 'prev':
if idx - 1 >= 0:
prev_link = sorted_all_links[idx - 1]
prev_approvers = self._get_approvers_for_link(prev_link, self)
if len(prev_approvers) == 1 and prev_approvers[0] == current_approver:
invalid_links |= prev_link
allowed_link_ids = [lid for lid in trans_link_ids if lid not in invalid_links.ids]
default_trans_link_id = allowed_link_ids[0] if allowed_link_ids else trans_link_id
return {
"name": _("评论"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "soong.workflow.comment.wizard",
"views": [(wizard.id, "form")],
"view_id": wizard.id,
"target": "new",
"context": {
"default_return_link_id": return_link_id,
"default_res_id": self.id,
'default_comment_note_id': comment_note_id,
"default_code": self.code,
"default_workflow_record_id": workflow_record_id.id,
'default_illustrate':btn_rec.node_id.illustrate,
"default_res_model": self._name,
"default_node_btn_id": btn_rec.id,
"default_is_supervising": is_supervising,
"default_trans_link_id": default_trans_link_id,
"all_workflow_links": allowed_link_ids,
"default_invalid_link_ids": invalid_links.ids,
},
}
def get_all_sub_departments(self, department, level=0, print_tree=True):
all_departments = department.child_ids
# 打印当前部门的子部门
if print_tree and department.child_ids:
indent = " " * level # 使用空格缩进表示层级
print(f"{indent}{department.name} 的子部门:")
for child in department.child_ids:
child_indent = " " * (level + 1)
print(f"{child_indent}- {child.name} (ID: {child.id})")
# 递归获取每个子部门的子部门
for child in department.child_ids:
child_sub_departments = self.get_all_sub_departments(child, level + 1, print_tree)
all_departments |= child_sub_departments
return all_departments
@api.constrains("state")
def _update_wkf_records(self):
for rec in self:
record_obj = self.env["soong.workflow.record"].search([("res_model", "=", rec._name), ("res_id", "=", rec.id)])
record_id = self.env[rec._name].search([('id', '=', rec.id)])
current_user_id = self.env.user.id
if self.state in ['申请人', '拟办', '草稿', 'draft']:
if not record_obj:
values = {
"code": record_id.code,
"res_model": rec._name,
"department_id": rec.department_id.id,
"res_id": rec.id,
"workflow_id": rec.workflow_id.id,
"node_id": rec.node_id.id,
"done_by": self.env.uid,
"users_ids": [(4, current_user_id)],
"employee_ids": self.env.user.employee_id,
"write_date2": fields.Datetime.now(),
}
# 查找匹配的字段
matched_fields = self.find_fields_by_string(record_id)
# 提取"事由"或"正文"字段的值
text_fields = [f for f in matched_fields if
'事由' in f['field_string'] or '正文' in f['field_string']]
if text_fields:
# 优先使用"事由"字段,没有则使用第一个"正文"字段
reason_field = next((f for f in text_fields if '事由' in f['field_string']), text_fields[0])
values["text"] = getattr(rec, reason_field['field_name'], '')
# 提取"附件"字段的值
attachment_fields = [f for f in matched_fields if '附件' in f['field_string']]
if attachment_fields and attachment_fields[0]['field_type'] in ['many2many', 'one2many']:
# 处理附件关系字段
attachment_field = attachment_fields[0]
attachment_ids = getattr(rec, attachment_field['field_name'], [])
if attachment_ids:
values["documents_ids"] = [(6, 0, attachment_ids.ids)]
# 创建工作流记录
self.env["soong.workflow.record"].create(values)
if record_obj:
# 获取上一节点的结束时间(即log_ids最后一条记录的lase_click_date)
node_update_time = False
try:
if hasattr(rec, 'log_ids') and rec.log_ids:
# 按ID排序获取最后一条日志
sorted_logs = rec.log_ids.sorted(lambda log: log.id)
if sorted_logs and sorted_logs[-1].lase_click_date:
node_update_time = sorted_logs[-1].lase_click_date
# 补丁:如果审批状态已经是结束,但还没拿到时间,则尝试用当前时间作为办结时间
if not node_update_time and record_obj.approve_state == 'end':
node_update_time = fields.Datetime.now()
except Exception as e:
_logger.debug(f"Failed to get node_update_time: {e}")
# 更新记录
update_vals = {
"workflow_id": rec.workflow_id.id,
"node_id": rec.node_id.id,
"done_by": self.env.uid,
"write_date2": fields.Datetime.now(),
}
if node_update_time:
update_vals["node_update_time"] = node_update_time
record_obj.update(update_vals)
def find_fields_by_string(self,record_id):
"""
根据模型名称和记录ID查找字段,其string属性包含特定关键词
:param record_id: 记录id
:return: 匹配的字段信息列表
"""
# 定义要查找的关键词
keywords = ['正文', '事由', '附件']
# 获取模型的所有字段
fields = record_id.fields_get()
# 存储匹配的字段信息
matched_fields = []
# 遍历所有字段,检查string属性
for field_name, field_attrs in fields.items():
field_string = field_attrs.get('string', '')
# 检查字段的string是否包含任何关键词
if any(keyword in field_string for keyword in keywords):
matched_fields.append({
'field_name': field_name,
'field_string': field_string,
'field_type': field_attrs.get('type'),
})
return matched_fields
def unlink(self):
self.mapped("log_ids").unlink()
self.mapped("log_ids2").unlink()
if self.env.user.name != 'Administrator':
if len(self.log_ids) > 1:
raise UserError("该流程不允许删除,如果要调整请联系管理员")
record_objs = self.env["soong.workflow.record"].search([("res_model", "=", self._name), ("res_id", "in", self.ids)])
record_objs.unlink()
return super(SoongWorkflowMixin, self).unlink()
def action_open_diagram(self):
self.ensure_one()
current_node_id = self.workflow_id.node_ids.filtered(
lambda n: n.node_name == self.state
)
highlight_color = self.workflow_id.current_stage_highlight_color
return {
"type": "ir.actions.client",
"tag": "open_diagram_plus_dialog",
"params": {
"act_window": {
"name": _("Workflow: %(name)s") % {"name": self.display_name},
"res_model": "soong.workflow",
"type": "ir.actions.act_window",
"res_id": self.workflow_id.id,
"target": "new",
"dialog_size": "medium",
"context": {
"highlight_node_id": current_node_id.id,
"highlight_node_color": highlight_color,
"diagram_readonly": True,
},
"views": [(False, "diagram_plus")],
}
}
}
# return {
# "name": _("Workflow: %(name)s")
# % {
# "name": self.display_name,
# },
# "res_model": "soong.workflow",
# "type": "ir.actions.act_window",
# "res_id": self.workflow_id.id,
# "target": "new",
# "dialog_size": "medium",
# "context": {
# "highlight_node_id": current_node_id.id,
# "highlight_node_color": highlight_color,
# "diagram_readonly": True,
# },
# "views": [(False, "diagram_plus")],
# }
@api.model
def get_view(self, view_id=None, view_type="form", **options):
res = super().get_view(view_id, view_type, **options)
active_id = self._context.get('active_id')
if '<form create="false"' not in res['arch']:
if view_type in ["tree", "form"]:
params = {}
if "field" in res["arch"]:
_logger.info("1---" + str(fields.datetime.now()))
res = self._remove_default_header(view_type, res)
_logger.info("2---" + str(fields.datetime.now()))
# res = self._remove_default_modifiers(view_type, res)
res = self._load_buttons_view(view_type, res)
_logger.info("3---" + str(fields.datetime.now()))
res = self._load_button_box_view(view_type, res)
_logger.info("4---" + str(fields.datetime.now()))
res = self._load_state_view(view_type, res)
_logger.info("5---" + str(fields.datetime.now()))
# res = self._load_one2many_views(view_type, res)
res = self._update_fields_view(view_type, res)
_logger.info("6---" + str(fields.datetime.now()))
# 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)
_logger.info("7---" + str(fields.datetime.now()))
# 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
# 采购订单流程结束后state会回到purchase,仅真正最后一个节点(无下游节点)的确认按钮额外允许该状态
if button.name == '确认' and model == 'purchase.order' and node.is_end and not node.out_link_ids:
invisible_condition = "state not in ('%s', 'purchase')" % 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), )
# 添加鼠标悬停提示
if button.name == '转交':
btn_exec_action_el.set("title", "审核,转至下一节点继续审批")
elif button.name == '结束':
btn_exec_action_el.set("title", "审批核准,流程到此结束,无下一节点人员可以选择")
elif button.name == '确认':
btn_exec_action_el.set("title", "传阅确认,不影响流程结束")
elif button.name == '加签':
btn_exec_action_el.set("title", "转交给下一节点一人或多人,完成后会返回回来")
elif button.name == '归档':
btn_exec_action_el.set("title", "按要求上传附件存单")
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 = "state != '{0}' ".format(str(node.node_name))
if button.review_type == 'user_role' and model != 'yuthon.seals.approval':
# 构建角色条件
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 = set(self.env['res.users'].search(role_domain).ids)
# 委托补充:角色组内的委托人(如部门经理A)若把流程委托给被委托人(b),
# 被委托人本人未必在该角色组内,也应能看到/操作该按钮
entrust_all = self.env['ir.model'].sudo().search(
[('model', '=', 'entrust.all')], limit=1)
model_rec = self.env['ir.model'].sudo().search(
[('model', '=', model)], limit=1) if model else None
entrusts = self.env['soong.workflow.entrust'].sudo().search([
('is_enable', '=', True),
('entrust_user_id', 'in', list(allowed_user_ids)),
])
for ent in entrusts:
ent_model_ids = ent.model_ids.ids
in_scope = (entrust_all and entrust_all.id in ent_model_ids) or (
model_rec and model_rec.id in ent_model_ids)
if in_scope and ent.entrust_user_id2:
allowed_user_ids.add(ent.entrust_user_id2.id)
allowed_user_ids = list(allowed_user_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 == '确认':
btn_exec_action_el.set("class", "btn btn-confirm")
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
# 采购订单流程结束后state会回到purchase,仅真正最后一个节点(无下游节点)的确认按钮额外允许该状态
if model == 'purchase.order' and node.is_end and not node.out_link_ids:
invisible = "state not in ('%s', 'purchase') or not is_confirm" % node.node_name
else:
invisible += "or not is_confirm"
if button.name == '结束':
btn_exec_action_el.set("class", "btn btn-finish")
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += " or not is_end or is_show_transfer"
if button.name == "退回":
btn_exec_action_el.set("class", "btn btn-return")
id_el = etree.Element("field")
id_el.set("name", "log_ids")
id_el.set("invisible", "1")
invisible += " or not log_ids or is_done or is_show_transfer"
if button.name == "分发":
btn_exec_action_el.set("class", "btn btn-distribute")
invisible += " or not is_send"
if button.name == "转交":
btn_exec_action_el.set("class", "btn btn-transfer")
invisible += " or is_done or is_show_transfer"
if button.name == "加签确认":
invisible += "or not is_show_transfer"
if button.name == "收回":
btn_exec_action_el.set("class", "btn btn-recall")
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 == "加签":
btn_exec_action_el.set("class", "btn btn-recall")
invisible += "or not is_show_add_sign"
if button.name == '归档':
btn_exec_action_el.set("class", "btn btn-archive")
invisible += "or not is_active"
custom_class = btn_exec_action_el.get("class", "")
btn_exec_action_el.set("class", custom_class)
# 添加鼠标悬停提示
if button.name == '转交':
btn_exec_action_el.set("title", "审核,转至下一节点继续审批")
elif button.name == '结束':
btn_exec_action_el.set("title", "审批核准,流程到此结束,无下一节点人员可以选择")
elif button.name == '确认':
btn_exec_action_el.set("title", "传阅确认,不影响流程结束")
elif button.name == '加签':
btn_exec_action_el.set("title", "转交给下一节点一人或多人,完成后会返回回来")
elif button.name == '归档':
btn_exec_action_el.set("title", "按要求上传附件存单")
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)
import_el = etree.Element("field")
import_el.set("name", "is_import")
import_el.set("invisible", "1")
header_el.append(import_el)
back_el = etree.Element("field")
back_el.set("name", "is_back")
back_el.set("invisible", "1")
header_el.append(back_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)
send_el = etree.Element("field")
send_el.set("name", "is_send")
send_el.set("invisible", "1")
header_el.append(send_el)
sign_el = etree.Element("field")
sign_el.set("name", "is_show_add_sign")
sign_el.set("invisible", "1")
header_el.append(sign_el)
approval_el = etree.Element("field")
approval_el.set("name", "approval_result")
approval_el.set("invisible", "1")
header_el.append(approval_el)
is_transfer_el = etree.Element("field")
is_transfer_el.set("name", "is_show_transfer")
is_transfer_el.set("invisible", "1")
header_el.append(is_transfer_el)
done_el = etree.Element("field")
done_el.set("name", "is_done")
done_el.set("invisible", "1")
header_el.append(done_el)
modify_btn_el = etree.SubElement(header_el, "button")
modify_btn_el.set("name", "action_is_modify")
modify_btn_el.set("string", "已修正")
modify_btn_el.set("type", "object")
modify_btn_el.set("invisible", "not is_modify")
modify_btn_el.set("class", "btn btn-primary")
modify_el = etree.Element("field")
modify_el.set("name", "is_modify")
modify_el.set("invisible", "1")
header_el.append(modify_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)
# 审批结果徽章(注入到 header 末尾,inline 不影响 sheet 布局,全局生效)
# 通过 → 绿色徽章,不通过 → 红色徽章,无结果时不显示
result_badge = etree.Element("span")
result_badge.set("class", "o_approval_result ms-2")
result_badge.set("invisible", "approval_result not in ('pass', 'fail')")
# 通过:绿色徽章
pass_badge = etree.SubElement(result_badge, "span")
pass_badge.set("class", "badge text-bg-success")
pass_badge.set("invisible", "approval_result != 'pass'")
pass_badge.text = "通过"
# 不通过:红色徽章
fail_badge = etree.SubElement(result_badge, "span")
fail_badge.set("class", "badge text-bg-danger")
fail_badge.set("invisible", "approval_result != 'fail'")
fail_badge.text = "不通过"
header_el.append(result_badge)
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()
_logger.info(f"--------{ext_ids}")
for ext_id in ext_ids:
_logger.info(f"--------{ext_id}")
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)
# _logger.info(f"-----{field_attrs.name}")
# _logger.info(f"-----{field_attrs.name.name}")
# _logger.info(f"-----{field_attrs.group_ids}")
# 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")
if self._name in ["purchase.order", "property.management"]:
# 原有流程节点状态只读 + 额外增加 state=purchase 也只读
field_inst.set("readonly", "(state in %s) or (state == 'purchase')" % readonly_arr)
else:
field_inst.set("readonly", "(state in %s) and (is_modify != True)" % 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)
def _get_approvers_for_link(self, link, record_id=None):
"""
参数:
link: trans.link 记录对象
record_id: 关联的业务记录对象
返回:
hr.employee 记录集(可能为空)
"""
if not link or isinstance(link, int):
return self.env['hr.employee']
workflow_id = link.node_button_id.workflow_id
node = workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', workflow_id.id)
])
button_priority_list = ["分发", "下发", "转交", "结束", "加签"]
button = False
if node.button_ids:
found_target_btn = False
for btn_name in button_priority_list:
for btn in sorted(node.button_ids, key=lambda x: x.sequence):
if btn.name == btn_name:
is_in_scope = True
if not btn.is_review_invisible:
button = btn
found_target_btn = True
break
if btn.is_review_invisible:
domain = eval(btn.invisible_expression)
model = record_id.env[record_id._name]
matched = model.search(domain + [('id', '=', record_id.id)])
is_in_scope = bool(matched)
if is_in_scope:
button = btn
found_target_btn = True
break
if found_target_btn:
break
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:
dept_id = record_id.department_id.id
# 按当前申请人自动匹配-本部门(保留部门和公司条件)
applicant_dept_ids = applicant.employee_ids.department_ids.ids
department_domain = [
'|', '|', '|',
('department_id', '=', dept_id),
('department_ids', 'in', [dept_id]),
('department_id', 'in', applicant_dept_ids),
('department_ids', 'in', applicant_dept_ids),
]
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]
if field_value:
if button.specify_field_id.relation == 'res.users':
employee = self.env['hr.employee'].sudo().search(
[('user_id', '=', field_value.id)],
limit=1
)
if employee:
return employee
elif button.specify_field_id.relation == 'hr.employee':
return field_value
else:
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'] # 默认返回空记录集
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="标题", required=True)
code = fields.Char(related="workflow_record_id.code", string="流水号")
workflow_id = fields.Many2one(related="workflow_record_id.workflow_id", string="审批类型")
company_id = fields.Many2one(related="workflow_record_id.company_id", string="公司")
department_id = fields.Many2one(related="workflow_record_id.department_id", 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="日期")