diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..944c18183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +#*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/crnd_web_diagram_plus/README.rst b/crnd_web_diagram_plus/README.rst new file mode 100644 index 000000000..d39ca70fc --- /dev/null +++ b/crnd_web_diagram_plus/README.rst @@ -0,0 +1,82 @@ +Web Diagram Plus +================ + +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +.. |badge3| image:: https://img.shields.io/badge/powered%20by-yodoo.systems-00a09d.png + :target: https://yodoo.systems + +.. |badge5| image:: https://img.shields.io/badge/maintainer-CR&D-purple.png + :target: https://crnd.pro/ + +.. |badge6| image:: https://img.shields.io/badge/GitHub-CRnD_Web_Diagram_Plus-green.png + :target: https://github.com/crnd-inc/crnd-web/tree/11.0/crnd_web_diagram_plus + + +|badge2| |badge5| |badge6| + +This is fork of `web_diagram `__ addon. + +Changes: + +- Applied `Pull Request `__ +- Possible to specify background and foreground colors of nodes (via fields) +- Possible to store node position in database, thus there is no more need + to rearrange flow manually each time you open diagram. + +Usage +''''' + +See real example of usage at `Bureaucrat Helpdesk Lite `__ + +Simple example for internal usage: + +.. code:: xml + + + model.name + diagram_plus + + + + + + + + + + +Launch your own ITSM system in 60 seconds: +'''''''''''''''''''''''''''''''''''''''''' + +Create your own `Bureaucrat ITSM `__ database + +|badge3| + +Bug Tracker +=========== + +Bugs are tracked on `https://crnd.pro/requests `_. +In case of trouble, please report there. + + +Maintainer +'''''''''' +.. image:: https://crnd.pro/web/image/3699/300x140/crnd.png + +Our web site: https://crnd.pro/ + +This module is maintained by the Center of Research & Development company. + +We can provide you further Odoo Support, Odoo implementation, Odoo customization, Odoo 3rd Party development and integration software, consulting services. Our main goal is to provide the best quality product for you. + +For any questions `contact us `__. diff --git a/crnd_web_diagram_plus/__init__.py b/crnd_web_diagram_plus/__init__.py new file mode 100644 index 000000000..91c5580fe --- /dev/null +++ b/crnd_web_diagram_plus/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/crnd_web_diagram_plus/__manifest__.py b/crnd_web_diagram_plus/__manifest__.py new file mode 100644 index 000000000..f72c0d3b4 --- /dev/null +++ b/crnd_web_diagram_plus/__manifest__.py @@ -0,0 +1,33 @@ +{ + 'name': 'CRnD Web Diagram Plus', + 'category': 'Technical Settings', + 'summary': """ + Odoo Web Diagram view by CRnD. + """, + 'author': 'Center of Research and Development', + 'support': 'info@crnd.pro', + 'website': 'https://crnd.pro', + 'license': 'LGPL-3', + 'version': '17.0.0.14.0', + 'depends': [ + 'web', + ], + 'assets': { + 'web.assets_backend': [ + 'crnd_web_diagram_plus/static/src/scss/diagram_view.scss', + 'crnd_web_diagram_plus/static/lib/js/underscore-umd.js', + 'crnd_web_diagram_plus/static/lib/js/raphael-2.0.2/raphael.js', + 'crnd_web_diagram_plus/static/lib/js/vec2.js', + 'crnd_web_diagram_plus/static/lib/js/graph.js', + 'crnd_web_diagram_plus/static/src/js/diagram_arch_parser.js', + 'crnd_web_diagram_plus/static/src/js/diagram_model.js', + 'crnd_web_diagram_plus/static/src/js/diagram_controller.js', + 'crnd_web_diagram_plus/static/src/js/diagram_renderer.js', + 'crnd_web_diagram_plus/static/src/js/diagram_view.js', + 'crnd_web_diagram_plus/static/src/xml/base_diagram.xml', + ], + }, + 'images': ['static/description/banner.png'], + 'installable': True, + 'auto_install': False, +} diff --git a/crnd_web_diagram_plus/changelog/changelog.0.6.0.md b/crnd_web_diagram_plus/changelog/changelog.0.6.0.md new file mode 100644 index 000000000..e69de29bb diff --git a/crnd_web_diagram_plus/changelog/changelog.0.9.0.md b/crnd_web_diagram_plus/changelog/changelog.0.9.0.md new file mode 100644 index 000000000..dc47bbb10 --- /dev/null +++ b/crnd_web_diagram_plus/changelog/changelog.0.9.0.md @@ -0,0 +1,9 @@ +Introduced a new feature empowering to determine the node that needs highlighting and assign a custom fill color to it. +Additionally, the diagram now supports a readonly mode. + +To highlight a node, the following values should be passed through the context: +- `highlight_node_id`: The ID of the node object to be highlighted; +- `highlight_node_color`: The color (in HEX or RGB format) to fill the highlighted node + +To enable the readonly mode for the diagram, the following value should be passed through the context: +- `diagram_readonly`: A boolean value indicating whether the diagram should be readonly. diff --git a/crnd_web_diagram_plus/controllers/__init__.py b/crnd_web_diagram_plus/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/crnd_web_diagram_plus/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/crnd_web_diagram_plus/controllers/main.py b/crnd_web_diagram_plus/controllers/main.py new file mode 100644 index 000000000..27b73c664 --- /dev/null +++ b/crnd_web_diagram_plus/controllers/main.py @@ -0,0 +1,223 @@ +import json +import logging +import odoo.http as http + +from odoo.tools.safe_eval import safe_eval + +from ..utils import str2bool + +_logger = logging.getLogger(__name__) + + +class DiagramPlusView(http.Controller): + + def _diagram_plus_view__find_nodes(self, diagram_id, + diagram_model, + node_model): + """ Find nodes for diagram + + :param int diagram_id: ID of record in diagram model to search + nodes for + :param str diagram_model: name of diagram model + :param str node_model: name of model for diagram nodes + :return recordset: nodes related diagram + """ + fields = http.request.env['ir.model.fields'].sudo() + field = fields.search([('model', '=', diagram_model), + ('relation', '=', node_model), + ('ttype', '=', 'one2many')]) + node_act = http.request.env[node_model] + return node_act.search( + [(field.relation_field, '=', diagram_id)]) + + # Just Copy+Paste+Edit of original Odoo's method + # pylint: disable=redefined-builtin,too-many-locals,too-many-statements + # pylint: disable=too-many-branches + @http.route('/web_diagram_plus/diagram/get_diagram_info', + type='json', auth='user') + def get_diagram_info(self, **kw): + res_id = kw.get('id', False) + model = kw.get('model', '') + node = kw.get('node', '') + connector = kw.get('connector', '') + src_node = kw.get('src_node', '') + des_node = kw.get('des_node', '') + label = kw.get('label', '') + 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', []) + diagram_readonly = kw.get('diagram_readonly', False) + + bgcolors = {} + shapes = {} + bgcolor = kw.get('bgcolor', '') + bg_color_field = kw.get('bg_color_field', '') + fg_color_field = kw.get('fg_color_field', '') + shape = kw.get('shape', '') + auto_layout = str2bool(kw.get('auto_layout'), True) + d_position_field = kw.get('d_position_field', False) + calc_auto_layout = str2bool(kw.get('calc_auto_layout'), False) + + init_view = False + x_offset = 50 + y_offset = 50 + + if bgcolor: + for color_spec in bgcolor.split(';'): + if color_spec: + colour, color_state = color_spec.split(':') + bgcolors[colour] = color_state + + if shape: + for shape_spec in shape.split(';'): + if shape_spec: + shape_colour, shape_color_state = shape_spec.split(':') + shapes[shape_colour] = shape_color_state + + ir_view = http.request.env['ir.ui.view'] + graphs = ir_view.crnd_diagram_plus_graph_get( + int(res_id), model, node, connector, src_node, des_node, + label, (140, 180)) + nodes = {} + isolate_nodes = {} + if not auto_layout and not calc_auto_layout: + nodes_data = self._diagram_plus_view__find_nodes( + diagram_id=res_id, diagram_model=model, node_model=node) + for n in nodes_data: + if n[d_position_field]: + nodes[str(n.id)] = { + 'name': n.name, + 'x': json.loads(n[d_position_field])['x'], + 'y': json.loads(n[d_position_field])['y'], + } + else: + isolate_nodes[n.id] = { + 'name': n.name, + } + if not nodes: + init_view = True + nodes = graphs['nodes'] + isolate_nodes = {} + for blnk_node in graphs['blank_nodes']: + isolate_nodes[blnk_node['id']] = blnk_node + + transitions = graphs['transitions'] + + if auto_layout or calc_auto_layout: + y = [ + t['y'] + for t in nodes.values() + if t['x'] == 20 + if t['y'] + ] + else: + y = [ + t['y'] + for t in nodes.values() + if t['y'] + ] + x = [ + t['x'] + for t in nodes.values() + if t['x'] + ] + # pylint: disable=consider-using-ternary + y_max = max(y) if y else 120 + x_min = min(x) - x_offset \ + if x and not auto_layout and not calc_auto_layout else 20 + + connectors = {} + list_tr = [] + + for tr in transitions: + list_tr.append(tr) + 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']) + _sourceid, label = graphs['label'][transition_id] + 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] + + # CRND FIX: restrict field by type (move computation to separate meth) + search_acts = self._diagram_plus_view__find_nodes( + diagram_id=res_id, diagram_model=model, node_model=node) + # CRND FIX END + data_acts = search_acts.read( + invisible_node_fields + visible_node_fields) + + for act in data_acts: + n = nodes.get(str(act['id'])) + if not n: + n = isolate_nodes.get(act['id'], {}) + y_max += 140 + n.update( + x=x_min + ( + 0 if auto_layout or calc_auto_layout else x_offset), + y=y_max) + nodes[str(act['id'])] = n + + n.update( + id=act['id'], + color='white', + options={} + ) + # -- CRND FIX: to set color of node bg and fg + if not bg_color_field: + for color, expr in bgcolors.items(): + if safe_eval(expr, act): + n['color'] = color + else: + n['color'] = act[bg_color_field] + n['fgcolor'] = act.get(fg_color_field, False) + # -- + + 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] + + if init_view and not auto_layout: + for key, n in nodes.items(): + n.update( + x=int(n['x']) + x_offset, + y=int(n['y']) + y_offset, + ) + if not auto_layout and not diagram_readonly: + http.request.env[node].browse([int(key)]).write({ + d_position_field: json.dumps({ + 'x': n['x'], + 'y': n['y'], + }) + }) + model_obj = http.request.env[model].sudo().browse([res_id])[0] + _id, name = model_obj.id, model_obj.display_name + highlight_node_id = kw.get('highlight_node_id') + if highlight_node_id: + highlight_node = nodes.get(str(highlight_node_id)) + highlight_color = kw.get('highlight_node_color', False) + highlight_node['highlight_node_color'] = highlight_color + return dict(nodes=nodes, + conn=connectors, + display_name=name, + parent_field=graphs['node_parent_field']) diff --git a/crnd_web_diagram_plus/i18n/crnd_web_diagram_plus.pot b/crnd_web_diagram_plus/i18n/crnd_web_diagram_plus.pot new file mode 100644 index 000000000..4619038d2 --- /dev/null +++ b/crnd_web_diagram_plus/i18n/crnd_web_diagram_plus.pot @@ -0,0 +1,152 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Confirm" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Delete" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Do you really want to change the positions of the nodes?" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "" diff --git a/crnd_web_diagram_plus/i18n/de.po b/crnd_web_diagram_plus/i18n/de.po new file mode 100644 index 000000000..66922083e --- /dev/null +++ b/crnd_web_diagram_plus/i18n/de.po @@ -0,0 +1,124 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-10-20 12:32+0000\n" +"PO-Revision-Date: 2020-10-20 12:32+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "" diff --git a/crnd_web_diagram_plus/i18n/fr.po b/crnd_web_diagram_plus/i18n/fr.po new file mode 100644 index 000000000..e0d63c93b --- /dev/null +++ b/crnd_web_diagram_plus/i18n/fr.po @@ -0,0 +1,124 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-10-20 12:33+0000\n" +"PO-Revision-Date: 2020-10-20 12:33+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "" diff --git a/crnd_web_diagram_plus/i18n/lt.po b/crnd_web_diagram_plus/i18n/lt.po new file mode 100644 index 000000000..0808e5838 --- /dev/null +++ b/crnd_web_diagram_plus/i18n/lt.po @@ -0,0 +1,124 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-12-22 14:52+0000\n" +"PO-Revision-Date: 2021-12-22 14:52+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "" diff --git a/crnd_web_diagram_plus/i18n/ru.po b/crnd_web_diagram_plus/i18n/ru.po new file mode 100644 index 000000000..746fb9c3d --- /dev/null +++ b/crnd_web_diagram_plus/i18n/ru.po @@ -0,0 +1,169 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-16 10:21+0000\n" +"PO-Revision-Date: 2019-09-16 10:21+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "Вид окна действия" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "Деятельность" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "Вы уверенны что хотите удалить этот узел? Это удалит все его связи." + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "Вы уверены что хотите удалить это соединение?" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "Создать" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "Новый узел" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "Открыть:" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "Связь" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "Вид" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "Тип представления" + +#~ msgid "Field %r used in attributes must be present in view but is missing:" +#~ msgstr "" +#~ "Поле %r, используемое в атрибутах, должно присутствовать в файле " +#~ "отображения:" + +#~ msgid "Field `%(field_name)s` does not exist" +#~ msgstr "Поле `%(field_name)s` не существует" + +#~ msgid "" +#~ "Field `%(field_name)s` must be present in diagram_plus[node][field], " +#~ "because it present in diagram_plus[node], but is missing." +#~ msgstr "" +#~ "Поле `%(field_name)s` должно быть в diagram_plus[node][field], потому что " +#~ "оно присутствует в diagram_plus[node]." + +#~ msgid "Model not found: %(model)s" +#~ msgstr "Модель не найдена: %(model)s" + +#~ msgid "Calendar" +#~ msgstr "Календарь" + +#~ msgid "Diagram" +#~ msgstr "Диаграмма" + +#~ msgid "Form" +#~ msgstr "Форма" + +#~ msgid "Gantt" +#~ msgstr "Диаграмма Ганта" + +#~ msgid "Graph" +#~ msgstr "График" + +#~ msgid "Kanban" +#~ msgstr "Канбан" + +#~ msgid "Pivot" +#~ msgstr "Центр" + +#~ msgid "Search" +#~ msgstr "Поиск" + +#~ msgid "Tree" +#~ msgstr "Дерево" diff --git a/crnd_web_diagram_plus/i18n/uk.po b/crnd_web_diagram_plus/i18n/uk.po new file mode 100644 index 000000000..ff6594a28 --- /dev/null +++ b/crnd_web_diagram_plus/i18n/uk.po @@ -0,0 +1,169 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-16 10:20+0000\n" +"PO-Revision-Date: 2019-09-16 10:20+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "Перегляд вікна дії" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "Діяльність" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "Ви впевнені що хочете видалити цей вузол? Це також видалить звʼязки." + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "Ви впевнені що хочете видалити це зʼєднання" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "Створити" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "Новий вузол" + +#. module: crnd_web_diagram_plus +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "Відкрити:" + +#. module: crnd_web_diagram_plus +#. openerp-web +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "Звʼязок" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "Перегляд" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "Тип перегляду" + +#~ msgid "Field %r used in attributes must be present in view but is missing:" +#~ msgstr "" +#~ "Поле %r, яке використовується в атрибутах, повинно бути присутнім у файлі " +#~ "відображення:" + +#~ msgid "Field `%(field_name)s` does not exist" +#~ msgstr "Поле `%(field_name)s` не існує" + +#~ msgid "" +#~ "Field `%(field_name)s` must be present in diagram_plus[node][field], " +#~ "because it present in diagram_plus[node], but is missing." +#~ msgstr "" +#~ "Поле `%(field_name)s` має бути присутнім у diagram_plus[node][field], " +#~ "тому що воно присутнє у diagram_plus[node]." + +#~ msgid "Model not found: %(model)s" +#~ msgstr "Модель не знайдена: %(model)s" + +#~ msgid "Calendar" +#~ msgstr "Календар" + +#~ msgid "Diagram" +#~ msgstr "Діаграма" + +#~ msgid "Form" +#~ msgstr "Вікно" + +#~ msgid "Gantt" +#~ msgstr "Діаграма Ґанта" + +#~ msgid "Graph" +#~ msgstr "Графік" + +#~ msgid "Kanban" +#~ msgstr "Канбан" + +#~ msgid "Pivot" +#~ msgstr "Зведена таблиця" + +#~ msgid "Search" +#~ msgstr "Пошук" + +#~ msgid "Tree" +#~ msgstr "Дерево" diff --git a/crnd_web_diagram_plus/i18n/zh_CN.po b/crnd_web_diagram_plus/i18n/zh_CN.po new file mode 100644 index 000000000..f453c8c3d --- /dev/null +++ b/crnd_web_diagram_plus/i18n/zh_CN.po @@ -0,0 +1,152 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crnd_web_diagram_plus +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "动作窗口视图" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "活动" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "您确定要删除此节点吗?这也将删除其链接。" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "您确定要删除此链接吗?" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "自动布局" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "无法计算diagram视图。看起来流程是不正确的!尝试在不使用diagram视图的情况下删除一些损坏的路线。" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Confirm" +msgstr "确认" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "创建:" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Delete" +msgstr "删除" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_actions_act_window_view__view_mode__diagram_plus +#: model:ir.model.fields.selection,name:crnd_web_diagram_plus.selection__ir_ui_view__type__diagram_plus +#, python-format +msgid "DiagramPlus" +msgstr "Diagram Plus" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Do you really want to change the positions of the nodes?" +msgstr "你真的想改变节点的位置吗?" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "字段d_position_field必须出现在diagram_plus[node]中,因为set auto_layout='False'" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "新节点" + +#. module: crnd_web_diagram_plus +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "在'diagram_plus_view'中只允许'node'和'arrow'标记,但是发现了%(tag_name)s。" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "打开:" + +#. module: crnd_web_diagram_plus +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "链接" + +#. module: crnd_web_diagram_plus +#: model:ir.model,name:crnd_web_diagram_plus.model_ir_ui_view +msgid "View" +msgstr "视图" + +#. module: crnd_web_diagram_plus +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:crnd_web_diagram_plus.field_ir_ui_view__type +msgid "View Type" +msgstr "视图类型" diff --git a/crnd_web_diagram_plus/models/__init__.py b/crnd_web_diagram_plus/models/__init__.py new file mode 100644 index 000000000..e17da5b00 --- /dev/null +++ b/crnd_web_diagram_plus/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_ui_view +from . import ir_act_window_view diff --git a/crnd_web_diagram_plus/models/ir_act_window_view.py b/crnd_web_diagram_plus/models/ir_act_window_view.py new file mode 100644 index 000000000..c99262725 --- /dev/null +++ b/crnd_web_diagram_plus/models/ir_act_window_view.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = 'ir.actions.act_window.view' + + view_mode = fields.Selection( + selection_add=[('diagram_plus', 'DiagramPlus')], + ondelete={'diagram_plus': 'cascade'}, + ) diff --git a/crnd_web_diagram_plus/models/ir_ui_view.py b/crnd_web_diagram_plus/models/ir_ui_view.py new file mode 100644 index 000000000..1847c38c9 --- /dev/null +++ b/crnd_web_diagram_plus/models/ir_ui_view.py @@ -0,0 +1,169 @@ +import logging + +from odoo import models, fields, api, tools, _ +from odoo.tools.safe_eval import safe_eval + +from ..utils import str2bool +from ..tools.graph import Graph + +_logger = logging.getLogger(__name__) + + +class IrUiView(models.Model): + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=translation-positional-used + # pylint: disable=too-many-statements + + _inherit = 'ir.ui.view' + + type = fields.Selection( + selection_add=[('diagram_plus', 'DiagramPlus')], + ondelete={'diagram_plus': 'cascade'} + ) + + def _postprocess_tag_node(self, node, name_manager, node_info): + if node.get('bg_color_field'): + name_manager.has_field(node, node.get('bg_color_field'), {}) + if node.get('fg_color_field'): + name_manager.has_field(node, node.get('fg_color_field'), {}) + if node.get('d_position_field'): + name_manager.has_field(node, node.get('d_position_field'), {}) + for child in node: + if child.tag == 'field': + name_manager.has_field(node, child.get('name'), {}) + node.remove(child) + + def _postprocess_tag_arrow(self, node, name_manager, node_info): + if node.get('source'): + name_manager.has_field(node, node.get('source'), {}) + if node.get('destination'): + name_manager.has_field(node, node.get('destination'), {}) + for child in node: + if child.tag == 'field': + name_manager.has_field(node, child.get('name'), {}) + node.remove(child) + + def _postprocess_tag_diagram_plus(self, node, name_manager, node_info): + # Here we store children in node_info, because we need to avoid futher + # post-processing of arrow/node nodes in context of diagram model. + node_info['children'] = [] + for child in node: + if child.tag == 'arrow': + self.with_context( + base_model_name=name_manager.model._name, + )._postprocess_view( + child, child.get('object'), + editable=node_info['editable'], + ) + elif child.tag == 'node': + sub_name_manager = self.with_context( + base_model_name=name_manager.model._name, + )._postprocess_view( + child, child.get('object'), + editable=node_info['editable'], + ) + has_create_access = sub_name_manager.model.check_access_rights( + 'create', raise_exception=False) + if not node.get('create') and not has_create_access: + node.set('create', 'false') + + def _validate_tag_diagram_plus(self, node, name_manager, node_info): + # Auto layout have to be enabled by default, but could be disabled + # with attribute on node + auto_layout = str2bool(node.attrib.get('auto_layout'), True) + for child in node: + if child.tag not in ("arrow", "node"): + msg = _( + "Only 'node' and 'arrow' tags allowed in " + "'diagram_plus_view', but %(tag_name)s found.", + ) % {'tag_name': child.tag} + self._raise_view_error(msg) + if child.tag == "node" and not auto_layout: + d_position_field = \ + child.get('d_position_field', False) + if not d_position_field: + message = _( + "Field d_position_field must be present in" + " diagram_plus[node], because set auto_layout='False'" + ) + self._raise_view_error(message) + + @api.model + def crnd_diagram_plus_graph_get(self, record_id, model, node_obj, conn_obj, + src_node, des_node, label, scale): + def rec_name(rec): + return (rec.name if 'name' in rec else + rec.x_name if 'x_name' in rec else + None) + + nodes = [] + nodes_name = [] + transitions = [] + start = [] + tres = {} + labels = {} + no_ancester = [] + blank_nodes = [] + + Model = self.sudo().env[model] + Node = self.sudo().env[node_obj] + + # pylint: disable=too-many-nested-blocks + for model_key, model_value in Model._fields.items(): + if model_value.type == 'one2many': + if model_value.comodel_name == node_obj: + _Node_Field = model_key + _Model_Field = model_value.inverse_name + for node_key, node_value in Node._fields.items(): + if node_value.type == 'one2many': + if node_value.comodel_name == conn_obj: + # _Source_Field = "Incoming Arrows" + # (connected via des_node) + if node_value.inverse_name == des_node: + _Source_Field = node_key + # _Destination_Field = "Outgoing Arrows" + # (connected via src_node) + if node_value.inverse_name == src_node: + _Destination_Field = node_key + + record = Model.browse(record_id) + for line in record[_Node_Field]: + if line[_Source_Field] or line[_Destination_Field]: + nodes_name.append((line.id, rec_name(line))) + nodes.append(line.id) + else: + blank_nodes.append({'id': line.id, 'name': rec_name(line)}) + + if 'flow_start' in line and line.flow_start: + start.append(line.id) + elif not line[_Source_Field]: + no_ancester.append(line.id) + + for t in line[_Destination_Field]: + transitions.append((line.id, t[des_node].id)) + tres[str(t['id'])] = (line.id, t[des_node].id) + label_string = "" + if label: + for lbl in safe_eval(label): + if (tools.ustr(lbl) in t and + tools.ustr(t[lbl]) == 'False'): + label_string += ' ' + else: + label_string = ( + label_string + " " + tools.ustr(t[lbl])) + labels[str(t['id'])] = (line.id, label_string) + + g = Graph(nodes, transitions, no_ancester) + g.process(start) + g.scale(*scale) + result = g.result_get() + results = {} + for node_id, node_name in nodes_name: + results[str(node_id)] = result[node_id] + results[str(node_id)]['name'] = node_name + return {'nodes': results, + 'transitions': tres, + 'label': labels, + 'blank_nodes': blank_nodes, + 'node_parent_field': _Model_Field} diff --git a/crnd_web_diagram_plus/static/description/banner.png b/crnd_web_diagram_plus/static/description/banner.png new file mode 100644 index 000000000..b08940fee Binary files /dev/null and b/crnd_web_diagram_plus/static/description/banner.png differ diff --git a/crnd_web_diagram_plus/static/description/color_diagram.png b/crnd_web_diagram_plus/static/description/color_diagram.png new file mode 100644 index 000000000..39042c0e9 Binary files /dev/null and b/crnd_web_diagram_plus/static/description/color_diagram.png differ diff --git a/crnd_web_diagram_plus/static/description/icon.png b/crnd_web_diagram_plus/static/description/icon.png new file mode 100644 index 000000000..0d2ff45fa Binary files /dev/null and b/crnd_web_diagram_plus/static/description/icon.png differ diff --git a/crnd_web_diagram_plus/static/description/index.html b/crnd_web_diagram_plus/static/description/index.html new file mode 100644 index 000000000..a79323ba8 --- /dev/null +++ b/crnd_web_diagram_plus/static/description/index.html @@ -0,0 +1,83 @@ +
+
+

