From ed5f0d6535de66fc77daaff6ed10e34e0105969b Mon Sep 17 00:00:00 2001 From: git_admin Date: Sun, 3 May 2026 18:55:03 +0000 Subject: [PATCH] Tower: upload cetmix_tower_yaml 18.0.2.0.0 (was 18.0.2.0.0, via marketplace) --- addons/cetmix_tower_yaml/README.rst | 81 ++ addons/cetmix_tower_yaml/__init__.py | 2 + addons/cetmix_tower_yaml/__manifest__.py | 43 + addons/cetmix_tower_yaml/demo/demo_data.xml | 13 + .../i18n/cetmix_tower_yaml.pot | 1048 +++++++++++++++++ addons/cetmix_tower_yaml/i18n/hr.po | 587 +++++++++ addons/cetmix_tower_yaml/i18n/it.po | 1042 ++++++++++++++++ addons/cetmix_tower_yaml/models/__init__.py | 27 + .../models/cx_tower_command.py | 43 + .../cetmix_tower_yaml/models/cx_tower_file.py | 46 + .../models/cx_tower_file_template.py | 25 + .../models/cx_tower_jet_action.py | 26 + .../models/cx_tower_jet_state.py | 22 + .../models/cx_tower_jet_template.py | 42 + .../cx_tower_jet_template_dependency.py | 19 + .../models/cx_tower_jet_waypoint_template.py | 32 + .../cetmix_tower_yaml/models/cx_tower_key.py | 20 + .../models/cx_tower_key_value.py | 18 + .../cetmix_tower_yaml/models/cx_tower_os.py | 20 + .../cetmix_tower_yaml/models/cx_tower_plan.py | 40 + .../models/cx_tower_plan_line.py | 21 + .../models/cx_tower_plan_line_action.py | 20 + .../models/cx_tower_scheduled_task.py | 42 + .../models/cx_tower_scheduled_task_cv.py | 36 + .../models/cx_tower_server.py | 52 + .../models/cx_tower_server_log.py | 23 + .../models/cx_tower_server_template.py | 41 + .../models/cx_tower_shortcut.py | 22 + .../cetmix_tower_yaml/models/cx_tower_tag.py | 16 + .../models/cx_tower_variable.py | 23 + .../models/cx_tower_variable_option.py | 18 + .../models/cx_tower_variable_value.py | 20 + .../models/cx_tower_yaml_manifest_author.py | 23 + .../models/cx_tower_yaml_manifest_template.py | 93 ++ .../models/cx_tower_yaml_mixin.py | 776 ++++++++++++ addons/cetmix_tower_yaml/pyproject.toml | 3 + addons/cetmix_tower_yaml/readme/CONFIGURE.md | 1 + .../cetmix_tower_yaml/readme/DESCRIPTION.md | 3 + addons/cetmix_tower_yaml/readme/HISTORY.md | 3 + addons/cetmix_tower_yaml/readme/USAGE.md | 1 + .../readme/newsfragments/.gitkeep | 0 .../security/cetmix_tower_yaml_groups.xml | 28 + .../cx_tower_yaml_wizard_access_rules.xml | 34 + .../security/ir.model.access.csv | 9 + .../static/description/icon.png | Bin 0 -> 22128 bytes .../static/description/index.html | 439 +++++++ addons/cetmix_tower_yaml/tests/__init__.py | 8 + .../cetmix_tower_yaml/tests/test_command.py | 334 ++++++ .../tests/test_file_template.py | 320 +++++ addons/cetmix_tower_yaml/tests/test_plan.py | 179 +++ .../tests/test_server_log.py | 127 ++ .../tests/test_server_yaml.py | 125 ++ .../tests/test_tower_yaml_mixin.py | 768 ++++++++++++ .../tests/test_yaml_export_wizard.py | 377 ++++++ .../tests/test_yaml_import_wizard.py | 1008 ++++++++++++++++ .../views/cx_tower_command_view.xml | 10 + .../views/cx_tower_file_template_view.xml | 13 + .../views/cx_tower_jet_template_view.xml | 37 + .../views/cx_tower_key_view.xml | 10 + .../views/cx_tower_os_view.xml | 10 + .../views/cx_tower_plan_view.xml | 10 + .../views/cx_tower_scheduled_task_view.xml | 13 + .../views/cx_tower_server_template_view.xml | 13 + .../views/cx_tower_server_view.xml | 10 + .../views/cx_tower_shortcut_view.xml | 10 + .../views/cx_tower_tag_view.xml | 10 + .../views/cx_tower_variable_value_view.xml | 13 + .../views/cx_tower_variable_view.xml | 10 + .../cx_tower_yaml_manifest_author_views.xml | 41 + .../cx_tower_yaml_manifest_template_views.xml | 54 + addons/cetmix_tower_yaml/views/menuitems.xml | 33 + addons/cetmix_tower_yaml/wizards/__init__.py | 4 + .../wizards/cx_tower_yaml_export_wiz.py | 367 ++++++ .../wizards/cx_tower_yaml_export_wiz.xml | 110 ++ .../cx_tower_yaml_export_wiz_download.py | 11 + .../cx_tower_yaml_export_wiz_download.xml | 18 + .../wizards/cx_tower_yaml_import_wiz.py | 493 ++++++++ .../wizards/cx_tower_yaml_import_wiz.xml | 135 +++ .../cx_tower_yaml_import_wiz_upload.py | 137 +++ .../cx_tower_yaml_import_wiz_upload.xml | 37 + 80 files changed, 9798 insertions(+) create mode 100644 addons/cetmix_tower_yaml/README.rst create mode 100644 addons/cetmix_tower_yaml/__init__.py create mode 100644 addons/cetmix_tower_yaml/__manifest__.py create mode 100644 addons/cetmix_tower_yaml/demo/demo_data.xml create mode 100644 addons/cetmix_tower_yaml/i18n/cetmix_tower_yaml.pot create mode 100644 addons/cetmix_tower_yaml/i18n/hr.po create mode 100644 addons/cetmix_tower_yaml/i18n/it.po create mode 100644 addons/cetmix_tower_yaml/models/__init__.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_command.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_file.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_file_template.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_jet_action.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_jet_state.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_jet_template.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_jet_template_dependency.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_jet_waypoint_template.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_key.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_key_value.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_os.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_plan.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_plan_line.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_plan_line_action.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_scheduled_task.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_scheduled_task_cv.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_server.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_server_log.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_server_template.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_shortcut.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_tag.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_variable.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_variable_option.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_variable_value.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_author.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py create mode 100644 addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py create mode 100644 addons/cetmix_tower_yaml/pyproject.toml create mode 100644 addons/cetmix_tower_yaml/readme/CONFIGURE.md create mode 100644 addons/cetmix_tower_yaml/readme/DESCRIPTION.md create mode 100644 addons/cetmix_tower_yaml/readme/HISTORY.md create mode 100644 addons/cetmix_tower_yaml/readme/USAGE.md create mode 100644 addons/cetmix_tower_yaml/readme/newsfragments/.gitkeep create mode 100644 addons/cetmix_tower_yaml/security/cetmix_tower_yaml_groups.xml create mode 100644 addons/cetmix_tower_yaml/security/cx_tower_yaml_wizard_access_rules.xml create mode 100644 addons/cetmix_tower_yaml/security/ir.model.access.csv create mode 100644 addons/cetmix_tower_yaml/static/description/icon.png create mode 100644 addons/cetmix_tower_yaml/static/description/index.html create mode 100644 addons/cetmix_tower_yaml/tests/__init__.py create mode 100644 addons/cetmix_tower_yaml/tests/test_command.py create mode 100644 addons/cetmix_tower_yaml/tests/test_file_template.py create mode 100644 addons/cetmix_tower_yaml/tests/test_plan.py create mode 100644 addons/cetmix_tower_yaml/tests/test_server_log.py create mode 100644 addons/cetmix_tower_yaml/tests/test_server_yaml.py create mode 100644 addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py create mode 100644 addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py create mode 100644 addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_command_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_file_template_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_jet_template_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_key_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_os_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_plan_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_scheduled_task_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_server_template_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_server_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_shortcut_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_tag_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_variable_value_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_variable_view.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_yaml_manifest_author_views.xml create mode 100644 addons/cetmix_tower_yaml/views/cx_tower_yaml_manifest_template_views.xml create mode 100644 addons/cetmix_tower_yaml/views/menuitems.xml create mode 100644 addons/cetmix_tower_yaml/wizards/__init__.py create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.xml create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz_download.py create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz_download.xml create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.xml create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py create mode 100644 addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.xml diff --git a/addons/cetmix_tower_yaml/README.rst b/addons/cetmix_tower_yaml/README.rst new file mode 100644 index 0000000..5b110cc --- /dev/null +++ b/addons/cetmix_tower_yaml/README.rst @@ -0,0 +1,81 @@ +================= +Cetmix Tower YAML +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:67e4f5c3d94d23c826affb93f25cdacdccc0ef69084fe3d293e9e140270b8394 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github + :target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_yaml + :alt: cetmix/cetmix-tower + +|badge1| |badge2| |badge3| + +This module implements YAML format data import/export for `Cetmix +Tower `__. + +Please refer to the `official +documentation `__ for detailed information. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Please refer to the `official +documentation `__ for detailed configuration +instructions. + +Usage +===== + +Please refer to the `official +documentation `__ for detailed usage +instructions. + +Changelog +========= + +18.0.2.0.0 (2026-04-07) +----------------------- + +- Features: Jets! (4700) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Cetmix + +Maintainers +----------- + +This module is part of the `cetmix/cetmix-tower `_ project on GitHub. + +You are welcome to contribute. diff --git a/addons/cetmix_tower_yaml/__init__.py b/addons/cetmix_tower_yaml/__init__.py new file mode 100644 index 0000000..aee8895 --- /dev/null +++ b/addons/cetmix_tower_yaml/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/addons/cetmix_tower_yaml/__manifest__.py b/addons/cetmix_tower_yaml/__manifest__.py new file mode 100644 index 0000000..9d03063 --- /dev/null +++ b/addons/cetmix_tower_yaml/__manifest__.py @@ -0,0 +1,43 @@ +# Copyright Cetmix OÜ 2024 +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Cetmix Tower YAML", + "summary": "Cetmix Tower YAML export/import", + "version": "18.0.2.0.0", + "development_status": "Beta", + "category": "Productivity", + "website": "https://tower.cetmix.com", + "author": "Cetmix", + "license": "AGPL-3", + "installable": True, + "depends": ["cetmix_tower_server"], + "external_dependencies": {"python": ["pyyaml"]}, + "data": [ + "security/cetmix_tower_yaml_groups.xml", + "security/cx_tower_yaml_wizard_access_rules.xml", + "security/ir.model.access.csv", + "views/cx_tower_command_view.xml", + "views/cx_tower_file_template_view.xml", + "views/cx_tower_plan_view.xml", + "views/cx_tower_server_template_view.xml", + "views/cx_tower_server_view.xml", + "views/cx_tower_variable_view.xml", + "views/cx_tower_variable_value_view.xml", + "views/cx_tower_os_view.xml", + "views/cx_tower_tag_view.xml", + "views/cx_tower_shortcut_view.xml", + "views/cx_tower_scheduled_task_view.xml", + "views/cx_tower_key_view.xml", + "views/cx_tower_jet_template_view.xml", + "views/cx_tower_yaml_manifest_template_views.xml", + "views/cx_tower_yaml_manifest_author_views.xml", + "wizards/cx_tower_yaml_export_wiz.xml", + "wizards/cx_tower_yaml_export_wiz_download.xml", + "wizards/cx_tower_yaml_import_wiz_upload.xml", + "wizards/cx_tower_yaml_import_wiz.xml", + "views/menuitems.xml", + ], + "demo": [ + "demo/demo_data.xml", + ], +} diff --git a/addons/cetmix_tower_yaml/demo/demo_data.xml b/addons/cetmix_tower_yaml/demo/demo_data.xml new file mode 100644 index 0000000..40f8de5 --- /dev/null +++ b/addons/cetmix_tower_yaml/demo/demo_data.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/addons/cetmix_tower_yaml/i18n/cetmix_tower_yaml.pot b/addons/cetmix_tower_yaml/i18n/cetmix_tower_yaml.pot new file mode 100644 index 0000000..13d2d0f --- /dev/null +++ b/addons/cetmix_tower_yaml/i18n/cetmix_tower_yaml.pot @@ -0,0 +1,1048 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_yaml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_export +msgid "" +"\n" +" Export data to YAML.\n" +" " +msgstr "" + +#. module: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_import +msgid "" +"\n" +" Import data from YAML.\n" +" " +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "'%s' is not a valid model" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +msgid "'invalid_model' is not a valid model" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"Important: To maintain data consistency, the following\n" +" model records will always be updated if they exist in Odoo:" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__add_manifest +msgid "Add Manifest" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "" +"Add entire child record definitions to the exported YAML file. Otherwise " +"only references to child records will be added." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__file_prefix +msgid "" +"Add prefix to the exported YAML file name when this template is selected" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:res.groups,name:cetmix_tower_yaml.group_export +#: model:res.groups,name:cetmix_tower_yaml.group_import +msgid "Allow" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_author_string +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Author" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.constraint,message:cetmix_tower_yaml.constraint_cx_tower_yaml_manifest_author_yaml_manifest_author_name_uniq +msgid "Author name must be unique." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_author_ids +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__author_ids +msgid "Authors" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_scheduled_task_cv__reference +msgid "" +"Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_command +msgid "Cetmix Tower Command" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_file_template +msgid "Cetmix Tower File Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan +msgid "Cetmix Tower Flight Plan" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line +msgid "Cetmix Tower Flight Plan Line" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line_action +msgid "Cetmix Tower Flight Plan Line Action" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_jet_action +msgid "Cetmix Tower Jet Action" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_jet_state +msgid "Cetmix Tower Jet State" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_jet_template +msgid "Cetmix Tower Jet Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_jet_template_dependency +msgid "Cetmix Tower Jet Template Dependency" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_jet_waypoint_template +msgid "Cetmix Tower Jet Waypoint Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_key +msgid "Cetmix Tower Key/Secret Storage" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_os +msgid "Cetmix Tower Operating System" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_key_value +msgid "Cetmix Tower Secret Value Storage" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_log +msgid "Cetmix Tower Server Log" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_template +msgid "Cetmix Tower Server Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_shortcut +msgid "Cetmix Tower Shortcut" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_tag +msgid "Cetmix Tower Tag" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable +msgid "Cetmix Tower Variable" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_option +msgid "Cetmix Tower Variable Options" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_value +msgid "Cetmix Tower Variable Values" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz_download +msgid "Cetmix Tower YAML Export File Download" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz +msgid "Cetmix Tower YAML Export Wizard" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz +msgid "Cetmix Tower YAML Import Wizard" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz_upload +msgid "Cetmix Tower YAML Import Wizard Upload" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_mixin +msgid "Cetmix Tower YAML rendering mixin" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_download_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Close" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Code preview" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_author_string +msgid "Comma-separated list" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__create +msgid "Create a new record" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.actions.act_window,help:cetmix_tower_yaml.action_yaml_manifest_author +msgid "Create your first YAML manifest author!" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.actions.act_window,help:cetmix_tower_yaml.action_yaml_manifest_template +msgid "Create your first YAML manifest template!" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__create_uid +msgid "Created by" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__create_date +msgid "Created on" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_currency +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_currency +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__currency +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Currency" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__currency +msgid "Currency for pricing information." +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "Currency is required when price is specified" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +msgid "Custom" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license_text +msgid "Custom license text when license type is Custom." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_scheduled_task_cv +msgid "Custom variable values for scheduled tasks" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "" +"Deferred relation resolution failed:\n" +"%(details)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_description +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_description +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Description" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Detailed description (optional)" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__display_name +msgid "Display Name" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Error creating record '%(reference)s' in model '%(model)s': %(error)s" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Error updating record %(reference)s: %(error)s" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +msgid "Euro" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"Existing record will be updated with the new data. Related records, present in the YAML code, will be updated too.\n" +" If any of those related records doesn't exist, it will be created automatically." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "Explode Child Records" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_command_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_file_template_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_jet_template_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_key_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_os_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_plan_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_scheduled_task_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_server_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_server_template_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_shortcut_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_tag_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_variable_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_variable_value_export_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_jet_template_view_form +msgid "Export YAML" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +msgid "Failed to convert dictionary to YAML: %(error)s" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "" +"Failed to encode YAML content. Please ensure all characters are UTF-8 " +"compatible." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__file_name +msgid "File Name" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "File is empty" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "File is not a valid base64-encoded file" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__file_prefix +msgid "File prefix" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Following secrets are used in the code:
%(secrets)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Generate YAML file" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__id +msgid "ID" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__if_record_exists +msgid "If Record Exists" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Import" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_yaml_import_wiz_upload +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_cetmix_tower_yaml_import +msgid "Import YAML" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Import result: %(model)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Information" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "Invalid YAML file" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Key/Secrets" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__write_date +msgid "Last Updated on" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_name +msgid "Leave this field blank if you don't want to create a manifest" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_license +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_license +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_license_text +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license_text +msgid "License Text" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License and pricing" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_license_text +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License text" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "License text is required for a custom license" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license +msgid "License used for the code snippet." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__author_ids +msgid "List of author names to include in the YAML manifest." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__secret_list +msgid "List of secrets present in the YAML file (formatted as HTML list)" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Manifest" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_manifest_author_action +msgid "Manifest Authors" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_template_id +msgid "Manifest Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_manifest_template +msgid "Manifest Templates" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "Model '%s' does not support YAML import" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +msgid "Model 'command_run_wizard' does not support YAML import" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_names +msgid "Model Names" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_names +msgid "Models to create records in" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_scheduled_task_cv__name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Name" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__name +msgid "Name of the manifest template." +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "No YAML code is present." +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "No model or records selected" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "No records were created or updated" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "No valid records selected" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "OSs" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__preview_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__preview_code +msgid "Preview Code" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Preview code" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_price +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_price +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Price" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +msgid "Process" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +msgid "Provide Custom License Text when License is set to 'Custom'." +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "" +"Record %(record_model)s '%(record_reference)s': field '%(field)s' could not " +"resolve %(target_model)s '%(target_reference)s'" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "" +"Record '%(record)s': field '%(field)s' could not resolve %(target_model)s " +"'%(target_reference)s'" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Record Import" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "Record model is missing for record %s" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Record reference is missing" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +msgid "Records of the following models were created or updated: %(models)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_scheduled_task_cv__reference +msgid "Reference" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "Remove Empty x2m Field Values" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "" +"Remove empty Many2one, Many2many and One2many field values from the exported" +" YAML file." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_scheduled_task +msgid "Scheduled Task" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__secret_list +msgid "Secret List" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Select a pre-defined template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Select a template to auto-populate manifest fields" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_summary +msgid "Short summary that includes core information. 160 symbols max" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Short summary, 160 symbols max" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__skip +msgid "Skip record" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_name +msgid "Snippet Name" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_file_name +msgid "Snippet file name without extension, eg 'my_snippet'" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_summary +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_summary +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Summary" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Tags" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "This may overwrite existing records. Proceed?" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"To create new entities instead of updating existing ones, remove or modify\n" +" the reference field in the YAML code for those entities." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__preview_code +msgid "Toggle to show or hide YAML code preview" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +msgid "US Dollar" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__update +msgid "Update existing record" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_version +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Use the Major.Minor.Patch format, e.g. 1.2.3" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +msgid "Values must be a dictionary" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Variable Options" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Variables" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_version +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_version +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__version +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Version" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__version +msgid "Version in Major.Minor.Patch format, e.g. 1.0.0" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +msgid "Version must be in format Major.Minor.Patch, e.g. 1.2.3" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +msgid "Version must be in the Major.Minor.Patch format, e.g. 1.2.3" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_website +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_website +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__website +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Website" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__website +msgid "Website URL for the manifest." +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__if_record_exists +msgid "What to do if record with the same reference already exists" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +msgid "Wrong value for 'access_level' key: %(acv)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_jet_template_view_form +msgid "YAML" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.module.category,name:cetmix_tower_yaml.ir_module_category_tower_yaml_export +msgid "YAML Export" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_settings_root +msgid "YAML Export/Import" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_file_name +msgid "YAML File Name" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.module.category,name:cetmix_tower_yaml.ir_module_category_tower_yaml_import +msgid "YAML Import" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_manifest_author +msgid "YAML Manifest Author" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_yaml_manifest_author +msgid "YAML Manifest Authors" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_manifest_tmpl +msgid "YAML Manifest Template" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_yaml_manifest_template +msgid "YAML Manifest Templates" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "YAML file cannot be decoded properly" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "YAML file doesn't contain any records" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.actions.act_window,help:cetmix_tower_yaml.action_yaml_manifest_author +msgid "" +"YAML manifest authors represent organizations or users who are authors of " +"YAML manifests." +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.actions.act_window,help:cetmix_tower_yaml.action_yaml_manifest_template +msgid "" +"YAML manifest templates represent pre-defined YAML manifests with their " +"metadata and configuration." +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "" +"YAML version is higher than version supported by your Cetmix Tower instance." +" %(code_version)s > %(tower_version)s" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_jet_action__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_jet_state__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_jet_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_jet_template_dependency__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_jet_waypoint_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key_value__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_scheduled_task__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_scheduled_task_cv__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_shortcut__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin__yaml_code +msgid "Yaml Code" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__yaml_file +msgid "Yaml File" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file_name +msgid "Yaml File Name" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +msgid "Yaml file doesn't contain valid data" +msgstr "" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +msgid "You are not allowed to create records from YAML" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_jet_template_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "my_snippet.yaml" +msgstr "" diff --git a/addons/cetmix_tower_yaml/i18n/hr.po b/addons/cetmix_tower_yaml/i18n/hr.po new file mode 100644 index 0000000..3627f4f --- /dev/null +++ b/addons/cetmix_tower_yaml/i18n/hr.po @@ -0,0 +1,587 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_yaml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-03-06 09:11+0000\n" +"Last-Translator: Bole \n" +"Language-Team: Croatian \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 5.10.3-dev\n" + +#. module: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_export +msgid "" +"\n" +" Export data to YAML.\n" +" " +msgstr "" +"\n" +" Izvoz podataka u YAML.\n" +" " + +#. module: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_import +msgid "" +"\n" +" Import data from YAML.\n" +" " +msgstr "" +"\n" +" Uvoz podataka iz YAML.\n" +" " + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"Important: To maintain data consistency, the following " +"model records will always be updated if they exist in Odoo:" +msgstr "" +"Važno: Za održavanje konsistencije podataka, sljedeći " +"zapisi modela će uvijek biti ažurirani ako postoje u Odoo:" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "" +"Add entire child record definitions to the exported YAML file. Otherwise " +"only references to child records will be added." +msgstr "" +"Dodaj sve podređene definicije podataka u izveženu YAML datoteku. Inače će " +"biti dodane samo reference na podređene zapise." + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_command +msgid "Cetmix Tower Command" +msgstr "Cetmix Tower Naredba" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan +msgid "Cetmix Tower Flight Plan" +msgstr "Cetmix Tower Plan Leta" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line +msgid "Cetmix Tower Flight Plan Line" +msgstr "Cetmix Tower Stavka plana leta" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line_action +msgid "Cetmix Tower Flight Plan Line Action" +msgstr "Cetmix Tower Akcija stavke plana leta" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_os +msgid "Cetmix Tower Operating System" +msgstr "Cetmix Tower Operativni sistem" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_log +msgid "Cetmix Tower Server Log" +msgstr "Cetmix Tower Log servera" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_template +msgid "Cetmix Tower Server Template" +msgstr "Cetmix Tower Predložak servera" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_tag +msgid "Cetmix Tower Tag" +msgstr "Cetmix Tower Oznaka" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable +msgid "Cetmix Tower Variable" +msgstr "Cetmix Tower Varijabla" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_option +msgid "Cetmix Tower Variable Options" +msgstr "Cetmix Tower Opcija varijable" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_value +msgid "Cetmix Tower Variable Values" +msgstr "Cetmix Tower Vrijednosti varijabli" + +#. module: cetmix_tower_yaml +#: model:ir.module.category,name:cetmix_tower_yaml.ir_module_category_tower_yaml +msgid "Cetmix Tower YAML" +msgstr "Cetmix Tower YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz_download +msgid "Cetmix Tower YAML Export File Download" +msgstr "Cetmix Tower YAML preuzimanje izvežene datoteke" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz +msgid "Cetmix Tower YAML Export Wizard" +msgstr "Cetmix Tower YAML Čarobnjak za izvoz" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz +msgid "Cetmix Tower YAML Import Wizard" +msgstr "Cetmix Tower YAML Čarobnjak za uvoz" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz_upload +msgid "Cetmix Tower YAML Import Wizard Upload" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_mixin +msgid "Cetmix Tower YAML rendering mixin" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_key +msgid "Cetmix Tower private key storage" +msgstr "Cetmix Tower Pohrana privatnih ključeva" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_download_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Close" +msgstr "Zatvori" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__comment +msgid "Comment" +msgstr "Komentar" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__comment +msgid "Comment to be added to the beginning of exported YAML file" +msgstr "Komentar koje će biti dodan na početku izvežene YAML datoteke" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Create New Record" +msgstr "Kreiraj novi zapis" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_file_template +msgid "Cx Tower File Template" +msgstr "CxTower Predložak datoteke" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin__display_name +msgid "Display Name" +msgstr "Naziv" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"Existing record will be updated with the new data. Related records, present in the YAML code, will be updated too.\n" +" If any of those related records doesn't exist, it will be created automatically." +msgstr "" +"Postojeći zapis će biti ažuriran sa novim podacima. Povezani zapisi, " +"prisutni u YAML kodu, će također biti ažurirani.\n" +" Ukoliko neki od " +"povezanih zapisa ne postoji, biti će kreiran automatski." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "Explode Child Records" +msgstr "Proširi podređene zapise" + +#. module: cetmix_tower_yaml +#: model:res.groups,name:cetmix_tower_yaml.group_export +msgid "Export" +msgstr "Izvoz" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +msgid "Export YAML" +msgstr "Izvoz YAML" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#, python-format +msgid "Failed to convert dictionary to YAML: %(error)s" +msgstr "Neuspjelo pretvaranje dictionary u YAML: %(error)s" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "" +"Failed to encode YAML content. Please ensure all characters are UTF-8 " +"compatible." +msgstr "" +"Neuspjelo kreiranje YAML sadržaja. Molimo provjerite jesu li svi znakovi UTF-" +"8 kompatabilni." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__file_name +msgid "File Name" +msgstr "Naziv datoteke" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "File is not a valid base64-encoded file" +msgstr "Datoteka nije ispravno base64 kodirana" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Generate YAML file" +msgstr "Generiraj YAML datoteku" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin__id +msgid "ID" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__update_existing_record +msgid "" +"If enabled, existing records will be updated with the new data. Otherwise, " +"new records will be created." +msgstr "" +"Ako je označeno, postojeći zapisi će biti ažurirani sa novim podacima. " +"Inače, novi zapisi će biti kreirani." + +#. module: cetmix_tower_yaml +#: model:res.groups,name:cetmix_tower_yaml.group_import +msgid "Import" +msgstr "Uvoz" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_yaml_import_wiz_upload +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_cetmix_tower_yaml_import +msgid "Import YAML" +msgstr "Uvoz YAML" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Invalid YAML file" +msgstr "Neispravna YAML datoteka" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Invalid model specified in the YAML file" +msgstr "Neispravan model naveden u YAML datoteci" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload____last_update +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin____last_update +msgid "Last Modified on" +msgstr "Zadnje modificirano" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_description +msgid "Model" +msgstr "" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_name +msgid "Model Name" +msgstr "Naziv modela" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Model does not support YAML import" +msgstr "Model ne podržava YAML uvoz" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_name +msgid "Model to create records in" +msgstr "Model u kojem će podaci biti kreirani" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "No YAML code is present." +msgstr "YAML kod nije dostupan." + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "No model for import is specified in the YAML file" +msgstr "Model za uvoz nije naveden u YAML datoteci" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "No model or records selected" +msgstr "Nisu odabrani zapisi ili model" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "OSs" +msgstr "" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Open Existing Record" +msgstr "Otvori postojeći zapis" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +msgid "Process" +msgstr "U procesu" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__record_id +msgid "Record" +msgstr "Zapis" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__record_id +msgid "Record ID to update" +msgstr "ID zapisa za ažuriranje" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "Remove Empty x2m Field Values" +msgstr "Ukloni prazne x2m vrijednosti polja" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "" +"Remove empty Many2one, Many2many and One2many field values from the exported" +" YAML file." +msgstr "" +"Ukloni prazne Many2one, Many2many i One2many vrijednosti polja iz izvežene " +"YAML datoteke." + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Tags" +msgstr "Oznake" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "This will create a new record. Proceed?" +msgstr "Ovo će kreirati novi zapis. Nastaviti?" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "This will overwrite the existing record. Proceed?" +msgstr "Ovo će prepisati preko postojećeg zapisa. Nastaviti?" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"To create new entities instead of updating existing ones, remove or modify " +"the reference field in the YAML code for those entities." +msgstr "" +"Za kreiranje novih entiteta umjesto ažuriranja postojećih, uklonite ili " +"modificirajte reference polje u YAML kodu za te zapise." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__update_existing_record +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Update Existing Record" +msgstr "Ažuriraj postojeći zapis" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +#, python-format +msgid "Values must be a dictionary" +msgstr "Vrijednosti moraju biti dictionary" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Variables" +msgstr "Varijable" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +#, python-format +msgid "Wrong value for 'access_level' key: %(acv)s" +msgstr "Pogrešna vrijednost za ključ 'access_level': %(acv)s" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +msgid "YAML" +msgstr "" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "YAML file cannot be decoded properly" +msgstr "YAML datoteku nije moguće pročitati ili dekodirati" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "" +"YAML file version is not supported. You may need to update the Cetmix Tower " +"Yaml module." +msgstr "" +"Verzija YAML datoteke nije podržana. Možda morate ažurirati Cetmix Tower " +"Yaml modul." + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +#, python-format +msgid "" +"YAML version is higher than version supported by your Cetmix Tower instance." +" %(code_version)s > %(tower_version)s" +msgstr "" +"YAML verzija je viša od verzije podržane na vašoj Cetmix Tower instanci. " +"%(code_version)s>%(tower_version)s" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin__yaml_code +msgid "Yaml Code" +msgstr "YAML kod" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__yaml_file +msgid "Yaml File" +msgstr "YAML datoteka" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file_name +msgid "Yaml File Name" +msgstr "YAML naziv datoteke" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Yaml file doesn't contain valid data" +msgstr "Yaml datoteka ne sadrži valjane podatke" + +#. module: cetmix_tower_yaml +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#, python-format +msgid "You are not allowed to create records from YAML" +msgstr "Nije vam dozvoljeno kreiranje zapisa pomoću YAML datoteka" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "Morate bit član \"YAML/Izvoz\" grupe za izvoz podataka u YAML." diff --git a/addons/cetmix_tower_yaml/i18n/it.po b/addons/cetmix_tower_yaml/i18n/it.po new file mode 100644 index 0000000..c1cd4b6 --- /dev/null +++ b/addons/cetmix_tower_yaml/i18n/it.po @@ -0,0 +1,1042 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_yaml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2025-09-04 16:34+0100\n" +"Last-Translator: \n" +"Language-Team: Italian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Poedit 2.3\n" + +#. module: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_export +msgid "" +"\n" +" Export data to YAML.\n" +" " +msgstr "" +"\n" +" Esporta i dati in YAML.\n" +" " + +#. module: cetmix_tower_yaml +#: model:res.groups,comment:cetmix_tower_yaml.group_import +msgid "" +"\n" +" Import data from YAML.\n" +" " +msgstr "" +"\n" +" Importa i dati da YAML.\n" +" " + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "'%s' is not a valid model" +msgstr "'%s' non è un modello valido" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#, python-format +msgid "'invalid_model' is not a valid model" +msgstr "'invalid_model' non è un modello valido" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Important: To maintain data consistency, the following model records will always be updated if they exist in Odoo:" +msgstr "Importante: per mantenere la coerenza dei dati, i record dei seguenti modelli verranno sempre aggiornati se esistono in Odoo:" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__add_manifest +msgid "Add Manifest" +msgstr "Aggiungi manifest" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "Add entire child record definitions to the exported YAML file. Otherwise only references to child records will be added." +msgstr "Aggiungere le definizioni di tutti i record figli allo YAML esportato. Altrimenti solo i riferimenti ai record figli verranno aggiunti." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__file_prefix +msgid "Add prefix to the exported YAML file name when this template is selected" +msgstr "Aggiungere un prefisso al nome del file YAML esportato quando è selezionato questo modello" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "After import, please check and provide secret values if needed for the following secrets: %(secrets)s" +msgstr "Dopo l'importazione, controllare e fornire i valori segreti, se necessario, per i seguenti segreti: %(secrets)s" + +#. module: cetmix_tower_yaml +#: model:res.groups,name:cetmix_tower_yaml.group_export +#: model:res.groups,name:cetmix_tower_yaml.group_import +msgid "Allow" +msgstr "Consenti" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_author_string +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Author" +msgstr "Autore" + +#. module: cetmix_tower_yaml +#: model:ir.model.constraint,message:cetmix_tower_yaml.constraint_cx_tower_yaml_manifest_author_yaml_manifest_author_name_uniq +msgid "Author name must be unique." +msgstr "Il nome dell'autore deve essere univoco." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_author_ids +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__author_ids +msgid "Authors" +msgstr "Autori" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_command +msgid "Cetmix Tower Command" +msgstr "Comando Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "File Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_file_template +msgid "Cetmix Tower File Template" +msgstr "Modello file Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan +msgid "Cetmix Tower Flight Plan" +msgstr "Piano di volo Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line +msgid "Cetmix Tower Flight Plan Line" +msgstr "Riga piano di volo Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_plan_line_action +msgid "Cetmix Tower Flight Plan Line Action" +msgstr "Azione riga piano di volo Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_key +msgid "Cetmix Tower Key/Secret Storage" +msgstr "Deposito chiave/segreto Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_os +msgid "Cetmix Tower Operating System" +msgstr "Sistema operativo Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_key_value +msgid "Cetmix Tower Secret Value Storage" +msgstr "Deposito segreto Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "Server Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_log +msgid "Cetmix Tower Server Log" +msgstr "Registro server Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_server_template +msgid "Cetmix Tower Server Template" +msgstr "Modello server Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_shortcut +msgid "Cetmix Tower Shortcut" +msgstr "Scorciatoia Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_tag +msgid "Cetmix Tower Tag" +msgstr "Etichetta Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable +msgid "Cetmix Tower Variable" +msgstr "Variabile Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_option +msgid "Cetmix Tower Variable Options" +msgstr "Opzioni variabile Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_variable_value +msgid "Cetmix Tower Variable Values" +msgstr "Valori variabile Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz_download +msgid "Cetmix Tower YAML Export File Download" +msgstr "Scarico file esportazione YAML Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_export_wiz +msgid "Cetmix Tower YAML Export Wizard" +msgstr "Procedura guidata esportazione YAML Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz +msgid "Cetmix Tower YAML Import Wizard" +msgstr "Procedura guidata importazione YAML Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_import_wiz_upload +msgid "Cetmix Tower YAML Import Wizard Upload" +msgstr "Aggiornamento procedura guidata importazione YAML Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_mixin +msgid "Cetmix Tower YAML rendering mixin" +msgstr "Mixin compilazione YAML Cetmix Tower" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_download_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Close" +msgstr "Chiudi" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_author_string +msgid "Comma-separated list" +msgstr "Elenco di valori separati da virgole" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__create +msgid "Create a new record" +msgstr "Crea nuovo record" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__create_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_currency +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_currency +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__currency +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Currency" +msgstr "Valuta" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__currency +msgid "Currency for pricing information." +msgstr "Valuta per le informazioni sui prezzi." + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "Currency is required when price is specified" +msgstr "La valuta è richiesta quando è indicato il prezzo" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +#, python-format +msgid "Custom" +msgstr "Personalizzata" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license_text +msgid "Custom license text when license type is Custom." +msgstr "Testo licenza personalizzato quando il tipo licenza è personalizzato." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_description +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_description +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Description" +msgstr "Descrizione" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Detailed description (optional)" +msgstr "Descrizione dettagliata (opzionale)" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__display_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Error creating record '%(reference)s' in model '%(model)s': %(error)s" +msgstr "Errore durante la creazione del record '%(reference)s' nel modello '%(model)s': %(error)s" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Error updating record %(reference)s: %(error)s" +msgstr "Errore durante l'aggiornamento del record %(reference)s: %(error)s" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +#, python-format +msgid "Euro" +msgstr "Euro" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "" +"Existing record will be updated with the new data. Related records, present in the YAML code, will be updated too.\n" +" If any of those related records doesn't exist, it will be created automatically." +msgstr "" +"Il record esistente verrà aggiornato con i nuovi dati. Anche i record correlati presenti nel codice YAML verranno aggiornati.\n" +" Se uno di tali record correlati non esiste, verrà creato automaticamente." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__explode_child_records +msgid "Explode Child Records" +msgstr "Esplodi record figli" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_command_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_file_template_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_key_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_os_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_plan_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_scheduled_task_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_server_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_server_template_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_shortcut_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_tag_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_variable_export_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_variable_value_export_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_shortcut_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.view_cx_tower_scheduled_task_view_form +msgid "Export YAML" +msgstr "Esporta YAML" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#, python-format +msgid "Failed to convert dictionary to YAML: %(error)s" +msgstr "Conversione dizionario in YAML fallita: %(error)s" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "Failed to encode YAML content. Please ensure all characters are UTF-8 compatible." +msgstr "Codifica del contenuto YAML non riuscita. Assicurarsi che tutti i caratteri siano compatibili con UTF-8." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__file_name +msgid "File Name" +msgstr "Nome file" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "File is not a valid base64-encoded file" +msgstr "Il file non è un file codificato in base64 valido" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__file_prefix +msgid "File prefix" +msgstr "Prefisso file" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Generate YAML file" +msgstr "Genera file YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__id +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__id +msgid "ID" +msgstr "ID" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__if_record_exists +msgid "If Record Exists" +msgstr "Se esiste il record" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Import" +msgstr "Importa" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_cx_tower_yaml_import_wiz_upload +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_cetmix_tower_yaml_import +msgid "Import YAML" +msgstr "Importa YAML" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Import result: %(model)s" +msgstr "Risultato importazione: %(model)s" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Information" +msgstr "Informazioni" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Invalid YAML file" +msgstr "File YAML non valido" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Key/Secrets" +msgstr "Chiavi/segreti" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__write_date +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_name +msgid "Leave this field blank if you don't want to create a manifest" +msgstr "Lasciare vuoto questo campo se non si vuole creare un manifest" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_license +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_license +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License" +msgstr "Licenza" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_license_text +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license_text +msgid "License Text" +msgstr "Testo licenza" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License and pricing" +msgstr "Licenza e prezzo" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_license_text +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "License text" +msgstr "Testo licenza" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "License text is required for a custom license" +msgstr "Il testo della licenza è richiesto per una licenza personalizzata" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__license +msgid "License used for the code snippet." +msgstr "Licenza utilizzata per il codice esempio." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__author_ids +msgid "List of author names to include in the YAML manifest." +msgstr "Elenco dei nomi degli autori da includere nel manifest YAML." + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__secret_list +msgid "List of secrets present in the YAML file" +msgstr "Elenco segreti presenti nel file YAML" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Manifest" +msgstr "Manifest" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_manifest_author_action +msgid "Manifest Authors" +msgstr "Autori manifest" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_template_id +msgid "Manifest Template" +msgstr "Modello manifest" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_manifest_template +msgid "Manifest Templates" +msgstr "Modelli manifest" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Model '%s' does not support YAML import" +msgstr "Il modello '%s' non supporta l'importazione YAML" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#, python-format +msgid "Model 'command_run_wizard' does not support YAML import" +msgstr "Il modello 'command_run_wizard' non supporta l'importazione YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_names +msgid "Model Names" +msgstr "Nome modelli" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__model_names +msgid "Models to create records in" +msgstr "Modelli in cui creare i record" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_author__name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Name" +msgstr "Nome" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__name +msgid "Name of the manifest template." +msgstr "Nome del modello manifest." + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "No YAML code is present." +msgstr "Codice YAML non presente." + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "No model or records selected" +msgstr "Nessun modello o record selezionato" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "No records were created or updated" +msgstr "Nessun record creato o aggiornato" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "No valid records selected" +msgstr "Nessun record valido selezionato" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "OSs" +msgstr "SO" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__preview_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__preview_code +msgid "Preview Code" +msgstr "Anteprima codice" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Preview code" +msgstr "Anteprima codice" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_price +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_price +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Price" +msgstr "Prezzo" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_upload_view_form +msgid "Process" +msgstr "Processo" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +#, python-format +msgid "Provide Custom License Text when License is set to 'Custom'." +msgstr "Fornire il testo della licenza quando la licenza è impostata su \"Personalizzata\"." + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Record Import" +msgstr "Importazione record" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Record model is missing for record %s" +msgstr "Manca il modello record per il record %s" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Record reference is missing" +msgstr "Manca il riferimento del record" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#, python-format +msgid "Records of the following models were created or updated: %(models)s" +msgstr "Sono stati creati o aggiornati i record dei seguenti modelli: %(models)s" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "Remove Empty x2m Field Values" +msgstr "Rimozione dei valori campo x2m vuoti" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__remove_empty_values +msgid "Remove empty Many2one, Many2many and One2many field values from the exported YAML file." +msgstr "Rimuovi i valori vuoti dei campi Many2one, Many2many e One2many dal file YAML esportato." + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_scheduled_task +msgid "Scheduled Task" +msgstr "Lavoro schedulato" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__secret_list +msgid "Secret List" +msgstr "Elenco segreto" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Select a pre-defined template" +msgstr "Selezionare un modello predefinito" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Select a template to auto-populate manifest fields" +msgstr "Selezionare un modello per compilare automaticamente i campi del manifest" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_summary +msgid "Short summary that includes core information. 160 symbols max" +msgstr "Riepilogo breve che include le informazioni principali. Massimo 160 caratteri" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Short summary, 160 symbols max" +msgstr "Sommario breve, massimo 160 caratteri" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__skip +msgid "Skip record" +msgstr "Salta record" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_name +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_name +msgid "Snippet Name" +msgstr "Nome snippet" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_file_name +msgid "Snippet file name without extension, eg 'my_snippet'" +msgstr "Nome file dello snippet senza estensione, ad es. 'my_snippet'" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_summary +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_summary +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Summary" +msgstr "Sommario" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Tags" +msgstr "Etichette" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "This may overwrite existing records. Proceed?" +msgstr "Questo sovrascriverà i record esistenti. Procedere?" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "To create new entities instead of updating existing ones, remove or modify the reference field in the YAML code for those entities." +msgstr "Per creare nuovi record anziché aggiornare quelli esistenti, rimuovere o modificare il campo reference nel codice YAML per tali record." + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +#, python-format +msgid "US Dollar" +msgstr "Dollaro USA" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields.selection,name:cetmix_tower_yaml.selection__cx_tower_yaml_import_wiz__if_record_exists__update +msgid "Update existing record" +msgstr "Aggiorna record esistente" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_version +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "Use the Major.Minor.Patch format, e.g. 1.2.3" +msgstr "Usa il formato Major.Minor.Patch, es. 1.2.3" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +#, python-format +msgid "Values must be a dictionary" +msgstr "I valori devono essere un dizionario" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Variable Options" +msgstr "Opzioni variabile" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Variables" +msgstr "Variabili" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_version +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_version +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__version +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Version" +msgstr "Versione" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__version +msgid "Version in Major.Minor.Patch format, e.g. 1.0.0" +msgstr "Versione in formato Major.Minor.Patch, es. 1.0.0" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_export_wiz.py:0 +#, python-format +msgid "Version must be in format Major.Minor.Patch, e.g. 1.2.3" +msgstr "La versione deve essere nel formato Major.Minor.Patch, es. 1.2.3" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py:0 +#, python-format +msgid "Version must be in the Major.Minor.Patch format, e.g. 1.2.3" +msgstr "La versione deve essere nel formato Major.Minor.Patch, es. 1.2.3" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__manifest_website +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__manifest_website +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__website +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_import_wiz_view_form +msgid "Website" +msgstr "Sito web" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_manifest_tmpl__website +msgid "Website URL for the manifest." +msgstr "URL del sito web del manifest" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,help:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__if_record_exists +msgid "What to do if record with the same reference already exists" +msgstr "Cosa fare se esiste già un record con lo stesso riferimento" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#: code:addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py:0 +#, python-format +msgid "Wrong value for 'access_level' key: %(acv)s" +msgstr "Valore errato per 'access_level' key: %(acv)s" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_shortcut_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.view_cx_tower_scheduled_task_view_form +msgid "YAML" +msgstr "YAML" + +#. module: cetmix_tower_yaml +#: model:ir.module.category,name:cetmix_tower_yaml.ir_module_category_tower_yaml_export +msgid "YAML Export" +msgstr "Esporta YAML" + +#. module: cetmix_tower_yaml +#: model:ir.ui.menu,name:cetmix_tower_yaml.menu_yaml_settings_root +msgid "YAML Export/Import" +msgstr "Esporta/Importa YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_file_name +msgid "YAML File Name" +msgstr "Nome file YAML" + +#. module: cetmix_tower_yaml +#: model:ir.module.category,name:cetmix_tower_yaml.ir_module_category_tower_yaml_import +msgid "YAML Import" +msgstr "Importa YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_manifest_author +msgid "YAML Manifest Author" +msgstr "Autore manifest YAML" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_yaml_manifest_author +msgid "YAML Manifest Authors" +msgstr "Autori manifest YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model,name:cetmix_tower_yaml.model_cx_tower_yaml_manifest_tmpl +msgid "YAML Manifest Template" +msgstr "Modello manifest YAML" + +#. module: cetmix_tower_yaml +#: model:ir.actions.act_window,name:cetmix_tower_yaml.action_yaml_manifest_template +msgid "YAML Manifest Templates" +msgstr "Modelli manifest YAML" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "YAML file cannot be decoded properly" +msgstr "Il file YAML non può essere decodificato in modo corretto" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "YAML file doesn't contain any records" +msgstr "Il file YAML non contiene nessun record" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "YAML version is higher than version supported by your Cetmix Tower instance. %(code_version)s > %(tower_version)s" +msgstr "La versione YAML è successiva a quella supportata dalla tua istanza Cetmix Tower. %(code_version)s > %(tower_version)s" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_command__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_file_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_key_value__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_os__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_plan_line_action__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_scheduled_task__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_log__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_server_template__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_shortcut__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_tag__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_option__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_variable_value__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_mixin__yaml_code +msgid "Yaml Code" +msgstr "Codice YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_import_wiz_upload__yaml_file +msgid "Yaml File" +msgstr "File YAML" + +#. module: cetmix_tower_yaml +#: model:ir.model.fields,field_description:cetmix_tower_yaml.field_cx_tower_yaml_export_wiz_download__yaml_file_name +msgid "Yaml File Name" +msgstr "Nome file YAML" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py:0 +#: code:addons/cetmix_tower_yaml/wizards/cx_tower_yaml_import_wiz_upload.py:0 +#, python-format +msgid "Yaml file doesn't contain valid data" +msgstr "Il file YAML non contiene dati validi" + +#. module: cetmix_tower_yaml +#. odoo-python +#: code:addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py:0 +#, python-format +msgid "You are not allowed to create records from YAML" +msgstr "Non si è autorizzati a creare record da YAML" + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_command_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_file_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_plan_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_template_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_server_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_shortcut_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.view_cx_tower_scheduled_task_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "Bisogna appartenere al gruppo \"YAML/Export\" per esportare dati in YAML." + +#. module: cetmix_tower_yaml +#: model_terms:ir.ui.view,arch_db:cetmix_tower_yaml.cx_tower_yaml_export_wiz_view_form +msgid "my_snippet.yaml" +msgstr "my_snippet.yaml" + +#~ msgid "Comment" +#~ msgstr "Commento" + +#~ msgid "Comment to be added to the beginning of exported YAML file" +#~ msgstr "Commento da aggiungere all'inizio del file YAML esportato" + +#~ msgid "Cetmix Tower YAML" +#~ msgstr "YAML Cetmix Tower" + +#~ msgid "Cetmix Tower private key storage" +#~ msgstr "Deposito chiave privata Cetmix Tower" + +#~ msgid "" +#~ "If enabled, existing records will be updated with the new data. " +#~ "Otherwise, new records will be created." +#~ msgstr "" +#~ "Se abilitata, i record esistenti verranno aggiornati con i nuovi dati. " +#~ "altrimenti, verranno creati nuovi record." + +#~ msgid "Invalid model specified in the YAML file" +#~ msgstr "Modello non valido indicato nel file YAML" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#~ msgid "Model" +#~ msgstr "Modello" + +#~ msgid "Open Existing Record" +#~ msgstr "Apri record esistente" + +#~ msgid "Record" +#~ msgstr "Record" + +#~ msgid "This will create a new record. Proceed?" +#~ msgstr "Questo creerà un nuovo record. Procedere?" + +#~ msgid "" +#~ "YAML file version is not supported. You may need to update the Cetmix " +#~ "Tower Yaml module." +#~ msgstr "" +#~ "La versione del file YAML non è supportata. Bisogna aggiornare il modulo " +#~ "YAML Cetmix Tower." + +#~ msgid "cx.tower.yaml.import.wiz" +#~ msgstr "cx.tower.yaml.import.wiz" + +#~ msgid "Add entire related record data instead of just a reference" +#~ msgstr "" +#~ "Aggiungere tutti i dati del record correlato invece di un solo riferimento" + +#~ msgid "Explode" +#~ msgstr "Esplodi" diff --git a/addons/cetmix_tower_yaml/models/__init__.py b/addons/cetmix_tower_yaml/models/__init__.py new file mode 100644 index 0000000..44d2afc --- /dev/null +++ b/addons/cetmix_tower_yaml/models/__init__.py @@ -0,0 +1,27 @@ +from . import cx_tower_yaml_mixin +from . import cx_tower_command +from . import cx_tower_file_template +from . import cx_tower_tag +from . import cx_tower_plan +from . import cx_tower_plan_line +from . import cx_tower_plan_line_action +from . import cx_tower_variable +from . import cx_tower_variable_option +from . import cx_tower_variable_value +from . import cx_tower_os +from . import cx_tower_server_template +from . import cx_tower_key +from . import cx_tower_key_value +from . import cx_tower_server_log +from . import cx_tower_shortcut +from . import cx_tower_scheduled_task +from . import cx_tower_scheduled_task_cv +from . import cx_tower_file +from . import cx_tower_server +from . import cx_tower_yaml_manifest_template +from . import cx_tower_yaml_manifest_author +from . import cx_tower_jet_template +from . import cx_tower_jet_template_dependency +from . import cx_tower_jet_state +from . import cx_tower_jet_action +from . import cx_tower_jet_waypoint_template diff --git a/addons/cetmix_tower_yaml/models/cx_tower_command.py b/addons/cetmix_tower_yaml/models/cx_tower_command.py new file mode 100644 index 0000000..412e01c --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_command.py @@ -0,0 +1,43 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerCommand(models.Model): + _name = "cx.tower.command" + _inherit = ["cx.tower.command", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "access_level", + "name", + "action", + "allow_parallel_run", + "note", + "os_ids", + "tag_ids", + "path", + "file_template_id", + "if_file_exists", + "disconnect_file", + "flight_plan_id", + "jet_template_id", + "jet_action_id", + "waypoint_template_id", + "fly_here", + "code", + "no_split_for_sudo", + "server_status", + "variable_ids", + "secret_ids", + ] + return res + + def _get_deferred_m2o_import_fields(self): + """Return m2o command fields resolved after the main import pass.""" + return { + "jet_template_id": "cx.tower.jet.template", + "jet_action_id": "cx.tower.jet.action", + "waypoint_template_id": "cx.tower.jet.waypoint.template", + } diff --git a/addons/cetmix_tower_yaml/models/cx_tower_file.py b/addons/cetmix_tower_yaml/models/cx_tower_file.py new file mode 100644 index 0000000..983fa40 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_file.py @@ -0,0 +1,46 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerFile(models.Model): + _name = "cx.tower.file" + _inherit = ["cx.tower.file", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "source", + "file_type", + "server_dir", + "code", + "file", + "variable_ids", + "secret_ids", + "template_id", + "keep_when_deleted", + "auto_sync", + "auto_sync_interval", + "sync_date_next", + "sync_date_last", + "server_response", + ] + return res + + def _post_create_write(self, op_type="write"): + # Do not pull/push files if they are being created from YAML + if self.env.context.get("from_yaml"): + return + super()._post_create_write(op_type) + + def _prepare_record_for_yaml(self): + """ + Override to drop file `code` when the source is 'server'. + """ + record_dict = super()._prepare_record_for_yaml() + + if record_dict.get("source") == "server": + record_dict["code"] = False + + return record_dict diff --git a/addons/cetmix_tower_yaml/models/cx_tower_file_template.py b/addons/cetmix_tower_yaml/models/cx_tower_file_template.py new file mode 100644 index 0000000..12572df --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_file_template.py @@ -0,0 +1,25 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerFileTemplate(models.Model): + _name = "cx.tower.file.template" + _inherit = ["cx.tower.file.template", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "source", + "file_type", + "server_dir", + "file_name", + "keep_when_deleted", + "tag_ids", + "note", + "code", + "variable_ids", + "secret_ids", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_jet_action.py b/addons/cetmix_tower_yaml/models/cx_tower_jet_action.py new file mode 100644 index 0000000..2af9459 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_jet_action.py @@ -0,0 +1,26 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerJetAction(models.Model): + _name = "cx.tower.jet.action" + _inherit = [ + "cx.tower.jet.action", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "note", + "priority", + "access_level", + "state_from_id", + "state_transit_id", + "state_to_id", + "state_error_id", + "plan_id", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_jet_state.py b/addons/cetmix_tower_yaml/models/cx_tower_jet_state.py new file mode 100644 index 0000000..f4b6801 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_jet_state.py @@ -0,0 +1,22 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerJetState(models.Model): + _name = "cx.tower.jet.state" + _inherit = [ + "cx.tower.jet.state", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "sequence", + "access_level", + "color", + "note", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_jet_template.py b/addons/cetmix_tower_yaml/models/cx_tower_jet_template.py new file mode 100644 index 0000000..3dca470 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_jet_template.py @@ -0,0 +1,42 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerJetTemplate(models.Model): + _name = "cx.tower.jet.template" + _inherit = [ + "cx.tower.jet.template", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "note", + "tag_ids", + "limit_per_server", + "show_in_create_wizard", + "plan_install_id", + "plan_uninstall_id", + "plan_clone_same_server_id", + "plan_clone_different_server_id", + "variable_value_ids", + "action_ids", + "template_requires_ids", + "waypoint_template_ids", + "server_log_ids", + "scheduled_task_ids", + ] + return res + + def _get_deferred_x2m_import_fields(self): + """Return x2m child records resolved after the main import pass.""" + return { + "template_requires_ids": { + "child_model": "cx.tower.jet.template.dependency", + "deferred_field": "template_required_id", + "target_model": "cx.tower.jet.template", + } + } diff --git a/addons/cetmix_tower_yaml/models/cx_tower_jet_template_dependency.py b/addons/cetmix_tower_yaml/models/cx_tower_jet_template_dependency.py new file mode 100644 index 0000000..da00652 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_jet_template_dependency.py @@ -0,0 +1,19 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerJetTemplateDependency(models.Model): + _name = "cx.tower.jet.template.dependency" + _inherit = [ + "cx.tower.jet.template.dependency", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "template_required_id", + "state_required_id", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_jet_waypoint_template.py b/addons/cetmix_tower_yaml/models/cx_tower_jet_waypoint_template.py new file mode 100644 index 0000000..f9dfd73 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_jet_waypoint_template.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerJetWaypointTemplate(models.Model): + _name = "cx.tower.jet.waypoint.template" + _inherit = [ + "cx.tower.jet.waypoint.template", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "sequence", + "access_level", + "jet_template_id", + "plan_create_id", + "plan_arrive_id", + "plan_leave_id", + "plan_delete_id", + "note", + ] + return res + + def _get_deferred_m2o_import_fields(self): + """Return m2o waypoint-template fields resolved after import.""" + return { + "jet_template_id": "cx.tower.jet.template", + } diff --git a/addons/cetmix_tower_yaml/models/cx_tower_key.py b/addons/cetmix_tower_yaml/models/cx_tower_key.py new file mode 100644 index 0000000..80797f6 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_key.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerKey(models.Model): + _name = "cx.tower.key" + _inherit = [ + "cx.tower.key", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "key_type", + "note", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_key_value.py b/addons/cetmix_tower_yaml/models/cx_tower_key_value.py new file mode 100644 index 0000000..f2a6b91 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_key_value.py @@ -0,0 +1,18 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerKeyValue(models.Model): + _name = "cx.tower.key.value" + _inherit = [ + "cx.tower.key.value", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "key_id", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_os.py b/addons/cetmix_tower_yaml/models/cx_tower_os.py new file mode 100644 index 0000000..5706768 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_os.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerOs(models.Model): + _name = "cx.tower.os" + _inherit = [ + "cx.tower.os", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "color", + "parent_id", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_plan.py b/addons/cetmix_tower_yaml/models/cx_tower_plan.py new file mode 100644 index 0000000..a5f3697 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_plan.py @@ -0,0 +1,40 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerPlan(models.Model): + _name = "cx.tower.plan" + _inherit = ["cx.tower.plan", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "access_level", + "allow_parallel_run", + "color", + "tag_ids", + "note", + "on_error_action", + "custom_exit_code", + "line_ids", + ] + return res + + def _get_deferred_x2m_import_fields(self): + """Defer plan lines whose command is not resolvable during nested import. + + Deep YAML (e.g. a command's waypoint inlines a jet template whose plans + reference that same command) creates a forward reference: plan lines are + prepared before the command exists in the database. Queue those lines + and create them after the main import pass when ``command_id`` can be + resolved. + """ + return { + "line_ids": { + "child_model": "cx.tower.plan.line", + "deferred_field": "command_id", + "target_model": "cx.tower.command", + } + } diff --git a/addons/cetmix_tower_yaml/models/cx_tower_plan_line.py b/addons/cetmix_tower_yaml/models/cx_tower_plan_line.py new file mode 100644 index 0000000..b28a997 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_plan_line.py @@ -0,0 +1,21 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerPlanLine(models.Model): + _name = "cx.tower.plan.line" + _inherit = ["cx.tower.plan.line", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "sequence", + "condition", + "use_sudo", + "path", + "command_id", + "action_ids", + "variable_ids", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_plan_line_action.py b/addons/cetmix_tower_yaml/models/cx_tower_plan_line_action.py new file mode 100644 index 0000000..ce3e36d --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_plan_line_action.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerPlanLineAction(models.Model): + _name = "cx.tower.plan.line.action" + _inherit = ["cx.tower.plan.line.action", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "sequence", + "condition", + "value_char", + "action", + "custom_exit_code", + "variable_value_ids", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task.py b/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task.py new file mode 100644 index 0000000..a439df9 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task.py @@ -0,0 +1,42 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerScheduledTask(models.Model): + _name = "cx.tower.scheduled.task" + _inherit = ["cx.tower.scheduled.task", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "sequence", + "action", + "command_id", + "plan_id", + "interval_number", + "interval_type", + "next_call", + "last_call", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + "custom_variable_value_ids", + ] + return res + + def _get_deferred_x2m_import_fields(self): + """Return scheduled-task child records resolved after import.""" + return { + "custom_variable_value_ids": { + "child_model": "cx.tower.scheduled.task.cv", + "deferred_field": "variable_value_id", + "target_model": "cx.tower.variable.value", + "skip_empty": True, + } + } diff --git a/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task_cv.py b/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task_cv.py new file mode 100644 index 0000000..688d752 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_scheduled_task_cv.py @@ -0,0 +1,36 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerScheduledTaskCv(models.Model): + _name = "cx.tower.scheduled.task.cv" + _inherit = [ + "cx.tower.scheduled.task.cv", + "cx.tower.yaml.mixin", + "cx.tower.reference.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += ["variable_value_id"] + return res + + def _post_process_yaml_dict_values(self, values): + """Populate required child fields from the linked variable value.""" + res = super()._post_process_yaml_dict_values(values) + variable_value_id = res.get("variable_value_id") + if variable_value_id: + variable_value = self.env["cx.tower.variable.value"].browse( + variable_value_id + ) + if variable_value.exists(): + res.update( + { + "name": variable_value.name, + "variable_id": variable_value.variable_id.id, + "option_id": variable_value.option_id.id or False, + "value_char": variable_value.value_char, + } + ) + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_server.py b/addons/cetmix_tower_yaml/models/cx_tower_server.py new file mode 100644 index 0000000..0633077 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_server.py @@ -0,0 +1,52 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerServer(models.Model): + _name = "cx.tower.server" + _inherit = [ + "cx.tower.server", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "ip_v4_address", + "ip_v6_address", + "skip_host_key", + "color", + "os_id", + "tag_ids", + "url", + "note", + "ssh_port", + "ssh_username", + "ssh_key_id", + "ssh_auth_mode", + "use_sudo", + "variable_value_ids", + "secret_ids", + "server_log_ids", + "shortcut_ids", + "scheduled_task_ids", + "plan_delete_id", + "file_ids", + "command_ids", + "plan_ids", + ] + return res + + def _get_force_x2m_resolve_models(self): + res = super()._get_force_x2m_resolve_models() + + # This is useful to avoid duplicating existing plans + res += [ + "cx.tower.shortcut", + "cx.tower.scheduled.task", + "cx.tower.command", + "cx.tower.plan", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_server_log.py b/addons/cetmix_tower_yaml/models/cx_tower_server_log.py new file mode 100644 index 0000000..34ff785 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_server_log.py @@ -0,0 +1,23 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerServerLog(models.Model): + _name = "cx.tower.server.log" + _inherit = [ + "cx.tower.server.log", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "log_type", + "command_id", + "use_sudo", + "file_template_id", + "file_id", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_server_template.py b/addons/cetmix_tower_yaml/models/cx_tower_server_template.py new file mode 100644 index 0000000..1390a06 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_server_template.py @@ -0,0 +1,41 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerServerTemplate(models.Model): + _name = "cx.tower.server.template" + _inherit = [ + "cx.tower.server.template", + "cx.tower.yaml.mixin", + ] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "color", + "os_id", + "tag_ids", + "note", + "ssh_port", + "ssh_username", + "ssh_key_id", + "ssh_auth_mode", + "use_sudo", + "variable_value_ids", + "server_log_ids", + "shortcut_ids", + "scheduled_task_ids", + "flight_plan_id", + "plan_delete_id", + ] + return res + + def _get_force_x2m_resolve_models(self): + res = super()._get_force_x2m_resolve_models() + + # Add Flight Plan in order to always try to use existing one + # This is useful to avoid duplicating existing plans + res += ["cx.tower.plan", "cx.tower.shortcut", "cx.tower.scheduled.task"] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_shortcut.py b/addons/cetmix_tower_yaml/models/cx_tower_shortcut.py new file mode 100644 index 0000000..51a2120 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_shortcut.py @@ -0,0 +1,22 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerShortcut(models.Model): + _name = "cx.tower.shortcut" + _inherit = ["cx.tower.shortcut", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "sequence", + "access_level", + "action", + "command_id", + "use_sudo", + "plan_id", + "note", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_tag.py b/addons/cetmix_tower_yaml/models/cx_tower_tag.py new file mode 100644 index 0000000..d1fc446 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_tag.py @@ -0,0 +1,16 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerTag(models.Model): + _name = "cx.tower.tag" + _inherit = ["cx.tower.tag", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "color", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_variable.py b/addons/cetmix_tower_yaml/models/cx_tower_variable.py new file mode 100644 index 0000000..0a80468 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_variable.py @@ -0,0 +1,23 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerVariable(models.Model): + _name = "cx.tower.variable" + _inherit = ["cx.tower.variable", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "access_level", + "variable_type", + "option_ids", + "applied_expression", + "validation_pattern", + "validation_message", + "note", + "tag_ids", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_variable_option.py b/addons/cetmix_tower_yaml/models/cx_tower_variable_option.py new file mode 100644 index 0000000..81b3835 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_variable_option.py @@ -0,0 +1,18 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerVariableOption(models.Model): + _name = "cx.tower.variable.option" + _inherit = ["cx.tower.variable.option", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "sequence", + "access_level", + "name", + "value_char", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_variable_value.py b/addons/cetmix_tower_yaml/models/cx_tower_variable_value.py new file mode 100644 index 0000000..947d860 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_variable_value.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerVariableValue(models.Model): + _name = "cx.tower.variable.value" + _inherit = ["cx.tower.variable.value", "cx.tower.yaml.mixin"] + + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "sequence", + "access_level", + "variable_id", + "value_char", + "variable_ids", + "required", + ] + return res diff --git a/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_author.py b/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_author.py new file mode 100644 index 0000000..e2dbbb5 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_author.py @@ -0,0 +1,23 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class CxTowerYamlManifestAuthor(models.Model): + """Author of a YAML manifest (can be one or many).""" + + _name = "cx.tower.yaml.manifest.author" + + _sql_constraints = [ + ( + "yaml_manifest_author_name_uniq", + "unique(name)", + "Author name must be unique.", + ) + ] + _description = "YAML Manifest Author" + _order = "name" + + name = fields.Char(required=True, translate=False) diff --git a/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py b/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py new file mode 100644 index 0000000..f0f3d57 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_yaml_manifest_template.py @@ -0,0 +1,93 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import re + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CxTowerYamlManifestTemplate(models.Model): + """Pre-defined YAML manifest template storing common metadata + such as authors, website, license, and currency for reuse + during YAML exports.""" + + _name = "cx.tower.yaml.manifest.tmpl" + _description = "YAML Manifest Template" + _order = "name" + + name = fields.Char( + required=True, + help="Name of the manifest template.", + ) + website = fields.Char(help="Website URL for the manifest.") + + author_ids = fields.Many2many( + "cx.tower.yaml.manifest.author", + string="Authors", + help="List of author names to include in the YAML manifest.", + ) + + license = fields.Selection( + selection=lambda self: self._selection_license(), + help="License used for the code snippet.", + ) + license_text = fields.Text( + help="Custom license text when license type is Custom.", + ) + + currency = fields.Selection( + selection=lambda self: self._selection_currency(), + help="Currency for pricing information.", + ) + + version = fields.Char( + help="Version in Major.Minor.Patch format, e.g. 1.0.0", + default="1.0.0", + ) + + file_prefix = fields.Char( + string="File prefix", + help="Add prefix to the exported YAML file name when this template is selected", + ) + + @api.model + def _selection_license(self): + """Return available license options for manifest.""" + return [ + ("agpl-3", "AGPL-3"), + ("lgpl-3", "LGPL-3"), + ("mit", "MIT"), + ("custom", _("Custom")), + ] + + @api.model + def _selection_currency(self): + """Return available currency options for manifest pricing.""" + return [ + ("EUR", _("Euro")), + ("USD", _("US Dollar")), + ] + + @api.constrains("license", "license_text") + def _check_license_text_for_custom(self): + """Ensure that custom license text is provided when license is 'custom'.""" + for rec in self: + if rec.license == "custom" and not (rec.license_text or "").strip(): + raise ValidationError( + _("Provide Custom License Text when License is set to 'Custom'.") + ) + + @api.constrains("version") + def _check_version_format(self): + """Ensure the template version follows the x.y.z semantic format. + + The version must consist of three non-negative integers (major, minor, patch) + separated by dots—for example, “1.2.3”. Raises a ValidationError otherwise. + """ + semver = re.compile(r"^\d+\.\d+\.\d+$") + for rec in self: + if rec.version and not semver.match(rec.version): + raise ValidationError( + _("Version must be in the Major.Minor.Patch format, e.g. 1.2.3") + ) diff --git a/addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py b/addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py new file mode 100644 index 0000000..13a7210 --- /dev/null +++ b/addons/cetmix_tower_yaml/models/cx_tower_yaml_mixin.py @@ -0,0 +1,776 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import copy +import logging + +import yaml + +from odoo import _, api, fields, models +from odoo.exceptions import AccessError, ValidationError + +_logger = logging.getLogger(__name__) +DEFERRED_M2O_IMPORT = object() + + +class CustomDumper(yaml.Dumper): + """Custom dumper to ensures code + is properly dumped in YAML + """ + + def represent_scalar(self, tag, value, style=None): + if isinstance(value, str) and "\n" in value: + style = "|" + return super().represent_scalar(tag, value, style) + + +class YamlExportCollector: + """ + Collector for YAML export. + Tracks unique records by their (model_name, reference) tuple to avoid duplicates. + """ + + def __init__(self): + """ + Initialize the collector. + """ + self.added_references = set() + + def add(self, key): + """ + Add a record to the collector if its reference is unique. + :param key: tuple, key of the record + """ + if key and key not in self.added_references: + self.added_references.add(key) + + def is_added(self, key): + """ + Check by (model, reference) tuple. + :param key: tuple, key of the record + :return: bool + """ + return key in self.added_references + + +class CxTowerYamlMixin(models.AbstractModel): + """Used to implement YAML rendering functions. + Inherit in your model in case you want to YAML instance of the records. + """ + + _name = "cx.tower.yaml.mixin" + _description = "Cetmix Tower YAML rendering mixin" + + # File format version in order to track compatibility + CETMIX_TOWER_YAML_VERSION = 1 + + # TO_YAML_* used to convert from Odoo field values to YAML + TO_YAML_ACCESS_LEVEL = {"1": "user", "2": "manager", "3": "root"} + + # TO_TOWER_* used to convert from YAML field values to Tower ones + TO_TOWER_ACCESS_LEVEL = {"user": "1", "manager": "2", "root": "3"} + + yaml_code = fields.Text( + compute="_compute_yaml_code", + inverse="_inverse_yaml_code", + groups="cetmix_tower_yaml.group_export,cetmix_tower_yaml.group_import", + ) + + def _compute_yaml_code(self): + """Compute YAML code based on model record data""" + # This is used for the file name. + # Eg cx.tower.command record will have 'command_' prefix. + for record in self: + # Use a shared collector from context when one is provided (e.g. by + # the export wizard for cross-record deduplication); otherwise use a + # fresh per-record collector so that each record's yaml_code is + # deterministic regardless of which sibling records are batched. + collector = record._context.get("yaml_collector") or YamlExportCollector() + # We are reading field list for each record + # because list of fields can differ from record to record + record.yaml_code = self._convert_dict_to_yaml( + record.with_context(yaml_collector=collector)._prepare_record_for_yaml() + ) + + def _inverse_yaml_code(self): + """Compose record based on provided YAML""" + for record in self: + if record.yaml_code: + record_yaml_dict = yaml.safe_load(record.yaml_code) + record_vals = record._post_process_yaml_dict_values(record_yaml_dict) + record.update(record_vals) + + @api.constrains("yaml_code") + def _check_yaml_code_write_access(self): + """ + Check if user has access to create records from YAML. + This is checked only when user already has access to export YAML. + Otherwise, the field is not accessible due to security group. + """ + if self.env.user.has_group("cetmix_tower_yaml.group_export") and ( + not self.env.user.has_group("cetmix_tower_yaml.group_import") + and not self.env.user._is_superuser() + ): + raise AccessError(_("You are not allowed to create records from YAML")) + + def create(self, vals_list): + # Handle validation error when field values are not valid + try: + return super().create(vals_list) + except ValueError as e: + raise ValidationError(str(e)) from e + + def write(self, vals): + # Handle validation error when field values are not valid + try: + return super().write(vals) + except ValueError as e: + raise ValidationError(str(e)) from e + + def action_open_yaml_export_wizard(self): + """Open YAML export wizard""" + + return { + "type": "ir.actions.act_window", + "res_model": "cx.tower.yaml.export.wiz", + "view_mode": "form", + "target": "new", + } + + def _convert_dict_to_yaml(self, values): + """Converts Python dictionary to YAML string. + + This is a helper function that is designed to be used + by any models that need to convert a dictionary to YAML. + + Args: + values (Dict): Dictionary containing data + to be converted to YAML format + Returns: + Text: YAML string + Raises: + ValidationError: If values is not a dictionary + or YAML conversion fails + """ + if not isinstance(values, dict): + raise ValidationError(_("Values must be a dictionary")) + try: + yaml_code = yaml.dump( + values, + Dumper=CustomDumper, + default_flow_style=False, + sort_keys=False, + ) + return yaml_code + except (yaml.YAMLError, UnicodeEncodeError) as e: + raise ValidationError( + _( + "Failed to convert dictionary" " to YAML: %(error)s", + error=str(e), + ) + ) from e + + def _prepare_record_for_yaml(self): + """Reads and processes current record before converting it to YAML + + Returns: + dict: values ready for YAML conversion + """ + self.ensure_one() + yaml_keys = self._get_fields_for_yaml() + record_dict = self.read(fields=yaml_keys)[0] + return self._post_process_record_values(record_dict) + + def _get_fields_for_yaml(self): + """Get ist of field to be present in YAML + + Set 'no_yaml_service_fields' context key to skip + service fields creation (cetmix_tower_yaml_version, cetmix_tower_model) + + Returns: + list(): list of fields to be used as YAML keys + """ + return ["reference"] + + def _get_force_x2m_resolve_models(self): + """List of models that will always try to be resolved + when referenced in x2m related fields. + + This is useful for models that should always use existing records + instead of creating new ones when referenced in x2m related fields. + Such as variables or tags. + + Returns: + List: list of models that will always try to be resolved + """ + return [ + "cx.tower.variable", + "cx.tower.variable.option", + "cx.tower.tag", + "cx.tower.os", + "cx.tower.key", + ] + + def _get_deferred_m2o_import_fields(self): + """Map m2o fields that should be resolved after the main import pass. + + Returns: + dict: Field name to expected target model mapping. + """ + return {} + + def _get_deferred_x2m_import_fields(self): + """Map x2m child records that should be created after the main import pass. + + Returns: + dict: Parent field name to deferred child spec mapping. + """ + return {} + + def _has_meaningful_yaml_value(self, value): + """Return whether a YAML value contains meaningful payload.""" + if value is False or value is None or value == "": + return False + if isinstance(value, dict): + if set(value.keys()) == {"reference"}: + return bool(value.get("reference")) + return any( + self._has_meaningful_yaml_value(item) + for key, item in value.items() + if key != "reference" + ) + if isinstance(value, list): + return any(self._has_meaningful_yaml_value(item) for item in value) + return True + + def _get_reference_only_yaml_relation_reference(self, value): + """Return reference for reference-only YAML relation values. + + Args: + value (str | dict): YAML relation value. + + Returns: + str | bool: Reference if the value is reference-only, otherwise False. + """ + if isinstance(value, str): + return value + if isinstance(value, dict) and set(value.keys()) == {"reference"}: + return value.get("reference") or False + return False + + def _queue_deferred_m2o_import(self, field, comodel, value): + """Queue unresolved m2o relation for the deferred import pass. + + Args: + field (str): Owner field name. + comodel (BaseModel): Related model. + value (str | dict): YAML relation value. + + Returns: + bool: True when the relation was queued for deferred resolution. + """ + queue = self._context.get("yaml_deferred_m2o_queue") + if queue is None: + return False + + deferred_fields = self._get_deferred_m2o_import_fields() + expected_model = deferred_fields.get(field) + if not expected_model or expected_model != comodel._name: + return False + + target_reference = self._get_reference_only_yaml_relation_reference(value) + if not target_reference or comodel.get_by_reference(target_reference): + return False + + record_reference = self._context.get("yaml_import_record_reference") + if not record_reference: + return False + + queue.append( + { + "record_model": self._name, + "record_reference": record_reference, + "field_name": field, + "target_model": comodel._name, + "target_reference": target_reference, + } + ) + return True + + def _queue_deferred_x2m_import(self, field, comodel, value): + """Queue unresolved x2m child record for the deferred import pass. + + Args: + field (str): Owner x2m field name. + comodel (BaseModel): Related child model. + value (dict): YAML child record value. + + Returns: + bool: True when the child was queued for deferred creation or + should be skipped (e.g., empty value with skip_empty=True). + """ + queue = self._context.get("yaml_deferred_x2m_queue") + if queue is None or not isinstance(value, dict): + return False + + deferred_fields = self._get_deferred_x2m_import_fields() + spec = deferred_fields.get(field) or {} + if spec.get("child_model") != comodel._name: + return False + + if spec.get("skip_empty") and not self._has_meaningful_yaml_value(value): + return True + + deferred_field = spec.get("deferred_field") + if not deferred_field: + return False + + target_model = spec.get("target_model") + target_value = value.get(deferred_field) + target_reference = self._get_reference_only_yaml_relation_reference( + target_value + ) + if not target_model or not target_reference: + return False + + target_record = self.env[target_model].get_by_reference(target_reference) + if target_record: + return False + + record_reference = self._context.get("yaml_import_record_reference") + if not record_reference: + return False + + queue.append( + { + "record_model": self._name, + "record_reference": record_reference, + "field_name": field, + "child_model": comodel._name, + "deferred_field": deferred_field, + "target_model": target_model, + "target_reference": target_reference, + "values": copy.deepcopy(value), + } + ) + return True + + def _get_yaml_duplicate_reference_dict(self, ref, values): + """Return the stub emitted when a record has already been serialized. + + The collector deduplicates by (model, reference); subsequent occurrences + are collapsed to a reference-only dict. Import must never attempt to create + from this stub — it must resolve the record by reference instead. + + Args: + ref (str): Record reference. + values (dict): Raw values (unused; kept for signature compatibility + in case subclasses need them). + + Returns: + dict: ``{"reference": ref}`` only. + """ + return {"reference": ref} + + def _post_process_record_values(self, values): + """Post process record values + before converting them to YAML + + Args: + values (dict): values returned by 'read' method + + Context: + explode_related_record: if set will return entire record dictionary + not just a reference + remove_empty_values: if set will remove empty values from the record + + Returns: + dict(): processed values + """ + collector = self._context.get("yaml_collector") + ref = values.get("reference") + collector_key = (self._name, ref) if ref else None + + if collector and collector_key and collector.is_added(collector_key): + return self._get_yaml_duplicate_reference_dict(ref, values) + + if collector and collector_key: + collector.add(collector_key) + + if collector and collector_key: + collector.add(collector_key) + + # We don't need id because we are not using it + values.pop("id", None) + + # Add YAML format version and model + if not self._context.get("no_yaml_service_fields"): + model_name = self._name.replace("cx.tower.", "").replace(".", "_") + model_values = { + "cetmix_tower_model": model_name, + } + else: + model_values = {} + + # Parse access level + access_level = values.pop("access_level", None) + if access_level: + model_values.update( + {"access_level": self.TO_YAML_ACCESS_LEVEL[access_level]} + ) + + values = {**model_values, **values} + # Copy values to avoid modifying the original values + new_values = values.copy() + + # Check if we need to return a record dict or just a reference + # Use context value first, revert to the record setting if not defined + explode_related_record = self._context.get("explode_related_record") + + # Check if we need to remove empty values + # Currently only x2m fields are supported + remove_empty_values = self._context.get("remove_empty_values") + + # Post process m2o and x2m fields + for key, value in values.items(): + # IMPORTANT: Odoo naming patterns must be followed for related fields. + # This is why we are checking for the field name ending here. + # Further checks for the field type are done + # in _process_relation_field_value() + if key.endswith("_id") or key.endswith("_ids"): + if not value and remove_empty_values: + del new_values[key] + else: + processed_value = self.with_context( + explode_related_record=explode_related_record + )._process_relation_field_value(key, value, record_mode=True) + new_values.update({key: processed_value}) + + return new_values + + def _post_process_yaml_dict_values(self, values): + """Post process dictionary values generated from YAML code + + Args: + values (dict): Dictionary generated from YAML + + Returns: + dict(): Post-processed values + """ + + # Remove model data because it is not a field + if "cetmix_tower_model" in values: + values.pop("cetmix_tower_model") + + # Parse access level + if "access_level" in values: + values_access_level = values["access_level"] + access_level = self.TO_TOWER_ACCESS_LEVEL.get(values_access_level) + if access_level: + values.update({"access_level": access_level}) + else: + raise ValidationError( + _( + "Wrong value for 'access_level' key: %(acv)s", + acv=values_access_level, + ) + ) + + # Leave supported keys only + supported_keys = self._get_fields_for_yaml() + filtered_values = {k: v for k, v in values.items() if k in supported_keys} + + # Post process m2o fields + for key, value in list(filtered_values.items()): + # IMPORTANT: Odoo naming patterns must be followed for related fields. + # This is why we are checking for the field name ending here. + # Further checks for the field type are done + # in _process_relation_field_value() + if key.endswith("_id") or key.endswith("_ids"): + processed_value = self.with_context( + explode_related_record=True, + yaml_import_record_reference=filtered_values.get("reference"), + )._process_relation_field_value(key, value, record_mode=False) + if processed_value is DEFERRED_M2O_IMPORT: + filtered_values.pop(key, None) + else: + filtered_values.update({key: processed_value}) + + return filtered_values + + def _process_relation_field_value(self, field, value, record_mode=False): + """Post process One2many, Many2many or Many2one value + + Args: + field (Char): Field the value belongs to + value (Char): Value to process + record_mode (Bool): If True process value as a record value + else process value as a YAML value + Context: + explode_related_record: if set will return entire record dictionary + not just a reference + Returns: + dict() or Char: record dictionary if fetch_record else reference + """ + # Step 1: Return False if the value is not set or the field is not found + if not value: + return False + + field_obj = self._fields.get(field) + if not field_obj: + return False + + # Step 2: Return False if the field type doesn't match + # or comodel is not defined + field_type = field_obj.type + if ( + field_type not in ["one2many", "many2many", "many2one"] + or not field_obj.comodel_name + ): + return False + + comodel = self.env[field_obj.comodel_name] + explode_related_record = self._context.get("explode_related_record") + + # Step 3: process value based on the field type + if field_type == "many2one": + return self._process_m2o_value( + field, comodel, value, explode_related_record, record_mode + ) + if field_type in ["one2many", "many2many"]: + return self._process_x2m_values( + field, comodel, field_type, value, explode_related_record, record_mode + ) + + # Step 4: fall back if field type is not supported + return False + + def _process_m2o_value( + self, field, comodel, value, explode_related_record, record_mode=False + ): + """Post process many2one value + Args: + field (Char): Field the value belongs to + comodel (BaseClass): Model the value belongs to + value (Char): Value to process + explode_related_record (Bool): If True return entire record dict + instead of a reference + record_mode (Bool): If True process value as a record value + else process value as a YAML value + + Returns: + dict() or Char: record dictionary if fetch_record else reference + """ + + # -- (Record -> YAML) + if record_mode: + # Retrieve the record based on the ID provided in the value + record = comodel.browse(value[0]) + + # If the context specifies to explode the related record, + # return its dictionary representation + if explode_related_record: + return ( + record.with_context( + no_yaml_service_fields=True + )._prepare_record_for_yaml() + if record + else False + ) + + # Otherwise, return just the reference (or False if record does not exist) + return record.reference if record else False + + # -- (YAML -> Record) + # Step 1: Process value in normal mode + record = False + if self._queue_deferred_m2o_import(field, comodel, value): + return DEFERRED_M2O_IMPORT + + # If the value is a string, it is treated as a reference + if isinstance(value, str): + reference = value + + # If the value is a dictionary, extract the reference from it + elif isinstance(value, dict): + reference = value.get("reference") + if self._get_reference_only_yaml_relation_reference(value): + record = False + else: + record = self._update_or_create_related_record( + comodel, reference, value, create_immediately=True + ) + + else: + return False + + # Step 2: Final fallback: attempt to retrieve the record by reference if set, + # return its ID or False + if not record and reference: + record = comodel.get_by_reference(reference) + return record.id if record else False + + def _process_x2m_values( + self, + field, + comodel, + field_type, + values, + explode_related_record, + record_mode=False, + ): + """Post process many2many value + Args: + field (Char): Field the value belongs to + comodel (BaseClass): Model the value belongs to + field_type (Char): Field type + values (list()): Values to process + explode_related_record (Bool): If True return entire record dict + instead of a reference + record_mode (Bool): If True process value as a record value + else process value as a YAML value + + Returns: + dict() or Char: record dictionary if fetch_record else reference + """ + + # -- (Record -> YAML) + if record_mode: + record_list = [] + for value in values: + # Retrieve the record based on the ID provided in the value + record = comodel.browse(value) + + # If the context specifies to explode the related record, + # return its dictionary representation + if explode_related_record: + record_list.append( + record.with_context( + no_yaml_service_fields=True + )._prepare_record_for_yaml() + if record + else False + ) + + # Otherwise, return just the reference + # (or False if record does not exist) + else: + record_list.append(record.reference if record else False) + + return record_list + + # -- (YAML -> Record) + # Step 1: Process value in normal mode + record_ids = [] + + for value in values: + record = False + # If the value is a string, it is treated as a reference + if isinstance(value, str): + reference = value + + # If the value is a dictionary, extract the reference from it + elif isinstance(value, dict): + if self._queue_deferred_x2m_import(field, comodel, value): + continue + reference = value.get("reference") + record = self._update_or_create_related_record( + comodel, + reference, + value, + create_immediately=field_type == "many2many", + ) + + # Step 2: Final fallback: attempt to retrieve the record by reference + # Return record ID or False if reference is not defined + if not record and reference: + record = comodel.get_by_reference(reference) + + # Save record data + if record: + record_ids.append( + record if isinstance(record, tuple) else (4, record.id) + ) + + return record_ids + + def _update_or_create_related_record( + self, model, reference, values, create_immediately=False + ): + """Update related record with provided values or create a new one + + Args: + model (BaseModel): Related record model + values (dict()): Values to update existing/create new record + reference (Char): Record reference + create_immediately (Bool): If True create a new record immediately. + Used for Many2one fields. + + Context: + force_create_related_record (Bool): If True, create a new record + even if reference is provided. + + Returns: + record: Existing record or new record tuple + """ + + # If reference is found, retrieve the corresponding record + if reference and ( + model._name in self._get_force_x2m_resolve_models() + or not self._context.get("force_create_related_record") + ): + record = model.get_by_reference(reference) + # If the record exists, update it with the values from the dictionary + if record: + # Remove reference from values to avoid possible consequences + values.pop("reference", None) + record.with_context(from_yaml=True).write( + record._post_process_yaml_dict_values(values) + ) + + # If the record does not exist, create a new one + else: + if set(values.keys()) == {"reference"}: + _logger.warning( + "Attempted to import a record for model '%s' " + "with reference " + "'%s', but only the 'reference' field was provided. " + "Creation will be skipped until the target record " + "exists.", + model._name, + reference, + ) + return False + if create_immediately: + record = model.with_context(from_yaml=True).create( + model._post_process_yaml_dict_values(values) + ) + else: + # Use "Create" service command tuple + record = (0, 0, model._post_process_yaml_dict_values(values)) + + # If there's no reference but value is a dict, create a new record + else: + # Only 'reference' provided, no other data: do not create, + # just log warning + if set(values.keys()) == {"reference"}: + _logger.warning( + "Attempted to import a record for model '%s' with reference " + "'%s', but only the 'reference' field was provided. " + "It is possible that this record has already been imported. " + "Creation will be skipped.", + model._name, + reference, + ) + return False + + if create_immediately: + record = model.with_context(from_yaml=True).create( + model._post_process_yaml_dict_values(values) + ) + else: + # Use "Create" service command tuple + record = (0, 0, model._post_process_yaml_dict_values(values)) + + # Return the record's ID if it exists, otherwise return False + return record or False diff --git a/addons/cetmix_tower_yaml/pyproject.toml b/addons/cetmix_tower_yaml/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/addons/cetmix_tower_yaml/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/addons/cetmix_tower_yaml/readme/CONFIGURE.md b/addons/cetmix_tower_yaml/readme/CONFIGURE.md new file mode 100644 index 0000000..8c717e5 --- /dev/null +++ b/addons/cetmix_tower_yaml/readme/CONFIGURE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions. diff --git a/addons/cetmix_tower_yaml/readme/DESCRIPTION.md b/addons/cetmix_tower_yaml/readme/DESCRIPTION.md new file mode 100644 index 0000000..2f07eba --- /dev/null +++ b/addons/cetmix_tower_yaml/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module implements YAML format data import/export for [Cetmix Tower](https://cetmix.com/tower). + +Please refer to the [official documentation](https://cetmix.com/tower) for detailed information. diff --git a/addons/cetmix_tower_yaml/readme/HISTORY.md b/addons/cetmix_tower_yaml/readme/HISTORY.md new file mode 100644 index 0000000..b0cfcea --- /dev/null +++ b/addons/cetmix_tower_yaml/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 18.0.2.0.0 (2026-04-07) + +- Features: Jets! (4700) diff --git a/addons/cetmix_tower_yaml/readme/USAGE.md b/addons/cetmix_tower_yaml/readme/USAGE.md new file mode 100644 index 0000000..901f5a6 --- /dev/null +++ b/addons/cetmix_tower_yaml/readme/USAGE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions. diff --git a/addons/cetmix_tower_yaml/readme/newsfragments/.gitkeep b/addons/cetmix_tower_yaml/readme/newsfragments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/addons/cetmix_tower_yaml/security/cetmix_tower_yaml_groups.xml b/addons/cetmix_tower_yaml/security/cetmix_tower_yaml_groups.xml new file mode 100644 index 0000000..ad81bf0 --- /dev/null +++ b/addons/cetmix_tower_yaml/security/cetmix_tower_yaml_groups.xml @@ -0,0 +1,28 @@ + + + + + YAML Export + + + + + YAML Import + + + + Allow + + + Export data to YAML. + + + + + Allow + + + Import data from YAML. + + + diff --git a/addons/cetmix_tower_yaml/security/cx_tower_yaml_wizard_access_rules.xml b/addons/cetmix_tower_yaml/security/cx_tower_yaml_wizard_access_rules.xml new file mode 100644 index 0000000..f5eb546 --- /dev/null +++ b/addons/cetmix_tower_yaml/security/cx_tower_yaml_wizard_access_rules.xml @@ -0,0 +1,34 @@ + + + + + Creator only + + + [('create_uid', '=', user.id)] + + + + + Creator only + + + [('create_uid', '=', user.id)] + + + + + Creator only + + + [('create_uid', '=', user.id)] + + + + + Creator only + + + [('create_uid', '=', user.id)] + + diff --git a/addons/cetmix_tower_yaml/security/ir.model.access.csv b/addons/cetmix_tower_yaml/security/ir.model.access.csv new file mode 100644 index 0000000..bb6e5dd --- /dev/null +++ b/addons/cetmix_tower_yaml/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_yaml_export_wizard,Export YAML,model_cx_tower_yaml_export_wiz,group_export,1,1,1,1 +access_yaml_export_wizard_download,Export YAML File,model_cx_tower_yaml_export_wiz_download,group_export,1,1,1,1 +access_yaml_import_wizard_upload,Import YAML,model_cx_tower_yaml_import_wiz_upload,group_import,1,1,1,1 +access_yaml_import_wizard,Import YAML,model_cx_tower_yaml_import_wiz,group_import,1,1,1,1 +access_manifest_tmpl_read_export,Manifest tmpl read (export),model_cx_tower_yaml_manifest_tmpl,cetmix_tower_yaml.group_export,1,0,0,0 +access_manifest_tmpl_admin,Manifest tmpl admin,model_cx_tower_yaml_manifest_tmpl,cetmix_tower_server.group_root,1,1,1,1 +access_manifest_author_read_export,Manifest author read (export),model_cx_tower_yaml_manifest_author,cetmix_tower_yaml.group_export,1,0,0,0 +access_manifest_author_admin,Manifest author admin,model_cx_tower_yaml_manifest_author,cetmix_tower_server.group_root,1,1,1,1 diff --git a/addons/cetmix_tower_yaml/static/description/icon.png b/addons/cetmix_tower_yaml/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2507f553896c442455b02ed5fa06b72ab398a990 GIT binary patch literal 22128 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 F{{#JqH_ZS5 literal 0 HcmV?d00001 diff --git a/addons/cetmix_tower_yaml/static/description/index.html b/addons/cetmix_tower_yaml/static/description/index.html new file mode 100644 index 0000000..44a84c3 --- /dev/null +++ b/addons/cetmix_tower_yaml/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +Cetmix Tower YAML + + + +
+

