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 0000000..2507f55 Binary files /dev/null and b/addons/cetmix_tower_yaml/static/description/icon.png differ 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.

+
+