diff --git a/addons/crm/static/src/views/fill_temporal_service.js b/addons/crm/static/src/views/fill_temporal_service.js index 93e153ad968..a2497bd3007 100644 --- a/addons/crm/static/src/views/fill_temporal_service.js +++ b/addons/crm/static/src/views/fill_temporal_service.js @@ -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})); } } diff --git a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js index 338e84f35cc..de4824c8da6 100644 --- a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js +++ b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js @@ -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; } diff --git a/addons/crm/static/src/views/forecast_search_model.js b/addons/crm/static/src/views/forecast_search_model.js index feeae811b6d..1bc76c64820 100644 --- a/addons/crm/static/src/views/forecast_search_model.js +++ b/addons/crm/static/src/views/forecast_search_model.js @@ -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; } diff --git a/addons/crm/static/tests/crm_rainbowman_tests.js b/addons/crm/static/tests/crm_rainbowman_tests.js index 1dbeb086290..29f40e99dca 100644 --- a/addons/crm/static/tests/crm_rainbowman_tests.js +++ b/addons/crm/static/tests/crm_rainbowman_tests.js @@ -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})) }, ], }, }, diff --git a/addons/crm/static/tests/forecast_kanban_tests.js b/addons/crm/static/tests/forecast_kanban_tests.js index a40859b0db6..7fa63f063b7 100644 --- a/addons/crm/static/tests/forecast_kanban_tests.js +++ b/addons/crm/static/tests/forecast_kanban_tests.js @@ -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'); }); }); diff --git a/addons/crm/static/tests/mock_server.js b/addons/crm/static/tests/mock_server.js index 956bcac276e..91c7174fda8 100644 --- a/addons/crm/static/tests/mock_server.js +++ b/addons/crm/static/tests/mock_server.js @@ -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) { diff --git a/addons/crm/static/tests/tours/crm_forecast_tour.js b/addons/crm/static/tests/tours/crm_forecast_tour.js index e5d99938e1f..ea9a8f98289 100644 --- a/addons/crm/static/tests/tours/crm_forecast_tour.js +++ b/addons/crm/static/tests/tours/crm_forecast_tour.js @@ -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" })); }, }, {