changeset bc49b54ef788 in modules/sale_opportunity:default
details: 
https://hg.tryton.org/modules/sale_opportunity?cmd=changeset&node=bc49b54ef788
description:
        Refactor reporting

        issue11681
        review417881003
diffstat:

 CHANGELOG                                                          |    1 +
 __init__.py                                                        |   14 +-
 message.xml                                                        |   45 +
 opportunity.py                                                     |  210 +----
 opportunity.xml                                                    |  161 ---
 opportunity_reporting.py                                           |  465 
++++++++++
 opportunity_reporting.xml                                          |  462 
+++++++++
 setup.py                                                           |    3 +-
 tests/scenario_sale_opportunity_reporting.rst                      |  149 +++
 tryton.cfg                                                         |    1 +
 view/opportunity_employee_context_form.xml                         |    9 -
 view/opportunity_employee_graph1.xml                               |   14 -
 view/opportunity_employee_graph2.xml                               |   13 -
 view/opportunity_employee_monthly_tree.xml                         |   26 -
 view/opportunity_employee_tree.xml                                 |   25 -
 view/opportunity_monthly_graph1.xml                                |   14 -
 view/opportunity_monthly_graph2.xml                                |   13 -
 view/opportunity_monthly_tree.xml                                  |   25 -
 view/opportunity_reporting_context_form.xml                        |   15 +
 view/opportunity_reporting_conversion_employee_graph_amount.xml    |    8 +
 view/opportunity_reporting_conversion_employee_graph_number.xml    |    8 +
 view/opportunity_reporting_conversion_employee_list.xml            |    8 +
 view/opportunity_reporting_conversion_graph_amount.xml             |   11 +
 view/opportunity_reporting_conversion_graph_number.xml             |   13 +
 view/opportunity_reporting_conversion_list.xml                     |   13 +
 view/opportunity_reporting_conversion_time_series_graph_amount.xml |    8 +
 view/opportunity_reporting_conversion_time_series_graph_number.xml |    8 +
 view/opportunity_reporting_conversion_time_series_list.xml         |    9 +
 view/opportunity_reporting_main_graph_amount.xml                   |   12 +
 view/opportunity_reporting_main_graph_number.xml                   |   12 +
 view/opportunity_reporting_main_list.xml                           |   16 +
 view/opportunity_reporting_main_time_series_graph_amount.xml       |    8 +
 view/opportunity_reporting_main_time_series_graph_number.xml       |    8 +
 view/opportunity_reporting_main_time_series_list.xml               |   10 +
 34 files changed, 1302 insertions(+), 515 deletions(-)

diffs (2005 lines):

diff -r 0a4fd3b4c04a -r bc49b54ef788 CHANGELOG
--- a/CHANGELOG Sun Sep 11 01:27:48 2022 +0200
+++ b/CHANGELOG Sun Sep 11 01:30:15 2022 +0200
@@ -1,3 +1,4 @@
+* Refactor reporting
 * Use default customer payment term
 * Allow to store currency on opportunity
 
