Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c20cb81
[MIG] web_diagram: move from odoo
mathben Mar 26, 2026
a7e0669
[FIX] web_diagram: fix qunit test asset injection for Odoo 14
mathben Mar 26, 2026
61d2ff2
[FIX] web_diagram: register diagram as valid ir.ui.view type
mathben Mar 26, 2026
17855c2
[FIX] web_diagram: validate node/arrow fields against their own model
mathben Mar 26, 2026
1efee67
[FIX] web_diagram: fix BasicView field processing for Odoo 14
mathben Mar 26, 2026
b4557ea
[FIX] web_diagram: restore graph_get removed in Odoo 14
mathben Mar 26, 2026
92480c0
[FIX] web_diagram: bundle graph layout class removed from odoo.tools
mathben Mar 26, 2026
f571639
[MIG] web_diagram: migrate asset declarations to Odoo 15
mathben Mar 26, 2026
ba72df6
[FIX] web_diagram: adapt view postprocessing for Odoo 15
mathben Mar 26, 2026
e90dede
[FIX] web_diagram: javascript change parameter to get id object
mathben Mar 26, 2026
178654b
[MIG] web_diagram: migrate JS and assets to Odoo 16
mathben Mar 26, 2026
7738d57
[ADD] web_diagram: add readme documentation
mathben Mar 27, 2026
a25af16
[ADD] web_diagram_builder: initial module
mmaanneell May 5, 2026
f969f6d
[ADD] web_diagram_builder: CSV import/export and node/link detail views
mmaanneell May 5, 2026
521f513
[ADD] web_diagram_builder: French (fr_CA) translations
mmaanneell May 5, 2026
ae2471b
[ADD] web_diagram_builder: in-app tutorial (help wizard + Quick Guide…
mmaanneell May 5, 2026
8ebc4ae
[ADD] web_diagram_builder: find path between two nodes
mmaanneell May 5, 2026
eae31ef
[ADD] web_diagram_builder: add Python unit tests
mmaanneell May 5, 2026
ed4c335
[MIG] web_diagram: migrate Cytoscape-based diagram view to Odoo 16
mmaanneell May 5, 2026
5a29329
[ADD] web_diagram: add navigation help popup with bilingual support
mmaanneell May 5, 2026
b77dc2f
[ADD] web_diagram: add Python unit tests for view validation
mmaanneell May 5, 2026
f87ca39
[IMP] web_diagram_builder: OCA compliance metadata and readme
mmaanneell May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions web_diagram/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, SUPERUSER_ID
from . import controllers, models


def post_init_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
langs = env["res.lang"].search([("active", "=", True), ("code", "!=", "en_US")]).mapped("code")
if langs:
module = env["ir.module.module"].search([("name", "=", "web_diagram")])
module._update_translations(filter_lang=langs)
37 changes: 37 additions & 0 deletions web_diagram/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Odoo Web Diagram',
'category': 'Hidden',
'description': """
Openerp Web Diagram view.
=========================

""",
'version': "16.0.1.0.0",
'depends': ['web'],
'post_init_hook': 'post_init_hook',
'data': [
'security/ir.model.access.csv',
'views/diagram_nav_help_views.xml',
],
'assets': {
'web.assets_backend': [
'web_diagram/static/lib/js/cytoscape.min.js',
'web_diagram/static/lib/js/dagre.min.js',
'web_diagram/static/lib/js/cytoscape-dagre.min.js',
'web_diagram/static/src/scss/diagram_view.scss',
'web_diagram/static/src/js/diagram_model.js',
'web_diagram/static/src/js/diagram_controller.js',
'web_diagram/static/src/js/diagram_renderer.js',
'web_diagram/static/src/js/diagram_view.js',
'web_diagram/static/src/xml/base_diagram.xml',
],
'web.qunit_suite_tests': [
'web_diagram/static/tests/diagram_tests.js',
],
},
'auto_install': True,
'license': 'LGPL-3',
'test': ['tests/test_ir_ui_view.py'],
}
3 changes: 3 additions & 0 deletions web_diagram/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import main
194 changes: 194 additions & 0 deletions web_diagram/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import deque

import odoo.http as http

from odoo.tools.safe_eval import safe_eval

# Spacing between nodes
NODE_W = 150 # horizontal gap between nodes
NODE_H = 110 # vertical gap between levels
MAX_COLS = 8 # max nodes per row before wrapping within a level


def _parse_key_value_spec(spec_str):
"""Parse a semicolon-separated list of 'key:value' pairs into a dict.
Malformed pairs (missing colon) are silently skipped.
"""
result = {}
for item in (spec_str or '').split(';'):
if ':' in item:
key, value = item.split(':', 1)
result[key] = value
return result


def _tree_layout(nodes, transitions):
"""Compact top-down tree layout.

- Parents appear above their children.
- Siblings of the same parent are grouped together horizontally.
- Each depth level is capped at MAX_COLS columns; extra nodes wrap to
a sub-row within the same level (vertical scroll only, no wide canvas).
- Isolated nodes (no edges) are placed at the very bottom.
"""
# Build children map and track which nodes have a parent
children = {}
has_parent = set()

for _tr_id, (src_id, dst_id) in transitions.items():
src_str = str(src_id)
dst_str = str(dst_id)
children.setdefault(src_str, [])
if dst_str not in children[src_str]:
children[src_str].append(dst_str)
has_parent.add(dst_str)

all_ids = set(nodes.keys())
roots = [nid for nid in all_ids if nid not in has_parent]

# BFS to build depth and BFS order (siblings grouped by parent)
depth = {}
visited = set()
queue = deque()
for root in roots:
depth[root] = 0
queue.append(root)
visited.add(root)

