[ADD] base modules

This commit is contained in:
Muhammad
2024-04-07 12:43:39 +05:00
parent 311598a929
commit fa3d921e2d
276 changed files with 51186 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
odoo.define('web_cohort.CohortController', function (require) {
'use strict';
const AbstractController = require('web.AbstractController');
const config = require('web.config');
const core = require('web.core');
const framework = require('web.framework');
const session = require('web.session');
const qweb = core.qweb;
const _t = core._t;
var CohortController = AbstractController.extend({
custom_events: Object.assign({}, AbstractController.prototype.custom_events, {
row_clicked: '_onRowClicked',
}),
/**
* @override
* @param {Widget} parent
* @param {CohortModel} model
* @param {CohortRenderer} renderer
* @param {Object} params
* @param {string} params.modelName
* @param {string} params.title
* @param {Object} params.measures
* @param {Object} params.intervals
* @param {string} params.dateStartString
* @param {string} params.dateStopString
* @param {string} params.timeline
* @param {Array[]} params.views
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.title = params.title;
this.measures = params.measures;
this.intervals = params.intervals;
this.dateStartString = params.dateStartString;
this.dateStopString = params.dateStopString;
this.timeline = params.timeline;
this.views = params.views;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Returns the current mode, measure and groupbys, so we can restore the
* view when we save the current state in the search view, or when we add it
* to the dashboard.
*
* @override
* @returns {Object}
*/
getOwnedQueryParams: function () {
var state = this.model.get();
return {
context: {
cohort_measure: state.measure,
cohort_interval: state.interval,
}
};
},
/**
* @override
* @param {jQuery} [$node]
*/
renderButtons: function ($node) {
this.$buttons = $(qweb.render('CohortView.buttons', {
measures: _.sortBy(_.pairs(this.measures), function(x){ return x[1].toLowerCase(); }),
intervals: this.intervals,
isMobile: config.device.isMobile
}));
this.$measureList = this.$buttons.find('.o_cohort_measures_list');
this.$buttons.on('click', 'button', this._onButtonClick.bind(this));
if ($node) {
this.$buttons.appendTo($node);
}
},
/**
* Makes sure that the buttons in the control panel matches the current
* state (so, correct active buttons and stuff like that);
*
* @override
*/
updateButtons: function () {
if (!this.$buttons) {
return;
}
var data = this.model.get();
// Hide download button if no cohort data
var noData = !data.report.rows.length &&
(!data.comparisonReport ||
!data.comparisonReport.rows.length);
this.$buttons.find('.o_cohort_download_button').toggleClass(
'd-none',
noData
);
if (config.device.isMobile) {
var $activeInterval = this.$buttons
.find('.o_cohort_interval_button[data-interval="' + data.interval + '"]');
this.$buttons.find('.dropdown_cohort_content').text($activeInterval.text());
}
this.$buttons.find('.o_cohort_interval_button').removeClass('active');
this.$buttons
.find('.o_cohort_interval_button[data-interval="' + data.interval + '"]')
.addClass('active');
_.each(this.$measureList.find('.dropdown-item'), function (el) {
var $el = $(el);
$el.toggleClass('selected', $el.data('field') === data.measure);
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Export cohort data in Excel file
*
* @private
*/
_downloadExcel: function () {
var data = this.model.get();
data = _.extend(data, {
title: this.title,
interval_string: this.intervals[data.interval].toString(), // intervals are lazy-translated
measure_string: this.measures[data.measure] || _t('Count'),
date_start_string: this.dateStartString,
date_stop_string: this.dateStopString,
timeline: this.timeline,
});
framework.blockUI();
session.get_file({
url: '/web/cohort/export',
data: {data: JSON.stringify(data)},
complete: framework.unblockUI,
error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
/**
* @private
* @param {string} interval
*/
_setInterval: function (interval) {
this.update({interval: interval});
},
/**
* @private
* @param {string} measure should be a valid (and aggregatable) field name
*/
_setMeasure: function (measure) {
this.update({measure: measure});
},
/**
* @override
* @private
* @returns {Promise}
*/
_update: function () {
this.updateButtons();
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Do what need to be done when a button from the control panel is clicked.
*
* @private
* @param {MouseEvent} ev
*/
_onButtonClick: function (ev) {
var $btn = $(ev.currentTarget);
if ($btn.hasClass('o_cohort_interval_button')) {
this._setInterval($btn.data('interval'));
} else if ($btn.hasClass('o_cohort_download_button')) {
this._downloadExcel();
} else if ($btn.closest('.o_cohort_measures_list').length) {
ev.preventDefault();
ev.stopPropagation();
this._setMeasure($btn.data('field'));
}
},
/**
* Open view when clicked on row
*
* @private
* @param {OdooEvent} event
*/
_onRowClicked: function (event) {
this.do_action({
type: 'ir.actions.act_window',
name: this.title,
res_model: this.modelName,
views: this.views,
domain: event.data.domain,
});
},
});
return CohortController;
});

View File

@@ -0,0 +1,143 @@
odoo.define('web_cohort.CohortModel', function (require) {
'use strict';
var AbstractModel = require('web.AbstractModel');
var CohortModel = AbstractModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
* @returns {Object}
*/
__get: function () {
const { rangeDescription, comparisonRangeDescription } = this.timeRanges;
return Object.assign({}, this.data, {
hasContent: !this._isEmpty(),
isSample: this.isSampleModel,
rangeDescription,
comparisonRangeDescription
});
},
/**
* @override
* @param {Object} params
* @param {string} params.modelName
* @param {string} params.dateStart
* @param {string} params.dateStop
* @param {string} params.measure
* @param {string} params.interval
* @param {Array[]} params.domain
* @param {string} params.mode
* @param {string} params.timeline
* @param {Object} params.timeRanges
* @returns {Promise}
*/
__load: function (params) {
this.modelName = params.modelName;
this.dateStart = params.dateStart;
this.dateStop = params.dateStop;
this.measure = params.measure;
this.interval = params.interval;
this.domain = params.domain;
this.mode = params.mode;
this.timeline = params.timeline;
this.data = {
measure: this.measure,
interval: this.interval,
};
this.context = params.context;
this.timeRanges = params.timeRanges;
return this._fetchData();
},
/**
* Reload data.
*
* @param {any} handle
* @param {Object} params
* @param {string} [params.measure]
* @param {string} [params.interval]
* @param {Array[]} [params.domain]
* @param {Object} [params.timeRanges]
* @returns {Promise}
*/
__reload: function (handle, params) {
if ('measure' in params) {
this.data.measure = params.measure;
}
if ('interval' in params) {
this.data.interval = params.interval;
}
if ('domain' in params) {
this.domain = params.domain;
}
if ('timeRanges' in params) {
this.timeRanges = params.timeRanges;
}
return this._fetchData();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Fetch cohort data.
*
* @private
* @returns {Promise}
*/
_fetchData: function () {
const domains = this._getDomains();
const proms = domains.map(domain => {
return this._rpc({
model: this.modelName,
method: 'get_cohort_data',
kwargs: {
date_start: this.dateStart,
date_stop: this.dateStop,
measure: this.data.measure,
interval: this.data.interval,
domain: domain,
mode: this.mode,
timeline: this.timeline,
context: this.context
}
});
});
return Promise.all(proms).then(([report, comparisonReport]) => {
this.data.report = report;
this.data.comparisonReport = comparisonReport;
});
},
/**
* @private
* @returns {Array[]}
*/
_getDomains: function () {
const { range, comparisonRange } = this.timeRanges;
if (!range) {
return [this.domain];
}
return [
this.domain.concat(range),
this.domain.concat(comparisonRange),
];
},
/**
* @override
*/
_isEmpty() {
let rowCount = this.data.report.rows.length;
if (this.data.comparisonReport) {
rowCount += this.data.comparisonReport.rows.length;
}
return rowCount === 0;
},
});
return CohortModel;
});

View File

@@ -0,0 +1,83 @@
odoo.define('web_cohort.CohortRenderer', function (require) {
'use strict';
const OwlAbstractRenderer = require('web.AbstractRendererOwl');
const field_utils = require('web.field_utils');
const patchMixin = require('web.patchMixin');
class CohortRenderer extends OwlAbstractRenderer {
constructor() {
super(...arguments);
this.sampleDataTargets = ['table'];
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @param {integer} value
* @returns {Array} first integers from 0 to value-1
*/
_range(value) {
return _.range(value);
}
/**
* @param {float} value
* @returns {string} formatted value with 1 digit
*/
_formatFloat(value) {
return field_utils.format.float(value, null, {
digits: [42, 1],
});
}
/**
* @param {float} value
* @returns {string} formatted value with 1 digit
*/
_formatPercentage(value) {
return field_utils.format.percentage(value, null, {
digits: [42, 1],
});
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onClickRow(ev) {
if (!ev.target.classList.contains('o_cohort_value')) {
return;
}
const rowData = ev.currentTarget.dataset;
const rowIndex = rowData.rowIndex;
const colIndex = ev.target.dataset.colIndex;
const row = (rowData.type === 'data') ?
this.props.report.rows[rowIndex] :
this.props.comparisonReport.rows[rowIndex];
const rowDomain = row ? row.domain : [];
const cellContent = row ? row.columns[colIndex] : false;
const cellDomain = cellContent ? cellContent.domain : [];
const fullDomain = rowDomain.concat(cellDomain);
if (cellDomain.length) {
fullDomain.unshift('&');
}
if (fullDomain.length) {
this.trigger('row_clicked', {
domain: fullDomain
});
}
}
}
CohortRenderer.template = 'web_cohort.CohortRenderer';
return patchMixin(CohortRenderer);
});

View File

@@ -0,0 +1,136 @@
odoo.define('web_cohort.CohortView', function (require) {
'use strict';
var AbstractView = require('web.AbstractView');
var core = require('web.core');
var view_registry = require('web.view_registry');
var CohortController = require('web_cohort.CohortController');
var CohortModel = require('web_cohort.CohortModel');
var CohortRenderer = require('web_cohort.CohortRenderer');
const RendererWrapper = require('web.RendererWrapper');
var _t = core._t;
var _lt = core._lt;
var intervals = {
day: _lt('Day'),
week: _lt('Week'),
month: _lt('Month'),
year: _lt('Year'),
};
var CohortView = AbstractView.extend({
display_name: _lt('Cohort'),
icon: 'fa-signal',
config: _.extend({}, AbstractView.prototype.config, {
Model: CohortModel,
Controller: CohortController,
Renderer: CohortRenderer,
}),
viewType: 'cohort',
searchMenuTypes: ['filter', 'comparison', 'favorite'],
/**
* @override
*/
init: function (viewInfo, params) {
this._super.apply(this, arguments);
var fields = this.fields;
var attrs = this.arch.attrs;
const additionalMeasures = params.additionalMeasures || [];
if (!attrs.date_start) {
throw new Error(_lt('Cohort view has not defined "date_start" attribute.'));
}
if (!attrs.date_stop) {
throw new Error(_lt('Cohort view has not defined "date_stop" attribute.'));
}
// Renderer Parameters
var measures = {};
_.each(fields, function (field, name) {
if (name !== 'id' && field.store === true && _.contains(['integer', 'float', 'monetary'], field.type)) {
measures[name] = field.string;
}
});
this.arch.children.forEach(field => {
let fieldName = field.attrs.name;
// Remove invisible fields from the measures
if (
!additionalMeasures.includes(fieldName) &&
field.attrs.invisible && py.eval(field.attrs.invisible)
) {
delete measures[fieldName];
return;
}
if (fieldName in measures && field.attrs.string) {
measures[fieldName] = field.attrs.string;
}
});
measures.__count__ = _t('Count');
this.rendererParams.measures = measures;
this.rendererParams.intervals = intervals;
// Controller Parameters
this.controllerParams.measures = _.omit(measures, '__count__');
this.controllerParams.intervals = intervals;
this.controllerParams.title = params.title || attrs.string || _t('Untitled');
// Used in export
// Retrieve form and list view ids from the action to open those views
// when a row of the cohort view is clicked
this.controllerParams.views = [
_findViewID('list'),
_findViewID('form'),
];
function _findViewID(viewType) {
var action = params.action;
if (action === undefined) {
return [false, viewType];
}
var contextID = viewType === 'list' ? action.context.list_view_id : action.context.form_view_id;
var result = _.findWhere(action.views, {type: viewType});
return [contextID || (result ? result.viewID : false), viewType];
}
},
_updateMVCParams: function () {
this._super.apply(this, arguments);
// Model Parameters
var context = this.loadParams.context;
var attrs = this.arch.attrs;
this.loadParams.dateStart = context.cohort_date_start || attrs.date_start;
this.loadParams.dateStop = context.cohort_date_stop || attrs.date_stop;
this.loadParams.mode = context.cohort_mode || attrs.mode || 'retention';
this.loadParams.timeline = context.cohort_timeline || attrs.timeline || 'forward';
this.loadParams.measure = context.cohort_measure || attrs.measure || '__count__';
this.loadParams.interval = context.cohort_interval || attrs.interval || 'day';
this.rendererParams.mode = this.loadParams.mode;
this.rendererParams.timeline = this.loadParams.timeline;
this.rendererParams.dateStartString = this.fields[this.loadParams.dateStart].string;
this.rendererParams.dateStopString = this.fields[this.loadParams.dateStop].string;
this.controllerParams.dateStartString = this.rendererParams.dateStartString;
this.controllerParams.dateStopString = this.rendererParams.dateStopString;
this.controllerParams.timeline = this.rendererParams.timeline;
},
/**
* @override
*/
getRenderer(parent, state) {
state = Object.assign({}, state, this.rendererParams);
return new RendererWrapper(null, this.config.Renderer, state);
},
});
view_registry.add('cohort', CohortView);
return CohortView;
});

View File

@@ -0,0 +1,92 @@
odoo.define('web_cohort/static/src/js/sample_server.js', function (require) {
"use strict";
const SampleServer = require('web.SampleServer');
/**
* This function mocks calls to the 'get_cohort_data' method. It is
* registered to the SampleServer's mockRegistry, so it is called with a
* SampleServer instance as "this".
* @private
* @param {Object} params
* @param {string} params.model
* @param {Object} params.kwargs
* @returns {Object}
*/
function _mockGetCohortData(params) {
const { model } = params;
const { date_start, interval, measure, mode, timeline } = params.kwargs;
const columns_avg = {};
const rows = [];
let initialChurnValue = 0;
const groups = this._mockReadGroup({ model, fields: [date_start], groupBy: [date_start + ':' + interval] });
const totalCount = groups.length;
let totalValue = 0;
for (const group of groups) {
const format = SampleServer.FORMATS[interval];
const displayFormat = SampleServer.DISPLAY_FORMATS[interval];
const date = moment(group[date_start + ':' + interval], format);
const now = moment();
let colStartDate = date.clone();
if (timeline === 'backward') {
colStartDate = colStartDate.subtract(15, interval);
}
let value = measure === '__count__' ?
this._getRandomInt(SampleServer.MAX_INTEGER) :
this._generateFieldValue(model, measure);
value = value || 25;
totalValue += value;
let initialValue = value;
let max = value;
const columns = [];
for (let column = 0; column <= 15; column++) {
if (!columns_avg[column]) {
columns_avg[column] = { percentage: 0, count: 0 };
}
if (colStartDate.clone().add(column, interval) > now) {
columns.push({ value: '-', churn_value: '-', percentage: '' });
continue;
}
let colValue = 0;
if (max > 0) {
colValue = Math.min(Math.round(Math.random() * max), max);
max -= colValue;
}
if (timeline === 'backward' && column === 0) {
initialValue = Math.min(Math.round(Math.random() * value), value);
initialChurnValue = value - initialValue;
}
const previousValue = column === 0 ? initialValue : columns[column - 1].value;
const remainingValue = previousValue - colValue;
const previousChurnValue = column === 0 ? initialChurnValue : columns[column - 1].churn_value;
const churn_value = colValue + previousChurnValue;
let percentage = value ? parseFloat(remainingValue / value) : 0;
if (mode === 'churn') {
percentage = 1 - percentage;
}
percentage = Number((100 * percentage).toFixed(1));
columns_avg[column].percentage += percentage;
columns_avg[column].count += 1;
columns.push({
value: remainingValue,
churn_value,
percentage,
period: column, // used as a t-key but we don't care about value itself
});
}
const keepRow = columns.some(c => c.percentage !== '');
if (keepRow) {
rows.push({ date: date.format(displayFormat), value, columns });
}
}
const avg_value = totalCount ? (totalValue / totalCount) : 0;
const avg = { avg_value, columns_avg };
return { rows, avg };
}
SampleServer.mockRegistry.add('get_cohort_data', _mockGetCohortData);
});

View File

@@ -0,0 +1,67 @@
// ------------------------------------------------------------------
// Cohort View
// ------------------------------------------------------------------
$o-cohort-heading-bg-color: darken(theme-color('light'), 4%);
$o-cohort-border-color: darken($o-cohort-heading-bg-color, 6%);
$o-cohort-hover-color: lighten($o-cohort-heading-bg-color, 2%);
.o_cohort_view {
.table {
border-bottom: 1px solid $o-cohort-border-color;
thead {
background-color: $o-cohort-heading-bg-color;
> tr > th {
border-bottom: 1px solid $o-cohort-border-color;
}
}
tbody {
> tr {
&.o_cohort_row_clickable {
&:hover {
.o_cohort_value {
cursor: pointer;
}
background-color: $o-cohort-hover-color;
@include media-breakpoint-up(lg) {
td:first-child {
position: relative;
&:before {
content: '';
@include o-position-absolute($top: 0px, $left: 0px);
height: 100%;
width: 3px;
background-color: $o-brand-primary;
}
}
}
}
}
& > td {
padding: 0px;
> div {
padding: 3px;
&.o_cohort_highlight {
margin: 2px;
border-radius: 2px;
}
}
}
}
}
tfoot {
background-color: $o-cohort-heading-bg-color;
font-weight: 500;
}
tr > th, tr > td {
border-left: 1px solid $o-cohort-border-color;
text-align: center;
vertical-align: middle;
}
}
.o_cohort_no_data {
padding: 15px;
font-size: 18px;
border: 1px solid $o-cohort-border-color;
background-color: $o-cohort-heading-bg-color;
}
}

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="CohortView.buttons">
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
Measures <span class="caret" />
</button>
<div class="dropdown-menu o_cohort_measures_list">
<button t-foreach="measures" t-as="measure"
type="button"
class="dropdown-item"
t-att-data-field="measure[0]">
<t t-esc="measure[1]"/>
</button>
<div t-if="_.keys(measures).length" class="dropdown-divider"/>
<button type="button" class="dropdown-item" data-field="__count__">Count</button>
</div>
</div>
<div class="btn-group" t-if="isMobile">
<a class="btn btn-link dropdown-toggle" href="#" data-toggle="dropdown" aria-expanded="false">
<span class="dropdown_cohort_content mr4"></span>
</a>
<div class="dropdown-menu" role="menu">
<button t-foreach="intervals" t-as="interval" class="btn btn-secondary o_cohort_interval_button dropdown-item" t-att-data-interval="interval" style="display:block;">
<t t-esc="intervals[interval]" />
</button>
</div>
</div>
<div class="btn-group" t-else="">
<button t-foreach="intervals" t-as="interval" class="btn btn-secondary o_cohort_interval_button" t-att-data-interval="interval">
<t t-esc="intervals[interval]" />
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary fa fa-download o_cohort_download_button" title="Download in Excel file"></button>
</div>
</t>
<div t-name="web_cohort.CohortRenderer" class="o_cohort_view" owl="1">
<div t-if="props.hasContent">
<div t-if="props.comparisonReport &amp;&amp; (props.comparisonReport.rows.length || props.report.rows.length)" class="table-responsive">
<t t-call="CohortView.tableTitle">
<t t-set="title" t-value="props.rangeDescription"/>
</t>
</div>
<div t-if="props.report.rows.length" class="table-responsive">
<t t-call="CohortView.tableContent">
<t t-set="col_length" t-value="props.report.rows[0].columns.length"/>
<t t-set="report_rows" t-value="props.report.rows"/>
<t t-set="report_avg" t-value="props.report.avg" />
</t>
</div>
<div t-if="!props.report.rows.length &amp;&amp; props.comparisonReport &amp;&amp; props.comparisonReport.rows.length" class="o_cohort_no_data text-center">
No data available.
</div>
<br/>
<div t-if="props.comparisonReport &amp;&amp; (props.report.rows.length || props.comparisonReport.rows.length)" class="table-responsive">
<t t-call="CohortView.tableTitle">
<t t-set="title" t-value="props.comparisonRangeDescription"/>
</t>
</div>
<div t-if="props.comparisonReport &amp;&amp; props.comparisonReport.rows.length" class="table-responsive">
<t t-call="CohortView.tableContent">
<t t-set="col_length" t-value="props.comparisonReport.rows[0].columns.length"/>
<t t-set="report_rows" t-value="props.comparisonReport.rows"/>
<t t-set="report_avg" t-value="props.comparisonReport.avg" />
</t>
</div>
<div t-if="props.report.rows.length &amp;&amp; props.comparisonReport &amp;&amp; !props.comparisonReport.rows.length" class="o_cohort_no_data text-center">
No data available.
</div>
</div>
<t t-if="!props.hasContent or (props.isSample and !props.isEmbedded)">
<t t-if="props.noContentHelp" t-call="web.ActionHelper">
<t t-set="noContentHelp" t-value="props.noContentHelp"/>
</t>
<t t-else="" t-call="web.NoContentHelper"/>
</t>
</div>
<t t-name="CohortView.tableTitle" owl="1">
<table class="table text-center mb0">
<thead>
<tr>
<th colspan="16">
<t t-esc="title" />
</th>
</tr>
</thead>
</table>
</t>
<t t-name="CohortView.tableContent" owl="1">
<table class="table text-center mb0">
<thead>
<tr>
<th rowspan="2"><t t-esc="props.dateStartString" /></th>
<th rowspan="2">
<t t-esc="props.measures[props.measure]"/>
</th>
<th colspan="16">
<t t-esc="props.dateStopString" /> - By <t t-esc="props.intervals[props.interval]" />
</th>
</tr>
<tr>
<th t-foreach="_range(col_length)" t-as="intervalNumber">
<t t-if="props.timeline === 'backward'">
<t t-esc="intervalNumber - (col_length - 1)"/>
</t>
<t t-else="">
+<t t-esc="intervalNumber"/>
</t>
</th>
</tr>
</thead>
<tbody>
<tr t-foreach="report_rows" t-as="row" t-key="row.date" data-type="data" t-att-data-row-index="row_index" class="o_cohort_row_clickable" t-on-click="_onClickRow">
<td class="o_cohort_value">
<t t-esc="row.date" />
</td>
<td class="o_cohort_value">
<t t-esc="_formatFloat(row.value)" />
</td>
<td t-foreach="row.columns" t-as="col" t-key="col.period">
<t t-if="col.percentage !== ''">
<t t-set="count" t-value="mode === 'churn' ? (col.churn_value === '-' ? '' : col.churn_value) : (col.value === '-' ? '' : col.value)"/>
<t t-set="measure" t-value="props.measures[props.measure]"/>
<div class="o_cohort_highlight"
t-attf-title="Periode: {{col.period}}&#10;{{measure}}: {{count}}"
t-attf-style="background-color: rgba(0, 160, 157, {{col.percentage/100.0}}); color: {{col.percentage gt 50 and '#FFFFFF' or 'inherit'}}"
t-att-class="{o_cohort_value: col.value !== '-'}">
<t t-esc="_formatPercentage(col.percentage / 100.0)"/>
</div>
</t>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
Average
</td>
<td>
<t t-esc="_formatFloat(report_avg.avg_value)"/>
</td>
<td t-foreach="report_avg.columns_avg" t-as="col">
<t t-if="report_avg.columns_avg[col]['count']">
<t t-esc="_formatPercentage(report_avg.columns_avg[col]['percentage'] / (report_avg.columns_avg[col]['count'] * 100.0))" />
</t>
</td>
</tr>
</tfoot>
</table>
</t>
</templates>

View File

@@ -0,0 +1,672 @@
odoo.define('web_cohort.cohort_tests', function (require) {
'use strict';
var CohortView = require('web_cohort.CohortView');
var testUtils = require('web.test_utils');
const cpHelpers = testUtils.controlPanel;
var createView = testUtils.createView;
var createActionManager = testUtils.createActionManager;
var patchDate = testUtils.mock.patchDate;
QUnit.module('Views', {
beforeEach: function () {
this.data = {
subscription: {
fields: {
id: {string: 'ID', type: 'integer'},
start: {string: 'Start', type: 'date', sortable: true},
stop: {string: 'Stop', type: 'date', sortable: true},
recurring: {string: 'Recurring Price', type: 'integer', store: true},
},
records: [
{id: 1, start: '2017-07-12', stop: '2017-08-11', recurring: 10},
{id: 2, start: '2017-08-14', stop: '', recurring: 20},
{id: 3, start: '2017-08-21', stop: '2017-08-29', recurring: 10},
{id: 4, start: '2017-08-21', stop: '', recurring: 20},
{id: 5, start: '2017-08-23', stop: '', recurring: 10},
{id: 6, start: '2017-08-24', stop: '', recurring: 22},
{id: 7, start: '2017-08-24', stop: '2017-08-29', recurring: 10},
{id: 8, start: '2017-08-24', stop: '', recurring: 22},
]
},
lead: {
fields: {
id: {string: 'ID', type: 'integer'},
start: {string: 'Start', type: 'date'},
stop: {string: 'Stop', type: 'date'},
revenue: {string: 'Revenue', type: 'float', store: true},
},
records: [
{id: 1, start: '2017-07-12', stop: '2017-08-11', revenue: 1200.20},
{id: 2, start: '2017-08-14', stop: '', revenue: 500},
{id: 3, start: '2017-08-21', stop: '2017-08-29', revenue: 5599.99},
{id: 4, start: '2017-08-21', stop: '', revenue: 13500},
{id: 5, start: '2017-08-23', stop: '', revenue: 6000},
{id: 6, start: '2017-08-24', stop: '', revenue: 1499.99},
{id: 7, start: '2017-08-24', stop: '2017-08-29', revenue: 16000},
{id: 8, start: '2017-08-24', stop: '', revenue: 22000},
]
},
attendee: {
fields: {
id: {string: 'ID', type: 'integer'},
event_begin_date: {string: 'Event Start Date', type: 'date'},
registration_date: {string: 'Registration Date', type: 'date'},
},
records: [
{id: 1, event_begin_date: '2018-06-30', registration_date: '2018-06-13'},
{id: 2, event_begin_date: '2018-06-30', registration_date: '2018-06-20'},
{id: 3, event_begin_date: '2018-06-30', registration_date: '2018-06-22'},
{id: 4, event_begin_date: '2018-06-30', registration_date: '2018-06-22'},
{id: 5, event_begin_date: '2018-06-30', registration_date: '2018-06-29'},
]
},
};
}
}, function () {
QUnit.module('CohortView');
QUnit.test('simple cohort rendering', async function (assert) {
assert.expect(7);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" />',
});
assert.containsOnce(cohort, '.table',
'should have a table');
assert.ok(cohort.$('.table thead tr:first th:first:contains(Start)').length,
'should contain "Start" in header of first column');
assert.ok(cohort.$('.table thead tr:first th:nth-child(3):contains(Stop - By Day)').length,
'should contain "Stop - By Day" in title');
assert.ok(cohort.$('.table thead tr:nth-child(2) th:first:contains(+0)').length,
'interval should start with 0');
assert.ok(cohort.$('.table thead tr:nth-child(2) th:nth-child(16):contains(+15)').length,
'interval should end with 15');
assert.strictEqual(cohort.$buttons.find('.o_cohort_measures_list').length, 1,
'should have list of measures');
assert.strictEqual(cohort.$buttons.find('.o_cohort_interval_button').length, 4,
'should have buttons of intervals');
cohort.destroy();
});
QUnit.test('no content helper', async function (assert) {
assert.expect(1);
this.data.subscription.records = [];
var cohort = await createView({
View: CohortView,
model: "subscription",
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" />',
});
assert.containsOnce(cohort, 'div.o_view_nocontent');
cohort.destroy();
});
QUnit.test('no content helper after update', async function (assert) {
assert.expect(2);
var cohort = await createView({
View: CohortView,
model: "subscription",
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" measure="recurring"/>',
});
assert.containsNone(cohort, 'div.o_view_nocontent');
await cohort.update({domain: [['recurring', '>', 25]]});
assert.containsOnce(cohort, 'div.o_view_nocontent');
cohort.destroy();
});
QUnit.test('correctly set by default measure and interval', async function (assert) {
assert.expect(4);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" />'
});
assert.hasClass(cohort.$buttons.find('.o_cohort_measures_list [data-field=__count__]'),'selected',
'count should by default for measure');
assert.hasClass(cohort.$buttons.find('.o_cohort_interval_button[data-interval=day]'),'active',
'day should by default for interval');
assert.ok(cohort.$('.table thead tr:first th:nth-child(2):contains(Count)').length,
'should contain "Count" in header of second column');
assert.ok(cohort.$('.table thead tr:first th:nth-child(3):contains(Stop - By Day)').length,
'should contain "Stop - By Day" in title');
cohort.destroy();
});
QUnit.test('correctly sort measure items', async function (assert) {
assert.expect(1);
var data = this.data;
// It's important to compare capitalized and lowercased words
// to be sure the sorting is effective with both of them
data.subscription.fields.flop = {string: 'Abc', type: 'integer', store: true};
data.subscription.fields.add = {string: 'add', type: 'integer', store: true};
data.subscription.fields.zoo = {string: 'Zoo', type: 'integer', store: true};
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop"/>',
});
const buttonsEls = cpHelpers.getButtons(cohort);
const measureButtonEls = buttonsEls[0].querySelectorAll('.o_cohort_measures_list > button');
assert.deepEqual(
[...measureButtonEls].map(e => e.innerText.trim()),
["Abc", "add", "Recurring Price", "Zoo", "Count"]
);
cohort.destroy();
});
QUnit.test('correctly set measure and interval after changed', async function (assert) {
assert.expect(8);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" measure="recurring" interval="week" />'
});
assert.hasClass(cohort.$buttons.find('.o_cohort_measures_list [data-field=recurring]'),'selected',
'should recurring for measure');
assert.hasClass(cohort.$buttons.find('.o_cohort_interval_button[data-interval=week]'),'active',
'should week for interval');
assert.ok(cohort.$('.table thead tr:first th:nth-child(2):contains(Recurring Price)').length,
'should contain "Recurring Price" in header of second column');
assert.ok(cohort.$('.table thead tr:first th:nth-child(3):contains(Stop - By Week)').length,
'should contain "Stop - By Week" in title');
await testUtils.dom.click(cohort.$buttons.find('.dropdown-toggle:contains(Measures)'));
await testUtils.dom.click(cohort.$buttons.find('.o_cohort_measures_list [data-field=__count__]'));
assert.hasClass(cohort.$buttons.find('.o_cohort_measures_list [data-field=__count__]'),'selected',
'should active count for measure');
assert.ok(cohort.$('.table thead tr:first th:nth-child(2):contains(Count)').length,
'should contain "Count" in header of second column');
await testUtils.dom.click(cohort.$buttons.find('.o_cohort_interval_button[data-interval=month]'));
assert.hasClass(cohort.$buttons.find('.o_cohort_interval_button[data-interval=month]'),'active',
'should active month for interval');
assert.ok(cohort.$('.table thead tr:first th:nth-child(3):contains(Stop - By Month)').length,
'should contain "Stop - By Month" in title');
cohort.destroy();
});
QUnit.test('cohort view without attribute invisible on field', async function (assert) {
assert.expect(3);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: `<cohort string="Subscription" date_start="start" date_stop="stop"/>`,
});
await testUtils.dom.click(cohort.$('.btn-group:first button'));
assert.containsN(cohort, '.o_cohort_measures_list button', 2);
assert.containsOnce(cohort, '.o_cohort_measures_list button[data-field="recurring"]');
assert.containsOnce(cohort, '.o_cohort_measures_list button[data-field="__count__"]');
cohort.destroy();
});
QUnit.test('cohort view with attribute invisible on field', async function (assert) {
assert.expect(2);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: `
<cohort string="Subscription" date_start="start" date_stop="stop">
<field name="recurring" invisible="1"/>
</cohort>`,
});
await testUtils.dom.click(cohort.$('.btn-group:first button'));
assert.containsOnce(cohort, '.o_cohort_measures_list button');
assert.containsNone(cohort, '.o_cohort_measures_list button[data-field="recurring"]');
cohort.destroy();
});
QUnit.test('export cohort', async function (assert) {
assert.expect(6);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" />',
session: {
get_file: async function (params) {
var data = JSON.parse(params.data.data);
assert.strictEqual(params.url, '/web/cohort/export');
assert.strictEqual(data.interval_string, 'Day');
assert.strictEqual(data.measure_string, 'Count');
assert.strictEqual(data.date_start_string, 'Start');
assert.strictEqual(data.date_stop_string, 'Stop');
assert.strictEqual(data.title, 'Subscription');
params.complete();
return true;
},
},
});
await testUtils.dom.click(cohort.$buttons.find('.o_cohort_download_button'));
cohort.destroy();
});
QUnit.test('when clicked on cell redirects to the correct list/form view ', async function(assert) {
assert.expect(6);
var actionManager = await createActionManager({
data: this.data,
archs: {
'subscription,false,cohort': '<cohort string="Subscriptions" date_start="start" date_stop="stop" measure="__count__" interval="week" />',
'subscription,my_list_view,list': '<tree>' +
'<field name="start"/>' +
'<field name="stop"/>' +
'</tree>',
'subscription,my_form_view,form': '<form>' +
'<field name="start"/>' +
'<field name="stop"/>' +
'</form>',
'subscription,false,list': '<tree>' +
'<field name="recurring"/>' +
'<field name="start"/>' +
'</tree>',
'subscription,false,form': '<form>' +
'<field name="recurring"/>' +
'<field name="start"/>' +
'</form>',
'subscription,false,search': '<search></search>',
},
intercepts: {
do_action: function (ev) {
actionManager.doAction(ev.data.action, ev.data.options);
},
},
});
await actionManager.doAction({
name: 'Subscriptions',
res_model: 'subscription',
type: 'ir.actions.act_window',
views: [[false, 'cohort'], ['my_list_view', 'list'], ['my_form_view', 'form']],
});
// Going to the list view, while clicking Period / Count cell
await testUtils.dom.click(actionManager.$('td.o_cohort_value:first'));
assert.strictEqual(actionManager.$('.o_list_view th:nth(1)').text(), 'Start',
"First field in the list view should be start");
assert.strictEqual(actionManager.$('.o_list_view th:nth(2)').text(), 'Stop',
"Second field in the list view should be stop");
// Going back to cohort view
await testUtils.dom.click(actionManager.$('.o_back_button'));
// Going to the list view
await testUtils.dom.click(actionManager.$('td div.o_cohort_value:first'));
assert.strictEqual(actionManager.$('.o_list_view th:nth(1)').text(), 'Start',
"First field in the list view should be start");
assert.strictEqual(actionManager.$('.o_list_view th:nth(2)').text(), 'Stop',
"Second field in the list view should be stop");
// Going to the form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row'));
assert.hasAttrValue(actionManager.$('.o_form_view span:first'), 'name', 'start',
"First field in the form view should be start");
assert.hasAttrValue(actionManager.$('.o_form_view span:nth(1)'), 'name', 'stop',
"Second field in the form view should be stop");
actionManager.destroy();
});
QUnit.test('test mode churn', async function(assert) {
assert.expect(3);
var cohort = await createView({
View: CohortView,
model: 'lead',
data: this.data,
arch: '<cohort string="Leads" date_start="start" date_stop="stop" interval="week" mode="churn" />',
mockRPC: function(route, args) {
assert.strictEqual(args.kwargs.mode, "churn", "churn mode should be sent via RPC");
return this._super(route, args);
},
});
assert.strictEqual(cohort.$('td .o_cohort_value:first').text().trim(), '0%', 'first col should display 0 percent');
assert.strictEqual(cohort.$('td .o_cohort_value:nth(4)').text().trim(), '100%', 'col 5 should display 100 percent');
cohort.destroy();
});
QUnit.test('test backward timeline', async function (assert) {
assert.expect(7);
var cohort = await createView({
View: CohortView,
model: 'attendee',
data: this.data,
arch: '<cohort string="Attendees" date_start="event_begin_date" date_stop="registration_date" interval="day" timeline="backward" mode="churn"/>',
mockRPC: function (route, args) {
assert.strictEqual(args.kwargs.timeline, "backward", "backward timeline should be sent via RPC");
return this._super(route, args);
},
});
assert.ok(cohort.$('.table thead tr:nth-child(2) th:first:contains(-15)').length,
'interval should start with -15');
assert.ok(cohort.$('.table thead tr:nth-child(2) th:nth-child(16):contains(0)').length,
'interval should end with 0');
assert.strictEqual(cohort.$('td .o_cohort_value:first').text().trim(), '20%', 'first col should display 20 percent');
assert.strictEqual(cohort.$('td .o_cohort_value:nth(5)').text().trim(), '40%', 'col 6 should display 40 percent');
assert.strictEqual(cohort.$('td .o_cohort_value:nth(7)').text().trim(), '80%', 'col 8 should display 80 percent');
assert.strictEqual(cohort.$('td .o_cohort_value:nth(14)').text().trim(), '100%', 'col 15 should display 100 percent');
cohort.destroy();
});
QUnit.test('when clicked on cell redirects to the action list/form view passed in context', async function(assert) {
assert.expect(6);
var actionManager = await createActionManager({
data: this.data,
archs: {
'subscription,false,cohort': '<cohort string="Subscriptions" date_start="start" date_stop="stop" measure="__count__" interval="week" />',
'subscription,my_list_view,list': '<tree>' +
'<field name="start"/>' +
'<field name="stop"/>' +
'</tree>',
'subscription,my_form_view,form': '<form>' +
'<field name="start"/>' +
'<field name="stop"/>' +
'</form>',
'subscription,false,list': '<tree>' +
'<field name="recurring"/>' +
'<field name="start"/>' +
'</tree>',
'subscription,false,form': '<form>' +
'<field name="recurring"/>' +
'<field name="start"/>' +
'</form>',
'subscription,false,search': '<search></search>',
},
intercepts: {
do_action: function (ev) {
actionManager.doAction(ev.data.action, ev.data.options);
},
},
});
await actionManager.doAction({
name: 'Subscriptions',
res_model: 'subscription',
type: 'ir.actions.act_window',
views: [[false, 'cohort']],
context: {list_view_id: 'my_list_view', form_view_id: 'my_form_view'},
});
// Going to the list view, while clicking Period / Count cell
await testUtils.dom.click(actionManager.$('td.o_cohort_value:first'));
assert.strictEqual(actionManager.$('.o_list_view th:nth(1)').text(), 'Start',
"First field in the list view should be start");
assert.strictEqual(actionManager.$('.o_list_view th:nth(2)').text(), 'Stop',
"Second field in the list view should be stop");
// Going back to cohort view
await testUtils.dom.click($('.o_back_button'));
// Going to the list view
await testUtils.dom.click(actionManager.$('td div.o_cohort_value:first'));
assert.strictEqual(actionManager.$('.o_list_view th:nth(1)').text(), 'Start',
"First field in the list view should be start");
assert.strictEqual(actionManager.$('.o_list_view th:nth(2)').text(), 'Stop',
"Second field in the list view should be stop");
// Going to the form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row'));
assert.hasAttrValue(actionManager.$('.o_form_view span:first'), 'name', 'start',
"First field in the form view should be start");
assert.hasAttrValue(actionManager.$('.o_form_view span:nth(1)'), 'name', 'stop',
"Second field in the form view should be stop");
actionManager.destroy();
});
QUnit.test('rendering of a cohort view with comparison', async function (assert) {
assert.expect(29);
var unpatchDate = patchDate(2017, 7, 25, 1, 0, 0);
var actionManager = await createActionManager({
data: this.data,
archs: {
'subscription,false,cohort': '<cohort string="Subscriptions" date_start="start" date_stop="stop" measure="__count__" interval="week" />',
'subscription,false,search': `
<search>
<filter date="start" name="date_filter" string="Date"/>
</search>
`,
},
intercepts: {
do_action: function (ev) {
actionManager.doAction(ev.data.action, ev.data.options);
},
},
});
await actionManager.doAction({
name: 'Subscriptions',
res_model: 'subscription',
type: 'ir.actions.act_window',
views: [[false, 'cohort']],
});
function verifyContents(results) {
var $tables = actionManager.$('table');
assert.strictEqual($tables.length, results.length, 'There should be ' + results.length + ' tables');
var result;
$tables.each(function () {
result = results.shift();
var $table = $(this);
var rowCount = $table.find('.o_cohort_row_clickable').length;
if (rowCount) {
assert.strictEqual(rowCount, result, 'the table should contain ' + result + ' rows');
} else {
assert.strictEqual($table.find('th:first').text().trim(), result,
'the table should contain the time range description' + result);
}
});
}
// with no comparison, with data (no filter)
verifyContents([3]);
assert.containsNone(actionManager, '.o_cohort_no_data');
assert.containsNone(actionManager, 'div.o_view_nocontent');
// with no comparison with no data (filter on 'last_year')
await cpHelpers.toggleFilterMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, 'Date');
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', '2016');
verifyContents([]);
assert.containsNone(actionManager, '.o_cohort_no_data');
assert.containsOnce(actionManager, 'div.o_view_nocontent');
// with comparison active, data and comparisonData (filter on 'this_month' + 'previous_period')
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', '2016');
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', 'August');
await cpHelpers.toggleComparisonMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, 'Date: Previous period');
verifyContents(['August 2017', 2, 'July 2017', 1]);
assert.containsNone(actionManager, '.o_cohort_no_data');
assert.containsNone(actionManager, 'div.o_view_nocontent');
// with comparison active, data, no comparisonData (filter on 'this_year' + 'previous_period')
await cpHelpers.toggleFilterMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, 'Date');
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', 'August');
verifyContents(['2017', 3, '2016']);
assert.containsOnce(actionManager, '.o_cohort_no_data');
assert.containsNone(actionManager, 'div.o_view_nocontent');
// with comparison active, no data, comparisonData (filter on 'Q4' + 'previous_period')
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', 'Q4');
verifyContents(['Q4 2017', 'Q3 2017', 3]);
assert.containsOnce(actionManager, '.o_cohort_no_data');
assert.containsNone(actionManager, 'div.o_view_nocontent');
// with comparison active, no data, no comparisonData (filter on 'last_year' + 'previous_period')
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', '2016');
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', '2017');
verifyContents([]);
assert.containsNone(actionManager, '.o_cohort_no_data');
assert.containsOnce(actionManager, 'div.o_view_nocontent');
unpatchDate();
actionManager.destroy();
});
QUnit.test('verify context', async function (assert) {
assert.expect(1);
var cohort = await createView({
View: CohortView,
model: 'subscription',
data: this.data,
arch: '<cohort string="Subscription" date_start="start" date_stop="stop" />',
mockRPC: function (route, args) {
if (args.method === 'get_cohort_data') {
assert.ok(args.kwargs.context);
}
return this._super.apply(this, arguments);
},
});
cohort.destroy();
});
QUnit.test('empty cohort view with action helper', async function (assert) {
assert.expect(4);
const cohort = await createView({
View: CohortView,
model: "subscription",
data: this.data,
arch: '<cohort date_start="start" date_stop="stop"/>',
domain: [['id', '<', 0]],
viewOptions: {
action: {
context: {},
help: '<p class="abc">click to add a foo</p>'
}
},
});
assert.containsOnce(cohort, '.o_view_nocontent .abc');
assert.containsNone(cohort, 'table');
await cohort.reload({ domain: [] });
assert.containsNone(cohort, '.o_view_nocontent .abc');
assert.containsOnce(cohort, 'table');
cohort.destroy();
});
QUnit.test('empty cohort view with sample data', async function (assert) {
assert.expect(7);
const cohort = await createView({
View: CohortView,
model: "subscription",
data: this.data,
arch: '<cohort sample="1" date_start="start" date_stop="stop"/>',
domain: [['id', '<', 0]],
viewOptions: {
action: {
context: {},
help: '<p class="abc">click to add a foo</p>'
}
},
});
assert.hasClass(cohort.el, 'o_view_sample_data');
assert.containsOnce(cohort, '.o_view_nocontent .abc');
assert.containsOnce(cohort, 'table.o_sample_data_disabled');
await cohort.reload({ domain: [] });
assert.doesNotHaveClass(cohort.el, 'o_view_sample_data');
assert.containsNone(cohort, '.o_view_nocontent .abc');
assert.containsOnce(cohort, 'table');
assert.doesNotHaveClass(cohort.$('table'), 'o_sample_data_disabled');
cohort.destroy();
});
QUnit.test('non empty cohort view with sample data', async function (assert) {
assert.expect(7);
const cohort = await createView({
View: CohortView,
model: "subscription",
data: this.data,
arch: '<cohort sample="1" date_start="start" date_stop="stop"/>',
viewOptions: {
action: {
context: {},
help: '<p class="abc">click to add a foo</p>'
}
},
});
assert.doesNotHaveClass(cohort.el, 'o_view_sample_data');
assert.containsNone(cohort, '.o_view_nocontent .abc');
assert.containsOnce(cohort, 'table');
assert.doesNotHaveClass(cohort.$('table'), 'o_sample_data_disabled');
await cohort.reload({ domain: [['id', '<', 0]] });
assert.doesNotHaveClass(cohort.el, 'o_view_sample_data');
assert.containsOnce(cohort, '.o_view_nocontent .abc');
assert.containsNone(cohort, 'table');
cohort.destroy();
});
});
});