diff -r 0a4fd3b4c04a -r bc49b54ef788 __init__.py
--- a/__init__.py       Sun Sep 11 01:27:48 2022 +0200
+++ b/__init__.py       Sun Sep 11 01:30:15 2022 +0200
@@ -4,17 +4,21 @@
 from trytond.pool import Pool
 
 from . import (
-    account, company, configuration, opportunity, party, product, sale)
+    account, company, configuration, opportunity, opportunity_reporting, party,
+    product, sale)
 
 
 def register():
     Pool.register(
         opportunity.SaleOpportunity,
         opportunity.SaleOpportunityLine,
-        opportunity.SaleOpportunityEmployee,
-        opportunity.SaleOpportunityEmployeeContext,
-        opportunity.SaleOpportunityMonthly,
-        opportunity.SaleOpportunityEmployeeMonthly,
+        opportunity_reporting.Context,
+        opportunity_reporting.Main,
+        opportunity_reporting.MainTimeseries,
+        opportunity_reporting.Conversion,
+        opportunity_reporting.ConversionTimeseries,
+        opportunity_reporting.ConversionEmployee,
+        opportunity_reporting.ConversionEmployeeTimeseries,
         configuration.Configuration,
         configuration.ConfigurationSequence,
         sale.Sale,
diff -r 0a4fd3b4c04a -r bc49b54ef788 message.xml
--- a/message.xml       Sun Sep 11 01:27:48 2022 +0200
+++ b/message.xml       Sun Sep 11 01:30:15 2022 +0200
@@ -9,5 +9,50 @@
         <record model="ir.message" id="msg_modify_origin_opportunity">
             <field name="text">You cannot modify the opportunity origin of the 
sale "%(sale)s".</field>
         </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_number_help">
+            <field name="text">Number of opportunities</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_number_trend">
+            <field name="text">Number Trend</field>
+        </record>
+        <record model="ir.message" id="msg_sale_opportunity_reporting_amount">
+            <field name="text">Amount</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_amount_trend">
+            <field name="text">Amount Trend</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_converted">
+            <field name="text">Converted</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_conversion_rate">
+            <field name="text">Conversion Rate</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_conversion_trend">
+            <field name="text">Conversion Trend</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_converted_amount">
+            <field name="text">Converted Amount</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_converted_amount_trend">
+            <field name="text">Converted Amount Trend</field>
+        </record>
+        <record model="ir.message" id="msg_sale_opportunity_reporting_won">
+            <field name="text">Won</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_winning_rate">
+            <field name="text">Winning Rate</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_winning_trend">
+            <field name="text">Winning Trend</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_won_amount">
+            <field name="text">Won Amount</field>
+        </record>
+        <record model="ir.message" 
id="msg_sale_opportunity_reporting_won_amount_trend">
+            <field name="text">Won Amount Trend</field>
+        </record>
+        <record model="ir.message" id="msg_sale_opportunity_reporting_lost">
+            <field name="text">Lost</field>
+        </record>
     </data>
 </tryton>
diff -r 0a4fd3b4c04a -r bc49b54ef788 opportunity.py
--- a/opportunity.py    Sun Sep 11 01:27:48 2022 +0200
+++ b/opportunity.py    Sun Sep 11 01:30:15 2022 +0200
@@ -4,10 +4,7 @@
 import datetime
 from itertools import groupby
 
-from sql import Literal, Null
-from sql.aggregate import Count, Max, Sum
-from sql.conditionals import Case, Coalesce
-from sql.functions import DateTrunc, Extract
+from sql import Null
 
 from trytond.i18n import gettext
 from trytond.ir.attachment import AttachmentCopyMixin
@@ -598,208 +595,3 @@
     @classmethod
     def search_rec_name(cls, name, clause):
         return [('product.rec_name',) + tuple(clause[1:])]
-
-
-class SaleOpportunityReportMixin:
-    __slots__ = ()
-    number = fields.Integer('Number')
-    converted = fields.Integer('Converted')
-    conversion_rate = fields.Function(fields.Float('Conversion Rate',
-        digits=(1, 4)), 'get_conversion_rate')
-    won = fields.Integer('Won')
-    winning_rate = fields.Function(fields.Float('Winning Rate', digits=(1, 4)),
-        'get_winning_rate')
-    lost = fields.Integer('Lost')
-    company = fields.Many2One('company.company', 'Company')
-    currency = fields.Function(fields.Many2One('currency.currency',
-        'Currency'), 'get_currency')
-    amount = Monetary("Amount", currency='currency', digits='currency')
-    converted_amount = Monetary(
-        "Converted Amount", currency='currency', digits='currency')
-    conversion_amount_rate = fields.Function(fields.Float(
-        'Conversion Amount Rate', digits=(1, 4)), 'get_conversion_amount_rate')
-    won_amount = Monetary("Won Amount", currency='currency', digits='currency')
-    winning_amount_rate = fields.Function(fields.Float(
-            'Winning Amount Rate', digits=(1, 4)), 'get_winning_amount_rate')
-
-    @staticmethod
-    def _converted_state():
-        return ['converted', 'won']
-
-    @staticmethod
-    def _won_state():
-        return ['won']
-
-    @staticmethod
-    def _lost_state():
-        return ['lost']
-
-    def get_conversion_rate(self, name):
-        if self.number:
-            digits = getattr(self.__class__, name).digits[1]
-            return round(float(self.converted) / self.number, digits)
-        else:
-            return 0.0
-
-    def get_winning_rate(self, name):
-        if self.number:
-            digits = getattr(self.__class__, name).digits[1]
-            return round(float(self.won) / self.number, digits)
-        else:
-            return 0.0
-
-    def get_currency(self, name):
-        return self.company.currency.id
-
-    def get_conversion_amount_rate(self, name):
-        if self.amount:
-            digits = getattr(self.__class__, name).digits[1]
-            return round(
-                float(self.converted_amount) / float(self.amount), digits)
-        else:
-            return 0.0
-
-    def get_winning_amount_rate(self, name):
-        if self.amount:
-            digits = getattr(self.__class__, name).digits[1]
-            return round(float(self.won_amount) / float(self.amount), digits)
-        else:
-            return 0.0
-
-    @classmethod
-    def table_query(cls):
-        Opportunity = Pool().get('sale.opportunity')
-        opportunity = Opportunity.__table__()
-        return opportunity.select(
-            Max(opportunity.create_uid).as_('create_uid'),
-            Max(opportunity.create_date).as_('create_date'),
-            Max(opportunity.write_uid).as_('write_uid'),
-            Max(opportunity.write_date).as_('write_date'),
-            opportunity.company,
-            Count(Literal(1)).as_('number'),
-            Sum(Case(
-                    (opportunity.state.in_(cls._converted_state()),
-                        Literal(1)), else_=Literal(0))).as_('converted'),
-            Sum(Case(
-                    (opportunity.state.in_(cls._won_state()),
-                        Literal(1)), else_=Literal(0))).as_('won'),
-            Sum(Case(
-                    (opportunity.state.in_(cls._lost_state()),
-                        Literal(1)), else_=Literal(0))).as_('lost'),
-            Sum(opportunity.amount).as_('amount'),
-            Sum(Case(
-                    (opportunity.state.in_(cls._converted_state()),
-                        opportunity.amount),
-                    else_=Literal(0))).as_('converted_amount'),
-            Sum(Case(
-                    (opportunity.state.in_(cls._won_state()),
-                        opportunity.amount),
-                    else_=Literal(0))).as_('won_amount'))
-
-
-class SaleOpportunityEmployee(SaleOpportunityReportMixin, ModelSQL, ModelView):
-    'Sale Opportunity per Employee'
-    __name__ = 'sale.opportunity_employee'
-    employee = fields.Many2One('company.employee', 'Employee')
-
-    @classmethod
-    def table_query(cls):
-        query = super(SaleOpportunityEmployee, cls).table_query()
-        opportunity, = query.from_
-        query.columns += (
-            Coalesce(opportunity.employee, 0).as_('id'),
-            opportunity.employee,
-            )
-        where = Literal(True)
-        if Transaction().context.get('start_date'):
-            where &= (opportunity.start_date
-                >= Transaction().context['start_date'])
-        if Transaction().context.get('end_date'):
-            where &= (opportunity.start_date
-                <= Transaction().context['end_date'])
-        query.where = where
-        query.group_by = (opportunity.employee, opportunity.company)
-        return query
-
-
-class SaleOpportunityEmployeeContext(ModelView):
-    'Sale Opportunity per Employee Context'
-    __name__ = 'sale.opportunity_employee.context'
-    start_date = fields.Date('Start Date')
-    end_date = fields.Date('End Date')
-
-
-class MonthLabelMixin:
-    __slots__ = ()
-
-    month_label = fields.Function(fields.Char("Month"), 'get_month_label')
-
-    @classmethod
-    def order_month_label(cls, tables):
-        table, _ = tables[None]
-        return [table.month]
-
-    def get_month_label(self, name):
-        return self.month.strftime('%Y-%m')
-
-
-class SaleOpportunityMonthly(
-        MonthLabelMixin, SaleOpportunityReportMixin, ModelSQL, ModelView):
-    'Sale Opportunity per Month'
-    __name__ = 'sale.opportunity_monthly'
-    month = fields.Date("Month")
-
-    @classmethod
-    def __setup__(cls):
-        super(SaleOpportunityMonthly, cls).__setup__()
-        cls._order.insert(0, ('month', 'DESC'))
-
-    @classmethod
-    def table_query(cls):
-        pool = Pool()
-        Month = pool.get('ir.calendar.month')
-        month = Month.__table__()
-        query = super(SaleOpportunityMonthly, cls).table_query()
-        opportunity, = query.from_
-        month_timestamp = DateTrunc('MONTH', opportunity.start_date)
-        id_ = Extract('EPOCH', month_timestamp)
-        month = cls.month.sql_cast(month_timestamp)
-        query.columns += (
-            id_.as_('id'),
-            month.as_('month'),
-            )
-        query.group_by = [id_, month, opportunity.company]
-        return query
-
-
-class SaleOpportunityEmployeeMonthly(
-        MonthLabelMixin, SaleOpportunityReportMixin, ModelSQL, ModelView):
-    'Sale Opportunity per Employee per Month'
-    __name__ = 'sale.opportunity_employee_monthly'
-    month = fields.Date("Month")
-    employee = fields.Many2One('company.employee', 'Employee')
-
-    @classmethod
-    def __setup__(cls):
-        super(SaleOpportunityEmployeeMonthly, cls).__setup__()
-        cls._order.insert(1, ('month', 'DESC'))
-        cls._order.insert(2, ('employee', 'ASC'))
-
-    @classmethod
-    def table_query(cls):
-        pool = Pool()
-        Month = pool.get('ir.calendar.month')
-        month = Month.__table__()
-        query = super(SaleOpportunityEmployeeMonthly, cls).table_query()
-        opportunity, = query.from_
-        month_timestamp = DateTrunc('MONTH', opportunity.start_date)
-        id_ = Extract('EPOCH', month_timestamp)
-        month = cls.month.sql_cast(month_timestamp)
-        query.columns += (
-            id_.as_('id'),
-            month.as_('month'),
-            opportunity.employee,
-            )
-        query.group_by = [
-            id_, month, opportunity.employee, opportunity.company]
-        return query
diff -r 0a4fd3b4c04a -r bc49b54ef788 opportunity.xml
--- a/opportunity.xml   Sun Sep 11 01:27:48 2022 +0200
+++ b/opportunity.xml   Sun Sep 11 01:30:15 2022 +0200
@@ -241,166 +241,5 @@
             <field name="type">tree</field>
             <field name="name">opportunity_line_tree</field>
         </record>
-
-        <record model="ir.ui.view" id="opportunity_employee_view_tree">
-            <field name="model">sale.opportunity_employee</field>
-            <field name="type">tree</field>
-            <field name="name">opportunity_employee_tree</field>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_employee_view_graph1">
-            <field name="model">sale.opportunity_employee</field>
-            <field name="type">graph</field>
-            <field name="name">opportunity_employee_graph1</field>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_employee_view_graph2">
-            <field name="model">sale.opportunity_employee</field>
-            <field name="type">graph</field>
-            <field name="name">opportunity_employee_graph2</field>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_employee_context_view_form">
-            <field name="model">sale.opportunity_employee.context</field>
-            <field name="type">form</field>
-            <field name="name">opportunity_employee_context_form</field>
-        </record>
-
-        <record model="ir.action.act_window" 
id="act_opportunity_employee_form">
-            <field name="name">Opportunities per Employee</field>
-            <field name="res_model">sale.opportunity_employee</field>
-            <field 
name="context_model">sale.opportunity_employee.context</field>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_employee_form_view1">
-            <field name="sequence" eval="10"/>
-            <field name="view" ref="opportunity_employee_view_tree"/>
-            <field name="act_window" ref="act_opportunity_employee_form"/>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_employee_form_view2">
-            <field name="sequence" eval="20"/>
-            <field name="view" ref="opportunity_employee_view_graph1"/>
-            <field name="act_window" ref="act_opportunity_employee_form"/>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_employee_form_view3">
-            <field name="sequence" eval="30"/>
-            <field name="view" ref="opportunity_employee_view_graph2"/>
-            <field name="act_window" ref="act_opportunity_employee_form"/>
-        </record>
-
-        <record model="ir.rule.group" 
id="rule_group_opportunity_employee_companies">
-            <field name="name">User in companies</field>
-            <field name="model" search="[('model', '=', 
'sale.opportunity_employee')]"/>
-            <field name="global_p" eval="True"/>
-        </record>
-        <record model="ir.rule" id="rule_opportunity_employee_companies">
-            <field name="domain"
-                eval="[('company', 'in', Eval('companies', []))]"
-                pyson="1"/>
-            <field name="rule_group" 
ref="rule_group_opportunity_employee_companies"/>
-        </record>
-
-        <menuitem
-            parent="sale.menu_reporting"
-            action="act_opportunity_employee_form"
-            sequence="50"
-            id="menu_opportunity_employee_open"/>
-
-        <record model="ir.ui.view" id="opportunity_monthly_view_tree">
-            <field name="model">sale.opportunity_monthly</field>
-            <field name="type">tree</field>
-            <field name="name">opportunity_monthly_tree</field>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_monthly_view_graph1">
-            <field name="model">sale.opportunity_monthly</field>
-            <field name="type">graph</field>
-            <field name="name">opportunity_monthly_graph1</field>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_monthly_view_graph2">
-            <field name="model">sale.opportunity_monthly</field>
-            <field name="type">graph</field>
-            <field name="name">opportunity_monthly_graph2</field>
-        </record>
-
-        <record model="ir.action.act_window" id="act_opportunity_monthly_form">
-            <field name="name">Opportunities per Month</field>
-            <field name="res_model">sale.opportunity_monthly</field>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_monthly_form_view1">
-            <field name="sequence" eval="10"/>
-            <field name="view" ref="opportunity_monthly_view_tree"/>
-            <field name="act_window" ref="act_opportunity_monthly_form"/>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_monthly_form_view2">
-            <field name="sequence" eval="20"/>
-            <field name="view" ref="opportunity_monthly_view_graph1"/>
-            <field name="act_window" ref="act_opportunity_monthly_form"/>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_monthly_form_view3">
-            <field name="sequence" eval="30"/>
-            <field name="view" ref="opportunity_monthly_view_graph2"/>
-            <field name="act_window" ref="act_opportunity_monthly_form"/>
-        </record>
-
-        <menuitem
-            parent="sale.menu_reporting"
-            action="act_opportunity_monthly_form"
-            sequence="50"
-            id="menu_opportunity_monthly_form"/>
-
-        <record model="ir.rule.group" 
id="rule_group_opportunity_monthly_companies">
-            <field name="name">User in companies</field>
-            <field name="model" search="[('model', '=', 
'sale.opportunity_monthly')]"/>
-            <field name="global_p" eval="True"/>
-        </record>
-        <record model="ir.rule" id="rule_opportunity_monthly_companies">
-            <field name="domain"
-                eval="[('company', 'in', Eval('companies', []))]"
-                pyson="1"/>
-            <field name="rule_group" 
ref="rule_group_opportunity_monthly_companies"/>
-        </record>
-
-        <record model="ir.ui.view" id="opportunity_employee_monthly_view_tree">
-            <field name="model">sale.opportunity_employee_monthly</field>
-            <field name="type">tree</field>
-            <field name="name">opportunity_employee_monthly_tree</field>
-        </record>
-
-        <record model="ir.action.act_window" 
id="act_opportunity_employee_monthly_form">
-            <field name="name">Opportunities per Employee per Month</field>
-            <field name="res_model">sale.opportunity_employee_monthly</field>
-        </record>
-        <record model="ir.action.act_window.view"
-            id="act_opportunity_employee_monthly_form_view1">
-            <field name="sequence" eval="10"/>
-            <field name="view" ref="opportunity_employee_monthly_view_tree"/>
-            <field name="act_window" 
ref="act_opportunity_employee_monthly_form"/>
-        </record>
-
-        <menuitem
-            parent="sale.menu_reporting"
-            action="act_opportunity_employee_monthly_form"
-            sequence="50"
-            id="menu_opportunity_employee_monthly_form"/>
-
-        <record model="ir.rule.group" 
id="rule_group_opportunity_employee_monthly_companies">
-            <field name="name">User in companies</field>
-            <field name="model" search="[('model', '=', 
'sale.opportunity_employee_monthly')]"/>
-            <field name="global_p" eval="True"/>
-        </record>
-        <record model="ir.rule" 
id="rule_opportunity_employee_monthly_companies">
-            <field name="domain"
-                eval="[('company', 'in', Eval('companies', []))]"
-                pyson="1"/>
-            <field name="rule_group" 
ref="rule_group_opportunity_employee_monthly_companies"/>
-        </record>
-
     </data>
 </tryton>
