182 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Part of Odoo. See LICENSE file for full copyright and licensing details.
 | |
| 
 | |
| from collections import defaultdict
 | |
| from datetime import datetime
 | |
| from dateutil.relativedelta import relativedelta
 | |
| from odoo import api, fields, models
 | |
| from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
 | |
| from odoo.osv import expression
 | |
| 
 | |
| 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
 | |
|         measure_is_many2one = self._fields.get(measure) and self._fields.get(measure).type == 'many2one'
 | |
|         field_measure = (
 | |
|             [measure + ':count_distinct']
 | |
|             if measure_is_many2one
 | |
|             else ([measure] if  self._fields.get(measure) else [])
 | |
|         )
 | |
|         row_groups = self._read_group_raw(
 | |
|             domain=domain,
 | |
|             fields=[date_start] + field_measure,
 | |
|             groupby=date_start + ':' + interval
 | |
|         )
 | |
|         for group in row_groups:
 | |
|             dates = group['%s:%s' % (date_start, interval)]
 | |
|             if not dates:
 | |
|                 continue
 | |
|             # Split with space for smoothly format datetime field
 | |
|             clean_start_date = dates[0].split('/')[0].split(' ')[0]
 | |
|             cohort_start_date = fields.Datetime.from_string(clean_start_date)
 | |
|             if measure == '__count__':
 | |
|                 value = float(group[date_start + '_count'])
 | |
|             else:
 | |
|                 value = float(group[measure] or 0.0)
 | |
|             total_value += value
 | |
| 
 | |
|             sub_group = self._read_group_raw(
 | |
|                 domain=group['__domain'],
 | |
|                 fields=[date_stop] + field_measure,
 | |
|                 groupby=date_stop + ':' + interval
 | |
|             )
 | |
|             sub_group_per_period = {}
 | |
|             for g in sub_group:
 | |
|                 d_stop = g["%s:%s" % (date_stop, interval)]
 | |
|                 if d_stop:
 | |
|                     date_group = fields.Datetime.from_string(d_stop[0].split('/')[0])
 | |
|                     group_interval = date_group.strftime(DISPLAY_FORMATS[interval])
 | |
|                     sub_group_per_period[group_interval] = g
 | |
| 
 | |
|             columns = []
 | |
|             initial_value = value
 | |
|             col_range = range(-15, 1) if timeline == 'backward' else range(0, 16)
 | |
|             for col_index, col in enumerate(col_range):
 | |
|                 col_start_date = cohort_start_date
 | |
|                 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 > datetime.today():
 | |
|                     columns_avg[col_index]
 | |
|                     columns.append({
 | |
|                         'value': '-',
 | |
|                         'churn_value': '-',
 | |
|                         'percentage': '',
 | |
|                     })
 | |
|                     continue
 | |
| 
 | |
|                 significative_period = col_start_date.strftime(DISPLAY_FORMATS[interval])
 | |
|                 col_group = sub_group_per_period.get(significative_period, {})
 | |
|                 if not col_group:
 | |
|                     col_value = 0.0
 | |
|                 elif measure == '__count__':
 | |
|                     col_value = col_group[date_stop + '_count']
 | |
|                 else:
 | |
|                     col_value = col_group[measure] or 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_raw(
 | |
|                         domain=outside_timeline_domain,
 | |
|                         fields=field_measure,
 | |
|                         groupby=[]
 | |
|                     )
 | |
|                     if measure == '__count__':
 | |
|                         initial_value = float(col_group[0]['__count'])
 | |
|                     else:
 | |
|                         initial_value = float(col_group[0][measure] or 0.0)
 | |
|                     initial_churn_value = 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 = value and (col_remaining_value) / 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':
 | |
|                     domain = [
 | |
|                         (date_stop, '<', col_end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)),
 | |
|                     ]
 | |
|                 else:
 | |
|                     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': domain,
 | |
|                     'period': period,
 | |
|                 })
 | |
| 
 | |
|             rows.append({
 | |
|                 'date': dates[1],
 | |
|                 '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},
 | |
|         }
 |