diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index 04ddfd21db1..b18bb9a1f2a 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict +import itertools from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -131,22 +132,25 @@ class AccountAnalyticAccount(models.Model): return super().copy_data(default) @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - """ - Override read_group to calculate the sum of the non-stored fields that depend on the user context - """ - res = super(AccountAnalyticAccount, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - accounts = self.env['account.analytic.account'] - for line in res: - if '__domain' in line: - accounts = self.search(line['__domain']) - if 'balance' in fields: - line['balance'] = sum(accounts.mapped('balance')) - if 'debit' in fields: - line['debit'] = sum(accounts.mapped('debit')) - if 'credit' in fields: - line['credit'] = sum(accounts.mapped('credit')) - return res + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): + """ Override _read_group to aggregate no-store compute: balance/debit/credit """ + SPECIAL = {'balance:sum', 'debit:sum', 'credit:sum'} + if SPECIAL.isdisjoint(aggregates): + return super()._read_group(domain, groupby, aggregates, having, offset, limit, order) + + base_aggregates = [*(agg for agg in aggregates if agg not in SPECIAL), 'id:recordset'] + base_result = super()._read_group(domain, groupby, base_aggregates, having, offset, limit, order) + + # base_result = [(a1, b1, records), (a2, b2, records), ...] + result = [] + for *other, records in base_result: + for index, spec in enumerate(itertools.chain(groupby, aggregates)): + if spec in SPECIAL: + field_name = spec.split(':')[0] + other.insert(index, sum(records.mapped(field_name))) + result.append(tuple(other)) + + return result @api.depends('line_ids.amount') def _compute_debit_credit_balance(self): diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py index 9f6b15c83d0..1fde6a68aee 100644 --- a/addons/product_margin/models/product_product.py +++ b/addons/product_margin/models/product_product.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import itertools import time from odoo import api, fields, models @@ -47,50 +48,37 @@ class ProductProduct(models.Model): help="Expected margin * 100 / Expected Sale") @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): """ - Inherit read_group to calculate the sum of the non-stored fields, as it is not automatically done anymore through the XML. + Inherit _read_group to calculate the sum of the non-stored fields, as it is not automatically done anymore through the XML. """ fields_list = ['turnover', 'sale_avg_price', 'sale_purchase_price', 'sale_num_invoiced', 'purchase_num_invoiced', 'sales_gap', 'purchase_gap', 'total_cost', 'sale_expected', 'normal_cost', 'total_margin', 'expected_margin', 'total_margin_rate', 'expected_margin_rate'] + SPECIAL = {f'{field_name}:sum' for field_name in fields_list} # Only sum is allowed - # Not any of the fields_list support aggregate function like :sum - def truncate_aggr(field): - field_no_aggr, _sep, agg = field.partition(':') - if field_no_aggr in fields_list: - if agg and agg != 'sum': - raise NotImplementedError('Aggregate functions other than \':sum\' are not allowed.') - return field_no_aggr - return field - fields = {truncate_aggr(field) for field in fields} + if SPECIAL.isdisjoint(aggregates): + return super()._read_group(domain, groupby, aggregates, having, offset, limit, order) - res = super(ProductProduct, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - if any(x in fields for x in fields_list): - # Calculate first for every product in which line it needs to be applied - re_ind = 0 - prod_re = {} - tot_products = self.browse([]) - for re in res: - if re.get('__domain'): - products = self.search(re['__domain']) - tot_products |= products - for prod in products: - prod_re[prod.id] = re_ind - re_ind += 1 - res_val = tot_products._compute_product_margin_fields_values(field_names=[x for x in fields if fields in fields_list]) - for key in res_val: - for l in res_val[key]: - re = res[prod_re[key]] - if re.get(l): - re[l] += res_val[key][l] - else: - re[l] = res_val[key][l] - return res + base_aggregates = [*(agg for agg in aggregates if agg not in SPECIAL), 'id:recordset'] + base_result = super()._read_group(domain, groupby, base_aggregates, having, offset, limit, order) - def _compute_product_margin_fields_values(self, field_names=None): - if field_names is None: - field_names = [] + # Force the compute with all records + all_records = self.browse().union(*(item[-1] for item in base_result)) + all_records._compute_product_margin_fields_values() + + # base_result = [(a1, b1, records), (a2, b2, records), ...] + result = [] + for *other, records in base_result: + for index, spec in enumerate(itertools.chain(groupby, aggregates)): + if spec in SPECIAL: + field_name = spec.split(':')[0] + other.insert(index, sum(records.mapped(field_name))) + result.append(tuple(other)) + + return result + + def _compute_product_margin_fields_values(self): date_from = self.env.context.get('date_from', time.strftime('%Y-01-01')) date_to = self.env.context.get('date_to', time.strftime('%Y-12-31')) invoice_state = self.env.context.get('invoice_state', 'open_paid') diff --git a/addons/project/report/project_task_burndown_chart_report.py b/addons/project/report/project_task_burndown_chart_report.py index 08cc8045a7e..3a1978854ac 100644 --- a/addons/project/report/project_task_burndown_chart_report.py +++ b/addons/project/report/project_task_burndown_chart_report.py @@ -2,9 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models -from odoo.models import regex_field_agg, VALID_AGGREGATE_FUNCTIONS from odoo.exceptions import UserError -from odoo.tools import OrderedSet from odoo.addons.resource.models.utils import filter_domain_leaf diff --git a/addons/purchase_stock/report/vendor_delay_report.py b/addons/purchase_stock/report/vendor_delay_report.py index dde37851510..aac602768c9 100644 --- a/addons/purchase_stock/report/vendor_delay_report.py +++ b/addons/purchase_stock/report/vendor_delay_report.py @@ -54,37 +54,9 @@ FROM stock_move m GROUP BY m.id )""") - @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - if all('on_time_rate' not in field for field in fields): - res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - return res - - for field in fields: - if 'on_time_rate' not in field: - continue - - fields.remove(field) - - agg = field.split(':')[1:] - if agg and agg[0] != 'sum': - raise NotImplementedError('Aggregate functions other than \':sum\' are not allowed.') - - qty_total = field.replace('on_time_rate', 'qty_total') - if qty_total not in fields: - fields.append(qty_total) - qty_on_time = field.replace('on_time_rate', 'qty_on_time') - if qty_on_time not in fields: - fields.append(qty_on_time) - break - - res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - - for group in res: - if group['qty_total'] == 0: - on_time_rate = 100 - else: - on_time_rate = group['qty_on_time'] / group['qty_total'] * 100 - group.update({'on_time_rate': on_time_rate}) - - return res + def _read_group_select(self, aggregate_spec, query): + if aggregate_spec == 'on_time_rate:sum': + # Make a weigthed average instead of simple average for these fields + sql_expression = 'SUM(qty_on_time) / SUM(qty_total) * 100' + return sql_expression, ['on_time_rate', 'qty_on_time', 'qty_total'] + return super()._read_group_select(aggregate_spec, query) diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 99f48e4feb7..714b7caecaa 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -293,27 +293,16 @@ class StockQuant(models.Model): """ Only allowed fields should be modified """ return super(StockQuant, self.with_context(inventory_mode=True))._load_records_write(values) - @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - """ Override to set the `inventory_quantity` field if we're in "inventory mode" as well - as to compute the sum of the `available_quantity` field. - """ - if 'available_quantity' in fields: - if 'quantity' not in fields: - fields.append('quantity') - if 'reserved_quantity' not in fields: - fields.append('reserved_quantity') - if 'inventory_quantity_auto_apply' in fields and 'quantity' not in fields: - fields.append('quantity') - result = super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - for group in result: - if self.env.context.get('inventory_report_mode'): - group['inventory_quantity'] = False - if 'available_quantity' in fields: - group['available_quantity'] = group['quantity'] - group['reserved_quantity'] - if 'inventory_quantity_auto_apply' in fields: - group['inventory_quantity_auto_apply'] = group['quantity'] - return result + def _read_group_select(self, aggregate_spec, query): + if aggregate_spec == 'inventory_quantity:sum' and self.env.context.get('inventory_report_mode'): + return "NULL", [] + if aggregate_spec == 'available_quantity:sum': + quantity_expr, quantity_fnames = self._read_group_select('quantity:sum', query) + reserved_quantity_expr, reserved_quantity_fnames = self._read_group_select('reserved_quantity:sum', query) + return f'{quantity_expr} - {reserved_quantity_expr}', quantity_fnames + reserved_quantity_fnames + if aggregate_spec == 'inventory_quantity_auto_apply:sum': + return self._read_group_select('quantity:sum', query) + return super()._read_group_select(aggregate_spec, query) @api.model def get_import_templates(self): diff --git a/addons/stock_account/models/stock_quant.py b/addons/stock_account/models/stock_quant.py index 3736a69cf63..441a54f5f82 100644 --- a/addons/stock_account/models/stock_quant.py +++ b/addons/stock_account/models/stock_quant.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import itertools from odoo import api, fields, models, _ from odoo.tools.float_utils import float_is_zero from odoo.tools.misc import groupby @@ -37,19 +38,28 @@ class StockQuant(models.Model): quant.value = quant.quantity * quant.product_id.with_company(quant.company_id).value_svl / quantity @api.model - def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): """ This override is done in order for the grouped list view to display the total value of the quants inside a location. This doesn't work out of the box because `value` is a computed field. """ - if 'value' not in fields: - return super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - res = super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) - for group in res: - if group.get('__domain'): - quants = self.search(group['__domain']) - group['value'] = sum(quant.value for quant in quants) - return res + SPECIAL = {'value:sum'} + if SPECIAL.isdisjoint(aggregates): + return super()._read_group(domain, groupby, aggregates, having, offset, limit, order) + + base_aggregates = [*(agg for agg in aggregates if agg not in SPECIAL), 'id:recordset'] + base_result = super()._read_group(domain, groupby, base_aggregates, having, offset, limit, order) + + # base_result = [(a1, b1, records), (a2, b2, records), ...] + result = [] + for *other, records in base_result: + for index, spec in enumerate(itertools.chain(groupby, aggregates)): + if spec in SPECIAL: + field_name = spec.split(':')[0] + other.insert(index, sum(records.mapped(field_name))) + result.append(tuple(other)) + + return result def _apply_inventory(self): for accounting_date, inventory_ids in groupby(self, key=lambda q: q.accounting_date):