diff -r 0a4fd3b4c04a -r bc49b54ef788 opportunity_reporting.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/opportunity_reporting.py  Sun Sep 11 01:30:15 2022 +0200
@@ -0,0 +1,465 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from itertools import tee, zip_longest
+
+from dateutil.relativedelta import relativedelta
+from sql import Literal, Null, With
+from sql.aggregate import Count, Min, Sum
+from sql.conditionals import Case
+from sql.functions import CurrentTimestamp
+try:
+    import pygal
+except ImportError:
+    pygal = None
+
+from trytond.i18n import lazy_gettext
+from trytond.model import ModelSQL, ModelView, fields
+from trytond.modules.currency.fields import Monetary
+from trytond.pool import Pool
+from trytond.pyson import Eval, If
+from trytond.transaction import Transaction
+
+
+def pairwise(iterable):
+    a, b = tee(iterable)
+    next(b)
+    return zip_longest(a, b)
+
+
+class Abstract(ModelSQL):
+
+    company = fields.Many2One(
+        'company.company', lazy_gettext('sale.msg_sale_reporting_company'))
+
+    number = fields.Integer(lazy_gettext('sale.msg_sale_reporting_number'),
+        help=lazy_gettext(
+            'sale_opportunity.msg_sale_opportunity_reporting_number_help'))
+    number_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_number_trend')),
+        'get_trend')
+
+    amount = Monetary(
+        lazy_gettext('sale_opportunity.msg_sale_opportunity_reporting_amount'),
+        currency='currency', digits='currency')
+    amount_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_amount_trend')),
+        'get_trend')
+
+    converted = fields.Integer(
+        lazy_gettext(
+            'sale_opportunity.msg_sale_opportunity_reporting_converted'))
+    conversion_rate = fields.Function(
+        fields.Float(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_conversion_rate'),
+            digits=(1, 4)), 'get_rate')
+    conversion_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_conversion_trend')),
+        'get_trend')
+    converted_amount = Monetary(
+        lazy_gettext(
+            'sale_opportunity.'
+            'msg_sale_opportunity_reporting_converted_amount'),
+        currency='currency', digits='currency')
+    converted_amount_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_converted_amount_trend')),
+        'get_trend')
+
+    time_series = None
+
+    currency = fields.Function(
+        fields.Many2One(
+            'currency.currency',
+            lazy_gettext('sale.msg_sale_reporting_currency')),
+        'get_currency')
+
+    @classmethod
+    def table_query(cls):
+        from_item, tables, withs = cls._joins()
+        return from_item.select(*cls._columns(tables, withs),
+            where=cls._where(tables, withs),
+            group_by=cls._group_by(tables, withs),
+            with_=withs.values())
+
+    @classmethod
+    def _joins(cls):
+        pool = Pool()
+        Company = pool.get('company.company')
+        Currency = pool.get('currency.currency')
+        Opportunity = pool.get('sale.opportunity')
+        context = Transaction().context
+
+        tables = {}
+        company = context.get('company')
+        tables['opportunity'] = opportunity = Opportunity.__table__()
+        tables['opportunity.company'] = company = Company.__table__()
+        withs = {}
+        currency_opportunity = With(query=Currency.currency_rate_sql())
+        withs['currency_opportunity'] = currency_opportunity
+        currency_company = With(query=Currency.currency_rate_sql())
+        withs['currency_company'] = currency_company
+
+        from_item = (opportunity
+            .join(currency_opportunity,
+                condition=(
+                    opportunity.currency == currency_opportunity.currency)
+                & (currency_opportunity.start_date <= opportunity.start_date)
+                & ((currency_opportunity.end_date == Null)
+                    | (currency_opportunity.end_date > opportunity.start_date))
+                )
+            .join(company, condition=opportunity.company == company.id)
+            .join(currency_company,
+                condition=(company.currency == currency_company.currency)
+                & (currency_company.start_date <= opportunity.start_date)
+                & ((currency_company.end_date == Null)
+                    | (currency_company.end_date > opportunity.start_date))
+                ))
+        return from_item, tables, withs
+
+    @classmethod
+    def _columns(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return [
+            cls._column_id(tables, withs).as_('id'),
+            Literal(0).as_('create_uid'),
+            CurrentTimestamp().as_('create_date'),
+            cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'),
+            cls.write_date.sql_cast(Literal(Null)).as_('write_date'),
+            opportunity.company.as_('company'),
+            Count(Literal(1)).as_('number'),
+            Sum(opportunity.amount).as_('amount'),
+            Sum(Case(
+                    (opportunity.state.in_(cls._converted_states()),
+                        Literal(1)), else_=Literal(0))).as_('converted'),
+            Sum(Case(
+                    (opportunity.state.in_(cls._converted_states()),
+                        opportunity.amount),
+                    else_=Literal(0))).as_('converted_amount'),
+            ]
+
+    @classmethod
+    def _column_id(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return Min(opportunity.id)
+
+    @classmethod
+    def _group_by(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return [opportunity.company]
+
+    @classmethod
+    def _where(cls, tables, withs):
+        context = Transaction().context
+        opportunity = tables['opportunity']
+
+        where = opportunity.company == context.get('company')
+
+        date = cls._column_date(tables, withs)
+        from_date = context.get('from_date')
+        if from_date:
+            where &= date >= from_date
+        to_date = context.get('to_date')
+        if to_date:
+            where &= date <= to_date
+        return where
+
+    @classmethod
+    def _column_date(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return opportunity.start_date
+
+    @classmethod
+    def _converted_states(cls):
+        return ['converted', 'won']
+
+    @classmethod
+    def _field_name_strip(cls, name, suffix):
+        name = name[:-len(suffix)]
+        return (name
+            .replace('conversion', 'converted')
+            .replace('winning', 'won'))
+
+    def get_rate(self, name):
+        if self.number:
+            digits = getattr(self.__class__, name).digits[1]
+            name = self._field_name_strip(name, '_rate')
+            value = float(getattr(self, name))
+            return round(value / self.number, digits)
+        else:
+            return 0.0
+
+    @property
+    def time_series_all(self):
+        delta = self._period_delta()
+        for ts, next_ts in pairwise(self.time_series or []):
+            yield ts
+            if delta and next_ts:
+                date = ts.date + delta
+                while date < next_ts.date:
+                    yield None
+                    date += delta
+
+    @classmethod
+    def _period_delta(cls):
+        context = Transaction().context
+        return {
+            'year': relativedelta(years=1),
+            'month': relativedelta(months=1),
+            'day': relativedelta(days=1),
+            }.get(context.get('period'))
+
+    def get_trend(self, name):
+        name = self._field_name_strip(name, '_trend')
+        if pygal:
+            chart = pygal.Line()
+            chart.add('', [getattr(ts, name) if ts else 0
+                    for ts in self.time_series_all])
+            return chart.render_sparktext()
+
+    def get_currency(self, name):
+        return self.company.currency.id
+
+
+class AbstractTimeseries(Abstract):
+
+    date = fields.Date(lazy_gettext('sale.msg_sale_reporting_date'))
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order = [('date', 'ASC')]
+
+    @classmethod
+    def _columns(cls, tables, withs):
+        return super()._columns(tables, withs) + [
+            cls._column_date(tables, withs).as_('date')]
+
+    @classmethod
+    def _group_by(cls, tables, withs):
+        return super()._group_by(tables, withs) + [
+            cls._column_date(tables, withs)]
+
+
+class AbstractConversion(Abstract):
+
+    won = fields.Integer(
+        lazy_gettext('sale_opportunity.msg_sale_opportunity_reporting_won'))
+    winning_rate = fields.Function(
+        fields.Float(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_winning_rate'),
+            digits=(1, 4)),
+        'get_rate')
+    winning_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_winning_trend')),
+        'get_trend')
+    won_amount = Monetary(
+        lazy_gettext(
+            'sale_opportunity.msg_sale_opportunity_reporting_won_amount'),
+        currency='currency', digits='currency')
+    won_amount_trend = fields.Function(
+        fields.Char(lazy_gettext(
+                'sale_opportunity.'
+                'msg_sale_opportunity_reporting_won_amount_trend')),
+        'get_trend')
+
+    lost = fields.Integer(
+        lazy_gettext('sale_opportunity.msg_sale_opportunity_reporting_lost'))
+
+    @classmethod
+    def _columns(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return super()._columns(tables, withs) + [
+            Sum(Case(
+                    (opportunity.state.in_(cls._won_states()),
+                        Literal(1)), else_=Literal(0))).as_('won'),
+            Sum(Case(
+                    (opportunity.state.in_(cls._won_states()),
+                        opportunity.amount),
+                    else_=Literal(0))).as_('won_amount'),
+            Sum(Case(
+                    (opportunity.state.in_(cls._lost_states()),
+                        Literal(1)), else_=Literal(0))).as_('lost'),
+            ]
+
+    @classmethod
+    def _column_date(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return opportunity.end_date
+
+    @classmethod
+    def _won_states(cls):
+        return ['won']
+
+    @classmethod
+    def _lost_states(cls):
+        return ['lost']
+
+    @classmethod
+    def _opportunity_states(cls):
+        return cls._converted_states() + cls._won_states() + cls._lost_states()
+
+
+class AbstractConversionTimeseries(AbstractConversion, AbstractTimeseries):
+    pass
+
+
+class Context(ModelView):
+    "Sale Opportunity Reporting Context"
+    __name__ = 'sale.opportunity.reporting.context'
+
+    company = fields.Many2One('company.company', "Company", required=True)
+    from_date = fields.Date("From Date",
+        domain=[
+            If(Eval('to_date') & Eval('from_date'),
+                ('from_date', '<=', Eval('to_date')),
+                ()),
+            ])
+    to_date = fields.Date("To Date",
+        domain=[
+            If(Eval('from_date') & Eval('to_date'),
+                ('to_date', '>=', Eval('from_date')),
+                ()),
+            ])
+    period = fields.Selection([
+            ('year', "Year"),
+            ('month', "Month"),
+            ('day', "Day"),
+            ], "Period", required=True)
+
+    @classmethod
+    def default_company(cls):
+        return Transaction().context.get('company')
+
+    @classmethod
+    def default_from_date(cls):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        context = Transaction().context
+        if 'from_date' in context:
+            return context['from_date']
+        return Date.today() - relativedelta(years=1)
+
+    @classmethod
+    def default_to_date(cls):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        context = Transaction().context
+        if 'to_date' in context:
+            return context['to_date']
+        return Date.today()
+
+    @classmethod
+    def default_period(cls):
+        return Transaction().context.get('period', 'month')
+
+
+class Main(Abstract, ModelView):
+    "Sale Opportunity Reporting"
+    __name__ = 'sale.opportunity.reporting.main'
+
+    time_series = fields.Function(fields.One2Many(
+            'sale.opportunity.reporting.main.time_series', None,
+            lazy_gettext('sale.msg_sale_reporting_time_series')),
+        'get_time_series')
+
+    def get_rec_name(self, name):
+        return ''
+
+    def get_time_series(self, name):
+        pool = Pool()
+        Timeseries = pool.get('sale.opportunity.reporting.main.time_series')
+        return [t.id for t in Timeseries.search([])]
+
+
+class MainTimeseries(AbstractTimeseries, ModelView):
+    "Sale Opportunity Reporting"
+    __name__ = 'sale.opportunity.reporting.main.time_series'
+
+
+class Conversion(AbstractConversion, ModelView):
+    "Sale Opportunity Reporting Conversion"
+    __name__ = 'sale.opportunity.reporting.conversion'
+
+    time_series = fields.Function(fields.One2Many(
+            'sale.opportunity.reporting.conversion.time_series', None,
+            lazy_gettext('sale.msg_sale_reporting_time_series')),
+        'get_time_series')
+
+    def get_rec_name(self, name):
+        return ''
+
+    def get_time_series(self, name):
+        pool = Pool()
+        Timeseries = pool.get(
+            'sale.opportunity.reporting.conversion.time_series')
+        return [t.id for t in Timeseries.search([])]
+
+
+class ConversionTimeseries(AbstractConversionTimeseries, ModelView):
+    "Sale Opportunity Reporting Conversion"
+    __name__ = 'sale.opportunity.reporting.conversion.time_series'
+
+
+class EmployeeMixin:
+    __slots__ = ()
+
+    employee = fields.Many2One('company.employee', "Employee")
+
+    @classmethod
+    def _columns(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return super()._columns(tables, withs) + [
+            opportunity.employee.as_('employee')]
+
+    @classmethod
+    def _group_by(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return super()._group_by(tables, withs) + [
+            opportunity.employee]
+
+    def get_rec_name(self, name):
+        return self.employee.rec_name
+
+
+class ConversionEmployee(EmployeeMixin, AbstractConversion, ModelView):
+    "Sale Opportunity Reporting Conversion per Employee"
+    __name__ = 'sale.opportunity.reporting.conversion.employee'
+
+    time_series = fields.One2Many(
+        'sale.opportunity.reporting.conversion.employee.time_series',
+        'employee', lazy_gettext('sale.msg_sale_reporting_time_series'))
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order.insert(0, ('employee', 'ASC'))
+
+    @classmethod
+    def _column_id(cls, tables, withs):
+        opportunity = tables['opportunity']
+        return opportunity.employee
+
+    @classmethod
+    def _where(cls, tables, withs):
+        opportunity = tables['opportunity']
+        where = super()._where(tables, withs)
+        where &= opportunity.employee != Null
+        return where
+
+
+class ConversionEmployeeTimeseries(
+        EmployeeMixin, AbstractConversionTimeseries, ModelView):
+    "Sale Opportunity Reporting Conversion per Employee"
+    __name__ = 'sale.opportunity.reporting.conversion.employee.time_series'
diff -r 0a4fd3b4c04a -r bc49b54ef788 opportunity_reporting.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/opportunity_reporting.xml Sun Sep 11 01:30:15 2022 +0200
@@ -0,0 +1,462 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+    <data>
+        <menuitem
+            name="Opportunities"
+            parent="sale.menu_reporting"
+            sequence="50"
+            id="menu_reporting_opportunity"
+            icon="tryton-graph"/>
+
+        <record model="ir.ui.view" id="reporting_context_view_form">
+            <field name="model">sale.opportunity.reporting.context</field>
+            <field name="type">form</field>
+            <field name="name">opportunity_reporting_context_form</field>
+        </record>
+
+        <!-- Main -->
+
+        <record model="ir.ui.view" id="reporting_main_view_list">
+            <field name="model">sale.opportunity.reporting.main</field>
+            <field name="type">tree</field>
+            <field name="name">opportunity_reporting_main_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_view_graph_number">
+            <field name="model">sale.opportunity.reporting.main</field>
+            <field name="type">graph</field>
+            <field name="name">opportunity_reporting_main_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_view_graph_amount">
+            <field name="model">sale.opportunity.reporting.main</field>
+            <field name="type">graph</field>
+            <field name="name">opportunity_reporting_main_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_main">
+            <field name="name">Opportunities</field>
+            <field name="res_model">sale.opportunity.reporting.main</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_main_view_list"/>
+            <field name="act_window" ref="act_reporting_main"/>
+        </record>
+        <record model="ir.action.keyword" id="act_reporting_main_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model" ref="menu_reporting_opportunity"/>
+            <field name="action" ref="act_reporting_main"/>
+        </record>
+
+        <record model="ir.rule.group" id="rule_group_reporting_main_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_main_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_main_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_main">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" id="access_reporting_main_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_time_series_view_list">
+            <field 
name="model">sale.opportunity.reporting.main.time_series</field>
+            <field name="type">tree</field>
+            <field 
name="name">opportunity_reporting_main_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_main_time_series_view_graph_number">
+            <field 
name="model">sale.opportunity.reporting.main.time_series</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" ref="reporting_main_view_graph_number"/>
+            <field 
name="name">opportunity_reporting_main_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_main_time_series_view_graph_amount">
+            <field 
name="model">sale.opportunity.reporting.main.time_series</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" ref="reporting_main_view_graph_amount"/>
+            <field 
name="name">opportunity_reporting_main_time_series_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_main_time_series">
+            <field name="name">Opportunities</field>
+            <field 
name="res_model">sale.opportunity.reporting.main.time_series</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_main_time_series_view_list"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_main_time_series_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_main_time_series_view_graph_amount"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_main_time_series_list_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">sale.opportunity.reporting.main,-1</field>
+            <field name="action" ref="act_reporting_main_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_main_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_main_time_series_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_main_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_main_time_series">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main.time_series')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_main_time_series_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.main.time_series')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <!-- Conversion -->
+
+        <record model="ir.ui.view" id="reporting_conversion_view_list">
+            <field name="model">sale.opportunity.reporting.conversion</field>
+            <field name="type">tree</field>
+            <field name="name">opportunity_reporting_conversion_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_conversion_view_graph_number">
+            <field name="model">sale.opportunity.reporting.conversion</field>
+            <field name="type">graph</field>
+            <field 
name="name">opportunity_reporting_conversion_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_conversion_view_graph_amount">
+            <field name="model">sale.opportunity.reporting.conversion</field>
+            <field name="type">graph</field>
+            <field 
name="name">opportunity_reporting_conversion_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_conversion">
+            <field name="name">Opportunity Conversions</field>
+            <field 
name="res_model">sale.opportunity.reporting.conversion</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_conversion_view_list"/>
+            <field name="act_window" ref="act_reporting_conversion"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_conversion_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model" ref="menu_reporting_opportunity"/>
+            <field name="action" ref="act_reporting_conversion"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_conversion_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_conversion_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_conversion_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_conversion">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" id="access_reporting_conversion_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_time_series_view_list">
+            <field 
name="model">sale.opportunity.reporting.conversion.time_series</field>
+            <field name="type">tree</field>
+            <field 
name="name">opportunity_reporting_conversion_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_time_series_view_graph_number">
+            <field 
name="model">sale.opportunity.reporting.conversion.time_series</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" 
ref="reporting_conversion_view_graph_number"/>
+            <field 
name="name">opportunity_reporting_conversion_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_time_series_view_graph_amount">
+            <field 
name="model">sale.opportunity.reporting.conversion.time_series</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" 
ref="reporting_conversion_view_graph_amount"/>
+            <field 
name="name">opportunity_reporting_conversion_time_series_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_conversion_time_series">
+            <field name="name">Opportunity Conversions</field>
+            <field 
name="res_model">sale.opportunity.reporting.conversion.time_series</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_time_series_list_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" 
ref="reporting_conversion_time_series_view_list"/>
+            <field name="act_window" 
ref="act_reporting_conversion_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_time_series_list_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_conversion_time_series_view_graph_number"/>
+            <field name="act_window" 
ref="act_reporting_conversion_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_time_series_list_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_conversion_time_series_view_graph_amount"/>
+            <field name="act_window" 
ref="act_reporting_conversion_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_conversion_time_series_list_keyword1">
+            <field name="keyword">tree_open</field>
+            <field 
name="model">sale.opportunity.reporting.conversion,-1</field>
+            <field name="action" ref="act_reporting_conversion_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_conversion_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_conversion_time_series_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_conversion_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_conversion_time_series">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.time_series')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_conversion_time_series_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.time_series')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <!-- Conversion Employee -->
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_view_list">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" ref="reporting_conversion_view_list"/>
+            <field 
name="name">opportunity_reporting_conversion_employee_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_view_graph_number">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" 
ref="reporting_conversion_view_graph_number"/>
+            <field 
name="name">opportunity_reporting_conversion_employee_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_view_graph_amount">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee</field>
+            <field name="type" eval="None"/>
+            <field name="inherit" 
ref="reporting_conversion_view_graph_amount"/>
+            <field 
name="name">opportunity_reporting_conversion_employee_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_conversion_employee">
+            <field name="name">Opportunity Conversions per Employee</field>
+            <field 
name="res_model">sale.opportunity.reporting.conversion.employee</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_conversion_employee_view_list"/>
+            <field name="act_window" ref="act_reporting_conversion_employee"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_conversion_employee_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_conversion_employee"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_conversion_employee_view_graph_amount"/>
+            <field name="act_window" ref="act_reporting_conversion_employee"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_conversion_employee_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model" ref="menu_reporting_opportunity"/>
+            <field name="action" ref="act_reporting_conversion_employee"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_conversion_employee_keyword2">
+            <field name="keyword">tree_open</field>
+            <field 
name="model">sale.opportunity.reporting.conversion,-1</field>
+            <field name="action" ref="act_reporting_conversion_employee"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_conversion_employee_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_conversion_employee_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_conversion_employee_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_conversion_employee">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_conversion_employee_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_time_series_view_list">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee.time_series</field>
+            <field name="type">tree</field>
+            <field 
name="name">opportunity_reporting_conversion_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_time_series_view_graph_number">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">opportunity_reporting_conversion_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_conversion_employee_time_series_view_graph_amount">
+            <field 
name="model">sale.opportunity.reporting.conversion.employee.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">opportunity_reporting_conversion_time_series_graph_amount</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_conversion_employee_time_series">
+            <field name="name">Opportunity Conversions per Employee</field>
+            <field 
name="res_model">sale.opportunity.reporting.conversion.employee.time_series</field>
+            <field 
name="context_model">sale.opportunity.reporting.context</field>
+            <field
+                name="domain"
+                eval="[('employee', '=', Eval('active_id', -1))]"
+                pyson="1"/>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_time_series_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" 
ref="reporting_conversion_employee_time_series_view_list"/>
+            <field name="act_window" 
ref="act_reporting_conversion_employee_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_time_series_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_conversion_employee_time_series_view_graph_number"/>
+            <field name="act_window" 
ref="act_reporting_conversion_employee_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_conversion_employee_time_series_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_conversion_employee_time_series_view_graph_amount"/>
+            <field name="act_window" 
ref="act_reporting_conversion_employee_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_conversion_employee_time_series_keyword1">
+            <field name="keyword">tree_open</field>
+            <field 
name="model">sale.opportunity.reporting.conversion.employee,-1</field>
+            <field name="action" 
ref="act_reporting_conversion_employee_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_conversion_employee_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_conversion_employee_time_series_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_conversion_employee_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_conversion_employee_time_series">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee.time_series')]"/>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_conversion_employee_time_series_sale">
+            <field name="model" search="[('model', '=', 
'sale.opportunity.reporting.conversion.employee.time_series')]"/>
+            <field name="group" ref="sale.group_sale"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+    </data>
+</tryton>
diff -r 0a4fd3b4c04a -r bc49b54ef788 setup.py
--- a/setup.py  Sun Sep 11 01:27:48 2022 +0200
+++ b/setup.py  Sun Sep 11 01:30:15 2022 +0200
@@ -58,7 +58,7 @@
 if local_version:
     version += '+' + '.'.join(local_version)
 
-requires = ['python-sql >= 0.4']
+requires = ['python-sql >= 0.4', 'python-dateutil']
 for dep in info.get('depends', []):
     if not re.match(r'(ir|res)(\W|$)', dep):
         requires.append(get_require_version('trytond_%s' % dep))
@@ -138,6 +138,7 @@
     python_requires='>=3.7',
     install_requires=requires,
     extras_require={
+        'sparklines': ['pygal'],
         'test': tests_require,
         },
     dependency_links=dependency_links,
diff -r 0a4fd3b4c04a -r bc49b54ef788 
tests/scenario_sale_opportunity_reporting.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_sale_opportunity_reporting.rst     Sun Sep 11 01:30:15 
2022 +0200
@@ -0,0 +1,149 @@
+===================================
+Sale Opportunity Reporting Scenario
+===================================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model, Wizard
+    >>> from trytond.tests.tools import activate_modules
+    >>> from trytond.modules.company.tests.tools import (
+    ...     create_company, get_company)
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_chart, get_accounts)
+
+    >>> today = dt.date.today()
+
+Activate modules::
+
+    >>> config = activate_modules('sale_opportunity')
+
+    >>> Employee = Model.get('company.employee')
+    >>> Opportunity = Model.get('sale.opportunity')
+    >>> Party = Model.get('party.party')
+    >>> ProductCategory = Model.get('product.category')
+
+Create company::
+
+    >>> _ = create_company()
+    >>> company = get_company()
+
+Create employees::
+
+    >>> employee1_party = Party(name="Employee 1")
+    >>> employee1_party.save()
+    >>> employee1 = Employee(party=employee1_party)
+    >>> employee1.save()
+
+    >>> employee2_party = Party(name="Employee 2")
+    >>> employee2_party.save()
+    >>> employee2 = Employee(party=employee2_party)
+    >>> employee2.save()
+
+Create parties::
+
+    >>> customer1 = Party(name="Customer 1")
+    >>> customer1.save()
+    >>> customer2 = Party(name="Customer 2")
+    >>> customer2.save()
+
+Create leads and opportunities::
+
+    >>> lead = Opportunity(party=customer1)
+    >>> lead.amount = Decimal('1000.00')
+    >>> lead.save()
+
+    >>> opportunity = Opportunity(party=customer1)
+    >>> opportunity.amount = Decimal('2000.00')
+    >>> opportunity.employee = employee1
+    >>> opportunity.click('opportunity')
+
+    >>> opportunity = Opportunity(party=customer2)
+    >>> opportunity.amount = Decimal('500.00')
+    >>> opportunity.employee = employee2
+    >>> opportunity.end_date = today
+    >>> opportunity.click('opportunity')
+    >>> opportunity.click('convert')
+
+    >>> opportunity = Opportunity(party=customer1)
+    >>> opportunity.amount = Decimal('700.00')
+    >>> opportunity.employee = employee1
+    >>> opportunity.click('opportunity')
+    >>> opportunity.click('convert')
+    >>> sale, = opportunity.sales
+    >>> line = sale.lines.new()
+    >>> line.quantity = 1
+    >>> line.unit_price = Decimal('800.00')
+    >>> sale.invoice_address, = sale.party.addresses
+    >>> sale.click('quote')
+    >>> sale.click('confirm')
+
+    >>> opportunity = Opportunity(party=customer2)
+    >>> opportunity.amount = Decimal('200.00')
+    >>> opportunity.employee = employee2
+    >>> opportunity.click('opportunity')
+    >>> opportunity.click('lost')
+
+Check opportunity reporting::
+
+    >>> Main = Model.get('sale.opportunity.reporting.main')
+    >>> context = dict(
+    ...     from_date=today,
+    ...     to_date=today,
+    ...     period='month')
+    >>> with config.set_context(context=context):
+    ...     reports = Main.find([])
+    >>> report, = reports
+    >>> report.number
+    5
+    >>> report.amount
+    Decimal('4500.0')
+    >>> report.converted
+    2
+    >>> report.conversion_rate
+    0.4
+    >>> report.converted_amount == Decimal('1300.00')
+    True
+
+    >>> report, = report.time_series
+    >>> report.number
+    5
+    >>> report.amount
+    Decimal('4500.0')
+    >>> report.converted
+    2
+    >>> report.conversion_rate
+    0.4
+    >>> report.converted_amount == Decimal('1300.00')
+    True
+
+
+Check conversion reporting::
+
+    >>> Conversion = Model.get('sale.opportunity.reporting.conversion')
+    >>> with config.set_context(context=context):
+    ...     reports = Conversion.find([])
+    >>> report, = reports
+    >>> report.number
+    3
+    >>> report.converted
+    2
+    >>> report.won
+    1
+    >>> report.winning_rate
+    0.3333
+    >>> report.won_amount == Decimal('800.00')
+    True
+    >>> report.lost
+    1
+    >>> len(report.time_series)
+    1
+
+    >>> ConversionEmployee = Model.get(
+    ...     'sale.opportunity.reporting.conversion.employee')
+    >>> with config.set_context(context=context):
+    ...     reports = ConversionEmployee.find([])
+    >>> len(reports)
+    2
diff -r 0a4fd3b4c04a -r bc49b54ef788 tryton.cfg
--- a/tryton.cfg        Sun Sep 11 01:27:48 2022 +0200
+++ b/tryton.cfg        Sun Sep 11 01:30:15 2022 +0200
@@ -13,4 +13,5 @@
     opportunity.xml
     party.xml
     configuration.xml
+    opportunity_reporting.xml
     message.xml
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_employee_context_form.xml
--- a/view/opportunity_employee_context_form.xml        Sun Sep 11 01:27:48 
2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<form>
-    <label name="start_date"/>
-    <field name="start_date"/>
-    <label name="end_date"/>
-    <field name="end_date"/>
-</form>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_employee_graph1.xml
--- a/view/opportunity_employee_graph1.xml      Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<graph>
-    <x>
-        <field name="employee"/>
-    </x>
-    <y>
-        <field name="number"/>
-        <field name="converted"/>
-        <field name="won"/>
-        <field name="lost"/>
-    </y>
-</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_employee_graph2.xml
--- a/view/opportunity_employee_graph2.xml      Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<graph>
-    <x>
-        <field name="employee"/>
-    </x>
-    <y>
-        <field name="amount"/>
-        <field name="converted_amount"/>
-        <field name="won_amount"/>
-    </y>
-</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_employee_monthly_tree.xml
--- a/view/opportunity_employee_monthly_tree.xml        Sun Sep 11 01:27:48 
2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<tree>
-    <field name="month"/>
-    <field name="employee" expand="1"/>
-    <field name="number" optional="1"/>
-    <field name="converted" optional="1"/>
-    <field name="won" optional="0"/>
-    <field name="lost" optional="1"/>
-    <field name="conversion_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_rate"/>
-    </field>
-    <field name="winning_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_rate"/>
-    </field>
-    <field name="amount" optional="1"/>
-    <field name="converted_amount" optional="1"/>
-    <field name="won_amount" optional="0"/>
-    <field name="conversion_amount_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_amount_rate"/>
-    </field>
-    <field name="winning_amount_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_amount_rate"/>
-    </field>
-</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_employee_tree.xml
--- a/view/opportunity_employee_tree.xml        Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<tree>
-    <field name="employee" expand="1"/>
-    <field name="number" optional="1"/>
-    <field name="converted" optional="1"/>
-    <field name="won" optional="0"/>
-    <field name="lost" optional="1"/>
-    <field name="conversion_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_rate"/>
-    </field>
-    <field name="winning_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_rate"/>
-    </field>
-    <field name="amount" optional="1"/>
-    <field name="converted_amount" optional="1"/>
-    <field name="won_amount" optional="0"/>
-    <field name="conversion_amount_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_amount_rate"/>
-    </field>
-    <field name="winning_amount_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_amount_rate"/>
-    </field>
-</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_monthly_graph1.xml
--- a/view/opportunity_monthly_graph1.xml       Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<graph>
-    <x>
-        <field name="month_label"/>
-    </x>
-    <y>
-        <field name="number"/>
-        <field name="converted"/>
-        <field name="won"/>
-        <field name="lost"/>
-    </y>
-</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_monthly_graph2.xml
--- a/view/opportunity_monthly_graph2.xml       Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<graph>
-    <x>
-        <field name="month_label"/>
-    </x>
-    <y>
-        <field name="amount"/>
-        <field name="converted_amount"/>
-        <field name="won_amount"/>
-    </y>
-</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_monthly_tree.xml
--- a/view/opportunity_monthly_tree.xml Sun Sep 11 01:27:48 2022 +0200
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-<?xml version="1.0"?>
-<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
-this repository contains the full copyright notices and license terms. -->
-<tree>
-    <field name="month"/>
-    <field name="number" optional="1"/>
-    <field name="converted" optional="1"/>
-    <field name="won" optional="0"/>
-    <field name="lost" optional="1"/>
-    <field name="conversion_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_rate"/>
-    </field>
-    <field name="winning_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_rate"/>
-    </field>
-    <field name="amount" optional="1"/>
-    <field name="converted_amount" optional="1"/>
-    <field name="won_amount" optional="0"/>
-    <field name="conversion_amount_rate" factor="100" optional="1">
-        <suffix string="%" name="conversion_amount_rate"/>
-    </field>
-    <field name="winning_amount_rate" factor="100" optional="0">
-        <suffix string="%" name="winning_amount_rate"/>
-    </field>
-</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_reporting_context_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_context_form.xml       Sun Sep 11 01:30:15 
2022 +0200
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="from_date"/>
+    <group id="dates" col="-1">
+        <field name="from_date"/>
+        <label name="to_date"/>
+        <field name="to_date"/>
+    </group>
+    <label name="period"/>
+    <field name="period"/>
+    <label name="company"/>
+    <field name="company"/>
+</form>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_employee_graph_amount.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_employee_graph_amount.xml   Sun Sep 
11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="employee"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_employee_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_employee_graph_number.xml   Sun Sep 
11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="employee"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_employee_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_employee_list.xml   Sun Sep 11 
01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//field[@name='won']" position="before">
+        <field name="employee" expand="1"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_graph_amount.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_graph_amount.xml    Sun Sep 11 
01:30:15 2022 +0200
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<graph>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="won_amount"/>
+    </y>
+</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_graph_number.xml    Sun Sep 11 
01:30:15 2022 +0200
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<graph>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="won"/>
+        <field name="lost"/>
+    </y>
+</graph>
+
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_list.xml    Sun Sep 11 01:30:15 
2022 +0200
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree keyword_open="1">
+    <field name="won" sum="Won" optional="1"/>
+    <field name="winning_rate" factor="100">
+        <suffix string="%" name="winning_rate"/>
+    </field>
+    <field name="winning_trend" expand="1"/>
+    <field name="won_amount" sum="Won Amount" optional="1"/>
+    <field name="won_amount_trend" expand="1" optional="1"/>
+    <field name="lost" sum="Lost" optional="1"/>
+</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_time_series_graph_amount.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_time_series_graph_amount.xml        
Sun Sep 11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="date"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_time_series_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_time_series_graph_number.xml        
Sun Sep 11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="date"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_conversion_time_series_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_conversion_time_series_list.xml        Sun Sep 
11 01:30:15 2022 +0200
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="date"/>
+    <field name="won" sum="Won" optional="0"/>
+    <field name="won_amount" sum="Won Amount" optional="0"/>
+    <field name="lost" sum="Lost" optional="1"/>
+</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_main_graph_amount.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_graph_amount.xml  Sun Sep 11 01:30:15 
2022 +0200
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<graph>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="amount"/>
+        <field name="converted_amount"/>
+    </y>
+</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_main_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_graph_number.xml  Sun Sep 11 01:30:15 
2022 +0200
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<graph>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="number"/>
+        <field name="converted"/>
+    </y>
+</graph>
diff -r 0a4fd3b4c04a -r bc49b54ef788 view/opportunity_reporting_main_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_list.xml  Sun Sep 11 01:30:15 2022 +0200
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree keyword_open="1">
+    <field name="number" sum="Number"/>
+    <field name="number_trend" expand="1"/>
+    <field name="amount" sum="Amount" optional="1"/>
+    <field name="amount_trend" expand="1" optional="1"/>
+    <field name="converted" sum="Converted" optional="1"/>
+    <field name="conversion_rate" factor="100" optional="0">
+        <suffix string="%" name="conversion_rate"/>
+    </field>
+    <field name="conversion_trend" expand="1" optional="0"/>
+    <field name="converted_amount" sum="Converted Amount" optional="1"/>
+    <field name="converted_amount_trend" expand="1" optional="1"/>
+</tree>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_main_time_series_graph_amount.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_time_series_graph_amount.xml      Sun Sep 
11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="date"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_main_time_series_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_time_series_graph_number.xml      Sun Sep 
11 01:30:15 2022 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+    <xpath expr="//x/field[@name='id']" position="replace">
+        <field name="date"/>
+    </xpath>
+</data>
diff -r 0a4fd3b4c04a -r bc49b54ef788 
view/opportunity_reporting_main_time_series_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/opportunity_reporting_main_time_series_list.xml      Sun Sep 11 
01:30:15 2022 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="date"/>
+    <field name="number" sum="Number"/>
+    <field name="amount" sum="Amount" optional="1"/>
+    <field name="converted" sum="Converted" optional="0"/>
+    <field name="converted_amount" sum="Converted Amount" optional="1"/>
+</tree>

Reply via email to