CRnD Web Diagram Plus

+
+
+

License: LGPL-3 CR&D GitHub

+


+
+
+ +
+
+

CR&D fork of web_diagram addon.

+

Changes:

+
    +
  • Applied the following Pull Request that fixes an error on the diagram view. It appears when the diagram object has 2 fields (one2many + many2one) that points to related nodes.
  • +
  • Added possibility to specify background and foreground colors of nodes (via fields).
  • +
  • Possible to store node position in database, thus there is no more need + to rearrange flow manually each time you open diagram.
  • +
+


+

An example of internal usage:

+
+    <record id="some_id" model="ir.ui.view">
+        <field name="model">model.name</field>
+        <field name="type">diagram_plus</field>
+        <field name="arch" type="xml">
+            <diagram_plus>
+                <node object="name.of.model"
+                      bgcolor="from_old_diagram_non_priority"
+                      bg_color_field="name_of_field_bg_color"
+                      fg_color_field="name_of_field_fg_color"
+                      d_position_field="name_of_field_to_store_position">
+                </node>
+                <arrow object="name.of.model"
+                       source="source_field(from)"
+                       destination="destination_field(to)"
+                       label="['name_of_label_field']">
+                </arrow>
+            </diagram_plus>
+        </field>
+    </record>
+
+
+colored diagram +


+
+
+
+ +
+
+

Launch your own ITSM system in 60 seconds:

+

Create your own Bureaucrat ITSM database +yodoo.systems +

+
+
+ +
+
+

Maintainer

+CR&D + + + +

This module is maintained by the Center of Research & Development +company. +

+

+We can provide you further Odoo Support, Odoo implementation, Odoo customization, Odoo 3rd Party development and integration software, consulting services. Our main goal is to provide the best quality product for you. +

