From dd595904a26074b86bab6939c6157369263eeaa4 Mon Sep 17 00:00:00 2001 From: KaySar12 Date: Mon, 10 Mar 2025 14:17:20 +0700 Subject: [PATCH] update --- Makefile | 2 +- odoo/upgrade_code/17.5-02-replace-attrs.py | 172 +++++++++++++++++++++ setup/replace_attrs.py | 2 +- 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 odoo/upgrade_code/17.5-02-replace-attrs.py 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[\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 + 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 + 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: