Files
odoo_source/addons/crm/static/tests/forecast_kanban_tests.js
T
abd-msyukyu-odoo 3df9f5d689 [IMP] crm: add a forecast feature (kanban, pivot, graph, list)
* Currently located in CRM as it is its only use. After Owl-ification of Web
  Client it will probably be added as a core feature.

* Objectives:

  - Kanban view:
    - Kanban groups on a specific date/time field should be contiguous and show
      a short term vision. Empty groups should be shown
      -> FillTemporalService
      -> ForecastKanbanModel
      -> context.forecast_field

    - Add the next period (i.e. month) with one click in the kanban view
      -> ForecastKanbanRenderer
      -> ForecastKanbanController
      -> ForecastColumnQuickCreate

    - Store specific time ranges for each granularity (year, month, ...), so
      that when the user switches back to one his progress will be recovered.
      (reset on page refresh)
      -> FillTemporalService

  - Graph, Kanban, List, Pivot views:
    - Filter out data from the past (previous periods), while keeping every
      records from the current period even if we are in the middle of it
      -> ForecastModelExtension
      -> context.forecast_filter
      -> context.forecast_field

* context key `forecast_field`:

  - Used to set the date/time field name, on which the forecast logic should be
    applied

  - This key will be used by :
      a js model (for a view) / ForecastModelExtension / FillTemporalService

* ForecastModelExtension and context key `forecast_filter`:

  This extension will use a filter, the groupBy value, and the
  context.forecast_field to apply a custom domain constraint.

  This domain constraint can also be applied by default with the
  FillTemporalService, but it was decided to extract it to a filter for the user
  to be able to disable it, to see "old forgotten records" and update them if
  necessary. In other words, this filter enables or disables the first bound of
  the domain for the read group.

  - Filter "Forecast":
    This filter should set the context key `forecast_filter:1`, in order
    to only get future records starting from the start of the period
    containing "now".
    example:
      now:      2021-04-26
      groupBy:  month
      => all records starting from 2021-04-01 match the filter

  - If none of the groupBy values are the forecast_field, filters the records
    on the forecast_field starting from "now"

* FillTemporalService:

  This service will be used to generate or recover `FillTemporalPeriod`.
  Depending on the configuration, it will return an instance handling a
  certain `forecast_field`, with a certain `granularity` (hour, day, week,
  month, quarter, year), for a certain `modelName`. This can be used in
  multiple views to always get the same object, if it concerns the same model,
  the same field and the same granularity.

  The generated `FillTemporalPeriod` object is used to compute the final domain
  and context to be provided to the backend `read_group`.

  The objective was to limit the amount of desired groups regarding a specific
  time period (cycle). Here are the cycles depending on the granularity:
    granularity   -   cycle
    -------------------------------
    hour          -   one day
    day           -   one week
    week          -   one week
    month         -   one year
    quarter       -   one year
    year          -   one year

  In order to get a minimal amount of groups after a read_group, we can provide
  an integer in the configuration : `min_groups`, which will guarantee this
  amount of groups, regardless of the rest of the configuration

  The default maximum amount of groups returned depends on the end of the
  specific cycle reached after guaranteeing `min_groups`

  We can always alter the result by directly modifying the `start` and `end`
  properties of the `FillTemporalPeriod`, using the dedicated methods to do so.

  Configuration of the domain with the keys `force[Start|End]Bound`
  - true:         applies a lower|upper bound for the domain, which means that
                  the subsequent read_group will search for groups [from the
                  start|until the end] of the FillTemporalPeriod
  - false:        no domain constraint applied, which means that at least every
                  groups with records in the database will be returned

  Configuration of the context with the keys `forceFilling[From|To]`
  - true:         set fill_temporal.fill_[from|to], which means that at least
                  every groups [from the start|until the end] of the
                  `FillTemporalPeriod` will be returned as contiguous groups
                  by a read_group
  - false:        does not set fill_temporal.fill_[from|to], which means that
                  read_group will only return groups from|until the first|last
                  one with at least one record

  The `expand` method can be used to add one interval to the end of the
  `FillTemporalPeriod` depending on the granularity (add one group)

