diff --git a/odoo_module_migrate/base_migration_script.py b/odoo_module_migrate/base_migration_script.py
index f42e792c..0ea4a486 100644
--- a/odoo_module_migrate/base_migration_script.py
+++ b/odoo_module_migrate/base_migration_script.py
@@ -208,10 +208,17 @@ def process_file(
replaces.update(self._TEXT_REPLACES.get(extension, {}))
replaces.update(renamed_models.get("replaces"))
replaces.update(removed_models.get("replaces"))
-
new_text = tools._replace_in_file(
absolute_file_path, replaces, "Change file content of %s" % filename
)
+ field_renames = renamed_fields.get("replaces")
+ # To be safe we only rename fields on files associated with the current replaces
+ if field_renames:
+ new_text = tools._replace_field_names(
+ absolute_file_path,
+ field_renames,
+ "Updated field names of %s" % filename,
+ )
# Display errors if the new content contains some obsolete
# pattern
@@ -260,17 +267,34 @@ def handle_renamed_fields(self, removed_fields):
For now this handler is simple but the idea would be to improve it
with deeper analysis and direct replaces if it is possible and secure.
For that analysis model_name could be used
+ It also will add to the replaces key of the returned dictionary a key value pair
+ to be used in _replace_in_file
"""
- res = {}
+ res = {"warnings": {}, "replaces": {}}
+ res["replaces"] = {}
for model_name, old_field_name, new_field_name, more_info in removed_fields:
+ # if model_name in res['replaces']:
+ # res['replaces'][model_name].update({old_field_name: new_field_name,})
+ # else:
+ res["replaces"].update(
+ {
+ model_name: {
+ old_field_name: new_field_name,
+ }
+ }
+ )
msg = "On the model %s, the field %s was renamed to %s.%s" % (
model_name,
old_field_name,
new_field_name,
" %s" % more_info if more_info else "",
)
- res[r"""(['"]{0}['"]|\.{0}[\s,=])""".format(old_field_name)] = msg
- return {"warnings": res}
+ res["warnings"].update(
+ {
+ r"""(['"]{0}['"]|\.{0}[\s,=])""".format(old_field_name): msg,
+ }
+ )
+ return res
def handle_deprecated_modules(self, manifest_path, deprecated_modules):
current_manifest_text = tools._read_content(manifest_path)
diff --git a/odoo_module_migrate/migration_scripts/migrate_130_140.py b/odoo_module_migrate/migration_scripts/migrate_130_140.py
index e98c2e42..a442277d 100644
--- a/odoo_module_migrate/migration_scripts/migrate_130_140.py
+++ b/odoo_module_migrate/migration_scripts/migrate_130_140.py
@@ -142,6 +142,36 @@ def reformat_deprecated_tags(
logger.debug("Reformatted files:\n" f"{list(reformatted_files)}")
+def refactor_action_read(**kwargs):
+ """
+ replace action.read() by _for_xml_id to avoid access rights issue
+
+ ##### case 1: pattern for case action.read[0] right after self.env.ref
+ ## action = self.env.ref('sale.action_orders')
+ ## action = action.read()[0]
+
+ ##### case 2: pattern for case having new line between action.read[0] and self.env.ref
+ ## action = self.env.ref('sale.action_orders')
+ ## .........
+ ## .........
+ ## action = action.read()[0]
+ """
+ logger = kwargs["logger"]
+ tools = kwargs["tools"]
+ module_path = kwargs["module_path"]
+ file_paths = _get_files(module_path, ".py")
+
+ old_term = r"action.*= self.env.ref\((.*)\)((\n.+)+?)?(\n.+)(action\.read\(\)\[0\])"
+ new_term = r'\2\4self.env["ir.actions.act_window"]._for_xml_id(\1)'
+ for file_path in file_paths:
+ logger.debug(f"refactor file {file_path}")
+ tools._replace_in_file(
+ file_path,
+ {old_term: new_term},
+ log_message="refactor action.read[0] to _for_xml_id",
+ )
+
+
_TEXT_REPLACES = {
".js": {
r"tour\.STEPS\.SHOW_APPS_MENU_ITEM": "tour.stepUtils.showAppsMenuItem()",
@@ -155,5 +185,5 @@ def reformat_deprecated_tags(
class MigrationScript(BaseMigrationScript):
- _GLOBAL_FUNCTIONS = [reformat_deprecated_tags]
+ _GLOBAL_FUNCTIONS = [reformat_deprecated_tags, refactor_action_read]
_TEXT_REPLACES = _TEXT_REPLACES
diff --git a/odoo_module_migrate/migration_scripts/migrate_130_allways.py b/odoo_module_migrate/migration_scripts/migrate_130_allways.py
new file mode 100644
index 00000000..a620f58c
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/migrate_130_allways.py
@@ -0,0 +1,83 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+import re
+from odoo_module_migrate.base_migration_script import BaseMigrationScript
+
+
+def multi_value_translation_replacement_function(match, single_quote=True):
+ format_string = match.group(1)
+ dictionary_entries = match.group(2)
+
+ formatted_entries = []
+ for entry in dictionary_entries.split(","):
+ if ":" in entry:
+ [key, value] = entry.split(":")
+ formatted_entries.append(
+ "{}={}".format(key.strip().strip("'").strip('"'), value.strip())
+ )
+
+ formatted_entries = ", ".join(formatted_entries)
+
+ if single_quote:
+ return f"_('{format_string}', {formatted_entries})"
+ return f'_("{format_string}", {formatted_entries})'
+
+
+def format_parenthesis(match):
+ format_string = match.group(1)
+ dictionary_entries = match.group(2)
+
+ if dictionary_entries.endswith(","):
+ dictionary_entries = dictionary_entries[:-1]
+
+ return f"_({format_string}, {dictionary_entries})"
+
+
+def format_replacement_function(match, single_quote=True):
+ format_string = re.sub(r"\{\d*\}", "%s", match.group(1))
+ format_string = re.sub(r"{(\w+)}", r"%(\1)s", format_string)
+ arguments = " ".join(match.group(2).split())
+
+ if arguments.endswith(","):
+ arguments = arguments[:-1]
+
+ if single_quote:
+ return f"_('{format_string}', {arguments})"
+ return f'_("{format_string}", {arguments})'
+
+
+def replace_translation_function(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".py",))
+
+ replaces = {
+ r'_\(\s*"([^"]+)"\s*\)\s*%\s*\{([^}]+)\}': lambda match: multi_value_translation_replacement_function(
+ match, single_quote=False
+ ),
+ r"_\(\s*'([^']+)'\s*\)\s*%\s*\{([^}]+)\}": lambda match: multi_value_translation_replacement_function(
+ match, single_quote=True
+ ),
+ r'_\((["\'].*?%[ds].*?["\'])\)\s*%\s*\(\s*(.+)\s*\)': format_parenthesis,
+ r'_\((["\'].*?%[ds].*?["\'])\)\s*?%\s*?([^\s]+)': r"_(\1, \2)",
+ r'_\(\s*"([^"]*)"\s*\)\.format\(\s*(\s*[^)]+)\)': lambda match: format_replacement_function(
+ match, single_quote=False
+ ),
+ r"_\(\s*'([^']*)'\s*\)\.format\(\s*(\s*[^)]+)\)": lambda match: format_replacement_function(
+ match, single_quote=True
+ ),
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"""Improve _() function: {file}""",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+class MigrationScript(BaseMigrationScript):
+
+ _GLOBAL_FUNCTIONS = [replace_translation_function]
diff --git a/odoo_module_migrate/migration_scripts/migrate_150_allways.py b/odoo_module_migrate/migration_scripts/migrate_150_allways.py
new file mode 100644
index 00000000..3c18eee2
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/migrate_150_allways.py
@@ -0,0 +1,28 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo_module_migrate.base_migration_script import BaseMigrationScript
+
+
+def replace_toggle_button(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml",))
+ replaces = {
+ r'widget="\s*toggle_button\s*"': 'widget="boolean_toggle"',
+ r"widget='\s*toggle_button\s*'": 'widget="boolean_toggle"',
+ r'\s*toggle_button\s*': 'boolean_toggle',
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"Replace toggle_button widget to boolean_toggle widget in file: {file}",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+class MigrationScript(BaseMigrationScript):
+ _GLOBAL_FUNCTIONS = [replace_toggle_button]
diff --git a/odoo_module_migrate/migration_scripts/migrate_170_180.py b/odoo_module_migrate/migration_scripts/migrate_170_180.py
index 930ebf72..df74ba17 100644
--- a/odoo_module_migrate/migration_scripts/migrate_170_180.py
+++ b/odoo_module_migrate/migration_scripts/migrate_170_180.py
@@ -9,7 +9,7 @@
def replace_tree_with_list_in_views(
logger, module_path, module_name, manifest_path, migration_steps, tools
):
- files_to_process = tools.get_files(module_path, (".xml", ".js", ".py"))
+ files_to_process = tools.get_files(module_path, (".xml", ".js", ".py",))
reg_tree_to_list_xml_mode = re.compile(
r"""(]* name=["'](view_mode|name|binding_view_types)["'][^>]*>([^<>]+[,.])?\s*)tree(\s*([,.][^<>]+)?)"""
@@ -84,6 +84,26 @@ def replace_chatter_self_closing(match):
logger.error(f"Error processing file {file}: {str(e)}")
+def replace_deprecated_kanban_box_card_menu(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml", ".js", ".py",))
+ replaces = {
+ "kanban-card": "card",
+ "kanban-box": "card",
+ "kanban-menu": "menu",
+ }
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"""Replace kanban-card and kanban-box with card, also change kanban-menu with menu" in file: {file}""",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
def replace_user_has_groups(
logger, module_path, module_name, manifest_path, migration_steps, tools
):
@@ -100,9 +120,240 @@ def replace_user_has_groups(
logger.error(f"Error processing file {file}: {str(e)}")
+def remove_deprecated_kanban_click_classes(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml",))
+
+ replaces = {
+ "oe_kanban_global_click_edit": "",
+ "oe_kanban_global_click": "",
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"Remove deprecated kanban click classes in file: {file}",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+def replace_kanban_color_picker_widget(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml",))
+
+ replaces = {
+ # Case 1: Match any ul tag containing both oe_kanban_colorpicker class and data-field
+ # Example:
+ # Example:
+ r']*?class="[^"]*?oe_kanban_colorpicker[^"]*?"[^>]*?data-field="([^"]+)"[^>]*?>(?:
)?': r'',
+ # Case 2: Same as Case 1 but with data-field appearing before class
+ # Example:
+ # Example:
+ r']*?data-field="([^"]+)"[^>]*?class="[^"]*?oe_kanban_colorpicker[^"]*?"[^>]*?>(?:
)?': r'',
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"Replace kanban colorpicker with field widget in file: {file}",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+def remove_kanban_tooltip(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml",))
+ reg_tooltip_template = (
+ r"""]*>[\s\S]*?\s*"""
+ )
+ reg_tooltip_attr = r"""\s+tooltip=["']kanban-tooltip["']"""
+
+ replaces = {
+ reg_tooltip_template: "",
+ reg_tooltip_attr: "",
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"Removed kanban tooltip feature in file: {file}",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+def replace_type_edit(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ """Replace type='edit' with type='open' in elements."""
+ files_to_process = tools.get_files(module_path, (".xml",))
+
+ reg_type_edit = r"""type=["']edit["']"""
+
+ replaces = {
+ reg_type_edit: 'type="open"',
+ }
+
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"Replaced type='edit' with type='open' in file: {file}",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+def replace_editable_attribute(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".xml", ".js", ".py",))
+ replaces = {
+ 'editable="1"': 'editable="bottom"',
+ "editable='1'": 'editable="bottom"',
+ r'1': 'bottom',
+ }
+ for file in files_to_process:
+ try:
+ tools._replace_in_file(
+ file,
+ replaces,
+ log_message=f"""Replace editable="1" by "bottom" in file: {file}""",
+ )
+ except Exception as e:
+ logger.error(f"Error processing file {file}: {str(e)}")
+
+
+def replace_slugify(
+ logger, module_path, module_name, manifest_path, migration_steps, tools
+):
+ files_to_process = tools.get_files(module_path, (".py",))
+
+ for file in files_to_process:
+ try:
+ content = tools._read_content(file)
+ content = re.sub(
+ r"from\s+odoo\.addons\.http_routing\.models\.ir_http\s+import\s+slugify\b.*\n",
+ "",
+ content,
+ )
+ # process in controller (*.py) file are using request
+ has_request = "request" in content
+ if has_request:
+ content = re.sub(
+ r"""(?]*?)\s+string="[^"]*": \1
diff --git a/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/invisible_fields.yaml b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/invisible_fields.yaml
new file mode 100644
index 00000000..22f65075
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/invisible_fields.yaml
@@ -0,0 +1,2 @@
+.xml:
+ invisible=(["']1["']|["']True["']|["']true["'])|column_invisible=(["']1["']|["']True["']|["']true["']): "[18] Fields that are required by Python expressions (invisible, column_invisible, readonly, required, context, domain) are now added automatically as invisible and readonly. You can remove these fields from XML views unless they are necessary for specific logic. More details: https://github.com/odoo/odoo/pull/137031."
diff --git a/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_color.yaml b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_color.yaml
new file mode 100644
index 00000000..5cd9e898
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_color.yaml
@@ -0,0 +1,2 @@
+.xml:
+ (?:t-attf-class|class)="[^"]*(?:kanban_getcolor|kanban_color|kanban_getcolorname)\([^"]*"|class="[^"]*oe_kanban_color_\d+[^"]*": "[18.0] Kanban color methods (kanban_getcolor, kanban_color, kanban_getcolorname) and static color classes (oe_kanban_color_X) have been deprecated in favor of the highlight_color attribute on kanban root node. This attribute will properly color the left border of the kanban cards. No need to set color classes or use color methods anymore. These methods will be removed in 18.0. More details: https://github.com/odoo/odoo/pull/167751"
diff --git a/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_image.yaml b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_image.yaml
new file mode 100644
index 00000000..ff92cd58
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/kanban_image.yaml
@@ -0,0 +1,2 @@
+.xml:
+ kanban_image\(['"][^'"]+["']\s*,\s*['"][^'"]+["']\s*,\s*[^)]+\): "[18.0] The kanban_image helper is deprecated in favor of . More details: https://github.com/odoo/odoo/pull/167751/commits/6c5896724abee4cee0a26d0b41de9781c8e51621"
diff --git a/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/translations.yaml b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/translations.yaml
new file mode 100644
index 00000000..ef345337
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/text_warnings/migrate_170_180/translations.yaml
@@ -0,0 +1,2 @@
+.py:
+ from odoo(\.tools(\.translate)?)? import .*_: "[18] When possible, it is better to rely on `Env._`: replace '_('text')' by 'self.env._('text')' and remove '_' from the imports. More details: https://github.com/odoo/odoo/pull/174844"
diff --git a/odoo_module_migrate/migration_scripts/text_warnings/migrate_allways/unnamed_placeholders_translated_string.yaml b/odoo_module_migrate/migration_scripts/text_warnings/migrate_allways/unnamed_placeholders_translated_string.yaml
new file mode 100644
index 00000000..08b82628
--- /dev/null
+++ b/odoo_module_migrate/migration_scripts/text_warnings/migrate_allways/unnamed_placeholders_translated_string.yaml
@@ -0,0 +1,2 @@
+.py:
+ _\(\s*["\'].*?%s.*?["\']: "[Warning] In some languages the order of the placeholders may have to be modified, which is impossible if they are unnamed. We can use named placedholders to avoid this"
diff --git a/odoo_module_migrate/tools.py b/odoo_module_migrate/tools.py
index 7618bcc4..5f560739 100644
--- a/odoo_module_migrate/tools.py
+++ b/odoo_module_migrate/tools.py
@@ -5,11 +5,19 @@
import subprocess
import re
import pathlib
+from lxml import etree
+from dataclasses import fields
from .config import _AVAILABLE_MIGRATION_STEPS
from .log import logger
+CLASS_PATTERN = re.compile(
+ r"(class\s+\w+\s*\(\s*(?:\w+\.)?\w+(?:,\s*(?:\w+\.)?\w+)*\)\s*:\s*(?:\n\s+.*)+)",
+ re.MULTILINE,
+)
+
+
def _get_available_init_version_names():
return [x["init_version_name"] for x in _AVAILABLE_MIGRATION_STEPS]
@@ -65,6 +73,46 @@ def _replace_in_file(file_path, replaces, log_message=False):
return new_text
+def _replace_field_names(file_path, replaces, log_message=False):
+ current_text = _read_content(file_path)
+ new_text = current_text
+ # if the field is a python file with _inherit = model_name or model.name
+ # we try to replace the fields
+ model = get_model(file_path)
+ if model in replaces:
+ model_field_name_replaces = replaces[model]
+ # This replace is more careful on when and where we do replaces because the idea is to only change field
+ # names instead of everywhere (i.e. changing move_type to type affects the arch type on xml files)
+ if ".xml" in file_path:
+ # replace only between inside the arch tags
+ xml_data_bytes = new_text.encode("utf-8")
+ root = etree.fromstring(xml_data_bytes)
+ archs = root.xpath('.//field[@name="arch"]')
+ # 3 looped for, not a good look
+ for arch in archs:
+ for tag in arch:
+ for old_term, new_term in model_field_name_replaces.items():
+ new_tag = etree.fromstring(
+ etree.tostring(tag).decode().replace(old_term, new_term)
+ )
+ arch.replace(tag, new_tag)
+ new_text = etree.tostring(
+ root, pretty_print=True, xml_declaration=True, encoding="UTF-8"
+ ).decode()
+ elif ".py" in file_path:
+ # replace only inside of classes
+ for old_term, new_term in model_field_name_replaces.items():
+ new_text = replace_in_classes(new_text, old_term, new_term)
+
+ # Write file if changed
+ if new_text != current_text:
+ if not log_message:
+ log_message = "Changing content of file: %s" % file_path.name
+ logger.info(log_message)
+ _write_content(file_path, new_text)
+ return new_text
+
+
def get_files(module_path, extensions):
"""
Returns a list of files with the specified extensions within the module_path.
@@ -75,7 +123,38 @@ def get_files(module_path, extensions):
if not module_dir.is_dir():
raise Exception(f"'{module_path}' is not a valid directory.")
+ if isinstance(extensions, str):
+ extensions = (extensions,)
+
for ext in extensions:
file_paths.extend(module_dir.rglob(f"*{ext}"))
return file_paths
+
+
+def get_model(absolute_filepath):
+ model = ""
+ match = ""
+ with open(absolute_filepath, "r") as file:
+ file_content = file.read()
+ if "xml" in absolute_filepath:
+ match = re.search(
+ r"([a-zA-Z0-9_.]+)", file_content
+ )
+ elif "py" in absolute_filepath:
+ match = re.search(r"_inherit\s*=\s*['\"]([^'\"]+)['\"]", file_content)
+ if match:
+ model = match.group(1)
+ return model
+
+
+def replace_in_classes(code, old_text, new_text):
+ # Find all classes in the code
+ classes = CLASS_PATTERN.findall(code)
+
+ # Replace old_text with new_text in each class body
+ for cls in classes:
+ updated_class = cls.replace(old_text, new_text)
+ code = code.replace(cls, updated_class)
+
+ return code
diff --git a/tests/data_result/module_130_140/__init__.py b/tests/data_result/module_130_140/__init__.py
index e69de29b..0650744f 100644
--- a/tests/data_result/module_130_140/__init__.py
+++ b/tests/data_result/module_130_140/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/tests/data_result/module_130_140/models/__init__.py b/tests/data_result/module_130_140/models/__init__.py
new file mode 100644
index 00000000..91fed54d
--- /dev/null
+++ b/tests/data_result/module_130_140/models/__init__.py
@@ -0,0 +1 @@
+from . import res_partner
diff --git a/tests/data_result/module_130_140/models/res_partner.py b/tests/data_result/module_130_140/models/res_partner.py
new file mode 100644
index 00000000..791d96fa
--- /dev/null
+++ b/tests/data_result/module_130_140/models/res_partner.py
@@ -0,0 +1,58 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import _, models
+from odoo.exceptions import ValidationError
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ def test_improve_translation(self):
+ return _("It's %s", 2024)
+
+ def test_improve_transalation_with_line_break(self):
+ raise ValidationError(
+ _("The '%s' is empty or 0. It should have a non-null value.", "Price")
+ )
+
+ def test_improve_translation_with_parenthesis(self):
+ return _("It's %s", 2024)
+
+ def test_improve_translation_with_single_quote(self):
+ return _('It is %s', 2024)
+
+ def test_improve_translation_with_parenthesis_and_line_break(self):
+ raise ValidationError(
+ _("%s are only valid until %s", "User", 2024)
+ )
+
+ def test_improve_translation_with_brackets(self):
+ return _("User %(name)s has %(items)s items", name="Dev", items=5)
+
+ def test_improve_translation_with_single_quote(self):
+ return _('User %(name)s has %(items)s items', name='Dev', items=5)
+
+ def test_improve_translation_with_brackets_and_line_break(self):
+ return _("User %(name)s has %(items)s items", name="Dev", items=5)
+
+ def test_improve_translation_with_format(self):
+ return _("It's %s", 2024)
+
+ def test_improve_translation_with_format_and_single_quote(self):
+ return _('It is %s', 2024)
+
+ def test_improve_translation_with_format_and_line_break(self):
+ return _("It's %s", 2024)
+
+ def test_improve_translation_with_format_has_end_comma(self):
+ return _("It's %s", 2024)
+
+ def test_improve_translation_with_format_multi_params(self):
+ return _("User %(name)s has %(items)s items", name="Dev", items=5)
+
+ def test_improve_translation_with_format_multi_params_and_line_break(self):
+ return _("User %(name)s has %(items)s items", name="Dev", items=5)
+
+ def test_improve_translation_with_format_multi_params_has_end_comma(self):
+ return _('User %(name)s has "acb" %(items)s items', name="Dev", items=5)
+
\ No newline at end of file
diff --git a/tests/data_result/module_160_170/views/res_partner.xml b/tests/data_result/module_160_170/views/res_partner.xml
index 510c407a..3176300a 100644
--- a/tests/data_result/module_160_170/views/res_partner.xml
+++ b/tests/data_result/module_160_170/views/res_partner.xml
@@ -10,6 +10,7 @@
+
diff --git a/tests/data_result/module_170_180/__init__.py b/tests/data_result/module_170_180/__init__.py
index 0650744f..f7209b17 100644
--- a/tests/data_result/module_170_180/__init__.py
+++ b/tests/data_result/module_170_180/__init__.py
@@ -1 +1,2 @@
from . import models
+from . import controllers
diff --git a/tests/data_result/module_170_180/__manifest__.py b/tests/data_result/module_170_180/__manifest__.py
index 4599a7e8..3532b29d 100644
--- a/tests/data_result/module_170_180/__manifest__.py
+++ b/tests/data_result/module_170_180/__manifest__.py
@@ -10,5 +10,11 @@
],
'data': [
'views/res_partner.xml',
+ 'views/product_template_view.xml',
],
+ 'assets': {
+ 'web.assets_backend': [
+ 'module_170/static/src/js/main.js',
+ ],
+ },
}
diff --git a/tests/data_result/module_170_180/controllers/__init__.py b/tests/data_result/module_170_180/controllers/__init__.py
new file mode 100644
index 00000000..12a7e529
--- /dev/null
+++ b/tests/data_result/module_170_180/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/tests/data_result/module_170_180/controllers/main.py b/tests/data_result/module_170_180/controllers/main.py
new file mode 100644
index 00000000..2efb3cf3
--- /dev/null
+++ b/tests/data_result/module_170_180/controllers/main.py
@@ -0,0 +1,8 @@
+from odoo import http
+from odoo.http import request
+
+
+class MainController(http.Controller):
+ @http.route("/home/main", type="http", auth="public", website=True)
+ def redirect_to_main(self):
+ partner_name = request.env["ir.http"]._slugify(request.env.user.partner_id.name)
diff --git a/tests/data_result/module_170_180/models/__init__.py b/tests/data_result/module_170_180/models/__init__.py
index 91fed54d..a6eaa2fa 100644
--- a/tests/data_result/module_170_180/models/__init__.py
+++ b/tests/data_result/module_170_180/models/__init__.py
@@ -1 +1,3 @@
from . import res_partner
+from . import website
+from . import fetchmail
diff --git a/tests/data_result/module_170_180/models/__init__.py.orig b/tests/data_result/module_170_180/models/__init__.py.orig
new file mode 100644
index 00000000..5af42624
--- /dev/null
+++ b/tests/data_result/module_170_180/models/__init__.py.orig
@@ -0,0 +1,6 @@
+from . import res_partner
+<<<<<<< HEAD
+from . import website
+=======
+from . import fetchmail
+>>>>>>> f65d6f4 ([IMP] _() improvements)
diff --git a/tests/data_result/module_170_180/models/fetchmail.py b/tests/data_result/module_170_180/models/fetchmail.py
new file mode 100644
index 00000000..cceb5f3d
--- /dev/null
+++ b/tests/data_result/module_170_180/models/fetchmail.py
@@ -0,0 +1,20 @@
+from odoo import models, tools, _
+from odoo.tools import misc
+from odoo.tools.misc import find_in_path
+from odoo.tools import config, consteq, file_path
+from odoo.tools.misc import lazy
+from odoo.tools import config, consteq, file_path
+from ssl import SSLError
+from odoo.exceptions import UserError
+
+
+class FetchMailServer(models.Model):
+ _inherit = "fetchmail.server"
+
+ def example_method_use_ustr(self):
+ try:
+ server_name = self.name
+ description = self._description
+ connection = self.connect(allow_archived=True)
+ except SSLError as e:
+ raise UserError(_(e))
diff --git a/tests/data_result/module_170_180/models/res_partner.py b/tests/data_result/module_170_180/models/res_partner.py
index 21b4ba9d..30818f67 100644
--- a/tests/data_result/module_170_180/models/res_partner.py
+++ b/tests/data_result/module_170_180/models/res_partner.py
@@ -5,6 +5,11 @@ class ResPartner(models.Model):
_inherit = "res.partner"
test_field_1 = fields.Boolean()
+ test_unaccent = fields.Char(string="test_unaccent", default="test")
+ test_unaccent_only = fields.Char()
+ test_unaccent_only_html = fields.Html()
+ test_unaccent_multiline = fields.Char(string="test_unaccent", default="test")
+ test_unaccent_only_multiline = fields.Text()
def example_method(self):
self.env.ref('module_name.tree_view').write({'view_mode': 'list'})
diff --git a/tests/data_result/module_170_180/models/website.py b/tests/data_result/module_170_180/models/website.py
new file mode 100644
index 00000000..8b76e566
--- /dev/null
+++ b/tests/data_result/module_170_180/models/website.py
@@ -0,0 +1,9 @@
+from odoo import api, models
+
+
+class Website(models.Model):
+ _inherit = "website"
+
+ @api.model
+ def example_method_use_slugify(self, page_name):
+ return "/" + self.env["ir.http"]._slugify(page_name, max_length=1024, path=True)
diff --git a/tests/data_result/module_170_180/security/ir.model.access.csv b/tests/data_result/module_170_180/security/ir.model.access.csv
new file mode 100644
index 00000000..97dd8b91
--- /dev/null
+++ b/tests/data_result/module_170_180/security/ir.model.access.csv
@@ -0,0 +1 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/tests/data_result/module_170_180/static/file_not_process.js b/tests/data_result/module_170_180/static/file_not_process.js
new file mode 100644
index 00000000..950603d4
--- /dev/null
+++ b/tests/data_result/module_170_180/static/file_not_process.js
@@ -0,0 +1,4 @@
+/** @odoo-module **/
+
+// example js file include tag @odoo-module but not process this file
+// because it does not in static/src or static/tests
diff --git a/tests/data_result/module_170_180/static/src/js/main.js b/tests/data_result/module_170_180/static/src/js/main.js
new file mode 100644
index 00000000..67e3b532
--- /dev/null
+++ b/tests/data_result/module_170_180/static/src/js/main.js
@@ -0,0 +1 @@
+// example js file include tag @odoo-module
diff --git a/tests/data_result/module_170_180/views/product_template_view.xml b/tests/data_result/module_170_180/views/product_template_view.xml
new file mode 100644
index 00000000..5a2db6e6
--- /dev/null
+++ b/tests/data_result/module_170_180/views/product_template_view.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ product.template.list.custom
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/tests/data_result/module_170_180/views/res_partner.xml b/tests/data_result/module_170_180/views/res_partner.xml
index ff22ea0c..fc3141f8 100644
--- a/tests/data_result/module_170_180/views/res_partner.xml
+++ b/tests/data_result/module_170_180/views/res_partner.xml
@@ -7,10 +7,14 @@
+
+
+
+ bottom
-
+
res.partner.form
res.partner
@@ -36,6 +40,15 @@
+
+ boolean_toggle
+
+
+ boolean_toggle
+
+
+ boolean_toggle
+
@@ -62,4 +75,96 @@
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data_template/module_130/__init__.py b/tests/data_template/module_130/__init__.py
index e69de29b..0650744f 100644
--- a/tests/data_template/module_130/__init__.py
+++ b/tests/data_template/module_130/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/tests/data_template/module_130/models/__init__.py b/tests/data_template/module_130/models/__init__.py
new file mode 100644
index 00000000..91fed54d
--- /dev/null
+++ b/tests/data_template/module_130/models/__init__.py
@@ -0,0 +1 @@
+from . import res_partner
diff --git a/tests/data_template/module_130/models/res_partner.py b/tests/data_template/module_130/models/res_partner.py
new file mode 100644
index 00000000..aa0c41bf
--- /dev/null
+++ b/tests/data_template/module_130/models/res_partner.py
@@ -0,0 +1,75 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import _, models
+from odoo.exceptions import ValidationError
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ def test_improve_translation(self):
+ return _("It's %s") % 2024
+
+ def test_improve_transalation_with_line_break(self):
+ raise ValidationError(
+ _("The '%s' is empty or 0. It should have a non-null value.")
+ % "Price"
+ )
+
+ def test_improve_translation_with_parenthesis(self):
+ return _("It's %s") % (2024)
+
+ def test_improve_translation_with_single_quote(self):
+ return _('It is %s') % (2024)
+
+ def test_improve_translation_with_parenthesis_and_line_break(self):
+ raise ValidationError(
+ _("%s are only valid until %s") % (
+ "User", 2024,
+ )
+ )
+
+ def test_improve_translation_with_brackets(self):
+ return _("User %(name)s has %(items)s items") % {"name":"Dev", "items": 5}
+
+ def test_improve_translation_with_single_quote(self):
+ return _('User %(name)s has %(items)s items') % {'name':'Dev', 'items': 5}
+
+ def test_improve_translation_with_brackets_and_line_break(self):
+ return _(
+ "User %(name)s has %(items)s items"
+ ) % {
+ "name": "Dev",
+ "items": 5,
+ }
+
+ def test_improve_translation_with_format(self):
+ return _("It's {}").format(2024)
+
+ def test_improve_translation_with_format_and_single_quote(self):
+ return _('It is {}').format(2024)
+
+ def test_improve_translation_with_format_and_line_break(self):
+ return _(
+ "It's {}"
+ ).format(
+ 2024
+ )
+
+ def test_improve_translation_with_format_has_end_comma(self):
+ return _("It's {}").format(2024,)
+
+ def test_improve_translation_with_format_multi_params(self):
+ return _("User {name} has {items} items").format(name="Dev", items=5)
+
+ def test_improve_translation_with_format_multi_params_and_line_break(self):
+ return _(
+ "User {name} has {items} items"
+ ).format(
+ name="Dev",
+ items=5
+ )
+
+ def test_improve_translation_with_format_multi_params_has_end_comma(self):
+ return _('User {name} has "acb" {items} items').format(name="Dev", items=5,)
+
\ No newline at end of file
diff --git a/tests/data_template/module_160/views/res_partner.xml b/tests/data_template/module_160/views/res_partner.xml
index 510c407a..e414fef1 100644
--- a/tests/data_template/module_160/views/res_partner.xml
+++ b/tests/data_template/module_160/views/res_partner.xml
@@ -7,9 +7,10 @@
-
+
+
diff --git a/tests/data_template/module_170/__init__.py b/tests/data_template/module_170/__init__.py
index 0650744f..f7209b17 100644
--- a/tests/data_template/module_170/__init__.py
+++ b/tests/data_template/module_170/__init__.py
@@ -1 +1,2 @@
from . import models
+from . import controllers
diff --git a/tests/data_template/module_170/__manifest__.py b/tests/data_template/module_170/__manifest__.py
index 4bee0f35..15bb5d3a 100644
--- a/tests/data_template/module_170/__manifest__.py
+++ b/tests/data_template/module_170/__manifest__.py
@@ -10,5 +10,11 @@
],
'data': [
'views/res_partner.xml',
+ 'views/product_template_view.xml',
],
+ 'assets': {
+ 'web.assets_backend': [
+ 'module_170/static/src/js/main.js',
+ ],
+ },
}
diff --git a/tests/data_template/module_170/controllers/__init__.py b/tests/data_template/module_170/controllers/__init__.py
new file mode 100644
index 00000000..12a7e529
--- /dev/null
+++ b/tests/data_template/module_170/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/tests/data_template/module_170/controllers/main.py b/tests/data_template/module_170/controllers/main.py
new file mode 100644
index 00000000..3746d404
--- /dev/null
+++ b/tests/data_template/module_170/controllers/main.py
@@ -0,0 +1,9 @@
+from odoo import http
+from odoo.addons.http_routing.models.ir_http import slugify
+from odoo.http import request
+
+
+class MainController(http.Controller):
+ @http.route("/home/main", type="http", auth="public", website=True)
+ def redirect_to_main(self):
+ partner_name = slugify(request.env.user.partner_id.name)
diff --git a/tests/data_template/module_170/models/__init__.py b/tests/data_template/module_170/models/__init__.py
index 91fed54d..a6eaa2fa 100644
--- a/tests/data_template/module_170/models/__init__.py
+++ b/tests/data_template/module_170/models/__init__.py
@@ -1 +1,3 @@
from . import res_partner
+from . import website
+from . import fetchmail
diff --git a/tests/data_template/module_170/models/fetchmail.py b/tests/data_template/module_170/models/fetchmail.py
new file mode 100644
index 00000000..2db55e63
--- /dev/null
+++ b/tests/data_template/module_170/models/fetchmail.py
@@ -0,0 +1,22 @@
+from odoo import models, tools, _
+from odoo.tools import misc
+from odoo.tools import ustr
+from odoo.tools.misc import ustr
+from odoo.tools.misc import ustr, find_in_path
+from odoo.tools import ustr, config, consteq, file_path
+from odoo.tools.misc import lazy, ustr
+from odoo.tools import config, consteq, ustr, file_path
+from ssl import SSLError
+from odoo.exceptions import UserError
+
+
+class FetchMailServer(models.Model):
+ _inherit = "fetchmail.server"
+
+ def example_method_use_ustr(self):
+ try:
+ server_name = ustr(self.name)
+ description = misc.ustr(self._description)
+ connection = self.connect(allow_archived=True)
+ except SSLError as e:
+ raise UserError(_(tools.ustr(e)))
diff --git a/tests/data_template/module_170/models/res_partner.py b/tests/data_template/module_170/models/res_partner.py
index 36281105..52cdd201 100644
--- a/tests/data_template/module_170/models/res_partner.py
+++ b/tests/data_template/module_170/models/res_partner.py
@@ -5,6 +5,15 @@ class ResPartner(models.Model):
_inherit = "res.partner"
test_field_1 = fields.Boolean()
+ test_unaccent = fields.Char(string="test_unaccent", unaccent=False, default="test")
+ test_unaccent_only = fields.Char(unaccent=False)
+ test_unaccent_only_html = fields.Html(unaccent=False)
+ test_unaccent_multiline = fields.Char(string="test_unaccent",
+ unaccent=False,
+ default="test")
+ test_unaccent_only_multiline = fields.Text(
+ unaccent=False,
+ )
def example_method(self):
self.env.ref('module_name.tree_view').write({'view_mode': 'tree'})
diff --git a/tests/data_template/module_170/models/website.py b/tests/data_template/module_170/models/website.py
new file mode 100644
index 00000000..a95b4163
--- /dev/null
+++ b/tests/data_template/module_170/models/website.py
@@ -0,0 +1,10 @@
+from odoo import api, models
+from odoo.addons.http_routing.models.ir_http import slugify
+
+
+class Website(models.Model):
+ _inherit = "website"
+
+ @api.model
+ def example_method_use_slugify(self, page_name):
+ return "/" + slugify(page_name, max_length=1024, path=True)
diff --git a/tests/data_template/module_170/security/ir.model.access.csv b/tests/data_template/module_170/security/ir.model.access.csv
new file mode 100644
index 00000000..97dd8b91
--- /dev/null
+++ b/tests/data_template/module_170/security/ir.model.access.csv
@@ -0,0 +1 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/tests/data_template/module_170/static/file_not_process.js b/tests/data_template/module_170/static/file_not_process.js
new file mode 100644
index 00000000..950603d4
--- /dev/null
+++ b/tests/data_template/module_170/static/file_not_process.js
@@ -0,0 +1,4 @@
+/** @odoo-module **/
+
+// example js file include tag @odoo-module but not process this file
+// because it does not in static/src or static/tests
diff --git a/tests/data_template/module_170/static/src/js/main.js b/tests/data_template/module_170/static/src/js/main.js
new file mode 100644
index 00000000..d357df08
--- /dev/null
+++ b/tests/data_template/module_170/static/src/js/main.js
@@ -0,0 +1,6 @@
+/** @odoo-module **/
+/* @odoo-module */
+/** @odoo-module */
+/* @odoo-module **/
+
+// example js file include tag @odoo-module
diff --git a/tests/data_template/module_170/views/product_template_view.xml b/tests/data_template/module_170/views/product_template_view.xml
new file mode 100644
index 00000000..784bbf52
--- /dev/null
+++ b/tests/data_template/module_170/views/product_template_view.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ product.template.list.custom
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/tests/data_template/module_170/views/res_partner.xml b/tests/data_template/module_170/views/res_partner.xml
index 9beaf9b9..ca5db983 100644
--- a/tests/data_template/module_170/views/res_partner.xml
+++ b/tests/data_template/module_170/views/res_partner.xml
@@ -5,12 +5,16 @@
res.partner
-
+
+
+
+
+ 1
-
+
res.partner.form
res.partner
@@ -32,10 +36,21 @@
res.partner
-
+
+
+ toggle_button
+
+
+ toggle_button
+
+
+
+ toggle_button
+
+
@@ -69,4 +84,107 @@
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.view.kanban
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+