{"record":{"id":"d5b94ae0a7ebd615","repo":"odoo/odoo","slug":"cannot-get-aggregation-details-from-a-line-not-usi","errorCode":null,"errorMessage":"Cannot get aggregation details from a line not using 'aggregation' engine","messagePattern":"Cannot get aggregation details from a line not using 'aggregation' engine","errorType":"exception","errorClass":"UserError","httpStatus":null,"severity":"error","filePath":"addons/account/models/account_report.py","lineNumber":870,"sourceCode":"            if domains:\n                sub_expressions |= self.env['account.report.expression'].search(Domain.OR(domains))\n\n            to_expand = sub_expressions.filtered(lambda x: x.engine == 'aggregation' and x not in result)\n            result |= sub_expressions\n\n        return result\n\n    def _get_aggregation_terms_details(self):\n        \"\"\" Computes the details of each aggregation expression in self, and returns them in the form of a single dict aggregating all the results.\n\n        Example of aggregation details:\n        formula 'A.balance + B.balance + A.other'\n        will return: {'A': {'balance', 'other'}, 'B': {'balance'}}\n        \"\"\"\n        totals_by_code = defaultdict(set)\n        for expression in self:\n            if expression.engine != 'aggregation':\n                raise UserError(_(\"Cannot get aggregation details from a line not using 'aggregation' engine\"))\n\n            expression_terms = re.split('[-+/*]', re.sub(r'[\\s()]', '', expression.formula))\n            for term in expression_terms:\n                if term and not re.match(r'^([0-9]*[.])?[0-9]*$', term): # term might be empty if the formula contains a negative term\n                    line_code, total_name = term.split('.')\n                    totals_by_code[line_code].add(total_name)\n\n            if expression.subformula:\n                if_other_expr_match = re.match(r'if_other_expr_(above|below)\\((?P<line_code>.+)[.](?P<expr_label>.+),.+\\)', expression.subformula)\n                if if_other_expr_match:\n                    totals_by_code[if_other_expr_match['line_code']].add(if_other_expr_match['expr_label'])\n\n        return totals_by_code\n\n    def _get_matching_tags(self):\n        \"\"\" Returns all the signed account.account.tags records whose name matches any of the formulas of the tax_tags expressions contained in self.\n        \"\"\"\n        tag_expressions = self.filtered(lambda x: x.engine == 'tax_tags')","sourceCodeStart":852,"sourceCodeEnd":888,"githubUrl":"https://github.com/odoo/odoo/blob/1e661df964b1b264c9cef3ab28430d4785be3fda/addons/account/models/account_report.py#L852-L888","documentation":"Raised by `_get_aggregation_terms_details()` on `account.report.expression`: this helper parses aggregation formulas by splitting them into `line_code.total` terms, so every expression in `self` must have `engine == 'aggregation'`. Passing any expression with a different engine (`domain`, `tax_tags`, `account_codes`, `external`) is a programming error.","triggerScenarios":"Calling `expression._get_aggregation_terms_details()` where `expression` (or any record in the recordset) has an engine other than `'aggregation'`. In framework code this is called on `to_expand` candidates; in custom code it is usually called on an unfiltered expression recordset.","commonSituations":"Custom report tooling iterating over all expressions of a line/report without filtering by engine; changing an expression's engine after formulas were set; subclass code reusing the helper for validation.","solutions":["Filter first: `expressions.filtered(lambda e: e.engine == 'aggregation')._get_aggregation_terms_details()`","If a term is genuinely computed another way, move it to an `aggregation`-engine expression or stop feeding it to this helper"],"exampleFix":"# before\ndetails = line.expression_ids._get_aggregation_terms_details()  # may contain non-aggregation expressions\n\n# after\ndetails = line.expression_ids.filtered(lambda e: e.engine == 'aggregation')._get_aggregation_terms_details()","handlingStrategy":"type-guard","validationCode":"agg_exprs = expressions.filtered(lambda e: e.engine == 'aggregation')\ndetails = agg_exprs._get_aggregation_terms_details()","typeGuard":"def is_aggregation_expression(expr) -> bool:\n    \"\"\"_get_aggregation_terms_details only accepts aggregation-engine expressions.\"\"\"\n    return expr.engine == 'aggregation'","tryCatchPattern":"from odoo.exceptions import UserError\ntry:\n    details = exprs._get_aggregation_terms_details()\nexcept UserError:\n    details = exprs.filtered(lambda e: e.engine == 'aggregation')._get_aggregation_terms_details()","preventionTips":["Always filter recordsets by engine before calling engine-specific helpers","Keep `_get_aggregation_terms_details` calls inside aggregation-related code paths only"],"tags":["odoo","accounting","account-report","aggregation","engine","api-misuse"],"backgroundTag":null,"analyzedSha":"1e661df964b1b264c9cef3ab28430d4785be3fda","analyzedAt":"2026-08-15T05:22:16.142Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}