* any "forecast" view :
  - should define a `forecast_field` in the context
  - should use the `forecast_model_extension` with the searchModel to be able to
    filter "future" records correctly
  - could make use of the `FillTemporalService` to maintain a consistent domain
    and context when grouping by the desired date field. The domain and
    context are to be applied on __load and __reload calls in the MODEL, on the
    provided domain and context, to further filter the groups returned by a
    read_group.

* forecast views: graph, kanban, list, pivot

  * kanban
    uses: forecast_model_extension, fill_temporal_service

    has a custom `column_quick_create` limited to the forecast_field which
    allows to add the next group (period depends on granularity)

    works with the sample_server even thought the domain and the context are
    modified during the __load/__reload processes.

  * graph, pivot, list
    use: forecast_model_extension

Task-ID: 2243913
PR odoo/odoo#69380
2021-06-30 09:51:42 +00:00

259 lines
12 KiB
JavaScript

/** @odoo-module */
import FillTemporalService from '../src/js/forecast/fill_temporal_service';
import { ForecastKanbanView } from '../src/js/forecast/forecast_views';
import testUtils from 'web.test_utils';
QUnit.module('Crm Forecast Model Extension', {
beforeEach: async function () {
this.testKanbanView = {
arch: `
<kanban js_class="forecast_kanban">
<field name="date_deadline"/>
<field name="date_closed"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
archs: {
"crm.lead,false,search": `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
<filter name='groupby_date_closed' context="{'group_by':'date_closed'}"/>
</search>`
},
data: {
'crm.lead': {
fields: {
name: {string: 'Name', type: 'char'},
date_deadline: {string: "Expected closing", type: 'date'},
date_closed: {string: "Closed Date", type: 'datetime'},
},
records: [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01', date_closed: '2021-01-01 00:00:00'},
{id: 2, name: 'Lead 2', date_deadline: '2021-01-20', date_closed: '2021-01-20 00:00:00'},
{id: 3, name: 'Lead 3', date_deadline: '2021-02-01', date_closed: '2021-02-01 00:00:00'},
{id: 4, name: 'Lead 4', date_deadline: '2021-02-20', date_closed: '2021-02-20 00:00:00'},
{id: 5, name: 'Lead 5', date_deadline: '2021-03-01', date_closed: '2021-03-01 00:00:00'},
{id: 6, name: 'Lead 6', date_deadline: '2021-03-20', date_closed: '2021-03-20 00:00:00'},
],
},
},
services: { 'fillTemporalService': FillTemporalService },
model: 'crm.lead',
View: ForecastKanbanView,
viewOptions: {
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: 'date_deadline',
},
},
groupBy: ['date_deadline'],
};
this.unPatchDate = testUtils.mock.patchDate(2021, 1, 10, 0, 0, 0);
},
afterEach: async function () {
this.unPatchDate();
},
}, function () {
QUnit.test("filter out every records before the start of the current month with forecast_filter for a date field", async function (assert) {
// the filter is used by the forecast model extension, and applies the forecast_filter context key,
// which adds a domain constraint on the field marked in the other context key forecast_field
assert.expect(7);
const kanban = await testUtils.createView(this.testKanbanView);
// the filter is active
assert.containsN(kanban, '.o_kanban_group', 2, "There should be 2 columns");
assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column February should contain 2 record");
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column March should contain 2 records");
// remove the filter
await testUtils.dom.click($('.o_searchview_facet:contains("Forecast") .o_facet_remove'));
assert.containsN(kanban, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column January should contain 2 record");
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column February should contain 2 records");
assert.containsN(kanban, '.o_kanban_group:nth-child(3) .o_kanban_record', 2,
"3nd column March should contain 2 records");
kanban.destroy();
});
QUnit.test("filter out every records before the start of the current month with forecast_filter for a datetime field", async function (assert) {
// same tests as for the date field
assert.expect(7);
this.testKanbanView.viewOptions.context = {
search_default_forecast: true,
search_default_groupby_date_closed: true,
forecast_field: 'date_closed',
};
this.testKanbanView.groupBy = ['date_closed'];
const kanban = await testUtils.createView(this.testKanbanView);
// with the filter
assert.containsN(kanban, '.o_kanban_group', 2, "There should be 2 columns");
assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column February should contain 2 record");
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column March should contain 2 records");
// remove the filter
await testUtils.dom.click($('.o_searchview_facet:contains("Forecast") .o_facet_remove'));
assert.containsN(kanban, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column January should contain 2 record");
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column February should contain 2 records");
assert.containsN(kanban, '.o_kanban_group:nth-child(3) .o_kanban_record', 2,
"3nd column March should contain 2 records");
kanban.destroy();
});
});
QUnit.module('Crm Fill Temporal Service', {
/**
* Remark: -> the filter with the groupBy is needed for the model_extension to access the groupby
* when created with testUtils.createView. Not needed in production.
* -> testKanbanView.groupBy is still needed to apply the groupby on the view
*/
beforeEach: async function () {
this.testKanbanView = {
arch: `
<kanban js_class="forecast_kanban">
<field name="date_deadline"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
archs: {
"crm.lead,false,search": `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
</search>`
},
data: {
'crm.lead': {
fields: {
name: {string: 'Name', type: 'char'},
date_deadline: {string: "Expected Closing", type: 'date'},
},
},
},
services: { 'fillTemporalService': FillTemporalService },
model: 'crm.lead',
View: ForecastKanbanView,
viewOptions: {
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: 'date_deadline',
},
},
groupBy: ['date_deadline'],
};
this.unPatchDate = testUtils.mock.patchDate(2021, 9, 10, 0, 0, 0);
},
afterEach: async function () {
this.unPatchDate();
},
}, function () {
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
QUnit.test("Forecast on months, until the end of the year of the latest data", async function (assert) {
assert.expect(3);
this.testKanbanView.data['crm.lead'].records = [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01'},
{id: 2, name: 'Lead 2', date_deadline: '2021-02-01'},
{id: 3, name: 'Lead 3', date_deadline: '2021-11-01'},
{id: 4, name: 'Lead 4', date_deadline: '2022-01-01'},
];
this.testKanbanView.mockRPC = function (route, args) {
if (route === '/web/dataset/call_kw/crm.lead/web_read_group') {
assert.deepEqual(args.kwargs.context.fill_temporal, {
fill_from: "2021-10-01",
min_groups: 4,
});
assert.deepEqual(args.kwargs.domain, [
"&", "|",
["date_deadline", "=", false], ["date_deadline", ">=", "2021-10-01"],
"|",
["date_deadline", "=", false], ["date_deadline", "<", "2023-01-01"],
]);
}
return this._super.apply(this, arguments);
};
const kanban = await testUtils.createView(this.testKanbanView);
assert.strictEqual(kanban.call('fillTemporalService', 'getFillTemporalPeriod', {
modelName: 'crm.lead',
field: {
name: 'date_deadline',
type: 'date',
},
granularity: 'month',
}).end.format('YYYY-MM-DD'), '2022-02-01');
kanban.destroy();
});
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
QUnit.test("Forecast on years, until the end of the year of the latest data", async function (assert) {
assert.expect(3);
this.testKanbanView.data['crm.lead'].records = [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01'},
{id: 2, name: 'Lead 2', date_deadline: '2022-02-01'},
{id: 3, name: 'Lead 3', date_deadline: '2027-11-01'},
];
this.testKanbanView.groupBy = ['date_deadline:year'];
this.testKanbanView.archs["crm.lead,false,search"] =
this.testKanbanView.archs["crm.lead,false,search"].replace("'date_deadline'", "'date_deadline:year'");
this.testKanbanView.mockRPC = function (route, args) {
if (route === '/web/dataset/call_kw/crm.lead/web_read_group') {
assert.deepEqual(args.kwargs.context.fill_temporal, {
fill_from: "2021-01-01",
min_groups: 4,
});
assert.deepEqual(args.kwargs.domain, [
"&", "|",
["date_deadline", "=", false], ["date_deadline", ">=", "2021-01-01"],
"|",
["date_deadline", "=", false], ["date_deadline", "<", "2025-01-01"],
]);
}
return this._super.apply(this, arguments);
};
const kanban = await testUtils.createView(this.testKanbanView);
assert.strictEqual(kanban.call('fillTemporalService', 'getFillTemporalPeriod', {
modelName: 'crm.lead',
field: {
name: 'date_deadline',
type: 'date',
},
granularity: 'year',
}).end.format('YYYY-MM-DD'), '2023-01-01');
kanban.destroy();
});
});