Files
gzth/soong_workflow/models/soong_workflow_batch_approval.py
T
2026-08-03 11:20:18 +08:00

192 lines
10 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 -*-
# 一键审批(批量):复用现有 soong_workflow 引擎无交互推进,不动 mixin/record 现有方法
from odoo import models
from odoo.exceptions import UserError
class SoongWorkflowRecord(models.Model):
_inherit = 'soong.workflow.record'
def action_batch_approve(self):
"""批量一键审批:对选中的“我的待办”记录,按当前节点自动执行 确认+转交/结束(方案A:只批当前登录人所在级并推进)"""
current_user = self.env.user
success_records = self.env['soong.workflow.record']
failed_info = [] # 收集失败原因,保证单条失败不影响其余
for rec in self:
# 限制:只处理审批类型为"加班"的工作流记录
if rec.workflow_id.name != '加班':
failed_info.append('记录%s:审批类型【%s】不是加班,已跳过' % (rec.id, rec.workflow_id.name or '空'))
continue
try:
# 取到对应的业务记录(如 overtime.approval)
biz = self.env[rec.res_model].browse(rec.res_id).exists()
if not biz:
failed_info.append('记录%s:业务记录不存在' % rec.id)
continue
# 当前节点:优先用 record 上的 node_id,回退到业务记录 state 对应的节点
node = rec.node_id or (biz.workflow_id.node_ids.filtered(lambda n: n.node_name == biz.state)[:1] if biz.workflow_id else False)
if not node:
failed_info.append('记录%s:当前节点为空' % rec.id)
continue
# 前置校验:当前登录人必须是当前节点的待办人(有待办/办理中的 log)
pending_log = biz.log_ids.filtered(
lambda l: l.model == biz._name
and l.res_id == biz.id
and l.node_id.id == node.id
and l.done_by.id == current_user.id
and l.approval_state in ('0', '1')
)
if not pending_log:
failed_info.append('记录%s:当前用户不是该节点待办人' % rec.id)
continue
# 取当前节点上当前登录人可见的 确认/转交/结束 按钮
visible_btns = self._get_visible_action_buttons(node, biz)
# 先执行“确认”(若挂了该按钮):标记当前节点审毕,不推进
if visible_btns.get('确认'):
self._auto_exec_engine(biz, visible_btns['确认'], node, is_transfer=False)
# 再判定推进动作:末节点“结束”办结,中间节点“转交”推进
if visible_btns.get('结束'):
self._auto_exec_engine(biz, visible_btns['结束'], node, is_transfer=False)
elif visible_btns.get('转交'):
self._auto_exec_engine(biz, visible_btns['转交'], node, is_transfer=True)
else:
# 既无结束也无转交:若连确认都没有,则本节点无可执行动作
if not visible_btns.get('确认'):
failed_info.append('记录%s:当前节点无可见的确认/转交/结束按钮' % rec.id)
continue
success_records += rec
except Exception as e:
failed_info.append('记录%s:%s' % (rec.id, str(e)))
# 汇总结果,用通知展示
if failed_info:
message = '一键审批完成:成功 %s 条,失败 %s 条。\n失败明细:\n%s' % (
len(success_records), len(failed_info), '\n'.join(failed_info))
notify_type = 'warning'
else:
message = '一键审批完成:成功 %s 条,全部通过。' % len(success_records)
notify_type = 'success'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '一键审批结果',
'message': message,
'type': notify_type,
'sticky': True,
}
}
def action_batch_approve_all(self):
"""移动端 kanban 一键审批:处理当前用户所有【待办+加班】审批,不依赖勾选/当前视图"""
current_user = self.env.user
# 自行搜索当前用户的待办加班审批,避免依赖勾选或当前视图记录集
records = self.search([
('workflow_id.name', '=', '加班'),
('is_to_do', '=', True),
('users_ids', 'in', [current_user.id]),
])
if not records:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '一键审批',
'message': '当前没有待审批的加班流程。',
'type': 'info',
'sticky': False,
}
}
# 复用逐条审批逻辑(含当前用户必须是节点待办人的前置校验)
return records.action_batch_approve()
def _get_visible_action_buttons(self, node, biz):
"""取当前节点上当前登录人可见的 确认/转交/结束 按钮,返回 {按钮名: 按钮对象}"""
result = {}
action_names = ('确认', '转交', '结束')
for btn in node.button_ids.sorted('sequence'):
if btn.name not in action_names:
continue
# 按引擎规则判断按钮可见性
if not self._is_button_visible(btn, biz):
continue
result[btn.name] = btn
return result
def _is_button_visible(self, btn, biz):
"""判断按钮对当前记录是否可见(复刻引擎的 is_review_invisible + invisible_expression 规则)"""
if not btn.is_review_invisible:
return True
if not btn.invisible_expression:
return True
# 与引擎一致:把当前记录 id 拼进 domain 判定是否命中
domain = eval(btn.invisible_expression)
return bool(biz.search(domain + [('id', '=', biz.id)]))
def _auto_exec_engine(self, biz, btn_rec, node, is_transfer):
"""复用现有引擎无交互执行某个按钮:先校验、再更新 log、最后 add_comment,避免失败时部分办结"""
# 1. 先调 _add_comment 仅计算 wizard 字段(不更新 log,仅取 context;会 unlink trans.link,属可接受副作用)
# _add_comment 内部不使用传入的 logs,这里用当前节点最新 log 占位
log_placeholder = self.env['soong.workflow.log'].search([
('model', '=', biz._name),
('res_id', '=', biz.id),
('node_id', '=', btn_rec.node_id.id),
], order='lase_click_date DESC', limit=1)
cx = self.env.context.copy()
cx.update({
'active_id': biz.id,
'active_ids': biz.ids,
'active_model': biz._name,
'btn_key': btn_rec.btn_key,
})
action = biz.with_context(cx)._add_comment(log_placeholder, btn_rec)
ctx = action.get('context', {}) or {}
# 2. 构造 wizard 基础字段,并对转交做前置校验(在更新当前节点 log 之前,防止部分办结)
wizard_vals = {
'res_model': ctx.get('default_res_model') or biz._name,
'res_id': ctx.get('default_res_id') or biz.id,
'code': ctx.get('default_code') or biz.code,
'node_btn_id': ctx.get('default_node_btn_id'),
'workflow_record_id': ctx.get('default_workflow_record_id'),
}
if is_transfer:
trans_link_id = ctx.get('default_trans_link_id')
if not trans_link_id:
raise UserError('当前节点【%s】无法计算出有效下一节点(后续节点按钮均不可见或路径为空)' % node.name)
trans_link = self.env['trans.link'].browse(trans_link_id)
approvers = biz._get_approvers_for_link(trans_link, biz)
if not approvers:
raise UserError('下一节点【%s】未匹配到审批人,请检查节点配置' % trans_link.name)
# 下一节点所有审批人都选
wizard_vals['trans_link_id'] = trans_link_id
wizard_vals['employee_ids'] = [(6, 0, approvers.ids)]
# 关键:按下一节点的并/串审模式设置,否则 add_comment 默认走串审,破坏并审逻辑
next_node = biz.workflow_id.node_ids.search([
('name', '=', trans_link.name),
('workflow_id', '=', biz.workflow_id.id),
], limit=1)
if next_node:
wizard_vals['approval_concatenation'] = next_node.approval_concatenation
# 取当前用户常用意见第一条作为审批意见(与 wizard 的 comment_note_id 下拉 domain 一致)
user = self.env.user
user_role_ids = self.env['soong.user.role']
for emp in user.employee_ids:
user_role_ids |= emp.user_role_ids
if emp.user_role_id:
user_role_ids |= emp.user_role_id
comment_note_id = False
if user_role_ids:
note = self.env['soong.note'].search([
('note_setting_id.users_role_ids', 'in', user_role_ids.ids)
], order='id', limit=1)
comment_note_id = note.id if note else False
wizard_vals['comment_note_id'] = comment_note_id
# 3. 校验通过后再更新/创建当前节点 log(与 btn_exec_action 一致)
log_obj = biz._create_update_logs(btn_rec)
# 4. 构造 wizard 并直接执行推进(绕过 UI 弹窗)
# 关键:必须把 active_model/active_id 指向业务记录,否则节点按钮的服务器动作会把 obj 当成 soong.workflow.record
wizard = self.env['soong.workflow.comment.wizard'].with_context(
active_model=biz._name,
active_id=biz.id,
active_ids=biz.ids,
).create(wizard_vals)
wizard.add_comment()