View File

@@ -0,0 +1,171 @@
odoo.define('web_cohort.MockServer', function (require) {
'use strict';
var MockServer = require('web.MockServer');
MockServer.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
* @returns {Promise}
*/
_performRpc: function (route, args) {
if (args.method === 'get_cohort_data') {
return this._mockGetCohortData(args.model, args.kwargs);
} else {
return this._super(route, args);
}
},
/**
* @private
* @param {string} model
* @param {Object} kwargs
* @returns {Promise}
*/
_mockGetCohortData: function (model, kwargs) {
var self = this;
var displayFormats = {
'day': 'DD MMM YYYY',
'week': 'ww YYYY',
'month': 'MMMM YYYY',
'year': 'Y',
};
var rows = [];
var totalValue = 0;
var initialChurnValue = 0;
var columnsAvg = {};
var groups = this._mockReadGroup(model, {
domain: kwargs.domain,
fields: [kwargs.date_start],
groupby: [kwargs.date_start + ':' + kwargs.interval],
});
var totalCount = groups.length;
_.each(groups, function (group) {
var format;
switch (kwargs.interval) {
case 'day':
format = 'YYYY-MM-DD';
break;
case 'week':
format = 'ww YYYY';
break;
case 'month':
format = 'MMMM YYYY';
break;
case 'year':
format = 'Y';
break;
}
var cohortStartDate = moment(group[kwargs.date_start + ':' + kwargs.interval], format);
var records = self._mockSearchReadController({
model: model,
domain: group.__domain,
});
var value = 0;
if (kwargs.measure === '__count__') {
value = records.length;
} else {
if (records.length) {
value = _.pluck(records.records, kwargs.measure).reduce(function (a, b) {
return a + b;
});
}
}
totalValue += value;
var initialValue = value;
var columns = [];
var colStartDate = cohortStartDate.clone();
if (kwargs.timeline === 'backward') {
colStartDate = colStartDate.subtract(15, kwargs.interval);
}
for (var column = 0; column <= 15; column++) {
if (!columnsAvg[column]) {
columnsAvg[column] = {'percentage': 0, 'count': 0};
}
if (column !== 0) {
colStartDate.add(1, kwargs.interval);
}
if (colStartDate > moment()) {
columnsAvg[column]['percentage'] += 0;
columnsAvg[column]['count'] += 0;
columns.push({
'value': '-',
'churn_value': '-',
'percentage': '',
});
continue;
}
var compareDate = colStartDate.format(displayFormats[kwargs.interval]);
var colRecords = _.filter(records.records, function (record) {
return record[kwargs.date_stop] && moment(record[kwargs.date_stop], 'YYYY-MM-DD').format(displayFormats[kwargs.interval]) == compareDate;
});
var colValue = 0;
if (kwargs.measure === '__count__') {
colValue = colRecords.length;
} else {
if (colRecords.length) {
colValue = _.pluck(colRecords, kwargs.measure).reduce(function (a, b) {
return a + b;
});
}
}
if (kwargs.timeline === 'backward' && column === 0) {
colRecords = _.filter(records.records, function (record) {
return record[kwargs.date_stop] && moment(record[kwargs.date_stop], 'YYYY-MM-DD') >= colStartDate;
});
if (kwargs.measure === '__count__') {
initialValue = colRecords.length;
} else {
if (colRecords.length) {
initialValue = _.pluck(colRecords, kwargs.measure).reduce(function (a, b) {
return a + b;
});
}
}
initialChurnValue = value - initialValue;
}
var previousValue = column === 0 ? initialValue : columns[column - 1]['value'];
var remainingValue = previousValue - colValue;
var previousChurnValue = column === 0 ? initialChurnValue : columns[column - 1]['churn_value'];
var churnValue = colValue + previousChurnValue;
var percentage = value ? parseFloat(remainingValue / value) : 0;
if (kwargs.mode === 'churn') {
percentage = 1 - percentage;
}
percentage = Number((100 * percentage).toFixed(1));
columnsAvg[column]['percentage'] += percentage;
columnsAvg[column]['count'] += 1;
columns.push({
'value': remainingValue,
'churn_value': churnValue,
'percentage': percentage,
'domain': [],
'period': compareDate,
});
}
rows.push({
'date': cohortStartDate.format(displayFormats[kwargs.interval]),
'value': value,
'domain': group.__domain,
'columns': columns,
});
});
return Promise.resolve({
'rows': rows,
'avg': {'avg_value': totalCount ? (totalValue / totalCount) : 0, 'columns_avg': columnsAvg},
});
},
});
});