diff --git a/yuthon_bpd/__init__.py b/yuthon_bpd/__init__.py new file mode 100644 index 0000000..3dbc753 --- /dev/null +++ b/yuthon_bpd/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from . import models +from . import controllers + + +def post_init_hook(env): + """安装后自动建一张示例流程图(数据来自 static/src/bpd_default.json)。""" + env['yuthon.bpd.diagram']._ensure_default_diagram() diff --git a/yuthon_bpd/__manifest__.py b/yuthon_bpd/__manifest__.py new file mode 100644 index 0000000..aebd092 --- /dev/null +++ b/yuthon_bpd/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Yuthon BPD 业务流程图', + 'summary': '泳道式业务流程图(可拖拽新建关联流程)', + 'version': '18.0.1.0.0', + 'category': 'Tools', + 'license': 'OPL-1', + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + # 注意顺序:menu.xml 里的 会在解析时立即 + # env.ref() 解析动作的 xmlid,因此定义动作的 bpd_views.xml 必须在前。 + 'views/bpd_views.xml', + 'views/menu.xml', + ], + 'post_init_hook': 'post_init_hook', + 'installable': True, + 'application': True, +} diff --git a/yuthon_bpd/controllers/__init__.py b/yuthon_bpd/controllers/__init__.py new file mode 100644 index 0000000..757b12a --- /dev/null +++ b/yuthon_bpd/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/yuthon_bpd/controllers/main.py b/yuthon_bpd/controllers/main.py new file mode 100644 index 0000000..b61bae6 --- /dev/null +++ b/yuthon_bpd/controllers/main.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +import json + +from odoo import http +from odoo.exceptions import AccessError +from odoo.http import request, Response + + +def _json(data, status=200): + return Response(json.dumps(data, ensure_ascii=False), + mimetype='application/json', status=status) + + +def _body(): + try: + return json.loads(request.httprequest.get_data(as_text=True) or '{}') + except (ValueError, TypeError): + return {} + + +class BpdController(http.Controller): + """业务流程图前端的数据接口。 + + 读图对所有登录用户开放;新建节点 / 连线 / 移动 / 改名 / 删除 等写操作 + 只有系统管理员有 write/create 权限(权限由 ir.model.access.csv 控制), + 普通用户会收到 403,前端据此提示「需管理员权限」。 + """ + + # ---------------- 只读:拿单张图 / 图列表 ---------------- + @http.route('/yuthon_bpd/api/diagram/data', type='http', + auth='user', methods=['GET'], csrf=False) + def api_diagram_data(self, **kw): + """不带 id:返回默认图(key=erp_mto);带 ?id=N:返回指定图。""" + Diagram = request.env['yuthon.bpd.diagram'] + did = kw.get('id') + if did not in (None, '', '0'): + try: + did = int(did) + except (TypeError, ValueError): + return _json({'error': 'bad_id'}, 400) + rec = Diagram.browse(did).exists() + if not rec: + return _json({'error': 'not_found'}, 404) + return _json({'diagram': rec.to_json()}) + data = Diagram.get_default() + if not data: + return _json({'error': 'no_diagram'}, 404) + return _json({'diagram': data}) + + @http.route('/yuthon_bpd/api/diagram/list', type='http', + auth='user', methods=['GET'], csrf=False) + def api_diagram_list(self, **kw): + """供前端下拉切换:返回所有流程图的 id / 名称 / key。""" + recs = request.env['yuthon.bpd.diagram'].search([], order='id') + return _json({'diagrams': [ + {'id': r.id, 'name': r.name, 'key': r.key or ''} for r in recs + ]}) + + # ---------------- 写:节点 ---------------- + @http.route('/yuthon_bpd/api/diagram//node', type='http', + auth='user', methods=['POST'], csrf=False) + def api_create_node(self, did, **kw): + diagram = request.env['yuthon.bpd.diagram'].browse(did).exists() + if not diagram: + return _json({'error': 'not_found'}, 404) + p = _body() + label = (p.get('label') or '').strip()[:120] + if not label: + return _json({'error': 'empty_label'}, 400) + lane_id = p.get('lane_id') or False + try: + node = request.env['yuthon.bpd.node'].create({ + 'diagram_id': did, + 'lane_id': int(lane_id) if lane_id else False, + 'label': label, + 'sub': (p.get('sub') or '').strip()[:60], + 'x': int(p.get('x', 0) or 0), + 'y': int(p.get('y', 0) or 0), + 'w': int(p.get('w', 96) or 96), + 'h': int(p.get('h', 34) or 34), + 'shape': p.get('shape') or 'box', + 'variant': p.get('variant') or 'doc', + }) + except AccessError: + return _json({'error': 'forbidden'}, 403) + except Exception as exc: + if 'unique' in str(exc).lower(): + return _json({'error': 'duplicate'}, 409) + raise + return _json({'node': node.to_json()}) + + @http.route('/yuthon_bpd/api/node//move', type='http', + auth='user', methods=['POST'], csrf=False) + def api_move_node(self, nid, **kw): + node = request.env['yuthon.bpd.node'].browse(nid).exists() + if not node: + return _json({'error': 'not_found'}, 404) + p = _body() + try: + node.write({ + 'x': int(p.get('x', node.x)), + 'y': int(p.get('y', node.y)), + }) + except AccessError: + return _json({'error': 'forbidden'}, 403) + return _json({'ok': True}) + + @http.route('/yuthon_bpd/api/node//rename', type='http', + auth='user', methods=['POST'], csrf=False) + def api_rename_node(self, nid, **kw): + node = request.env['yuthon.bpd.node'].browse(nid).exists() + if not node: + return _json({'error': 'not_found'}, 404) + p = _body() + label = (p.get('label') or '').strip()[:120] + if not label: + return _json({'error': 'empty_label'}, 400) + try: + node.write({'label': label}) + except AccessError: + return _json({'error': 'forbidden'}, 403) + return _json({'ok': True, 'label': node.label}) + + # ---------------- 写:连线 ---------------- + @http.route('/yuthon_bpd/api/diagram//edge', type='http', + auth='user', methods=['POST'], csrf=False) + def api_create_edge(self, did, **kw): + diagram = request.env['yuthon.bpd.diagram'].browse(did).exists() + if not diagram: + return _json({'error': 'not_found'}, 404) + p = _body() + try: + frm = int(p.get('from')) + to = int(p.get('to')) + except (TypeError, ValueError): + return _json({'error': 'bad_params'}, 400) + if frm == to: + return _json({'error': 'same_node'}, 400) + Node = request.env['yuthon.bpd.node'] + if not (Node.browse(frm).exists() and Node.browse(to).exists()): + return _json({'error': 'node_missing'}, 404) + try: + edge = request.env['yuthon.bpd.edge'].create({ + 'diagram_id': did, + 'from_id': frm, + 'to_id': to, + 'label': (p.get('label') or '').strip()[:60], + 'style': p.get('style') or 'solid', + }) + except AccessError: + return _json({'error': 'forbidden'}, 403) + except Exception as exc: + if 'unique' in str(exc).lower(): + return _json({'error': 'duplicate'}, 409) + raise + return _json({'edge': edge.to_json()}) + + @http.route('/yuthon_bpd/api/edge//delete', type='http', + auth='user', methods=['POST'], csrf=False) + def api_delete_edge(self, eid, **kw): + edge = request.env['yuthon.bpd.edge'].browse(eid).exists() + if not edge: + return _json({'error': 'not_found'}, 404) + try: + edge.unlink() + except AccessError: + return _json({'error': 'forbidden'}, 403) + return _json({'ok': True}) + + # ---------------- 写:泳道 ---------------- + @http.route('/yuthon_bpd/api/diagram//lane', type='http', + auth='user', methods=['POST'], csrf=False) + def api_create_lane(self, did, **kw): + diagram = request.env['yuthon.bpd.diagram'].browse(did).exists() + if not diagram: + return _json({'error': 'not_found'}, 404) + p = _body() + name = (p.get('name') or '新泳道').strip()[:60] + try: + lane = request.env['yuthon.bpd.lane'].create({ + 'diagram_id': did, + 'name': name, + 'kind': p.get('kind') or 'column', + 'x': int(p.get('x', 24) or 24), + 'y': int(p.get('y', 86) or 86), + 'w': int(p.get('w', 300) or 300), + 'h': int(p.get('h', 600) or 600), + 'head_h': int(p.get('head_h', 34) or 34), + 'label_pos': p.get('label_pos') or 'top-center', + }) + except AccessError: + return _json({'error': 'forbidden'}, 403) + return _json({'lane': { + 'id': lane.id, 'name': lane.name, 'kind': lane.kind, + 'x': lane.x, 'y': lane.y, 'w': lane.w, 'h': lane.h, + 'head_h': lane.head_h, 'label_pos': lane.label_pos, + }}) diff --git a/yuthon_bpd/models/__init__.py b/yuthon_bpd/models/__init__.py new file mode 100644 index 0000000..6c1e64f --- /dev/null +++ b/yuthon_bpd/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import bpd_diagram +from . import bpd_lane +from . import bpd_node +from . import bpd_edge diff --git a/yuthon_bpd/models/bpd_diagram.py b/yuthon_bpd/models/bpd_diagram.py new file mode 100644 index 0000000..bea2a55 --- /dev/null +++ b/yuthon_bpd/models/bpd_diagram.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import json +import logging + +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 + +_logger = logging.getLogger(__name__) + +TEMPLATE_FILE = 'yuthon_bpd/static/src/bpd_default.json' + + +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 + + # ---------------- 入口:后台跳前端 ---------------- + def action_open_visual(self): + """后台表单按钮:在新标签打开该图的可视化编辑页(带 diagram id)。""" + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'url': '/yuthon_bpd/static/src/bpd_view.html?id=%s' % self.id, + 'target': 'new', + } diff --git a/yuthon_bpd/models/bpd_edge.py b/yuthon_bpd/models/bpd_edge.py new file mode 100644 index 0000000..f948749 --- /dev/null +++ b/yuthon_bpd/models/bpd_edge.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class BpdEdge(models.Model): + """流程图连线(流程走向 / 关联流程)。""" + _name = 'yuthon.bpd.edge' + _description = '流程连线' + _order = 'sequence, id' + + diagram_id = fields.Many2one( + 'yuthon.bpd.diagram', string='流程图', required=True, + ondelete='cascade', index=True, + ) + from_id = fields.Many2one( + 'yuthon.bpd.node', string='起点', required=True, ondelete='cascade', + ) + to_id = fields.Many2one( + 'yuthon.bpd.node', string='终点', required=True, ondelete='cascade', + ) + label = fields.Char(string='线标签') + style = fields.Selection( + [('solid', '实线'), ('dashed', '虚线')], + string='线型', default='solid', required=True, + ) + sequence = fields.Integer(default=10) + + _sql_constraints = [ + ('bpd_edge_uniq', 'unique(diagram_id, from_id, to_id)', + '同一条流程连线不能重复'), + ] + + def to_json(self): + self.ensure_one() + return { + 'id': self.id, + 'from': self.from_id.id, + 'to': self.to_id.id, + 'label': self.label or '', + 'style': self.style, + } diff --git a/yuthon_bpd/models/bpd_lane.py b/yuthon_bpd/models/bpd_lane.py new file mode 100644 index 0000000..a7e7803 --- /dev/null +++ b/yuthon_bpd/models/bpd_lane.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class BpdLane(models.Model): + """流程图泳道 / 分组框。 + + 泳道就是画布上的一个矩形,坐标用画布逻辑单位(默认 1560 x 980)。 + 这样参考图那种"手工排版的泳道图"可以被完整表达,而不必受自动布局约束。 + """ + _name = 'yuthon.bpd.lane' + _description = '流程图泳道' + _order = 'sequence, id' + _rec_name = 'name' + + diagram_id = fields.Many2one( + 'yuthon.bpd.diagram', string='流程图', required=True, + ondelete='cascade', index=True, + ) + name = fields.Char(string='名称', required=True) + kind = fields.Selection( + [('column', '纵向泳道'), ('band', '横向泳道'), ('group', '分组框')], + string='类型', default='column', required=True, + help='纵向泳道 = 一列(如销售管理);横向泳道 = 一条横带(如仓库管理、财务管理)', + ) + x = fields.Integer(string='X', default=0, help='矩形左上角 X(画布逻辑单位)') + y = fields.Integer(string='Y', default=0, help='矩形左上角 Y(画布逻辑单位)') + w = fields.Integer(string='宽', default=300) + h = fields.Integer(string='高', default=600) + head_h = fields.Integer( + string='标题高', default=34, + help='仅纵向泳道使用:顶部标题框的高度;横向泳道标题直接写在带子里', + ) + label_pos = fields.Selection( + [('top-center', '顶部居中'), ('bottom-center', '底部居中'), ('left', '左侧')], + string='标题位置', default='top-center', + ) + sequence = fields.Integer(default=10) + note = fields.Char(string='备注') + + node_ids = fields.One2many('yuthon.bpd.node', 'lane_id', string='节点') + node_count = fields.Integer(string='节点数', compute='_compute_node_count') + + def _compute_node_count(self): + for lane in self: + lane.node_count = len(lane.node_ids) diff --git a/yuthon_bpd/models/bpd_node.py b/yuthon_bpd/models/bpd_node.py new file mode 100644 index 0000000..74d6053 --- /dev/null +++ b/yuthon_bpd/models/bpd_node.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class BpdNode(models.Model): + """流程图节点(一个业务动作 / 单据 / 角色)。 + + x / y 是节点**中心**坐标,单位与泳道一致。拖动节点时前端只改这两个值, + 所以画布上任何手工微调都能持久化。 + """ + _name = 'yuthon.bpd.node' + _description = '流程节点' + _order = 'sequence, id' + _rec_name = 'label' + + diagram_id = fields.Many2one( + 'yuthon.bpd.diagram', string='流程图', required=True, + ondelete='cascade', index=True, + ) + lane_id = fields.Many2one( + 'yuthon.bpd.lane', string='所属泳道', ondelete='set null', index=True, + ) + label = fields.Char(string='名称', required=True) + sub = fields.Char(string='副标题', help='节点下方的小字,如「批次」') + x = fields.Integer(string='X', default=0) + y = fields.Integer(string='Y', default=0) + w = fields.Integer(string='宽', default=96) + h = fields.Integer(string='高', default=34) + shape = fields.Selection( + [('box', '矩形'), ('diamond', '菱形'), ('pill', '圆角胶囊'), ('text', '纯文字')], + string='形状', default='box', required=True, + ) + variant = fields.Selection( + [ + ('doc', '普通单据'), + ('start', '起点 / 外部角色'), + ('primary', '核心(仓库 / 财务)'), + ('gateway', '判断(核销)'), + ('ghost', '虚化(任务池)'), + ], + string='样式', default='doc', required=True, + ) + sequence = fields.Integer(default=10) + note = fields.Text(string='说明') + + edge_out_ids = fields.One2many('yuthon.bpd.edge', 'from_id', string='出线') + edge_in_ids = fields.One2many('yuthon.bpd.edge', 'to_id', string='入线') + + def to_json(self): + self.ensure_one() + return { + 'id': self.id, + 'lane_id': self.lane_id.id or None, + 'lane': self.lane_id.name or '', + 'label': self.label or '', + 'sub': self.sub or '', + 'x': self.x, 'y': self.y, 'w': self.w, 'h': self.h, + 'shape': self.shape, + 'variant': self.variant, + 'note': self.note or '', + } diff --git a/yuthon_bpd/security/ir.model.access.csv b/yuthon_bpd/security/ir.model.access.csv new file mode 100644 index 0000000..6d65f37 --- /dev/null +++ b/yuthon_bpd/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_yuthon_bpd_diagram_admin,yuthon.bpd.diagram.admin,model_yuthon_bpd_diagram,base.group_system,1,1,1,1 +access_yuthon_bpd_diagram_user,yuthon.bpd.diagram.user,model_yuthon_bpd_diagram,base.group_user,1,0,0,0 +access_yuthon_bpd_lane_admin,yuthon.bpd.lane.admin,model_yuthon_bpd_lane,base.group_system,1,1,1,1 +access_yuthon_bpd_lane_user,yuthon.bpd.lane.user,model_yuthon_bpd_lane,base.group_user,1,0,0,0 +access_yuthon_bpd_node_admin,yuthon.bpd.node.admin,model_yuthon_bpd_node,base.group_system,1,1,1,1 +access_yuthon_bpd_node_user,yuthon.bpd.node.user,model_yuthon_bpd_node,base.group_user,1,0,0,0 +access_yuthon_bpd_edge_admin,yuthon.bpd.edge.admin,model_yuthon_bpd_edge,base.group_system,1,1,1,1 +access_yuthon_bpd_edge_user,yuthon.bpd.edge.user,model_yuthon_bpd_edge,base.group_user,1,0,0,0 diff --git a/yuthon_bpd/static/src/bpd_default.json b/yuthon_bpd/static/src/bpd_default.json new file mode 100644 index 0000000..522ff86 --- /dev/null +++ b/yuthon_bpd/static/src/bpd_default.json @@ -0,0 +1,133 @@ +{ + "key": "erp_mto", + "name": "ERP 业务流程(离散制造-MTO)", + "title": "ERP(离散制造-MTO)", + "canvas": { "width": 1560, "height": 980 }, + "theme": { + "pageBg": "#EFF7FD", + "canvasBg": "#FFFFFF", + "canvasBorder": "#CFE6F5", + "bannerFrom": "#1DA48E", + "bannerTo": "#31C3AA", + "laneBorder": "#8ADACB", + "laneHeadBorder": "#6ACDBB", + "laneHeadText": "#0F9E87", + "bandBg": "#F0FBF8", + "bandText": "#0F9E87", + "nodeFill": "#E8F2FB", + "nodeBorder": "#A9CFE9", + "nodeText": "#5B7B94", + "nodeSubText": "#93AFC3", + "startFill": "#EAF4FD", + "primaryFill": "#C6E1F8", + "primaryBorder": "#93C6EA", + "primaryText": "#2F6D92", + "ghostFill": "#F5FAFE", + "ghostBorder": "#D5E8F5", + "ghostText": "#A2BCCE", + "gatewayFill": "#E5F6F1", + "gatewayBorder": "#98D3C2", + "gatewayText": "#3E8F81", + "flow": "#4FC0A8" + }, + "lanes": [ + { "name": "销售管理", "kind": "column", "x": 24, "y": 86, "w": 326, "h": 804, "label_pos": "top-center" }, + { "name": "技术管理", "kind": "column", "x": 350, "y": 86, "w": 223, "h": 804, "label_pos": "top-center" }, + { "name": "生产管理", "kind": "column", "x": 573, "y": 86, "w": 725, "h": 804, "label_pos": "top-center" }, + { "name": "采购管理", "kind": "column", "x": 1298, "y": 86, "w": 238, "h": 804, "label_pos": "top-center" }, + { "name": "仓库管理", "kind": "band", "x": 573, "y": 470, "w": 725, "h": 140, "label_pos": "bottom-center" }, + { "name": "财务管理", "kind": "band", "x": 24, "y": 906, "w": 1512, "h": 62, "label_pos": "bottom-center" } + ], + "nodes": [ + { "id": "n_customer", "lane": "销售管理", "label": "客户", "x": 235, "y": 130, "w": 96, "h": 34, "variant": "start" }, + { "id": "n_quote", "lane": "销售管理", "label": "报价单", "x": 235, "y": 196, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_so", "lane": "销售管理", "label": "销售订单", "x": 235, "y": 262, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_deliver", "lane": "销售管理", "label": "销售出库/退货", "sub": "批次", "x": 235, "y": 372, "w": 116, "h": 34, "variant": "doc" }, + { "id": "n_ar", "lane": "销售管理", "label": "应收账款明细", "x": 235, "y": 690, "w": 120, "h": 34, "variant": "doc" }, + { "id": "n_bill", "lane": "销售管理", "label": "开票", "x": 124, "y": 772, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_recon1", "lane": "销售管理", "label": "对账", "x": 235, "y": 772, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_bad", "lane": "销售管理", "label": "坏账发票", "x": 235, "y": 840, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_ver1", "lane": "财务管理", "label": "收款单核销", "x": 235, "y": 937, "w": 110, "h": 42, "variant": "gateway", "shape": "diamond" }, + + { "id": "n_design", "lane": "技术管理", "label": "方案设计", "x": 388, "y": 229, "w": 104, "h": 34, "variant": "doc" }, + { "id": "n_route", "lane": "技术管理", "label": "生产工序", "x": 508, "y": 160, "w": 100, "h": 34, "variant": "doc" }, + { "id": "n_product", "lane": "技术管理", "label": "产品信息", "x": 508, "y": 229, "w": 100, "h": 34, "variant": "doc" }, + { "id": "n_bom", "lane": "技术管理", "label": "产品BOM", "x": 508, "y": 298, "w": 100, "h": 34, "variant": "doc" }, + + { "id": "n_shop", "lane": "生产管理", "label": "车间", "x": 660, "y": 130, "w": 96, "h": 34, "variant": "start" }, + { "id": "n_plan", "lane": "生产管理", "label": "生产计划", "x": 639, "y": 328, "w": 100, "h": 34, "variant": "doc" }, + { "id": "n_pool", "lane": "生产管理", "label": "生产任务池", "x": 774, "y": 238, "w": 100, "h": 30, "variant": "ghost" }, + { "id": "n_wo", "lane": "生产管理", "label": "生产工单", "sub": "批次", "x": 774, "y": 328, "w": 100, "h": 34, "variant": "doc" }, + { "id": "n_issue", "lane": "生产管理", "label": "生产领/退料", "x": 919, "y": 328, "w": 120, "h": 34, "variant": "doc" }, + { "id": "n_qc", "lane": "生产管理", "label": "生产报工/质检", "x": 1080, "y": 328, "w": 132, "h": 34, "variant": "doc" }, + { "id": "n_receipt", "lane": "生产管理", "label": "生产入库", "x": 1231, "y": 328, "w": 100, "h": 34, "variant": "doc" }, + { "id": "n_wh", "lane": "仓库管理", "label": "仓库", "x": 838, "y": 400, "w": 100, "h": 34, "variant": "primary" }, + { "id": "n_out_oth", "lane": "仓库管理", "label": "其他出库单", "x": 660, "y": 555, "w": 110, "h": 34, "variant": "doc" }, + { "id": "n_transfer", "lane": "仓库管理", "label": "库存调拨", "x": 838, "y": 555, "w": 110, "h": 34, "variant": "doc" }, + { "id": "n_count", "lane": "仓库管理", "label": "库存盘点", "x": 1080, "y": 555, "w": 110, "h": 34, "variant": "doc" }, + { "id": "n_in_oth", "lane": "仓库管理", "label": "其他入库单", "x": 1226, "y": 555, "w": 110, "h": 34, "variant": "doc" }, + + { "id": "n_vendor", "lane": "采购管理", "label": "供应商", "x": 1360, "y": 130, "w": 96, "h": 34, "variant": "start" }, + { "id": "n_preq", "lane": "采购管理", "label": "采购申请", "x": 1360, "y": 196, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_po", "lane": "采购管理", "label": "采购订单", "x": 1360, "y": 262, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_grn", "lane": "采购管理", "label": "采购入库/退货", "sub": "批次", "x": 1360, "y": 372, "w": 116, "h": 34, "variant": "doc" }, + { "id": "n_ap", "lane": "采购管理", "label": "应付账款明细", "x": 1360, "y": 690, "w": 120, "h": 34, "variant": "doc" }, + { "id": "n_recon2", "lane": "采购管理", "label": "对账", "x": 1350, "y": 772, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_getinv", "lane": "采购管理", "label": "收票", "x": 1478, "y": 772, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_ininv", "lane": "采购管理", "label": "进项发票", "x": 1350, "y": 840, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_ver2", "lane": "财务管理", "label": "付款单核销", "x": 1430, "y": 937, "w": 110, "h": 42, "variant": "gateway", "shape": "diamond" }, + + { "id": "n_cash", "lane": "财务管理", "label": "收款单", "x": 520, "y": 937, "w": 96, "h": 34, "variant": "doc" }, + { "id": "n_fin", "lane": "财务管理", "label": "财务", "x": 790, "y": 937, "w": 120, "h": 34, "variant": "primary" }, + { "id": "n_pay", "lane": "财务管理", "label": "付款单", "x": 1050, "y": 937, "w": 96, "h": 34, "variant": "doc" } + ], + "edges": [ + { "from": "n_customer", "to": "n_quote" }, + { "from": "n_quote", "to": "n_so" }, + { "from": "n_so", "to": "n_deliver" }, + { "from": "n_deliver", "to": "n_ar" }, + { "from": "n_ar", "to": "n_bill" }, + { "from": "n_ar", "to": "n_recon1" }, + { "from": "n_bill", "to": "n_bad" }, + { "from": "n_recon1", "to": "n_bad" }, + { "from": "n_bad", "to": "n_ver1" }, + { "from": "n_deliver", "to": "n_wh" }, + { "from": "n_so", "to": "n_plan", "style": "dashed" }, + + { "from": "n_quote", "to": "n_design" }, + { "from": "n_design", "to": "n_route" }, + { "from": "n_design", "to": "n_product" }, + { "from": "n_design", "to": "n_bom" }, + + { "from": "n_shop", "to": "n_plan" }, + { "from": "n_plan", "to": "n_wo" }, + { "from": "n_wo", "to": "n_pool" }, + { "from": "n_wo", "to": "n_issue" }, + { "from": "n_issue", "to": "n_qc" }, + { "from": "n_qc", "to": "n_receipt" }, + { "from": "n_wo", "to": "n_wh" }, + { "from": "n_issue", "to": "n_wh" }, + { "from": "n_receipt", "to": "n_wh" }, + { "from": "n_wh", "to": "n_out_oth" }, + { "from": "n_wh", "to": "n_transfer" }, + { "from": "n_wh", "to": "n_count" }, + { "from": "n_wh", "to": "n_in_oth" }, + + { "from": "n_vendor", "to": "n_preq" }, + { "from": "n_preq", "to": "n_po" }, + { "from": "n_po", "to": "n_grn" }, + { "from": "n_grn", "to": "n_ap" }, + { "from": "n_grn", "to": "n_wh" }, + { "from": "n_ap", "to": "n_recon2" }, + { "from": "n_ap", "to": "n_ininv" }, + { "from": "n_recon2", "to": "n_getinv" }, + { "from": "n_ininv", "to": "n_getinv" }, + { "from": "n_ininv", "to": "n_ver2" }, + { "from": "n_getinv", "to": "n_ver2" }, + + { "from": "n_fin", "to": "n_cash" }, + { "from": "n_cash", "to": "n_ver1" }, + { "from": "n_fin", "to": "n_pay" }, + { "from": "n_pay", "to": "n_ver2" } + ] +} diff --git a/yuthon_bpd/static/src/bpd_view.html b/yuthon_bpd/static/src/bpd_view.html new file mode 100644 index 0000000..600a48b --- /dev/null +++ b/yuthon_bpd/static/src/bpd_view.html @@ -0,0 +1,814 @@ + + + + + +业务流程图 + + + +
+
+ 业务流程图 + 加载中… + + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+

节点详情

+ +
+ +
+ +
+
+
+ + + + + + + diff --git a/yuthon_bpd/static/src/img/flow.png b/yuthon_bpd/static/src/img/flow.png new file mode 100644 index 0000000..4195720 Binary files /dev/null and b/yuthon_bpd/static/src/img/flow.png differ diff --git a/yuthon_bpd/views/bpd_views.xml b/yuthon_bpd/views/bpd_views.xml new file mode 100644 index 0000000..4612c69 --- /dev/null +++ b/yuthon_bpd/views/bpd_views.xml @@ -0,0 +1,85 @@ + + + + + yuthon.bpd.diagram.form + yuthon.bpd.diagram + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + yuthon.bpd.diagram.list + yuthon.bpd.diagram + + + + + + + + + + + + 流程图 + yuthon.bpd.diagram + list,form + + +
diff --git a/yuthon_bpd/views/menu.xml b/yuthon_bpd/views/menu.xml new file mode 100644 index 0000000..d60ebec --- /dev/null +++ b/yuthon_bpd/views/menu.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + 流程图视图 + /yuthon_bpd/static/src/bpd_view.html + new + + +