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>