[REF] _read_group_raw doesn't exists anymore

`_read_group_raw` has been remove in previous commit. Convert all usage.

Part-of: odoo/odoo#110737
This commit is contained in:
Rémy Voet (ryv)
2023-04-19 21:58:28 +02:00
parent 81a892ac8c
commit b60cf1f977
3 changed files with 51 additions and 211 deletions
+15 -11
View File
@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import babel.dates
import werkzeug
from odoo import http, fields, tools
from odoo import http, fields, tools, models
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.portal.controllers.portal import pager as portal_pager
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.osv import expression
from odoo.tools.misc import get_lang
class PortalMailGroup(http.Controller):
@@ -23,22 +25,24 @@ class PortalMailGroup(http.Controller):
def _get_archives(self, group_id):
"""Return the different date range and message count for the group messages."""
domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group_id)]])
results = request.env['mail.group.message']._read_group_raw(
results = request.env['mail.group.message']._read_group(
domain,
['subject', 'create_date'],
groupby=['create_date'], orderby='create_date')
groupby=['create_date:month'],
aggregates=['__count'],
)
date_groups = []
for result in results:
(dates_range, label) = result['create_date']
start, end = dates_range.split('/')
locale = get_lang(self.env).code
fmt = models.READ_GROUP_DISPLAY_FORMAT['month']
interval = models.READ_GROUP_TIME_GRANULARITY['month']
for start, count in results:
label = babel.dates.format_datetime(start, format=fmt, locale=locale)
date_groups.append({
'date': label,
'date_begin': fields.Date.to_string(fields.Date.to_date(start)),
'date_end': fields.Date.to_string(fields.Date.to_date(end)),
'messages_count': result['create_date_count'],
'date_begin': fields.Date.to_string(start),
'date_end': fields.Date.to_string(start + interval),
'messages_count': count,
})
thread_domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
@@ -47,45 +47,37 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
'user_ids',
]
def _get_group_by_SQL(self, task_specific_domain, count_field, select_terms, from_clause, where_clause,
where_clause_params, groupby_terms, orderby_terms, limit, offset, groupby, annotated_groupbys,
prefix_term, prefix_terms):
""" Prepare and return the SQL to be used for the read_group. """
def _where_calc(self, domain, active_test=True):
burndown_specific_domain, task_specific_domain = self._determine_domains(domain)
main_query = super()._where_calc(burndown_specific_domain, active_test)
# Build the query on `project.task` with the domain fields that are linked to that model. This is done in order
# to be able to reduce the number of treated records in the query by limiting them to the one corresponding to
# the ids that are returned from this sub query.
self.env['project.task']._flush_search(task_specific_domain, fields=self.task_specific_fields)
project_task_query = self.env['project.task']._where_calc(task_specific_domain)
project_task_from_clause, project_task_where_clause, project_task_where_clause_params = project_task_query.get_sql()
# Insert `WHERE` clause parameter that apply on `project_task` prior to the one that apply on
# `project_task_burndown_chart_report` as the `project_task` CTE is placed at the beginning of the `SQL`.
main_query._where_params = project_task_where_clause_params + main_query._where_params
# Get the stage_id `ir.model.fields`'s id in order to inject it directly in the query and avoid having to join
# on `ir_model_fields` table.
IrModelFieldsSudo = self.env['ir.model.fields'].sudo()
field_id = IrModelFieldsSudo.search([('name', '=', 'stage_id'), ('model', '=', 'project.task')]).id
# Get the date aggregation SQL statement in order to be able to inject it in the SQL.
date_group_by_field = next(filter(lambda gb: gb.startswith('date'), groupby))
date_annotated_groupby = [
annotated_groupby for annotated_groupby in annotated_groupbys
if annotated_groupby['groupby'] == date_group_by_field
][0]
date_begin, date_end = (
date_annotated_groupby['qualified_field'].replace(
'"%s"."%s"' % (self._table, date_annotated_groupby['field']), '"%s_%s"' % (date_annotated_groupby['field'], field)
)
for field in ['begin', 'end']
)
# Insert `WHERE` clause parameter that apply on `project_task` prior to the one that apply on
# `project_task_burndown_chart_report` as the `project_task` CTE is placed at the beginning of the `SQL`.
for param in reversed(project_task_where_clause_params):
where_clause_params.insert(0, param)
groupby = self.env.context['project_task_burndown_chart_report_groupby']
date_groupby = [g for g in groupby if g.startswith('date')][0]
# Computes the interval which needs to be used in the `SQL` depending on the date group by interval.
if date_annotated_groupby['groupby'].split(':')[1] != 'quarter':
interval = '1 %s' % date_annotated_groupby['groupby'].split(':')[1]
else:
interval = '3 month'
interval = date_groupby.split(':')[1]
sql_interval = '1 %s' % interval if interval != 'quarter' else '3 month'
simple_date_groupby_sql, __ = self._read_group_groupby(f"date:{interval}", main_query)
# Removing unexistant table name from the expression
simple_date_groupby_sql = simple_date_groupby_sql.replace('"project_task_burndown_chart_report".', '')
burndown_chart_query = """
WITH task_ids AS (
@@ -94,7 +86,7 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
%(task_query_where)s
),
all_stage_task_moves AS (
SELECT count(*) as %(count_field)s,
SELECT count(*) as __count,
sum(planned_hours) as planned_hours,
project_id,
%(date_begin)s as date_begin,
@@ -177,44 +169,22 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
project_id,
stage_id,
date,
%(count_field)s
__count
FROM all_stage_task_moves t
JOIN LATERAL generate_series(t.date_begin, t.date_end-INTERVAL '1 day', '%(interval)s')
AS date ON TRUE
""" % {
'task_query_from': project_task_from_clause,
'task_query_where': prefix_term('WHERE', project_task_where_clause),
'count_field': count_field,
'date_begin': date_begin,
'date_end': date_end,
'interval': interval,
'task_query_where': f'WHERE {project_task_where_clause}' if project_task_where_clause else '',
'date_begin': simple_date_groupby_sql.replace('"date"', '"date_begin"'),
'date_end': simple_date_groupby_sql.replace('"date"', '"date_end"'),
'interval': sql_interval,
'field_id': field_id,
}
# Replace, in the `FROM` clause generated on `project_task_burndown_chart_report`, the
# `project_task_burndown_chart_report` table name by the burndown_chart_query `SQL` aliased as
# `project_task_burndown_chart_report`.
from_clause = from_clause.replace('"project_task_burndown_chart_report"', '(%s) AS "project_task_burndown_chart_report"' % burndown_chart_query, 1)
main_query._tables['project_task_burndown_chart_report'] = burndown_chart_query
return """
SELECT min("%(table)s".id) AS id, sum(%(table)s.%(count_field)s) AS "%(count_field)s" %(extra_fields)s
FROM %(from)s
%(where)s
%(groupby)s
%(orderby)s
%(limit)s
%(offset)s
""" % {
'table': self._table,
'count_field': count_field,
'extra_fields': prefix_terms(',', select_terms),
'from': from_clause,
'where': prefix_term('WHERE', where_clause),
'groupby': prefix_terms('GROUP BY', groupby_terms),
'orderby': prefix_terms('ORDER BY', orderby_terms),
'limit': prefix_term('LIMIT', int(limit) if limit else None),
'offset': prefix_term('OFFSET', int(offset) if limit else None),
}
return main_query
@api.model
def _validate_group_by(self, groupby):
@@ -252,150 +222,16 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
non_task_specific_domain = filter_domain_leaf(domain, lambda field: field not in self.task_specific_fields)
return non_task_specific_domain, task_specific_domain
@api.model
def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
""" Although not being a good practice, this code is, for a big part, duplicated from `read_group_raw` from
`models.py`. In order to be able to use the report on big databases, it is necessary to inject `WHERE`
statements at the lowest levels in the report `SQL`. As a result, using a view was no more an option as
`Postgres` could not optimise the `SQL`.
The code of `fill_temporal` has been removed from what's available in `models.py` as it is not relevant in the
context of the Burndown Chart. Indeed, series are generated so no empty are returned by the `SQL`, except if
explicitly specified in the domain through the `date` field, which is then expected.
"""
# TODO
# --- Below code is custom
def _read_group_select(self, aggregate_spec, query):
if aggregate_spec == '__count':
return f'SUM("{self._table}"."__count")', []
return super()._read_group_select(aggregate_spec, query)
def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None):
self._validate_group_by(groupby)
burndown_specific_domain, task_specific_domain = self._determine_domains(domain)
self = self.with_context(project_task_burndown_chart_report_groupby=groupby)
# --- Below code is from models.py read_group_raw
self.check_access_rights('read')
query = self._where_calc(burndown_specific_domain)
fields = fields or [f.name for f in self._fields.values() if f.store]
groupby = [groupby] if isinstance(groupby, str) else list(OrderedSet(groupby))
groupby_list = groupby[:1] if lazy else groupby
annotated_groupbys = [self._read_group_process_groupby(gb, query) for gb in groupby_list]
groupby_fields = [g['field'] for g in annotated_groupbys]
order = orderby or ','.join([g for g in groupby_list])
groupby_dict = {gb['groupby']: gb for gb in annotated_groupbys}
self._apply_ir_rules(query, 'read')
for gb in groupby_fields:
if gb not in self._fields:
raise UserError(_("Unknown field %r in 'groupby'", gb))
if not self._fields[gb].base_field.groupable:
raise UserError(_(
"Field %s is not a stored field, only stored fields (regular or "
"many2many) are valid for the 'groupby' parameter", self._fields[gb],
))
aggregated_fields = []
select_terms = []
fnames = [] # list of fields to flush
for fspec in fields:
if fspec == 'sequence':
continue
if fspec == '__count':
# the web client sometimes adds this pseudo-field in the list
continue
match = regex_field_agg.match(fspec)
if not match:
raise UserError(_("Invalid field specification %r.", fspec))
name, func, fname = match.groups()
if func:
# we have either 'name:func' or 'name:func(fname)'
fname = fname or name
field = self._fields.get(fname)
if not field:
raise ValueError(_("Invalid field %r on model %r", (fname, self._name)))
if not (field.base_field.store and field.base_field.column_type):
raise UserError(_("Cannot aggregate field %r.", fname))
if func not in VALID_AGGREGATE_FUNCTIONS:
raise UserError(_("Invalid aggregation function %r.", func))
else:
# we have 'name', retrieve the aggregator on the field
field = self._fields.get(name)
if not field:
raise ValueError(_("Invalid field %r on model %r", (name, self._name)))
if not (field.base_field.store and
field.base_field.column_type and field.group_operator):
continue
func, fname = field.group_operator, name
fnames.append(fname)
if fname in groupby_fields:
continue
if name in aggregated_fields:
raise UserError(_("Output name %r is used twice.", name))
aggregated_fields.append(name)
expr = self._inherits_join_calc(self._table, fname, query)
if func.lower() == 'count_distinct':
term = 'COUNT(DISTINCT %s) AS "%s"' % (expr, name)
else:
term = '%s(%s) AS "%s"' % (func, expr, name)
select_terms.append(term)
for gb in annotated_groupbys:
select_terms.append('%s as "%s" ' % (gb['qualified_field'], gb['groupby']))
# --- Below code is custom
# --- As the report is base on `project.task` we flush that specific model
# self._flush_search(domain, fields=fnames + groupby_fields)
self.env['project.task']._flush_search(task_specific_domain, fields=self.task_specific_fields)
# --- Below code is from models.py read_group_raw
groupby_terms, orderby_terms = self._read_group_prepare(order, aggregated_fields, annotated_groupbys, query)
from_clause, where_clause, where_clause_params = query.get_sql()
if lazy and (len(groupby_fields) >= 2 or not self._context.get('group_by_no_leaf')):
count_field = groupby_fields[0] if len(groupby_fields) >= 1 else '_'
else:
count_field = '_'
count_field += '_count'
prefix_terms = lambda prefix, terms: (prefix + " " + ",".join(terms)) if terms else ''
prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else ''
# --- Below code is custom
query = self._get_group_by_SQL(task_specific_domain, count_field, select_terms, from_clause, where_clause,
where_clause_params, groupby_terms, orderby_terms, limit, offset, groupby,
annotated_groupbys, prefix_term, prefix_terms)
# --- Below code is from models.py read_group_raw
self._cr.execute(query, where_clause_params)
fetched_data = self._cr.dictfetchall()
if not groupby_fields:
return fetched_data
self._read_group_resolve_many2x_fields(fetched_data, annotated_groupbys)
data = [{k: self._read_group_prepare_data(k, v, groupby_dict) for k, v in r.items()} for r in fetched_data]
result = [self._read_group_format_result(d, annotated_groupbys, groupby, domain) for d in data]
# --- Below code is custom
# --- We removed fill_temporal handling as not relevant in the context of the Burndown Chart
# --- Below code is from models.py read_group_raw
if lazy:
# Right now, read_group only fill results in lazy mode (by default).
# If you need to have the empty groups in 'eager' mode, then the
# method _read_group_fill_results need to be completely reimplemented
# in a sane way
result = self._read_group_fill_results(
domain, groupby_fields[0], groupby[len(annotated_groupbys):],
aggregated_fields, count_field, result, read_group_order=order,
)
return result
return super()._read_group(
domain=domain, groupby=groupby, aggregates=aggregates,
having=having, offset=offset, limit=limit, order=order,
)
@@ -2456,7 +2456,7 @@ export class DynamicGroupList extends DynamicList {
}
// When group_by_no_leaf key is present FIELD_ID_count doesn't exist
// we have to get the count from `__count` instead
// see _read_group_raw in models.py
// see _read_group in models.py
case `__count`:
case `${groupByField.name}_count`: {
groupParams.count = value;