[IMP] crm: stop using moment
Replace moment calls by their equivalent with the luxon library in CRM, and make use of date utils from the l10n module. Task-3056665 closes odoo/odoo#109882 Related: odoo/enterprise#35777 Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
|
||||
/**
|
||||
* Configuration depending on the granularity:
|
||||
* @param {function} startOf function to get the start moment of the period from a moment
|
||||
* Configuration depending on the granularity, using Luxon DateTime objects:
|
||||
* @param {function} startOf function to get a DateTime at the beginning of a period
|
||||
* from another DateTime.
|
||||
* @param {int} cycle amount of 'granularity' periods constituting a cycle. The cycle duration
|
||||
* is arbitrary for each granularity:
|
||||
* cycle --- granularity
|
||||
@@ -15,35 +20,35 @@ import { registry } from "@web/core/registry";
|
||||
* 1 year month
|
||||
* 1 year quarter
|
||||
* 1 year year # we are not using a greater time period in Odoo (yet)
|
||||
* @param {int} cyclePos function to get the position (index) in the cycle from a moment.
|
||||
* {1} is the first index. {+1} is used for functions which have an index
|
||||
* @param {int} cyclePos function to get the position (index) in the cycle from a DateTime.
|
||||
* {1} is the first index. {+1} is used for properties which have an index
|
||||
* starting from 0, to standardize between granularities.
|
||||
*/
|
||||
export const GRANULARITY_TABLE = {
|
||||
hour: {
|
||||
startOf: (x) => x.startOf("hour"),
|
||||
cycle: 24,
|
||||
cyclePos: (x) => x.hour() + 1,
|
||||
cyclePos: (x) => x.hour + 1,
|
||||
},
|
||||
day: {
|
||||
startOf: (x) => x.startOf("day"),
|
||||
cycle: 7,
|
||||
cyclePos: (x) => x.isoWeekday(),
|
||||
cyclePos: (x) => x.weekday,
|
||||
},
|
||||
week: {
|
||||
startOf: (x) => x.startOf("isoWeek"),
|
||||
startOf: (x) => x.startOf("week"),
|
||||
cycle: 1,
|
||||
cyclePos: (x) => 1,
|
||||
},
|
||||
month: {
|
||||
startOf: (x) => x.startOf("month"),
|
||||
cycle: 12,
|
||||
cyclePos: (x) => x.month() + 1,
|
||||
cyclePos: (x) => x.month,
|
||||
},
|
||||
quarter: {
|
||||
startOf: (x) => x.startOf("quarter"),
|
||||
cycle: 4,
|
||||
cyclePos: (x) => x.quarter(),
|
||||
cyclePos: (x) => x.quarter,
|
||||
},
|
||||
year: {
|
||||
startOf: (x) => x.startOf("year"),
|
||||
@@ -52,23 +57,6 @@ export const GRANULARITY_TABLE = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* configuration depending on the time type:
|
||||
* @param {string} format moment format to display this type as a string
|
||||
* @param {string} minGranularity granularity of the smallest time interval used in Odoo for this
|
||||
* type
|
||||
*/
|
||||
export const FIELD_TYPE_TABLE = {
|
||||
date: {
|
||||
format: "YYYY-MM-DD",
|
||||
minGranularity: "day",
|
||||
},
|
||||
datetime: {
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
minGranularity: "second",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* fill_temporal period:
|
||||
* Represents a specific date/time range for a specific model, field and granularity.
|
||||
@@ -102,15 +90,19 @@ export class FillTemporalPeriod {
|
||||
this._computeEnd();
|
||||
}
|
||||
/**
|
||||
* Compute the moment for the start of the period containing "now"
|
||||
* Compute this.start: the DateTime for the start of the period containing
|
||||
* the current time ("now").
|
||||
* i.e. 2020-10-01 13:43:17 -> the current "hour" DateTime started at:
|
||||
* 2020-10-01 13:00:00
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_computeStart() {
|
||||
this.start = GRANULARITY_TABLE[this.granularity].startOf(moment());
|
||||
this.start = GRANULARITY_TABLE[this.granularity].startOf(luxon.DateTime.now());
|
||||
}
|
||||
/**
|
||||
* Compute the moment for the end of the fill_temporal period. This bound is exclusive.
|
||||
* Compute this.end: the DateTime for the end of the fill_temporal period.
|
||||
* This bound is exclusive.
|
||||
* The fill_temporal period is the number of [granularity] from [start] to the end of the
|
||||
* [cycle] reached after adding [minGroups]
|
||||
* i.e. we are in october 2020 :
|
||||
@@ -125,51 +117,49 @@ export class FillTemporalPeriod {
|
||||
_computeEnd() {
|
||||
const cycle = GRANULARITY_TABLE[this.granularity].cycle;
|
||||
const cyclePos = GRANULARITY_TABLE[this.granularity].cyclePos(this.start);
|
||||
/**
|
||||
* fillTemporalPeriod formula explanation :
|
||||
* We want to know how many steps need to be taken from the current position until the end
|
||||
* of the cycle reached after guaranteeing minGroups positions. Let's call this cycle (C).
|
||||
*
|
||||
* (1) compute the steps needed to reach the last position of the current cycle, from the
|
||||
* current position:
|
||||
* {cycle - cyclePos}
|
||||
*
|
||||
* (2) ignore {minGroups - 1} steps from the position reached in (1). Now, the current
|
||||
* position is somewhere in (C). One step from minGroups is reserved to reach the first
|
||||
* position after (C), hence {-1}
|
||||
*
|
||||
* (3) compute the additional steps needed to reach the last position of (C), from the
|
||||
* position reached in (2):
|
||||
* {cycle - (minGroups - 1) % cycle}
|
||||
*
|
||||
* (4) combine (1) and (3), the sum should not be greater than a full cycle (-> truncate):
|
||||
* {(2 * cycle - (minGroups - 1) % cycle - cyclePos) % cycle}
|
||||
*
|
||||
* (5) add minGroups!
|
||||
*/
|
||||
// fillTemporalPeriod formula explanation :
|
||||
// We want to know how many steps need to be taken from the current position until the end
|
||||
// of the cycle reached after guaranteeing minGroups positions. Let's call this cycle (C).
|
||||
//
|
||||
// (1) compute the steps needed to reach the last position of the current cycle, from the
|
||||
// current position:
|
||||
// {cycle - cyclePos}
|
||||
//
|
||||
// (2) ignore {minGroups - 1} steps from the position reached in (1). Now, the current
|
||||
// position is somewhere in (C). One step from minGroups is reserved to reach the first
|
||||
// position after (C), hence {-1}
|
||||
//
|
||||
// (3) compute the additional steps needed to reach the last position of (C), from the
|
||||
// position reached in (2):
|
||||
// {cycle - (minGroups - 1) % cycle}
|
||||
//
|
||||
// (4) combine (1) and (3), the sum should not be greater than a full cycle (-> truncate):
|
||||
// {(2 * cycle - (minGroups - 1) % cycle - cyclePos) % cycle}
|
||||
//
|
||||
// (5) add minGroups!
|
||||
const fillTemporalPeriod = ((2 * cycle - ((this.minGroups - 1) % cycle) - cyclePos) % cycle) + this.minGroups;
|
||||
this.end = moment(this.start).add(fillTemporalPeriod, `${this.granularity}s`);
|
||||
this.end = this.start.plus({[`${this.granularity}s`]: fillTemporalPeriod});
|
||||
this.computedEnd = true;
|
||||
}
|
||||
/**
|
||||
* The server needs a date/time in UTC, but we don't want a day shift in case
|
||||
* of dates, even if the date is not in UTC
|
||||
*
|
||||
* @param {moment} bound the moment to be formatted (this.start or this.end)
|
||||
* @param {DateTime} bound the DateTime to be formatted (this.start or this.end)
|
||||
*/
|
||||
_getFormattedServerDate(bound) {
|
||||
if (bound.isUTC() || this.field.type === "date") {
|
||||
return bound.format(FIELD_TYPE_TABLE[this.field.type].format);
|
||||
if (this.field.type === "date") {
|
||||
return serializeDate(bound);
|
||||
} else {
|
||||
return moment.utc(bound).format(FIELD_TYPE_TABLE[this.field.type].format);
|
||||
return serializeDateTime(bound);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Object} configuration
|
||||
* @param {Array[]} [domain]
|
||||
* @param {boolean} [forceStartBound=true] whether this.start moment must be used as a domain
|
||||
* @param {boolean} [forceStartBound=true] whether this.start DateTime must be used as a domain
|
||||
* constraint to limit read_group results or not
|
||||
* @param {boolean} [forceEndBound=true] whether this.end moment must be used as a domain
|
||||
* @param {boolean} [forceEndBound=true] whether this.end DateTime must be used as a domain
|
||||
* constraint to limit read_group results or not
|
||||
* @returns {Array[]} new domain
|
||||
*/
|
||||
@@ -210,9 +200,9 @@ export class FillTemporalPeriod {
|
||||
fillTemporal.fill_from = this._getFormattedServerDate(this.start);
|
||||
}
|
||||
if (forceFillingTo) {
|
||||
fillTemporal.fill_to = this._getFormattedServerDate(
|
||||
moment(this.end).subtract(1, FIELD_TYPE_TABLE[this.field.type].minGranularity)
|
||||
);
|
||||
// smallest time interval used in Odoo for the current date type
|
||||
const minGranularity = this.field.type === "date" ? "days" : "seconds";
|
||||
fillTemporal.fill_to = this._getFormattedServerDate(this.end.minus({[minGranularity]: 1}));
|
||||
}
|
||||
context = { ...context, fill_temporal: fillTemporal };
|
||||
return context;
|
||||
@@ -225,29 +215,29 @@ export class FillTemporalPeriod {
|
||||
this.minGroups = minGroups || 1;
|
||||
}
|
||||
/**
|
||||
* sets the end of the period to the desired moment. It must be greater
|
||||
* sets the end of the period to the desired DateTime. It must be greater
|
||||
* than start. Changes the default behavior of getContext forceFillingTo
|
||||
* (becomes true instead of false)
|
||||
*
|
||||
* @param {moment} end
|
||||
* @param {DateTime} end
|
||||
*/
|
||||
setEnd(end) {
|
||||
this.end = moment.max(this.start, end);
|
||||
this.end = luxon.DateTime.max(this.start, end);
|
||||
this.computedEnd = false;
|
||||
}
|
||||
/**
|
||||
* sets the start of the period to the desired moment. It must be smaller than end
|
||||
* sets the start of the period to the desired DateTime. It must be smaller than end
|
||||
*
|
||||
* @param {moment} start
|
||||
* @param {DateTime} start
|
||||
*/
|
||||
setStart(start) {
|
||||
this.start = moment.min(this.end, start);
|
||||
this.start = luxon.DateTime.min(this.end, start);
|
||||
}
|
||||
/**
|
||||
* Adds one "granularity" period to [this.end], to expand the current fill_temporal period
|
||||
*/
|
||||
expand() {
|
||||
this.setEnd(this.end.add(1, `${this.granularity}s`));
|
||||
this.setEnd(this.end.plus({[`${this.granularity}s`]: 1}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CrmKanbanModel, CrmKanbanGroup } from "@crm/views/crm_kanban/crm_kanban_model";
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
|
||||
export class ForecastKanbanModel extends CrmKanbanModel {
|
||||
setup(params, { fillTemporalService }) {
|
||||
@@ -75,7 +76,7 @@ export class ForecastKanbanDynamicGroupList extends ForecastKanbanModel.DynamicG
|
||||
const result = await super.load(...arguments);
|
||||
const lastGroup = this.groups.filter((grp) => grp.value).slice(-1)[0];
|
||||
if (lastGroup) {
|
||||
this.fillTemporalPeriod.setEnd(moment.utc(lastGroup.range[this.groupBy[0]].to));
|
||||
this.fillTemporalPeriod.setEnd(deserializeDateTime(lastGroup.range[this.groupBy[0]].to));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { makeContext } from "@web/core/context";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
import {
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
|
||||
/**
|
||||
* This is the conversion of ForecastModelExtension. See there for more
|
||||
* explanations of what is done here.
|
||||
*/
|
||||
const DATE_FORMAT = {
|
||||
datetime: "YYYY-MM-DD HH:mm:ss",
|
||||
date: "YYYY-MM-DD",
|
||||
};
|
||||
|
||||
export class ForecastSearchModel extends SearchModel {
|
||||
/**
|
||||
@@ -65,9 +65,7 @@ export class ForecastSearchModel extends SearchModel {
|
||||
*/
|
||||
_getForecastStart(forecastField) {
|
||||
if (!this.forecastStart) {
|
||||
/** @todo stop using moment */
|
||||
const { type } = this.searchViewFields[forecastField];
|
||||
let startMoment;
|
||||
const groupBy = this.groupBy;
|
||||
const firstForecastGroupBy = groupBy.find((gb) => gb.includes(forecastField));
|
||||
let granularity = "month";
|
||||
@@ -76,12 +74,8 @@ export class ForecastSearchModel extends SearchModel {
|
||||
} else if (groupBy.length) {
|
||||
granularity = "day";
|
||||
}
|
||||
startMoment = moment().startOf(granularity);
|
||||
if (type === "datetime") {
|
||||
startMoment = moment.utc(startMoment);
|
||||
}
|
||||
const format = DATE_FORMAT[type];
|
||||
this.forecastStart = startMoment.format(format);
|
||||
const startDateTime = luxon.DateTime.now().startOf(granularity);
|
||||
this.forecastStart = type === "datetime" ? serializeDateTime(startDateTime) : serializeDate(startDateTime);
|
||||
}
|
||||
return this.forecastStart;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getFixture,
|
||||
} from '@web/../tests/helpers/utils';
|
||||
import testUtils from 'web.test_utils';
|
||||
import { serializeDateTime } from "@web/core/l10n/dates";
|
||||
const find = testUtils.dom.find;
|
||||
|
||||
let target;
|
||||
@@ -25,7 +26,7 @@ function getMockRpc(assert) {
|
||||
|
||||
QUnit.module('Crm Rainbowman Triggers', {
|
||||
beforeEach: function () {
|
||||
const format = "YYYY-MM-DD HH:mm:ss";
|
||||
const now = luxon.DateTime.now();
|
||||
const serverData = {
|
||||
models: {
|
||||
'res.users': {
|
||||
@@ -72,15 +73,15 @@ QUnit.module('Crm Rainbowman Triggers', {
|
||||
records : [
|
||||
{ id: 1, name: 'Lead 1', planned_revenue: 5.0, stage_id: 1, team_id: 1, user_id: 1 },
|
||||
{ id: 2, name: 'Lead 2', planned_revenue: 5.0, stage_id: 2, team_id: 2, user_id: 4 },
|
||||
{ id: 3, name: 'Lead 3', planned_revenue: 3.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(5, 'days').format(format) },
|
||||
{ id: 4, name: 'Lead 4', planned_revenue: 4.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(23, 'days').format(format) },
|
||||
{ id: 5, name: 'Lead 5', planned_revenue: 7.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(20, 'days').format(format) },
|
||||
{ id: 3, name: 'Lead 3', planned_revenue: 3.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: serializeDateTime(now.minus({days: 5})) },
|
||||
{ id: 4, name: 'Lead 4', planned_revenue: 4.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: serializeDateTime(now.minus({days: 23})) },
|
||||
{ id: 5, name: 'Lead 5', planned_revenue: 7.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: serializeDateTime(now.minus({days: 20})) },
|
||||
{ id: 6, name: 'Lead 6', planned_revenue: 4.0, stage_id: 2, team_id: 1, user_id: 2 },
|
||||
{ id: 7, name: 'Lead 7', planned_revenue: 1.8, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(23, 'days').format(format) },
|
||||
{ id: 7, name: 'Lead 7', planned_revenue: 1.8, stage_id: 3, team_id: 2, user_id: 3, date_closed: serializeDateTime(now.minus({days: 23})) },
|
||||
{ id: 8, name: 'Lead 8', planned_revenue: 1.9, stage_id: 1, team_id: 2, user_id: 3 },
|
||||
{ id: 9, name: 'Lead 9', planned_revenue: 1.5, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(5, 'days').format(format) },
|
||||
{ id: 9, name: 'Lead 9', planned_revenue: 1.5, stage_id: 3, team_id: 2, user_id: 3, date_closed: serializeDateTime(now.minus({days: 5})) },
|
||||
{ id: 10, name: 'Lead 10', planned_revenue: 1.7, stage_id: 2, team_id: 2, user_id: 3 },
|
||||
{ id: 11, name: 'Lead 11', planned_revenue: 2.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(5, 'days').format(format) },
|
||||
{ id: 11, name: 'Lead 11', planned_revenue: 2.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: serializeDateTime(now.minus({days: 5})) },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -219,7 +219,7 @@ QUnit.module('Crm Fill Temporal Service', {
|
||||
type: 'date',
|
||||
},
|
||||
granularity: 'month',
|
||||
}).end.format('YYYY-MM-DD'), '2022-02-01');
|
||||
}).end.toFormat('yyyy-MM-dd'), '2022-02-01');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -262,6 +262,6 @@ QUnit.module('Crm Fill Temporal Service', {
|
||||
type: 'date',
|
||||
},
|
||||
granularity: 'year',
|
||||
}).end.format('YYYY-MM-DD'), '2023-01-01');
|
||||
}).end.toFormat('yyyy-MM-dd'), '2023-01-01');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
|
||||
patch(MockServer.prototype, "CRM", {
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -19,21 +20,21 @@ patch(MockServer.prototype, "CRM", {
|
||||
const record = records.find(r => r.id === args.args[0][0]);
|
||||
const won_stage = this.models['crm.stage'].records.find(s => s.is_won);
|
||||
if (record.stage_id === won_stage.id && record.user_id && record.team_id && record.planned_revenue > 0) {
|
||||
const now = moment();
|
||||
const now = luxon.DateTime.now();
|
||||
let query_result = {};
|
||||
// Total won
|
||||
query_result['total_won'] = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id).length;
|
||||
// Max team 30 days
|
||||
const recordsTeam30 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
|
||||
const recordsTeam30 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as('days') <= 30));
|
||||
query_result['max_team_30'] = Math.max(...recordsTeam30.map(r => r.planned_revenue));
|
||||
// Max team 7 days
|
||||
const recordsTeam7 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
|
||||
const recordsTeam7 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as('days') <= 7));
|
||||
query_result['max_team_7'] = Math.max(...recordsTeam7.map(r => r.planned_revenue));
|
||||
// Max User 30 days
|
||||
const recordsUser30 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
|
||||
const recordsUser30 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as('days') <= 30));
|
||||
query_result['max_user_30'] = Math.max(...recordsUser30.map(r => r.planned_revenue));
|
||||
// Max User 7 days
|
||||
const recordsUser7 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
|
||||
const recordsUser7 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as('days') <= 7));
|
||||
query_result['max_user_7'] = Math.max(...recordsUser7.map(r => r.planned_revenue));
|
||||
|
||||
if (query_result.total_won === 1) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module */
|
||||
import tour from 'web_tour.tour';
|
||||
const today = moment();
|
||||
const today = luxon.DateTime.now();
|
||||
|
||||
tour.register('crm_forecast', {
|
||||
test: true,
|
||||
@@ -39,7 +39,7 @@ tour.register('crm_forecast', {
|
||||
}, {
|
||||
trigger: "div[name=date_deadline] input",
|
||||
content: "complete expected closing",
|
||||
run: `text ${today.format("MM/DD/YYYY")}`,
|
||||
run: `text ${today.toFormat("MM/dd/yyyy")}`,
|
||||
}, {
|
||||
trigger: "div[name=date_deadline] input",
|
||||
content: "click to make the datepicker disappear",
|
||||
@@ -68,7 +68,7 @@ tour.register('crm_forecast', {
|
||||
trigger: ".o_field_widget[name=date_deadline] input",
|
||||
content: "complete expected closing",
|
||||
run: function (actions) {
|
||||
actions.text(`text ${moment(today).add(5, 'months').startOf('month').subtract(1, 'days').format("MM/DD/YYYY")}`, this.$anchor);
|
||||
actions.text(`text ${today.plus({months: 5}).startOf('month').minus({days: 1}).toFormat("MM/dd/yyyy")}`, this.$anchor);
|
||||
this.$anchor[0].dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
|
||||
},
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user