diff --git a/Makefile b/Makefile
index c1c82a367..023735b45 100644
--- a/Makefile
+++ b/Makefile
@@ -87,7 +87,7 @@ cleanup_addons:
 install_modules:
 	${PYTHON} odoo-bin --config=${CONFIG} -d ${DATABASE} -i ${MODULES} --xmlrpc-port=${PORT}
 upgrade_modules:
-	${PYTHON} odoo-bin upgrade_code --addons-path ${UPGRADE_DIR} --from ${OLD_VER} --to ${NEW_VER}
+	${PYTHON} odoo-bin upgrade_code --addons-path=${UPGRADE_DIR} --from ${OLD_VER} --to ${NEW_VER} --dry-run
 
 ##### Docker Deployment #########
 run_test_docker: 
diff --git a/odoo/upgrade_code/17.5-02-replace-attrs.py b/odoo/upgrade_code/17.5-02-replace-attrs.py
new file mode 100644
index 000000000..0cd94ebcb
--- /dev/null
+++ b/odoo/upgrade_code/17.5-02-replace-attrs.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+import re
+from bs4 import BeautifulSoup as bs
+from ast import literal_eval
+import logging
+
+# Set up logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Constants
+NEW_ATTRS = {'required', 'invisible', 'readonly', 'column_invisible'}
+
+def upgrade(file_manager):
+    """Upgrade XML files by converting 'attrs' and 'states' into new attributes."""
+    # Filter files to only XML files
+    files = [file for file in file_manager if file.path.suffix == '.xml']
+    if not files:
+        return
+
+    # Regex patterns with re.VERBOSE for readability
+    percent_d_re = re.compile(r"""
+        %           # Start with percent
+        \(          # Opening parenthesis
+        '?          # Optional single quote
+        "?          # Optional double quote
+        (?P<key>[\w\.\d_]+)  # Capture key (word chars, dots, digits, underscores)
+        '?          # Optional single quote
+        "?          # Optional double quote
+        \)          # Closing parenthesis
+        d           # Ends with 'd'
+    """, re.VERBOSE)
+
+    # Helper functions
+    def normalize_domain(domain):
+        if len(domain) == 1:
+            return domain
+        result = []
+        expected = 1
+        op_arity = {'!': 1, '&': 2, '|': 2}
+        for token in domain:
+            if expected == 0:
+                result[0:0] = ['&']
+                expected = 1
+            if isinstance(token, (list, tuple)):
+                expected -= 1
+                token = tuple(token)
+            else:
+                expected += op_arity.get(token, 0) - 1
+            result.append(token)
+        return result
+
+    def stringify_leaf(leaf):
+        operator = str(leaf[1])
+        if operator == '=':
+            operator = '=='
+        elif 'like' in operator:
+            operator = 'not in' if 'not' in operator else 'in'
+            return f"{leaf[2] if isinstance(leaf[2], str) else str(leaf[2])} {operator} {leaf[0]}"
+        right = str(leaf[2]) if isinstance(leaf[2], (list, tuple, set, int, float, bool)) or leaf[2] in ('True', 'False', '1', '0') else f"'{leaf[2]}'"
+        return f"{leaf[0]} {operator} {right}"
+
+    def stringify_attr(stack):
+        if stack in (True, False, 'True', 'False', 1, 0, '1', '0'):
+            return str(stack)
+        last_paren_idx = max(i for i, item in enumerate(stack[::-1]) if item not in ('|', '!'))
+        stack = normalize_domain(stack)[::-1]
+        result = []
+        for idx, item in enumerate(stack):
+            if item == '!':
+                expr = result.pop()
+                result.append(f"(not ({expr}))")
+            elif item in ('&', '|'):
+                try:
+                    left, right = result.pop(), result.pop()
+                    op = 'and' if item == '&' else 'or'
+                    form = f"({left} {op} {right})" if idx > last_paren_idx else f"{left} {op} {right}"
+                    result.append(form)
+                except IndexError:
+                    result.append(f"{left} {op}")
+            else:
+                result.append(stringify_leaf(item))
+        return result[0]
+
+    def get_new_attrs(attrs):
+        try:
+            # Use ast.literal_eval for safe evaluation of Python literals
+            attrs_dict = literal_eval(attrs.strip())
+            if not isinstance(attrs_dict, dict):
+                logger.warning(f"Invalid attrs format, expected dict, got {attrs_dict}")
+                return {}
+            return {attr: stringify_attr(attrs_dict[attr]) for attr in NEW_ATTRS if attr in attrs_dict}
+        except (ValueError, SyntaxError) as e:
+            logger.error(f"Failed to parse attrs '{attrs}': {e}")
+            return {}
+        except Exception as e:
+            logger.error(f"Unexpected error parsing attrs '{attrs}': {e}")
+            return {}
+
+    # Process each file
+    for fileno, file in enumerate(files, start=1):
+        content = file.content
+
+        # Handle percent-d replacements
+        percent_d_results = {}
+        for i, match in enumerate(percent_d_re.findall(content), 1):
+            placeholder = f"'REPLACEME{i}'"
+            percent_d_results[i] = match[0]  # Full match
+            content = content.replace(match[0], placeholder)
+
+        # Parse XML with BeautifulSoup
+        soup = bs(content, 'xml')
+        tags_with_attrs = soup.select('[attrs]')
+        attr_tags = soup.select('attribute[name="attrs"]')
+        tags_with_states = soup.select('[states]')
+        state_tags = soup.select('attribute[name="states"]')
+
+        if not (tags_with_attrs or attr_tags or tags_with_states or state_tags):
+            continue
+
+        # Process tags with attrs
+        for tag in tags_with_attrs:
+            new_attrs = get_new_attrs(tag['attrs'])
+            if new_attrs:  # Only proceed if parsing succeeded
+                del tag['attrs']
+                for attr, value in new_attrs.items():
+                    tag[attr] = value
+            else:
+                logger.warning(f"Skipping tag in {file.path} due to invalid attrs: {tag.get('attrs')}")
+
+        # Process <attribute name="attrs">
+        for attr_tag in attr_tags:
+            new_attrs = get_new_attrs(attr_tag.text)
+            if new_attrs:
+                for attr, value in new_attrs.items():
+                    new_tag = soup.new_tag('attribute', name=attr)
+                    new_tag.string = value
+                    attr_tag.insert_after(new_tag)
+                attr_tag.decompose()
+            else:
+                logger.warning(f"Skipping attribute tag in {file.path} due to invalid attrs: {attr_tag.text}")
+
+        # Process tags with states
+        for tag in tags_with_states:
+            base_invisible = tag.get('invisible', '')
+            if base_invisible and not base_invisible.endswith(('or', 'and')):
+                base_invisible += ' or '
+            states = [f"'{s.strip()}'" for s in tag['states'].split(',')]
+            tag['invisible'] = f"{base_invisible}state not in [{','.join(states)}]"
+            del tag['states']
+
+        # Process <attribute name="states">
+        for state_tag in state_tags:
+            states = [f"'{s.strip()}'" for s in state_tag.text.split(',')]
+            parent = state_tag.parent
+            inv_tag = next((t for t in parent.findAll('attribute') if t['name'] == 'invisible'), None)
+            if not inv_tag:
+                inv_tag = soup.new_tag('attribute', name='invisible')
+            current = inv_tag.text or ''
+            new_text = f"state not in [{','.join(states)}]"
+            inv_tag.string = f"{current} or {new_text}" if current else new_text
+            state_tag.insert_after(inv_tag)
+            state_tag.decompose()
+
+        # Write back with percent-d restored
+        output = str(soup)
+        for i, original in percent_d_results.items():
+            output = output.replace(f"'REPLACEME{i}'", original)
+        
+        file.content = output
+        file_manager.print_progress(fileno, len(files))
\ No newline at end of file
diff --git a/setup/replace_attrs.py b/setup/replace_attrs.py
index 03a7df75f..6ddf799c5 100644
--- a/setup/replace_attrs.py
+++ b/setup/replace_attrs.py
@@ -228,7 +228,7 @@ print('################## Run  Debug ##################')
 print('################################################')
 
 if nofilesfound:
-    print('No XML Files with "attrs" or "states" found in dir " %s "' % root_dir)
+    print('No XML Files with "attrs" or "states" found in dir "%s"' % root_dir)
 
 print('Succeeded on files')
 for file in ok_files: