Files
yuthon_base/yuthon_bpd/models/bpd_diagram.py
T
2026-09-23 18:06:45 +08:00

206 lines
7.5 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 -*-
import json
import logging
import os
from odoo import api, fields, models
try: # Odoo 各版本导出位置略有差异,双保险
from odoo.tools import file_open
except ImportError: # pragma: no cover
from odoo.tools.misc import file_open
try: # Odoo 18 推荐用 tools.misc.file_path(get_module_resource 已弃用)
from odoo.tools.misc import file_path
except ImportError: # pragma: no cover
file_path = None
_logger = logging.getLogger(__name__)
TEMPLATE_FILE = 'yuthon_bpd/static/src/bpd_default.json'
VIEW_FILE = 'yuthon_bpd/static/src/bpd_view.html'
class BpdDiagram(models.Model):
"""业务流程图主模型。
一张图 = 若干泳道 + 若干节点 + 若干连线。坐标全部使用画布逻辑单位,
与前端 SVG 的 viewBox 一致,因此后端只存「逻辑坐标」,渲染缩放交给前端。
"""
_name = 'yuthon.bpd.diagram'
_description = '业务流程图'
_order = 'id'
name = fields.Char(string='名称', required=True)
key = fields.Char(string='关键字', index=True)
canvas_w = fields.Integer(string='画布宽', default=1560)
canvas_h = fields.Integer(string='画布高', default=980)
theme = fields.Text(
string='主题配色',
default='{}',
help='JSON 格式配色主题,前端读取后直接映射到节点/泳道/连线',
)
lane_ids = fields.One2many('yuthon.bpd.lane', 'diagram_id', string='泳道')
node_ids = fields.One2many('yuthon.bpd.node', 'diagram_id', string='节点')
edge_ids = fields.One2many('yuthon.bpd.edge', 'diagram_id', string='连线')
# ---------------- 模板装载 ----------------
@api.model
def _ensure_default_diagram(self):
"""安装后确保存在一张示例流程图(数据来自 bpd_default.json)。"""
if self.search_count([('key', '=', 'erp_mto')]):
return None
return self._load_template(TEMPLATE_FILE)
@api.model
def _load_template(self, path):
try:
with file_open(path, 'r') as f:
data = json.load(f)
except (IOError, OSError, ValueError) as exc:
_logger.warning('加载 BPD 模板失败: %s', exc)
return None
theme = data.get('theme') or {}
diagram = self.create({
'name': data.get('name') or '业务流程图',
'key': data.get('key'),
'canvas_w': (data.get('canvas') or {}).get('width', 1560),
'canvas_h': (data.get('canvas') or {}).get('height', 980),
'theme': json.dumps(theme, ensure_ascii=False),
})
lane_map = {}
for i, l in enumerate(data.get('lanes', [])):
lane = self.env['yuthon.bpd.lane'].create({
'diagram_id': diagram.id,
'name': l.get('name', '泳道'),
'kind': l.get('kind', 'column'),
'x': l.get('x', 0),
'y': l.get('y', 0),
'w': l.get('w', 300),
'h': l.get('h', 600),
'head_h': l.get('head_h', 34),
'label_pos': l.get('label_pos', 'top-center'),
'sequence': i,
})
lane_map[l.get('name')] = lane.id
node_map = {}
for i, n in enumerate(data.get('nodes', [])):
node = self.env['yuthon.bpd.node'].create({
'diagram_id': diagram.id,
'lane_id': lane_map.get(n.get('lane')) or False,
'label': n.get('label', ''),
'sub': n.get('sub', ''),
'x': n.get('x', 0),
'y': n.get('y', 0),
'w': n.get('w', 96),
'h': n.get('h', 34),
'shape': n.get('shape', 'box'),
'variant': n.get('variant', 'doc'),
'sequence': i,
})
node_map[n.get('id')] = node.id
for e in data.get('edges', []):
frm = node_map.get(e.get('from'))
to = node_map.get(e.get('to'))
if not frm or not to:
continue
self.env['yuthon.bpd.edge'].create({
'diagram_id': diagram.id,
'from_id': frm,
'to_id': to,
'label': e.get('label', ''),
'style': e.get('style', 'solid'),
})
_logger.info('已装载 BPD 示例流程图 key=%s', data.get('key'))
return diagram
# ---------------- 序列化 ----------------
def to_json(self):
self.ensure_one()
try:
theme = json.loads(self.theme or '{}') if self.theme else {}
except (ValueError, TypeError):
theme = {}
return {
'id': self.id,
'name': self.name,
'key': self.key or '',
'canvas': {'width': self.canvas_w, 'height': self.canvas_h},
'theme': theme,
'lanes': [{
'id': l.id,
'name': l.name,
'kind': l.kind,
'x': l.x, 'y': l.y, 'w': l.w, 'h': l.h,
'head_h': l.head_h,
'label_pos': l.label_pos,
} for l in self.lane_ids],
'nodes': [n.to_json() for n in self.node_ids],
'edges': [e.to_json() for e in self.edge_ids],
}
@api.model
def get_default(self):
"""前端拿图:优先 key=erp_mto,否则取第一张。"""
rec = self.search([('key', '=', 'erp_mto')], limit=1) \
or self.search([], limit=1, order='id')
return rec.to_json() if rec else None
# ---------------- 入口:后台打开前端 ----------------
@api.model
def view_asset_version(self):
"""前端静态页的版本号(取文件 mtime)。
bpd_view.html 的响应头是 Cache-Control: max-age=604800(7 天),
浏览器可能一直用旧副本。拿它拼 ?v=... 可打破缓存。
"""
if not file_path:
return 0
try:
return int(os.path.getmtime(file_path(VIEW_FILE, filter_ext=('.html',))))
except (OSError, ValueError):
return 0
@api.model
def refresh_visual_action_params(self):
"""升级模块时把最新版本号写进 client action 的 params(供菜单入口用)。
数据 XML 里用 <function> 调用,故不受 noupdate 影响,每次升级都会刷新。
"""
action = self.env.ref('yuthon_bpd.yuthon_bpd_action_visual',
raise_if_not_found=False)
if action:
action.params = {'v': self.view_asset_version()}
def action_open_visual(self):
"""后台表单按钮:在 Odoo 内部打开该图的可视化视图(不再跳走)。
返回客户端动作 yuthon_bpd.visual,由前端 OWL 组件用 iframe 承载
bpd_view.html 并撑满内容区;顶部面包屑可返回原表单。
"""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'yuthon_bpd.visual',
'name': self.name,
'params': {'diagram_id': self.id, 'v': self.view_asset_version()},
'target': 'current',
'context': dict(self.env.context, active_id=self.id, active_model=self._name),
}
def action_open_visual_url(self):
"""后台表单按钮:在新标签打开独立页面(多屏对照时用)。"""
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'url': '/yuthon_bpd/static/src/bpd_view.html?id=%s' % self.id,
'target': 'new',
}