+
+
diff --git a/crnd_web_diagram_plus/static/src/js/diagram_arch_parser.js b/crnd_web_diagram_plus/static/src/js/diagram_arch_parser.js new file mode 100644 index 000000000..eac5dc7b2 --- /dev/null +++ b/crnd_web_diagram_plus/static/src/js/diagram_arch_parser.js @@ -0,0 +1,70 @@ +/** @odoo-module **/ + +import {visitXML} from "@web/core/utils/xml"; +import {archParseBoolean, getActiveActions} from "@web/views/utils"; + +export class DiagramPlusArchParser { + parse(xmlDoc, resModel, resId, fields = {}) { + let archInfo = { + fields, fieldAttrs: {}, + groupBy: [], + measures: [], + domain: [], + resModel, + resId, + }; + let self = this; + + visitXML(xmlDoc, (node) => { + if (node.tagName === 'diagram_plus') { + let labels = Array.from(node.children) + .filter(child => child.tagName === 'LABEL') + .map(label => label.getAttribute('string')); + archInfo = Object.assign(archInfo, { + activeActions: getActiveActions(node), + labels: labels, + auto_layout: 'auto_layout' in node.attributes ? archParseBoolean(node.attributes.auto_layout.value) : true, + }); + } else if (node.tagName === 'node') { + let invisible_nodes = []; + let visible_nodes = []; + let node_fields_string = []; + Array.from(node.children).forEach(function (child) { + if (child.getAttribute('invisible') === '1') { + invisible_nodes.push(child.getAttribute('name')); + } else { + let fieldString = fields[child.getAttribute('name')]?.string || self.toTitleCase(child.getAttribute('name')); + visible_nodes.push(child.getAttribute('name')); + node_fields_string.push(fieldString); + } + }); + + archInfo = Object.assign(archInfo, { + nodes: node, + node_model: node.attributes.object.value, + invisible_nodes, + visible_nodes, + node_fields_string, + }); + } else if (node.tagName === 'arrow') { + let connector_fields_string = Array.from(node.children).map(function (conn) { + return fields[conn.getAttribute('name')]?.string || self.toTitleCase(conn.getAttribute('name')); + }); + archInfo = Object.assign(archInfo, { + connectors: node, + connector_model: node.attributes.object.value, + connector_fields_string + }); + } + }); + + return archInfo; + } + + toTitleCase(str) { + return str.replace(/\b\w/g, function (txt) { + return txt.toUpperCase(); + }); + } + +} diff --git a/crnd_web_diagram_plus/static/src/js/diagram_controller.js b/crnd_web_diagram_plus/static/src/js/diagram_controller.js new file mode 100644 index 000000000..7deb7445d --- /dev/null +++ b/crnd_web_diagram_plus/static/src/js/diagram_controller.js @@ -0,0 +1,207 @@ +/** @odoo-module **/ + +import {sprintf} from "@web/core/utils/strings"; +import {_t} from "@web/core/l10n/translation"; +import {Layout} from "@web/search/layout"; +import {useModelWithSampleData} from "@web/model/model"; +import {standardViewProps} from "@web/views/standard_view_props"; +import {useSetupView} from "@web/views/view_hook"; +import {FormViewDialog} from "@web/views/view_dialogs/form_view_dialog"; +import {useOwnedDialogs, useService} from "@web/core/utils/hooks"; +import {Component, useSubEnv, useRef, useState} from "@odoo/owl"; +import {uuid} from "@web/views/utils"; +import { + ConfirmationDialog +} from "@web/core/confirmation_dialog/confirmation_dialog"; + +function useUniqueDialog() { + const displayDialog = useOwnedDialogs(); + let close = null; + return (...args) => { + if (close) { + close(); + } + close = displayDialog(...args); + }; +} + +/** + * Diagram Controller + */ +export class DiagramPlusController extends Component { + setup() { + useSubEnv({ + custom_events: { + add_edge: this._onAddEdge.bind(this), + edit_edge: this._onEditEdge.bind(this), + edit_node: this._onEditNode.bind(this), + remove_edge: this._onRemoveEdge.bind(this), + remove_node: this._onRemoveNode.bind(this), + change_node_position: this._onChangeNodePosition.bind(this), + } + }); + this.dialog = useService("dialog"); + this.displayDialog = useUniqueDialog(); + this.state = useState({ + uuid: uuid() + }) + this.model = useState(useModelWithSampleData(this.props.Model, this.props.modelParams)); + useSetupView({ + rootRef: useRef("root"), + getContext: () => this.props.context, + }); + } + + async handleDataChange() { + await this.model.reload() + this.state.uuid = uuid(); + } + + async _addNode(event) { + let {node_model} = this.model.get(); + return new Promise((resolve) => { + this.displayDialog( + FormViewDialog, { + resModel: node_model, + resId: false, + context: this.props.context || {}, + title: sprintf("%s %s", _t("Create:"), _t('Activity')), + onRecordSaved: async () => { + await this.handleDataChange() + }, + }, {onClose: () => resolve()} + ); + }); + } + + _autoLayout() { + let self = this; + return this.dialog.add(ConfirmationDialog, { + body: _t("Do you really want to change the positions of the nodes?"), + confirmLabel: _t("Confirm"), + confirm: async () => { + self.model.calc_auto_layout = true; + await this.handleDataChange() + }, + cancel: () => { + }, + }); + } + + _onRemoveNode(record_id) { + let {node_model} = this.model.get(); + let self = this; + return this.dialog.add(ConfirmationDialog, { + body: _t("Are you sure you want to remove this node? This will remove its connected transitions as well."), + confirmLabel: _t("Delete"), + confirm: async () => { + await self.model.orm.call(node_model, "unlink", [record_id]) + await this.handleDataChange() + }, + cancel: () => { + }, + }); + } + + _onEditNode(record_id) { + let {diagram_readonly, node_model} = this.model.get(); + if (diagram_readonly) { + return + } + return new Promise((resolve) => { + this.displayDialog( + FormViewDialog, { + resModel: node_model, + resId: record_id, + context: this.context, + title: sprintf("%s %s", _t("Open:"), _t('Activity')), + onRecordSaved: async () => { + await this.handleDataChange() + }, + }, {onClose: () => resolve()} + ); + }); + } + + async _onChangeNodePosition(node) { + let {node_model} = this.model.get(); + let d_position_field = $(this.model.nodes).attr('d_position_field'); + let node_position = JSON.stringify(node.get_pos()); + let values = {}; + values[d_position_field] = node_position; + await this.model.orm.call(node_model, "write", [[node.id], values]) + return this.handleDataChange() + } + + _onAddEdge(data) { + let {diagram_readonly, connectors, connector_model} = this.model.get(); + if (diagram_readonly) { + return false + } + let ctx = this.props.context || {} + ctx[`default_${$(connectors).attr('source')}`] = data.source_id; + ctx[`default_${$(connectors).attr('destination')}`] = data.dest_id; + return new Promise((resolve) => { + this.displayDialog( + FormViewDialog, { + resModel: connector_model, + resId: false, + context: ctx, + title: sprintf("%s %s", _t("Create:"), _t('Transition')), + onRecordSaved: async () => { + await this.handleDataChange() + }, + }, {onClose: () => resolve()} + ); + }); + } + + _onRemoveEdge(record_id) { + let {diagram_readonly, connector_model} = this.model.get(); + if (diagram_readonly) { + return + } + let self = this; + return this.dialog.add(ConfirmationDialog, { + body: _t("Are you sure you want to remove this transition?"), + confirmLabel: _t("Delete"), + confirm: async () => { + await self.model.orm.call(connector_model, "unlink", [record_id]) + await this.handleDataChange() + }, + cancel: () => { + }, + }); + } + + _onEditEdge(record_id) { + let {diagram_readonly, connector_model} = this.model.get(); + + if (diagram_readonly) { + return + } + return new Promise((resolve) => { + this.displayDialog( + FormViewDialog, { + resModel: connector_model, + resId: record_id, + context: this.context, + title: sprintf("%s %s", _t("Open:"), _t('Transition')), + onRecordSaved: async () => { + await this.handleDataChange() + }, + }, {onClose: () => resolve()} + ); + }); + } +} + +DiagramPlusController.template = "DiagramPlusView.Controller"; +DiagramPlusController.components = {Layout}; +DiagramPlusController.props = { + ...standardViewProps, + Model: Function, + modelParams: Object, + Renderer: Function, + buttonTemplate: String, +}; diff --git a/crnd_web_diagram_plus/static/src/js/diagram_model.js b/crnd_web_diagram_plus/static/src/js/diagram_model.js new file mode 100644 index 000000000..888a96652 --- /dev/null +++ b/crnd_web_diagram_plus/static/src/js/diagram_model.js @@ -0,0 +1,110 @@ +/** @odoo-module **/ +import {KeepLast, Race} from "@web/core/utils/concurrency"; +import {Model} from "@web/model/model"; +import { useService } from "@web/core/utils/hooks"; +/** + * DiagramModel + */ +export class DiagramPlusModel extends Model { + setup(params) { + this.rpc = useService('rpc'); + this.keepLast = new KeepLast(); + this.race = new Race(); + this.params = params; + const _fetchDiagramInfo = this._fetchDiagramInfo.bind(this); + this._fetchDiagramInfo = (...args) => { + return this.race.add(_fetchDiagramInfo(...args)); + }; + } + get() { + let data = $.extend(true, {}, { + labels: this.labels, + nodes: this.datanodes, + edges: this.edges, + node_model: this.node_model, + parent_field: this.parent_field, + resId: this.resId, + connector_model: this.connector_model, + connectors: this.connectors, + auto_layout: this.auto_layout, + diagram_readonly: this.diagram_readonly, + }); + return data; + } + + async load(searchParams) { + let {params} = this; + this.blocks = [{'name': 'one'}, {'name': 'two'}, {'name': 'three'}]; + this.searchParams = searchParams; + this.modelName = params.resModel; + this.res_id = params.resId; + this.node_model = params.node_model; + this.connector_model = params.connector_model; + this.connectors = params.connectors; + this.nodes = params.nodes; + this.visible_nodes = params.visible_nodes; + this.invisible_nodes = params.invisible_nodes; + this.node_fields_string = params.node_fields_string; + this.connector_fields_string = params.connector_fields_string; + this.labels = params.labels; + this.auto_layout = params.auto_layout; + this.diagram_readonly = searchParams.context.diagram_readonly; + this.active_actions = params.activeActions; + // fetch from context, passed through action following values + this.highlight_node_id = searchParams.context.highlight_node_id; + this.highlight_node_color = searchParams.context.highlight_node_color; + await this._fetchDiagramInfo(); + } + + async reload() { + await this._fetchDiagramInfo(); + this.blocks = [{'name': 'one'}, {'name': 'two'}, {'name': 'three'}, {'name': 'four'}]; + } + + getFetchParams() { + let {source, destination, label} = this.connectors.attributes; + let {bgcolor, bg_color_field, fg_color_field, shape, d_position_field} = this.nodes.attributes; + let fetchParams = { + id: this.res_id, + model: this.modelName, + node: this.node_model, + connector: this.connector_model, + src_node: source?.value, + des_node: destination?.value, + label: label?.value, + bgcolor: bgcolor?.value, + bg_color_field: bg_color_field?.value, + fg_color_field: fg_color_field?.value, + shape: shape?.value, + d_position_field: d_position_field?.value, + visible_nodes: this.visible_nodes, + invisible_nodes: this.invisible_nodes, + node_fields_string: this.node_fields_string, + connector_fields_string: this.connector_fields_string, + auto_layout: this.auto_layout, + calc_auto_layout: this.calc_auto_layout || false, + diagram_readonly: this.diagram_readonly, + // In order to assign a highlight color to a specific node, + // pass variables to the controller, enabling the identification + // of the highlight node. Once the highlight node is determined, + // the assigned highlight color can be applied to it. + // Additionally, include the "diagram" readonly attribute to prevent + // controller from executing write actions when in readonly mode. + highlight_node_id: this.highlight_node_id, + highlight_node_color: this.highlight_node_color, + } + return fetchParams; + } + async _loadDiagramInfo() { + let params = this.getFetchParams(); + return this.rpc('/web_diagram_plus/diagram/get_diagram_info', params) + } + + async _fetchDiagramInfo() { + let data = await this.keepLast.add(this._loadDiagramInfo()); + this.calc_auto_layout = false; + this.datanodes = data.nodes; + this.edges = data.conn; + this.parent_field = data.parent_field; + } +} diff --git a/crnd_web_diagram_plus/static/src/js/diagram_renderer.js b/crnd_web_diagram_plus/static/src/js/diagram_renderer.js new file mode 100644 index 000000000..4a862449c --- /dev/null +++ b/crnd_web_diagram_plus/static/src/js/diagram_renderer.js @@ -0,0 +1,212 @@ +/** @odoo-module **/ + +import { + Graph as CuteGraphPlus, + GraphNode as CuteNodePlus, + GraphEdge as CuteEdgePlus, + wordwrap as CuteGraphPlus_wordwrap, +} from '../../lib/js/graph'; +import { + Component, + onMounted, + onWillUnmount, + useState +} from "@odoo/owl"; +import {useService} from "@web/core/utils/hooks"; + +/** + * Diagram Renderer + * + * The diagram renderer responsability is to render a diagram view, + * that is, a set of (labelled) nodes and edges. + * To do that, it uses the Raphael.js + * library. + */ + +export class DiagramPlusRenderer extends Component { + setup() { + this.uiService = useService("ui"); + this.model = this.props.model; + this.node_size_x = 110; + this.node_size_y = 80; + this.diagram_padding = 20; + this.diagram_in_dom = false; + this.diagram_offset = this.props.model.auto_layout ? 50 : 0; + this.diagram_readonly = this.props.model.diagram_readonly; + this.state = useState({ + uuid: this.props.uuid, + nodes: this.props.model.datanodes, + edges: this.props.model.edges, + labels: this.props.model.labels, + auto_layout: this.props.model.auto_layout, + }); + this.events + onMounted(async () => { + await Promise.resolve(); + this.$el = $(this.uiService.activeElement).find('.o_diagram_plus_view'); + this.$diagram_container = this.$el.find('.o_diagram_plus'); + this.renderChart() + }); + onWillUnmount(() => { + this.diagram_in_dom = false; + }); + } + renderChart() { + let self = this; + let {nodes, edges} = this.state; + let id_to_node = {}; + let style = this.getDiagramStyle(); + // Remove previous diagram + this.$diagram_container.empty(); + let $div = $('
').css( + { + position: 'absolute', + top: -10000, + right: -10000, + } + ).appendTo($('body')); + let r = new Raphael($div[0], '100%', '100%'); + this.graph = new CuteGraphPlus( + r, style, this.$diagram_container[0], this.diagram_readonly); + Object.entries(nodes).forEach(function (item) { + let node = item[1]; + let n = new CuteNodePlus( + self.graph, + // FIXME the +50 should be in the layout algorithm + node.x + self.diagram_offset, + node.y + self.diagram_offset, + CuteGraphPlus_wordwrap(node.name, 14), + node.shape === 'rectangle' ? 'rect' : 'circle', + node.color === 'white' || node.color === 'gray' + ? style[node.color] : node.color, + node.fgcolor === false + ? style.node_label_color : node.fgcolor, + node.highlight_node_color, + ); + + n.id = node.id; + id_to_node[node.id] = n; + }); + Object.entries(edges).forEach(function (item) { + let edge = item[1]; + let e = new CuteEdgePlus( + self.graph, + CuteGraphPlus_wordwrap(edge.signal, 32), + id_to_node[edge.s_id], + // WORKAROUND + id_to_node[edge.d_id] || id_to_node[edge.s_id] + ); + e.id = edge.id; + }); + $div.contents().appendTo(this.$diagram_container); + $div.remove(); + + CuteNodePlus.destruction_callback = function (cutenode) { + let {remove_node} = self.env.custom_events + remove_node(cutenode.id); + return new Promise(() => {}); + }; + CuteNodePlus.double_click_callback = function (cutenode) { + let {edit_node} = self.env.custom_events + edit_node(cutenode.id); + }; + CuteNodePlus.drag_up_callback = function (cutenode) { + if (!self.state.auto_layout) { + let {change_node_position} = self.env.custom_events + change_node_position(cutenode); + } + }; + CuteEdgePlus.new_edge_callback = function (cuteedge) { + let {add_edge} = self.env.custom_events + add_edge({ + source_id: cuteedge.get_start().id, + dest_id: cuteedge.get_end().id, + }); + return new Promise(() => {}); + }; + CuteEdgePlus.destruction_callback = function (cuteedge) { + let {remove_edge} = self.env.custom_events + remove_edge(cuteedge.id); + return new Promise(() => {}); + }; + + CuteEdgePlus.double_click_callback = function (cuteedge) { + let {edit_edge} = self.env.custom_events + edit_edge(cuteedge.id); + }; + + CuteEdgePlus.creation_callback = function (node_start, node_end) { + return {label: ''}; + }; + + this.diagram_in_dom = true; + this.align_diagram(); + } + + getDiagramStyle() { + return { + edge_color: "#A0A0A0", + edge_label_color: "#555", + edge_label_font_size: 10, + edge_width: 2, + edge_spacing: 100, + edge_loop_radius: 100, + node_label_color: "#333", + node_label_font_size: 12, + node_outline_color: "#333", + node_outline_width: 1, + node_selected_color: "#0097BE", + node_selected_width: 2, + node_size_x: this.node_size_x, + node_size_y: this.node_size_y, + connector_active_color: "#FFF", + connector_radius: 4, + close_button_radius: 8, + close_button_color: "#333", + close_button_x_color: "#FFF", + gray: "#DCDCDC", + white: "#FFF", + viewport_margin: 50, + }; + } + + align_diagram() { + if (!this.auto_layout) { + // Alignment of the diagram to the left and if it has a height + // less than the height of the container, then in the center, + // else to the top + let nodes = this.state.nodes; + + let array_of_x = Object.values(nodes).map(function (node) { + return node.x; + }); + let array_of_y = Object.values(nodes).map(function (node) { + return node.y; + }); + + let min_x = Math.min.apply(Math, array_of_x); + let min_y = Math.min.apply(Math, array_of_y); + let max_y = Math.max.apply(Math, array_of_y); + + let diagram_height = Math.abs(max_y - min_y) + + this.node_size_y + 2 * this.diagram_padding; + let d_container_height = this.$diagram_container.height(); + + let tr_x = this.node_size_x / 2 + this.diagram_padding - min_x; + let tr_y = 0; + if (d_container_height >= diagram_height) { + let d_h_offset = (d_container_height - + diagram_height - this.node_size_y) / 2 + tr_y = this.node_size_y + d_h_offset - min_y; + } else { + tr_y = this.node_size_y - min_y; + } + + this.graph.translate_graph(tr_x, tr_y); + } + } +} + +DiagramPlusRenderer.template = "DiagramPlusView"; +DiagramPlusRenderer.components = {}; +DiagramPlusRenderer.props = ['model'] diff --git a/crnd_web_diagram_plus/static/src/js/diagram_view.js b/crnd_web_diagram_plus/static/src/js/diagram_view.js new file mode 100644 index 000000000..68be3440c --- /dev/null +++ b/crnd_web_diagram_plus/static/src/js/diagram_view.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { DiagramPlusModel } from "./diagram_model"; +import { DiagramPlusController } from "./diagram_controller"; +import { DiagramPlusRenderer } from "./diagram_renderer"; +import { DiagramPlusArchParser } from "./diagram_arch_parser"; +const viewRegistry = registry.category("views"); + + +/** + * Diagram View + */ +export const DiagramPlusView = { + display_name: _t('DiagramPlus'), + type: 'diagram_plus', + icon: 'fa-code-fork', + multiRecord: false, + searchable: false, + withSearchPanel: false, + withSearchBar: false, + Controller: DiagramPlusController, + Renderer: DiagramPlusRenderer, + Model: DiagramPlusModel, + ArchParser: DiagramPlusArchParser, + buttonTemplate: "DiagramPlusView.buttons", + props: (genericProps, view) => { + const { ArchParser } = view; + const { arch, resModel, resId, fields} = genericProps; + let archInfo = new ArchParser().parse(arch, resModel, resId, fields); + return { + ...genericProps, + modelParams: archInfo, + Model: view.Model, + Renderer: view.Renderer, + buttonTemplate: view.buttonTemplate, + }; + }, +}; + +viewRegistry.add('diagram_plus', DiagramPlusView); diff --git a/crnd_web_diagram_plus/static/src/scss/diagram_view.scss b/crnd_web_diagram_plus/static/src/scss/diagram_view.scss new file mode 100644 index 000000000..2e9ff6e6b --- /dev/null +++ b/crnd_web_diagram_plus/static/src/scss/diagram_view.scss @@ -0,0 +1,18 @@ +.o_diagram_plus_view, +.o_diagram_plus { + height: 100%; + background-color: white; + overflow-y: hidden; + + .o_diagram_plus_header { + padding-top: 16px; + padding-right: 16px; + padding-bottom: 16px; + padding-left: 16px; + + > span { + display: block; + text-align: justify; + } + } +} diff --git a/crnd_web_diagram_plus/static/src/xml/base_diagram.xml b/crnd_web_diagram_plus/static/src/xml/base_diagram.xml new file mode 100644 index 000000000..3703955f7 --- /dev/null +++ b/crnd_web_diagram_plus/static/src/xml/base_diagram.xml @@ -0,0 +1,31 @@ + diff --git a/crnd_web_diagram_plus/static/tests/diagram_tests.js b/crnd_web_diagram_plus/static/tests/diagram_tests.js new file mode 100644 index 000000000..5ceb9bc80 --- /dev/null +++ b/crnd_web_diagram_plus/static/tests/diagram_tests.js @@ -0,0 +1,398 @@ +/** @odoo-module **/ + +// import DiagramPlusView from './../src/js/diagram_view'; +// import { +// createView, +// dom, +// fields, +// nextTick, +// } from 'web.test_utils'; +// import { +// GraphNode as CuteNodePlus, +// GraphEdge as CuteEdgePlus, +// } from './../src/js/graph'; +// +// QUnit.module('DiagramPlus', { +// beforeEach: function () { +// this.data = { +// workflow: { +// fields: { +// name: {string: 'Name', type: "char"}, +// }, +// records: [ +// {id: 1, name: "a workflow"}, +// ], +// }, +// node_model: { +// fields: { +// name: {string: 'Name', type: 'char'}, +// workflow_id: { +// string: 'Workflow ID', +// type: 'many2one', +// relation: 'workflow', +// }, +// }, +// records: [ +// {id: 1, name: "A first node", workflow_id: 1}, +// {id: 2, name: "A second node", workflow_id: 1}, +// {id: 3, name: "A third node", workflow_id: 1}, +// ], +// }, +// transition_model: { +// fields: { +// name: {string: 'Name', type: 'char'}, +// source_id: { +// string: 'Source ID', +// type: 'many2one', +// relation: 'node_model', +// }, +// dest_id: { +// string: 'Destination ID', +// type: 'many2one', +// relation: 'node_model', +// }, +// }, +// records: [ +// { +// id: 1, +// name: 'a transition from 1 to 2', +// source_id: 1, +// dest_id: 2, +// }, +// { +// id: 2, +// name: 'a transition from 2 to 3', +// source_id: 2, +// dest_id: 3, +// }, +// { +// id: 3, +// name: 'a transition from 2 to 1', +// source_id: 2, +// dest_id: 1, +// }, +// ], +// }, +// }; +// this.arch = '' + +// '' + +// '' + +// ''; +// this.archs = { +// 'node_model,false,form': '
' + +// '' + +// '' + +// '', +// 'transition_model,false,form': +// '
' + +// '' + +// '' + +// '' + +// '', +// }; +// this.mockRPC = function (route, args) { +// if (route !== '/web_diagram_plus/diagram/get_diagram_info') { +// return this._super.apply(this, arguments); +// } +// var data = this.data; +// var node_records = _.filter( +// data.node_model.records, +// {workflow_id: args.id} +// ); +// var transition_records = _.filter( +// data.transition_model.records, +// function (record) { +// return _.findWhere( +// node_records, +// {id: record.source_id} +// ); +// } +// ); +// return Promise.resolve({ +// parent_field: 'workflow_id', +// display_name: _.findWhere( +// data.workflow.records, +// {id: args.id} +// ).name, +// nodes: _.map(node_records, function (record) { +// return { +// id: record.id, +// name: record.name, +// color: 'gray', +// shape: record.id === 1 ? 'rectangle' : undefined, +// }; +// }), +// conn: _.map( +// transition_records, +// function (record) { +// var source_node = _.findWhere( +// data.node_model.records, +// {id: record.source_id} +// ); +// var dest_node = _.findWhere( +// data.node_model.records, +// {id: record.dest_id} +// ); +// return { +// source: source_node ? source_node.name: '', +// destination: dest_node ? dest_node.name: '', +// s_id: record.source_id, +// d_id: record.dest_id, +// signal: record.name, +// }; +// } +// ), +// }); +// }; +// }, +// afterEach: function () { +// $("body > i[title='Raphaël Colour Picker']").remove(); +// } +// }, function () { +// +// QUnit.module('DiagramPlusView'); +// +// QUnit.test('simple diagram plus rendering', async function (assert) { +// assert.expect(6); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// assert.containsOnce(diagram, '.o_diagram_plus svg', +// "draw the diagram inside the .o_diagram_plus div"); +// assert.strictEqual( +// diagram.$( +// '.o_diagram_plus path:not(#raphael-marker-block)' +// ).length, 3, +// "diagram should contain 3 transitions"); +// assert.containsN(diagram, '.o_diagram_plus ellipse', 2, +// "diagram should contain 2 'ellipse' nodes (nodes 2 and 3)"); +// // -1 because the lib always generates a rect tag that isn't a node +// assert.strictEqual(diagram.$('.o_diagram_plus rect').length - 1, 1, +// "diagram should contain 1 'rectangle' node (node 1)"); +// assert.containsN(diagram, '.o_diagram_plus_header span', 2, +// "diagram should contain 2 header rows"); +// assert.strictEqual( +// diagram.$('.o_diagram_plus_header span:eq(0)').text(), +// 'A first label', +// "diagram label is correctly inserted"); +// diagram.destroy(); +// }); +// +// QUnit.test('node plus creation', async function (assert) { +// assert.expect(4); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// archs: this.archs, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.containsN(diagram, '.o_diagram_plus ellipse', 2, +// "diagram should contain 2 'ellipse' nodes (nodes 2 and 3)"); +// assert.strictEqual( +// diagram.$('text:contains(a new node)').length, 0, +// "diagram should only have the default nodes at start"); +// +// await dom.click( +// diagram.$buttons.find('.o_diagram_plus_new_button')); +// await fields.editInput( +// $('.modal-body input:first'), 'a new node'); +// await fields.editInput( +// $('.modal-body input:last'), 1); +// await dom.click( +// $('.modal-footer button.btn-primary')); +// +// assert.containsN( +// diagram, '.o_diagram_plus ellipse', 3, +// "diagram should contain 3 'ellipse' nodes now " + +// "(nodes 2, 3 and the new one)"); +// assert.strictEqual( +// diagram.$('text:contains(a new node)').length, 1, +// "diagram should only have the default nodes at start"); +// diagram.destroy(); +// }); +// +// QUnit.test('node plus edition', async function (assert) { +// assert.expect(2); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// archs: this.archs, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.strictEqual(diagram.$('.o_diagram_plus text').first().text(), +// 'A first node', +// "diagram first node should have default name at first"); +// +// CuteNodePlus.double_click_callback({id: 1}); +// await nextTick(); +// await fields.editInput( +// $('.modal-body input:first'), 'An edited node'); +// await dom.click( +// $('.modal-footer button.btn-primary')); +// +// assert.strictEqual( +// diagram.$('text').first().text(), 'An edited node', +// "diagram first node should now have new name"); +// +// diagram.destroy(); +// }); +// +// QUnit.test('node plus deletion', async function (assert) { +// assert.expect(2); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.containsN(diagram, '.o_diagram_plus ellipse', 2, +// "diagram should contain 2 'ellipse' nodes (nodes 2 and 3)"); +// +// CuteNodePlus.destruction_callback({id: 2}); +// await nextTick(); +// await dom.click($('.modal-footer button.btn-primary')); +// +// assert.containsOnce(diagram, '.o_diagram_plus ellipse', +// "diagram should contain 1 'ellipse' nodes (node 2)"); +// +// diagram.destroy(); +// }); +// +// QUnit.test('edge plus creation', async function (assert) { +// assert.expect(4); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// archs: this.archs, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.containsN(diagram, '.o_diagram_plus path', 4, +// "diagram should contain 4 'path' nodes " + +// "(#raphael-marker-block, and transitions 1, 2 and 3)"); +// assert.strictEqual( +// diagram.$('text:contains(a transition from 1 to 3)').length, 0, +// "diagram should not have a transition from 1 to 3 at start"); +// +// CuteEdgePlus.new_edge_callback({ +// get_start: function () {return {id: 1};}, +// get_end: function () {return {id: 3};}, +// }); +// await nextTick(); +// await fields.editInput( +// $('.modal-body input:first'), 'a transition from 1 to 3'); +// await dom.click( +// $('.modal-footer button.btn-primary')); +// +// assert.containsN(diagram, '.o_diagram_plus path', 5, +// "diagram should contain 4 'path' nodes " + +// "(#raphael-marker-block, transitions 1, 2, 3, and the new one)" +// ); +// assert.strictEqual( +// diagram.$('text:contains(a transition from 1 to 3)').length, 1, +// "diagram should now have a transition from 1 to 3"); +// +// diagram.destroy(); +// }); +// +// QUnit.test('edge plus edition', async function (assert) { +// assert.expect(4); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// archs: this.archs, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.strictEqual( +// diagram.$('text:contains(a transition from 1 to 2)').length, 1, +// "diagram edge should have default name at start"); +// assert.strictEqual( +// diagram.$('text:contains(An edited edge)').length, 0, +// "diagram should only have the default edges at start"); +// +// CuteEdgePlus.double_click_callback({id: 1}); +// await nextTick(); +// await fields.editInput( +// $('.modal-body input:first'), 'An edited edge'); +// await dom.click( +// $('.modal-footer button.btn-primary')); +// +// assert.strictEqual( +// diagram.$('text:contains(a transition from 1 to 2)').length, 0, +// "diagram edge should not have default name anymore"); +// assert.strictEqual( +// diagram.$('text:contains(An edited edge)').length, 1, +// "diagram should now have the new edge"); +// +// diagram.destroy(); +// }); +// +// QUnit.test('edge plus deletion', async function (assert) { +// assert.expect(4); +// +// var diagram = await createView({ +// View: DiagramPlusView, +// model: 'workflow', +// data: this.data, +// arch: this.arch, +// res_id: 1, +// mockRPC: this.mockRPC, +// }); +// +// assert.containsN(diagram, '.o_diagram_plus path', 4, +// "diagram should contain 4 'path' nodes " + +// "(#raphael-marker-block, and transitions 1, 2 and 3)"); +// assert.strictEqual( +// diagram.$('text:contains(a transition from 2 to 1)').length, 1, +// "diagram edge should have default name at start"); +// +// CuteEdgePlus.destruction_callback({id: 3}); +// await nextTick(); +// await dom.click($('.modal-footer button.btn-primary')); +// +// assert.containsN(diagram, '.o_diagram_plus path', 3, +// "diagram should contain 3 'path' nodes " + +// "(#raphael-marker-block, and transitions 1 and 2)"); +// assert.strictEqual( +// diagram.$('text:contains(a transition from 2 to 1)').length, 0, +// "diagram edge label should have been deleted"); +// +// diagram.destroy(); +// }); +// }); diff --git a/crnd_web_diagram_plus/tools/__init__.py b/crnd_web_diagram_plus/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crnd_web_diagram_plus/tools/graph.py b/crnd_web_diagram_plus/tools/graph.py new file mode 100644 index 000000000..1920ddb4c --- /dev/null +++ b/crnd_web_diagram_plus/tools/graph.py @@ -0,0 +1,818 @@ +# pylint: disable=all +# flake8: noqa + +# Copied from original Odoo code: +# https://github.com/odoo/odoo/blob/3e8b930b18b53e0d5cce3170d8c0f5cd8c7f4f6e/odoo/tools/graph.py +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import operator +import math + +from odoo import exceptions, _ + +# Limit the max number of iterations in computation of graph. +GUARD_COUNTER_LIMIT = 20000 + + +class Graph: + def __init__(self, nodes, transitions, no_ancester=None): + """Initialize graph's object + @param nodes list of ids of nodes in the graph + @param transitions list of edges in the graph in the form + (source_node, destination_node) + @param no_ancester list of nodes with no incoming edges + """ + + self.nodes = nodes or [] + self.edges = transitions or [] + self.no_ancester = no_ancester or {} + trans = {} + + for t in transitions: + trans.setdefault(t[0], []) + trans[t[0]].append(t[1]) + self.transitions = trans + self.result = {} + + def init_rank(self): + """ Computes rank of the nodes of the graph by finding + initial feasible tree + """ + self.edge_wt = {} + for link in self.links: + self.edge_wt[link] = ( + self.result[link[1]]['x'] - self.result[link[0]]['x']) + + tot_node = len(self.partial_order) + # do until all the nodes in the component are searched + g_counter = 1 + while self.tight_tree() < tot_node and g_counter < GUARD_COUNTER_LIMIT: + g_counter += 1 + if g_counter >= GUARD_COUNTER_LIMIT: + raise exceptions.UserError(_( + "Cannot compute diagram view. " + "It seems that the flow is incorrect! " + "Try to remove some broken routes without using " + "diagram view." + )) + list_node = [] + list_edge = [] + + for node in self.nodes: + if node not in self.reachable_nodes: + list_node.append(node) + + for edge in self.edge_wt: + if edge not in self.tree_edges: + list_edge.append(edge) + + slack = 100 + + for edge in list_edge: + if ((edge[0] in self.reachable_nodes and edge[1] not in self.reachable_nodes) or + (edge[1] in self.reachable_nodes and edge[0] not in self.reachable_nodes)): + if slack > self.edge_wt[edge] - 1: + slack = self.edge_wt[edge] - 1 + new_edge = edge + + if new_edge[0] not in self.reachable_nodes: + delta = -(self.edge_wt[new_edge] - 1) + else: + delta = self.edge_wt[new_edge] - 1 + + for node in self.result: + if node in self.reachable_nodes: + self.result[node]['x'] += delta + + for edge in self.edge_wt: + self.edge_wt[edge] = ( + self.result[edge[1]]['x'] - self.result[edge[0]]['x']) + + self.init_cutvalues() + + def tight_tree(self): + self.reachable_nodes = [] + self.tree_edges = [] + self.reachable_node(self.start) + return len(self.reachable_nodes) + + def reachable_node(self, node): + """ Find the nodes of the graph which are only 1 rank apart + from each other + """ + if node not in self.reachable_nodes: + self.reachable_nodes.append(node) + for edge in self.edge_wt: + if edge[0] == node: + if self.edge_wt[edge] == 1: + self.tree_edges.append(edge) + if edge[1] not in self.reachable_nodes: + self.reachable_nodes.append(edge[1]) + self.reachable_node(edge[1]) + + def init_cutvalues(self): + """ Initialize cut values of edges of the feasible tree. + Edges with negative cut-values are removed from the tree + to optimize rank assignment + """ + self.cut_edges = {} + self.head_nodes = [] + i = 0 + + for edge in self.tree_edges: + self.head_nodes = [] + rest_edges = [] + rest_edges += self.tree_edges + del rest_edges[i] + self.head_component(self.start, rest_edges) + i += 1 + positive = 0 + negative = 0 + for source_node in self.transitions: + if source_node in self.head_nodes: + for dest_node in self.transitions[source_node]: + if dest_node not in self.head_nodes: + negative += 1 + else: + for dest_node in self.transitions[source_node]: + if dest_node in self.head_nodes: + positive += 1 + + self.cut_edges[edge] = positive - negative + + def head_component(self, node, rest_edges): + """ Find nodes which are reachable from the starting node, + after removing an edge + """ + if node not in self.head_nodes: + self.head_nodes.append(node) + + for edge in rest_edges: + if edge[0] == node: + self.head_component(edge[1], rest_edges) + + def process_ranking(self, node, level=0): + """ Computes initial feasible ranking after making graph acyclic + with depth-first search + """ + if node not in self.result: + self.result[node] = {'y': None, 'x': level, 'mark': 0} + else: + if level > self.result[node]['x']: + self.result[node]['x'] = level + + if self.result[node]['mark']==0: + self.result[node]['mark'] = 1 + for sec_end in self.transitions.get(node, []): + self.process_ranking(sec_end, level + 1) + + def make_acyclic(self, parent, node, level, tree): + """ Computes Partial-order of the nodes with depth-first search + """ + if node not in self.partial_order: + self.partial_order[node] = {'level': level, 'mark': 0} + if parent: + tree.append((parent, node)) + + if self.partial_order[node]['mark'] == 0: + self.partial_order[node]['mark'] = 1 + for sec_end in self.transitions.get(node, []): + self.links.append((node, sec_end)) + self.make_acyclic(node, sec_end, level + 1, tree) + + return tree + + def rev_edges(self, tree): + """ reverse the direction of the edges whose + source-node-partail_order > destination-node-partail_order + to make the graph acyclic + """ + is_cyclic = False + i = 0 + # Note, here 'links' is modified, thus there is not possible + # to use enumerate + # TODO: may be it have sense to rewrite without modification + # of self.links? + for link in self.links: + src = link[0] + des = link[1] + edge_len = self.partial_order[des]['level'] - self.partial_order[src]['level'] + if edge_len < 0: + del self.links[i] + self.links.insert(i, (des, src)) + self.transitions[src].remove(des) + self.transitions.setdefault(des, []).append(src) + is_cyclic = True + elif math.fabs(edge_len) > 1: + is_cyclic = True + i += 1 + + return is_cyclic + + def exchange(self, e, f): + """ Exchange edges to make feasible-tree optimized + + :param e: edge with negative cut-value + :param f: new edge with minimum slack-value + """ + del self.tree_edges[self.tree_edges.index(e)] + self.tree_edges.append(f) + self.init_cutvalues() + + def enter_edge(self, edge): + """ Finds a new_edge with minimum slack value to replace an edge with + negative cut-value + + @param edge edge with negative cut-value + """ + self.head_nodes = [] + rest_edges = [] + rest_edges += self.tree_edges + del rest_edges[rest_edges.index(edge)] + self.head_component(self.start, rest_edges) + + if edge[1] in self.head_nodes: + lst_h_nodes = [] + for node in self.result: + if node not in self.head_nodes: + lst_h_nodes.append(node) + self.head_nodes = lst_h_nodes + + slack = 100 + new_edge = edge + for source_node in self.transitions: + if source_node in self.head_nodes: + for dest_node in self.transitions[source_node]: + if dest_node not in self.head_nodes: + if slack > (self.edge_wt[edge] - 1): + slack = self.edge_wt[edge] - 1 + new_edge = (source_node, dest_node) + + return new_edge + + def leave_edge(self): + """Returns the edge with negative cut_value(if exists) + """ + if self.critical_edges: + for edge in self.critical_edges: + self.cut_edges[edge] = 0 + + for edge in self.cut_edges: + if self.cut_edges[edge] < 0: + return edge + + return None + + def finalize_rank(self, node, level): + self.result[node]['x'] = level + for destination in self.optimal_edges.get(node, []): + self.finalize_rank(destination, level+1) + + def normalize(self): + """The ranks are normalized by setting the least rank to zero. + """ + least_rank = min(x['x'] for x in self.result.values()) + + if least_rank != 0: + for node in self.result: + self.result[node]['x'] -= least_rank + + def make_chain(self): + """ Edges between nodes more than one rank apart are replaced by + chains of unit length edges between temporary nodes. + """ + for edge in self.edge_wt: + if self.edge_wt[edge] > 1: + self.transitions[edge[0]].remove(edge[1]) + start = self.result[edge[0]]['x'] + end = self.result[edge[1]]['x'] + + for rank in range(start+1, end): + if not self.result.get((rank, 'temp'), False): + self.result[(rank, 'temp')] = { + 'y': None, + 'x': rank, + 'mark': 0, + } + + for rank in range(start, end): + if start == rank: + self.transitions[edge[0]].append((rank + 1, 'temp')) + elif rank == end-1: + self.transitions.setdefault( + (rank, 'temp'), []).append(edge[1]) + else: + self.transitions.setdefault( + (rank, 'temp'), []).append((rank + 1, 'temp')) + + def init_order(self, node, level): + """Initialize orders the nodes in each rank with depth-first search + """ + if not self.result[node]['y']: + self.result[node]['y'] = self.order[level] + self.order[level] += 1 + + for sec_end in self.transitions.get(node, []): + if node != sec_end: + self.init_order(sec_end, self.result[sec_end]['x']) + + def order_heuristic(self): + for __ in range(12): + self.wmedian() + + def wmedian(self): + """ Applies median heuristic to find optimized order of the nodes + within their ranks + """ + for level in self.levels: + + node_median = [] + nodes = self.levels[level] + for node in nodes: + node_median.append((node, self.median_value(node, level-1))) + + sort_list = sorted(node_median, key=operator.itemgetter(1)) + + # Compute new list of nodes for this level (sorted by median value) + self.levels[level] = [t[0] for t in sort_list] + + order = 0 + # TODO: Is it correct, may be we have to use nodes from + # self.levels[level] + for node in nodes: + self.result[node]['y'] = order + order += 1 + + def median_value(self, node, adj_rank): + """ Returns median value of a vertex , defined as the median position + of the adjacent vertices + + :param node: node to process + :param adj_rank: rank 1 less than the node's rank + """ + adj_nodes = self.adj_position(node, adj_rank) + length = len(adj_nodes) + m = length // 2 + + if length == 0: + return -1.0 + if length % 2 == 1: + return adj_nodes[m] # median of the middle element + if length == 2: + return (adj_nodes[0] + adj_nodes[1]) / 2 + + left = adj_nodes[m-1] - adj_nodes[0] + right = adj_nodes[length-1] - adj_nodes[m] + return ( + (adj_nodes[m-1] * right) + (adj_nodes[m] * left) + ) / (left + right) + + def adj_position(self, node, adj_rank): + """ Returns list of the present positions of the nodes adjacent to node + in the given adjacent rank. + + :param node: node to process + :param adj_rank: rank 1 less than the node's rank + """ + pre_level_nodes = self.levels.get(adj_rank, []) + adj_nodes = [] + + if pre_level_nodes: + for src in pre_level_nodes: + if self.transitions.get(src) and node in self.transitions[src]: + adj_nodes.append(self.result[src]['y']) + + return adj_nodes + + def preprocess_order(self): + levels = {} + for r in self.partial_order: + lvl = self.result[r]['x'] + levels.setdefault(lvl, []) + levels[lvl].append(r) + + self.levels = levels + + def graph_order(self): + """ Finds actual-order of the nodes with respect to maximum number of + nodes in a rank in component + """ + mid_pos = 0.0 + max_level = max(len(x) for x in self.levels.values()) + + for level in self.levels: + if level: + no = len(self.levels[level]) + factor = (max_level - no) * 0.10 + lst = self.levels[level] + lst.reverse() + + if no % 2 == 0: + first_half = lst[no // 2:] + factor = -factor + else: + first_half = lst[no // 2 + 1:] + # for the case when horizontal graph is there + if max_level == 1: + self.result[lst[no // 2]]['y'] = ( + mid_pos + ( + self.result[lst[no // 2]]['x'] % 2 * 0.5 + ) + ) + else: + self.result[lst[no // 2]]['y'] = mid_pos + factor + + last_half = lst[:no // 2] + + i = 1 + for node in first_half: + self.result[node]['y'] = mid_pos - (i + factor) + i += 1 + + i = 1 + for node in last_half: + self.result[node]['y'] = mid_pos + (i + factor) + i += 1 + else: + self.max_order += max_level+1 + mid_pos = self.result[self.start]['y'] + + def tree_order(self, node, last=0): + mid_pos = self.result[node]['y'] + l = self.transitions.get(node, []) + l.reverse() + no = len(l) + + rest = no % 2 + first_half = l[no // 2 + rest:] + last_half = l[:no // 2] + + for i, child in enumerate(first_half): + self.result[child]['y'] = mid_pos - (i+1 - (0 if rest else 0.5)) + + if self.transitions.get(child, False): + if last: + self.result[child]['y'] = ( + last + len(self.transitions[child]) / 2 + 1) + last = self.tree_order(child, last) + + if rest: + mid_node = l[no // 2] + self.result[mid_node]['y'] = mid_pos + + if self.transitions.get(mid_node, False): + if last: + self.result[mid_node]['y'] = ( + last + len(self.transitions[mid_node]) / 2 + 1) + if node != mid_node: + last = self.tree_order(mid_node) + else: + if last: + self.result[mid_node]['y'] = last + 1 + self.result[node]['y'] = self.result[mid_node]['y'] + mid_pos = self.result[node]['y'] + + i = 1 + last_child = None + for child in last_half: + self.result[child]['y'] = mid_pos + (i - (0 if rest else 0.5)) + last_child = child + i += 1 + if self.transitions.get(child, False): + if last: + self.result[child]['y'] = ( + last + len(self.transitions[child]) / 2 + 1) + if node != child: + last = self.tree_order(child, last) + + if last_child: + last = self.result[last_child]['y'] + + return last + + def process_order(self): + """ Finds actual-order of the nodes with respect to maximum number of + nodes in a rank in component + """ + if self.Is_Cyclic: + max_level = max(len(x) for x in self.levels.values()) + + if max_level % 2: + self.result[self.start]['y'] = (max_level+1) / 2 + self.max_order + (self.max_order and 1) + else: + self.result[self.start]['y'] = max_level / 2 + self.max_order + (self.max_order and 1) + + self.graph_order() + + else: + self.result[self.start]['y'] = 0 + self.tree_order(self.start, 0) + min_order = math.fabs(min(x['y'] for x in self.result.values())) + + index = self.start_nodes.index(self.start) + + roots = [] + if index > 0: + for start in self.start_nodes[:index]: + same = True + for edge in self.tree_list[start][1:]: + if edge in self.tree_list[self.start]: + continue + + same = False + break + + if same: + roots.append(start) + + if roots: + min_order += self.max_order + else: + min_order += self.max_order + 1 + + for level in self.levels: + for node in self.levels[level]: + self.result[node]['y'] += min_order + + if roots: + roots.append(self.start) + one_level_el = self.tree_list[self.start][0][1] + base = self.result[one_level_el]['y'] # * 2 / (index + 2) + + no = len(roots) + first_half = roots[:no // 2] + + if no % 2 == 0: + last_half = roots[no // 2:] + else: + last_half = roots[no // 2 + 1:] + + factor = -math.floor(no // 2) + for start in first_half: + self.result[start]['y'] = base + factor + factor += 1 + + if no % 2: + self.result[roots[no // 2]]['y'] = base + factor + factor += 1 + + for start in last_half: + self.result[start]['y'] = base + factor + factor += 1 + + self.max_order = max(x['y'] for x in self.result.values()) + + def find_starts(self): + """ Finds other start nodes of the graph in the case + when graph is disconnected + """ + rem_nodes = [] + for node in self.nodes: + if not self.partial_order.get(node): + rem_nodes.append(node) + + g_counter = 0 + while g_counter < GUARD_COUNTER_LIMIT: + g_counter += 1 + if g_counter >= GUARD_COUNTER_LIMIT: + raise exceptions.UserError(_( + "Cannot compute diagram view. " + "It seems that the flow is incorrect! " + "Try to remove some broken routes without using " + "diagram view." + )) + if len(rem_nodes) == 1: + self.start_nodes.append(rem_nodes[0]) + break + else: + count = 0 + new_start = rem_nodes[0] + largest_tree = [] + + for node in rem_nodes: + self.partial_order = {} + tree = self.make_acyclic(None, node, 0, []) + if len(tree)+1 > count: + count = len(tree) + 1 + new_start = node + largest_tree = tree + else: + if not largest_tree: + new_start = rem_nodes[0] + rem_nodes.remove(new_start) + + self.start_nodes.append(new_start) + + for edge in largest_tree: + if edge[0] in rem_nodes: + rem_nodes.remove(edge[0]) + if edge[1] in rem_nodes: + rem_nodes.remove(edge[1]) + + if not rem_nodes: + break + + def rank(self): + """ Finds the optimized rank of the nodes using + Network-simplex algorithm + """ + self.levels = {} + self.critical_edges = [] + self.partial_order = {} + self.links = [] + self.Is_Cyclic = False + + self.tree_list[self.start] = self.make_acyclic(None, self.start, 0, []) + self.Is_Cyclic = self.rev_edges(self.tree_list[self.start]) + self.process_ranking(self.start) + self.init_rank() + + # make cut values of all tree edges to 0 to optimize feasible tree + e = self.leave_edge() + + g_counter = 0 + while e and g_counter < GUARD_COUNTER_LIMIT: + g_counter += 1 + if g_counter >= GUARD_COUNTER_LIMIT: + raise exceptions.UserError(_( + "Cannot compute diagram view. " + "It seems that the flow is incorrect! " + "Try to remove some broken routes without using " + "diagram view." + )) + + f = self.enter_edge(e) + if e == f: + self.critical_edges.append(e) + else: + self.exchange(e, f) + e = self.leave_edge() + + # finalize rank using optimum feasible tree + # self.optimal_edges = {} + # for edge in self.tree_edges: + # source = self.optimal_edges.setdefault(edge[0], []) + # source.append(edge[1]) + # + # self.finalize_rank(self.start, 0) + + # normalization + self.normalize() + for edge in self.edge_wt: + self.edge_wt[edge] = ( + self.result[edge[1]]['x'] - self.result[edge[0]]['x']) + + def order_in_rank(self): + """ Finds optimized order of the nodes within their ranks using + median heuristic + """ + self.make_chain() + self.preprocess_order() + self.order = {} + max_rank = max(x for x in self.levels) + + for i in range(max_rank + 1): + self.order[i] = 0 + + self.init_order(self.start, self.result[self.start]['x']) + + for level in self.levels: + self.levels[level].sort(key=lambda x: self.result[x]['y']) + + self.order_heuristic() + self.process_order() + + def process(self, starting_node): + """Process the graph to find ranks and order of the nodes + @param starting_node node from where to start the graph search + """ + self.start_nodes = starting_node or [] + self.partial_order = {} + self.links = [] + self.tree_list = {} + + if self.nodes: + if self.start_nodes: + # add dummy edges to the nodes which does not have + # any incoming edges + tree = self.make_acyclic(None, self.start_nodes[0], 0, []) + + for node in self.no_ancester: + for sec_node in self.transitions.get(node, []): + if sec_node in self.partial_order: + self.transitions[self.start_nodes[0]].append(node) + break + + self.partial_order = {} + tree = self.make_acyclic(None, self.start_nodes[0], 0, []) + + # if graph is disconnected or no start-node is given + # than to find starting_node for each component of the node + if len(self.nodes) > len(self.partial_order): + self.find_starts() + + self.max_order = 0 + # for each component of the graph find ranks and order of the nodes + for s in self.start_nodes: + self.start = s + # First step: Network simplex algorithm + self.rank() + # Second step: ordering nodes within ranks + self.order_in_rank() + + def __str__(self): + result = '' + for lvl in self.levels: + result += 'PosY: ' + str(lvl) + '\n' + for node in self.levels[lvl]: + result += '\tPosX: %s - Node: %s\n' % ( + str(self.result[node]['y']), + str(node)) + return result + + def scale(self, maxx, maxy, nwidth=0, nheight=0, margin=20): + """ Computes actual co-ordiantes of the nodes + """ + # for flat edges ie. source an destination nodes are on the same rank + for src in self.transitions: + for des in self.transitions[src]: + if self.result[des]['x'] - self.result[src]['x'] == 0: + self.result[src]['x'] += 0.08 + self.result[des]['x'] -= 0.08 + + factorX = maxx + nheight + factorY = maxy + nwidth + + for node in self.result: + self.result[node]['y'] = ( + (self.result[node]['y']) * factorX + margin) + self.result[node]['x'] = ( + (self.result[node]['x']) * factorY + margin) + + def result_get(self): + return self.result + + +def main(): + starting_node = ['profile'] # put here nodes with flow_start=True + nodes = [ + 'project', 'account', 'hr', 'base', 'product', 'mrp', + 'test', 'profile', + ] + transitions = [ + ('profile', 'mrp'), + ('mrp', 'project'), + ('project', 'product'), + ('mrp', 'hr'), + ('mrp', 'test'), + ('project', 'account'), + ('project', 'hr'), + ('product', 'base'), + ('account', 'product'), + ('account', 'test'), + ('account', 'base'), + ('hr', 'base'), + ('test', 'base') + ] + + radius = 20 + g = Graph(nodes, transitions) + g.process(starting_node) + g.scale(radius * 3, radius * 3, radius, radius) + + from PIL import Image + from PIL import ImageDraw + img = Image.new("RGB", (800, 600), "#ffffff") + draw = ImageDraw.Draw(img) + + result = g.result_get() + node_res = {} + for node in nodes: + node_res[node] = result[node] + + for name, node in node_res.items(): + + draw.arc( + (int(node['y']-radius), + int(node['x']-radius), + int(node['y']+radius), + int(node['x']+radius)), + 0, 360, (128, 128, 128)) + draw.text( + (int(node['y']), + int(node['x'])), + str(name), + (128, 128, 128)) + + for t in transitions: + draw.line( + (int(node_res[t[0]]['y']), + int(node_res[t[0]]['x']), + int(node_res[t[1]]['y']), + int(node_res[t[1]]['x'])), + (128, 128, 128)) + img.save("graph.png", "PNG") + + +if __name__ == '__main__': + main() diff --git a/crnd_web_diagram_plus/utils.py b/crnd_web_diagram_plus/utils.py new file mode 100644 index 000000000..2f7a0f749 --- /dev/null +++ b/crnd_web_diagram_plus/utils.py @@ -0,0 +1,25 @@ +def str2bool(value, default=None): + """ Convert str value to boolean value. + + If value could be parsed ('true' or 'false') then + converted value will be returned. + If value could not be parsed, default value will be returned. + + :param str value: value to convert to boolean + :param bool default: Default value + :return bool: value converted to value + """ + if value is None: + return default + + if value is True or value is False: + return value + + val = value.lower() + + if val == 'true': + return True + if val == 'false': + return False + + return default diff --git a/soong_workflow/__init__.py b/soong_workflow/__init__.py new file mode 100644 index 000000000..cde864bae --- /dev/null +++ b/soong_workflow/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/soong_workflow/__manifest__.py b/soong_workflow/__manifest__.py new file mode 100644 index 000000000..4890f2126 --- /dev/null +++ b/soong_workflow/__manifest__.py @@ -0,0 +1,24 @@ +{ + "name": "Soong Workflow", + "summary": "基于开源diagram模块,构建的审批流", + "category": "Extra Tools", + "author": "soong bo", + "license": "AGPL-3", + "version": "17.0.0.1.0", + "depends": [ + "base", + "crnd_web_diagram_plus", + ], + "data": [ + "security/soong_workflow_security.xml", + "security/ir.model.access.csv", + "views/menuitem.xml", + "views/soong_workflow_node_views.xml", + "views/soong_workflow_link_views.xml", + "views/soong_workflow_views.xml", + 'views/soong_workflow_record.xml' + ], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/soong_workflow/i18n/soong_workflow.pot b/soong_workflow/i18n/soong_workflow.pot new file mode 100644 index 000000000..e9bb5f215 --- /dev/null +++ b/soong_workflow/i18n/soong_workflow.pot @@ -0,0 +1,884 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * soong_workflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-15 07:40+0000\n" +"PO-Revision-Date: 2024-07-15 07:40+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "" +"Note: The result of executing the expression must be a " +"res.users recordset." +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +msgid "" +"Note: The return value of Operation Code should be True." +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +msgid "" +"Note: Workflow engine can only alter normal models like\n" +" crm.*, sale.*, hr.*, etc... (not transient like wizards nor abstract like reports)\n" +" also resource (res.*) & interface (ir.*) models are excluded from workflow engine." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__action_type +msgid "Action Type" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "Actions" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__group +msgid "Any user in a specific group" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__is_visible +msgid "Appear in Statusbar" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__sequence +msgid "Arrange buttons by defining sequence." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__sequence +msgid "Arrange node by defining sequence." +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Attributes" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__bg_color +msgid "Background Color" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__res_bg_color +msgid "Backgroung Color" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__btn_key +msgid "Button Key" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__name +msgid "Button String" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__button_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Buttons" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__flow_end +msgid "Check it if this node is the ending node." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__flow_start +msgid "Check it if this node is the starting node." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__action_type +msgid "Choose type of action to be trigger by the button." +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "Condition" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Confirm" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__is_highlight +msgid "Control highlighting of the button if needs user attention.." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__is_visible +msgid "Control visiability of the node/state in view." +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__create_uid +msgid "Created by" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__create_date +msgid "Created on" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__current_stage_highlight_color +msgid "Current Stage Highlight Color" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Delete" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__node_to +msgid "Destination Node" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__diagram_position +msgid "Diagram Position" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#, python-format +msgid "DiagramPlus" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__display_name +msgid "Display Name" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Do you really want to change the positions of the nodes?" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__has_icon +msgid "Enable it to add icon to the button." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__model_id +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__model_id +msgid "Enter business model you would like to modify its workflow." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__name +msgid "Enter button string name that will appear in the view." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_link__condition_code +msgid "Enter condition to pass thru this link." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_link__name +msgid "Enter friendly link name that describe the process." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__icon +msgid "" +"Enter icon name like: fa-print, it's recommended to use FontAwesome Icons." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__name +msgid "Enter string name of the node." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__name +msgid "Field" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__field_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Fields" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__flow_end +msgid "Flow End" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__flow_start +msgid "Flow Start" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__node_name +msgid "Generated technical name which used by backend code." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__name +msgid "Give workflow a name." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__group_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Groups" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__has_icon +msgid "Has Icon" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__btn_hide +msgid "Hide Button if Condition isn't fulfilled" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__id +msgid "ID" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__icon +msgid "Icon" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__btn_hide +msgid "If condition is false the button will be hidden." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__in_link_ids +msgid "Incoming Transitions" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Ingoing Links" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__invisible +msgid "Invisible" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__is_highlight +msgid "Is Highlighted" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__label_color +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__res_label_color +msgid "Label Color" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__write_date +msgid "Last Updated on" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__link_id +msgid "Link" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__link_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Links" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Load Model Fields" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Main Information" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__model_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__model_id +msgid "Model" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__model_id +msgid "Model Ref." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_uniq_model +#, python-format +msgid "Model must be unique." +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_ir_model +msgid "Models" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__name +msgid "Name" +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__node_id +msgid "Node Ref." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__node_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +msgid "Nodes" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Nodes' sequence must be unique per workflow." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__condition_code +msgid "Operation Code" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Outgoing Links" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__out_link_ids +msgid "Outgoing Transitions" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__code +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__code +msgid "Python Code" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__expression +msgid "Python Expression" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__readonly +msgid "Readonly" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__remove_default_attrs_mod +msgid "Remove Default Attributes & Modifiers" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__required +msgid "Required" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__res_id +msgid "Res ID" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__res_model +msgid "Res Model" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_expression +msgid "Review Expression" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_id +msgid "Reviewer" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_group_id +msgid "Reviewer group" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__use_custom_colors +msgid "Select colors from the palette manually" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__sequence +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__sequence +msgid "Sequence" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__server_action_id +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__action +msgid "Server Action" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow +msgid "Soong Workflow" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_link +msgid "Soong Workflow Links" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_mixin +msgid "Soong Workflow Mixin" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node_button +msgid "Soong Workflow Node Buttons" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node_field +msgid "Soong Workflow Node Fields" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node +msgid "Soong Workflow Nodes" +msgstr "" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_record +msgid "Soong Workflow Record" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_link.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_link.py:0 +#, python-format +msgid "Sorry, source & destination nodes can't be the same." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__node_from +msgid "Source Node" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__individual +msgid "Specific user" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#, python-format +msgid "Start and end cannot be selected for the node" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__state +msgid "Status" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__node_name +msgid "Technical Name" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__current_stage_highlight_color +msgid "The color to highlight the current stage on flow" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__remove_default_attrs_mod +msgid "" +"This option will remove default attributes set on fields & buttons of current model view in order to customized all attributes depending on your needs\n" +"Attributes like: [required, readonly, invisible]." +msgstr "" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__link +msgid "Trigger Link" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__use_custom_colors +msgid "Use Custom Colors" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__user_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Users" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__review_type +msgid "Validated by" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#, python-format +msgid "View Flow" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_ir_model__is_workflow +msgid "Whether the model supports workflow approval" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__win_act_id +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__win_act +msgid "Window Action" +msgstr "" + +#. module: soong_workflow +#: model:ir.actions.act_window,name:soong_workflow.soong_workflow_record_action +#: model:ir.ui.menu,name:soong_workflow.soong_workflow_record_menu +#: model_terms:ir.ui.view,arch_db:soong_workflow.soong_workflow_record_search +#: model_terms:ir.ui.view,arch_db:soong_workflow.soong_workflow_record_tree +msgid "WorkFollow Record" +msgstr "" + +#. module: soong_workflow +#: model:ir.actions.act_window,name:soong_workflow.action_soong_workflow +#: model:ir.ui.menu,name:soong_workflow.menu_soong_workflow +#: model:ir.ui.menu,name:soong_workflow.menu_soong_workflow_root +msgid "Workflow" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__node_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__node_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__node_id +msgid "Workflow Node Ref." +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__workflow_id +msgid "Workflow Ref." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_node_uniq_node_name +#, python-format +msgid "Workflow and Technical Name must be unique." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Workflow must have only one start node." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_uniq_name +#, python-format +msgid "Workflow name must be unique." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#, python-format +msgid "Workflow: %(name)s" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Workflow: %(type_name)s" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__reviewer_expression +msgid "" +"Write Python code that defines the reviewer. The result of executing the " +"expression must be a res.users recordset." +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#, python-format +msgid "" +"Wrong python code defined.\n" +"\n" +"Error: %s\n" +"Line: %s, Column: %s\n" +"\n" +"%s" +msgstr "" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#, python-format +msgid "Wrong python condition defined." +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "ex. 'Draft' or 'Direct Manager'" +msgstr "" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "ex. 'draft' or 'direct_manager'" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_ir_model__is_workflow +msgid "is Workflow" +msgstr "" + +#. module: soong_workflow +#: model:ir.module.category,name:soong_workflow.module_category_soong_workflow +msgid "Workflow Management" +msgstr "" + +#. module: soong_workflow +#: model:res.groups,name:soong_workflow.group_soong_workflow_user +msgid "User" +msgstr "" + +#. module: soong_workflow +#: model:res.groups,name:soong_workflow.group_soong_workflow_manager +msgid "Manager" +msgstr "" diff --git a/soong_workflow/i18n/zh_CN.po b/soong_workflow/i18n/zh_CN.po new file mode 100644 index 000000000..1e7edd098 --- /dev/null +++ b/soong_workflow/i18n/zh_CN.po @@ -0,0 +1,892 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * soong_workflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-15 07:40+0000\n" +"PO-Revision-Date: 2024-07-15 07:40+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "" +"Note: The result of executing the expression must be a " +"res.users recordset." +msgstr "编写定义审核者的Python代码。执行表达式的结果必须是res.users记录集。" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +msgid "" +"Note: The return value of Operation Code should be True." +msgstr "注意: 执行代码返回值应该是True" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +msgid "" +"Note: Workflow engine can only alter normal models like\n" +" crm.*, sale.*, hr.*, etc... (not transient like wizards nor abstract like reports)\n" +" also resource (res.*) & interface (ir.*) models are excluded from workflow engine." +msgstr "" +"注意: 工作流引擎只能改变常规模型,比如crm.*, sale.*, " +"hr.*,等等……(向导那样瞬态模型的不行,报告那样抽象模型的不行)还有resource (res.*) 和 (ir.*)模型也被排除在工作流引擎之外。" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__action_type +msgid "Action Type" +msgstr "动作类型" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "Actions" +msgstr "操作" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Activity" +msgstr "活动" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__group +msgid "Any user in a specific group" +msgstr "指定权限组" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__is_visible +msgid "Appear in Statusbar" +msgstr "出现在状态栏中" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "" +"Are you sure you want to remove this node? This will remove its connected " +"transitions as well." +msgstr "您确定要删除此节点吗?这也将删除其链接。" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Are you sure you want to remove this transition?" +msgstr "您确定要删除此链接吗?" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__sequence +msgid "Arrange buttons by defining sequence." +msgstr "通过定义顺序字段排列按钮。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__sequence +msgid "Arrange node by defining sequence." +msgstr "通过定义顺序字段排列节点。" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Attributes" +msgstr "属性" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "Auto Layout" +msgstr "自动布局" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__bg_color +msgid "Background Color" +msgstr "背景颜色" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__res_bg_color +msgid "Backgroung Color" +msgstr "背景颜色" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__btn_key +msgid "Button Key" +msgstr "按钮Key" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__name +msgid "Button String" +msgstr "按钮名称" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__button_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Buttons" +msgstr "按钮" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/tools/graph.py:0 +#, python-format +msgid "" +"Cannot compute diagram view. It seems that the flow is incorrect! Try to " +"remove some broken routes without using diagram view." +msgstr "无法计算diagram视图。看起来流程是不正确的!尝试在不使用diagram视图的情况下删除一些损坏的路线。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__flow_end +msgid "Check it if this node is the ending node." +msgstr "检查该节点是否为结束节点。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__flow_start +msgid "Check it if this node is the starting node." +msgstr "检查该节点是否为开始节点。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__action_type +msgid "Choose type of action to be trigger by the button." +msgstr "通过按钮选择要触发的动作类型。" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_button +msgid "Condition" +msgstr "条件" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Confirm" +msgstr "确认" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__is_highlight +msgid "Control highlighting of the button if needs user attention.." +msgstr "如果需要用户注意,控制按钮的突出显示。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__is_visible +msgid "Control visiability of the node/state in view." +msgstr "控制视图中节点状态的可见性。" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Create:" +msgstr "创建:" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__create_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__create_uid +msgid "Created by" +msgstr "创建人" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__create_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__create_date +msgid "Created on" +msgstr "创建日期" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__current_stage_highlight_color +msgid "Current Stage Highlight Color" +msgstr "当前阶段高亮颜色" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Delete" +msgstr "删除" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__node_to +msgid "Destination Node" +msgstr "目标节点" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__diagram_position +msgid "Diagram Position" +msgstr "Diagram中的位置" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_view.js:0 +#, python-format +msgid "DiagramPlus" +msgstr "Diagram Plus" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__display_name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Do you really want to change the positions of the nodes?" +msgstr "你真的想改变节点的位置吗?" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__has_icon +msgid "Enable it to add icon to the button." +msgstr "使其能够向按钮添加图标。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__model_id +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__model_id +msgid "Enter business model you would like to modify its workflow." +msgstr "输入您想要修改其工作流的业务模型。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__name +msgid "Enter button string name that will appear in the view." +msgstr "输入将出现在视图中的按钮名称。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_link__condition_code +msgid "Enter condition to pass thru this link." +msgstr "输入通过此链接的条件。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_link__name +msgid "Enter friendly link name that describe the process." +msgstr "输入描述流程的链接名称。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__icon +msgid "" +"Enter icon name like: fa-print, it's recommended to use FontAwesome Icons." +msgstr "输入图标名称,如:fa-print,建议使用FontAwesome Icons。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__name +msgid "Enter string name of the node." +msgstr "输入节点名称。" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__name +msgid "Field" +msgstr "字段" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Field d_position_field must be present in diagram_plus[node], because set " +"auto_layout='False'" +msgstr "字段d_position_field必须出现在diagram_plus[node]中,因为set auto_layout='False'" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__field_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Fields" +msgstr "字段" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__flow_end +msgid "Flow End" +msgstr "流程结束" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__flow_start +msgid "Flow Start" +msgstr "流程开始" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__node_name +msgid "Generated technical name which used by backend code." +msgstr "生成后端代码使用的技术名称。" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__name +msgid "Give workflow a name." +msgstr "给工作流起一个名字" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__group_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Groups" +msgstr "群组" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__has_icon +msgid "Has Icon" +msgstr "有图标" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__btn_hide +msgid "Hide Button if Condition isn't fulfilled" +msgstr "如果条件不满足,隐藏按钮" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__id +msgid "ID" +msgstr "" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__icon +msgid "Icon" +msgstr "图标" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__btn_hide +msgid "If condition is false the button will be hidden." +msgstr "如果条件值为false,按钮将被隐藏。" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__in_link_ids +msgid "Incoming Transitions" +msgstr "传入的链接" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Ingoing Links" +msgstr "传入链接" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__invisible +msgid "Invisible" +msgstr "隐藏" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__is_highlight +msgid "Is Highlighted" +msgstr "突出显示" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__label_color +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__res_label_color +msgid "Label Color" +msgstr "标签的颜色" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__write_uid +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__write_uid +msgid "Last Updated by" +msgstr "最后更新人" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__write_date +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__write_date +msgid "Last Updated on" +msgstr "最后更新日期" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__link_id +msgid "Link" +msgstr "链接" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__link_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Links" +msgstr "链接" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Load Model Fields" +msgstr "加载模型字段" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Main Information" +msgstr "主要信息" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__model_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__model_id +msgid "Model" +msgstr "模型" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__model_id +msgid "Model Ref." +msgstr "模型参考" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_uniq_model +#, python-format +msgid "Model must be unique." +msgstr "模型必须是唯一的。" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_ir_model +msgid "Models" +msgstr "模型" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__name +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__name +msgid "Name" +msgstr "名称" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/xml/base_diagram.xml:0 +#, python-format +msgid "New Node" +msgstr "新节点" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__node_id +msgid "Node Ref." +msgstr "节点参考" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__node_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_link +msgid "Nodes" +msgstr "节点" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Nodes' sequence must be unique per workflow." +msgstr "每个工作流的节点顺序必须是唯一的。" + +#. module: soong_workflow +#. odoo-python +#: code:addons/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/models/ir_ui_view.py:0 +#, python-format +msgid "" +"Only 'node' and 'arrow' tags allowed in 'diagram_plus_view', but " +"%(tag_name)s found." +msgstr "在'diagram_plus_view'中只允许'node'和'arrow'标记,但是发现了%(tag_name)s。" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Open:" +msgstr "打开:" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__condition_code +msgid "Operation Code" +msgstr "执行代码" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "Outgoing Links" +msgstr "传出链接" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__out_link_ids +msgid "Outgoing Transitions" +msgstr "传出的链接" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__code +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__code +msgid "Python Code" +msgstr "Python代码" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__expression +msgid "Python Expression" +msgstr "Python表达式" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__readonly +msgid "Readonly" +msgstr "只读" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow__remove_default_attrs_mod +msgid "Remove Default Attributes & Modifiers" +msgstr "删除默认属性和修饰符" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__required +msgid "Required" +msgstr "必填" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__res_id +msgid "Res ID" +msgstr "记录ID" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__res_model +msgid "Res Model" +msgstr "记录模型" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_expression +msgid "Review Expression" +msgstr "审批表达式" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_id +msgid "Reviewer" +msgstr "审批者" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__reviewer_group_id +msgid "Reviewer group" +msgstr "审批组" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node__use_custom_colors +msgid "Select colors from the palette manually" +msgstr "手动从调色板中选择颜色" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__sequence +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__sequence +msgid "Sequence" +msgstr "顺序" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__server_action_id +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__action +msgid "Server Action" +msgstr "服务器动作" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow +msgid "Soong Workflow" +msgstr "工作流" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_link +msgid "Soong Workflow Links" +msgstr "工作流链接" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_mixin +msgid "Soong Workflow Mixin" +msgstr "工作流Mixin类" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node_button +msgid "Soong Workflow Node Buttons" +msgstr "工作流节点按钮" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node_field +msgid "Soong Workflow Node Fields" +msgstr "工作流节点字段" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_node +msgid "Soong Workflow Nodes" +msgstr "工作流节点" + +#. module: soong_workflow +#: model:ir.model,name:soong_workflow.model_soong_workflow_record +msgid "Soong Workflow Record" +msgstr "工作流记录" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_link.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_link.py:0 +#, python-format +msgid "Sorry, source & destination nodes can't be the same." +msgstr "抱歉,源节点和目标节点不能相同。" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__node_from +msgid "Source Node" +msgstr "源节点" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__review_type__individual +msgid "Specific user" +msgstr "指定用户" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#, python-format +msgid "Start and end cannot be selected for the node" +msgstr "该节点不能同时选择“流程开始”和“流程结束”" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__state +msgid "Status" +msgstr "状态" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__node_name +msgid "Technical Name" +msgstr "技术名称" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__current_stage_highlight_color +msgid "The color to highlight the current stage on flow" +msgstr "在流程中突出显示当前阶段的颜色" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow__remove_default_attrs_mod +msgid "" +"This option will remove default attributes set on fields & buttons of current model view in order to customized all attributes depending on your needs\n" +"Attributes like: [required, readonly, invisible]." +msgstr "此选项将删除当前模型视图字段和按钮上设置的默认属性,以便根据您的需要自定义所有属性,例如:[必需,只读,隐藏]。" + +#. module: soong_workflow +#. odoo-javascript +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#: code:addons/soong_workflow/crnd_web_diagram_plus/static/src/js/diagram_controller.js:0 +#, python-format +msgid "Transition" +msgstr "链接" + +#. module: soong_workflow +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__link +msgid "Trigger Link" +msgstr "触发链接" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__use_custom_colors +msgid "Use Custom Colors" +msgstr "使用自定义颜色" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_field__user_ids +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node_field +msgid "Users" +msgstr "用户" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__review_type +msgid "Validated by" +msgstr "审批人" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow +#, python-format +msgid "View Flow" +msgstr "工作流视图" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_ir_model__is_workflow +msgid "Whether the model supports workflow approval" +msgstr "模型是否支持工作流审批" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__win_act_id +#: model:ir.model.fields.selection,name:soong_workflow.selection__soong_workflow_node_button__action_type__win_act +msgid "Window Action" +msgstr "窗口操作" + +#. module: soong_workflow +#: model:ir.actions.act_window,name:soong_workflow.soong_workflow_record_action +#: model:ir.ui.menu,name:soong_workflow.soong_workflow_record_menu +#: model_terms:ir.ui.view,arch_db:soong_workflow.soong_workflow_record_search +#: model_terms:ir.ui.view,arch_db:soong_workflow.soong_workflow_record_tree +msgid "WorkFollow Record" +msgstr "工作流记录" + +#. module: soong_workflow +#: model:ir.actions.act_window,name:soong_workflow.action_soong_workflow +#: model:ir.ui.menu,name:soong_workflow.menu_soong_workflow +#: model:ir.ui.menu,name:soong_workflow.menu_soong_workflow_root +msgid "Workflow" +msgstr "工作流" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__node_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__node_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__node_id +msgid "Workflow Node Ref." +msgstr "工作流节点参考" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_link__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_mixin__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_node_button__workflow_id +#: model:ir.model.fields,field_description:soong_workflow.field_soong_workflow_record__workflow_id +msgid "Workflow Ref." +msgstr "工作流参考" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_node_uniq_node_name +#, python-format +msgid "Workflow and Technical Name must be unique." +msgstr "“工作流”和“技术名称”不能重复。" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Workflow must have only one start node." +msgstr "工作流必须只有一个开始节点。" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#: model:ir.model.constraint,message:soong_workflow.constraint_soong_workflow_uniq_name +#, python-format +msgid "Workflow name must be unique." +msgstr "工作流名称必须唯一。" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#, python-format +msgid "Workflow: %(name)s" +msgstr "工作流: %(name)s" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow.py:0 +#, python-format +msgid "Workflow: %(type_name)s" +msgstr "工作流: %(type_name)s" + +#. module: soong_workflow +#: model:ir.model.fields,help:soong_workflow.field_soong_workflow_node_button__reviewer_expression +msgid "" +"Write Python code that defines the reviewer. The result of executing the " +"expression must be a res.users recordset." +msgstr "编写定义审核者的Python代码。执行表达式的结果必须是res.users记录集。" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_node.py:0 +#, python-format +msgid "" +"Wrong python code defined.\n" +"\n" +"Error: %s\n" +"Line: %s, Column: %s\n" +"\n" +"%s" +msgstr "" +"python代码定义错误。\n" +"\n" +"错误: %s\n" +"行: %s, 列: %s\n" +"\n" +"%s" + +#. module: soong_workflow +#. odoo-python +#: code:addons/soong_workflow/models/soong_workflow_mixin.py:0 +#: code:addons/soong_workflow/soong_workflow/models/soong_workflow_mixin.py:0 +#, python-format +msgid "Wrong python condition defined." +msgstr "定义了错误的python条件。" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "ex. 'Draft' or 'Direct Manager'" +msgstr "例如 ‘草稿’或者‘直系领导’" + +#. module: soong_workflow +#: model_terms:ir.ui.view,arch_db:soong_workflow.view_form_soong_workflow_node +msgid "ex. 'draft' or 'direct_manager'" +msgstr "例如 ‘草稿’或者‘直系领导’" + +#. module: soong_workflow +#: model:ir.model.fields,field_description:soong_workflow.field_ir_model__is_workflow +msgid "is Workflow" +msgstr "是工作流" + +#. module: soong_workflow +#: model:ir.module.category,name:soong_workflow.module_category_soong_workflow +msgid "Workflow Management" +msgstr "工作流程管理" + +#. module: soong_workflow +#: model:res.groups,name:soong_workflow.group_soong_workflow_user +msgid "User" +msgstr "用户" + +#. module: soong_workflow +#: model:res.groups,name:soong_workflow.group_soong_workflow_manager +msgid "Manager" +msgstr "经理" diff --git a/soong_workflow/models/__init__.py b/soong_workflow/models/__init__.py new file mode 100644 index 000000000..0fc916d1d --- /dev/null +++ b/soong_workflow/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import soong_workflow +from . import soong_workflow_node +from . import soong_workflow_link +from . import ir_model +from . import soong_workflow_record +from . import soong_workflow_mixin diff --git a/soong_workflow/models/ir_model.py b/soong_workflow/models/ir_model.py new file mode 100644 index 000000000..65104478f --- /dev/null +++ b/soong_workflow/models/ir_model.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +import logging + +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError + +_logger = logging.getLogger(__name__) + + +class IrModel(models.Model): + _inherit = "ir.model" + + is_workflow = fields.Boolean( + string="is Workflow", + default=False, + help="Whether the model supports workflow approval", + ) + + def _reflect_model_params(self, model): + vals = super(IrModel, self)._reflect_model_params(model) + vals["is_workflow"] = ( + issubclass(type(model), self.pool["soong.workflow.mixin"]) + and model._name != "soong.workflow.mixin" + ) + return vals diff --git a/soong_workflow/models/soong_workflow.py b/soong_workflow/models/soong_workflow.py new file mode 100644 index 000000000..4c0dca55c --- /dev/null +++ b/soong_workflow/models/soong_workflow.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _, modules +from odoo.exceptions import ValidationError, UserError +import logging + +_logger = logging.getLogger(__name__) + +MODEL_DOMAIN = """[ + ('is_workflow', '=', True), + ('state', '=', 'base'), + ('transient', '=', False), + '!', + '|', + '|', + '|', + '|', + '|', + '|', + '|', + ('model', '=ilike', 'res.%'), + ('model', '=ilike', 'ir.%'), + ('model', '=ilike', 'soong.workflow%'), + ('model', '=ilike', 'bus.%'), + ('model', '=ilike', 'base.%'), + ('model', '=ilike', 'base_%'), + ('model', '=', 'base'), + ('model', '=', '_unknown'), + ]""" + + +class SoongWorkflow(models.Model): + _name = "soong.workflow" + _description = "Soong Workflow" + + _sql_constraints = [ + ("uniq_name", "unique(name)", _("Workflow name must be unique.")), + ("uniq_model", "unique(model_id)", _("Model must be unique.")), + ] + + name = fields.Char(string="Name", help="Give workflow a name.") + model_id = fields.Many2one( + "ir.model", + string="Model", + domain=MODEL_DOMAIN, + help="Enter business model you would like to modify its workflow.", + ) + node_ids = fields.One2many( + comodel_name="soong.workflow.node", inverse_name="workflow_id", string="Nodes" + ) + link_ids = fields.One2many( + comodel_name="soong.workflow.link", inverse_name="workflow_id", string="Links" + ) + remove_default_attrs_mod = fields.Boolean( + string="Remove Default Attributes & Modifiers", + default=True, + help="This option will remove default attributes set on fields & buttons of current model view in order to customized all attributes depending on your needs\nAttributes like: [required, readonly, invisible].", + ) + + current_stage_highlight_color = fields.Char( + default="rgb(0, 255, 255)", + help="The color to highlight the current stage on flow", + ) + + @api.constrains("node_ids") + def validate_nodes(self): + # Objects + wkf_node_obj = self.env["soong.workflow.node"] + for rec in self: + # Must have one flow start node + res = rec.node_ids.search_count( + [ + ("workflow_id", "=", rec.id), + ("flow_start", "=", True), + ] + ) + if res > 1: + raise ValidationError(_("Workflow must have only one start node.")) + # Nodes' sequence must be unique per workflow + for node in rec.node_ids: + res = wkf_node_obj.search_count( + [ + ("id", "!=", node.id), + ("workflow_id", "=", rec.id), + ("sequence", "=", node.sequence), + ] + ) + if res: + raise ValidationError( + _("Nodes' sequence must be unique per workflow.") + ) + + def action_workflow_diagram(self): + self.ensure_one() + action = self.env.ref("soong_workflow.action_soong_workflow").read()[0] + action["name"] = _("Workflow: %(type_name)s") % {"type_name": self.display_name} + action["context"] = {"default_workflow_id": self.id} + action["res_id"] = self.id + action["views"] = [(False, "diagram_plus"), (False, "form")] + return action diff --git a/soong_workflow/models/soong_workflow_link.py b/soong_workflow/models/soong_workflow_link.py new file mode 100644 index 000000000..c275a4aec --- /dev/null +++ b/soong_workflow/models/soong_workflow_link.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError, UserError +import logging + +_logger = logging.getLogger(__name__) + +CONDITION_CODE_TEMP = """# Available locals: +# - time, date, datetime, timedelta: Python libraries. +# - env: Odoo Environement. +# - model: Model of the record on which the action is triggered. +# - obj: Record on which the action is triggered if there is one, otherwise None. +# - user, Current user object. +# - workflow: Workflow engine. +# - syslog : syslog(message), function to log debug information to Odoo logging file or console. +# - warning: warning(message), Warning Exception to use with raise. + + +result = True""" + + +class SoongWorkflowLink(models.Model): + _name = "soong.workflow.link" + _description = "Soong Workflow Links" + + name = fields.Char( + string="Name", help="Enter friendly link name that describe the process." + ) + workflow_id = fields.Many2one( + comodel_name="soong.workflow", + string="Workflow Ref.", + ondelete="cascade", + required=True, + ) + condition_code = fields.Text( + string="Operation Code", + default=CONDITION_CODE_TEMP, + help="Enter condition to pass thru this link.", + ) + node_from = fields.Many2one( + comodel_name="soong.workflow.node", + string="Source Node", + ondelete="cascade", + required=True, + ) + node_to = fields.Many2one( + comodel_name="soong.workflow.node", + string="Destination Node", + ondelete="cascade", + required=True, + ) + + @api.constrains("node_from", "node_to") + def check_nodes(self): + for rec in self: + if rec.node_from == rec.node_to: + raise ValidationError( + _("Sorry, source & destination nodes can't be the same.") + ) + + @api.onchange("node_from", "node_to") + def onchange_nodes(self): + for rec in self: + if rec.node_from and rec.node_to: + rec.name = "%s -> %s" % (rec.node_from.name, rec.node_to.name) + + def trigger_link(self): + # Variables + cx = self.env.context + model_name = cx.get("active_model") + rec_id = cx.get("active_id") + model_obj = self.env[model_name].sudo() + rec = model_obj.browse(rec_id) + # Validation + if rec.state == self.node_from.node_name: + rec.state = self.node_to.node_name + return True diff --git a/soong_workflow/models/soong_workflow_mixin.py b/soong_workflow/models/soong_workflow_mixin.py new file mode 100644 index 000000000..877903f7e --- /dev/null +++ b/soong_workflow/models/soong_workflow_mixin.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import UserError +from datetime import datetime, date, time, timedelta +from lxml import etree +import logging + +_logger = logging.getLogger(__name__) + + +class SoongWorkflowMixin(models.AbstractModel): + _name = "soong.workflow.mixin" + _description = "Soong Workflow Mixin" + + @api.model + def _compute_default_state(self): + # Objects + wkf_obj = self.env["soong.workflow"] + rec_model = self._name + # Get Flow Start + wkf_recs = wkf_obj.search([("model_id", "=", rec_model)]) + for wkf_rec in wkf_recs: + for node in wkf_rec.node_ids: + if node.flow_start: + return node.node_name + + @api.model + def _load_state(self): + # Objects + wkf_obj = self.env["soong.workflow"] + # Variables + rec_model = self._name + ret = [] + # Get Status + wkf_recs = wkf_obj.search([("model_id", "=", rec_model)]) + for rec in wkf_recs: + for node in rec.node_ids: + ret.append((node.node_name, node.name)) + return ret + + state = fields.Selection( + selection="_load_state", + string="Status", + default=_compute_default_state, + track_visibility="onchange", + ) + workflow_id = fields.Many2one( + comodel_name="soong.workflow", + string="Workflow Ref.", + compute="_compute_workflow_id", + ) + + @api.depends("state") + def _compute_workflow_id(self): + # Objects + wkf_obj = self.env["soong.workflow"] + for rec in self: + # Variables + rec_model = rec._name + # Get Workflow + rec.workflow_id = wkf_obj.search([("model_id", "=", rec_model)]) + + node_id = fields.Many2one( + comodel_name="soong.workflow.node", + string="Workflow Node Ref.", + compute="_compute_node_id", + ) + + @api.depends("state") + def _compute_node_id(self): + # Objects + wkf_obj = self.env["soong.workflow"] + for rec in self: + # Variables + rec_model = rec._name + # Get Workflow Node + rec.node_id = wkf_obj.search( + [("model_id", "=", rec_model)] + ).node_ids.filtered(lambda n: n.node_name == rec.state) + + def btn_exec_action(self): + """ + Got invoked when a workflow button is called. + :return: button action return value. + """ + # Variables + cx = self.env.context.copy() or {} + cx.update({"active_id": self.id, "active_ids": self.ids}) + wkf_btn_obj = self.env["soong.workflow.node.button"] + btn_rec = wkf_btn_obj.search([("btn_key", "=", cx.get("btn_key", False))]) + if btn_rec: + return btn_rec.with_context(cx).run() + + @api.constrains("state") + def _update_wkf_records(self): + for rec in self: + record_obj = self.env["soong.workflow.record"].search( + [("res_model", "=", rec._name), ("res_id", "=", rec.id)] + ) + if not record_obj: + record_obj = self.env["soong.workflow.record"].create( + { + "res_model": rec._name, + "res_id": rec.id, + "workflow_id": rec.workflow_id.id, + "node_id": rec.node_id.id, + } + ) + record_obj.update( + {"workflow_id": rec.workflow_id.id, "node_id": rec.node_id.id} + ) + + def action_open_diagram(self): + self.ensure_one() + current_node_id = self.workflow_id.node_ids.filtered( + lambda n: n.node_name == self.state + ) + highlight_color = self.workflow_id.current_stage_highlight_color + + return { + "name": _("Workflow: %(name)s") + % { + "name": self.display_name, + }, + "res_model": "soong.workflow", + "type": "ir.actions.act_window", + "res_id": self.workflow_id.id, + "context": { + "highlight_node_id": current_node_id.id, + "highlight_node_color": highlight_color, + "diagram_readonly": True, + }, + "views": [(False, "diagram_plus")], + } + + # @api.model + # def _get_view(self, view_id=None, view_type='form', **options): + # """ Set the correct label for `unit_amount`, depending on company UoM """ + # arch, view = super()._get_view(view_id, view_type, **options) + # for node in arch.xpath("//header"): + # node.append(etree.XML("""booooooo""")) + # return arch, view + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + res = super().get_view(view_id, view_type, **options) + + if view_type in ["tree", "form"]: + if "field" in res["arch"]: + res = self._remove_default_header(view_type, res) + res = self._remove_default_modifiers(view_type, res) + res = self._load_buttons_view(view_type, res) + res = self._load_button_box_view(view_type, res) + res = self._load_state_view(view_type, res) + res = self._update_fields_view(view_type, res) + res = self._add_state_tree_view(view_type, res) + return res + + def _remove_default_header(self, view_type, res): + """ + Removes default header element from form view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Variables + arch = etree.fromstring(res["arch"]) + # Find Default Header Element & Remove it + if view_type == "form": + header_el = arch.xpath("//form/header") + header_el = header_el[0] if header_el else False + if header_el is not False: + header_el.getparent().remove(header_el) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _remove_default_modifiers(self, view_type, res): + """ + Removed fields default modifiers & attributes in form view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Variables + model = self._name + wkf_obj = self.env["soong.workflow"] + wkf_rec = wkf_obj.search([("model_id", "=", model)]) + arch = etree.fromstring(res["arch"]) + + # Validation + if not wkf_rec.remove_default_attrs_mod: + return res + fields_name = wkf_rec.node_ids.field_ids.name.mapped("name") + # Find Field & Remove Modifiers + if view_type == "form": + for field in arch.iter("field"): + field_name = field.attrib.get("name", False) + if not field_name or field_name not in fields_name: + continue + for attr in field.attrib.keys(): + if attr != "name": + field.attrib.pop(attr) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _load_buttons_view(self, view_type, res): + """ + Adds buttons to state bar in form view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Variables + this = self + model = self._name + wkf_obj = self.env["soong.workflow"] + arch = etree.fromstring(res["arch"]) + + # Helper Functions + def _check_condition_code(btn): + locals_dict = { + "env": this.env, + "model": this.env[model], + "obj": None, + "user": this.env["res.users"].browse(this.env.uid), + "datetime": datetime, + "time": time, + "date": date, + "timedelta": timedelta, + "workflow": this.env["soong.workflow"], + "warning": this.warning, + "syslog": this.syslog, + } + try: + safe_eval( + btn.reviewer_expression, + locals_dict=locals_dict, + mode="exec", + nocopy=True, + ) + user = "user" in locals_dict and locals_dict["user"] or False + if user and self.env.user in user: + return True + return False + except Warning as ex: + raise ex + except: + raise UserError(_("Wrong python condition defined.")) + + # Find header & ADD button + if view_type == "form": + # Get Some Elements + form_el = arch.xpath("//form") + form_el = form_el[0] if form_el else False + header_el = arch.xpath("//form/header") + header_el = header_el[0] if header_el else False + # Create Header Element If not Exists + if header_el is False: + header_el = etree.Element("header") + form_el.insert(0, header_el) + # Construct Buttons & Fields Tags + wkf_rec = wkf_obj.search([("model_id", "=", model)]) + for node in wkf_rec.node_ids: + for button in node.button_ids: + # Check Condition Code + if button.btn_hide: + if button.review_type == "individual": + if self.env.user != button.reviewer_id: + continue + elif button.review_type == "group": + if self.env.user not in button.reviewer_group_id.users: + continue + else: + if not _check_condition_code(button): + continue + # Add Button to View + btn_exec_action_el = etree.SubElement(header_el, "button") + btn_exec_action_el.set("name", "btn_exec_action") + btn_exec_action_el.set("string", button.name) + btn_exec_action_el.set("type", "object") + if button.is_highlight: + btn_exec_action_el.set("class", "oe_highlight") + if button.has_icon: + btn_exec_action_el.set("icon", button.icon) + btn_exec_action_el.set( + "invisible", "state != '%s'" % node.node_name + ) + btn_exec_action_el.set( + "context", + "{'btn_key':'%s','active_model':'%s'}" + % (button.btn_key, self._name), + ) + # setup_modifiers(btn_exec_action_el) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _load_button_box_view(self, view_type, res): + """ + Adds button_box in form view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + arch = etree.fromstring(res["arch"]) + + # Find header & ADD button + if view_type == "form": + # Get Some Elements + form_sheet_el = arch.xpath("//form/sheet") + form_sheet_el = form_sheet_el[0] if form_sheet_el else False + button_box_el = arch.xpath("//form/sheet/div[hasclass('oe_button_box')]") + button_box_el = button_box_el[0] if button_box_el else False + # Create button_box Element If not Exists + if button_box_el is False: + button_box_el = etree.Element("div") + button_box_el.set("class", "oe_button_box") + button_box_el.set("name", "button_box") + form_sheet_el.insert(0, button_box_el) + # Add Button to View + btn_exec_action_el = etree.SubElement(button_box_el, "button") + btn_exec_action_el.set("name", "action_open_diagram") + btn_exec_action_el.set("type", "object") + btn_exec_action_el.set("string", _("View Flow")) + btn_exec_action_el.set("class", "oe_stat_button") + btn_exec_action_el.set("icon", "fa-code-fork") + + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _load_state_view(self, view_type, res): + """ + Adds state bar to form view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Variables + wkf_obj = self.env["soong.workflow"] + model = self._name + visible_seq = [] + arch = etree.fromstring(res["arch"]) + wkf_rec = wkf_obj.search([("model_id", "=", model)]) + + # Find header & ADD state + if view_type == "form": + # Get Header Element + header_el = arch.xpath("//form/header") + header_el = header_el[0] if header_el else False + # Create State Element If not Exists + if header_el is not False: + state_el = etree.Element("field") + state_el.set("name", "state") + state_el.set("widget", "statusbar") + # Loop all nodes + for node in wkf_rec.node_ids: + # Read Visible States + if node.is_visible: + visible_seq.append(node.node_name) + # Set Attributes & Setup Modifiers for State Field + if visible_seq: + state_el.set("statusbar_visible", ",".join(visible_seq)) + header_el.append(state_el) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _update_fields_view(self, view_type, res): + """ + Updates fields attributes. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Objects + wkf_obj = self.env["soong.workflow"] + user_obj = self.env["res.users"] + # Variables + model = self._name + uid = self._uid + wkf_rec = wkf_obj.search([("model_id", "=", model)]) + arch = etree.fromstring(res["arch"]) + + # Helper Functions + def _get_external_id(group): + arr = [] + ext_ids = group._get_external_ids() + for ext_id in ext_ids: + arr.append(ext_ids[ext_id][0]) + return arr + + if view_type == "form": + # Read fields of view + for field in arch.iter("field"): + field_name = field.attrib.get("name", False) + if not field_name: + continue + # Get Fields Instance + field_inst = arch.xpath("//field[@name='%s']" % str(field_name)) + field_inst = field_inst[0] if field_inst else False + # Scope Variables + readonly_arr = [] + required_arr = [] + invisible_arr = [] + if field_inst is not False: + # Loop all nodes + for node in wkf_rec.node_ids: + # Loop Other Nodes + for field_attrs in node.field_ids: + # Record all states for each attribute + if ( + field_inst is not False + and field_attrs.name.name == field_inst.attrib["name"] + ): + flag_show = True + if field_attrs.readonly: + readonly_arr.append(node.node_name) + if field_attrs.required: + required_arr.append(node.node_name) + if field_attrs.invisible: + invisible_arr.append(node.node_name) + # Check Users & Groups + if field_attrs.user_ids: + user_rec = user_obj.browse(uid) + if user_rec not in field_attrs.user_ids: + flag_show = False + if field_attrs.group_ids: + has_group = False + user_rec = user_obj.browse(uid) + ext_ids = _get_external_id(field_attrs.group_ids) + for ext_id in ext_ids: + has_group = user_rec.has_group(ext_id) + if not has_group: + flag_show = False + if not flag_show and ( + field_attrs.group_ids or field_attrs.user_ids + ): + invisible_arr.append(node.node_name) + # Set Attributes + if readonly_arr: + field_inst.set("readonly", "state in %s" % readonly_arr) + if required_arr: + field_inst.set("required", "state in %s" % required_arr) + if invisible_arr: + field_inst.set("invisible", "state in %s" % invisible_arr) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def _add_state_tree_view(self, view_type, res): + """ + Adds state field to tree view. + :param view_type: Type of view now rendering. + :param res: View resource data. + :return: Updated view resource. + """ + # Variables + model = self._name + model_obj = self.env[model] + arch = etree.fromstring(res["arch"]) + + # Add State Field to Tree View if not Exists + if view_type == "tree": + # Get Header Element + tree_el = arch.xpath("//tree") + tree_el = tree_el[0] if tree_el else False + # Get State Element + state_el = arch.xpath("//field[@name='state']") + state_el = state_el[0] if state_el else False + if state_el is False: + state_el = etree.Element("field") + state_el.set("name", "state") + tree_el.append(state_el) + res["arch"] = etree.tostring(arch, encoding="unicode").replace("\t", "") + return res + + def warning(self, msg): + if not isinstance(msg, str): + msg = str(msg) + raise UserError(msg) + + def syslog(self, msg): + if not isinstance(msg, str): + msg = str(msg) + _logger.info(msg) diff --git a/soong_workflow/models/soong_workflow_node.py b/soong_workflow/models/soong_workflow_node.py new file mode 100644 index 000000000..c2aa7d95c --- /dev/null +++ b/soong_workflow/models/soong_workflow_node.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError, UserError +from datetime import datetime, date, time, timedelta +import random +import string +import logging + +_logger = logging.getLogger(__name__) + +CONDITION_CODE_TEMP = """# Available locals: +# - time, date, datetime, timedelta: Python libraries. +# - env: Odoo Environement. +# - model: Model of the record on which the action is triggered. +# - obj: Record on which the action is triggered if there is one, otherwise None. +# - user, Current user object. +# - workflow: Workflow engine. +# - syslog : syslog(message), function to log debug information to Odoo logging file or console. +# - warning: warning(message), Warning Exception to use with raise. +To return an user, assign: + + +user = env.user""" + +PYTHON_CODE_TEMP = """# Available locals: +# - time, date, datetime, timedelta: Python libraries. +# - env: Odoo Environement. +# - model: Model of the record on which the action is triggered. +# - obj: Record on which the action is triggered if there is one, otherwise None. +# - user, Current user object. +# - workflow: Workflow engine. +# - syslog : syslog(message), function to log debug information to Odoo logging file or console. +# - warning: warning(message), Warning Exception to use with raise. +# To return an action, assign: action = {...} + + +""" +DEFAULT_BG_COLOR = "rgb(255, 255, 255,1)" +DEFAULT_LABEL_COLOR = "rgb(81, 81, 81,1)" +START_BG_COLOR = "rgba(120,120,120,1)" +START_LABEL_COLOR = "rgba(255,255,255,1)" +END_BG_COLOR = "rgba(120,120,120,1)" +END_LABEL_COLOR = "rgba(255,255,255,1)" + + +class SoongWorkflowNode(models.Model): + _name = "soong.workflow.node" + _description = "Soong Workflow Nodes" + _order = "sequence" + + _sql_constraints = [ + ( + "uniq_node_name", + "unique(workflow_id,node_name)", + _("Workflow and Technical Name must be unique."), + ), + ] + + sequence = fields.Integer( + string="Sequence", default=10, help="Arrange node by defining sequence." + ) + name = fields.Char( + string="Name", translate=True, help="Enter string name of the node." + ) + node_name = fields.Char( + string="Technical Name", + help="Generated technical name which used by backend code.", + ) + workflow_id = fields.Many2one( + comodel_name="soong.workflow", + string="Workflow Ref.", + ondelete="cascade", + required=True, + ) + model_id = fields.Many2one( + "ir.model", + string="Model Ref.", + domain="[('state','=','base')]", + related="workflow_id.model_id", + required=True, + ) + flow_start = fields.Boolean( + string="Flow Start", help="Check it if this node is the starting node." + ) + flow_end = fields.Boolean( + string="Flow End", help="Check it if this node is the ending node." + ) + is_visible = fields.Boolean( + string="Appear in Statusbar", + default=True, + help="Control visiability of the node/state in view.", + ) + out_link_ids = fields.One2many( + comodel_name="soong.workflow.link", + inverse_name="node_from", + string="Outgoing Transitions", + ) + in_link_ids = fields.One2many( + comodel_name="soong.workflow.link", + inverse_name="node_to", + string="Incoming Transitions", + ) + + field_ids = fields.One2many( + comodel_name="soong.workflow.node.field", + inverse_name="node_id", + string="Fields", + ) + button_ids = fields.One2many( + comodel_name="soong.workflow.node.button", + inverse_name="node_id", + string="Buttons", + ) + + diagram_position = fields.Char(readonly=True, string="Diagram Position") + bg_color = fields.Char(default=DEFAULT_BG_COLOR, string="Background Color") + label_color = fields.Char(default=DEFAULT_LABEL_COLOR, string="Label Color") + + # Custom colors + use_custom_colors = fields.Boolean( + help="Select colors from the palette manually", string="Use Custom Colors" + ) + res_bg_color = fields.Char( + compute="_compute_custom_colors", readonly=True, string="Backgroung Color" + ) + res_label_color = fields.Char( + compute="_compute_custom_colors", readonly=True, string="Label Color" + ) + + @api.depends( + "bg_color", "label_color", "use_custom_colors", "flow_start", "flow_end" + ) + def _compute_custom_colors(self): + for rec in self: + if rec.use_custom_colors: + rec.res_bg_color = rec.bg_color + rec.res_label_color = rec.label_color + elif rec.flow_start: + rec.res_bg_color = START_BG_COLOR + rec.res_label_color = START_LABEL_COLOR + elif rec.flow_end: + rec.res_bg_color = END_BG_COLOR + rec.res_label_color = END_LABEL_COLOR + else: + rec.res_bg_color = DEFAULT_BG_COLOR + rec.res_label_color = DEFAULT_LABEL_COLOR + + @api.constrains("flow_start", "flow_end") + def _check_flow_start_end(self): + for rec in self: + if rec.flow_start and rec.flow_end: + raise UserError(_("Start and end cannot be selected for the node")) + + @api.onchange("name") + def _compute_node_name(self): + for rec in self: + if rec and rec.name: + name = rec.name.lower().strip().replace(" ", "_") + rec.node_name = name + + def btn_load_fields(self): + # Variables + field_obj = self.env["ir.model.fields"] + for rec in self: + # Clear Fields List + rec.field_ids.unlink() + # Load Fields + fields_objs = field_obj.search([("model_id", "=", rec.model_id.id)]) + for field in fields_objs: + rec.field_ids.create( + { + "model_id": rec.model_id.id, + "node_id": rec.id, + "name": field.id, + } + ) + + +class SoongWorkflowNodeButton(models.Model): + _name = "soong.workflow.node.button" + _description = "Soong Workflow Node Buttons" + _order = "sequence" + + def _generate_key(self): + return "".join( + random.SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(10) + ) + + name = fields.Char( + string="Button String", + translate=True, + help="Enter button string name that will appear in the view.", + ) + sequence = fields.Integer( + string="Sequence", default=10, help="Arrange buttons by defining sequence." + ) + is_highlight = fields.Boolean( + string="Is Highlighted", + default=True, + help="Control highlighting of the button if needs user attention..", + ) + has_icon = fields.Boolean( + string="Has Icon", help="Enable it to add icon to the button." + ) + icon = fields.Char( + string="Icon", + help="Enter icon name like: fa-print, it's recommended to use FontAwesome Icons.", + ) + btn_key = fields.Char(string="Button Key", default=_generate_key) + btn_hide = fields.Boolean( + string="Hide Button if Condition isn't fulfilled", + help="If condition is false the button will be hidden.", + ) + review_type = fields.Selection( + string="Validated by", + default="individual", + selection=[ + ("individual", "Specific user"), + ("group", "Any user in a specific group"), + ("expression", "Python Expression"), + ], + ) + reviewer_id = fields.Many2one(comodel_name="res.users", string="Reviewer") + reviewer_group_id = fields.Many2one( + comodel_name="res.groups", string="Reviewer group" + ) + reviewer_expression = fields.Text( + string="Review Expression", + default=CONDITION_CODE_TEMP, + help="Write Python code that defines the reviewer. " + "The result of executing the expression must be a res.users " + "recordset.", + ) + action_type = fields.Selection( + [ + ("link", "Trigger Link"), + ("code", "Python Code"), + ("action", "Server Action"), + ("win_act", "Window Action"), + ], + string="Action Type", + default="link", + help="Choose type of action to be trigger by the button.", + ) + link_id = fields.Many2one(comodel_name="soong.workflow.link", string="Link") + code = fields.Text(string="Python Code", default=PYTHON_CODE_TEMP) + server_action_id = fields.Many2one( + comodel_name="ir.actions.server", string="Server Action" + ) + win_act_id = fields.Many2one( + comodel_name="ir.actions.act_window", string="Window Action" + ) + node_id = fields.Many2one( + comodel_name="soong.workflow.node", + string="Workflow Node Ref.", + ondelete="cascade", + required=True, + ) + workflow_id = fields.Many2one( + "soong.workflow", + string="Workflow Ref.", + required=True, + ondelete="cascade", + ) + + @api.onchange("node_id") + def change_workflow(self): + for rec in self: + if isinstance(rec.id, int) and rec.node_id and rec.node_id.workflow_id: + rec.workflow_id = rec.node_id.workflow_id.id + elif self.env.context.get("default_node_id", 0): + model_id = ( + self.env["soong.workflow.node"] + .sudo() + .browse(self.env.context.get("default_node_id")) + .model_id.id + ) + rec.workflow_id = ( + self.env["soong.workflow"] + .sudo() + .search([("model_id", "=", model_id)]) + ) + + @api.constrains("btn_key") + def validation(self): + for rec in self: + # Check if there is no duplicate button key + res = self.search_count( + [ + ("id", "!=", rec.id), + ("btn_key", "=", rec.btn_key), + ] + ) + if res: + rec.btn_key = self._generate_key() + + def run(self): + for rec in self: + # Check Condition Before Executing Action + result = False + cx = self.env.context.copy() or {} + locals_dict = { + "env": self.env, + "model": self.env[cx.get("active_model", False)], + "obj": self.env[cx.get("active_model", False)].browse( + cx.get("active_id", 0) + ), + "user": self.env.user, + "datetime": datetime, + "time": time, + "date": date, + "timedelta": timedelta, + "workflow": self.env["soong.workflow"], + "warning": self.warning, + "syslog": self.syslog, + } + if rec.btn_hide: + if rec.review_type == "individual": + if rec.reviewer_id == self.env.user: + result = True + elif rec.review_type == "group": + if self.env.user in self.reviewer_group_id.users: + result = True + else: + try: + safe_eval( + rec.reviewer_expression, + locals_dict=locals_dict, + mode="exec", + nocopy=True, + ) + user = "user" in locals_dict and locals_dict["user"] or False + if user and self.env.user in user: + result = True + except ValidationError as ex: + raise ex + except SyntaxError as ex: + raise UserError( + _( + "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" + % ( + ex.args[0], + ex.args[1][1], + ex.args[1][2], + ex.args[1][3], + ) + ) + ) + else: + result = True + if result: + # Run Proper Action + func = getattr(self, "_run_%s" % rec.action_type) + return func() + + def _run_win_act(self): + # Variables + cx = self.env.context.copy() or {} + win_act_obj = self.env["ir.actions.act_window"] + # Run Window Action + for rec in self: + action = win_act_obj.with_context(cx).browse(rec.win_act_id.id).read()[0] + action["context"] = cx + return action + return False + + def _run_action(self): + # Variables + srv_act_obj = self.env["ir.actions.server"] + # Run Server Action + for rec in self: + srv_act_rec = srv_act_obj.browse(rec.server_action_id.id) + return srv_act_rec.run() + + def _run_code(self): + # Variables + cx = self.env.context.copy() or {} + locals_dict = { + "env": self.env, + "model": self.env[cx.get("active_model", False)], + "obj": self.env[cx.get("active_model", False)].browse( + cx.get("active_id", 0) + ), + "user": self.env.user, + "datetime": datetime, + "time": time, + "date": date, + "timedelta": timedelta, + "workflow": self.env["soong.workflow"], + "warning": self.warning, + "syslog": self.syslog, + } + # Run Code + for rec in self: + try: + safe_eval(rec.code, locals_dict=locals_dict, mode="exec", nocopy=True) + action = "action" in locals_dict and locals_dict["action"] or False + if action: + return action + except Warning as ex: + raise ex + except SyntaxError as ex: + raise UserError( + _( + "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" + % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) + ) + ) + return True + + def _run_link(self): + for rec in self: + # Check Condition Before Executing Action + result = False + cx = self.env.context.copy() or {} + locals_dict = { + "env": self.env, + "model": self.env[cx.get("active_model", False)], + "obj": self.env[cx.get("active_model", False)].browse( + cx.get("active_id", 0) + ), + "user": self.env.user, + "datetime": datetime, + "time": time, + "date": date, + "timedelta": timedelta, + "workflow": self.env["soong.workflow"], + "warning": self.warning, + "syslog": self.syslog, + } + try: + safe_eval( + rec.link_id.condition_code, + locals_dict=locals_dict, + mode="exec", + nocopy=True, + ) + result = "result" in locals_dict and locals_dict["result"] or False + except ValidationError as ex: + raise ex + except SyntaxError as ex: + raise UserError( + _( + "Wrong python code defined.\n\nError: %s\nLine: %s, Column: %s\n\n%s" + % (ex.args[0], ex.args[1][1], ex.args[1][2], ex.args[1][3]) + ) + ) + if result: + # Trigger link function + return rec.link_id.trigger_link() + + def warning(self, msg): + if not isinstance(msg, str): + msg = str(msg) + raise UserError(msg) + + def syslog(self, msg): + if not isinstance(msg, str): + msg = str(msg) + _logger.info(msg) + + +class SoongWorkflowNodeField(models.Model): + _name = "soong.workflow.node.field" + _description = "Soong Workflow Node Fields" + + name = fields.Many2one(comodel_name="ir.model.fields", string="Field") + model_id = fields.Many2one( + comodel_name="ir.model", string="Model", domain="[('state','=','base')]" + ) + readonly = fields.Boolean(string="Readonly") + required = fields.Boolean(string="Required") + invisible = fields.Boolean(string="Invisible") + group_ids = fields.Many2many(comodel_name="res.groups", string="Groups") + user_ids = fields.Many2many(comodel_name="res.users", string="Users") + node_id = fields.Many2one( + comodel_name="soong.workflow.node", + string="Node Ref.", + ondelete="cascade", + required=True, + ) diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py new file mode 100644 index 000000000..055ffe5a1 --- /dev/null +++ b/soong_workflow/models/soong_workflow_record.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError, UserError +import logging + +_logger = logging.getLogger(__name__) + + +class SoongWorkflowRecord(models.Model): + _name = "soong.workflow.record" + _description = "Soong Workflow Record" + + @api.depends("res_model", "res_id") + def compute_name(self): + for rec in self: + if rec.res_model and rec.res_id: + rec.name = self.env[rec.res_model].browse(rec.res_id).display_name + else: + rec.name = False + + name = fields.Char(string="Name", compute="compute_name", store=True) + res_model = fields.Char(string="Res Model", required=True) + res_id = fields.Integer(string="Res ID", required=True) + workflow_id = fields.Many2one(comodel_name="soong.workflow", string="Workflow Ref.") + node_id = fields.Many2one( + comodel_name="soong.workflow.node", string="Workflow Node Ref." + ) + + def open_record(self): + return { + "type": "ir.actions.act_window", + "res_model": self.res_model, + "res_id": self.res_id, + "name": self.name, + "view_mode": "form", + } diff --git a/soong_workflow/security/ir.model.access.csv b/soong_workflow/security/ir.model.access.csv new file mode 100644 index 000000000..a5bde2ae9 --- /dev/null +++ b/soong_workflow/security/ir.model.access.csv @@ -0,0 +1,17 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +,,,,,,, +access_soong_workflow_user,soong_workflow_user,model_soong_workflow,soong_workflow.group_soong_workflow_user,1,0,0,0 +access_soong_workflow_mixin_user,soong_workflow_mixin_user,model_soong_workflow_mixin,soong_workflow.group_soong_workflow_user,1,1,1,1 +access_soong_workflow_node_user,soong_workflow_node_user,model_soong_workflow_node,soong_workflow.group_soong_workflow_user,1,0,0,0 +access_soong_workflow_node_field_user,soong_workflow_node_field_user,model_soong_workflow_node_field,soong_workflow.group_soong_workflow_user,1,0,0,0 +access_soong_workflow_node_button_user,soong_workflow_node_button_user,model_soong_workflow_node_button,soong_workflow.group_soong_workflow_user,1,0,0,0 +access_soong_workflow_link_user,soong_workflow_link_user,model_soong_workflow_link,soong_workflow.group_soong_workflow_user,1,0,0,0 +,,,,,,, +access_soong_workflow_manager,soong_workflow_manager,model_soong_workflow,soong_workflow.group_soong_workflow_manager,1,1,1,1 +access_soong_workflow_mixin_manager,soong_workflow_mixin_manager,model_soong_workflow_mixin,soong_workflow.group_soong_workflow_manager,1,1,1,1 +access_soong_workflow_node_manager,soong_workflow_node_manager,model_soong_workflow_node,soong_workflow.group_soong_workflow_manager,1,1,1,1 +access_soong_workflow_node_field_manager,soong_workflow_node_field_manager,model_soong_workflow_node_field,soong_workflow.group_soong_workflow_manager,1,1,1,1 +access_soong_workflow_node_button_manager,soong_workflow_node_button_manager,model_soong_workflow_node_button,soong_workflow.group_soong_workflow_manager,1,1,1,1 +access_soong_workflow_link_manager,soong_workflow_link_manager,model_soong_workflow_link,soong_workflow.group_soong_workflow_manager,1,1,1,1 +,,,,,,, +access_soong_workflow_record_user,soong_workflow_record_user,model_soong_workflow_record,soong_workflow.group_soong_workflow_user,1,1,1,1 \ No newline at end of file diff --git a/soong_workflow/security/soong_workflow_security.xml b/soong_workflow/security/soong_workflow_security.xml new file mode 100644 index 000000000..c3b5dfb27 --- /dev/null +++ b/soong_workflow/security/soong_workflow_security.xml @@ -0,0 +1,18 @@ + + + + + Workflow Management + + + User + + + + Manager + + + + + + \ No newline at end of file diff --git a/soong_workflow/static/description/icon.png b/soong_workflow/static/description/icon.png new file mode 100644 index 000000000..7e556e56e Binary files /dev/null and b/soong_workflow/static/description/icon.png differ diff --git a/soong_workflow/views/menuitem.xml b/soong_workflow/views/menuitem.xml new file mode 100644 index 000000000..81ac3f116 --- /dev/null +++ b/soong_workflow/views/menuitem.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/soong_workflow/views/soong_workflow_link_views.xml b/soong_workflow/views/soong_workflow_link_views.xml new file mode 100644 index 000000000..94b5a51c4 --- /dev/null +++ b/soong_workflow/views/soong_workflow_link_views.xml @@ -0,0 +1,45 @@ + + + + + soong.workflow.link.form + soong.workflow.link + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + soong.workflow.link.tree + soong.workflow.link + + + + + + + + + +
diff --git a/soong_workflow/views/soong_workflow_node_views.xml b/soong_workflow/views/soong_workflow_node_views.xml new file mode 100644 index 000000000..9ca1770a0 --- /dev/null +++ b/soong_workflow/views/soong_workflow_node_views.xml @@ -0,0 +1,216 @@ + + + + + soong.workflow.node.form + soong.workflow.node + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + +
+ + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + soong.workflow.tree + soong.workflow + + + + + + + + + + + soong.workflow.diagram + soong.workflow + diagram_plus + + + + + + + + + + + + Workflow + ir.actions.act_window + soong.workflow + tree,form + + + + + diff --git a/test_soong_workflow/__init__.py b/test_soong_workflow/__init__.py new file mode 100644 index 000000000..cde864bae --- /dev/null +++ b/test_soong_workflow/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/test_soong_workflow/__manifest__.py b/test_soong_workflow/__manifest__.py new file mode 100644 index 000000000..cd0489ef6 --- /dev/null +++ b/test_soong_workflow/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Soong Workflow Test", + "summary": "Soong Workflow测试模块", + "category": "Extra Tools", + "author": "soong bo", + "license": "AGPL-3", + "version": "17.0.0.1.0", + "depends": [ + "soong_workflow", + ], + "data": [ + "security/ir.model.access.csv", + 'views/development_development_views.xml', + ], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/test_soong_workflow/i18n/test_soong_workflow.pot b/test_soong_workflow/i18n/test_soong_workflow.pot new file mode 100644 index 000000000..34bcda3b0 --- /dev/null +++ b/test_soong_workflow/i18n/test_soong_workflow.pot @@ -0,0 +1,71 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_soong_workflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-13 14:49+0000\n" +"PO-Revision-Date: 2024-07-13 14:49+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__create_uid +msgid "Created by" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__create_date +msgid "Created on" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.ui.menu,name:test_soong_workflow.developments_menu +msgid "Developments" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__display_name +msgid "Display Name" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__id +msgid "ID" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__write_date +msgid "Last Updated on" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__name +msgid "Name" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__state +msgid "Status" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model,name:test_soong_workflow.model_development_development +msgid "The development" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.actions.act_window,name:test_soong_workflow.development_action +msgid "development" +msgstr "" diff --git a/test_soong_workflow/i18n/zh_CN.po b/test_soong_workflow/i18n/zh_CN.po new file mode 100644 index 000000000..a1f48a621 --- /dev/null +++ b/test_soong_workflow/i18n/zh_CN.po @@ -0,0 +1,71 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_soong_workflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-13 14:50+0000\n" +"PO-Revision-Date: 2024-07-13 14:50+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__create_uid +msgid "Created by" +msgstr "创建人" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__create_date +msgid "Created on" +msgstr "创建日期" + +#. module: test_soong_workflow +#: model:ir.ui.menu,name:test_soong_workflow.developments_menu +msgid "Developments" +msgstr "开发" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__id +msgid "ID" +msgstr "" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__write_uid +msgid "Last Updated by" +msgstr "最后更新人" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__write_date +msgid "Last Updated on" +msgstr "最后更新日期" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__name +msgid "Name" +msgstr "名称" + +#. module: test_soong_workflow +#: model:ir.model.fields,field_description:test_soong_workflow.field_development_development__state +msgid "Status" +msgstr "状态" + +#. module: test_soong_workflow +#: model:ir.model,name:test_soong_workflow.model_development_development +msgid "The development" +msgstr "开发" + +#. module: test_soong_workflow +#: model:ir.actions.act_window,name:test_soong_workflow.development_action +msgid "development" +msgstr "开发" diff --git a/test_soong_workflow/models/__init__.py b/test_soong_workflow/models/__init__.py new file mode 100644 index 000000000..0f642ea56 --- /dev/null +++ b/test_soong_workflow/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import development diff --git a/test_soong_workflow/models/development.py b/test_soong_workflow/models/development.py new file mode 100644 index 000000000..62334f936 --- /dev/null +++ b/test_soong_workflow/models/development.py @@ -0,0 +1,12 @@ +from odoo import models, fields, _ + + +class Development(models.Model): + _name = 'development.development' + _inherit = 'soong.workflow.mixin' + _description = "The development" + + name = fields.Char() + + + diff --git a/test_soong_workflow/security/ir.model.access.csv b/test_soong_workflow/security/ir.model.access.csv new file mode 100644 index 000000000..05122d71c --- /dev/null +++ b/test_soong_workflow/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +,,,,,,, +access_development_development_manager,soong_workflow_ma11nager,model_development_development,soong_workflow.group_soong_workflow_manager,1,1,1,1 \ No newline at end of file diff --git a/test_soong_workflow/static/description/icon.png b/test_soong_workflow/static/description/icon.png new file mode 100644 index 000000000..7e556e56e Binary files /dev/null and b/test_soong_workflow/static/description/icon.png differ diff --git a/test_soong_workflow/views/development_development_views.xml b/test_soong_workflow/views/development_development_views.xml new file mode 100644 index 000000000..136e55f14 --- /dev/null +++ b/test_soong_workflow/views/development_development_views.xml @@ -0,0 +1,38 @@ + + + + development.development + +
+ + + + + +
+
+
+ + + development.development + + + + + + + + + development + ir.actions.act_window + development.development + tree,form + + + + +