bfs_order = []
while queue:
nid = queue.popleft()
bfs_order.append(nid)
for child in children.get(nid, []):
if child not in visited:
visited.add(child)
depth[child] = depth[nid] + 1
queue.append(child)

# Group nodes by depth level, preserving BFS order
# (so siblings of the same parent are consecutive)
levels = {}
for nid in bfs_order:
d = depth[nid]
levels.setdefault(d, [])
levels[d].append(nid)

# Assign positions level by level
current_y = 0
for d in sorted(levels.keys()):
level_nodes = levels[d]
n_rows = max(1, (len(level_nodes) + MAX_COLS - 1) // MAX_COLS)

for i, nid in enumerate(level_nodes):
col = i % MAX_COLS
wrap_row = i // MAX_COLS
nodes[nid]['x'] = col * NODE_W
nodes[nid]['y'] = current_y + wrap_row * NODE_H

# Advance y by: one main level gap + extra rows within this level
current_y += NODE_H * n_rows

# Place isolated nodes (not reachable from any root) at the bottom
unplaced = [nid for nid in all_ids if nid not in visited]
for i, nid in enumerate(unplaced):
nodes[nid]['x'] = (i % MAX_COLS) * NODE_W
nodes[nid]['y'] = current_y + NODE_H

return nodes


class DiagramView(http.Controller):

@http.route('/web_diagram/diagram/get_diagram_info', type='json', auth='user')
def get_diagram_info(self, id, model, node, connector,
src_node, des_node, label, **kw):

visible_node_fields = kw.get('visible_node_fields', [])
invisible_node_fields = kw.get('invisible_node_fields', [])
node_fields_string = kw.get('node_fields_string', [])
connector_fields = kw.get('connector_fields', [])
connector_fields_string = kw.get('connector_fields_string', [])

bgcolors = _parse_key_value_spec(kw.get('bgcolor', ''))
shapes = _parse_key_value_spec(kw.get('shape', ''))

ir_view = http.request.env['ir.ui.view']
graphs = ir_view.graph_get(int(id), model, node, connector, src_node,
des_node, label, (NODE_W, NODE_H))
nodes = graphs['nodes']
transitions = graphs['transitions']
isolate_nodes = {
blnk_node['id']: blnk_node
for blnk_node in graphs['blank_nodes']
}
y = [t['y'] for t in nodes.values() if t['x'] == 20 and t['y']]
y_max = (y and max(y)) or 120

connectors = {}
list_tr = list(transitions.keys())

for tr in transitions:
connectors.setdefault(tr, {
'id': int(tr),
's_id': transitions[tr][0],
'd_id': transitions[tr][1]
})

connector_model = http.request.env[connector]
data_connectors = connector_model.search([('id', 'in', list_tr)]).read(connector_fields)

for tr in data_connectors:
transition_id = str(tr['id'])
label = graphs['label'][transition_id][1]
t = connectors[transition_id]
t.update(
source=tr[src_node][1],
destination=tr[des_node][1],
options={},
signal=label
)

for i, fld in enumerate(connector_fields):
t['options'][connector_fields_string[i]] = tr[fld]

fields = http.request.env['ir.model.fields']
field = fields.search([('model', '=', model), ('relation', '=', node)], limit=1)
node_act = http.request.env[node]
if field and field.relation_field:
search_acts = node_act.search([(field.relation_field, '=', id)])
else:
search_acts = node_act.browse()
data_acts = search_acts.read(invisible_node_fields + visible_node_fields)

for act in data_acts:
act_id_str = str(act['id'])
n = nodes.get(act_id_str)
if not n:
n = isolate_nodes.get(act['id'], {})
y_max += NODE_H
n.update(x=20, y=y_max)
nodes[act_id_str] = n

n.update(id=act['id'], color='white', options={})

for color, expr in bgcolors.items():
if safe_eval(expr, act):
n['color'] = color

for shape, expr in shapes.items():
if safe_eval(expr, act):
n['shape'] = shape

for i, fld in enumerate(visible_node_fields):
n['options'][node_fields_string[i]] = act[fld]

# Apply compact hierarchical layout
nodes = _tree_layout(nodes, transitions)

name = http.request.env[model].browse(id).display_name
return dict(nodes=nodes,
conn=connectors,
display_name=name,
parent_field=graphs['node_parent_field'])
93 changes: 93 additions & 0 deletions web_diagram/i18n/af.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_diagram
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-08-25 10:26+0000\n"
"Last-Translator: <>\n"
"Language-Team: Afrikaans (http://www.transifex.com/odoo/odoo-9/language/"
"af/)\n"
"Language: af\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:249
#: code:addons/web_diagram/static/src/js/diagram.js:277
#, python-format
msgid "Activity"
msgstr "Aktiwiteit"

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:282
#: code:addons/web_diagram/static/src/js/diagram.js:326
#, python-format
msgid "Create:"
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:220
#, python-format
msgid ""
"Deleting this node cannot be undone.\n"
"It will also delete all connected transitions.\n"
"\n"
"Are you sure ?"
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:238
#, python-format
msgid ""
"Deleting this transition cannot be undone.\n"
"\n"
"Are you sure ?"
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:19
#, python-format
msgid "Diagram"
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:124
#, python-format
msgid "New"
msgstr "Nuwe"

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/xml/base_diagram.xml:5
#, python-format
msgid "New Node"
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:254
#: code:addons/web_diagram/static/src/js/diagram.js:310
#, python-format
msgid "Open: "
msgstr ""

#. module: web_diagram
#. openerp-web
#: code:addons/web_diagram/static/src/js/diagram.js:305
#: code:addons/web_diagram/static/src/js/diagram.js:321
#, python-format
msgid "Transition"
msgstr ""
Loading
Loading