192 lines
10 KiB
Python
192 lines
10 KiB
Python
# -*- 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()
|