diff --git a/addons/pos_sale/report/sale_report.py b/addons/pos_sale/report/sale_report.py
index 59632df0c993fdbb83655398ece9bac3cab17329..cf3ec8f47239809bfa97b9b1249a98226f09d404 100644
--- a/addons/pos_sale/report/sale_report.py
+++ b/addons/pos_sale/report/sale_report.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-from odoo import tools
 from odoo import api, fields, models
 
 
@@ -10,27 +9,29 @@ class SaleReport(models.Model):
 
     @api.model
     def _get_done_states(self):
-        done_states = super(SaleReport, self)._get_done_states()
+        done_states = super()._get_done_states()
         done_states.extend(['pos_done', 'invoiced'])
         return done_states
 
-    state = fields.Selection(selection_add=[('pos_draft', 'New'),
-                                            ('paid', 'Paid'),
-                                            ('pos_done', 'Posted'),
-                                            ('invoiced', 'Invoiced')], string='Status', readonly=True)
+    state = fields.Selection(
+        selection_add=[
+            ('pos_draft', 'New'),
+            ('paid', 'Paid'),
+            ('pos_done', 'Posted'),
+            ('invoiced', 'Invoiced')
+        ],
+    )
 
-    def _select_pos(self, fields=None):
-        if not fields:
-            fields = {}
-        select_ = '''
+    def _select_pos(self):
+        select_ = """
             MIN(l.id) AS id,
             l.product_id AS product_id,
             t.uom_id AS product_uom,
-            sum(l.qty) AS product_uom_qty,
-            sum(l.qty) AS qty_delivered,
-            0 as qty_to_deliver,
-            CASE WHEN pos.state = 'invoiced' THEN sum(l.qty) ELSE 0 END AS qty_invoiced,
-            CASE WHEN pos.state != 'invoiced' THEN sum(l.qty) ELSE 0 END AS qty_to_invoice,
+            SUM(l.qty) AS product_uom_qty,
+            SUM(l.qty) AS qty_delivered,
+            0 AS qty_to_deliver,
+            CASE WHEN pos.state = 'invoiced' THEN SUM(l.qty) ELSE 0 END AS qty_invoiced,
+            CASE WHEN pos.state != 'invoiced' THEN SUM(l.qty) ELSE 0 END AS qty_to_invoice,
             SUM(l.price_subtotal_incl) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total,
             SUM(l.price_subtotal) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal,
             (CASE WHEN pos.state != 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice,
@@ -45,7 +46,6 @@ class SaleReport(models.Model):
             NULL AS campaign_id,
             NULL AS medium_id,
             NULL AS source_id,
-            extract(epoch from avg(date_trunc('day',pos.date_order)-date_trunc('day',pos.create_date)))/(24*60*60)::decimal(16,2) AS delay,
             t.categ_id AS categ_id,
             pos.pricelist_id AS pricelist_id,
             NULL AS analytic_account_id,
@@ -54,37 +54,36 @@ class SaleReport(models.Model):
             partner.country_id AS country_id,
             partner.industry_id AS industry_id,
             partner.commercial_partner_id AS commercial_partner_id,
-            (sum(t.weight) * l.qty / u.factor) AS weight,
-            (sum(t.volume) * l.qty / u.factor) AS volume,
-            l.discount as discount,
-            sum((l.price_unit * l.discount * l.qty / 100.0 / CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END)) as discount_amount,
-            NULL as order_id
-        '''
+            (SUM(t.weight) * l.qty / u.factor) AS weight,
+            (SUM(t.volume) * l.qty / u.factor) AS volume,
+            l.discount AS discount,
+            SUM((l.price_unit * l.discount * l.qty / 100.0 / CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END)) AS discount_amount,
+            NULL AS order_id"""
 
-        for field in fields.keys():
-            select_ += ', NULL AS %s' % (field)
+        additional_fields_info = self._select_additional_fields()
+        template = """,
+            NULL AS %s"""
+        for fname in additional_fields_info.keys():
+            select_ += template % fname
         return select_
 
     def _from_pos(self):
-        from_ = '''
+        return """
             pos_order_line l
