186 lines
7.6 KiB
Python
186 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from datetime import date, datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
import babel.dates
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
from odoo.osv import expression
|
|
from odoo.tools.misc import get_lang
|
|
|
|
DISPLAY_FORMATS = {
|
|
'day': '%d %b %Y',
|
|
'week': 'W%W %Y',
|
|
'month': '%B %Y',
|
|
'year': '%Y',
|
|
}
|
|
|
|
|
|
class Base(models.AbstractModel):
|
|
_inherit = 'base'
|
|
|
|
@api.model
|
|
def get_cohort_data(self, date_start, date_stop, measure, interval, domain, mode, timeline):
|
|
"""
|
|
Get all the data needed to display a cohort view
|
|
|
|
:param date_start: the starting date to use in the group_by clause
|
|
:param date_stop: the date field which mark the change of state
|
|
:param measure: the field to aggregate
|
|
:param interval: the interval of time between two cells ('day', 'week', 'month', 'year')
|
|
:param domain: a domain to limit the read_group
|
|
:param mode: the mode of aggregation ('retention', 'churn') [default='retention']
|
|
:param timeline: the direction to display data ('forward', 'backward') [default='forward']
|
|
:return: dictionary containing a total amount of records considered and a
|
|
list of rows each of which contains 16 cells.
|
|
"""
|
|
rows = []
|
|
columns_avg = defaultdict(lambda: dict(percentage=0, count=0))
|
|
total_value = 0
|
|
initial_churn_value = 0
|
|
if measure != '__count':
|
|
domain = expression.AND([domain, [(measure, '!=', False)]])
|
|
measures = [f'{measure}:sum']
|
|
field = self._fields[measure]
|
|
if field.type == 'many2one':
|
|
measure = f'{measure}:count_distinct'
|
|
else:
|
|
measure = f'{measure}:{field.aggregator}'
|
|
measures.append(measure)
|
|
else:
|
|
measures = ['__count', '__count']
|
|
|
|
locale = get_lang(self.env).code
|
|
|
|
domain = expression.AND([domain, [(date_start, '!=', False)]]) # date not set are no take in account
|
|
row_groups = self._read_group(
|
|
domain=domain,
|
|
groupby=[date_start + ':' + interval],
|
|
aggregates=measures,
|
|
)
|
|
|
|
date_start_field = self._fields[date_start]
|
|
if date_start_field.type == 'datetime':
|
|
today = datetime.today()
|
|
convert_method = fields.Datetime.to_datetime
|
|
else:
|
|
today = date.today()
|
|
convert_method = fields.Date.to_date
|
|
|
|
for group_value, sum_value, value in row_groups:
|
|
total_value += value
|
|
group_domain = expression.AND([
|
|
domain,
|
|
['&', (date_start, '>=', group_value), (date_start, '<', group_value + models.READ_GROUP_TIME_GRANULARITY[interval])]
|
|
])
|
|
sub_group = self._read_group(
|
|
domain=group_domain,
|
|
groupby=[date_stop + ':' + interval],
|
|
aggregates=[measure],
|
|
)
|
|
sub_group_per_period = {
|
|
convert_method(group_value): aggregate_value
|
|
for group_value, aggregate_value in sub_group
|
|
}
|
|
|
|
columns = []
|
|
initial_value = sum_value
|
|
col_range = range(-15, 1) if timeline == 'backward' else range(0, 16)
|
|
for col_index, col in enumerate(col_range):
|
|
col_start_date = group_value
|
|
if interval == 'day':
|
|
col_start_date += relativedelta(days=col)
|
|
col_end_date = col_start_date + relativedelta(days=1)
|
|
elif interval == 'week':
|
|
col_start_date += relativedelta(days=7 * col)
|
|
col_end_date = col_start_date + relativedelta(days=7)
|
|
elif interval == 'month':
|
|
col_start_date += relativedelta(months=col)
|
|
col_end_date = col_start_date + relativedelta(months=1)
|
|
else:
|
|
col_start_date += relativedelta(years=col)
|
|
col_end_date = col_start_date + relativedelta(years=1)
|
|
|
|
if col_start_date > today:
|
|
columns_avg[col_index]
|
|
columns.append({
|
|
'value': '-',
|
|
'churn_value': '-',
|
|
'percentage': '',
|
|
})
|
|
continue
|
|
|
|
col_value = sub_group_per_period.get(col_start_date, 0.0)
|
|
|
|
# In backward timeline, if columns are out of given range, we need
|
|
# to set initial value for calculating correct percentage
|
|
if timeline == 'backward' and col_index == 0:
|
|
outside_timeline_domain = expression.AND(
|
|
[
|
|
group_domain,
|
|
['|',
|
|
(date_stop, '=', False),
|
|
(date_stop, '>=', fields.Datetime.to_string(col_start_date)),
|
|
]
|
|
]
|
|
)
|
|
col_group = self._read_group(
|
|
domain=outside_timeline_domain,
|
|
aggregates=[measure],
|
|
)
|
|
initial_value = float(col_group[0][0])
|
|
initial_churn_value = sum_value - initial_value
|
|
|
|
previous_col_remaining_value = initial_value if col_index == 0 else columns[-1]['value']
|
|
col_remaining_value = previous_col_remaining_value - col_value
|
|
percentage = sum_value and (col_remaining_value) / sum_value or 0
|
|
if mode == 'churn':
|
|
percentage = 1 - percentage
|
|
|
|
percentage = round(100 * percentage, 1)
|
|
|
|
columns_avg[col_index]['percentage'] += percentage
|
|
columns_avg[col_index]['count'] += 1
|
|
# For 'week' interval, we display a better tooltip (range like : '02 Jul - 08 Jul')
|
|
if interval == 'week':
|
|
period = "%s - %s" % (col_start_date.strftime('%d %b'), (col_end_date - relativedelta(days=1)).strftime('%d %b'))
|
|
else:
|
|
period = col_start_date.strftime(DISPLAY_FORMATS[interval])
|
|
|
|
if mode == 'churn':
|
|
mode_domain = [
|
|
(date_stop, '<', col_end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
|
]
|
|
else:
|
|
mode_domain = ['|',
|
|
(date_stop, '>=', col_end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
|
(date_stop, '=', False),
|
|
]
|
|
|
|
columns.append({
|
|
'value': col_remaining_value,
|
|
'churn_value': col_value + (columns[-1]['churn_value'] if col_index > 0 else initial_churn_value),
|
|
'percentage': percentage,
|
|
'domain': mode_domain,
|
|
'period': period,
|
|
})
|
|
|
|
rows.append({
|
|
'date': babel.dates.format_date(
|
|
group_value, format=models.READ_GROUP_DISPLAY_FORMAT[interval],
|
|
locale=locale,
|
|
),
|
|
'value': value,
|
|
'domain': group_domain,
|
|
'columns': columns,
|
|
})
|
|
|
|
return {
|
|
'rows': rows,
|
|
'avg': {'avg_value': total_value / len(rows) if rows else 0, 'columns_avg': columns_avg},
|
|
}
|