Refactor reporting
issue11681 review417881003
This commit is contained in:
1 parent
bc31ea4fbd
commit
fb385844d4
29 files changed
+1256
-469
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Refactor reporting
|
||||
* Use default customer payment term
|
||||
* Allow to store currency on opportunity
|
||||
|
||||
|
||||
+9
-5
@@ -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,
|
||||
|
||||
+45
@@ -9,5 +9,50 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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>
|
||||
+1
-209
@@ -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 @@ class SaleOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
|
||||
@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
|
||||
-161
@@ -241,166 +241,5 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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>
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -58,7 +58,7 @@ else:
|
||||
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 @@ setup(name=name,
|
||||
python_requires='>=3.7',
|
||||
install_requires=requires,
|
||||
extras_require={
|
||||
'sparklines': ['pygal'],
|
||||
'test': tests_require,
|
||||
},
|
||||
dependency_links=dependency_links,
|
||||
|
||||
@@ -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
|
||||
@@ -13,4 +13,5 @@ xml:
|
||||
opportunity.xml
|
||||
party.xml
|
||||
configuration.xml
|
||||
opportunity_reporting.xml
|
||||
message.xml
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+5
-6
@@ -1,9 +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. -->
|
||||
<form>
|
||||
<label name="start_date"/>
|
||||
<field name="start_date"/>
|
||||
<label name="end_date"/>
|
||||
<field name="end_date"/>
|
||||
</form>
|
||||
<data>
|
||||
<xpath expr="//x/field[@name='id']" position="replace">
|
||||
<field name="employee"/>
|
||||
</xpath>
|
||||
</data>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+1
-3
@@ -3,11 +3,9 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<graph>
|
||||
<x>
|
||||
<field name="month_label"/>
|
||||
<field name="id"/>
|
||||
</x>
|
||||
<y>
|
||||
<field name="amount"/>
|
||||
<field name="converted_amount"/>
|
||||
<field name="won_amount"/>
|
||||
</y>
|
||||
</graph>
|
||||
+2
-3
@@ -3,12 +3,11 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<graph>
|
||||
<x>
|
||||
<field name="month_label"/>
|
||||
<field name="id"/>
|
||||
</x>
|
||||
<y>
|
||||
<field name="number"/>
|
||||
<field name="converted"/>
|
||||
<field name="won"/>
|
||||
<field name="lost"/>
|
||||
</y>
|
||||
</graph>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+1
-2
@@ -3,11 +3,10 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<graph>
|
||||
<x>
|
||||
<field name="employee"/>
|
||||
<field name="id"/>
|
||||
</x>
|
||||
<y>
|
||||
<field name="amount"/>
|
||||
<field name="converted_amount"/>
|
||||
<field name="won_amount"/>
|
||||
</y>
|
||||
</graph>
|
||||
+1
-3
@@ -3,12 +3,10 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<graph>
|
||||
<x>
|
||||
<field name="employee"/>
|
||||
<field name="id"/>
|
||||
</x>
|
||||
<y>
|
||||
<field name="number"/>
|
||||
<field name="converted"/>
|
||||
<field name="won"/>
|
||||
<field name="lost"/>
|
||||
</y>
|
||||
</graph>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user