-                  join pos_order pos on (l.order_id=pos.id)
-                  left join res_partner partner ON (pos.partner_id = partner.id OR pos.partner_id = NULL)
-                    left join product_product p on (l.product_id=p.id)
-                    left join product_template t on (p.product_tmpl_id=t.id)
-                    LEFT JOIN uom_uom u ON (u.id=t.uom_id)
-                    LEFT JOIN pos_session session ON (session.id = pos.session_id)
-                    LEFT JOIN pos_config config ON (config.id = session.config_id)
-                left join product_pricelist pp on (pos.pricelist_id = pp.id)
-        '''
-        return from_
+            JOIN pos_order pos ON l.order_id = pos.id
+            LEFT JOIN res_partner partner ON (pos.partner_id=partner.id OR pos.partner_id = NULL)
+            LEFT JOIN product_product p ON l.product_id=p.id
+            LEFT JOIN product_template t ON p.product_tmpl_id=t.id
+            LEFT JOIN uom_uom u ON u.id=t.uom_id
+            LEFT JOIN pos_session session ON session.id = pos.session_id
+            LEFT JOIN pos_config config ON config.id = session.config_id"""
 
     def _where_pos(self):
-        where_ = 'l.sale_order_line_id is NULL'
-        return where_
+        return """
+            l.sale_order_line_id IS NULL"""
 
     def _group_by_pos(self):
-        groupby_ = '''
+        return """
             l.order_id,
             l.product_id,
             l.price_unit,
@@ -104,15 +103,14 @@ class SaleReport(models.Model):
             partner.industry_id,
             partner.commercial_partner_id,
             u.factor,
-            pos.crm_team_id
-        '''
-        return groupby_
+            pos.crm_team_id"""
 
-    def _query(self, with_clause='', fields=None, groupby='', from_clause=''):
-        if not fields:
-            fields = {}
-        res = super()._query(with_clause, fields, groupby, from_clause)
-        current = '(SELECT %s FROM %s WHERE %s GROUP BY %s)' % \
-                  (self._select_pos(fields), self._from_pos(), self._where_pos(), self._group_by_pos())
-
-        return '%s UNION ALL %s' % (res, current)
+    def _query(self):
+        res = super()._query()
+        return res + f"""UNION ALL (
+            SELECT {self._select_pos()}
+            FROM {self._from_pos()}
+            WHERE {self._where_pos()}
+            GROUP BY {self._group_by_pos()}
+            )
+        """
diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py
index 7bb5546e9e48e189975ab15b32d2b1f787b94056..217384f534abdcd9000ec0ac2ede6586c458a014 100644
--- a/addons/sale/report/sale_report.py
+++ b/addons/sale/report/sale_report.py
@@ -58,68 +58,79 @@ class SaleReport(models.Model):
 
     order_id = fields.Many2one('sale.order', 'Order #', readonly=True)
 
