Files
Enofirm-Temp/odoo/upgrade_code/17.5-02-replace-attrs.py
2026-09-18 13:55:25 +07:00

313 lines
13 KiB
Python

# -*- 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 = ['invisible', 'required', '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
# Helper functions
def normalize_domain(domain):
"""
Normalize Domain, taken from odoo/osv/expression.py -> just the part so that & operators are added where needed.
"""
if len(domain) == 1:
return domain
result = []
expected = 1 # expected number of expressions
op_arity = {'!': 1, '&': 2, '|': 2}
for token in domain:
if expected == 0: # more than expected, like in [A, B]
result[0:0] = ['&'] # put an extra '&' in front
expected = 1
if isinstance(token, (list, tuple)): # domain term
expected -= 1
token = tuple(token)
else:
expected += op_arity.get(token, 0) - 1
result.append(token)
return result
def stringify_leaf(leaf):
"""
Convert a domain leaf to python expression string
"""
operator = str(leaf[1])
left_operand = leaf[0]
right_operand = leaf[2]
# Handle '=?'
if operator == '=?':
if isinstance(right_operand, str):
right_operand = f"'{right_operand}'"
return f"({right_operand} in [None, False] or {left_operand} == {right_operand})"
# Handle '='
elif operator == '=':
if right_operand in (False, []): # Check for False or empty list
return f"not {left_operand}"
elif right_operand == True: # Check for True
return left_operand
operator = '=='
# Handle '!='
elif operator == '!=':
if right_operand in (False, []): # Check for False or empty list
return left_operand
elif right_operand == True: # Check for True
return f"not {left_operand}"
# Handle 'like' operators
elif 'like' in operator:
case_insensitive = 'ilike' in operator
if isinstance(right_operand, str) and re.search('[_%]', right_operand):
# Since wildcards won't work/be recognized after conversion we throw an error
raise Exception("Script doesn't support 'like' domains with wildcards")
if operator in ['=like', '=ilike']:
operator = '=='
else:
if 'not' in operator:
operator = 'not in'
else:
operator = 'in'
# Switch operands for 'in' operations
left_operand, right_operand = right_operand, left_operand
# Format right operand
if isinstance(right_operand, str):
right_operand = f"'{right_operand}'"
# Handle case insensitive operations
if 'like' in str(leaf[1]) and 'ilike' in str(leaf[1]):
return f"{left_operand}.lower() {operator} {right_operand}.lower()"
else:
return f"{left_operand} {operator} {right_operand}"
def stringify_attr(stack):
"""
Convert domain stack to python expression string
"""
if stack in (True, False, 'True', 'False', 1, 0, '1', '0'):
return str(stack)
try:
last_parenthesis_index = max(index for index, item in enumerate(stack[::-1]) if item not in ('|', '!'))
except ValueError:
last_parenthesis_index = 0
stack = normalize_domain(stack)
stack = stack[::-1]
result = []
for index, leaf_or_operator in enumerate(stack):
if leaf_or_operator == '!':
expr = result.pop()
result.append('(not (%s))' % expr)
elif leaf_or_operator in ['&', '|']:
left = result.pop()
try:
right = result.pop()
except IndexError:
res = left + ('%s' % ' and' if leaf_or_operator == '&' else ' or')
result.append(res)
continue
form = '(%s %s %s)'
if index > last_parenthesis_index:
form = '%s %s %s'
result.append(form % (left, 'and' if leaf_or_operator == '&' else 'or', right))
else:
result.append(stringify_leaf(leaf_or_operator))
return result[0] if result else ''
def get_new_attrs(attrs):
"""
Parse attrs string and return dictionary of new attributes
"""
new_attrs = {}
try:
# Temporarily replace dynamic variables in leafs
escaped_operators = ['=', '!=', '>', '>=', '<', '<=', '=\\?', '=like', 'like', 'not like', 'ilike', 'not ilike', '=ilike', 'in', 'not in', 'child_of', 'parent_of']
# Handle HTML entities
attrs = re.sub("&lt;", "<", attrs)
attrs = re.sub("&gt;", ">", attrs)
# Replace dynamic variables with placeholder strings
attrs = re.sub(f"([\"'](?:{'|'.join(escaped_operators)})[\"']\\s*,\\s*)(?!False|True)([\\w\\.]+)(?=\\s*[\\]\\)])", r"\1'__dynamic_variable__.\2'", attrs)
attrs = re.sub(r"(%\([\w\.]+\)d)", r"'__dynamic_variable__.\1'", attrs)
attrs = attrs.strip()
if re.search("^{.*}$", attrs, re.DOTALL):
attrs_dict = literal_eval(attrs.strip())
for attr, attr_value in attrs_dict.items():
if attr not in NEW_ATTRS:
continue
stringified_attr = stringify_attr(attr_value)
if isinstance(stringified_attr, str):
# Convert dynamic variable strings back to their original form
stringified_attr = re.sub(r"'__dynamic_variable__\.([^']+)'", r"\1", stringified_attr)
new_attrs[attr] = stringified_attr
return new_attrs
except Exception as e:
logger.error(f"Failed to parse attrs '{attrs}': {e}")
return {}
def get_combined_invisible_condition(invisible_attribute, states_attribute):
"""
Combine invisible attribute condition with states attribute
"""
invisible_attribute = invisible_attribute.strip() if invisible_attribute else ''
states_attribute = states_attribute.strip() if states_attribute else ''
if not states_attribute:
return invisible_attribute
states_list = [f"'{s.strip()}'" for s in states_attribute.split(',')]
states_to_add = f"state not in [{','.join(states_list)}]"
if invisible_attribute:
if invisible_attribute.endswith(('or', 'and')):
combined_invisible_condition = f"{invisible_attribute} {states_to_add}"
else:
combined_invisible_condition = f"{invisible_attribute} or {states_to_add}"
else:
combined_invisible_condition = states_to_add
return combined_invisible_condition
# Process each file
for fileno, file in enumerate(files, start=1):
try:
content = file.content
# Skip if no attrs or states
if not ('attrs' in content or 'states' in content):
continue
# 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:
attrs_value = tag.get('attrs', '')
new_attrs = get_new_attrs(attrs_value)
if new_attrs:
# Remove the original attrs attribute
del tag['attrs']
# Add new attributes, handling existing ones
for attr_name, attr_value in new_attrs.items():
if attr_name in tag.attrs:
# Combine with existing attribute
old_value = tag[attr_name]
if old_value in ['True', '1', True, 1]:
new_value = f"True or ({attr_value})"
elif old_value in ['False', '0', False, 0]:
new_value = f"False or ({attr_value})"
else:
new_value = f"({old_value}) or ({attr_value})"
tag[attr_name] = new_value
else:
tag[attr_name] = attr_value
# Process <attribute name="attrs">
for attr_tag in attr_tags:
attrs_value = attr_tag.get_text(strip=True) if attr_tag.get_text() else ''
new_attrs = get_new_attrs(attrs_value)
if new_attrs:
parent = attr_tag.parent
# Create new attribute tags
for attr_name, attr_value in new_attrs.items():
# Check if attribute already exists
existing_attr = parent.find('attribute', {'name': attr_name})
if existing_attr:
# Combine with existing attribute
old_value = existing_attr.get_text(strip=True) if existing_attr.get_text() else ''
if old_value in ['True', '1']:
new_value = f"True or ({attr_value})"
elif old_value in ['False', '0']:
new_value = f"False or ({attr_value})"
else:
new_value = f"({old_value}) or ({attr_value})"
existing_attr.string = new_value
else:
# Create new attribute tag
new_tag = soup.new_tag('attribute', name=attr_name)
new_tag.string = str(attr_value)
attr_tag.insert_before(new_tag)
# Remove the original attrs attribute tag
attr_tag.decompose()
# Process tags with states
for tag in tags_with_states:
states_value = tag.get('states', '')
invisible_value = tag.get('invisible', '')
new_invisible = get_combined_invisible_condition(invisible_value, states_value)
# Update attributes
if new_invisible:
tag['invisible'] = new_invisible
elif 'invisible' in tag.attrs:
del tag['invisible']
# Remove states attribute
del tag['states']
# Process <attribute name="states">
for state_tag in state_tags:
states_value = state_tag.get_text(strip=True) if state_tag.get_text() else ''
parent = state_tag.parent
# Find existing invisible attribute
invisible_attr = parent.find('attribute', {'name': 'invisible'})
invisible_value = invisible_attr.get_text(strip=True) if invisible_attr and invisible_attr.get_text() else ''
new_invisible = get_combined_invisible_condition(invisible_value, states_value)
if invisible_attr:
# Update existing invisible attribute
invisible_attr.string = new_invisible
else:
# Create new invisible attribute
new_tag = soup.new_tag('attribute', name='invisible')
new_tag.string = new_invisible
state_tag.insert_before(new_tag)
# Remove the states attribute tag
state_tag.decompose()
# Update file content
file.content = str(soup)
except Exception as e:
logger.error(f"Error processing file {file.path}: {e}")
continue
file_manager.print_progress(fileno, len(files))