diff --git a/odoo-bringout-oca-ocb-analytic/analytic/models/analytic_plan.py b/odoo-bringout-oca-ocb-analytic/analytic/models/analytic_plan.py index 0c518a18..de2d12cb 100644 --- a/odoo-bringout-oca-ocb-analytic/analytic/models/analytic_plan.py +++ b/odoo-bringout-oca-ocb-analytic/analytic/models/analytic_plan.py @@ -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' diff --git a/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_ui_view.py b/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_ui_view.py index 876b88b8..1c7194b1 100644 --- a/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_ui_view.py +++ b/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_ui_view.py @@ -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 diff --git a/odoo-bringout-oca-ocb-base/odoo/addons/base/tests/__init__.py b/odoo-bringout-oca-ocb-base/odoo/addons/base/tests/__init__.py index 9d842165..5e3f5b2f 100644 --- a/odoo-bringout-oca-ocb-base/odoo/addons/base/tests/__init__.py +++ b/odoo-bringout-oca-ocb-base/odoo/addons/base/tests/__init__.py @@ -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 diff --git a/odoo-bringout-oca-ocb-mail/mail/wizard/mail_compose_message.py b/odoo-bringout-oca-ocb-mail/mail/wizard/mail_compose_message.py index d23f94ee..05aaa921 100644 --- a/odoo-bringout-oca-ocb-mail/mail/wizard/mail_compose_message.py +++ b/odoo-bringout-oca-ocb-mail/mail/wizard/mail_compose_message.py @@ -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