[16.1] Backport view modifier compatibility

This commit is contained in:
Ernad Husremovic 2025-11-02 11:24:42 +01:00
parent 956889352c
commit c452dace3f
4 changed files with 173 additions and 0 deletions

View file

@ -188,6 +188,25 @@ class AccountAnalyticPlan(models.Model):
'company_id': self.env.company.id,
})
def _find_plan_column(self, model='account.analytic.line'):
self.ensure_one()
if not model:
model = 'account.analytic.line'
try:
model_obj = self.env[model]
except KeyError:
return None
fields_map = model_obj._fields
candidate = f'account_id_plan_{self.id}'
field = fields_map.get(candidate)
if field:
setattr(field, '_account_move_export_plan_specific', True)
return field
fallback = fields_map.get('account_id')
if fallback:
setattr(fallback, '_account_move_export_plan_specific', False)
return fallback
class AccountAnalyticApplicability(models.Model):
_name = 'account.analytic.applicability'

View file

@ -1133,6 +1133,141 @@ actual arch.
transfer_modifiers_to_node(modifiers, node)
return tree
def _apply_modern_modifiers(self, node):
"""Translate OWL-style inline modifiers to classic attrs entries."""
convertible = ('invisible', 'readonly', 'required')
attrs_dict = None
updated = False
def ensure_attrs():
nonlocal attrs_dict, updated
if attrs_dict is None:
current = node.get('attrs')
attrs_dict = ast.literal_eval(current.strip()) if current else {}
updated = True
return attrs_dict
for attr in convertible:
raw_value = node.attrib.get(attr)
if not raw_value:
continue
value = raw_value.strip()
try:
str2bool(value)
continue
except ValueError:
pass
domain = self._expression_to_domain(value)
if domain is None:
continue
attrs = ensure_attrs()
existing = attrs.get(attr)
if isinstance(existing, bool):
if existing:
node.attrib.pop(attr, None)
continue
existing = []
elif isinstance(existing, (list, tuple)):
existing = list(existing)
else:
existing = []
attrs[attr] = existing + list(domain)
node.attrib.pop(attr, None)
if updated and attrs_dict is not None:
node.set('attrs', repr(attrs_dict))
def _expression_to_domain(self, expr):
try:
tree = ast.parse(expr, mode='eval')
except SyntaxError:
return None
return self._domain_from_ast(tree.body)
def _domain_from_ast(self, node):
if isinstance(node, ast.BoolOp):
domains = [self._domain_from_ast(value) for value in node.values]
if any(d is None for d in domains):
return None
if isinstance(node.op, ast.And):
result = []
for domain in domains:
result.extend(domain)
return result
if isinstance(node.op, ast.Or):
result = []
for domain in domains:
if not domain:
continue
if not result:
result.extend(domain)
else:
result = ['|'] + result + list(domain)
return result
return None
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
domain = self._domain_from_ast(node.operand)
if domain is None:
return None
return ['!'] + domain
if isinstance(node, ast.Compare):
if len(node.ops) != 1 or len(node.comparators) != 1:
return None
field = self._field_name_from_ast(node.left)
value = self._literal_from_ast(node.comparators[0])
operator = self._operator_from_ast(node.ops[0])
if field is None or value is None or operator is None:
return None
return [(field, operator, value)]
if isinstance(node, ast.Name):
if node.id in ('True', 'False'):
return [] if node.id == 'True' else ['!', ('id', '!=', False)]
return [(node.id, '=', True)]
if isinstance(node, ast.Constant):
return [] if bool(node.value) else ['!', ('id', '!=', False)]
return None
@staticmethod
def _operator_from_ast(op):
mapping = {
ast.Eq: '=',
ast.NotEq: '!=',
ast.Gt: '>',
ast.GtE: '>=',
ast.Lt: '<',
ast.LtE: '<=',
ast.In: 'in',
ast.NotIn: 'not in',
ast.Is: '=',
ast.IsNot: '!=',
}
for ast_type, operator in mapping.items():
if isinstance(op, ast_type):
return operator
return None
@staticmethod
def _field_name_from_ast(node):
if isinstance(node, ast.Name):
return node.id
return None
def _literal_from_ast(self, node):
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
value = self._literal_from_ast(node.operand)
if isinstance(value, (int, float)):
return -value
return None
if isinstance(node, ast.NameConstant):
return node.value
return None
def _postprocess_view(self, node, model_name, editable=True, parent_name_manager=None, **options):
""" Process the given architecture, modifying it in-place to add and
remove stuff.
@ -1166,6 +1301,9 @@ actual arch.
while stack:
node, editable = stack.pop()
if isinstance(node.tag, str):
self._apply_modern_modifiers(node)
# compute default
tag = node.tag
had_parent = node.getparent() is not None

View file

@ -65,3 +65,4 @@ from . import test_neutralize
from . import test_config_parameter
from . import test_ir_module_category
from . import test_num2words_ar
from . import test_view_modifiers

View file

@ -55,6 +55,21 @@ class MailComposer(models.TransientModel):
if self._context.get('custom_layout') and 'default_email_layout_xmlid' not in self._context:
self = self.with_context(default_email_layout_xmlid=self._context['custom_layout'])
if self._context.get('default_res_ids') and not self._context.get('default_res_id'):
res_ids = self._context['default_res_ids']
if isinstance(res_ids, str):
try:
res_ids = ast.literal_eval(res_ids)
except (ValueError, SyntaxError):
res_ids = [res_ids]
if isinstance(res_ids, int):
res_ids = [res_ids]
if res_ids:
new_ctx = dict(self._context)
new_ctx.setdefault('active_ids', res_ids)
new_ctx['default_res_id'] = res_ids[0]
self = self.with_context(**new_ctx)
result = super(MailComposer, self).default_get(fields)
# author