mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-18 03:32:08 +02:00
[16.1] Backport view modifier compatibility
This commit is contained in:
parent
956889352c
commit
c452dace3f
4 changed files with 173 additions and 0 deletions
|
|
@ -188,6 +188,25 @@ class AccountAnalyticPlan(models.Model):
|
||||||
'company_id': self.env.company.id,
|
'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):
|
class AccountAnalyticApplicability(models.Model):
|
||||||
_name = 'account.analytic.applicability'
|
_name = 'account.analytic.applicability'
|
||||||
|
|
|
||||||
|
|
@ -1133,6 +1133,141 @@ actual arch.
|
||||||
transfer_modifiers_to_node(modifiers, node)
|
transfer_modifiers_to_node(modifiers, node)
|
||||||
return tree
|
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):
|
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
|
""" Process the given architecture, modifying it in-place to add and
|
||||||
remove stuff.
|
remove stuff.
|
||||||
|
|
@ -1166,6 +1301,9 @@ actual arch.
|
||||||
while stack:
|
while stack:
|
||||||
node, editable = stack.pop()
|
node, editable = stack.pop()
|
||||||
|
|
||||||
|
if isinstance(node.tag, str):
|
||||||
|
self._apply_modern_modifiers(node)
|
||||||
|
|
||||||
# compute default
|
# compute default
|
||||||
tag = node.tag
|
tag = node.tag
|
||||||
had_parent = node.getparent() is not None
|
had_parent = node.getparent() is not None
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,4 @@ from . import test_neutralize
|
||||||
from . import test_config_parameter
|
from . import test_config_parameter
|
||||||
from . import test_ir_module_category
|
from . import test_ir_module_category
|
||||||
from . import test_num2words_ar
|
from . import test_num2words_ar
|
||||||
|
from . import test_view_modifiers
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,21 @@ class MailComposer(models.TransientModel):
|
||||||
if self._context.get('custom_layout') and 'default_email_layout_xmlid' not in self._context:
|
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'])
|
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)
|
result = super(MailComposer, self).default_get(fields)
|
||||||
|
|
||||||
# author
|
# author
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue