Wipe cetmix_tower_yaml (polluted by overlapping uploads)
This commit is contained in:
@@ -1,152 +0,0 @@
|
|||||||
=================
|
|
||||||
Cetmix Tower YAML
|
|
||||||
=================
|
|
||||||
|
|
||||||
..
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! This file is generated by oca-gen-addon-readme !!
|
|
||||||
!! changes will be overwritten. !!
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! source digest: sha256:96e8f3f1df3ab25b952a9534d0914149740cc036b62efe2c7795f9d2d9636177
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
|
|
||||||
.. |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/16.0/cetmix_tower_yaml
|
|
||||||
:alt: cetmix/cetmix-tower
|
|
||||||
|
|
||||||
|badge1| |badge2| |badge3|
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
**Table of contents**
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:local:
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
Please refer to the `official
|
|
||||||
documentation <https://cetmix.com/tower>`__ for detailed configuration
|
|
||||||
instructions.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
=====
|
|
||||||
|
|
||||||
Please refer to the `official
|
|
||||||
documentation <https://cetmix.com/tower>`__ for detailed usage
|
|
||||||
instructions.
|
|
||||||
|
|
||||||
Changelog
|
|
||||||
=========
|
|
||||||
|
|
||||||
16.0.2.0.1 (2025-10-29)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Improve the way secrets are listed in the YAML import
|
|
||||||
widget. (5010)
|
|
||||||
|
|
||||||
16.0.1.4.2 (2025-10-06)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Bugfixes: Add the missing 'create' function decorator (4980)
|
|
||||||
|
|
||||||
16.0.1.4.1 (2025-08-26)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Bugfixes: Make selection values lowercase to simplify their
|
|
||||||
management. (4896)
|
|
||||||
|
|
||||||
16.0.1.3.0 (2025-07-30)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Optional behaviour when file uploaded by command already
|
|
||||||
exists on the server. (4740)
|
|
||||||
|
|
||||||
16.0.1.1.4 (2025-07-08)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Bugfixes: Fix missing model names in YAML exports when exporting
|
|
||||||
multiple commands with flight plans (4820)
|
|
||||||
|
|
||||||
16.0.1.1.3 (2025-07-07)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Bugfixes: Import servers with ``Password`` ssh authentication mode
|
|
||||||
(4812)
|
|
||||||
|
|
||||||
16.0.1.1.1 (2025-06-23)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: YAML code optimisation (4728)
|
|
||||||
|
|
||||||
16.0.1.1.0 (2025-06-20)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Export/import scheduled tasks to/from YAML. (4650)
|
|
||||||
|
|
||||||
16.0.1.0.5 (2025-05-21)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Export/import secret values related to Server. (4696)
|
|
||||||
|
|
||||||
16.0.1.0.4 (2025-05-16)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Export/import servers and files to/from YAML. (4670)
|
|
||||||
|
|
||||||
16.0.1.0.3 (2025-05-09)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Bugfixes: Non-critical issues and performance improvements. (4663)
|
|
||||||
|
|
||||||
16.0.1.0.2 (2025-04-30)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: User groups are visible without developer mode. (4642)
|
|
||||||
|
|
||||||
16.0.1.0.1 (2025-04-21)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- Features: Export additional fields for shortcuts, variables and
|
|
||||||
options. Add action menu to export keys/secrets. (4602)
|
|
||||||
|
|
||||||
16.0.1.0.0
|
|
||||||
----------
|
|
||||||
|
|
||||||
Release for Odoo 16.0
|
|
||||||
|
|
||||||
Bug Tracker
|
|
||||||
===========
|
|
||||||
|
|
||||||
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/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 <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_yaml%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
|
||||||
|
|
||||||
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 <https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_yaml>`_ project on GitHub.
|
|
||||||
|
|
||||||
You are welcome to contribute.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from . import models
|
|
||||||
from . import wizards
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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": "16.0.2.0.3",
|
|
||||||
"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_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",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo noupdate="1">
|
|
||||||
<!-- Add demo users to groups -->
|
|
||||||
<record id="base.user_admin" model="res.users">
|
|
||||||
<field
|
|
||||||
name="groups_id"
|
|
||||||
eval="[
|
|
||||||
(4, ref('cetmix_tower_yaml.group_export')),
|
|
||||||
(4, ref('cetmix_tower_yaml.group_import')),
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,587 +0,0 @@
|
|||||||
# Translation of Odoo Server.
|
|
||||||
# This file contains the translation of the following modules:
|
|
||||||
# * cetmix_tower_yaml
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Odoo Server 16.0\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"PO-Revision-Date: 2025-03-06 09:11+0000\n"
|
|
||||||
"Last-Translator: Bole <bole@dajmi5.com>\n"
|
|
||||||
"Language-Team: Croatian <https://hosted.weblate.org/projects/"
|
|
||||||
"tower-server-14-0-dev/cetmix_tower_yaml/hr/>\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 ""
|
|
||||||
"<strong>Important:</strong> To maintain data consistency, the following "
|
|
||||||
"model records will always be updated if they exist in Odoo:"
|
|
||||||
msgstr ""
|
|
||||||
"<strong>Važno:</strong> 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 <code>reference</code> field in the YAML code for those entities."
|
|
||||||
msgstr ""
|
|
||||||
"Za kreiranje novih entiteta umjesto ažuriranja postojećih, uklonite ili "
|
|
||||||
"modificirajte <code>reference</code> 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."
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
|||||||
def migrate(cr, version):
|
|
||||||
"""
|
|
||||||
Normalize existing license values to lowercase.
|
|
||||||
Runs only on upgrade (version != False).
|
|
||||||
"""
|
|
||||||
if not version:
|
|
||||||
return
|
|
||||||
# Skip rows already lowercase for efficiency
|
|
||||||
cr.execute(
|
|
||||||
"""
|
|
||||||
UPDATE cx_tower_yaml_manifest_tmpl
|
|
||||||
SET license = LOWER(TRIM(license))
|
|
||||||
WHERE license IS NOT NULL
|
|
||||||
AND (
|
|
||||||
license <> LOWER(license)
|
|
||||||
OR license <> TRIM(license)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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_file
|
|
||||||
from . import cx_tower_server
|
|
||||||
from . import cx_tower_yaml_manifest_template
|
|
||||||
from . import cx_tower_yaml_manifest_author
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# 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",
|
|
||||||
"flight_plan_id",
|
|
||||||
"code",
|
|
||||||
"server_status",
|
|
||||||
"variable_ids",
|
|
||||||
"secret_ids",
|
|
||||||
"no_split_for_sudo",
|
|
||||||
"if_file_exists",
|
|
||||||
"disconnect_file",
|
|
||||||
]
|
|
||||||
return res
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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",
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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",
|
|
||||||
]
|
|
||||||
return res
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# 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")
|
|
||||||
)
|
|
||||||
@@ -1,577 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
|
||||||
from odoo.exceptions import AccessError, ValidationError
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
# 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._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"))
|
|
||||||
|
|
||||||
@api.model_create_multi
|
|
||||||
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 _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 {"reference": ref}
|
|
||||||
|
|
||||||
# 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})
|
|
||||||
|
|
||||||
if collector and collector_key:
|
|
||||||
collector.add(collector_key)
|
|
||||||
|
|
||||||
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 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
|
|
||||||
)._process_relation_field_value(key, value, record_mode=False)
|
|
||||||
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(
|
|
||||||
comodel, value, explode_related_record, record_mode
|
|
||||||
)
|
|
||||||
if field_type in ["one2many", "many2many"]:
|
|
||||||
return self._process_x2m_values(
|
|
||||||
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, comodel, value, explode_related_record, record_mode=False
|
|
||||||
):
|
|
||||||
"""Post process many2one value
|
|
||||||
Args:
|
|
||||||
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 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")
|
|
||||||
|
|
||||||
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, comodel, field_type, values, explode_related_record, record_mode=False
|
|
||||||
):
|
|
||||||
"""Post process many2many value
|
|
||||||
Args:
|
|
||||||
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):
|
|
||||||
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 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:
|
|
||||||
if create_immediately:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["whool"]
|
|
||||||
build-backend = "whool.buildapi"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
## 16.0.2.0.1 (2025-10-29)
|
|
||||||
|
|
||||||
- Features: Improve the way secrets are listed in the YAML import widget. (5010)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.4.2 (2025-10-06)
|
|
||||||
|
|
||||||
- Bugfixes: Add the missing 'create' function decorator (4980)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.4.1 (2025-08-26)
|
|
||||||
|
|
||||||
- Bugfixes: Make selection values lowercase to simplify their management. (4896)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.3.0 (2025-07-30)
|
|
||||||
|
|
||||||
- Features: Optional behaviour when file uploaded by command already exists on the server. (4740)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.1.4 (2025-07-08)
|
|
||||||
|
|
||||||
- Bugfixes: Fix missing model names in YAML exports when exporting multiple commands with flight plans (4820)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.1.3 (2025-07-07)
|
|
||||||
|
|
||||||
- Bugfixes: Import servers with `Password` ssh authentication mode (4812)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.1.1 (2025-06-23)
|
|
||||||
|
|
||||||
- Features: YAML code optimisation (4728)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.1.0 (2025-06-20)
|
|
||||||
|
|
||||||
- Features: Export/import scheduled tasks to/from YAML. (4650)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.5 (2025-05-21)
|
|
||||||
|
|
||||||
- Features: Export/import secret values related to Server. (4696)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.4 (2025-05-16)
|
|
||||||
|
|
||||||
- Features: Export/import servers and files to/from YAML. (4670)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.3 (2025-05-09)
|
|
||||||
|
|
||||||
- Bugfixes: Non-critical issues and performance improvements. (4663)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.2 (2025-04-30)
|
|
||||||
|
|
||||||
- Features: User groups are visible without developer mode. (4642)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.1 (2025-04-21)
|
|
||||||
|
|
||||||
- Features: Export additional fields for shortcuts, variables and options.
|
|
||||||
Add action menu to export keys/secrets. (4602)
|
|
||||||
|
|
||||||
|
|
||||||
## 16.0.1.0.0
|
|
||||||
|
|
||||||
Release for Odoo 16.0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="ir_module_category_tower_yaml_export" model="ir.module.category">
|
|
||||||
<field name="parent_id" ref="cetmix_tower_server.ir_module_category_tower" />
|
|
||||||
<field name="name">YAML Export</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="ir_module_category_tower_yaml_import" model="ir.module.category">
|
|
||||||
<field name="parent_id" ref="cetmix_tower_server.ir_module_category_tower" />
|
|
||||||
<field name="name">YAML Import</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="group_export" model="res.groups">
|
|
||||||
<field name="name">Allow</field>
|
|
||||||
<field name="category_id" ref="ir_module_category_tower_yaml_export" />
|
|
||||||
<field name="comment">
|
|
||||||
Export data to YAML.
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="group_import" model="res.groups">
|
|
||||||
<field name="name">Allow</field>
|
|
||||||
<field name="category_id" ref="ir_module_category_tower_yaml_import" />
|
|
||||||
<field name="comment">
|
|
||||||
Import data from YAML.
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<odoo noupdate="1">
|
|
||||||
|
|
||||||
<!-- cx.tower.yaml.export.wiz -->
|
|
||||||
<record id="rule_cx_tower_yaml_export_wiz_creator_only" model="ir.rule">
|
|
||||||
<field name="name">Creator only</field>
|
|
||||||
<field name="model_id" ref="model_cx_tower_yaml_export_wiz" />
|
|
||||||
<field name="global" eval="True" />
|
|
||||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
|
||||||
</record>
|
|
||||||
<!-- cx.tower.yaml.export.wiz.download -->
|
|
||||||
|
|
||||||
<record
|
|
||||||
id="rule_cx_tower_yaml_export_wiz_download_creator_only"
|
|
||||||
model="ir.rule"
|
|
||||||
>
|
|
||||||
<field name="name">Creator only</field>
|
|
||||||
<field name="model_id" ref="model_cx_tower_yaml_export_wiz_download" />
|
|
||||||
<field name="global" eval="True" />
|
|
||||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- cx.tower.yaml.import.wiz -->
|
|
||||||
<record id="rule_cx_tower_yaml_import_wiz_creator_only" model="ir.rule">
|
|
||||||
<field name="name">Creator only</field>
|
|
||||||
<field name="model_id" ref="model_cx_tower_yaml_import_wiz" />
|
|
||||||
<field name="global" eval="True" />
|
|
||||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- cx.tower.yaml.import.wiz.upload -->
|
|
||||||
<record id="rule_cx_tower_yaml_import_wiz_upload_creator_only" model="ir.rule">
|
|
||||||
<field name="name">Creator only</field>
|
|
||||||
<field name="model_id" ref="model_cx_tower_yaml_import_wiz_upload" />
|
|
||||||
<field name="global" eval="True" />
|
|
||||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,534 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
|
||||||
<title>Cetmix Tower YAML</title>
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
/*
|
|
||||||
:Author: David Goodger (goodger@python.org)
|
|
||||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
|
||||||
:Copyright: This stylesheet has been placed in the public domain.
|
|
||||||
|
|
||||||
Default cascading style sheet for the HTML output of Docutils.
|
|
||||||
Despite the name, some widely supported CSS2 features are used.
|
|
||||||
|
|
||||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
|
||||||
customize this style sheet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* used to remove borders from tables and images */
|
|
||||||
.borderless, table.borderless td, table.borderless th {
|
|
||||||
border: 0 }
|
|
||||||
|
|
||||||
table.borderless td, table.borderless th {
|
|
||||||
/* Override padding for "table.docutils td" with "! important".
|
|
||||||
The right padding separates the table cells. */
|
|
||||||
padding: 0 0.5em 0 0 ! important }
|
|
||||||
|
|
||||||
.first {
|
|
||||||
/* Override more specific margin styles with "! important". */
|
|
||||||
margin-top: 0 ! important }
|
|
||||||
|
|
||||||
.last, .with-subtitle {
|
|
||||||
margin-bottom: 0 ! important }
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none }
|
|
||||||
|
|
||||||
.subscript {
|
|
||||||
vertical-align: sub;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
.superscript {
|
|
||||||
vertical-align: super;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
a.toc-backref {
|
|
||||||
text-decoration: none ;
|
|
||||||
color: black }
|
|
||||||
|
|
||||||
blockquote.epigraph {
|
|
||||||
margin: 2em 5em ; }
|
|
||||||
|
|
||||||
dl.docutils dd {
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
|
||||||
dl.docutils dt {
|
|
||||||
font-weight: bold }
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.abstract {
|
|
||||||
margin: 2em 5em }
|
|
||||||
|
|
||||||
div.abstract p.topic-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
|
||||||
div.hint, div.important, div.note, div.tip, div.warning {
|
|
||||||
margin: 2em ;
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em }
|
|
||||||
|
|
||||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
|
||||||
div.important p.admonition-title, div.note p.admonition-title,
|
|
||||||
div.tip p.admonition-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
|
||||||
div.danger p.admonition-title, div.error p.admonition-title,
|
|
||||||
div.warning p.admonition-title, .code .error {
|
|
||||||
color: red ;
|
|
||||||
font-weight: bold ;
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
|
||||||
compound paragraphs.
|
|
||||||
div.compound .compound-first, div.compound .compound-middle {
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
div.compound .compound-last, div.compound .compound-middle {
|
|
||||||
margin-top: 0.5em }
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.dedication {
|
|
||||||
margin: 2em 5em ;
|
|
||||||
text-align: center ;
|
|
||||||
font-style: italic }
|
|
||||||
|
|
||||||
div.dedication p.topic-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-style: normal }
|
|
||||||
|
|
||||||
div.figure {
|
|
||||||
margin-left: 2em ;
|
|
||||||
margin-right: 2em }
|
|
||||||
|
|
||||||
div.footer, div.header {
|
|
||||||
clear: both;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
div.line-block {
|
|
||||||
display: block ;
|
|
||||||
margin-top: 1em ;
|
|
||||||
margin-bottom: 1em }
|
|
||||||
|
|
||||||
div.line-block div.line-block {
|
|
||||||
margin-top: 0 ;
|
|
||||||
margin-bottom: 0 ;
|
|
||||||
margin-left: 1.5em }
|
|
||||||
|
|
||||||
div.sidebar {
|
|
||||||
margin: 0 0 0.5em 1em ;
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em ;
|
|
||||||
background-color: #ffffee ;
|
|
||||||
width: 40% ;
|
|
||||||
float: right ;
|
|
||||||
clear: right }
|
|
||||||
|
|
||||||
div.sidebar p.rubric {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-size: medium }
|
|
||||||
|
|
||||||
div.system-messages {
|
|
||||||
margin: 5em }
|
|
||||||
|
|
||||||
div.system-messages h1 {
|
|
||||||
color: red }
|
|
||||||
|
|
||||||
div.system-message {
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em }
|
|
||||||
|
|
||||||
div.system-message p.system-message-title {
|
|
||||||
color: red ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
div.topic {
|
|
||||||
margin: 2em }
|
|
||||||
|
|
||||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
|
||||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
|
||||||
margin-top: 0.4em }
|
|
||||||
|
|
||||||
h1.title {
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
h2.subtitle {
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
hr.docutils {
|
|
||||||
width: 75% }
|
|
||||||
|
|
||||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
|
||||||
clear: left ;
|
|
||||||
float: left ;
|
|
||||||
margin-right: 1em }
|
|
||||||
|
|
||||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
|
||||||
clear: right ;
|
|
||||||
float: right ;
|
|
||||||
margin-left: 1em }
|
|
||||||
|
|
||||||
img.align-center, .figure.align-center, object.align-center {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.align-center {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left }
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
clear: both ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right }
|
|
||||||
|
|
||||||
/* reset inner alignment in figures */
|
|
||||||
div.align-right {
|
|
||||||
text-align: inherit }
|
|
||||||
|
|
||||||
/* div.align-center * { */
|
|
||||||
/* text-align: left } */
|
|
||||||
|
|
||||||
.align-top {
|
|
||||||
vertical-align: top }
|
|
||||||
|
|
||||||
.align-middle {
|
|
||||||
vertical-align: middle }
|
|
||||||
|
|
||||||
.align-bottom {
|
|
||||||
vertical-align: bottom }
|
|
||||||
|
|
||||||
ol.simple, ul.simple {
|
|
||||||
margin-bottom: 1em }
|
|
||||||
|
|
||||||
ol.arabic {
|
|
||||||
list-style: decimal }
|
|
||||||
|
|
||||||
ol.loweralpha {
|
|
||||||
list-style: lower-alpha }
|
|
||||||
|
|
||||||
ol.upperalpha {
|
|
||||||
list-style: upper-alpha }
|
|
||||||
|
|
||||||
ol.lowerroman {
|
|
||||||
list-style: lower-roman }
|
|
||||||
|
|
||||||
ol.upperroman {
|
|
||||||
list-style: upper-roman }
|
|
||||||
|
|
||||||
p.attribution {
|
|
||||||
text-align: right ;
|
|
||||||
margin-left: 50% }
|
|
||||||
|
|
||||||
p.caption {
|
|
||||||
font-style: italic }
|
|
||||||
|
|
||||||
p.credits {
|
|
||||||
font-style: italic ;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
p.label {
|
|
||||||
white-space: nowrap }
|
|
||||||
|
|
||||||
p.rubric {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-size: larger ;
|
|
||||||
color: maroon ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
p.sidebar-title {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold ;
|
|
||||||
font-size: larger }
|
|
||||||
|
|
||||||
p.sidebar-subtitle {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
p.topic-title {
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
pre.address {
|
|
||||||
margin-bottom: 0 ;
|
|
||||||
margin-top: 0 ;
|
|
||||||
font: inherit }
|
|
||||||
|
|
||||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
|
||||||
margin-left: 2em ;
|
|
||||||
margin-right: 2em }
|
|
||||||
|
|
||||||
pre.code .ln { color: gray; } /* line numbers */
|
|
||||||
pre.code, code { background-color: #eeeeee }
|
|
||||||
pre.code .comment, code .comment { color: #5C6576 }
|
|
||||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
|
||||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
|
||||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
|
||||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
|
||||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
|
||||||
|
|
||||||
span.classifier {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-style: oblique }
|
|
||||||
|
|
||||||
span.classifier-delimiter {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
span.interpreted {
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
span.option {
|
|
||||||
white-space: nowrap }
|
|
||||||
|
|
||||||
span.pre {
|
|
||||||
white-space: pre }
|
|
||||||
|
|
||||||
span.problematic, pre.problematic {
|
|
||||||
color: red }
|
|
||||||
|
|
||||||
span.section-subtitle {
|
|
||||||
/* font-size relative to parent (h1..h6 element) */
|
|
||||||
font-size: 80% }
|
|
||||||
|
|
||||||
table.citation {
|
|
||||||
border-left: solid 1px gray;
|
|
||||||
margin-left: 1px }
|
|
||||||
|
|
||||||
table.docinfo {
|
|
||||||
margin: 2em 4em }
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
margin-top: 0.5em ;
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
table.footnote {
|
|
||||||
border-left: solid 1px black;
|
|
||||||
margin-left: 1px }
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th,
|
|
||||||
table.docinfo td, table.docinfo th {
|
|
||||||
padding-left: 0.5em ;
|
|
||||||
padding-right: 0.5em ;
|
|
||||||
vertical-align: top }
|
|
||||||
|
|
||||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
|
||||||
font-weight: bold ;
|
|
||||||
text-align: left ;
|
|
||||||
white-space: nowrap ;
|
|
||||||
padding-left: 0 }
|
|
||||||
|
|
||||||
/* "booktabs" style (no vertical lines) */
|
|
||||||
table.docutils.booktabs {
|
|
||||||
border: 0px;
|
|
||||||
border-top: 2px solid;
|
|
||||||
border-bottom: 2px solid;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table.docutils.booktabs * {
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
table.docutils.booktabs th {
|
|
||||||
border-bottom: thin solid;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
|
||||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
|
||||||
font-size: 100% }
|
|
||||||
|
|
||||||
ul.auto-toc {
|
|
||||||
list-style-type: none }
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="document" id="cetmix-tower-yaml">
|
|
||||||
<h1 class="title">Cetmix Tower YAML</h1>
|
|
||||||
|
|
||||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! This file is generated by oca-gen-addon-readme !!
|
|
||||||
!! changes will be overwritten. !!
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! source digest: sha256:96e8f3f1df3ab25b952a9534d0914149740cc036b62efe2c7795f9d2d9636177
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
|
||||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_yaml"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
|
|
||||||
<p>This module implements YAML format data import/export for <a class="reference external" href="https://cetmix.com/tower">Cetmix
|
|
||||||
Tower</a>.</p>
|
|
||||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
|
||||||
documentation</a> for detailed information.</p>
|
|
||||||
<p><strong>Table of contents</strong></p>
|
|
||||||
<div class="contents local topic" id="contents">
|
|
||||||
<ul class="simple">
|
|
||||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
|
||||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
|
||||||
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
|
|
||||||
<li><a class="reference internal" href="#section-1" id="toc-entry-4">16.0.2.0.1 (2025-10-29)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-2" id="toc-entry-5">16.0.1.4.2 (2025-10-06)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-3" id="toc-entry-6">16.0.1.4.1 (2025-08-26)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-4" id="toc-entry-7">16.0.1.3.0 (2025-07-30)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-5" id="toc-entry-8">16.0.1.1.4 (2025-07-08)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-6" id="toc-entry-9">16.0.1.1.3 (2025-07-07)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-7" id="toc-entry-10">16.0.1.1.1 (2025-06-23)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-8" id="toc-entry-11">16.0.1.1.0 (2025-06-20)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-9" id="toc-entry-12">16.0.1.0.5 (2025-05-21)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-10" id="toc-entry-13">16.0.1.0.4 (2025-05-16)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-11" id="toc-entry-14">16.0.1.0.3 (2025-05-09)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-12" id="toc-entry-15">16.0.1.0.2 (2025-04-30)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-13" id="toc-entry-16">16.0.1.0.1 (2025-04-21)</a></li>
|
|
||||||
<li><a class="reference internal" href="#section-14" id="toc-entry-17">16.0.1.0.0</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-18">Bug Tracker</a></li>
|
|
||||||
<li><a class="reference internal" href="#credits" id="toc-entry-19">Credits</a><ul>
|
|
||||||
<li><a class="reference internal" href="#authors" id="toc-entry-20">Authors</a></li>
|
|
||||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-21">Maintainers</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="configuration">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
|
||||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
|
||||||
documentation</a> for detailed configuration
|
|
||||||
instructions.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="usage">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
|
||||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
|
||||||
documentation</a> for detailed usage
|
|
||||||
instructions.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="changelog">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
|
|
||||||
<div class="section" id="section-1">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-4">16.0.2.0.1 (2025-10-29)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Improve the way secrets are listed in the YAML import
|
|
||||||
widget. (5010)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-2">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-5">16.0.1.4.2 (2025-10-06)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Bugfixes: Add the missing ‘create’ function decorator (4980)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-3">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-6">16.0.1.4.1 (2025-08-26)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Bugfixes: Make selection values lowercase to simplify their
|
|
||||||
management. (4896)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-4">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-7">16.0.1.3.0 (2025-07-30)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Optional behaviour when file uploaded by command already
|
|
||||||
exists on the server. (4740)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-5">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-8">16.0.1.1.4 (2025-07-08)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Bugfixes: Fix missing model names in YAML exports when exporting
|
|
||||||
multiple commands with flight plans (4820)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-6">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-9">16.0.1.1.3 (2025-07-07)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Bugfixes: Import servers with <tt class="docutils literal">Password</tt> ssh authentication mode
|
|
||||||
(4812)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-7">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-10">16.0.1.1.1 (2025-06-23)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: YAML code optimisation (4728)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-8">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-11">16.0.1.1.0 (2025-06-20)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Export/import scheduled tasks to/from YAML. (4650)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-9">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-12">16.0.1.0.5 (2025-05-21)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Export/import secret values related to Server. (4696)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-10">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-13">16.0.1.0.4 (2025-05-16)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Export/import servers and files to/from YAML. (4670)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-11">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-14">16.0.1.0.3 (2025-05-09)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Bugfixes: Non-critical issues and performance improvements. (4663)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-12">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-15">16.0.1.0.2 (2025-04-30)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: User groups are visible without developer mode. (4642)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-13">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-16">16.0.1.0.1 (2025-04-21)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Features: Export additional fields for shortcuts, variables and
|
|
||||||
options. Add action menu to export keys/secrets. (4602)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="section-14">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-17">16.0.1.0.0</a></h2>
|
|
||||||
<p>Release for Odoo 16.0</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="bug-tracker">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-18">Bug Tracker</a></h1>
|
|
||||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
|
|
||||||
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
|
|
||||||
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_yaml%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
|
||||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="credits">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-19">Credits</a></h1>
|
|
||||||
<div class="section" id="authors">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-20">Authors</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Cetmix</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="maintainers">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-21">Maintainers</a></h2>
|
|
||||||
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_yaml">cetmix/cetmix-tower</a> project on GitHub.</p>
|
|
||||||
<p>You are welcome to contribute.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
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
|
|
||||||
flight_plan_id: false
|
|
||||||
code: |-
|
|
||||||
cd /home/{{ tower.server.ssh_username }} \\
|
|
||||||
&& ls -lha
|
|
||||||
server_status: false
|
|
||||||
variable_ids: false
|
|
||||||
secret_ids: false
|
|
||||||
no_split_for_sudo: false
|
|
||||||
if_file_exists: skip
|
|
||||||
disconnect_file: 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
|
|
||||||
yaml_with_non_supported_keys = """access_level: manager
|
|
||||||
action: non_existing_action
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
with self.assertRaises(ValidationError) as e:
|
|
||||||
command_test.yaml_code = yaml_with_non_supported_keys
|
|
||||||
self.assertIn("non_existing_action", str(e.exception))
|
|
||||||
self.assertEqual(
|
|
||||||
str(e),
|
|
||||||
"Wrong value for cx.tower.command.action: 'non_existing_action'",
|
|
||||||
"Exception message doesn't match",
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
flight_plan_id: false
|
|
||||||
code: false
|
|
||||||
server_status: false
|
|
||||||
variable_ids: false
|
|
||||||
secret_ids: false
|
|
||||||
no_split_for_sudo: false
|
|
||||||
if_file_exists: skip
|
|
||||||
disconnect_file: 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
|
|
||||||
flight_plan_id: false
|
|
||||||
code: false
|
|
||||||
server_status: false
|
|
||||||
variable_ids: false
|
|
||||||
secret_ids: false
|
|
||||||
no_split_for_sudo: false
|
|
||||||
if_file_exists: skip
|
|
||||||
disconnect_file: 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",
|
|
||||||
)
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# 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",
|
|
||||||
)
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# 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",
|
|
||||||
)
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
|
|
||||||
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"]
|
|
||||||
TowerTag = cls.env["cx.tower.tag"]
|
|
||||||
cls.tag_doge = TowerTag.create({"name": "Doge", "reference": "doge"})
|
|
||||||
cls.tag_pepe = TowerTag.create({"name": "Pepe", "reference": "pepe"})
|
|
||||||
|
|
||||||
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"]
|
|
||||||
|
|
||||||
self.YamlMixin._patch_method("_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",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore original method
|
|
||||||
self.YamlMixin._revert_method("_get_fields_for_yaml")
|
|
||||||
|
|
||||||
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"]
|
|
||||||
|
|
||||||
self.YamlMixin._patch_method("_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",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore original method
|
|
||||||
self.YamlMixin._revert_method("_get_fields_for_yaml")
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
# 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
|
|
||||||
code: echo 'Test Command From Yaml'
|
|
||||||
server_status: false
|
|
||||||
no_split_for_sudo: false
|
|
||||||
if_file_exists: skip
|
|
||||||
disconnect_file: 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
|
|
||||||
code: echo 'Test Command From Yaml 2'
|
|
||||||
server_status: false
|
|
||||||
no_split_for_sudo: false
|
|
||||||
if_file_exists: skip
|
|
||||||
disconnect_file: 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")
|
|
||||||
@@ -1,703 +0,0 @@
|
|||||||
# 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 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_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"
|
|
||||||
)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_command_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.command.yaml.view.form</field>
|
|
||||||
<field name="model">cx.tower.command</field>
|
|
||||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_command_view_form" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="action_cx_tower_command_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_command" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_file_template_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.file.template.yaml.view.form</field>
|
|
||||||
<field name="model">cx.tower.file.template</field>
|
|
||||||
<field
|
|
||||||
name="inherit_id"
|
|
||||||
ref="cetmix_tower_server.cx_tower_file_template_view_form"
|
|
||||||
/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record
|
|
||||||
id="action_cx_tower_file_template_export_yaml"
|
|
||||||
model="ir.actions.act_window"
|
|
||||||
>
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_file_template" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record id="action_cx_tower_key_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_key" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record id="action_cx_tower_os_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_os" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_plan_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.plan.view.form</field>
|
|
||||||
<field name="model">cx.tower.plan</field>
|
|
||||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_plan_view_form" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="action_cx_tower_plan_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_plan" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="view_cx_tower_scheduled_task_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.scheduled.task.view.form</field>
|
|
||||||
<field name="model">cx.tower.scheduled.task</field>
|
|
||||||
<field
|
|
||||||
name="inherit_id"
|
|
||||||
ref="cetmix_tower_server.view_cx_tower_scheduled_task_view_form"
|
|
||||||
/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record
|
|
||||||
id="action_cx_tower_scheduled_task_export_yaml"
|
|
||||||
model="ir.actions.act_window"
|
|
||||||
>
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_scheduled_task" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_server_template_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.server.template.yaml.view.form</field>
|
|
||||||
<field name="model">cx.tower.server.template</field>
|
|
||||||
<field
|
|
||||||
name="inherit_id"
|
|
||||||
ref="cetmix_tower_server.cx_tower_server_template_view_form"
|
|
||||||
/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record
|
|
||||||
id="action_cx_tower_server_template_export_yaml"
|
|
||||||
model="ir.actions.act_window"
|
|
||||||
>
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_server_template" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_server_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.server.yaml.view.form</field>
|
|
||||||
<field name="model">cx.tower.server</field>
|
|
||||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_server_view_form" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="action_cx_tower_server_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_server" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_shortcut_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.shortcut.view.form</field>
|
|
||||||
<field name="model">cx.tower.shortcut</field>
|
|
||||||
<field
|
|
||||||
name="inherit_id"
|
|
||||||
ref="cetmix_tower_server.cx_tower_shortcut_view_form"
|
|
||||||
/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page name="yaml" string="YAML">
|
|
||||||
<div groups="!cetmix_tower_yaml.group_export">
|
|
||||||
<h3
|
|
||||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="object"
|
|
||||||
groups="cetmix_tower_yaml.group_export"
|
|
||||||
class="oe_highlight"
|
|
||||||
name="action_open_yaml_export_wizard"
|
|
||||||
string="Export YAML"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_cx_tower_shortcut_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_shortcut" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record id="action_cx_tower_tag_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_tag" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record
|
|
||||||
id="action_cx_tower_variable_value_export_yaml"
|
|
||||||
model="ir.actions.act_window"
|
|
||||||
>
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_variable_value" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
<record id="action_cx_tower_variable_export_yaml" model="ir.actions.act_window">
|
|
||||||
<field name="name">Export YAML</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="binding_model_id" ref="model_cx_tower_variable" />
|
|
||||||
<field name="binding_view_types">list</field>
|
|
||||||
<field name="groups_id" eval="[(4, ref('cetmix_tower_yaml.group_export'))]" />
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<odoo>
|
|
||||||
<record id="view_yaml_manifest_author_tree" model="ir.ui.view">
|
|
||||||
<field name="name">yaml.manifest.author.tree</field>
|
|
||||||
<field name="model">cx.tower.yaml.manifest.author</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<tree>
|
|
||||||
<field name="name" />
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_yaml_manifest_author_form" model="ir.ui.view">
|
|
||||||
<field name="name">yaml.manifest.author.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.manifest.author</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<sheet>
|
|
||||||
<group>
|
|
||||||
<field name="name" />
|
|
||||||
</group>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_yaml_manifest_author" model="ir.actions.act_window">
|
|
||||||
<field name="name">YAML Manifest Authors</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.manifest.author</field>
|
|
||||||
<field name="view_mode">tree,form</field>
|
|
||||||
<field name="target">current</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<odoo>
|
|
||||||
<record id="view_yaml_manifest_template_tree" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.manifest.tmpl.tree</field>
|
|
||||||
<field name="model">cx.tower.yaml.manifest.tmpl</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<tree>
|
|
||||||
<field name="name" />
|
|
||||||
<field name="file_prefix" />
|
|
||||||
<field name="author_ids" widget="many2many_tags" />
|
|
||||||
<field name="version" />
|
|
||||||
<field name="website" />
|
|
||||||
<field name="license" />
|
|
||||||
<field name="currency" />
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="view_yaml_manifest_template_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.manifest.tmpl.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.manifest.tmpl</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<sheet>
|
|
||||||
<group>
|
|
||||||
<field name="name" />
|
|
||||||
<field name="file_prefix" />
|
|
||||||
<field name="author_ids" widget="many2many_tags" />
|
|
||||||
<field name="version" />
|
|
||||||
<field name="website" />
|
|
||||||
<field name="license" />
|
|
||||||
<field
|
|
||||||
name="license_text"
|
|
||||||
attrs="{'invisible': [('license', '!=', 'custom')]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="currency"
|
|
||||||
attrs="{'invisible': [('license', '!=', 'custom')]}"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="action_yaml_manifest_template" model="ir.actions.act_window">
|
|
||||||
<field name="name">YAML Manifest Templates</field>
|
|
||||||
<field name="type">ir.actions.act_window</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.manifest.tmpl</field>
|
|
||||||
<field name="view_mode">tree,form</field>
|
|
||||||
<field name="view_id" ref="view_yaml_manifest_template_tree" />
|
|
||||||
<field name="target">current</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<odoo>
|
|
||||||
<!-- Import YAML -> Tools -->
|
|
||||||
<menuitem
|
|
||||||
id="menu_cetmix_tower_yaml_import"
|
|
||||||
name="Import YAML"
|
|
||||||
parent="cetmix_tower_server.menu_tools"
|
|
||||||
sequence="10"
|
|
||||||
groups="group_import"
|
|
||||||
action="action_cx_tower_yaml_import_wiz_upload"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- YAML Manifest Settings -> Settings -->
|
|
||||||
<menuitem
|
|
||||||
id="menu_yaml_settings_root"
|
|
||||||
name="YAML Export/Import"
|
|
||||||
parent="cetmix_tower_server.menu_settings"
|
|
||||||
sequence="60"
|
|
||||||
/>
|
|
||||||
<menuitem
|
|
||||||
id="menu_yaml_manifest_author_action"
|
|
||||||
name="Manifest Authors"
|
|
||||||
parent="menu_yaml_settings_root"
|
|
||||||
action="action_yaml_manifest_author"
|
|
||||||
sequence="1"
|
|
||||||
/>
|
|
||||||
<menuitem
|
|
||||||
id="menu_yaml_manifest_template"
|
|
||||||
name="Manifest Templates"
|
|
||||||
parent="menu_yaml_settings_root"
|
|
||||||
action="action_yaml_manifest_template"
|
|
||||||
sequence="2"
|
|
||||||
/>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from . import cx_tower_yaml_export_wiz
|
|
||||||
from . import cx_tower_yaml_export_wiz_download
|
|
||||||
from . import cx_tower_yaml_import_wiz
|
|
||||||
from . import cx_tower_yaml_import_wiz_upload
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
import base64
|
|
||||||
import re
|
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
|
|
||||||
from ..models.cx_tower_yaml_mixin import YamlExportCollector
|
|
||||||
|
|
||||||
FILE_HEADER = """
|
|
||||||
# This file is generated with Cetmix Tower.
|
|
||||||
# Details and documentation: https://cetmix.com/tower
|
|
||||||
"""
|
|
||||||
|
|
||||||
CLEAN_STR = re.compile(r"[^a-z0-9_]")
|
|
||||||
|
|
||||||
|
|
||||||
class CxTowerYamlExportWiz(models.TransientModel):
|
|
||||||
"""Cetmix Tower YAML Export Wizard"""
|
|
||||||
|
|
||||||
_name = "cx.tower.yaml.export.wiz"
|
|
||||||
_description = "Cetmix Tower YAML Export Wizard"
|
|
||||||
|
|
||||||
yaml_code = fields.Text()
|
|
||||||
yaml_file_name = fields.Char(
|
|
||||||
string="YAML File Name",
|
|
||||||
size=255,
|
|
||||||
default=lambda self: self._default_yaml_file_name(),
|
|
||||||
help="Snippet file name without extension, eg 'my_snippet'",
|
|
||||||
)
|
|
||||||
|
|
||||||
explode_child_records = fields.Boolean(
|
|
||||||
default=True,
|
|
||||||
help="Add entire child record definitions to the exported YAML file. "
|
|
||||||
"Otherwise only references to child records will be added.",
|
|
||||||
)
|
|
||||||
remove_empty_values = fields.Boolean(
|
|
||||||
string="Remove Empty x2m Field Values",
|
|
||||||
default=True,
|
|
||||||
help="Remove empty Many2one, Many2many and One2many"
|
|
||||||
" field values from the exported YAML file.",
|
|
||||||
)
|
|
||||||
preview_code = fields.Boolean()
|
|
||||||
add_manifest = fields.Boolean()
|
|
||||||
|
|
||||||
MANIFEST_FIELDS = [
|
|
||||||
"manifest_template_id",
|
|
||||||
"manifest_name",
|
|
||||||
"manifest_author_ids",
|
|
||||||
"manifest_version",
|
|
||||||
"manifest_summary",
|
|
||||||
"manifest_description",
|
|
||||||
"manifest_website",
|
|
||||||
"manifest_license",
|
|
||||||
"manifest_license_text",
|
|
||||||
"manifest_currency",
|
|
||||||
"manifest_price",
|
|
||||||
]
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_manifest_license_selection(self):
|
|
||||||
return self.env["cx.tower.yaml.manifest.tmpl"]._selection_license()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_manifest_currency_selection(self):
|
|
||||||
return self.env["cx.tower.yaml.manifest.tmpl"]._selection_currency()
|
|
||||||
|
|
||||||
manifest_template_id = fields.Many2one(
|
|
||||||
"cx.tower.yaml.manifest.tmpl",
|
|
||||||
)
|
|
||||||
manifest_name = fields.Char(
|
|
||||||
compute="_compute_manifest",
|
|
||||||
readonly=False,
|
|
||||||
store=True,
|
|
||||||
string="Snippet Name",
|
|
||||||
help="Leave this field blank if you don't want to create a manifest",
|
|
||||||
)
|
|
||||||
manifest_website = fields.Char(
|
|
||||||
compute="_compute_manifest",
|
|
||||||
readonly=False,
|
|
||||||
string="Website",
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
manifest_license = fields.Selection(
|
|
||||||
selection="_get_manifest_license_selection",
|
|
||||||
compute="_compute_manifest",
|
|
||||||
readonly=False,
|
|
||||||
string="License",
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
manifest_author_ids = fields.Many2many(
|
|
||||||
"cx.tower.yaml.manifest.author",
|
|
||||||
compute="_compute_manifest",
|
|
||||||
readonly=False,
|
|
||||||
string="Authors",
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
manifest_license_text = fields.Text(
|
|
||||||
compute="_compute_manifest", readonly=False, string="License Text", store=True
|
|
||||||
)
|
|
||||||
manifest_currency = fields.Selection(
|
|
||||||
selection="_get_manifest_currency_selection",
|
|
||||||
compute="_compute_manifest",
|
|
||||||
string="Currency",
|
|
||||||
readonly=False,
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
manifest_summary = fields.Char(
|
|
||||||
string="Summary",
|
|
||||||
size=160,
|
|
||||||
help="Short summary that includes core information. 160 symbols max",
|
|
||||||
)
|
|
||||||
manifest_description = fields.Text("Description")
|
|
||||||
manifest_price = fields.Float("Price")
|
|
||||||
|
|
||||||
manifest_version = fields.Char(
|
|
||||||
compute="_compute_manifest",
|
|
||||||
readonly=False,
|
|
||||||
store=True,
|
|
||||||
string="Version",
|
|
||||||
help="Use the Major.Minor.Patch format, e.g. 1.2.3",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _clean_yaml_basename(self, name: str) -> str:
|
|
||||||
"""
|
|
||||||
Return *always-valid* basename (no extension) built from arbitrary *name*.
|
|
||||||
"""
|
|
||||||
raw = (name or "").strip().lower()
|
|
||||||
base = raw[:-5] if raw.endswith(".yaml") else raw
|
|
||||||
base = CLEAN_STR.sub("_", base)
|
|
||||||
base = re.sub(r"_+", "_", base).strip("_") or "snippet"
|
|
||||||
return base
|
|
||||||
|
|
||||||
def _default_yaml_file_name(self):
|
|
||||||
"""
|
|
||||||
Build the *initial* file name shown to the user.
|
|
||||||
Pattern: <model>_<reference>, without “.yaml” suffix.
|
|
||||||
"""
|
|
||||||
records = self._get_model_record()
|
|
||||||
prefix = records._name.replace("cx.tower.", "").replace(".", "_")
|
|
||||||
ref = records.reference if len(records) == 1 else "selected"
|
|
||||||
return f"{prefix}_{ref}"
|
|
||||||
|
|
||||||
@api.depends("manifest_template_id")
|
|
||||||
def _compute_manifest(self):
|
|
||||||
mapping = {
|
|
||||||
"manifest_author_ids": "author_ids",
|
|
||||||
"manifest_website": "website",
|
|
||||||
"manifest_license": "license",
|
|
||||||
"manifest_license_text": "license_text",
|
|
||||||
"manifest_currency": "currency",
|
|
||||||
"manifest_version": "version",
|
|
||||||
}
|
|
||||||
for rec in self:
|
|
||||||
tmpl = rec.manifest_template_id
|
|
||||||
if not tmpl:
|
|
||||||
continue
|
|
||||||
for wiz_field, tmpl_field in mapping.items():
|
|
||||||
if not rec[wiz_field]:
|
|
||||||
rec[wiz_field] = tmpl[tmpl_field]
|
|
||||||
|
|
||||||
# prepend template's file prefix to YAML file name
|
|
||||||
prefix = (tmpl.file_prefix or "").strip()
|
|
||||||
if prefix:
|
|
||||||
# sanitize prefix without defaulting to a placeholder like "snippet"
|
|
||||||
raw = prefix.lower()
|
|
||||||
sanitized_prefix = re.sub(r"_+", "_", CLEAN_STR.sub("_", raw)).strip(
|
|
||||||
"_"
|
|
||||||
)
|
|
||||||
if sanitized_prefix:
|
|
||||||
# use current or default base name, then clean it
|
|
||||||
current = rec.yaml_file_name or rec._default_yaml_file_name()
|
|
||||||
base = rec._clean_yaml_basename(current)
|
|
||||||
# avoid double-prefixing
|
|
||||||
if not base.startswith(f"{sanitized_prefix}_"):
|
|
||||||
rec.yaml_file_name = rec._clean_yaml_basename(
|
|
||||||
f"{sanitized_prefix}_{base}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.onchange("manifest_license")
|
|
||||||
def _onchange_manifest_license(self):
|
|
||||||
"""Drop price and currency when user switches off the 'custom' license.
|
|
||||||
|
|
||||||
If manifest_license != 'custom', reset manifest_price to 0.0 and
|
|
||||||
manifest_currency to False so they won’t appear in the generated YAML.
|
|
||||||
"""
|
|
||||||
for rec in self:
|
|
||||||
if rec.manifest_license != "custom":
|
|
||||||
rec.manifest_price = 0.0
|
|
||||||
rec.manifest_currency = False
|
|
||||||
|
|
||||||
@api.onchange("explode_child_records", "remove_empty_values", *MANIFEST_FIELDS)
|
|
||||||
def onchange_explode_child_records(self):
|
|
||||||
"""Compute YAML code and file content."""
|
|
||||||
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
# Get model records
|
|
||||||
records = self._get_model_record()
|
|
||||||
if not records:
|
|
||||||
raise ValidationError(_("No valid records selected"))
|
|
||||||
|
|
||||||
explode_related_record = self.explode_child_records
|
|
||||||
remove_empty_values = self.remove_empty_values
|
|
||||||
|
|
||||||
# Prepare YAML header
|
|
||||||
yaml_header = FILE_HEADER.rstrip("\n")
|
|
||||||
# Use the YAML export collector for unique records
|
|
||||||
collector = YamlExportCollector()
|
|
||||||
record_list = []
|
|
||||||
for rec in records:
|
|
||||||
record_yaml_dict = rec.with_context(
|
|
||||||
explode_related_record=explode_related_record,
|
|
||||||
remove_empty_values=remove_empty_values,
|
|
||||||
yaml_collector=collector,
|
|
||||||
)._prepare_record_for_yaml()
|
|
||||||
|
|
||||||
if not record_yaml_dict:
|
|
||||||
continue
|
|
||||||
if isinstance(record_yaml_dict, dict) and list(record_yaml_dict) == [
|
|
||||||
"reference"
|
|
||||||
]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "cetmix_tower_model" not in record_yaml_dict:
|
|
||||||
record_yaml_dict["cetmix_tower_model"] = rec._name.replace(
|
|
||||||
"cx.tower.", ""
|
|
||||||
).replace(".", "_")
|
|
||||||
|
|
||||||
record_list.append(record_yaml_dict)
|
|
||||||
|
|
||||||
if not record_list:
|
|
||||||
self.yaml_code = f"{yaml_header}\n"
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.manifest_name:
|
|
||||||
manifest = {}
|
|
||||||
else:
|
|
||||||
lic = (self.manifest_license or "").lower()
|
|
||||||
|
|
||||||
fields_order = [
|
|
||||||
("name", self.manifest_name),
|
|
||||||
("summary", self.manifest_summary),
|
|
||||||
("description", self.manifest_description),
|
|
||||||
("author", self.manifest_author_ids.mapped("name")),
|
|
||||||
("version", self.manifest_version),
|
|
||||||
("website", self.manifest_website),
|
|
||||||
("license", self.manifest_license),
|
|
||||||
(
|
|
||||||
"license_text",
|
|
||||||
(self.manifest_license_text or "").strip()
|
|
||||||
if lic == "custom"
|
|
||||||
else None,
|
|
||||||
),
|
|
||||||
("price", self.manifest_price),
|
|
||||||
(
|
|
||||||
"currency",
|
|
||||||
self.manifest_currency if lic == "custom" else None,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
manifest = {k: v for k, v in fields_order if v not in (False, None, "", [])}
|
|
||||||
|
|
||||||
result_dict = {
|
|
||||||
"cetmix_tower_yaml_version": self.env[
|
|
||||||
"cx.tower.yaml.mixin"
|
|
||||||
].CETMIX_TOWER_YAML_VERSION,
|
|
||||||
}
|
|
||||||
if manifest:
|
|
||||||
result_dict["manifest"] = manifest
|
|
||||||
result_dict["records"] = record_list
|
|
||||||
|
|
||||||
self.yaml_code = f"{yaml_header}\n{records._convert_dict_to_yaml(result_dict)}"
|
|
||||||
|
|
||||||
@api.onchange("yaml_file_name")
|
|
||||||
def _onchange_yaml_file_name(self):
|
|
||||||
"""
|
|
||||||
Live-clean the YAML file name as the user types:
|
|
||||||
- lowercase, trim whitespace
|
|
||||||
- replace invalid characters with “_”
|
|
||||||
- collapse repeated underscores
|
|
||||||
- ensure a single “.yaml” suffix
|
|
||||||
"""
|
|
||||||
for rec in self:
|
|
||||||
rec.yaml_file_name = rec._clean_yaml_basename(rec.yaml_file_name)
|
|
||||||
|
|
||||||
@api.constrains("manifest_version")
|
|
||||||
def _check_manifest_version_format(self):
|
|
||||||
"""
|
|
||||||
Ensure the user types a semantic version (x.y.z) in the wizard itself.
|
|
||||||
"""
|
|
||||||
semver = re.compile(r"^\d+\.\d+\.\d+$")
|
|
||||||
for rec in self:
|
|
||||||
if rec.manifest_version and not semver.match(rec.manifest_version):
|
|
||||||
raise ValidationError(
|
|
||||||
_("Version must be in format Major.Minor.Patch, e.g. 1.2.3")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _validate_manifest(self):
|
|
||||||
"""Logical cross-checks before saving YAML."""
|
|
||||||
if self.manifest_price and not self.manifest_currency:
|
|
||||||
raise ValidationError(_("Currency is required when price is specified"))
|
|
||||||
if (self.manifest_license or "").lower() == "custom" and not (
|
|
||||||
self.manifest_license_text or ""
|
|
||||||
).strip():
|
|
||||||
raise ValidationError(_("License text is required for a custom license"))
|
|
||||||
|
|
||||||
def write(self, vals):
|
|
||||||
"""
|
|
||||||
Override write to always sanitize `yaml_file_name`
|
|
||||||
before persisting, making programmatic assignments safe.
|
|
||||||
"""
|
|
||||||
if "yaml_file_name" in vals:
|
|
||||||
vals["yaml_file_name"] = self._clean_yaml_basename(vals["yaml_file_name"])
|
|
||||||
return super().write(vals)
|
|
||||||
|
|
||||||
def action_generate_yaml_file(self):
|
|
||||||
"""Save YAML file"""
|
|
||||||
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
self._validate_manifest()
|
|
||||||
if not self.yaml_code:
|
|
||||||
raise ValidationError(_("No YAML code is present."))
|
|
||||||
|
|
||||||
# Generate YAML file
|
|
||||||
try:
|
|
||||||
yaml_file = base64.encodebytes(self.yaml_code.encode("utf-8"))
|
|
||||||
yaml_file_name = (
|
|
||||||
f"{self.yaml_file_name or self._default_yaml_file_name()}.yaml"
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"Failed to encode YAML content. Please ensure all characters are UTF-8 compatible." # noqa: E501
|
|
||||||
)
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
download_wizard = self.env["cx.tower.yaml.export.wiz.download"].create(
|
|
||||||
{
|
|
||||||
"yaml_file": yaml_file,
|
|
||||||
"yaml_file_name": yaml_file_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"res_model": "cx.tower.yaml.export.wiz.download",
|
|
||||||
"res_id": download_wizard.id,
|
|
||||||
"target": "new",
|
|
||||||
"view_mode": "form",
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_model_record(self):
|
|
||||||
"""Get model records based on context values
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: in case no model or records selected
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ModelRecords: a recordset of selected records
|
|
||||||
"""
|
|
||||||
model_name = self.env.context.get("active_model")
|
|
||||||
record_ids = self.env.context.get("active_ids")
|
|
||||||
if not model_name or not record_ids:
|
|
||||||
raise ValidationError(_("No model or records selected"))
|
|
||||||
return self.env[model_name].browse(record_ids)
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_yaml_export_wiz_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.export.wiz.view.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.export.wiz</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="yaml_file_name" placeholder="my_snippet.yaml" />
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<group>
|
|
||||||
<field name="explode_child_records" />
|
|
||||||
<field name="remove_empty_values" />
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="add_manifest" />
|
|
||||||
<field name="preview_code" />
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group
|
|
||||||
string="Manifest"
|
|
||||||
attrs="{'invisible': [('add_manifest','=',False)]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="manifest_template_id"
|
|
||||||
placeholder="Select a pre-defined template"
|
|
||||||
help="Select a template to auto-populate manifest fields"
|
|
||||||
/>
|
|
||||||
<group string="Information">
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="manifest_name"
|
|
||||||
attrs="{'required': [('add_manifest','!=',False)]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_summary"
|
|
||||||
attrs="{'required': [('manifest_name','!=',False)]}"
|
|
||||||
placeholder="Short summary, 160 symbols max"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_author_ids"
|
|
||||||
widget="many2many_tags"
|
|
||||||
attrs="{'required': [('manifest_name','!=',False)]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_version"
|
|
||||||
placeholder="Use the Major.Minor.Patch format, e.g. 1.2.3"
|
|
||||||
/>
|
|
||||||
<field name="manifest_website" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group string="License and pricing">
|
|
||||||
<field
|
|
||||||
name="manifest_license"
|
|
||||||
attrs="{'required': [('manifest_name','!=',False)]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_price"
|
|
||||||
attrs="{'invisible': [('manifest_license', '!=', 'custom')]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_currency"
|
|
||||||
attrs="{'invisible': [('manifest_price', '=', 0)]}"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<notebook>
|
|
||||||
|
|
||||||
<page
|
|
||||||
string="Description"
|
|
||||||
attrs="{'invisible': [('add_manifest','=',False)]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="manifest_description"
|
|
||||||
widget="text"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
placeholder="Detailed description (optional)"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<page
|
|
||||||
string="License text"
|
|
||||||
attrs="{'invisible': [('manifest_license', '!=', 'custom')]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="manifest_license_text"
|
|
||||||
widget="text"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
placeholder="License text"
|
|
||||||
attrs="{'required': [('manifest_license', '=', 'custom')]}"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<page
|
|
||||||
string="Preview code"
|
|
||||||
attrs="{'invisible': [('preview_code','=',False)]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="yaml_code"
|
|
||||||
widget="ace"
|
|
||||||
options="{'mode': 'yaml'}"
|
|
||||||
force_save="1"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
readonly="1"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
</notebook>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<button
|
|
||||||
string="Generate YAML file"
|
|
||||||
type="object"
|
|
||||||
name="action_generate_yaml_file"
|
|
||||||
class="oe_highlight"
|
|
||||||
/>
|
|
||||||
<button string="Close" special="cancel" />
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
from odoo import fields, models
|
|
||||||
|
|
||||||
|
|
||||||
class CxTowerYamlExportWizDownload(models.TransientModel):
|
|
||||||
_name = "cx.tower.yaml.export.wiz.download"
|
|
||||||
_description = "Cetmix Tower YAML Export File Download"
|
|
||||||
|
|
||||||
yaml_file = fields.Binary(readonly=True, attachment=False)
|
|
||||||
yaml_file_name = fields.Char(readonly=True)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_yaml_export_wiz_download_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.export.wiz.download.view.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.export.wiz.download</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="yaml_file" filename="yaml_file_name" />
|
|
||||||
<field name="yaml_file_name" invisible="1" />
|
|
||||||
</group>
|
|
||||||
<footer>
|
|
||||||
<button string="Close" special="cancel" class="oe_highlight" />
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
# Copyright (C) 2024 Cetmix OÜ
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from markupsafe import escape
|
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CxTowerYamlImportWiz(models.TransientModel):
|
|
||||||
"""
|
|
||||||
Process YAML data and create records in Odoo.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_name = "cx.tower.yaml.import.wiz"
|
|
||||||
_description = "Cetmix Tower YAML Import Wizard"
|
|
||||||
|
|
||||||
yaml_code = fields.Text(readonly=True)
|
|
||||||
model_names = fields.Char(readonly=True, help="Models to create records in")
|
|
||||||
if_record_exists = fields.Selection(
|
|
||||||
selection=[
|
|
||||||
("skip", "Skip record"),
|
|
||||||
("update", "Update existing record"),
|
|
||||||
("create", "Create a new record"),
|
|
||||||
],
|
|
||||||
default="skip",
|
|
||||||
required=True,
|
|
||||||
help="What to do if record with the same reference already exists",
|
|
||||||
)
|
|
||||||
secret_list = fields.Html(
|
|
||||||
help="List of secrets present in the YAML file (formatted as HTML list)",
|
|
||||||
compute="_compute_secret_list",
|
|
||||||
)
|
|
||||||
preview_code = fields.Boolean(
|
|
||||||
help="Toggle to show or hide YAML code preview",
|
|
||||||
)
|
|
||||||
manifest_name = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Snippet Name"
|
|
||||||
)
|
|
||||||
manifest_summary = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Summary"
|
|
||||||
)
|
|
||||||
manifest_description = fields.Text(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Description"
|
|
||||||
)
|
|
||||||
manifest_author_string = fields.Char(
|
|
||||||
readonly=True,
|
|
||||||
compute="_compute_yaml_data",
|
|
||||||
help="Comma-separated list",
|
|
||||||
string="Author",
|
|
||||||
)
|
|
||||||
manifest_version = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Version"
|
|
||||||
)
|
|
||||||
manifest_website = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Website"
|
|
||||||
)
|
|
||||||
manifest_license = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="License"
|
|
||||||
)
|
|
||||||
manifest_license_text = fields.Text(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="License text"
|
|
||||||
)
|
|
||||||
manifest_price = fields.Float(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Price"
|
|
||||||
)
|
|
||||||
manifest_currency = fields.Char(
|
|
||||||
readonly=True, compute="_compute_yaml_data", string="Currency"
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.depends("yaml_code")
|
|
||||||
def _compute_secret_list(self):
|
|
||||||
"""Compute list of secrets present in the YAML file"""
|
|
||||||
for record in self:
|
|
||||||
yaml_data = yaml.safe_load(record.yaml_code or "{}")
|
|
||||||
secret_list = self._extract_secret_names(yaml_data)
|
|
||||||
if not secret_list:
|
|
||||||
record.secret_list = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Build deterministic HTML list of secrets
|
|
||||||
items = "".join(f"<li>{escape(name)}</li>" for name in sorted(secret_list))
|
|
||||||
secrets_html = f"<ul>{items}</ul>"
|
|
||||||
|
|
||||||
record.secret_list = _(
|
|
||||||
"Following secrets are used in the code:<br/>%(secrets)s",
|
|
||||||
secrets=secrets_html,
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.depends("yaml_code")
|
|
||||||
def _compute_yaml_data(self):
|
|
||||||
for record in self:
|
|
||||||
data = yaml.safe_load(record.yaml_code or "{}")
|
|
||||||
|
|
||||||
manifest = data.get("manifest", {}) if isinstance(data, dict) else {}
|
|
||||||
authors = manifest.get("author")
|
|
||||||
if isinstance(authors, list | tuple):
|
|
||||||
manifest_author_string = ", ".join(authors)
|
|
||||||
elif isinstance(authors, str):
|
|
||||||
manifest_author_string = authors
|
|
||||||
else:
|
|
||||||
manifest_author_string = False
|
|
||||||
|
|
||||||
record.update(
|
|
||||||
{
|
|
||||||
"manifest_name": manifest.get("name"),
|
|
||||||
"manifest_summary": manifest.get("summary"),
|
|
||||||
"manifest_description": manifest.get("description"),
|
|
||||||
"manifest_author_string": manifest_author_string,
|
|
||||||
"manifest_version": manifest.get("version"),
|
|
||||||
"manifest_website": manifest.get("website"),
|
|
||||||
"manifest_license": manifest.get("license"),
|
|
||||||
"manifest_license_text": manifest.get("license_text"),
|
|
||||||
"manifest_price": manifest.get("price"),
|
|
||||||
"manifest_currency": manifest.get("currency"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_import_yaml(self):
|
|
||||||
"""Process YAML data and create records in Odoo"""
|
|
||||||
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
# Parse YAML code
|
|
||||||
yaml_data = yaml.safe_load(self.yaml_code)
|
|
||||||
records = yaml_data.get("records")
|
|
||||||
if not records:
|
|
||||||
raise ValidationError(_("YAML file doesn't contain any records"))
|
|
||||||
|
|
||||||
# Cache models
|
|
||||||
model_cache = {}
|
|
||||||
odoo_record_ids = []
|
|
||||||
|
|
||||||
# Process each record
|
|
||||||
for record in records:
|
|
||||||
record_reference = record.get("reference")
|
|
||||||
if not record_reference:
|
|
||||||
raise ValidationError(_("Record reference is missing"))
|
|
||||||
model_name = record.get("cetmix_tower_model")
|
|
||||||
if not model_name:
|
|
||||||
raise ValidationError(
|
|
||||||
_("Record model is missing for record %s", record_reference)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get model from cache or create new one
|
|
||||||
model = model_cache.get(model_name)
|
|
||||||
if not model:
|
|
||||||
model = self.env[
|
|
||||||
f"cx.tower.{model_name.replace('_', '.')}"
|
|
||||||
].with_context(skip_ssh_settings_check=(model_name == "server"))
|
|
||||||
model_cache[model_name] = model
|
|
||||||
|
|
||||||
# Get existing record by reference
|
|
||||||
# NOTE: we don't validate models here because they are
|
|
||||||
# already validated in the file upload wizard.
|
|
||||||
odoo_record = model.get_by_reference(record_reference)
|
|
||||||
|
|
||||||
# Skip
|
|
||||||
if self.if_record_exists == "skip" and odoo_record:
|
|
||||||
_logger.info(
|
|
||||||
"Skipping record '%s' in model '%s'" " because it already exists",
|
|
||||||
record_reference,
|
|
||||||
model_name,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Update existing record
|
|
||||||
elif self.if_record_exists == "update" and odoo_record:
|
|
||||||
try:
|
|
||||||
record_values = model.with_context(
|
|
||||||
force_create_related_record=False,
|
|
||||||
)._post_process_yaml_dict_values(record)
|
|
||||||
odoo_record.with_context(
|
|
||||||
from_yaml=True,
|
|
||||||
).write(record_values)
|
|
||||||
odoo_record_ids.append(odoo_record.id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"Error updating record %(reference)s: %(error)s",
|
|
||||||
reference=record_reference,
|
|
||||||
error=e,
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
_logger.info(
|
|
||||||
f"Updated record '{record_reference}' in model '{model_name}'"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Or create a new record
|
|
||||||
record_values = model.with_context(
|
|
||||||
force_create_related_record=self.if_record_exists == "create",
|
|
||||||
)._post_process_yaml_dict_values(record)
|
|
||||||
try:
|
|
||||||
odoo_record = model.with_context(
|
|
||||||
from_yaml=True,
|
|
||||||
).create(record_values)
|
|
||||||
odoo_record_ids.append(odoo_record.id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"Error creating record '%(reference)s' in model"
|
|
||||||
" '%(model)s': %(error)s",
|
|
||||||
reference=record_reference,
|
|
||||||
model=model_name,
|
|
||||||
error=e,
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
_logger.info(f"Created record '{record_reference}' in model '{model_name}'")
|
|
||||||
|
|
||||||
# No records were created or updated
|
|
||||||
if not odoo_record_ids:
|
|
||||||
action = {
|
|
||||||
"type": "ir.actions.client",
|
|
||||||
"tag": "display_notification",
|
|
||||||
"params": {
|
|
||||||
"title": _("Record Import"),
|
|
||||||
"message": _("No records were created or updated"),
|
|
||||||
"sticky": True,
|
|
||||||
"type": "warning",
|
|
||||||
"next": {"type": "ir.actions.act_window_close"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# All records from the same model
|
|
||||||
elif len(model_cache) == 1:
|
|
||||||
model = list(model_cache.values())[0]
|
|
||||||
action = {
|
|
||||||
"name": _("Import result: %(model)s", model=model._description),
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"res_model": model._name,
|
|
||||||
"target": "current",
|
|
||||||
"domain": [("id", "in", odoo_record_ids)],
|
|
||||||
}
|
|
||||||
if len(odoo_record_ids) == 1:
|
|
||||||
# Open single record in form view
|
|
||||||
action["res_id"] = odoo_record_ids[0]
|
|
||||||
action["view_mode"] = "form"
|
|
||||||
else:
|
|
||||||
# Open list view of all records
|
|
||||||
action["view_mode"] = "list,form"
|
|
||||||
|
|
||||||
# Records from different models
|
|
||||||
else:
|
|
||||||
model_names = ", ".join(
|
|
||||||
f"'{model._description}'" for model in model_cache.values()
|
|
||||||
)
|
|
||||||
action = {
|
|
||||||
"type": "ir.actions.client",
|
|
||||||
"tag": "display_notification",
|
|
||||||
"params": {
|
|
||||||
"title": _("Record Import"),
|
|
||||||
"message": _(
|
|
||||||
"Records of the following models were created "
|
|
||||||
"or updated: %(models)s",
|
|
||||||
models=model_names,
|
|
||||||
),
|
|
||||||
"sticky": True,
|
|
||||||
"type": "success",
|
|
||||||
"next": {"type": "ir.actions.act_window_close"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return action
|
|
||||||
|
|
||||||
def _extract_secret_names(self, data: dict) -> list:
|
|
||||||
"""Extract names of secrets from YAML data.
|
|
||||||
|
|
||||||
Supports both formats:
|
|
||||||
- secret_ids -> [{name: ...}]
|
|
||||||
- secret_ids -> [{key_id: {name: ...}}]
|
|
||||||
"""
|
|
||||||
secret_names = set()
|
|
||||||
|
|
||||||
def _recursive_extract(node):
|
|
||||||
"""Recursively extract secret names from nested structures."""
|
|
||||||
if isinstance(node, dict):
|
|
||||||
if "secret_ids" in node and isinstance(node["secret_ids"], list):
|
|
||||||
for item in node["secret_ids"]:
|
|
||||||
if not isinstance(item, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Format 1: direct name
|
|
||||||
if "name" in item:
|
|
||||||
secret_names.add(item["name"])
|
|
||||||
# Format 2: nested key_id -> name
|
|
||||||
elif (
|
|
||||||
"key_id" in item
|
|
||||||
and isinstance(item["key_id"], dict)
|
|
||||||
and "name" in item["key_id"]
|
|
||||||
):
|
|
||||||
secret_names.add(item["key_id"]["name"])
|
|
||||||
|
|
||||||
# Handle single ssh_key_id
|
|
||||||
if "ssh_key_id" in node and isinstance(node["ssh_key_id"], dict):
|
|
||||||
if "name" in node["ssh_key_id"]:
|
|
||||||
secret_names.add(node["ssh_key_id"]["name"])
|
|
||||||
|
|
||||||
# Recursively process the rest of the dictionary
|
|
||||||
for value in node.values():
|
|
||||||
_recursive_extract(value)
|
|
||||||
|
|
||||||
elif isinstance(node, list):
|
|
||||||
for item in node:
|
|
||||||
_recursive_extract(item)
|
|
||||||
|
|
||||||
_recursive_extract(data)
|
|
||||||
return list(secret_names)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_yaml_import_wiz_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.import.wiz.view.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.import.wiz</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="if_record_exists" />
|
|
||||||
</group>
|
|
||||||
<div
|
|
||||||
class="alert alert-info"
|
|
||||||
role="alert"
|
|
||||||
attrs="{'invisible': [('secret_list', '=', False)]}"
|
|
||||||
>
|
|
||||||
<field name="secret_list" nolabel="1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<group>
|
|
||||||
<field name="preview_code" widget="boolean_toggle" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
attrs="{'invisible': [
|
|
||||||
('manifest_name', '=', False),
|
|
||||||
]}"
|
|
||||||
>
|
|
||||||
<group string="Information">
|
|
||||||
<field name="manifest_name" string="Name" />
|
|
||||||
<field name="manifest_summary" string="Summary" />
|
|
||||||
<field name="manifest_author_string" string="Author" />
|
|
||||||
<field name="manifest_version" string="Version" />
|
|
||||||
<field
|
|
||||||
name="manifest_website"
|
|
||||||
string="Website"
|
|
||||||
attrs="{'invisible': [('manifest_website', '=', False)]}"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
<group string="License and pricing">
|
|
||||||
<field name="manifest_license" string="License" />
|
|
||||||
<field
|
|
||||||
name="manifest_price"
|
|
||||||
string="Price"
|
|
||||||
attrs="{'invisible': [('manifest_price', '=', False)]}"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="manifest_currency"
|
|
||||||
string="Currency"
|
|
||||||
attrs="{'invisible': [('manifest_currency', '=', False)]}"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<notebook>
|
|
||||||
<page
|
|
||||||
string="Description"
|
|
||||||
attrs="{'invisible':[('manifest_description','=',False)]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="manifest_description"
|
|
||||||
widget="text"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<page
|
|
||||||
string="License text"
|
|
||||||
attrs="{'invisible':[('manifest_license_text','=',False)]}"
|
|
||||||
>
|
|
||||||
<field
|
|
||||||
name="manifest_license_text"
|
|
||||||
widget="text"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
/>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<page
|
|
||||||
string="Code preview"
|
|
||||||
attrs="{'invisible': [('preview_code', '=', False)]}"
|
|
||||||
>
|
|
||||||
<group>
|
|
||||||
<field
|
|
||||||
name="yaml_code"
|
|
||||||
widget="ace"
|
|
||||||
options="{'mode': 'yaml'}"
|
|
||||||
force_save="1"
|
|
||||||
nolabel="1"
|
|
||||||
colspan="4"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
</notebook>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
attrs="{'invisible': [('if_record_exists', '!=', 'create')]}"
|
|
||||||
style="margin-bottom:0px;"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<strong
|
|
||||||
>Important:</strong> To maintain data consistency, the following
|
|
||||||
model records will always be updated if they exist in Odoo:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Variables</li>
|
|
||||||
<li>Variable Options</li>
|
|
||||||
<li>Key/Secrets</li>
|
|
||||||
<li>Tags</li>
|
|
||||||
<li>OSs</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
To create new entities instead of updating existing ones, remove or modify
|
|
||||||
the <code
|
|
||||||
>reference</code> field in the YAML code for those entities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
attrs="{'invisible': [('if_record_exists', '!=', 'update')]}"
|
|
||||||
style="margin-bottom:0px;"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Existing record will be updated with the new data. Related records, present in the YAML code, will be updated too.
|
|
||||||
If any of those related records doesn't exist, it will be created automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<button
|
|
||||||
string="Import"
|
|
||||||
type="object"
|
|
||||||
name="action_import_yaml"
|
|
||||||
class="oe_highlight"
|
|
||||||
attrs="{'invisible': [('if_record_exists', '!=', 'update')]}"
|
|
||||||
confirm="This may overwrite existing records. Proceed?"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
string="Import"
|
|
||||||
type="object"
|
|
||||||
name="action_import_yaml"
|
|
||||||
class="oe_highlight"
|
|
||||||
attrs="{'invisible': [('if_record_exists', '=', 'update')]}"
|
|
||||||
/>
|
|
||||||
<button string="Close" special="cancel" />
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import binascii
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from odoo import _, fields, models
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
class CxTowerYamlImportWizUpload(models.TransientModel):
|
|
||||||
"""
|
|
||||||
Upload YAML file and perform initial validation.
|
|
||||||
Submit YAML data to import wizard for further processing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_name = "cx.tower.yaml.import.wiz.upload"
|
|
||||||
_description = "Cetmix Tower YAML Import Wizard Upload"
|
|
||||||
|
|
||||||
file_name = fields.Char()
|
|
||||||
yaml_file = fields.Binary(required=True)
|
|
||||||
|
|
||||||
def action_import_yaml(self):
|
|
||||||
"""Parse YAML data to the import wizard
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Action Window: Action to open the import wizard
|
|
||||||
"""
|
|
||||||
|
|
||||||
decoded_file = self._extract_yaml_data()
|
|
||||||
|
|
||||||
import_wizard = self.env["cx.tower.yaml.import.wiz"].create(
|
|
||||||
{
|
|
||||||
"yaml_code": decoded_file,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"res_model": "cx.tower.yaml.import.wiz",
|
|
||||||
"res_id": import_wizard.id,
|
|
||||||
"view_mode": "form",
|
|
||||||
"target": "new",
|
|
||||||
}
|
|
||||||
|
|
||||||
def _extract_yaml_data(self):
|
|
||||||
"""Extract data from YAML file and validate them
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
decoded_file (Text): YAML code
|
|
||||||
Raises:
|
|
||||||
ValidationError: If the YAML file is invalid
|
|
||||||
or contains unsupported data
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
# Decode base64 file
|
|
||||||
try:
|
|
||||||
raw_bytes = b64decode(self.yaml_file or b"")
|
|
||||||
except (TypeError, binascii.Error) as e:
|
|
||||||
# Not a valid base-64 payload
|
|
||||||
raise ValidationError(_("File is not a valid base64-encoded file")) from e
|
|
||||||
|
|
||||||
if not raw_bytes:
|
|
||||||
raise ValidationError(_("File is empty"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
decoded_file = raw_bytes.decode("utf-8")
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
raise ValidationError(_("YAML file cannot be decoded properly")) from e
|
|
||||||
|
|
||||||
# Parse YAML file
|
|
||||||
try:
|
|
||||||
yaml_data = yaml.safe_load(decoded_file)
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
raise ValidationError(_("Invalid YAML file")) from e
|
|
||||||
|
|
||||||
if not yaml_data or not isinstance(yaml_data, dict):
|
|
||||||
raise ValidationError(_("Yaml file doesn't contain valid data"))
|
|
||||||
|
|
||||||
# Check Cetmix Tower YAML version
|
|
||||||
yaml_version = yaml_data.pop("cetmix_tower_yaml_version", None)
|
|
||||||
supported_version = self.env["cx.tower.yaml.mixin"].CETMIX_TOWER_YAML_VERSION
|
|
||||||
if (
|
|
||||||
yaml_version
|
|
||||||
and isinstance(yaml_version, int)
|
|
||||||
and yaml_version > supported_version
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"YAML version is higher than version"
|
|
||||||
" supported by your Cetmix Tower instance."
|
|
||||||
" %(code_version)s > %(tower_version)s",
|
|
||||||
code_version=yaml_version,
|
|
||||||
tower_version=supported_version,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get records from YAML
|
|
||||||
records = yaml_data.get("records")
|
|
||||||
if not records:
|
|
||||||
raise ValidationError(_("YAML file doesn't contain any records"))
|
|
||||||
|
|
||||||
# Collect and validate all record models
|
|
||||||
ir_model_obj = self.env["ir.model"]
|
|
||||||
unique_models = {}
|
|
||||||
|
|
||||||
# First pass: check all records have models and collect unique models
|
|
||||||
for record in records:
|
|
||||||
record_model = record.get("cetmix_tower_model")
|
|
||||||
if not record_model:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"Record model is missing for record %s",
|
|
||||||
record.get("reference", ""),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if record_model not in unique_models:
|
|
||||||
odoo_model = f"cx.tower.{record_model}".replace("_", ".")
|
|
||||||
unique_models[record_model] = odoo_model
|
|
||||||
|
|
||||||
# Second pass: validate all unique models in a single query
|
|
||||||
odoo_models = list(unique_models.values())
|
|
||||||
valid_models = {
|
|
||||||
model.model: model
|
|
||||||
for model in ir_model_obj.search([("model", "in", odoo_models)])
|
|
||||||
}
|
|
||||||
|
|
||||||
# Third pass: check models exist and support YAML import
|
|
||||||
for record_model, odoo_model in unique_models.items():
|
|
||||||
if odoo_model not in valid_models:
|
|
||||||
raise ValidationError(_("'%s' is not a valid model", record_model))
|
|
||||||
if not hasattr(self.env[odoo_model], "yaml_code"):
|
|
||||||
raise ValidationError(
|
|
||||||
_("Model '%s' does not support YAML import", record_model)
|
|
||||||
)
|
|
||||||
return decoded_file
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="cx_tower_yaml_import_wiz_upload_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">cx.tower.yaml.import.wiz.upload.view.form</field>
|
|
||||||
<field name="model">cx.tower.yaml.import.wiz.upload</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="file_name" invisible="1" />
|
|
||||||
<field
|
|
||||||
name="yaml_file"
|
|
||||||
filename="file_name"
|
|
||||||
options="{'accepted_file_extensions': '.yaml,.yml'}"
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
<footer>
|
|
||||||
<button
|
|
||||||
string="Process"
|
|
||||||
type="object"
|
|
||||||
name="action_import_yaml"
|
|
||||||
class="oe_highlight"
|
|
||||||
attrs="{'invisible': [('yaml_file', '=', False)]}"
|
|
||||||
/>
|
|
||||||
<button string="Close" special="cancel" />
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_cx_tower_yaml_import_wiz_upload" model="ir.actions.act_window">
|
|
||||||
<field name="name">Import YAML</field>
|
|
||||||
<field name="type">ir.actions.act_window</field>
|
|
||||||
<field name="res_model">cx.tower.yaml.import.wiz.upload</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
Reference in New Issue
Block a user