Cetmix Tower YAML

+ + +

Beta License: AGPL-3 cetmix/cetmix-tower

+

This module implements YAML format data import/export for Cetmix +Tower.

+

Please refer to the official +documentation for detailed information.

+

Table of contents

+ +
+

Configuration

+

Please refer to the official +documentation for detailed configuration +instructions.

+
+
+

Usage

+

Please refer to the official +documentation for detailed usage +instructions.

+
+
+

Changelog

+
+

18.0.2.0.0 (2026-04-07)

+
    +
  • Features: Jets! (4700)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Cetmix
  • +
+
+
+

Maintainers

+

This module is part of the cetmix/cetmix-tower project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/addons/cetmix_tower_yaml/tests/__init__.py b/addons/cetmix_tower_yaml/tests/__init__.py new file mode 100644 index 0000000..b5e1e10 --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/__init__.py @@ -0,0 +1,8 @@ +from . import test_command +from . import test_tower_yaml_mixin +from . import test_file_template +from . import test_plan +from . import test_yaml_export_wizard +from . import test_yaml_import_wizard +from . import test_server_log +from . import test_server_yaml diff --git a/addons/cetmix_tower_yaml/tests/test_command.py b/addons/cetmix_tower_yaml/tests/test_command.py new file mode 100644 index 0000000..c6ec7db --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_command.py @@ -0,0 +1,334 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import yaml + +from odoo.tests import TransactionCase + + +class TestTowerCommand(TransactionCase): + @classmethod + def setUpClass(cls, *args, **kwargs): + super().setUpClass(*args, **kwargs) + + cls.Command = cls.env["cx.tower.command"] + + # Expected YAML content of the test command + cls.command_test_yaml = """cetmix_tower_model: command +access_level: manager +reference: test_yaml_in_tests +name: Test YAML +action: ssh_command +allow_parallel_run: false +note: |- + Test YAML command conversion. + Ensure all fields are rendered properly. +os_ids: false +tag_ids: false +path: false +file_template_id: false +if_file_exists: skip +disconnect_file: false +flight_plan_id: false +jet_template_id: false +jet_action_id: false +waypoint_template_id: false +fly_here: false +code: |- + cd /home/{{ tower.server.ssh_username }} \\ + && ls -lha +no_split_for_sudo: false +server_status: false +variable_ids: false +secret_ids: false +""" + + # YAML content translated into Python dict + cls.command_test_yaml_dict = yaml.safe_load(cls.command_test_yaml) + + def test_yaml_from_command(self): + """Test if YAML is generated properly from a command""" + + # -- 0 -- + # Create test command + # Test command + command_test = self.Command.create( + { + "name": "Test YAML", + "reference": "test_yaml_in_tests", + "action": "ssh_command", + "code": """cd /home/{{ tower.server.ssh_username }} \\ +&& ls -lha""", + "note": """Test YAML command conversion. +Ensure all fields are rendered properly.""", + } + ) + + # -- 1 -- + # Check it YAML generated by the command matches + # YAML from the template file + self.assertEqual( + command_test.yaml_code, + self.command_test_yaml, + "YAML generated from command doesn't match template file one", + ) + + # -- 2 -- + # Check if YAML key values match Cetmix Tower ones + + self.assertEqual( + command_test.access_level, + self.Command.TO_TOWER_ACCESS_LEVEL[ + self.command_test_yaml_dict["access_level"] + ], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.action, + self.command_test_yaml_dict["action"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.allow_parallel_run, + self.command_test_yaml_dict["allow_parallel_run"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.code, + self.command_test_yaml_dict["code"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.name, + self.command_test_yaml_dict["name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.note, + self.command_test_yaml_dict["note"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.path, + self.command_test_yaml_dict["path"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.reference, + self.command_test_yaml_dict["reference"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.if_file_exists, + self.command_test_yaml_dict["if_file_exists"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command_test.disconnect_file, + self.command_test_yaml_dict["disconnect_file"], + "YAML value doesn't match Cetmix Tower one", + ) + + def test_command_from_yaml(self): + """Test if YAML is generated properly from a command""" + + def test_yaml(command): + """Checks if yaml values are inserted correctly + + Args: + command(cx.tower.command): _description_ + """ + self.assertEqual( + command.access_level, + self.Command.TO_TOWER_ACCESS_LEVEL[ + self.command_test_yaml_dict["access_level"] + ], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.action, + self.command_test_yaml_dict["action"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.allow_parallel_run, + self.command_test_yaml_dict["allow_parallel_run"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.code, + self.command_test_yaml_dict["code"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.name, + self.command_test_yaml_dict["name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.note, + self.command_test_yaml_dict["note"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.path, + self.command_test_yaml_dict["path"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.reference, + self.command_test_yaml_dict["reference"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.if_file_exists, + self.command_test_yaml_dict["if_file_exists"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + command.disconnect_file, + self.command_test_yaml_dict["disconnect_file"], + "YAML value doesn't match Cetmix Tower one", + ) + + # Create test command + command_test = self.Command.create( + {"name": "New Command", "action": "python_code"} + ) + + # -- 1 -- + # Insert YAML into the command and + # check if YAML key values match Cetmix Tower ones + command_test.yaml_code = self.command_test_yaml + test_yaml(command_test) + + # -- 2 -- + # Insert some non supported keys and ensure nothing bad happens + yaml_with_non_supported_keys = """access_level: manager +action: ssh_command +doge: wow +memes: much nice! +allow_parallel_run: false +cetmix_tower_model: command +code: |- + cd /home/{{ tower.server.ssh_username }} \\ + && ls -lha +file_template_id: false +flight_plan_id: false +name: Test YAML +note: |- + Test YAML command conversion. + Ensure all fields are rendered properly. +path: false +reference: test_yaml_in_tests +tag_ids: false +""" + command_test.yaml_code = yaml_with_non_supported_keys + test_yaml(command_test) + + # -- 3 -- + # Insert non existing selection field value and exception is raised + # TODO: Odoo 18.0 doesn't raise an exception + # when a selection field value is not valid. + # Add a method to handle this case. + + def test_command_with_action_file_template(self): + """Test command with 'File from template' action""" + yaml_with_reference = """cetmix_tower_model: command +access_level: manager +reference: such_much_test_command +name: Such Much Command +action: file_using_template +allow_parallel_run: false +note: Just a note +os_ids: false +tag_ids: false +path: false +file_template_id: my_custom_test_template +if_file_exists: skip +disconnect_file: false +flight_plan_id: false +jet_template_id: false +jet_action_id: false +waypoint_template_id: false +fly_here: false +code: false +no_split_for_sudo: false +server_status: false +variable_ids: false +secret_ids: false +""" + # Add file template + file_template = self.env["cx.tower.file.template"].create( + { + "name": "Such much demo", + "reference": "my_custom_test_template", + "file_name": "much_logs.txt", + "server_dir": "/var/log/my/files", + "source": "tower", + "file_type": "text", + "note": "Hey!", + "keep_when_deleted": False, + } + ) + command_with_template = self.Command.create( + { + "name": "Such Much Command", + "reference": "such_much_test_command", + "action": "file_using_template", + "note": "Just a note", + "file_template_id": file_template.id, + } + ) + + # -- 1 -- + # Check if final YAML composed correctly + self.assertEqual( + command_with_template.yaml_code, + yaml_with_reference, + "YAML is not composed correctly", + ) + + # -- 2 -- + # Explode related record and check the YAML + yaml_with_reference_exploded = """cetmix_tower_model: command +access_level: manager +reference: such_much_test_command +name: Such Much Command +action: file_using_template +allow_parallel_run: false +note: Just a note +os_ids: false +tag_ids: false +path: false +file_template_id: + reference: my_custom_test_template + name: Such much demo + source: tower + file_type: text + server_dir: /var/log/my/files + file_name: much_logs.txt + keep_when_deleted: false + tag_ids: false + note: Hey! + code: false + variable_ids: false + secret_ids: false +if_file_exists: skip +disconnect_file: false +flight_plan_id: false +jet_template_id: false +jet_action_id: false +waypoint_template_id: false +fly_here: false +code: false +no_split_for_sudo: false +server_status: false +variable_ids: false +secret_ids: false +""" + command_with_template.invalidate_recordset(["yaml_code"]) + self.assertEqual( + command_with_template.with_context(explode_related_record=True).yaml_code, + yaml_with_reference_exploded, + "YAML is not composed correctly", + ) diff --git a/addons/cetmix_tower_yaml/tests/test_file_template.py b/addons/cetmix_tower_yaml/tests/test_file_template.py new file mode 100644 index 0000000..097cb76 --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_file_template.py @@ -0,0 +1,320 @@ +import yaml + +from odoo.tests import TransactionCase + + +class TestTowerFileTemplate(TransactionCase): + @classmethod + def setUpClass(cls, *args, **kwargs): + super().setUpClass(*args, **kwargs) + + cls.FileTemplate = cls.env["cx.tower.file.template"] + + # Expected YAML content of the test file template + cls.file_template_test_yaml = """cetmix_tower_model: file_template +reference: dockerfile_unit_test +name: Dockerfile Test +source: tower +file_type: text +server_dir: /opt +file_name: Dockerfile +keep_when_deleted: true +tag_ids: false +note: |- + Used to build Odoo addons image. + Depends on Odoo core image. +code: |- + FROM odoo:{{ odoo_test_version }} + # Install git-aggregator and tools for requirements generation + RUN pip3 install --upgrade pip && pip install manifestoo setuptools-odoo git-aggregator + # Let's go! + USER odoo +variable_ids: false +secret_ids: false +""" # noqa + + # Expected YAML content of the test file template + # without empty x2mvalues + cls.file_template_test_yaml_no_empty_values = """cetmix_tower_model: file_template +reference: dockerfile_unit_test +name: Dockerfile Test +source: tower +file_type: text +server_dir: /opt +file_name: Dockerfile +keep_when_deleted: true +note: |- + Used to build Odoo addons image. + Depends on Odoo core image. +code: |- + FROM odoo:{{ odoo_test_version }} + # Install git-aggregator and tools for requirements generation + RUN pip3 install --upgrade pip && pip install manifestoo setuptools-odoo git-aggregator + # Let's go! + USER odoo +""" # noqa + + # YAML content translated into Python dict + cls.file_template_test_yaml_dict = yaml.safe_load(cls.file_template_test_yaml) + cls.file_template_test_yaml_dict_no_empty_values = yaml.safe_load( + cls.file_template_test_yaml_no_empty_values + ) + + def test_yaml_from_file_template(self): + """Test if YAML is generated properly from a file""" + + # -- 0 -- + # Create test file + # Test file + file_template_test = self.FileTemplate.create( + { + "name": "Dockerfile Test", + "reference": "dockerfile_unit_test", + "file_name": "Dockerfile", + "server_dir": "/opt", + "source": "tower", + "keep_when_deleted": True, + "file_type": "text", + "code": """FROM odoo:{{ odoo_test_version }} +# Install git-aggregator and tools for requirements generation +RUN pip3 install --upgrade pip && pip install manifestoo setuptools-odoo git-aggregator +# Let's go! +USER odoo""", + "note": """Used to build Odoo addons image. +Depends on Odoo core image.""", + } + ) + + # -- 1 -- + # Check it YAML generated by the file matches + # YAML from the template file + + self.assertEqual( + file_template_test.yaml_code, + self.file_template_test_yaml, + "YAML generated from file doesn't match template file one", + ) + + # -- 2 -- + # Check if YAML key values match Cetmix Tower ones + + self.assertEqual( + file_template_test.source, + self.file_template_test_yaml_dict["source"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.file_name, + self.file_template_test_yaml_dict["file_name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.code, + self.file_template_test_yaml_dict["code"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.name, + self.file_template_test_yaml_dict["name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.note, + self.file_template_test_yaml_dict["note"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.server_dir, + self.file_template_test_yaml_dict["server_dir"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.reference, + self.file_template_test_yaml_dict["reference"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.file_type, + self.file_template_test_yaml_dict["file_type"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.keep_when_deleted, + self.file_template_test_yaml_dict["keep_when_deleted"], + "YAML value doesn't match Cetmix Tower one", + ) + + def test_yaml_from_file_template_no_empty_values(self): + """Test if YAML is generated properly from a file""" + + # -- 0 -- + # Create test file + # Test file + file_template_test = self.FileTemplate.with_context( + remove_empty_values=True + ).create( + { + "name": "Dockerfile Test", + "reference": "dockerfile_unit_test", + "file_name": "Dockerfile", + "server_dir": "/opt", + "source": "tower", + "keep_when_deleted": True, + "file_type": "text", + "code": """FROM odoo:{{ odoo_test_version }} +# Install git-aggregator and tools for requirements generation +RUN pip3 install --upgrade pip && pip install manifestoo setuptools-odoo git-aggregator +# Let's go! +USER odoo""", + "note": """Used to build Odoo addons image. +Depends on Odoo core image.""", + } + ) + + # -- 1 -- + # Check it YAML generated by the file matches + # YAML from the template file + + self.assertEqual( + file_template_test.yaml_code, + self.file_template_test_yaml_no_empty_values, + "YAML generated from file doesn't match template file one", + ) + + # -- 2 -- + # Check if YAML key values match Cetmix Tower ones + + self.assertEqual( + file_template_test.source, + self.file_template_test_yaml_dict_no_empty_values["source"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.file_name, + self.file_template_test_yaml_dict_no_empty_values["file_name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.code, + self.file_template_test_yaml_dict_no_empty_values["code"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.name, + self.file_template_test_yaml_dict_no_empty_values["name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.note, + self.file_template_test_yaml_dict_no_empty_values["note"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.server_dir, + self.file_template_test_yaml_dict_no_empty_values["server_dir"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.reference, + self.file_template_test_yaml_dict_no_empty_values["reference"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.file_type, + self.file_template_test_yaml_dict_no_empty_values["file_type"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template_test.keep_when_deleted, + self.file_template_test_yaml_dict_no_empty_values["keep_when_deleted"], + "YAML value doesn't match Cetmix Tower one", + ) + + def test_file_template_from_yaml(self): + """Test if YAML is generated properly from a file""" + + def test_yaml(file_template): + """Checks if yaml values are inserted correctly + + Args: + file_template (cx.tower.file.template): File template + """ + self.assertEqual( + file_template.source, + self.file_template_test_yaml_dict["source"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.file_name, + self.file_template_test_yaml_dict["file_name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.code, + self.file_template_test_yaml_dict["code"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.name, + self.file_template_test_yaml_dict["name"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.note, + self.file_template_test_yaml_dict["note"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.server_dir, + self.file_template_test_yaml_dict["server_dir"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.reference, + self.file_template_test_yaml_dict["reference"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.file_type, + self.file_template_test_yaml_dict["file_type"], + "YAML value doesn't match Cetmix Tower one", + ) + self.assertEqual( + file_template.keep_when_deleted, + self.file_template_test_yaml_dict["keep_when_deleted"], + "YAML value doesn't match Cetmix Tower one", + ) + + # Create test file template + file_template_test = self.FileTemplate.create({"name": "New file template"}) + + # -- 1 -- + # Insert YAML into the file and + # check if YAML key values match Cetmix Tower ones + file_template_test.yaml_code = self.file_template_test_yaml + test_yaml(file_template_test) + + # -- 2 -- + # Insert some non supported keys and ensure nothing bad happens + yaml_with_non_supported_keys = """cetmix_tower_model: file_template +code: |- + FROM odoo:{{ odoo_test_version }} + # Install git-aggregator and tools for requirements generation + RUN pip3 install --upgrade pip && pip install manifestoo setuptools-odoo git-aggregator + # Let's go! + USER odoo +doge: SoMuch style! +file_name: Dockerfile +file_type: text +keep_when_deleted: true +name: Dockerfile Test +note: |- + Used to build Odoo addons image. + Depends on Odoo core image. +reference: dockerfile_unit_test +server_dir: /opt +source: tower +tag_ids: false +""" # noqa + file_template_test.yaml_code = yaml_with_non_supported_keys + test_yaml(file_template_test) diff --git a/addons/cetmix_tower_yaml/tests/test_plan.py b/addons/cetmix_tower_yaml/tests/test_plan.py new file mode 100644 index 0000000..4d9ec43 --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_plan.py @@ -0,0 +1,179 @@ +from odoo.tests import TransactionCase + + +class TestTowerPlan(TransactionCase): + @classmethod + def setUpClass(cls, *args, **kwargs): + super().setUpClass(*args, **kwargs) + + cls.Plan = cls.env["cx.tower.plan"] + + def test_plan_create_from_yaml(self): + """Test plan creation from YAML.""" + + plan_yaml = """cetmix_tower_model: plan +access_level: manager +reference: test_plan_from_yaml +name: 'Test Plan From Yaml' +allow_parallel_run: false +color: 0 +tag_ids: +- reference: doge_test_plan_tag + name: Doge Test Plan Tag + color: 1 +on_error_action: e +custom_exit_code: 0 +line_ids: +- sequence: 5 + condition: false + use_sudo: false + path: /such/much/{{ test_plan_dir }} + command_id: + access_level: manager + reference: very_much_command_test + name: Very much command + action: ssh_command + allow_parallel_run: false + note: false + code: Such much code + variable_ids: + - cetmix_tower_model: variable + reference: test_plan_dir + name: Test Plan Directory + action_ids: + - sequence: 1 + condition: == + value_char: '0' + action: n + custom_exit_code: 0 + variable_value_ids: + - cetmix_tower_model: variable_value + variable_id: + cetmix_tower_yaml_version: 1 + cetmix_tower_model: variable + reference: test_plan_branch + name: Test Plan Branch + value_char: production + - cetmix_tower_model: variable_value + variable_id: + cetmix_tower_yaml_version: 1 + cetmix_tower_model: variable + reference: test_plan_some_unique_variable + name: Test Plan Some Unique Variable + value_char: 'Final Value' + - cetmix_tower_model: plan_line_action + access_level: manager + sequence: 2 + condition: '>' + value_char: '0' + action: ec + custom_exit_code: 255 + variable_value_ids: false + variable_ids: false +""" + # -- 1 -- + # Create plan from YAML + plan_form_yaml = self.Plan.create( + {"name": "Name Placeholder", "yaml_code": plan_yaml} + ) + self.assertEqual( + plan_form_yaml.reference, + "test_plan_from_yaml", + "Reference is not set from YAML", + ) + # Name should be set from YAML + self.assertEqual( + plan_form_yaml.name, "Test Plan From Yaml", "Name is not set from YAML" + ) + + # -- 2 -- + # Check plan tags + plan_tags = plan_form_yaml.tag_ids + self.assertEqual(len(plan_tags), 1) + self.assertEqual(plan_tags.name, "Doge Test Plan Tag") + + # -- 3 -- + # Check plan lines + plan_lines = plan_form_yaml.line_ids + self.assertEqual(len(plan_lines), 1, "Line count is not 1") + self.assertFalse(plan_lines.condition, "Condition is not false") + self.assertEqual( + plan_lines.path, + "/such/much/{{ test_plan_dir }}", + "Path is not set from YAML", + ) + self.assertEqual( + plan_lines.command_id.reference, + "very_much_command_test", + "Command reference is not set from YAML", + ) + self.assertEqual( + plan_lines.command_id.name, + "Very much command", + "Command name is not set from YAML", + ) + self.assertEqual( + plan_lines.command_id.action, + "ssh_command", + "Command action is not set from YAML", + ) + self.assertFalse( + plan_lines.command_id.allow_parallel_run, + "Command allow parallel run is not set from YAML", + ) + self.assertFalse( + plan_lines.command_id.note, "Command note is not set from YAML" + ) + self.assertEqual( + plan_lines.command_id.variable_ids.mapped("reference"), + ["test_plan_dir"], + "Command variable ids is not set from YAML", + ) + self.assertEqual( + plan_lines.command_id.access_level, + "2", + "Command access level is not set from YAML", + ) + + # -- 4 -- + # Check plan line actions + plan_actions = plan_form_yaml.line_ids.action_ids + self.assertEqual(len(plan_actions), 2, "Action count is not 2") + self.assertEqual( + plan_actions[0].condition, "==", "First action condition is not equal" + ) + self.assertEqual( + plan_actions[0].value_char, "0", "First action value char is not 0" + ) + self.assertEqual(plan_actions[0].action, "n", "First action action is not n") + self.assertEqual( + plan_actions[0].custom_exit_code, + 0, + "First action custom exit code is not 0", + ) + self.assertEqual( + len(plan_actions[0].variable_value_ids), + 2, + "Number of variable value ids is not correct", + ) + self.assertEqual( + plan_actions[0].variable_value_ids.mapped("value_char"), + ["production", "Final Value"], + "Variable value chars are not correct", + ) + self.assertEqual( + plan_actions[1].condition, ">", "Second action condition is not greater" + ) + self.assertEqual( + plan_actions[1].value_char, "0", "Second action value char is not 0" + ) + self.assertEqual(plan_actions[1].action, "ec", "Second action action is not ec") + self.assertEqual( + plan_actions[1].custom_exit_code, + 255, + "Second action custom exit code is not 255", + ) + self.assertFalse( + plan_actions[1].variable_value_ids, + "Second action variable value ids is not false", + ) diff --git a/addons/cetmix_tower_yaml/tests/test_server_log.py b/addons/cetmix_tower_yaml/tests/test_server_log.py new file mode 100644 index 0000000..4849332 --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_server_log.py @@ -0,0 +1,127 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +""" +Tests for the cx.tower.server.log model YAML export/import. + +Covers: +1. YAML export of a file-type log must include `file_id` and allow suffixes. +2. A full round-trip (export → delete → import) preserves the `file_id` relation. +3. Exporting a non-file log must include a falsy `file_id`. +4. Importing YAML with a bogus `file_id` reference raises ValidationError. +""" + +import yaml + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestServerLog(TransactionCase): + """YAML export/import tests for cx.tower.server.log.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + env = cls.env + cls.File = env["cx.tower.file"] + cls.Server = env["cx.tower.server"] + cls.ServerLog = env["cx.tower.server.log"] + + # Create a file to reference from the log + cls.file = cls.File.create( + { + "name": "repos.yaml", + "reference": "reposyaml", + "source": "tower", + "file_type": "text", + "server_dir": "/tmp", + "code": "# Example\nHello, Tower!", + } + ) + + # Create a server (use password auth to satisfy constraints) + cls.server = cls.Server.create( + { + "name": "Srv-YAML-Test", + "reference": "srv_yaml_test", + "ip_v4_address": "127.0.0.1", + "ssh_username": "admin", + "ssh_port": 22, + "ssh_auth_mode": "p", + "ssh_password": "dummy", + "use_sudo": False, + } + ) + + # Create a file-type log linked to the file above + cls.log = cls.ServerLog.create( + { + "name": "Log from file", + "reference": "log_from_file", + "log_type": "file", + "file_id": cls.file.id, + "server_id": cls.server.id, + "use_sudo": False, + } + ) + + def test_yaml_export_contains_file_id(self): + """Exported YAML must include a file_id starting with the file's reference.""" + data = yaml.safe_load(self.log.yaml_code) + # Ensure file_id is present + self.assertIn("file_id", data, "`file_id` is missing from YAML export") + # Allow for auto-appended suffixes, so only check prefix + self.assertTrue( + data["file_id"].startswith(self.file.reference), + f"`file_id` value '{data['file_id']}' should start with " + f"'{self.file.reference}'", + ) + + def test_yaml_roundtrip_restores_file_id(self): + """A full export→delete→import cycle must restore the file_id relation.""" + yaml_dict = yaml.safe_load(self.log.yaml_code) + # Remove the original log + self.log.unlink() + # Recreate from YAML + vals = self.ServerLog._post_process_yaml_dict_values(yaml_dict) + restored = self.ServerLog.with_context(from_yaml=True).create(vals) + # Verify relation restored + self.assertEqual( + restored.file_id.id, + self.file.id, + "`file_id` was not restored after round-trip", + ) + + def test_yaml_export_without_file_id(self): + """Logs of non-file type should not include file_id in YAML.""" + cmd_log = self.ServerLog.create( + { + "name": "Log no file", + "reference": "log_no_file", + "log_type": "command", + "server_id": self.server.id, + "use_sudo": False, + } + ) + data = yaml.safe_load(cmd_log.yaml_code) + # key is present, but must be falsy + self.assertIn("file_id", data, "`file_id` key is missing") + self.assertFalse( + data["file_id"], + "`file_id` for non-file log must be False/empty", + ) + + def test_yaml_import_with_missing_file_reference(self): + """Missing file reference is accepted, but file_id stays empty.""" + yaml_dict = yaml.safe_load(self.log.yaml_code) + yaml_dict["file_id"] = "does_not_exist" + + vals = self.ServerLog._post_process_yaml_dict_values(yaml_dict) + new_log = self.ServerLog.with_context(from_yaml=True).create(vals) + + # Log is created, but the relation is not resolved + self.assertFalse( + new_log.file_id, + "file_id should be empty when reference cannot be resolved", + ) diff --git a/addons/cetmix_tower_yaml/tests/test_server_yaml.py b/addons/cetmix_tower_yaml/tests/test_server_yaml.py new file mode 100644 index 0000000..8fa003a --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_server_yaml.py @@ -0,0 +1,125 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +""" +Tests for cx.tower.server YAML export/import covering command_ids and plan_ids. +""" + +import yaml + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestServerYAML(TransactionCase): + """YAML export/import tests for cx.tower.server with commands and plans.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + env = cls.env + cls.Server = env["cx.tower.server"] + cls.Command = env["cx.tower.command"] + cls.Plan = env["cx.tower.plan"] + + # Create a command to attach (use defaults for access_level) + cls.command = cls.Command.create( + { + "name": "Test Command", + "reference": "test_command", + "action": "ssh_command", + "allow_parallel_run": False, + } + ) + + # Create a flight plan to attach + cls.plan = cls.Plan.create( + { + "name": "Test Flight Plan", + "reference": "test_plan", + "allow_parallel_run": False, + "color": 0, + } + ) + + # Create server and link command and plan + cls.server = cls.Server.create( + { + "name": "Server YAML Test", + "reference": "srv_yaml_test", + "ip_v4_address": "127.0.0.1", + "ssh_username": "admin", + "ssh_port": 22, + "ssh_auth_mode": "p", + "ssh_password": "dummy", + "use_sudo": False, + # Link the m2m fields + "command_ids": [(6, 0, [cls.command.id])], + "plan_ids": [(6, 0, [cls.plan.id])], + } + ) + + def test_yaml_export_contains_command_and_plan(self): + """Exported YAML include command_ids and plan_ids with correct references.""" + data = yaml.safe_load(self.server.yaml_code) + # Check command_ids + self.assertIn( + "command_ids", + data, + "`command_ids` is missing from YAML export", + ) + self.assertIsInstance( + data["command_ids"], list, "`command_ids` should be a list in YAML" + ) + self.assertTrue( + data["command_ids"], + "`command_ids` list should not be empty", + ) + # Only reference should be exported + self.assertEqual( + data["command_ids"][0], + self.command.reference, + "Exported command reference does not match", + ) + + # Check plan_ids + self.assertIn( + "plan_ids", + data, + "`plan_ids` is missing from YAML export", + ) + self.assertIsInstance( + data["plan_ids"], list, "`plan_ids` should be a list in YAML" + ) + self.assertTrue( + data["plan_ids"], + "`plan_ids` list should not be empty", + ) + self.assertEqual( + data["plan_ids"][0], + self.plan.reference, + "Exported plan reference does not match", + ) + + def test_yaml_roundtrip_restores_command_and_plan(self): + """A full export→delete→import cycle must restore the m2m relations.""" + yaml_dict = yaml.safe_load(self.server.yaml_code) + # Remove original server + self.server.unlink() + # Prepare values and import + vals = self.Server._post_process_yaml_dict_values(yaml_dict) + restored = self.Server.with_context( + from_yaml=True, skip_ssh_settings_check=True + ).create(vals) + + # Verify m2m links restored + self.assertEqual( + restored.command_ids.ids, + [self.command.id], + "`command_ids` were not restored correctly", + ) + self.assertEqual( + restored.plan_ids.ids, + [self.plan.id], + "`plan_ids` were not restored correctly", + ) diff --git a/addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py b/addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py new file mode 100644 index 0000000..df2e700 --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_tower_yaml_mixin.py @@ -0,0 +1,768 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from unittest.mock import patch + +from odoo import _ +from odoo.exceptions import AccessError, ValidationError +from odoo.tests import TransactionCase, tagged + + +class TestTowerYamlMixin(TransactionCase): + @classmethod + def setUpClass(cls, *args, **kwargs): + super().setUpClass(*args, **kwargs) + cls.Users = cls.env["res.users"].with_context(no_reset_password=True) + cls.YamlMixin = cls.env["cx.tower.yaml.mixin"] + cls.Command = cls.env["cx.tower.command"] + cls.JetTemplate = cls.env["cx.tower.jet.template"] + cls.ScheduledTask = cls.env["cx.tower.scheduled.task"] + TowerTag = cls.env["cx.tower.tag"] + cls.tag_doge = TowerTag.create({"name": "Doge", "reference": "doge"}) + cls.tag_pepe = TowerTag.create({"name": "Pepe", "reference": "pepe"}) + cls.jet_state_running = cls.env["cx.tower.jet.state"].get_by_reference( + "running" + ) + cls.command_for_schedule = cls.Command.create( + {"name": "Command for schedule", "action": "ssh_command"} + ) + cls.jet_template_existing = cls.env["cx.tower.jet.template"].create( + {"name": "Existing Jet Template", "reference": "existing_jet_template"} + ) + cls.waypoint_template_existing = cls.env[ + "cx.tower.jet.waypoint.template" + ].create( + { + "name": "Existing Waypoint Template", + "reference": "existing_waypoint_template", + "jet_template_id": cls.jet_template_existing.id, + } + ) + + def test_convert_dict_to_yaml(self): + # -- 1 -- + # Test regular flow + self.assertEqual( + self.YamlMixin._convert_dict_to_yaml({"a": 1, "b": 2}), + "a: 1\nb: 2\n", + "Dictionary was not converted to YAML correctly", + ) + + # -- 2 -- + # Test flow with exception due to wrong values + with self.assertRaises(ValidationError) as e: + self.YamlMixin._convert_dict_to_yaml("not_a_dict") + self.assertEqual( + str(e.exception), + _("Values must be a dictionary"), + "Exception message doesn't match", + ) + + def test_yaml_field_access(self): + # Create Root user with no access to the 'yaml_code field + user_root = self.Users.create( + { + "name": "Root User", + "login": "root@example.com", + "groups_id": [ + (4, self.env.ref("base.group_user").id), + (4, self.env.ref("cetmix_tower_server.group_root").id), + ], + } + ) + with self.assertRaises(AccessError): + self.tag_doge.with_user(user_root).read(["yaml_code"]) + + # Add user to the 'cetmix_tower_yaml.group_export' group + # and check if access is granted + user_root.write( + {"groups_id": [(4, self.env.ref("cetmix_tower_yaml.group_export").id)]} + ) + yaml_code = ( + self.tag_doge.with_user(user_root).read(["yaml_code"])[0].get("yaml_code") + ) + + # Modify YAML code and check if it's saved + yaml_code = yaml_code.replace("Doge", "WowDoge") + with self.assertRaises(AccessError): + self.tag_doge.with_user(user_root).write({"yaml_code": yaml_code}) + + # Add user to the 'cetmix_tower_yaml.group_import' group + # and check if access is granted + user_root.write( + {"groups_id": [(4, self.env.ref("cetmix_tower_yaml.group_import").id)]} + ) + self.tag_doge.with_user(user_root).write({"yaml_code": yaml_code}) + self.assertEqual( + self.tag_doge.with_user(user_root).yaml_code, + yaml_code, + "YAML code was not saved", + ) + + def test_post_process_record_values(self): + """Test value post processing. + We test common fields only because this method can be overridden + in models inheriting this mixin. + """ + + # Patch method to return "access_level" field too + def _get_fields_for_yaml(self): + return ["access_level", "name", "reference"] + + with patch( + "odoo.addons.cetmix_tower_yaml.models.cx_tower_yaml_mixin.CxTowerYamlMixin._get_fields_for_yaml", + _get_fields_for_yaml, + ): + source_values = { + "access_level": "3", + "id": 22332, + "name": "Doge Much Like", + "reference": "such_much_doge", + } + + result_values = self.YamlMixin._post_process_record_values( + source_values.copy() + ) + + self.assertNotIn("id", result_values, "ID must be removed") + self.assertEqual( + result_values["access_level"], + self.YamlMixin.TO_YAML_ACCESS_LEVEL[source_values["access_level"]], + "Access level is not parsed correctly", + ) + self.assertEqual( + result_values["name"], + source_values["name"], + "Other values should remain unchanged", + ) + self.assertEqual( + result_values["reference"], + source_values["reference"], + "Other values should remain unchanged", + ) + + def test_post_process_yaml_dict_values(self): + """Test YAML dict value post processing. + We test common fields only because this method can be overridden + in models inheriting this mixin. + """ + + # Patch method to return "access_level" field too + def _get_fields_for_yaml(self): + return ["access_level", "name", "reference"] + + with patch( + "odoo.addons.cetmix_tower_yaml.models.cx_tower_yaml_mixin.CxTowerYamlMixin._get_fields_for_yaml", + _get_fields_for_yaml, + ): + # -- 1 -- + # Test regular flow + source_values = { + "access_level": "user", + "name": "Doge Much Like", + "reference": "such_much_doge", + "some_doge_field": "some_meme", + } + + result_values = self.YamlMixin._post_process_yaml_dict_values( + source_values.copy() + ) + self.assertNotIn( + "some_doge_field", result_values, "Non listed fields must be removed" + ) + self.assertEqual( + result_values["access_level"], + self.YamlMixin.TO_TOWER_ACCESS_LEVEL[source_values["access_level"]], + "Access level is not parsed correctly", + ) + self.assertEqual( + result_values["name"], + source_values["name"], + "Other values should remain unchanged", + ) + self.assertEqual( + result_values["reference"], + source_values["reference"], + "Other values should remain unchanged", + ) + + # -- Test 2 -- + # Submit wrong value for access level + source_values.update( + { + "access_level": "doge", + } + ) + with self.assertRaises(ValidationError) as e: + result_values = self.YamlMixin._post_process_yaml_dict_values( + source_values.copy() + ) + self.assertEqual( + str(e.exception), + _( + "Wrong value for 'access_level' key: %(acv)s", + acv="doge", + ), + "Exception message doesn't match", + ) + + def test_post_process_yaml_dict_values_defers_command_template_links(self): + """Reference-only unresolved command template links must be deferred.""" + deferred_queue = [] + values = { + "reference": "command_deferred_links", + "name": "Command Deferred Links", + "action": "jet_action", + "jet_template_id": "future_jet_template", + "waypoint_template_id": {"reference": "future_waypoint_template"}, + } + + result_values = self.Command.with_context( + yaml_deferred_m2o_queue=deferred_queue + )._post_process_yaml_dict_values(values) + + self.assertNotIn( + "jet_template_id", + result_values, + "Deferred jet template link must be omitted from first-pass values", + ) + self.assertNotIn( + "waypoint_template_id", + result_values, + "Deferred waypoint template link must be omitted from first-pass values", + ) + self.assertEqual(len(deferred_queue), 2, "Two deferred items must be queued") + self.assertEqual( + deferred_queue[0]["record_reference"], + values["reference"], + "Deferred queue must preserve command reference", + ) + self.assertEqual( + deferred_queue[0]["field_name"], + "jet_template_id", + "Deferred queue must preserve the deferred field name", + ) + self.assertEqual( + deferred_queue[1]["field_name"], + "waypoint_template_id", + "Deferred queue must preserve each deferred field separately", + ) + + def test_post_process_yaml_dict_values_resolves_existing_command_template_links( + self, + ): + """Already existing command template links must be resolved immediately.""" + deferred_queue = [] + values = { + "reference": "command_immediate_links", + "name": "Command Immediate Links", + "action": "create_waypoint", + "jet_template_id": self.jet_template_existing.reference, + "waypoint_template_id": { + "reference": self.waypoint_template_existing.reference + }, + } + + result_values = self.Command.with_context( + yaml_deferred_m2o_queue=deferred_queue + )._post_process_yaml_dict_values(values) + + self.assertEqual( + result_values["jet_template_id"], + self.jet_template_existing.id, + "Existing jet template must resolve during the first import pass", + ) + self.assertEqual( + result_values["waypoint_template_id"], + self.waypoint_template_existing.id, + "Existing waypoint template must resolve during the first import pass", + ) + self.assertFalse( + deferred_queue, + "No deferred items must be queued when targets already exist", + ) + + def test_post_process_yaml_dict_values_defers_template_dependency_children(self): + """Unresolved template dependency children must be deferred.""" + deferred_queue = [] + values = { + "reference": "owner_template_deferred_dependency", + "name": "Owner Template Deferred Dependency", + "template_requires_ids": [ + { + "reference": False, + "template_required_id": { + "reference": "future_template_dependency_target" + }, + "state_required_id": { + "reference": self.jet_state_running.reference + }, + } + ], + } + + result_values = self.JetTemplate.with_context( + yaml_deferred_x2m_queue=deferred_queue + )._post_process_yaml_dict_values(values) + + self.assertEqual( + result_values.get("template_requires_ids"), + [], + "Deferred dependency child must be removed from first-pass create values", + ) + self.assertEqual( + len(deferred_queue), + 1, + "One dependency child must be queued for deferred creation", + ) + self.assertEqual( + deferred_queue[0]["field_name"], + "template_requires_ids", + "Deferred queue must preserve the parent x2m field name", + ) + self.assertEqual( + deferred_queue[0]["target_reference"], + "future_template_dependency_target", + "Deferred queue must preserve the missing dependency target reference", + ) + + def test_post_process_yaml_dict_values_skips_empty_scheduled_task_custom_values( + self, + ): + """Placeholder scheduled-task custom values must be skipped.""" + deferred_queue = [] + scheduled_task_values = { + "reference": "scheduled_task_skip_empty_child", + "name": "Scheduled Task Skip Empty Child", + "action": "command", + "command_id": self.command_for_schedule.reference, + "interval_number": 1, + "interval_type": "days", + "next_call": "2026-03-27 00:00:00", + "custom_variable_value_ids": [{"reference": False}], + } + + result_values = self.ScheduledTask.with_context( + yaml_deferred_x2m_queue=deferred_queue + )._post_process_yaml_dict_values(scheduled_task_values) + + self.assertEqual( + result_values.get("custom_variable_value_ids"), + [], + "Placeholder child rows must be removed from scheduled task import values", + ) + self.assertFalse( + deferred_queue, + "Empty placeholder rows must be skipped rather than deferred", + ) + + def test_post_process_yaml_dict_values_defers_scheduled_task_custom_values(self): + """Unresolved scheduled-task custom values must be deferred.""" + deferred_queue = [] + scheduled_task_values = { + "reference": "scheduled_task_deferred_custom_value", + "name": "Scheduled Task Deferred Custom Value", + "action": "command", + "command_id": self.command_for_schedule.reference, + "interval_number": 1, + "interval_type": "days", + "next_call": "2026-03-27 00:00:00", + "custom_variable_value_ids": [ + { + "reference": False, + "variable_value_id": {"reference": "future_variable_value_ref"}, + } + ], + } + + result_values = self.ScheduledTask.with_context( + yaml_deferred_x2m_queue=deferred_queue + )._post_process_yaml_dict_values(scheduled_task_values) + + self.assertEqual( + result_values.get("custom_variable_value_ids"), + [], + "Deferred scheduled-task child rows must be removed from first-pass values", + ) + self.assertEqual( + len(deferred_queue), + 1, + "One scheduled-task custom value row must be queued for deferred creation", + ) + self.assertEqual( + deferred_queue[0]["field_name"], + "custom_variable_value_ids", + "Deferred queue must preserve the scheduled-task child field name", + ) + self.assertEqual( + deferred_queue[0]["target_reference"], + "future_variable_value_ref", + "Deferred queue must preserve the missing variable value reference", + ) + + def test_process_relation_field_value_reference_only_dict_no_placeholder_create( + self, + ): + """Reference-only dict must not create placeholder m2o records.""" + command = self.Command.create( + { + "name": "Command reference-only dict", + "action": "file_using_template", + } + ) + missing_reference = "missing_file_template_reference_only" + + result = command._process_relation_field_value( + field="file_template_id", + value={"reference": missing_reference}, + record_mode=False, + ) + + self.assertFalse( + result, + "Reference-only dict must stay unresolved instead of creating a record", + ) + self.assertFalse( + self.env["cx.tower.file.template"].get_by_reference(missing_reference), + "Reference-only dict must not create a placeholder related record", + ) + + def test_process_relation_field_value_no_explode(self): + """Test non exploded related field values. + Non exploded values represent related record with reference only. + + Covers the following child functions: + - _process_m2o_value(..) + - _process_x2m_values(..) + """ + + # We are using command with file template for that + file_template = self.env["cx.tower.file.template"].create( + {"name": "Test m2o", "reference": "test_m2o"} + ) + command = self.env["cx.tower.command"].create( + { + "name": "Command test m2o", + "action": "file_using_template", + "file_template_id": file_template.id, + "tag_ids": [(4, self.tag_doge.id), (4, self.tag_pepe.id)], + } + ) + + # -- 1 -- + # Record -> Yaml + + # -- 1.1 -- + # Many2one + result = command._process_relation_field_value( + field="file_template_id", + value=(command.file_template_id.id, command.file_template_id.name), + record_mode=True, + ) + self.assertEqual( + result, file_template.reference, "Reference was not resolved correctly" + ) + # -- 1.2 -- + # Many2many + result = command._process_relation_field_value( + field="tag_ids", + value=[self.tag_doge.id, self.tag_pepe.id], + record_mode=True, + ) + + self.assertEqual(len(result), 2, "Must be 2 references") + self.assertIn( + self.tag_doge.reference, result, "Reference was not resolved correctly" + ) + self.assertIn( + self.tag_pepe.reference, result, "Reference was not resolved correctly" + ) + + # -- 2 -- + # Yaml -> Record + + # -- 2.1. -- + # Many2one + result = command._process_relation_field_value( + field="file_template_id", value=file_template.reference, record_mode=False + ) + self.assertEqual( + result, file_template.id, "Record ID was not resolved correctly" + ) + + # -- 2.2 -- + # Many2many + result = command._process_relation_field_value( + field="tag_ids", + value=[self.tag_doge.reference, self.tag_pepe.reference], + record_mode=False, + ) + self.assertEqual(len(result), 2, "Must be 2 records") + self.assertIn( + (4, self.tag_doge.id), result, "Record ID was not resolved correctly" + ) + self.assertIn( + (4, self.tag_pepe.id), result, "Record ID was not resolved correctly" + ) + + # -- 3 -- + # Yaml with non existing reference -> Record + result = command._process_relation_field_value( + field="file_template_id", value="such_much_not_reference", record_mode=False + ) + self.assertFalse(result, "Must be 'False'") + + # -- 4 -- + # No record -> Yaml + result = command._process_relation_field_value( + field="file_template_id", + value=self.env["cx.tower.file.template"], + record_mode=True, + ) + self.assertFalse(result, "Result must be 'False'") + + def test_process_relation_field_value_explode(self): + """Test exploded related field values. + Exploded values represent related record with a child YAML structure. + + Covers the following child functions: + - _process_m2o_value(..) + - _process_x2m_values(..) + """ + + # We are using command with file template for that + file_template = self.env["cx.tower.file.template"].create( + {"name": "Test m2o", "reference": "test_m2o"} + ) + file_template_values = file_template.with_context( + no_yaml_service_fields=True + )._prepare_record_for_yaml() + tag_doge_values = self.tag_doge.with_context( + no_yaml_service_fields=True + )._prepare_record_for_yaml() + tag_pepe_values = self.tag_pepe.with_context( + no_yaml_service_fields=True + )._prepare_record_for_yaml() + command = ( + self.env["cx.tower.command"] + .create( + { + "name": "Command test m2o", + "action": "file_using_template", + "file_template_id": file_template.id, + "tag_ids": [(4, self.tag_doge.id), (4, self.tag_pepe.id)], + } + ) + .with_context(explode_related_record=True) + ) # and this is the actual trigger + + # -- 1 -- + # Record -> Yaml + + # -- 1.1 -- + # Many2one + result = command._process_relation_field_value( + field="file_template_id", + value=(command.file_template_id.id, command.file_template_id.name), + record_mode=True, + ) + self.assertEqual( + result, file_template_values, "Reference was not resolved correctly" + ) + + # -- 1.2 -- + # Many2many + result = command._process_relation_field_value( + field="tag_ids", + value=[self.tag_doge.id, self.tag_pepe.id], + record_mode=True, + ) + self.assertEqual(len(result), 2, "Must be 2 records") + self.assertIn(tag_doge_values, result, "Record ID was not resolved correctly") + self.assertIn(tag_pepe_values, result, "Record ID was not resolved correctly") + + # -- 2 -- + # Yaml -> Record + + # -- 2.1 -- + # Many2one + result = command._process_relation_field_value( + field="file_template_id", value=file_template_values, record_mode=False + ) + self.assertEqual( + result, file_template.id, "Record ID was not resolved correctly" + ) + + # -- 2.2 -- + # Many2many + result = command._process_relation_field_value( + field="tag_ids", value=[tag_doge_values, tag_pepe_values], record_mode=False + ) + self.assertEqual(len(result), 2, "Must be 2 records") + self.assertIn( + (4, self.tag_doge.id), result, "Record ID was not resolved correctly" + ) + self.assertIn( + (4, self.tag_pepe.id), result, "Record ID was not resolved correctly" + ) + # -- 3 -- + # Yaml with non existing reference -> Record + file_template_values.update( + { + "name": "Very new name", + "reference": "such_much_not_reference", + "source": "server", + "file_type": "binary", + } + ) + result = command._process_relation_field_value( + field="file_template_id", value=file_template_values, record_mode=False + ) + + # New record must be created + record = self.env["cx.tower.file.template"].browse(result) + self.assertEqual( + record.name, file_template_values["name"], "New record value doesn't match" + ) + self.assertEqual( + record.reference, + file_template_values["reference"], + "New record value doesn't match", + ) + self.assertEqual( + record.source, + file_template_values["source"], + "New record value doesn't match", + ) + self.assertEqual( + record.file_type, + file_template_values["file_type"], + "New record value doesn't match", + ) + + # -- 4 -- + # Yaml with no reference at all -> Record + values_with_no_references = { + "name": "Sorry no reference here", + "source": "tower", + "file_type": "binary", + } + result = command._process_relation_field_value( + field="file_template_id", value=values_with_no_references, record_mode=False + ) + + # New record must be created + record = self.env["cx.tower.file.template"].browse(result) + + self.assertEqual( + record.name, + values_with_no_references["name"], + "New record value doesn't match", + ) + self.assertEqual( + record.source, + values_with_no_references["source"], + "New record value doesn't match", + ) + self.assertEqual( + record.file_type, + values_with_no_references["file_type"], + "New record value doesn't match", + ) + + # -- 5 -- + # No record -> Yaml + result = command._process_relation_field_value( + field="file_template_id", + value=self.env["cx.tower.file.template"], + record_mode=True, + ) + self.assertFalse(result, "Result must be 'False'") + + def test_update_or_create_related_record(self): + """Test if related record is updated or created correctly""" + + # -- 1 -- + # Update existing values + # We are using file template for that + FileTemplateModel = self.env["cx.tower.file.template"] + file_template = self.env["cx.tower.file.template"].create( + {"name": "Test m2o", "reference": "test_m2o"} + ) + values_to_update = {"name": "Much new name"} + record = FileTemplateModel._update_or_create_related_record( + model=FileTemplateModel, + reference=file_template.reference, + values=values_to_update, + ) + self.assertEqual( + record.name, values_to_update["name"], "Value was not updated properly" + ) + self.assertEqual(record.id, file_template.id, "Same record must be updated") + + # -- 2 -- + # Reference not found. Must create a new record + values_to_update = {"name": "Doge file"} + record = FileTemplateModel._update_or_create_related_record( + model=FileTemplateModel, + reference="doge_file", + values=values_to_update, + create_immediately=True, + ) + self.assertEqual( + record.name, values_to_update["name"], "Value was not updated properly" + ) + self.assertNotEqual(record.id, file_template.id, "New record must be created") + + # -- 2 -- + # Reference not provided. Must create a new record + values_to_update = {"name": "Doge file"} + record = FileTemplateModel._update_or_create_related_record( + model=FileTemplateModel, + reference=False, + values=values_to_update, + create_immediately=True, + ) + self.assertEqual( + record.name, values_to_update["name"], "Value was not updated properly" + ) + self.assertNotEqual(record.id, file_template.id, "New record must be created") + + @tagged("post_install", "-at_install") + def test_prepare_record_truncates_code_for_server_files(self): + """Mixin must set code=False for cx.tower.file when source=='server'.""" + File = self.env["cx.tower.file"] + srv_file = File.create( + { + "name": "srv.log", + "reference": "srvlog", + "source": "server", + "file_type": "text", + "server_dir": "/tmp", + "code": "BIG DATA", + } + ) + rec = srv_file._prepare_record_for_yaml() + self.assertIn("code", rec) + self.assertFalse(rec["code"], "Expected code=False for server-sourced files") + + @tagged("post_install", "-at_install") + def test_prepare_record_keeps_code_for_tower_files(self): + """Mixin must keep code for cx.tower.file when source=='tower'.""" + File = self.env["cx.tower.file"] + tw_file = File.create( + { + "name": "local.txt", + "reference": "localtxt", + "source": "tower", + "file_type": "text", + "server_dir": "/etc", + "code": "SMALL DATA", + } + ) + rec = tw_file._prepare_record_for_yaml() + self.assertEqual( + rec["code"], + "SMALL DATA", + "Expected original code for tower-sourced files", + ) diff --git a/addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py b/addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py new file mode 100644 index 0000000..cd27abf --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py @@ -0,0 +1,377 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 + +import yaml + +from odoo.exceptions import AccessError, ValidationError + +from odoo.addons.base.tests.common import BaseCommon + + +class TestYamlExportWizard(BaseCommon): + @classmethod + def setUpClass(cls, *args, **kwargs): + super().setUpClass(*args, **kwargs) + + # Used to ensure that the file header + # is present in the YAML code + cls.file_header = """ +# This file is generated with Cetmix Tower. +# Details and documentation: https://cetmix.com/tower +""" + # Create a command + cls.TowerCommand = cls.env["cx.tower.command"] + cls.command_test_wizard = cls.TowerCommand.create( + { + "reference": "test_command_from_yaml", + "name": "Test Command From Yaml", + "code": "echo 'Test Command From Yaml'", + } + ) + cls.command_test_wizard_2 = cls.TowerCommand.create( + { + "reference": "test_command_from_yaml_2", + "name": "Test Command From Yaml 2", + "code": "echo 'Test Command From Yaml 2'", + } + ) + + # Create a flight plan + cls.FlightPlan = cls.env["cx.tower.plan"] + cls.flight_plan_test_wizard = cls.FlightPlan.create( + { + "name": "Test Flight Plan From Yaml", + "line_ids": [ + ( + 0, + 0, + { + "command_id": cls.command_test_wizard.id, + }, + ) + ], + } + ) + + # Create a server template + cls.ServerTemplate = cls.env["cx.tower.server.template"] + cls.server_template_test_wizard = cls.ServerTemplate.create( + { + "name": "Test Server Template From Yaml", + "flight_plan_id": cls.flight_plan_test_wizard.id, + } + ) + + # Create a wizard and trigger onchange + cls.YamlExportWizard = cls.env["cx.tower.yaml.export.wiz"] + cls.test_wizard = cls.YamlExportWizard.with_context( + active_model="cx.tower.server.template", + active_ids=[cls.server_template_test_wizard.id], + ).create({}) + cls.test_wizard.onchange_explode_child_records() + + def test_user_without_export_group_cannot_export(self): + """Test if user without export group cannot export""" + + # Tower manager user without export group + self.user_yaml_export = self.env["res.users"].create( + { + "name": "No Yaml Export User", + "login": "no_yaml_export_user", + "groups_id": [ + (4, self.env.ref("cetmix_tower_server.group_manager").id) + ], + } + ) + with self.assertRaises(AccessError): + self.test_wizard.with_user(self.user_yaml_export).read([]) + + def test_yaml_export_wizard_yaml_generation(self): + """Test code generation of YAML export wizard.""" + + wizard_yaml = """ +# This file is generated with Cetmix Tower. +# Details and documentation: https://cetmix.com/tower +cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: command + access_level: manager + reference: test_command_from_yaml + name: Test Command From Yaml + action: ssh_command + allow_parallel_run: false + note: false + path: false + if_file_exists: skip + disconnect_file: false + fly_here: false + code: echo 'Test Command From Yaml' + no_split_for_sudo: false + server_status: false +- cetmix_tower_model: command + access_level: manager + reference: test_command_from_yaml_2 + name: Test Command From Yaml 2 + action: ssh_command + allow_parallel_run: false + note: false + path: false + if_file_exists: skip + disconnect_file: false + fly_here: false + code: echo 'Test Command From Yaml 2' + no_split_for_sudo: false + server_status: false +""" + + # -- 1 -- + # Test with two commands + context = { + "default_explode_child_records": True, + "default_remove_empty_values": True, + "active_model": "cx.tower.command", + "active_ids": [self.command_test_wizard.id, self.command_test_wizard_2.id], + } + wizard = self.YamlExportWizard.with_context(context).create({}) # pylint: disable=context-overridden # new need a new clean context + wizard.onchange_explode_child_records() + self.assertEqual(wizard.yaml_code, wizard_yaml) + + def test_yaml_export_wizard(self): + """Test the YAML export wizard.""" + + # -- 1 -- + # Test wizard action + result = self.test_wizard.action_generate_yaml_file() + self.assertEqual( + result["type"], "ir.actions.act_window", "Action should be a window" + ) + self.assertEqual( + result["res_model"], + "cx.tower.yaml.export.wiz.download", + "Result model should be the download wizard", + ) + self.assertTrue(result["res_id"], "Wizard should have been created") + + # -- 2 -- + # Ensure download wizard file name is generated + # based on the record reference + download_wizard = self.env["cx.tower.yaml.export.wiz.download"].browse( + result["res_id"] + ) + self.assertEqual( + download_wizard.yaml_file_name, + f"server_template_{self.server_template_test_wizard.reference}.yaml", + "YAML file name should be generated based on record reference", + ) + + # -- 3 -- + # Decode YAML file and check if it's valid + yaml_file_content = base64.decodebytes(download_wizard.yaml_file).decode( + "utf-8" + ) + self.assertEqual( + yaml_file_content, + self.test_wizard.yaml_code, + "YAML file content should be the same as the original YAML code", + ) + + # -- 4 -- + # Test if empty YAML code is handled correctly + self.test_wizard.yaml_code = "" + with self.assertRaises(ValidationError): + self.test_wizard.action_generate_yaml_file() + + def test_reference_object_uniqueness(self): + """ + Ensure each reference is exported as a full object only once + (other times only as ref). + """ + + # Prepare YAML export for flight_plan with two same commands + self.flight_plan_test_wizard.line_ids = [ + (0, 0, {"command_id": self.command_test_wizard.id}), + (0, 0, {"command_id": self.command_test_wizard.id}), + ] + + # Prepare YAML code + self.test_wizard.onchange_explode_child_records() + yaml_data = yaml.safe_load(self.test_wizard.yaml_code) + + # reference counters + ref_full = set() + ref_refs = set() + + # Recursively walk through the YAML data and count references + def walk(obj): + if isinstance(obj, dict): + ref = obj.get("reference") + # dict only with "reference" = ref, otherwise — full object + if ref: + if list(obj.keys()) == ["reference"]: + ref_refs.add(ref) + else: + ref_full.add(ref) + for v in obj.values(): + walk(v) + elif isinstance(obj, list): + for v in obj: + walk(v) + + # Walk through the YAML data + walk(yaml_data["records"]) + + # Each reference as a full object — only once + for ref in ref_full: + self.assertEqual( + list(ref_full).count(ref), + 1, + f"Reference '{ref}' appears as a full object more than once", + ) + # Check that no full objects appear more than once + self.assertEqual( + len(ref_full), + len(set(ref_full)), + "Some full objects appear more than once", + ) + + # Check that for each ref there is no only reference, but no full object + for ref in ref_refs: + self.assertIn( + ref, + ref_full, + f"Reference '{ref}' is used only as a reference, " + "but no full object present", + ) + + def test_export_required_model_name_in_yaml(self): + """ + Test that the model name is required in the YAML file for each record + """ + # create a command to run flight plan + command_run_flight_plan = self.TowerCommand.create( + { + "name": "Run Flight Plan", + "action": "plan", + "flight_plan_id": self.flight_plan_test_wizard.id, + } + ) + # export 2 commands: command_run_flight_plan and command_test_wizard + wizard = self.YamlExportWizard.with_context( + active_model="cx.tower.command", + active_ids=[command_run_flight_plan.id, self.command_test_wizard.id], + ).create({}) + + wizard.onchange_explode_child_records() + + yaml_data = yaml.safe_load(wizard.yaml_code) + + # check that the model name is present in the YAML file for each record + for record in yaml_data["records"]: + self.assertIn("cetmix_tower_model", record) + + def test_default_yaml_file_name_is_used(self): + """ + Wizard should pre-fill `yaml_file_name` with the auto-generated + value that ends with '.yaml' and contains the model prefix. + """ + wiz = self.YamlExportWizard.with_context( + active_model="cx.tower.command", + active_ids=[self.command_test_wizard.id], + ).create({}) + + default_name = wiz.yaml_file_name + + self.assertFalse( + default_name.endswith(".yaml"), + "Default file name must NO have .yaml suffix", + ) + self.assertIn( + "command_", + default_name, + "Default file name should include model prefix", + ) + + def test_yaml_file_name_is_auto_fixed(self): + """ + When the user assigns an invalid name, wizard should auto-sanitise + it to a safe *basename* (lowercase, underscores, no extension). + """ + wiz = self.YamlExportWizard.with_context( + active_model="cx.tower.command", + active_ids=[self.command_test_wizard.id], + ).create({}) + + # user enters a 'dirty' name with spaces, capitals, symbols + wiz.write({"yaml_file_name": "My File!@# .YAML"}) + + # write() override strips to a basename WITHOUT '.yaml' + self.assertEqual( + wiz.yaml_file_name, + "my_file", + "Wizard field must hold only the cleaned basename, without extension", + ) + + def test_action_generate_appends_extension(self): + """ + When generating the download record, the system must append + the `.yaml` extension to the sanitized basename. + """ + wiz = self.YamlExportWizard.with_context( + active_model="cx.tower.command", + active_ids=[self.command_test_wizard.id], + ).create({}) + wiz.onchange_explode_child_records() + act = wiz.action_generate_yaml_file() + download = self.env["cx.tower.yaml.export.wiz.download"].browse(act["res_id"]) + self.assertTrue(download.yaml_file_name.endswith(".yaml")) + + def test_custom_requires_text(self): + """Creating a template with license 'custom' but no text must fail""" + with self.assertRaises(ValidationError): + self.env["cx.tower.yaml.manifest.tmpl"].create( + { + "name": "Bad Manifest", + "license": "custom", + } + ) + + tmpl_ok = self.env["cx.tower.yaml.manifest.tmpl"].create( + { + "name": "Good Manifest", + "license": "custom", + "license_text": "Custom license terms", + } + ) + self.assertEqual(tmpl_ok.license, "custom") + self.assertEqual(tmpl_ok.license_text, "Custom license terms") + + with self.assertRaises(ValidationError): + self.env["cx.tower.yaml.manifest.tmpl"].create( + { + "name": "Bad Manifest 2", + "license": "custom", + "license_text": " ", + } + ) + + def test_wizard_resets_price_on_license_change(self): + """Wizard must reset price/currency when license changes away from 'custom'""" + wiz = self.YamlExportWizard.new( + { + "manifest_license": "custom", + "manifest_price": 42.0, + "manifest_currency": "EUR", + } + ) + wiz.manifest_license = "agpl-3" + wiz._onchange_manifest_license() + self.assertEqual(wiz.manifest_price, 0.0) + self.assertFalse(wiz.manifest_currency) + + wiz.manifest_price = 7.5 + wiz.manifest_currency = "USD" + wiz.manifest_license = "custom" + wiz._onchange_manifest_license() + self.assertEqual(wiz.manifest_price, 7.5) + self.assertEqual(wiz.manifest_currency, "USD") diff --git a/addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py b/addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py new file mode 100644 index 0000000..944e00e --- /dev/null +++ b/addons/cetmix_tower_yaml/tests/test_yaml_import_wizard.py @@ -0,0 +1,1008 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +import base64 + +import yaml + +from odoo import _ +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase +from odoo.tools import mute_logger + + +class TestTowerYamlImportWizUpload(TransactionCase): + """Test Tower YAML Import Wizard Upload""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Variables + cls.Variable = cls.env["cx.tower.variable"] + cls.variable_yaml_test = cls.Variable.create( + {"name": "YAML Test", "reference": "yaml_test"} + ) + cls.variable_yaml_url = cls.Variable.create( + {"name": "YAML URL", "reference": "yaml_url"} + ) + + # Tags + cls.Tag = cls.env["cx.tower.tag"] + cls.tag_yaml_test = cls.Tag.create( + {"name": "YAML Test", "reference": "yaml_test"} + ) + cls.tag_another_yaml_test = cls.Tag.create( + {"name": "Another YAML Test", "reference": "another_yaml_test"} + ) + + # Commands + cls.Command = cls.env["cx.tower.command"] + cls.command_yaml_test = cls.Command.create( + {"name": "Test Yaml Command", "reference": "test_yaml_command"} + ) + + # Flight Plan + cls.FlightPlan = cls.env["cx.tower.plan"] + cls.flight_plan_yaml_test = cls.FlightPlan.create( + { + "name": "Test Yaml Flight Plan", + "reference": "test_yaml_flight_plan", + "line_ids": [ + ( + 0, + 0, + { + "condition": False, + "use_sudo": False, + "command_id": cls.command_yaml_test.id, + }, + ), + ], + } + ) + + # Create Server Template used for testing + cls.server_template_yaml_test = cls.env["cx.tower.server.template"].create( + { + "name": "Test Server Template", + "tag_ids": [ + (4, cls.tag_yaml_test.id), + (4, cls.tag_another_yaml_test.id), + ], + "variable_value_ids": [ + ( + 0, + 0, + { + "variable_id": cls.variable_yaml_test.id, + "value_char": "Some Test Value", + }, + ), + ( + 0, + 0, + { + "variable_id": cls.variable_yaml_url.id, + "value_char": "https://cetmix.com", + }, + ), + ], + "flight_plan_id": cls.flight_plan_yaml_test.id, + } + ) + + # Server Logs + cls.ServerLog = cls.env["cx.tower.server.log"] + cls.server_log_yaml_test = cls.ServerLog.create( + { + "name": "Test Server Log", + "reference": "test_server_log", + "command_id": cls.command_yaml_test.id, + "log_type": "command", + "server_template_id": cls.server_template_yaml_test.id, + } + ) + + # Create an export wizard and generate YAML code + context = { + "active_model": "cx.tower.server.template", + "active_ids": [cls.server_template_yaml_test.id], + } + cls.export_wizard = ( + cls.env["cx.tower.yaml.export.wiz"].with_context(context).create({}) # pylint: disable=context-overridden # new need a new clean context + ) + cls.export_wizard.onchange_explode_child_records() + cls.export_wizard.action_generate_yaml_file() + cls.yaml_code = cls.export_wizard.yaml_code + cls.yaml_file = base64.b64encode(cls.yaml_code.encode("utf-8")) + + # YAML import upload wizard + cls.YamlImportWizUpload = cls.env["cx.tower.yaml.import.wiz.upload"] + cls.yaml_upload_wizard = cls.YamlImportWizUpload.create( + {"yaml_file": cls.yaml_file, "file_name": "test_yaml_file.yaml"} + ) + + # YAML import wizard + cls.import_wizard_action = cls.yaml_upload_wizard.action_import_yaml() + cls.import_wizard = cls.env[cls.import_wizard_action["res_model"]].browse( + cls.import_wizard_action["res_id"] + ) + cls.import_wizard.if_record_exists = "update" + + def _create_import_wizard(self, yaml_code, if_record_exists="create"): + """Create import wizard for ad-hoc YAML payload.""" + return self.env["cx.tower.yaml.import.wiz"].create( + { + "yaml_code": yaml_code, + "if_record_exists": if_record_exists, + } + ) + + def test_extract_yaml_data(self): + """Test extract YAML data from file""" + + # -- 1 -- + # Test if YAML file is valid + extracted_yaml_data = self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + extracted_yaml_data, + self.yaml_code, + "YAML code is not extracted correctly", + ) + + # -- 2 -- + # Test if invalid model is handled properly + # Replace model name with invalid model + self.invalid_yaml_code = self.yaml_code.replace( + "server_template", "invalid_model" + ) + self.invalid_yaml_file = base64.b64encode( + self.invalid_yaml_code.encode("utf-8") + ) + self.yaml_upload_wizard.yaml_file = self.invalid_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _("'invalid_model' is not a valid model"), + "Exception message does not match", + ) + # -- 3 -- + # Test if non YAML supported model is handled properly + # Replace model name with non YAML supported model + self.non_yaml_supported_yaml_code = self.yaml_code.replace( + "server_template", "command_run_wizard" + ) + self.non_yaml_supported_yaml_file = base64.b64encode( + self.non_yaml_supported_yaml_code.encode("utf-8") + ) + self.yaml_upload_wizard.yaml_file = self.non_yaml_supported_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _("Model 'command_run_wizard' does not support YAML import"), + "Exception message does not match", + ) + + # -- 4 -- + # Test if YAML that is not a dictionary is handled properly + self.invalid_yaml_file = base64.b64encode(b"Invalid YAML file") + self.yaml_upload_wizard.yaml_file = self.invalid_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _("Yaml file doesn't contain valid data"), + "Exception message does not match", + ) + + # -- 5 -- + # Test if TypeError is handled properly + self.non_unicode_yaml_file = base64.b64encode(b"\x80") + self.yaml_upload_wizard.yaml_file = self.non_unicode_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _("YAML file cannot be decoded properly"), + "Exception message does not match", + ) + + # -- 6 -- + # Test if YAML file is empty + self.empty_yaml_file = "" + self.yaml_upload_wizard.yaml_file = self.empty_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _("File is empty"), + "Exception message does not match", + ) + + # -- 7 -- + # Test if YAML file with unsupported YAML version is handled properly + yaml_with_unsupported_version = self.yaml_code.replace( + f"cetmix_tower_yaml_version: {self.FlightPlan.CETMIX_TOWER_YAML_VERSION}", + f"cetmix_tower_yaml_version: {self.FlightPlan.CETMIX_TOWER_YAML_VERSION + 1}", # noqa: E501 + ) + self.unsupported_yaml_version_yaml_file = base64.b64encode( + yaml_with_unsupported_version.encode("utf-8") + ) + self.yaml_upload_wizard.yaml_file = self.unsupported_yaml_version_yaml_file + with self.assertRaises(ValidationError) as e: + self.yaml_upload_wizard._extract_yaml_data() + self.assertEqual( + str(e.exception), + _( + "YAML version is higher than version" + " supported by your Cetmix Tower instance." + " %(code_version)s > %(tower_version)s", + code_version=self.FlightPlan.CETMIX_TOWER_YAML_VERSION + 1, + tower_version=self.FlightPlan.CETMIX_TOWER_YAML_VERSION, + ), + "Exception message does not match", + ) + + # -- 8 -- + # Test YAML file with no records + self.import_wizard.yaml_code = "cetmix_tower_yaml_version: 1" + with self.assertRaises(ValidationError) as e: + self.import_wizard.action_import_yaml() + self.assertEqual( + str(e.exception), + _("YAML file doesn't contain any records"), + "Exception message does not match", + ) + + def test_action_import_yaml_skip_if_exists(self): + """Test YAML import wizard action when skipping an existing record""" + + self.import_wizard.if_record_exists = "skip" + + # Run import wizard action + import_wizard_result_action = self.import_wizard.action_import_yaml() + + # Test if action is composed properly + self.assertEqual( + import_wizard_result_action["type"], + "ir.actions.client", + "Import wizard action type is not correct", + ) + self.assertEqual( + import_wizard_result_action["tag"], + "display_notification", + "Import wizard action tag is not correct", + ) + self.assertEqual( + import_wizard_result_action["params"]["title"], + _("Record Import"), + "Import wizard action title is not correct", + ) + self.assertEqual( + import_wizard_result_action["params"]["message"], + _("No records were created or updated"), + "Import wizard action message is not correct", + ) + self.assertEqual( + import_wizard_result_action["params"]["sticky"], + True, + "Import wizard action sticky is not correct", + ) + self.assertEqual( + import_wizard_result_action["params"]["type"], + "warning", + "Import wizard action type is not correct", + ) + + def test_action_import_yaml_update_existing_record(self): + """Test YAML import wizard action when updating an existing record""" + + # -- 1 -- + # Test if new import wizard record is created properly + self.assertEqual( + self.import_wizard_action["res_model"], + "cx.tower.yaml.import.wiz", + "Import wizard action model is not correct", + ) + self.assertEqual( + self.import_wizard_action["view_mode"], + "form", + "Import wizard action view mode is not correct", + ) + + # -- 2 -- + # Modify Server Template name and variable value + self.import_wizard.yaml_code = self.import_wizard.yaml_code.replace( + "name: Test Server Template", + "name: Updated Test Server Template", + ).replace( + "value_char: Some Test Value", + "value_char: Updated Test Value", + ) + variable_value_to_update = ( + self.server_template_yaml_test.variable_value_ids.filtered( + lambda v: v.value_char == "Some Test Value" + ) + ) + + # Run import wizard action another time + import_wizard_result_action = self.import_wizard.action_import_yaml() + + # -- 3 -- + # Test if record is updated properly + self.assertEqual( + import_wizard_result_action["res_model"], + "cx.tower.server.template", + "Import wizard action model is not correct", + ) + self.assertEqual( + import_wizard_result_action["domain"], + [("id", "in", self.server_template_yaml_test.ids)], + "ID must match existing record ID", + ) + self.assertEqual( + self.server_template_yaml_test.name, + "Updated Test Server Template", + "Record is not updated properly", + ) + self.assertEqual( + variable_value_to_update.value_char, + "Updated Test Value", + "Variable value is not updated properly", + ) + + # -- 4 -- + # Test if server log remains the same + self.assertEqual( + len(self.server_template_yaml_test.server_log_ids), + 1, + "Server Log must remain the same", + ) + self.assertEqual( + self.server_log_yaml_test.id, + self.server_template_yaml_test.server_log_ids.id, + "Server Log must remain the same", + ) + + def test_action_import_yaml_create_new_record(self): + """Test YAML import wizard action when creating a new record""" + self.import_wizard.if_record_exists = "create" + with mute_logger("odoo.addons.cetmix_tower_yaml.models.cx_tower_yaml_mixin"): + import_wizard_result_action = self.import_wizard.action_import_yaml() + + # -- 1 -- + # Test if new record is created instead of updating existing one + self.assertEqual( + import_wizard_result_action["res_model"], + "cx.tower.server.template", + "Import wizard action model is not correct", + ) + self.assertNotEqual( + import_wizard_result_action["domain"], + f"[('id', '=', {self.server_template_yaml_test.ids})]", + "ID must not match existing record ID", + ) + + # -- 2 -- + # Ensure that existing flight plan is used instead of creating a new one + new_server_template = self.env[import_wizard_result_action["res_model"]].search( + import_wizard_result_action["domain"] + ) + self.assertEqual( + new_server_template.flight_plan_id, + self.flight_plan_yaml_test, + "Existing flight plan must be used", + ) + + # -- 3 -- + # Ensure that existing tags are used instead of creating new ones + for tag in self.server_template_yaml_test.tag_ids: + self.assertIn( + tag, + new_server_template.tag_ids, + "Existing tag must be used", + ) + + # -- 4 -- + # Ensure that new variable values are created + for variable_value in self.server_template_yaml_test.variable_value_ids: + self.assertNotIn( + variable_value, + new_server_template.variable_value_ids, + "New variable value must be created instead of updating existing one", + ) + + # -- 5 -- + # Test if server log is created instead of updated + for server_log in self.server_template_yaml_test.server_log_ids: + self.assertNotIn( + server_log, + new_server_template.server_log_ids, + "New Server Log must be created instead of updating existing one", + ) + + def test_extract_secret_names(self): + """Test extract secret names from YAML data""" + + # NB: this is not a real model, it's just for testing + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: test_model + access_level: manager + reference: such_much_test_record + name: Such Much Command + action: file_using_template + allow_parallel_run: false + note: Just a note + os_ids: false + tag_ids: false + path: false + file_template_id: false + flight_plan_id: false + code: false + variable_ids: false + secret_ids: false + ssh_key_id: + reference: test_ssh_key + name: Test SSH Key + key_type: k + note: false +- cetmix_tower_model: another_test_model + reference: such_much_test_record_2 + name: Such Much Test Record 2 + note: Just a note 2 + ssh_key_id: + reference: test_ssh_key + name: Test SSH Key + key_type: k + note: false + secret_ids: + - reference: secret_2 + name: Secret 2 + key_type: s + note: false + - reference: secret_3 + name: Secret 3 + key_type: s + note: false +- cetmix_tower_model: another_test_model + reference: such_much_test_record_3 + name: Such Much Test Record 3 + note: Just a note 3 + ssh_key_id: + reference: another_ssh_key + name: Another SSH Key + sub_record: + reference: such_much_test_record_4 + name: Such Much Test Record 4 + note: Just a note 4 + secret_ids: + - reference: secret_1 + name: Secret 3 + key_type: s + note: false + - reference: secret_2 + name: Secret 4 + key_type: s + note: false + file_template_id: + reference: my_custom_test_template + name: Such much demo + source: tower + file_type: text + server_dir: /var/log/my/files + file_name: much_logs.txt + keep_when_deleted: false + tag_ids: false + note: Hey! + code: false + variable_ids: false + secret_ids: false + flight_plan_id: false + code: false + variable_ids: false + secret_ids: + - reference: secret_1 + name: Secret 1 + key_type: s + note: false + - reference: secret_2 + name: Secret 2 + key_type: s + note: false +""" + secret_list = self.env["cx.tower.yaml.import.wiz"]._extract_secret_names( + yaml.safe_load(yaml_code) + ) + # We expect 6 secrets in the list: + # 2 keys: 'Test SSH Key', 'Another SSH Key' + # 4 secrets: 'Secret 3', 'Secret 4', 'Secret 1', 'Secret 2' + self.assertEqual(len(secret_list), 6, "Secret list length is not correct") + self.assertIn("Test SSH Key", secret_list, "Key is not in the list") + self.assertIn("Another SSH Key", secret_list, "Key is not in the list") + self.assertIn("Secret 3", secret_list, "Key is not in the list") + self.assertIn("Secret 4", secret_list, "Key is not in the list") + self.assertIn("Secret 1", secret_list, "Key is not in the list") + self.assertIn("Secret 2", secret_list, "Key is not in the list") + + def test_extract_secret_names_with_key_id(self): + """Test extract secret names when secrets are nested under key_id""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: test_model + reference: rec_1 + name: Test Record + secret_ids: + - key_id: + reference: secret_1 + name: Nested Secret 1 + - key_id: + reference: secret_2 + name: Nested Secret 2 + ssh_key_id: + name: SSH Key Nested +""" + secret_list = self.env["cx.tower.yaml.import.wiz"]._extract_secret_names( + yaml.safe_load(yaml_code) + ) + + # We expect 3 secrets total: + # - SSH Key Nested (from ssh_key_id) + # - Nested Secret 1 + # - Nested Secret 2 + self.assertCountEqual( + secret_list, + ["Nested Secret 1", "Nested Secret 2", "SSH Key Nested"], + "Unexpected secrets extracted for nested structure", + ) + + def test_create_records_different_models(self): + """Test create records with different models""" + + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: command + access_level: manager + reference: much_much_command + name: Much Much Command + action: file_using_template + allow_parallel_run: false + note: Just a note + os_ids: false + tag_ids: false + path: false + file_template_id: false + flight_plan_id: false + code: false + variable_ids: false + secret_ids: false + ssh_key_id: + reference: test_ssh_key + name: Test SSH Key + key_type: k + note: false +- cetmix_tower_model: server_template + reference: wow_much_server_template + name: Wow Much Server Template + note: Just a note 2 +- cetmix_tower_model: tag + reference: such_much_tag + name: Such Much Tag +""" + # Create a new command record + self.import_wizard.if_record_exists = "update" + self.import_wizard.yaml_code = yaml_code + + action = self.import_wizard.action_import_yaml() + + # Check if action is composed properly + self.assertEqual( + action["type"], + "ir.actions.client", + "Import wizard action type is not correct", + ) + self.assertEqual( + action["tag"], + "display_notification", + "Import wizard action tag is not correct", + ) + self.assertEqual( + action["params"]["title"], + _("Record Import"), + "Import wizard action title is not correct", + ) + self.assertEqual( + action["params"]["type"], + "success", + "Import wizard action type is not correct", + ) + self.assertEqual( + action["params"]["sticky"], + True, + "Import wizard action sticky is not correct", + ) + + # Check command + self.assertTrue( + self.Command.get_by_reference("much_much_command"), + "Command must be created", + ) + + # Check server template + self.assertTrue( + self.env["cx.tower.server.template"].get_by_reference( + "wow_much_server_template" + ), + "Server template must be created", + ) + + # Check tag + self.assertTrue( + self.Tag.get_by_reference("such_much_tag"), "Tag must be created" + ) + + def test_action_import_yaml_defers_command_template_links(self): + """Forward references for command template links must resolve after import.""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: command + reference: deferred_jet_command + name: Deferred Jet Command + action: jet_action + jet_template_id: deferred_jet_template +- cetmix_tower_model: command + reference: deferred_waypoint_command + name: Deferred Waypoint Command + action: create_waypoint + waypoint_template_id: + reference: deferred_waypoint_template +- cetmix_tower_model: jet_template + reference: deferred_jet_template + name: Deferred Jet Template +- cetmix_tower_model: jet_waypoint_template + reference: deferred_waypoint_template + name: Deferred Waypoint Template + jet_template_id: deferred_jet_template +""" + wiz = self._create_import_wizard(yaml_code) + + wiz.action_import_yaml() + + deferred_jet_command = self.Command.get_by_reference("deferred_jet_command") + deferred_waypoint_command = self.Command.get_by_reference( + "deferred_waypoint_command" + ) + deferred_jet_template = self.env["cx.tower.jet.template"].get_by_reference( + "deferred_jet_template" + ) + deferred_waypoint_template = self.env[ + "cx.tower.jet.waypoint.template" + ].get_by_reference("deferred_waypoint_template") + + self.assertTrue( + deferred_jet_command, + "Jet action command must be created during YAML import", + ) + self.assertEqual( + deferred_jet_command.jet_template_id, + deferred_jet_template, + "Deferred jet template link must be written after the main import pass", + ) + self.assertTrue( + deferred_waypoint_command, + "Waypoint command must be created during YAML import", + ) + self.assertEqual( + deferred_waypoint_command.waypoint_template_id, + deferred_waypoint_template, + "Deferred waypoint template link must be written " + "after the main import pass", + ) + self.assertEqual( + deferred_waypoint_template.jet_template_id, + deferred_jet_template, + "Waypoint template must keep its deferred jet template link after import", + ) + + def test_action_import_yaml_defers_plan_line_command_forward_ref(self): + """Plan lines may reference a command still being created (deep YAML cycle).""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: command + reference: yaml_defer_plan_circ_cmd + name: YAML defer plan circ command + action: create_waypoint + allow_parallel_run: false + note: false + path: false + if_file_exists: skip + disconnect_file: false + fly_here: false + waypoint_template_id: + reference: yaml_defer_plan_circ_wp + name: YAML defer circ waypoint + sequence: 10 + access_level: manager + note: false + jet_template_id: + reference: yaml_defer_plan_circ_jet + name: YAML defer circ jet + note: false + limit_per_server: 0 + show_in_create_wizard: false + action_ids: + - reference: yaml_defer_plan_circ_action + name: YAML defer circ action + note: false + priority: 10 + access_level: manager + state_from_id: + reference: stopped + state_transit_id: + reference: building + state_to_id: + reference: stopped + plan_id: + reference: yaml_defer_plan_circ_plan + name: YAML defer circ plan + access_level: manager + allow_parallel_run: false + color: 0 + note: false + on_error_action: e + custom_exit_code: 0 + line_ids: + - reference: yaml_defer_plan_circ_line + sequence: 10 + condition: false + use_sudo: false + path: false + command_id: + reference: yaml_defer_plan_circ_cmd + is_make_copy: false +""" + wiz = self._create_import_wizard(yaml_code) + wiz.action_import_yaml() + + command = self.Command.get_by_reference("yaml_defer_plan_circ_cmd") + plan = self.FlightPlan.get_by_reference("yaml_defer_plan_circ_plan") + self.assertTrue(command, "Command must be created") + self.assertTrue(plan, "Plan must be created") + self.assertEqual( + len(plan.line_ids), + 1, + "Deferred plan line must be created after the command exists", + ) + self.assertEqual( + plan.line_ids.command_id, + command, + "Plan line command_id must resolve to the enclosing command", + ) + + def test_action_import_yaml_defers_template_dependency_children(self): + """Forward references inside template dependency children must resolve.""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: jet_template + reference: owner_template_with_dependency + name: Owner Template With Dependency + template_requires_ids: + - reference: false + template_required_id: + reference: future_template_dependency + state_required_id: + reference: running +- cetmix_tower_model: jet_template + reference: future_template_dependency + name: Future Template Dependency +""" + wiz = self._create_import_wizard(yaml_code) + + wiz.action_import_yaml() + + owner_template = self.env["cx.tower.jet.template"].get_by_reference( + "owner_template_with_dependency" + ) + future_template = self.env["cx.tower.jet.template"].get_by_reference( + "future_template_dependency" + ) + + self.assertTrue(owner_template, "Owner template must be created") + self.assertTrue( + owner_template.template_requires_ids, + "Deferred dependency child must be created after the main import pass", + ) + self.assertEqual( + owner_template.template_requires_ids.template_required_id, + future_template, + "Deferred dependency child must resolve the required template", + ) + self.assertEqual( + owner_template.template_requires_ids.state_required_id.reference, + "running", + "Deferred dependency child must preserve its required state", + ) + + def test_action_import_yaml_defers_scheduled_task_custom_values(self): + """Forward references inside scheduled-task custom values must resolve.""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: scheduled_task + reference: deferred_scheduled_task + name: Deferred Scheduled Task + action: command + command_id: + reference: test_yaml_command + interval_number: 1 + interval_type: days + next_call: 2026-03-27 00:00:00 + custom_variable_value_ids: + - reference: false + variable_value_id: + reference: deferred_scheduled_task_variable_value +- cetmix_tower_model: variable_value + reference: deferred_scheduled_task_variable_value + variable_id: + reference: yaml_test + value_char: Deferred Value +""" + wiz = self._create_import_wizard(yaml_code) + + wiz.action_import_yaml() + + scheduled_task = self.env["cx.tower.scheduled.task"].get_by_reference( + "deferred_scheduled_task" + ) + variable_value = self.env["cx.tower.variable.value"].get_by_reference( + "deferred_scheduled_task_variable_value" + ) + + self.assertTrue(scheduled_task, "Scheduled task must be created") + self.assertEqual( + len(scheduled_task.custom_variable_value_ids), + 1, + "Deferred scheduled-task custom value must be created after import", + ) + self.assertEqual( + scheduled_task.custom_variable_value_ids.variable_value_id, + variable_value, + "Deferred scheduled-task custom value must resolve variable_value_id", + ) + self.assertEqual( + scheduled_task.custom_variable_value_ids.variable_id, + variable_value.variable_id, + "Deferred scheduled-task custom value must restore variable_id", + ) + self.assertEqual( + scheduled_task.custom_variable_value_ids.value_char, + variable_value.value_char, + "Deferred scheduled-task custom value must restore value_char", + ) + + def test_action_import_yaml_aggregates_missing_deferred_links(self): + """Missing deferred command template links must fail with one clear error.""" + yaml_code = """cetmix_tower_yaml_version: 1 +records: +- cetmix_tower_model: command + reference: command_missing_jet_template + name: Command Missing Jet Template + action: jet_action + jet_template_id: missing_jet_template +- cetmix_tower_model: command + reference: command_missing_waypoint_template + name: Command Missing Waypoint Template + action: create_waypoint + waypoint_template_id: + reference: missing_waypoint_template +- cetmix_tower_model: command + reference: command_valid_deferred_template + name: Command Valid Deferred Template + action: jet_action + jet_template_id: valid_deferred_jet_template +- cetmix_tower_model: jet_template + reference: valid_deferred_jet_template + name: Valid Deferred Jet Template +""" + wiz = self._create_import_wizard(yaml_code) + + with self.assertRaises(ValidationError) as err: + wiz.action_import_yaml() + + error_message = str(err.exception) + self.assertIn( + "Deferred relation resolution failed", + error_message, + "Import must raise one aggregated deferred-resolution error", + ) + self.assertIn( + "Record cx.tower.command 'command_missing_jet_template': " + "field 'jet_template_id'", + error_message, + "Error must include unresolved jet template links", + ) + self.assertIn( + "cx.tower.jet.template 'missing_jet_template'", + error_message, + "Error must include missing jet template reference", + ) + self.assertIn( + "Record cx.tower.command 'command_missing_waypoint_template': " + "field 'waypoint_template_id'", + error_message, + "Error must include unresolved waypoint template links", + ) + self.assertIn( + "cx.tower.jet.waypoint.template 'missing_waypoint_template'", + error_message, + "Error must include missing waypoint template reference", + ) + + def test_yaml_import_server_without_password(self): + """Wizard should import server without ssh_password.""" + yaml_code = ( + "cetmix_tower_yaml_version: 1\n" + "records:\n" + "- reference: srv_nopass\n" + " cetmix_tower_model: server\n" + " name: YAML NoPass\n" + " ssh_auth_mode: p\n" + " ssh_username: root\n" + " ip_v4_address: 10.0.0.3\n" + ) + wiz = self.env["cx.tower.yaml.import.wiz"].create( + { + "yaml_code": yaml_code, + "if_record_exists": "create", + } + ) + wiz.action_import_yaml() + + srv = self.env["cx.tower.server"].get_by_reference("srv_nopass") + self.assertTrue(srv, "Server was not created") + self.assertFalse( + srv._get_secret_value("ssh_password"), + "ssh_password must stay empty after import", + ) + + def test_orm_create_server_requires_password(self): + """Creating a server via ORM/UI must fail when ssh_password is missing.""" + with self.assertRaises(ValidationError) as err: + self.env["cx.tower.server"].create( + { + "reference": "srv_ui", + "name": "UI NoPass", + "ssh_auth_mode": "p", + "ssh_username": "root", + "ip_v4_address": "10.0.0.2", + } + ) + self.assertIn("Please provide SSH password", str(err.exception)) + + def test_yaml_import_server_with_skip_ssh_check(self): + """Explicit skip_ssh_settings_check also bypasses password validation.""" + yaml_code = ( + "cetmix_tower_yaml_version: 1\n" + "records:\n" + "- reference: srv_skip\n" + " cetmix_tower_model: server\n" + " name: YAML Skip Check\n" + " ssh_auth_mode: p\n" + " ssh_username: root\n" + " ip_v4_address: 10.0.0.4\n" + ) + wiz = self.env["cx.tower.yaml.import.wiz"].create( + { + "yaml_code": yaml_code, + "if_record_exists": "create", + } + ) + wiz.with_context(skip_ssh_settings_check=True).action_import_yaml() + + srv = self.env["cx.tower.server"].get_by_reference("srv_skip") + self.assertTrue( + srv, "Server must be created when skip_ssh_settings_check is set" + ) diff --git a/addons/cetmix_tower_yaml/views/cx_tower_command_view.xml b/addons/cetmix_tower_yaml/views/cx_tower_command_view.xml new file mode 100644 index 0000000..1f6f0b8 --- /dev/null +++ b/addons/cetmix_tower_yaml/views/cx_tower_command_view.xml @@ -0,0 +1,10 @@ + + + + Export YAML + cx.tower.yaml.export.wiz + form + new + + + diff --git a/addons/cetmix_tower_yaml/views/cx_tower_file_template_view.xml b/addons/cetmix_tower_yaml/views/cx_tower_file_template_view.xml new file mode 100644 index 0000000..41c3dae --- /dev/null +++ b/addons/cetmix_tower_yaml/views/cx_tower_file_template_view.xml @@ -0,0 +1,13 @@ + + + + Export YAML + cx.tower.yaml.export.wiz + form + new + + + diff --git a/addons/cetmix_tower_yaml/views/cx_tower_jet_template_view.xml b/addons/cetmix_tower_yaml/views/cx_tower_jet_template_view.xml new file mode 100644 index 0000000..2216608 --- /dev/null +++ b/addons/cetmix_tower_yaml/views/cx_tower_jet_template_view.xml @@ -0,0 +1,37 @@ + + + + cx.tower.jet.template.yaml.view.form + cx.tower.jet.template + + + + +
+

You must be a member of the "YAML/Export" group to export data as YAML.

+
+