Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5691525
[ADD] replace action.read() by _for_xml_id to avoid access rights issue
anothingguy Oct 26, 2023
0286030
[ADD] Add error message chart_template_ref argument has been removed …
lef-adhoc Oct 17, 2024
9022d3e
[IMP] slugify deprecation
xaviedoanhduy Oct 21, 2024
51aac35
[IMP] replace editable attribute on list view
xaviedoanhduy Oct 24, 2024
551394f
[IMP] automatically remove @odoo-module from js assets
xaviedoanhduy Oct 23, 2024
7e2785d
[IMP] Add error messages for deprecated '_check_recursion' and '_chec…
lef-adhoc Oct 29, 2024
a0d6bc4
[IMP] Add warning for direct '_' import in translations
lef-adhoc Nov 1, 2024
52f1172
[IMP] rewrite translatable strings to use named placeholders
chaule97 Oct 30, 2024
d11e677
[IMP] *: Add warning for auto-added invisible/readonly fields in XML …
lef-adhoc Nov 13, 2024
f549d0d
[IMP] remove depcreate class `oe_kanban_global_click` and `oe_kanban_…
Nov 20, 2024
10d35b1
[IMP] remove deprecated `oe_kanban_colorpicker` and warning deprecate…
Nov 20, 2024
0b15b65
[IMP] remove deprecated kanban-tooltip
Nov 21, 2024
9ad9c1d
[IMP] warning deprecated kanban_image
Nov 21, 2024
79a4bdd
[IMP] Changed type='edit' to type='open' to open records
Nov 21, 2024
385e7f1
[IMP] tools.get_file to support single extension
xaviedoanhduy Nov 27, 2024
e7fc0d4
[UPG] Base migration upgrade for fields rename.
Apr 25, 2025
874cc7e
[IMP] Warning for _invalidate_cache()
thienvh332 Jan 7, 2025
095965f
[ADD] stock: renamed/removed 'stock.move.line' fields from 15.0 to 16.0
sebalix Apr 3, 2025
f7edb43
[IMP] Update removed_models and removed_fields datasets based on Open…
thienvh332 Aug 25, 2025
7422b80
[ADD] Add warning for deprecated _notify_progress in favor to _commit…
ced-adhoc Sep 24, 2025
aa85478
[IMP] *: add warning for auto-added invisible/readonly fields in XML …
nicomacr Oct 8, 2025
13f48f5
[ADD] Add environment read-only warning to migration script
vib-adhoc Oct 20, 2025
97d2d8d
[IMP] convert attrs attribute to attributes during v17 migration
hbrunn Dec 8, 2025
9695b0f
migrate_180_190 - upgrade_sql_constraints - ignore syntax error when …
cuongnmtm Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 26 additions & 4 deletions odoo_module_migrate/base_migration_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -260,17 +267,32 @@ 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:
model_info = res["replaces"].get(model_name)
if model_info:
model_info.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)
Expand Down
32 changes: 31 additions & 1 deletion odoo_module_migrate/migration_scripts/migrate_130_140.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()",
Expand All @@ -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
83 changes: 83 additions & 0 deletions odoo_module_migrate/migration_scripts/migrate_130_allways.py
Original file line number Diff line number Diff line change
@@ -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'_\(\s*(["\'].*?%[ds].*?["\'])\s*\)\s*%\s*\(\s*(.+)\s*\)': format_parenthesis,
r'_\(\s*(["\'].*?%[ds].*?["\'])\s*\)\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]
159 changes: 158 additions & 1 deletion odoo_module_migrate/migration_scripts/migrate_160_170.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,148 @@ def _check_open_form_view(logger, file_path: Path):
)


def _move_attrs_to_attributes_view(logger, file_path: Path):
"""Transform <field attrs={'required': [('field', '=', value)]}> to <field required="field == value" /> in views"""
parser = et.XMLParser()
tree = et.parse(str(file_path.resolve()), parser)
field_selector = "record[@model='ir.ui.view']/field[@name='arch']"
modified = False

def leaf_to_python(leaf):
left, operator, right = leaf.elts
if operator.value in ("!=", "="):
if (
isinstance(right, ast.Constant)
and isinstance(right.value, bool)
and right.value in (False, True)
):
falsy = (
operator.value == "="
and not right.value
or operator.value == "!="
and right.value
)
return "{}{}{}".format(
falsy and "not" or "",
falsy and " " or "",
left.value,
)
if isinstance(right, ast.List) and not right.elts:
falsy = operator.value == "="
return "{}{}{}".format(
falsy and "not" or "",
falsy and " " or "",
left.value,
)

return "{} {} {}".format(
left.value,
operator.value if operator.value != "=" else "==",
ast.unparse(right),
)

def get_operand(domain):
if not domain.elts:
return ast.List([])

current = domain.elts[0]

if isinstance(current, ast.Tuple | ast.List):
return ast.List([current])

if isinstance(current, ast.Constant):
left = get_operand(ast.List(domain.elts[1:]))

if current.value == "!":
return ast.List([current] + left.elts)

right = get_operand(ast.List(domain.elts[1 + len(left.elts) :]))
return ast.List([current] + left.elts + right.elts)

def domain_to_python(domain):
if not domain.elts:
return ""
if len(domain.elts) == 1:
return leaf_to_python(domain.elts[0])
first = domain.elts[0]
if isinstance(first, ast.Constant):
if first.value in ("&", "|"):
left = get_operand(ast.List(domain.elts[1:]))
right = get_operand(ast.List(domain.elts[len(left.elts) + 1 :]))
tail = domain_to_python(
ast.List(domain.elts[len(left.elts) + len(right.elts) + 1 :])
)
return (
"("
+ domain_to_python(left)
+ (" and " if first.value == "&" else " or ")
+ domain_to_python(right)
+ ")"
+ (tail and f" and {tail}" or "")
)
if first.value == "!":
left = get_operand(ast.List(domain.elts[1:]))
tail = domain_to_python(ast.List(domain.elts[len(left.elts) + 1 :]))
return "not ({})".format(domain_to_python(left)) + (
tail and f" and {tail}" or ""
)

raise ValueError("unknown operator")
if isinstance(first, ast.List | ast.Tuple):
return (
domain_to_python(ast.List([domain.elts[0]]))
+ " and "
+ domain_to_python(ast.List(domain.elts[1:]))
)
raise ValueError("malformed domain")

def attrs_to_attributes(attrs_string):
try:
attrs_expression = ast.parse(attrs_string.strip(), mode="eval")
except:
return {}
if not isinstance(attrs_expression, ast.Expression):
return {}
if not isinstance(attrs_expression.body, ast.Dict):
return {}
attrs = attrs_expression.body
result = {}
for key, value in zip(attrs.keys, attrs.values):
try:
result[key.value] = domain_to_python(value)
except:
result[key.value] = f"False # could not parse {ast.unparse(value)}"
return result

for arch in tree.xpath(f"{field_selector} | data/{field_selector}"):
# <field attrs="{}" />
for node in arch.xpath("//*[@attrs]"):
attributes = attrs_to_attributes(node.attrib["attrs"])
if not attributes:
continue
node.attrib.update(attributes)
del node.attrib["attrs"]
modified = True
# inherited views
for node in arch.xpath("//attribute[@name='attrs']"):
attributes = attrs_to_attributes(node.text)
if not attributes:
continue
parent = node.getparent()
for key, value in attributes.items():
new_node = et.SubElement(parent, "attribute", name=key)
new_node.text = value
parent.remove(node)
modified = True

if modified:
tree.write(file_path, xml_declaration=True)
with open(file_path, "r+") as xml_file:
xml_file.write('<?xml version="1.0" encoding="utf-8"?>')
xml_file.seek(0, 2)
xml_file.write("\n")


def _check_open_form(
logger, module_path, module_name, manifest_path, migration_steps, tools
):
Expand All @@ -272,6 +414,17 @@ def _check_open_form(
_check_open_form_view(logger, file_path)


def _move_attrs_to_attributes(
logger, module_path, module_name, manifest_path, migration_steps, tools
):
reformat_file_ext = ".xml"
file_paths = _get_files(module_path, reformat_file_ext)
logger.debug(f"{reformat_file_ext} files found:\n" f"{list(map(str, file_paths))}")

for file_path in file_paths:
_move_attrs_to_attributes_view(logger, file_path)


def _reformat_read_group(
logger, module_path, module_name, manifest_path, migration_steps, tools
):
Expand All @@ -291,4 +444,8 @@ def _reformat_read_group(

class MigrationScript(BaseMigrationScript):

_GLOBAL_FUNCTIONS = [_check_open_form, _reformat_read_group]
_GLOBAL_FUNCTIONS = [
_check_open_form,
_reformat_read_group,
_move_attrs_to_attributes,
]
Loading