-    def _select_sale(self, fields=None):
-        if not fields:
-            fields = {}
+    def _with_sale(self):
+        return ""
+
+    def _select_sale(self):
         select_ = """
-            coalesce(min(l.id), -s.id) as id,
-            l.product_id as product_id,
-            t.uom_id as product_uom,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as product_uom_qty,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_delivered / u.factor * u2.factor) ELSE 0 END as qty_delivered,
-            CASE WHEN l.product_id IS NOT NULL THEN SUM((l.product_uom_qty - l.qty_delivered) / u.factor * u2.factor) ELSE 0 END as qty_to_deliver,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_invoiced / u.factor * u2.factor) ELSE 0 END as qty_invoiced,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_to_invoice / u.factor * u2.factor) ELSE 0 END as qty_to_invoice,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.price_total / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as price_total,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.price_subtotal / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as price_subtotal,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.untaxed_amount_to_invoice / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as untaxed_amount_to_invoice,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(l.untaxed_amount_invoiced / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as untaxed_amount_invoiced,
-            count(*) as nbr,
-            s.name as name,
-            s.date_order as date,
-            s.state as state,
-            s.partner_id as partner_id,
-            s.user_id as user_id,
-            s.company_id as company_id,
-            s.campaign_id as campaign_id,
-            s.medium_id as medium_id,
-            s.source_id as source_id,
-            extract(epoch from avg(date_trunc('day',s.date_order)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay,
-            t.categ_id as categ_id,
-            s.pricelist_id as pricelist_id,
-            s.analytic_account_id as analytic_account_id,
-            s.team_id as team_id,
+            coalesce(min(l.id), -s.id) AS id,
+            l.product_id AS product_id,
+            t.uom_id AS product_uom,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.product_uom_qty / u.factor * u2.factor) ELSE 0 END AS product_uom_qty,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.qty_delivered / u.factor * u2.factor) ELSE 0 END AS qty_delivered,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM((l.product_uom_qty - l.qty_delivered) / u.factor * u2.factor) ELSE 0 END AS qty_to_deliver,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.qty_invoiced / u.factor * u2.factor) ELSE 0 END AS qty_invoiced,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.qty_to_invoice / u.factor * u2.factor) ELSE 0 END AS qty_to_invoice,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_total / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END AS price_total,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_subtotal / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END AS price_subtotal,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_to_invoice / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END AS untaxed_amount_to_invoice,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_invoiced / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END AS untaxed_amount_invoiced,
+            COUNT(*) AS nbr,
+            s.name AS name,
+            s.date_order AS date,
+            s.state AS state,
+            s.partner_id AS partner_id,
+            s.user_id AS user_id,
+            s.company_id AS company_id,
+            s.campaign_id AS campaign_id,
+            s.medium_id AS medium_id,
+            s.source_id AS source_id,
+            t.categ_id AS categ_id,
+            s.pricelist_id AS pricelist_id,
+            s.analytic_account_id AS analytic_account_id,
+            s.team_id AS team_id,
             p.product_tmpl_id,
-            partner.country_id as country_id,
-            partner.industry_id as industry_id,
-            partner.commercial_partner_id as commercial_partner_id,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(p.weight * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as weight,
-            CASE WHEN l.product_id IS NOT NULL THEN sum(p.volume * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as volume,
-            l.discount as discount,
-            CASE WHEN l.product_id IS NOT NULL THEN sum((l.price_unit * l.product_uom_qty * l.discount / 100.0 / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END))ELSE 0 END as discount_amount,
-            s.id as order_id
-        """
+            partner.country_id AS country_id,
+            partner.industry_id AS industry_id,
+            partner.commercial_partner_id AS commercial_partner_id,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(p.weight * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END AS weight,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM(p.volume * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END AS volume,
+            l.discount AS discount,
+            CASE WHEN l.product_id IS NOT NULL THEN SUM((l.price_unit * l.product_uom_qty * l.discount / 100.0 / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END))ELSE 0 END AS discount_amount,
+            s.id AS order_id"""
+
+        additional_fields_info = self._select_additional_fields()
+        template = """,
+            %s AS %s"""
+        for fname, query_info in additional_fields_info.items():
+            select_ += template % (query_info, fname)
 
-        for field in fields.values():
-            select_ += field
         return select_
 
-    def _from_sale(self, from_clause=''):
-        from_ = """
-                sale_order_line l
-                      right outer join sale_order s on (s.id=l.order_id)
-                      join res_partner partner on s.partner_id = partner.id
-                        left join product_product p on (l.product_id=p.id)
-                            left join product_template t on (p.product_tmpl_id=t.id)
-                    left join uom_uom u on (u.id=l.product_uom)
-                    left join uom_uom u2 on (u2.id=t.uom_id)
-                    left join product_pricelist pp on (s.pricelist_id = pp.id)
-                %s
-        """ % from_clause
-        return from_
-
-    def _group_by_sale(self, groupby=''):
-        groupby_ = """
+    def _select_additional_fields(self):
+        """Hook to return additional fields SQL specification for select part of the table query.
+
+        :returns: mapping field -> SQL computation of field, will be converted to '_ AS _field' in the final table definition
+        :rtype: dict
+        """
+        return {}
+
+    def _from_sale(self):
+        return """
+            sale_order_line l
+            RIGHT OUTER JOIN sale_order s ON s.id=l.order_id
+            JOIN res_partner partner ON s.partner_id = partner.id
+            LEFT JOIN product_product p ON l.product_id=p.id
+            LEFT JOIN product_template t ON p.product_tmpl_id=t.id
+            LEFT JOIN uom_uom u ON u.id=l.product_uom
+            LEFT JOIN uom_uom u2 ON u2.id=t.uom_id"""
+
+    def _where_sale(self):
+        return """
+            l.display_type IS NULL"""
+
+    def _group_by_sale(self):
+        return """
             l.product_id,
             l.order_id,
             t.uom_id,
@@ -141,18 +152,19 @@ class SaleReport(models.Model):
             partner.industry_id,
             partner.commercial_partner_id,
             l.discount,
-            s.id %s
-        """ % (groupby)
-        return groupby_
+            s.id"""
 
-    def _query(self, with_clause='', fields=None, groupby='', from_clause=''):
-        if not fields:
-            fields = {}
-        with_ = ("WITH %s" % with_clause) if with_clause else ""
-        return '%s (SELECT %s FROM %s WHERE l.display_type IS NULL GROUP BY %s)' % \
-               (with_, self._select_sale(fields), self._from_sale(from_clause), self._group_by_sale(groupby))
+    def _query(self):
+        with_ = self._with_sale()
+        return f"""
+            {"WITH" + with_ + "(" if with_ else ""}
+            SELECT {self._select_sale()}
+            FROM {self._from_sale()}
+            WHERE {self._where_sale()}
+            GROUP BY {self._group_by_sale()}
+            {")" if with_ else ""}
+        """
 
     def init(self):
-        # self._table = sale_report
         tools.drop_view_if_exists(self.env.cr, self._table)
-        self.env.cr.execute("""CREATE or REPLACE VIEW %s as (%s)""" % (self._table, self._query()))
+        self.env.cr.execute(f"CREATE OR REPLACE VIEW {self._table} AS {self._query()}")
diff --git a/addons/sale_margin/report/sale_report.py b/addons/sale_margin/report/sale_report.py
index 1ac87891e97cd35e94430b6f29b9ea5eaacde52c..e43a053a351700e914e0ee50331c9decbebe8bb2 100644
--- a/addons/sale_margin/report/sale_report.py
+++ b/addons/sale_margin/report/sale_report.py
@@ -9,6 +9,7 @@ class SaleReport(models.Model):
 
     margin = fields.Float('Margin')
 
-    def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
-        fields['margin'] = ", SUM(l.margin / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) AS margin"
-        return super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
+    def _select_additional_fields(self):
+        res = super()._select_additional_fields()
+        res['margin'] = "SUM(l.margin / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END)"
+        return res
diff --git a/addons/sale_stock/report/sale_report.py b/addons/sale_stock/report/sale_report.py
index 3c62a27930d4c81fb34ed26781a163b3f29ac15c..e4a5e74c7a4a274a4a7afd2e97a6271d7a701f30 100644
--- a/addons/sale_stock/report/sale_report.py
+++ b/addons/sale_stock/report/sale_report.py
@@ -9,7 +9,13 @@ class SaleReport(models.Model):
 
     warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True)
 
-    def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
-        fields['warehouse_id'] = ", s.warehouse_id as warehouse_id"
-        groupby += ', s.warehouse_id'
-        return super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
+    def _select_additional_fields(self):
+        res = super()._select_additional_fields()
+        res['warehouse_id'] = "s.warehouse_id"
+        return res
+
+    def _group_by_sale(self):
+        res = super()._group_by_sale()
+        res += """,
+            s.warehouse_id"""
+        return res
diff --git a/addons/website_sale/report/sale_report.py b/addons/website_sale/report/sale_report.py
index 6ecf7967cadf9c835a438a2880b1fe6edf7416ae..560e54b2eaa37719d4a5c117cf69c943766813bc 100644
--- a/addons/website_sale/report/sale_report.py
+++ b/addons/website_sale/report/sale_report.py
@@ -8,7 +8,13 @@ class SaleReport(models.Model):
 
     website_id = fields.Many2one('website', readonly=True)
 
-    def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
-        fields['website_id'] = ", s.website_id as website_id"
-        groupby += ', s.website_id'
-        return super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
+    def _select_additional_fields(self):
+        res = super()._select_additional_fields()
+        res['website_id'] = "s.website_id"
+        return res
+
+    def _group_by_sale(self):
+        res = super()._group_by_sale()
+        res += """,
+            s.website_id"""
+        return res