diff --git a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js
index faec6e7b949..a212e1761ef 100644
--- a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js
+++ b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js
@@ -9,7 +9,7 @@ odoo.define('account.dashboard.setup.tour', function (require) {
tour.register('account_render_report', {
test: true,
url: '/web',
- }, [tour.STEPS.SHOW_APPS_MENU_ITEM,
+ }, [tour.stepUtils.showAppsMenuItem(),
{
id: 'account_menu_click',
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
diff --git a/addons/account/static/tests/tours/tax_group_tests.js b/addons/account/static/tests/tours/tax_group_tests.js
index 7d3ffd9ce85..08cb218f741 100644
--- a/addons/account/static/tests/tours/tax_group_tests.js
+++ b/addons/account/static/tests/tours/tax_group_tests.js
@@ -8,7 +8,7 @@ odoo.define('account.tax.group.tour.tests', function (require) {
tour.register('account_tax_group', {
test: true,
url: "/web",
- }, [tour.STEPS.SHOW_APPS_MENU_ITEM,
+ }, [tour.stepUtils.showAppsMenuItem(),
{
content: "Go to Invoicing",
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
diff --git a/addons/crm/static/src/js/tours/crm.js b/addons/crm/static/src/js/tours/crm.js
index 57f7b1d0df6..f0a5813de14 100644
--- a/addons/crm/static/src/js/tours/crm.js
+++ b/addons/crm/static/src/js/tours/crm.js
@@ -8,7 +8,7 @@ var _t = core._t;
tour.register('crm_tour', {
url: "/web",
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
content: _t("Ready to boost your sales? Your Pipeline can be found here, under the CRM app."),
position: 'right',
@@ -71,7 +71,7 @@ tour.register('crm_tour', {
extra_trigger: '.o_lead_opportunity_form',
content: _t("Use the breadcrumbs to go back to your sales pipeline."),
position: "bottom"
-}, tour.STEPS.TOGGLE_HOME_MENU, tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, tour.stepUtils.toggleHomeMenu(), tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="base.menu_administration"]',
content: _t("Configuration options are available in the Settings app."),
position: "bottom",
@@ -85,7 +85,7 @@ tour.register('crm_tour', {
trigger: "div#invite_users .o_user_emails",
content: _t("Invite coworkers via email.
Enter one email per line."),
position: "right"
-}, tour.STEPS.TOGGLE_HOME_MENU, tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, tour.stepUtils.toggleHomeMenu(), tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
content: _t("Good job! You completed the tour of the CRM app."),
position: 'right',
diff --git a/addons/event_sale/static/tests/tours/event_configurator_ui.js b/addons/event_sale/static/tests/tours/event_configurator_ui.js
index b34187d796f..1170a142a09 100644
--- a/addons/event_sale/static/tests/tours/event_configurator_ui.js
+++ b/addons/event_sale/static/tests/tours/event_configurator_ui.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('event_configurator_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
diff --git a/addons/hr_expense/static/src/js/tours/hr_expense.js b/addons/hr_expense/static/src/js/tours/hr_expense.js
index eddc613b1ce..0422d6870db 100644
--- a/addons/hr_expense/static/src/js/tours/hr_expense.js
+++ b/addons/hr_expense/static/src/js/tours/hr_expense.js
@@ -8,7 +8,7 @@ var _t = core._t;
tour.register('hr_expense_tour' , {
url: "/web"
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
content: _t("Want to manage your expenses? It starts here."),
position: 'right',
diff --git a/addons/hr_recruitment/static/src/js/tours/hr_recruitment.js b/addons/hr_recruitment/static/src/js/tours/hr_recruitment.js
index 0f814426e8b..1939b5baf6c 100644
--- a/addons/hr_recruitment/static/src/js/tours/hr_recruitment.js
+++ b/addons/hr_recruitment/static/src/js/tours/hr_recruitment.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
var _t = core._t;
-tour.register('hr_recruitment_tour', [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+tour.register('hr_recruitment_tour', [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="hr_recruitment.menu_hr_recruitment_root"]',
content: _t('Want to start recruiting like a pro? Start here.'),
position: 'right',
diff --git a/addons/point_of_sale/static/src/js/tours/point_of_sale.js b/addons/point_of_sale/static/src/js/tours/point_of_sale.js
index 5fc69341602..161b7516082 100644
--- a/addons/point_of_sale/static/src/js/tours/point_of_sale.js
+++ b/addons/point_of_sale/static/src/js/tours/point_of_sale.js
@@ -9,7 +9,7 @@ var _t = core._t;
tour.register('point_of_sale_tour', {
url: "/web",
rainbowMan: false,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
content: _t("Ready to launch your point of sale?"),
width: 215,
diff --git a/addons/project/static/src/js/tours/project.js b/addons/project/static/src/js/tours/project.js
index 951390455b9..407fa9cdb4b 100644
--- a/addons/project/static/src/js/tours/project.js
+++ b/addons/project/static/src/js/tours/project.js
@@ -8,7 +8,7 @@ var _t = core._t;
tour.register('project_tour', {
url: "/web",
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
content: _t('Want a better way to manage your projects? It starts here.'),
position: 'right',
diff --git a/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js b/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js
index 9148f888b53..7ffd79137b7 100644
--- a/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js
+++ b/addons/purchase_product_matrix/static/tests/tours/purchase_product_matrix_tour.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('purchase_matrix_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="purchase.menu_purchase_root"]',
}, {
trigger: ".o_list_button_add",
diff --git a/addons/sale/static/src/js/tours/sale.js b/addons/sale/static/src/js/tours/sale.js
index c1177ccc4d0..4853c905b8b 100644
--- a/addons/sale/static/src/js/tours/sale.js
+++ b/addons/sale/static/src/js/tours/sale.js
@@ -8,7 +8,7 @@ var _t = core._t;
tour.register('sale_tour', {
url: "/web",
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
content: _t('Open Sales app to send your first quotation in a few clicks.'),
position: 'right',
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_advanced_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_advanced_ui.js
index 0f351b8be5e..16251f5612f 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_advanced_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_advanced_ui.js
@@ -8,7 +8,7 @@ var optionVariantImage;
tour.register('sale_product_configurator_advanced_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', // Note: The module sale_management is mandatory
edition: 'community'
}, {
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_edition_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_edition_ui.js
index 907cc86382a..14cf07663ee 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_edition_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_edition_ui.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('sale_product_configurator_edition_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js
index c68fc9debec..c566ce5b1da 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('sale_product_configurator_optional_products_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_pricelist_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_pricelist_ui.js
index ff761c0b540..60fc965b39b 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_pricelist_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_pricelist_ui.js
@@ -8,7 +8,7 @@ tour.register('sale_product_configurator_pricelist_tour', {
test: true,
},
[
-tour.STEPS.SHOW_APPS_MENU_ITEM,
+tour.stepUtils.showAppsMenuItem(),
{
content: "navigate to the sale app",
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_single_custom_attribute_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_single_custom_attribute_ui.js
index 4c1b68da988..8b1db607579 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_single_custom_attribute_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_single_custom_attribute_ui.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('sale_product_configurator_single_custom_attribute_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js
index d1857944ae7..fad1fbd3309 100644
--- a/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js
+++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js
@@ -9,7 +9,7 @@ var tour = require('web_tour.tour');
tour.register('sale_product_configurator_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
diff --git a/addons/sale_product_matrix/static/tests/tours/sale_product_matrix_tour.js b/addons/sale_product_matrix/static/tests/tours/sale_product_matrix_tour.js
index deb3c8bb2ba..f5b3ff51c68 100644
--- a/addons/sale_product_matrix/static/tests/tours/sale_product_matrix_tour.js
+++ b/addons/sale_product_matrix/static/tests/tours/sale_product_matrix_tour.js
@@ -6,7 +6,7 @@ var tour = require('web_tour.tour');
tour.register('sale_matrix_tour', {
url: "/web",
test: true,
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
+}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
}, {
trigger: ".o_list_button_add",
diff --git a/addons/web_tour/static/src/js/tip.js b/addons/web_tour/static/src/js/tip.js
index 3bc950ba8f8..d9446341de7 100644
--- a/addons/web_tour/static/src/js/tip.js
+++ b/addons/web_tour/static/src/js/tip.js
@@ -135,6 +135,7 @@ var Tip = Widget.extend({
p = $location.css("position");
} while (
$location.hasClass('dropdown-menu') ||
+ $location.hasClass('o_notebook_headers') ||
(
(o === "visible" || o === "hidden") &&
p !== "fixed" &&
@@ -319,6 +320,10 @@ Tip.getConsumeEventType = function ($element) {
if ($element.hasClass("o_field_date_range")) {
return "apply.daterangepicker input";
}
+ if (config.device.isMobile &&
+ $element.closest('.o_field_widget').is('.o_field_many2one, .o_field_many2many')) {
+ return "click";
+ }
return "input";
} else if ($element.hasClass('ui-draggable-handle')) {
return "drag";
diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 969f2dd66a0..647d0fe2563 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -2,9 +2,11 @@ odoo.define('web_tour.TourManager', function(require) {
"use strict";
var core = require('web.core');
+var config = require('web.config');
var local_storage = require('web.local_storage');
var mixins = require('web.mixins');
var utils = require('web_tour.utils');
+var TourStepUtils = require('web_tour.TourStepUtils');
var RainbowMan = require('web.RainbowMan');
var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
var ServicesMixin = require('web.ServicesMixin');
@@ -116,7 +118,8 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
return tour.wait_for.then((function () {
tour.current_step = parseInt(local_storage.getItem(get_step_key(name))) || 0;
tour.steps = _.filter(tour.steps, (function (step) {
- return !step.edition || step.edition === this.edition;
+ return (!step.edition || step.edition === this.edition) &&
+ (step.mobile === undefined || step.mobile === config.device.isMobile);
}).bind(this));
if (tour_is_consumed || tour.current_step >= tour.steps.length) {
@@ -425,30 +428,6 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
}
}
},
-
- /**
- * Tour predefined steps
- */
- STEPS: {
- SHOW_APPS_MENU_ITEM: {
- edition: 'community',
- trigger: '.o_menu_apps a',
- auto: true,
- position: "bottom",
- },
-
- TOGGLE_HOME_MENU: {
- edition: "enterprise",
- trigger: ".o_main_navbar .o_menu_toggle",
- content: _t('Click on the Home icon to navigate across apps.'),
- position: "bottom",
- },
-
- WEBSITE_NEW_PAGE: {
- trigger: "#new-content-menu > a",
- auto: true,
- position: "bottom",
- },
- },
+ stepUtils: new TourStepUtils(this)
});
});
diff --git a/addons/web_tour/static/src/js/tour_service.js b/addons/web_tour/static/src/js/tour_service.js
index 87cf69b7d83..af4c8db0dc5 100644
--- a/addons/web_tour/static/src/js/tour_service.js
+++ b/addons/web_tour/static/src/js/tour_service.js
@@ -7,9 +7,6 @@ var rpc = require('web.rpc');
var session = require('web.session');
var TourManager = require('web_tour.TourManager');
-if (config.device.isMobile) {
- return Promise.reject();
-}
/**
* @namespace
* @property {Object} active_tooltips
diff --git a/addons/web_tour/static/src/js/tour_step_utils.js b/addons/web_tour/static/src/js/tour_step_utils.js
new file mode 100644
index 00000000000..cd92f6fe019
--- /dev/null
+++ b/addons/web_tour/static/src/js/tour_step_utils.js
@@ -0,0 +1,160 @@
+odoo.define('web_tour.TourStepUtils', function (require) {
+'use strict';
+
+const core = require('web.core');
+const _t = core._t;
+
+return core.Class.extend({
+ _getHelpMessage: (functionName, ...args) => `Generated by function tour utils ${functionName}(${args.join(', ')})`,
+
+ addDebugHelp: helpMessage => step => {
+ if (typeof step.debugHelp === 'string') {
+ step.debugHelp = step.debugHelp + '\n' + helpMessage;
+ } else {
+ step.debugHelp = helpMessage;
+ }
+ return step;
+ },
+
+ editionEnterpriseModifier(step) {
+ step.edition = 'enterprise';
+ return step;
+ },
+
+ mobileModifier(step) {
+ step.mobile = true;
+ return step;
+ },
+
+ showAppsMenuItem() {
+ return {
+ edition: 'community',
+ trigger: '.o_menu_apps a',
+ auto: true,
+ position: 'bottom',
+ };
+ },
+
+ toggleHomeMenu() {
+ return {
+ edition: 'enterprise',
+ trigger: '.o_main_navbar .o_menu_toggle',
+ content: _t('Click on the Home icon to navigate across apps.'),
+ position: 'bottom',
+ };
+ },
+
+ autoExpandMoreButtons(extra_trigger) {
+ return {
+ trigger: '.oe_button_box',
+ extra_trigger: extra_trigger,
+ auto: true,
+ run: actions => {
+ const $more = $('.oe_button_box .o_button_more');
+ if ($more.length) {
+ actions.click($more);
+ }
+ },
+ };
+ },
+
+ goBackBreadcrumbsMobile(description, ...extraTrigger) {
+ return extraTrigger.map(element => ({
+ mobile: true,
+ trigger: '.breadcrumb-item:not(.d-none):first',
+ extra_trigger: element,
+ content: description,
+ position: 'bottom',
+ debugHelp: this._getHelpMessage('goBackBreadcrumbsMobile', description, ...extraTrigger),
+ }));
+ },
+
+ goToAppSteps(dataMenuXmlid, description) {
+ return [
+ this.showAppsMenuItem(),
+ {
+ trigger: `.o_app[data-menu-xmlid="${dataMenuXmlid}"]`,
+ content: description,
+ position: 'right',
+ edition: 'community',
+ },
+ {
+ trigger: `.o_app[data-menu-xmlid="${dataMenuXmlid}"]`,
+ content: description,
+ position: 'bottom',
+ edition: 'enterprise',
+ },
+ ].map(this.addDebugHelp(this._getHelpMessage('goToApp', dataMenuXmlid, description)));
+ },
+
+ openBuggerMenu(extraTrigger) {
+ return {
+ mobile: true,
+ trigger: '.o_mobile_menu_toggle',
+ extra_trigger: extraTrigger,
+ content: _t('Open bugger menu.'),
+ position: 'bottom',
+ debugHelp: this._getHelpMessage('openBuggerMenu', extraTrigger),
+ };
+ },
+
+ statusbarButtonsSteps(innerTextButton, description, extraTrigger) {
+ return [
+ {
+ mobile: true,
+ trigger: '.o_statusbar_buttons .btn.dropdown-toggle:contains(Action)',
+ extra_trigger: extraTrigger,
+ content: _t('Open Action Dropdown Menu.'),
+ position: 'bottom',
+ }, {
+ trigger: `.o_statusbar_buttons button:enabled:contains('${innerTextButton}')`,
+ content: description,
+ position: 'bottom',
+ },
+ ].map(this.addDebugHelp(this._getHelpMessage('statusbarButtonsSteps', innerTextButton, description, extraTrigger)));
+ },
+
+ simulateEnterKeyboardInSearchModal() {
+ return {
+ mobile: true,
+ trigger: '.o_searchview_input',
+ extra_trigger: '.modal:not(.o_inactive_modal) .dropdown-menu.o_searchview_autocomplete',
+ position: 'bottom',
+ run: action => {
+ const keyboardEventUp = new KeyboardEvent('keyup', {
+ bubbles: true,
+ cancelable: true,
+ key: 'Enter',
+ code: 'Enter',
+ which: 13,
+ keyCode: 13
+ });
+ action.tip_widget.$anchor[0].dispatchEvent(keyboardEventUp);
+ },
+ debugHelp: this._getHelpMessage('simulateEnterKeyboardInSearchModal'),
+ };
+ },
+
+ mobileKanbanSearchMany2X(modalTitle, valueSearched) {
+ return [
+ {
+ mobile: true,
+ trigger: '.o_enable_searchview',
+ extra_trigger: `.modal:not(.o_inactive_modal) .modal-title:contains('${modalTitle}')`,
+ position: 'bottom',
+ }, {
+ mobile: true,
+ trigger: '.o_searchview_input',
+ position: 'bottom',
+ run: `text ${valueSearched}`,
+ },
+ this.simulateEnterKeyboardInSearchModal(),
+ {
+ mobile: true,
+ trigger: `.o_kanban_record .o_kanban_record_title :contains('${valueSearched}')`,
+ position: 'bottom',
+ },
+ ].map(this.addDebugHelp(this._getHelpMessage('mobileKanbanSearchMany2X', modalTitle, valueSearched)));
+ },
+});
+});
diff --git a/addons/web_tour/views/tour_templates.xml b/addons/web_tour/views/tour_templates.xml
index 816405a39d5..09376487f73 100644
--- a/addons/web_tour/views/tour_templates.xml
+++ b/addons/web_tour/views/tour_templates.xml
@@ -10,6 +10,7 @@
+
diff --git a/addons/website/static/tests/tours/dashboard_tour.js b/addons/website/static/tests/tours/dashboard_tour.js
index b02747c6caa..de1a5f10473 100644
--- a/addons/website/static/tests/tours/dashboard_tour.js
+++ b/addons/website/static/tests/tours/dashboard_tour.js
@@ -6,7 +6,7 @@ var tour = require("web_tour.tour");
tour.register("backend_dashboard", {
test: true,
url: "/web",
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM,
+}, [tour.stepUtils.showAppsMenuItem(),
{
trigger: 'a[data-menu-xmlid="website.menu_website_configuration"]',
}, {
diff --git a/addons/website_blog/static/src/js/tours/website_blog.js b/addons/website_blog/static/src/js/tours/website_blog.js
index c9a798b2abf..db7510ceb9f 100644
--- a/addons/website_blog/static/src/js/tours/website_blog.js
+++ b/addons/website_blog/static/src/js/tours/website_blog.js
@@ -8,7 +8,11 @@ odoo.define("website_blog.tour", function (require) {
tour.register("blog", {
url: "/",
- }, [tour.STEPS.WEBSITE_NEW_PAGE, {
+ }, [{
+ trigger: '#new-content-menu > a',
+ auto: true,
+ position: 'bottom',
+ }, {
trigger: "a[data-action=new_blog_post]",
content: _t("Select this menu item to create a new blog post."),
position: "bottom",
diff --git a/addons/website_event/static/src/js/tours/website_event.js b/addons/website_event/static/src/js/tours/website_event.js
index 8e693c9ef8e..3f704012527 100644
--- a/addons/website_event/static/src/js/tours/website_event.js
+++ b/addons/website_event/static/src/js/tours/website_event.js
@@ -8,7 +8,11 @@ odoo.define("website_event.tour", function (require) {
tour.register("event", {
url: "/",
- }, [tour.STEPS.WEBSITE_NEW_PAGE, {
+ }, [{
+ trigger: '#new-content-menu > a',
+ auto: true,
+ position: 'bottom',
+ }, {
trigger: "a[data-action=new_event]",
content: _t("Click here to create a new event."),
position: "bottom",
diff --git a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js
index e32a70ace08..4f079715b2e 100644
--- a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js
+++ b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js
@@ -1,48 +1,39 @@
-odoo.define('test_main_flows.tour', function(require) {
+odoo.define('test_main_flows.tour', function (require) {
"use strict";
-var core = require('web.core');
-var tour = require('web_tour.tour');
-
-var _t = core._t;
+const {_t} = require('web.core');
+const tour = require('web_tour.tour');
tour.register('main_flow_tour', {
test: true,
url: "/web",
-}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
- trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
- content: _t('Organize your sales activities with the Sales app.'),
- position: 'right',
- edition: 'community'
-}, {
- trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
- content: _t('Organize your sales activities with the Sales app.'),
- position: 'bottom',
- edition: 'enterprise'
-}, {
+}, [
+...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the Sales app.')),
+tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
+{
// Add Stockable product
trigger: ".o_menu_sections a:contains('Products')",
extra_trigger: '.o_main_navbar',
- content: _t("Let\'s create products."),
+ content: _t("Let's create products."),
position: "bottom",
}, {
trigger: ".o_menu_sections a:has(span:contains('Products'))",
- content: _t("Let\'s create products."),
+ content: _t("Let's create products."),
position: "bottom"
}, {
trigger: '.o-kanban-button-new',
- extra_trigger: '.o_kanban_view',
- content: _t('Let\'s create your first product.'),
+ extra_trigger: "li.breadcrumb-item.active:contains('Products')",
+ content: _t("Let's create your first product."),
position: 'bottom',
}, {
trigger: 'input[name=name]',
extra_trigger: '.o_form_sheet',
- content: _t('Let\'s enter the name.'),
+ content: _t("Let's enter the name."),
position: 'left',
run: 'text the_flow.product',
}, {
- trigger: "select[name=type]",
- content: _t('Let\'s enter the product type'),
+ trigger: "select[name=type]",
+ content: _t("Let's enter the product type"),
position: 'left',
run: 'text "product"',
}, {
@@ -66,11 +57,12 @@ tour.register('main_flow_tour', {
content: _t('Go to main tab'),
position: 'top',
}, {
+ mobile: false,
trigger: ".o_field_widget[name=taxes_id] input",
content: _t("Focus on customer taxes field."),
run: function (actions) {
actions.click();
- var $e = $('.ui-menu-item:not(.o_m2o_dropdown_option) > a.ui-state-active');
+ const $e = $('.ui-menu-item:not(.o_m2o_dropdown_option) > a.ui-state-active');
if ($e.length) {
actions.click($e);
} else {
@@ -79,20 +71,11 @@ tour.register('main_flow_tour', {
},
}, {
trigger: '.o_form_button_save',
- content: _t('Save this product and the modifications you\'ve made to it.'),
+ content: _t("Save this product and the modifications you've made to it."),
position: 'bottom',
-}, {
- trigger: ".oe_button_box",
- extra_trigger: '.o_form_readonly',
- auto: true,
- run: function (actions) {
- // auto expand "More" buttons
- var $more = $(".oe_button_box .o_button_more");
- if ($more.length) {
- actions.click($more);
- }
- },
-},{
+},
+tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
+{
trigger: ".oe_button_box .oe_stat_button:has(div[name=bom_count])",
extra_trigger: '.o_form_readonly',
content: _t('See Bill of material'),
@@ -103,20 +86,41 @@ tour.register('main_flow_tour', {
position: "bottom",
}, {
// Add first component
+ // FIXME in mobile replace list by kanban + form
trigger: ".o_field_x2many_list_row_add > a",
extra_trigger: ".o_form_editable",
content: _t("Click here to add some lines."),
position: "bottom",
}, {
+ mobile: false,
trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
content: _t("Select a product, or create a new one on the fly."),
position: "right",
run: "text the_flow.component1",
}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.component1')",
auto: true,
+}, {
+ mobile: true,
+ trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
+ content: _t("Click here to open kanban search mobile."),
+ position: "bottom",
+}, {
+ mobile: true,
+ trigger: ".modal-dialog .btn:contains('Create')",
+ extra_trigger: ".modal-dialog",
+ content: _t("Click here to add new line."),
+ position: "left",
+}, {
+ mobile: true,
+ trigger: '.modal-body .o_form_editable .o_field_widget.o_input[name="name"]',
+ content: _t("Select a product, or create a new one on the fly."),
+ position: "right",
+ run: "text the_flow.component1",
}, {
// Edit first component
+ mobile: false,
trigger: ".o_selected_row .o_external_button",
content: _t("Click here to edit your component"),
position: "right",
@@ -133,52 +137,127 @@ tour.register('main_flow_tour', {
content: _t('Go to purchase tab'),
position: 'top',
}, {
- trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
- content: _t('Let\'s enter the cost price'),
+ mobile: false,
+ trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
+ content: _t("Let's enter the cost price"),
position: 'right',
}, {
- trigger: ".o_field_widget[name=name] input",
+ mobile: false,
+ trigger: ".o_field_widget[name=name] input",
extra_trigger: ".modal-dialog",
content: _t('Select a seller'),
position: 'top',
run: "text the_flow.vendor",
}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.vendor')",
in_modal: false,
}, {
- trigger: ".o_field_widget[name=price]",
+ mobile: true,
+ trigger: ".o_field_widget[name=seller_ids] .o-kanban-button-new",
+ content: _t("Let's select a vendor"),
+ position: 'bottom',
+}, {
+ mobile: true,
+ trigger: ".o_form_editable .o_field_many2one[name=name] input",
+ extra_trigger: ".modal:not(.o_inactive_modal) .o_form_editable div:contains('Vendor')",
+ content: _t("Select a vendor, or create a new one on the fly."),
+ position: "bottom",
+}, {
+ mobile: true,
+ trigger: ".modal-footer .btn:contains('Create')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Vendor')",
+ content: _t("Select a vendor, or create a new one on the fly."),
+ position: "right",
+}, {
+ mobile: true,
+ trigger: ".modal-dialog .o_field_widget[name=name] input",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-dialog .o_field_radio.o_field_widget[name=company_type]",
+ content: _t('Select a seller'),
+ position: 'top',
+ run: "text the_flow.vendor",
+}, {
+ mobile: true,
+ trigger: '.btn:has(:contains(Save))',
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Vendor')",
+ content: _t("Save this product and the modifications you've made to it."),
+ position: 'right',
+}, {
+ trigger: ".o_field_widget[name=price]",
content: _t('Set the cost price'),
position: 'right',
run: "text 1",
}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save & Close')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Vendor')",
+ content: _t('Save & Close'),
+ position: 'right',
+}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Component')",
+ content: _t('Save'),
+ position: 'right',
+}, {
+ mobile: true,
+ trigger: '.o_field_widget[name=code]',
+ extra_trigger: ".o_field_widget[name=product_id] > .o_external_button", // Wait name_create
+ // click somewhere else to exit cell focus
+}, {
+ mobile: false,
trigger: 'label:contains("Vendor Taxes")',
extra_trigger: ".o_field_widget[name=name] > .o_external_button", // Wait name_create
// click somewhere else to exit cell focus
-},
-{
+}, {
+ mobile: false,
trigger: ".modal-footer .btn-primary",
content: _t('Save'),
position: 'bottom',
-},
- {
+}, {
// Add second component
+ mobile: false,
trigger: ".o_field_x2many_list_row_add > a",
extra_trigger: "body:not(:has(table.o_list_table div.o_field_widget[name='product_id'] input))",
content: _t("Click here to add some lines."),
position: "bottom",
-},
-{
+}, {
+ mobile: true,
+ trigger: ".o_field_x2many_list_row_add > a",
+ extra_trigger: ".o_form_editable",
+ content: _t("Click here to add some lines."),
+ position: "bottom",
+}, {
+ mobile: true,
+ trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
+ content: _t("Click here to open kanban search mobile."),
+ position: "bottom",
+}, {
+ mobile: true,
+ trigger: ".modal-dialog .btn:contains('Create')",
+ extra_trigger: ".modal-dialog",
+ content: _t("Click here to add new line."),
+ position: "left",
+}, {
+ mobile: true,
+ trigger: ".modal-body .o_form_editable .o_field_widget.o_input[name=\"name\"]",
+ content: _t("Select a product, or create a new one on the fly."),
+ position: "right",
+ run: "text the_flow.component2",
+}, {
+ mobile: false,
trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
extra_trigger: '.o_field_widget[name=bom_line_ids] .o_data_row:nth(1).o_selected_row',
content: _t("Select a product, or create a new one on the fly."),
position: "right",
run: "text the_flow.component2",
-},
-{
+}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.component2')",
auto: true,
}, {
// Edit second component
+ mobile: false,
trigger: ".o_selected_row .o_external_button",
content: _t("Click here to edit your component"),
position: "right",
@@ -187,28 +266,63 @@ tour.register('main_flow_tour', {
content: _t('Go to purchase tab'),
position: 'top',
}, {
- trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
- content: _t('Let\'s enter the cost price'),
+ mobile: true,
+ trigger: ".o_field_widget[name=seller_ids] .o-kanban-button-new",
+ content: _t("Let's select a vendor"),
+ position: 'bottom',
+}, {
+ mobile: true,
+ trigger: '.o_form_editable .o_field_many2one[name="name"] input',
+ extra_trigger: ".modal:not(.o_inactive_modal) .o_form_editable div:contains('Vendor')",
+ content: _t("Select a vendor, or create a new one on the fly."),
+ position: "bottom",
+},
+...tour.stepUtils.mobileKanbanSearchMany2X('Vendor', 'the_flow.vendor'),
+{
+ mobile: false,
+ trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
+ content: _t("Let's enter the cost price"),
position: 'right',
}, {
- trigger: ".o_field_widget[name=name] input",
+ mobile: false,
+ trigger: ".o_field_widget[name=name] input",
extra_trigger: ".modal-dialog",
content: _t('Select a seller'),
position: 'top',
run: "text the_flow.vendor",
}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.vendor')",
auto: true,
in_modal: false,
}, {
- trigger: ".o_field_widget[name=price]",
+ trigger: ".o_field_widget[name=price]",
content: _t('Set the cost price'),
position: 'right',
run: "text 1",
}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save & Close')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Vendor')",
+ content: _t('Save & Close'),
+ position: 'right',
+}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Component')",
+ content: _t('Save'),
+ position: 'right',
+}, {
+ mobile: true,
+ trigger: '.o_field_widget[name=code]',
+ extra_trigger: ".o_field_widget[name=product_id] > .o_external_button", // Wait name_create
+ // click somewhere else to exit cell focus
+}, {
+ mobile: false,
trigger: 'label:contains("Vendor Taxes")',
// click somewhere else to exit cell focus
-},{
+}, {
+ mobile: false,
trigger: ".modal-footer .btn-primary",
extra_trigger: ".o_field_widget[name=seller_ids] .o_data_row td:nth-child(2):contains('the_flow.vendor')",
content: _t('Save'),
@@ -219,19 +333,27 @@ tour.register('main_flow_tour', {
content: _t('Save the bom.'),
position: 'bottom',
}, {
+ mobile: false,
trigger: ".breadcrumb-item:first",
content: _t("Use the breadcrumbs to go back to products."),
position: "bottom"
-}, {
+},
+...tour.stepUtils.goBackBreadcrumbsMobile(
+ _t("Use the breadcrumbs to go back to products."),
+ undefined,
+ ".breadcrumb-item.active:contains('Bill of Materials')",
+ ".breadcrumb-item.active:contains('the_flow.product')"
+ ),
+{
// Add service product
trigger: '.o-kanban-button-new',
extra_trigger: '.o_kanban_view',
- content: _t('Let\'s create your second product.'),
+ content: _t("Let's create your second product."),
position: 'bottom',
}, {
trigger: 'input[name=name]',
extra_trigger: '.o_form_sheet',
- content: _t('Let\'s enter the name.'),
+ content: _t("Let's enter the name."),
position: 'left',
run: 'text the_flow.service',
}, {
@@ -240,11 +362,12 @@ tour.register('main_flow_tour', {
position: 'left',
run: 'text "service"',
}, {
+ mobile: false,
trigger: ".o_field_widget[name=taxes_id] input",
content: _t("Focus on customer taxes field."),
run: function (actions) {
actions.click();
- var $e = $('.ui-menu-item:not(.o_m2o_dropdown_option) > a.ui-state-active');
+ const $e = $('.ui-menu-item:not(.o_m2o_dropdown_option) > a.ui-state-active');
if ($e.length) {
actions.click($e);
} else {
@@ -264,17 +387,43 @@ tour.register('main_flow_tour', {
content: _t('Change track service'),
position: 'left',
}, {
+ mobile: false,
trigger: '.o_field_widget[name=project_id] input',
content: _t('Choose project'),
position: 'left',
run: 'text the_flow.project',
}, {
+ mobile: true,
+ trigger: '.o_field_widget[name=project_id] input',
+ content: _t('Choose project'),
+ position: 'left',
+}, {
+ mobile: true,
+ trigger: ".modal-dialog .btn:contains('Create')",
+ extra_trigger: ".modal-dialog",
+ content: _t("Click here to add new line."),
+ position: "left",
+}, {
+ mobile: true,
+ trigger: 'input[name=name]',
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Project')",
+ content: _t('Let\'s enter the name.'),
+ position: 'left',
+ run: 'text the_flow.project',
+}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.project')",
auto: true,
+}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Project')",
+ content: _t('Save'),
+ position: 'right',
}, {
trigger: '.o_form_button_save',
extra_trigger: '.o_field_widget[name=project_id] > .o_external_button', // Wait name_create
- content: _t('Save this product and the modifications you\'ve made to it.'),
+ content: _t("Save this product and the modifications you've made to it."),
position: 'bottom',
}, {
// Create an opportunity
@@ -282,17 +431,9 @@ tour.register('main_flow_tour', {
trigger: '.o_menu_toggle',
content: _t('Go back to the home menu'),
position: 'bottom',
-}, tour.STEPS.SHOW_APPS_MENU_ITEM, {
- trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
- content: _t('Organize your sales activities with the CRM app.'),
- position: 'right',
- edition: 'community'
-}, {
- trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
- content: _t('Organize your sales activities with the CRM app.'),
- position: 'bottom',
- edition: 'enterprise'
-}, {
+},
+...tour.stepUtils.goToAppSteps('crm.crm_menu_root', _t('Organize your sales activities with the CRM app.')),
+{
trigger: ".o-kanban-button-new",
extra_trigger: '.o_opportunity_kanban',
content: _t("Click here to create your first opportunity and add it to your pipeline."),
@@ -303,74 +444,140 @@ tour.register('main_flow_tour', {
position: "right",
run: "text the_flow.opportunity",
}, {
+ mobile: false,
trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] input",
content: _t("Write the name of your customer to create one on the fly, or select an existing one."),
position: "left",
run: "text the_flow.customer",
}, {
+ mobile: true,
+ trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] input",
+ content: _t("Write the name of your customer to create one on the fly, or select an existing one."),
+ position: "left",
+}, {
+ mobile: true,
+ trigger: ".modal-dialog .btn:contains('Create')",
+ extra_trigger: ".modal-dialog",
+ content: _t("Click here to add new line."),
+ position: "left",
+}, {
+ mobile: true,
+ trigger: '.o_field_widget[name=name] input',
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Customer')",
+ content: _t('Let\'s enter the name.'),
+ position: 'left',
+ run: 'text the_flow.customer',
+}, {
+ mobile: false,
trigger: ".ui-menu-item > a:contains('the_flow.customer')",
auto: true,
+}, {
+ mobile: true,
+ trigger: ".modal-footer .btn-primary:contains('Save')",
+ extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Customer')",
+ content: _t('Save'),
+ position: 'right',
}, {
trigger: ".o_kanban_quick_create .o_kanban_add",
extra_trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] > .o_external_button", // Wait name_create
content: _t("Click here to add your opportunity."),
position: "right",
}, {
+ mobile: false,
trigger: ".o_kanban_group:first .o_kanban_record:has(span:contains('the_flow.opportunity'))",
content: _t("Drag & drop opportunities between columns as you progress in your sales cycle."),
position: "right",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
}, {
+ mobile: false,
trigger: ".o_kanban_group:eq(2) > .o_kanban_record:has(span:contains('the_flow.opportunity'))",
content: _t("Click on an opportunity to zoom to it."),
position: "bottom",
}, {
-// Create a quotation
- trigger: ".o_statusbar_buttons > button:enabled:contains('New Quotation')",
- content: _t('
Create a quotation
'), - position: "right" + mobile: true, + trigger: ".o_kanban_group:first .o_kanban_record:has(span:contains('the_flow.opportunity'))", + content: _t("Open the_flow.opportunity"), + position: "bottom", }, { + mobile: true, + trigger: ".o_statusbar_status .btn.dropdown-toggle:contains(New)", + content: _t("Change status from New to proposition."), + position: "bottom", +}, { + mobile: true, + trigger: ".o_statusbar_status .btn:contains(Proposition)", + content: _t("Change status from New to proposition."), + position: "bottom", +}, +// Create a quotation +...tour.stepUtils.statusbarButtonsSteps('New Quotation', _t('Create a quotation
')), +{ + mobile: false, trigger: ".o_field_widget[name=order_line] .o_field_x2many_list_row_add > a", content: _t("Click here to add some lines to your quotations."), position: "bottom", +}, { + mobile: true, + trigger: ".o_field_widget[name=order_line] .btn:contains(Add)", + content: _t("Click here to add some lines to your quotations."), + position: "bottom", }, { /** * We need both triggers because the "sale_product_configurator" module replaces the * "product_id" field with a "product_template_id" field. * This selector will still only ever select one element. */ + mobile: false, trigger: ".o_field_widget[name=product_id] input, .o_field_widget[name=product_template_id] input", content: _t("Select a product, or create a new one on the fly. The product will define the default sales price (that you can change), taxes and description automatically."), position: "right", run: function (actions) { actions.text("the_flow.product", this.$anchor); // fake keydown to trigger search - var keyDownEvent = jQuery.Event("keydown"); + const keyDownEvent = $.Event("keydown"); keyDownEvent.which = 42; this.$anchor.trigger(keyDownEvent); - var $descriptionElement = $('.o_form_editable textarea[name="name"]'); + const $descriptionElement = $('.o_form_editable textarea[name="name"]'); // when description changes, we know the product has been loaded - $descriptionElement.change(function () { - if ($(this).val().indexOf('the_flow.product') !== -1){ + $descriptionElement.on('change', function () { + if ($(this).val().indexOf('the_flow.product') !== -1) { $(this).addClass('product_loading_success'); } }); }, }, { + mobile: false, trigger: ".ui-menu-item > a:contains('the_flow.product')", }, { + mobile: false, trigger: '.o_form_editable textarea[name="name"].product_loading_success', run: function () {} // wait for product loading }, { + mobile: true, + trigger: ".o_field_widget[name=product_id] input", + extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Order Lines')", + content: _t("Select a product, or create a new one on the fly. The product will define the default sales price (that you can change), taxes and description automatically."), + position: "right", +}, +...tour.stepUtils.mobileKanbanSearchMany2X('Product', 'the_flow.product'), +{ + mobile: false, trigger: ".o_field_widget[name=order_line] .o_field_x2many_list_row_add > a", content: _t("Click here to add some lines to your quotations."), position: "bottom", +}, { + mobile: true, + trigger: ".modal-footer .btn-primary:contains('Save & New')", + extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Order Lines')", + content: _t('Save & New'), + position: 'right', }, { /** * We need both triggers because the "sale_product_configurator" module replaces the * "product_id" field with a "product_template_id" field. * This selector will still only ever select one element. */ + mobile: false, trigger: ".o_field_widget[name=product_id] input, .o_field_widget[name=product_template_id] input", extra_trigger: '.o_field_widget[name=order_line] .o_data_row:nth(1).o_selected_row', content: _t("Select a product"), @@ -378,48 +585,60 @@ tour.register('main_flow_tour', { run: function (actions) { actions.text("the_flow.service", this.$anchor); // fake keydown to trigger search - var keyDownEvent = jQuery.Event("keydown"); + const keyDownEvent = $.Event("keydown"); keyDownEvent.which = 42; this.$anchor.trigger(keyDownEvent); - var $descriptionElement = $('.o_form_editable textarea[name="name"]'); + const $descriptionElement = $('.o_form_editable textarea[name="name"]'); // when description changes, we know the product has been loaded - $descriptionElement.change(function () { - if ($(this).val().indexOf('the_flow.service') !== -1){ + $descriptionElement.on('change', function () { + if ($(this).val().indexOf('the_flow.service') !== -1) { $(this).addClass('product_service_loading_success'); } }); }, }, { + mobile: false, trigger: ".ui-menu-item > a:contains('the_flow.service')", }, { + mobile: false, trigger: '.o_form_editable textarea[name="name"].product_service_loading_success', run: function () {} // wait for product loading }, { + mobile: false, trigger: 'label:contains("Untaxed Amount")', // click somewhere else to exit cell focus }, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Send by Email')", - extra_trigger: ".o_statusbar_status .btn-primary:contains('Quotation')", - content: _t("Try to send it to email"), - position: "bottom", -}, { + mobile: true, + trigger: ".o_field_widget[name=product_id] input", + extra_trigger: '.o_field_x2many_kanban[name=order_line] .oe_kanban_card:contains(the_flow.product)', + content: _t("Select a product, or create a new one on the fly. The product will define the default sales price (that you can change), taxes and description automatically."), + position: "right", +}, +...tour.stepUtils.mobileKanbanSearchMany2X('Product', 'the_flow.service'), +{ + mobile: true, + trigger: ".modal-footer .btn-primary:contains('Save & Close')", + extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Order Lines')", + content: _t('Save & Close'), + position: 'right', +}, +...tour.stepUtils.statusbarButtonsSteps('Send by Email', _t("Try to send it to email"), ".o_statusbar_status .btn-primary:contains('Quotation')"), +{ trigger: ".o_field_widget[name=email]", content: _t("Enter an email address"), position: "right", run: "text test@the_flow.com", -},{ +}, { trigger: ".modal-footer .btn-primary", content: _t("Save your changes"), position: "bottom", -}, { +}, { trigger: ".modal-footer .btn-primary span:contains('Send')", content: _t("Try to send it to email"), position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Confirm')", - content: _t("Confirm this quotation
"), - position: "bottom" -}, { +}, +...tour.stepUtils.statusbarButtonsSteps('Confirm', _t("Confirm this quotation
")), +{ trigger: ".o_form_button_save", extra_trigger: ".o_statusbar_status .btn-primary:contains('Sales Order')", content: _t("Save this quotation
"), @@ -430,18 +649,12 @@ tour.register('main_flow_tour', { trigger: '.o_menu_toggle', content: _t('Go back to the home menu'), position: 'bottom', -}, tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app:contains("Inventory")', - content: _t('Go to Inventory'), - position: 'right', - edition: 'community' -}, { - trigger: '.o_app > div:contains("Inventory")', - content: _t('Go to Inventory'), - position: 'bottom', - edition: 'enterprise' -}, { +}, +...tour.stepUtils.goToAppSteps('stock.menu_stock_root', _t('Go to Inventory')), +tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Overview')"), +{ trigger: ".o_menu_sections a:contains('Master Data')", + extra_trigger: '.o_main_navbar', content: _t("Go to Master Data"), position: "bottom" }, { @@ -449,18 +662,33 @@ tour.register('main_flow_tour', { content: _t("Reordering Rules"), position: "bottom" }, { + mobile: false, trigger: ".o_list_button_add", content: _t("Let's create a new reordering rule"), position: "right", }, { + mobile: true, + trigger: ".o-kanban-button-new", + content: _t("Let's create a new reordering rule"), + position: "right", +}, { + mobile: false, trigger: ".o_field_widget[name=product_id] input", content: _t("Write the name of your product."), position: "top", run: "text the_flow.component2", }, { + mobile: false, trigger: ".ui-menu-item > a:contains('the_flow.component2')", auto: true, }, { + mobile: true, + trigger: ".o_field_widget[name=product_id] input", + content: _t("Write the name of your product."), + position: "top", +}, +...tour.stepUtils.mobileKanbanSearchMany2X('Product', 'the_flow.component2'), +{ trigger: ".o_field_widget[name=product_min_qty]", content: _t("Set the minimum product quantity"), position: "right", @@ -474,12 +702,14 @@ tour.register('main_flow_tour', { trigger: ".o_form_button_save", content: _t("Save this reordering rule
"), position: "bottom" -}, { +}, +tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"), +{ // Run the schedulers trigger: ".o_menu_sections a:contains('Operations')", content: _t("Go to Run Schedulers"), position: "bottom" -},{ +}, { trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_procurement_compute']", content: _t("Click on schedulers"), position: "bottom" @@ -494,33 +724,23 @@ tour.register('main_flow_tour', { trigger: '.o_menu_toggle', content: _t('Go back to the home menu'), position: 'bottom', -}, tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app:contains("Purchase")', - content: _t('Go to Purchase'), - position: 'right', - edition: 'community' -}, { - trigger: '.o_app > div:contains("Purchase")', - content: _t('Go to Purchase'), - position: 'bottom', - edition: 'enterprise' -}, { +}, +...tour.stepUtils.goToAppSteps('purchase.menu_purchase_root', _t('Go to Purchase')), +{ + mobile: false, trigger: '.o_data_row:has(.o_data_cell:contains("the_flow.vendor"))', content: _t('Select the generated request for quotation'), position: 'bottom', }, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Confirm Order')", - content: _t("Confirm quotation"), - position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Receive Products')", - content: _t("Receive Product"), - position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Validate')", - content: _t("Validate"), - position: "bottom", -}, { + mobile: true, + trigger: '.o_kanban_record:has(.o_kanban_record_title:contains("the_flow.vendor"))', + content: _t('Select the generated request for quotation'), + position: 'bottom', +}, +...tour.stepUtils.statusbarButtonsSteps('Confirm Order', _t("Confirm quotation")), +...tour.stepUtils.statusbarButtonsSteps('Receive Products', _t("Receive Product"), ".o_statusbar_status .btn.dropdown-toggle:contains('Purchase Order')"), +...tour.stepUtils.statusbarButtonsSteps('Validate', _t("Validate"), ".o_statusbar_status .btn.dropdown-toggle:contains('Ready')"), +{ trigger: ".modal-footer .btn-primary", extra_trigger: ".modal-dialog", content: _t("Apply"), @@ -529,19 +749,11 @@ tour.register('main_flow_tour', { trigger: ".o_back_button a, .breadcrumb-item:not('.active'):last", content: _t('go back to the purchase order'), position: 'bottom', - }, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Create Bill')", - content: _t('go to Vendor Bills'), - position: 'bottom', -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Post')", - content: _t("Try to send it to email"), - position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Register Payment')", - content: _t("Register Payment"), - position: "bottom", -}, { +}, +...tour.stepUtils.statusbarButtonsSteps('Create Bill', _t('go to Vendor Bills'), ".o_statusbar_status .btn.dropdown-toggle:contains('Purchase Order')"), +...tour.stepUtils.statusbarButtonsSteps('Post', _t("Try to send it to email"), ".o_statusbar_status .btn.dropdown-toggle:contains('Draft')"), +...tour.stepUtils.statusbarButtonsSteps('Register Payment', _t("Register Payment"), ".o_statusbar_status .btn.dropdown-toggle:contains('Posted')"), +{ trigger: ".modal-footer .btn-primary", content: _t("Validate"), position: "bottom", @@ -550,60 +762,65 @@ tour.register('main_flow_tour', { trigger: '.o_menu_toggle', content: _t('Go back to the home menu'), position: 'bottom', -}, tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app:contains("Manufacturing")', - content: _t('Go to Manufacturing'), - position: 'right', - edition: 'community' -}, { - trigger: '.o_app > div:contains("Manufacturing")', - content: _t('Go to Manufacturing'), - position: 'bottom', - edition: 'enterprise' -}, { +}, +...tour.stepUtils.goToAppSteps('mrp.menu_mrp_root', _t('Go to Manufacturing')), +tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing Orders'), li.breadcrumb-item.active:contains('Work Centers Overview')"), +{ + mobile: false, trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_manufacturing']", content: _t('Click on Operations menuitem'), position: 'bottom', }, { + mobile: false, trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_production_action']", content: _t('Open manufacturing orders'), position: 'bottom', }, { + mobile: false, trigger: '.o_data_row:has(.o_data_cell:contains("the_flow.product")):first', content: _t('Select the generated manufacturing order'), position: 'bottom', }, { - trigger: ".o_statusbar_buttons > button[name='action_assign']:enabled", - content: _t("Check availability"), + mobile: true, + trigger: ".o_menu_sections a:contains('Operations')", + extra_trigger: '.o_main_navbar', + content: _t("Let's go to manufacturing order."), position: "bottom", }, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Produce')", - content: _t("Produce"), - position: "bottom", + mobile: true, + trigger: ".o_menu_sections a:has(span:contains('Manufacturing Orders'))", + content: _t("Let's go to manufacturing order."), + position: "bottom" }, { - trigger: ".modal-footer .btn-primary:nth-child(3)", + mobile: true, + trigger: '.o_kanban_record:has(.o_kanban_record_title:contains("the_flow.product")):first', + extra_trigger: "li.breadcrumb-item.active:contains('Manufacturing Orders')", + content: _t('Select the generated manufacturing order'), + position: 'bottom', +}, +...tour.stepUtils.statusbarButtonsSteps('Check availability', _t("Check availability")), +...tour.stepUtils.statusbarButtonsSteps('Produce', _t("Produce"), "body.o_web_client:not(.oe_wait)"), +{ + mobile: false, + trigger: ".modal-footer .btn-primary:nth-child(3)", content: _t('Record Production'), position: 'bottom', }, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Mark as Done')", - content: _t("Mark as Done"), - position: "bottom", -}, { + mobile: true, + trigger: '.modal-footer .btn-primary[name="do_produce"]:not(.o_invisible_modifier)', + content: _t('Record Production'), + position: 'bottom', +}, +...tour.stepUtils.statusbarButtonsSteps('Mark as Done', _t("Mark as Done"), ".o_statusbar_status .btn.dropdown-toggle:contains('To Close')"), +{ edition: "enterprise", trigger: '.o_menu_toggle', content: _t('Go back to the home menu'), position: 'bottom', -}, tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - content: _t('Organize your sales activities with the Sales app.'), - position: 'bottom', - edition: 'community' -}, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - content: _t('Organize your sales activities with the Sales app.'), - position: 'bottom', - edition: 'enterprise' -}, { +}, +...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the Sales app.')), +tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"), +{ trigger: ".o_menu_sections a[data-menu-xmlid='sale.sale_order_menu']", content: _t("Go to Sales menu"), position: "bottom" @@ -612,19 +829,37 @@ tour.register('main_flow_tour', { content: _t("Go to the sales orders"), position: "bottom" }, { + mobile: false, trigger: ".o_data_row:first", extra_trigger: '.o_control_panel .breadcrumb:contains("Sales Orders")', content: _t("Go to the last sale order"), position: "right" }, { + mobile: true, + trigger: ".o_kanban_record:first", + extra_trigger: '.o_control_panel .breadcrumb:contains("Sales Orders")', + content: _t("Go to the last sale order"), + position: "bottom" +}, +tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("S0")')), +{ + mobile: false, trigger: '.oe_button_box .oe_stat_button:has(div[name=tasks_count])', content: _t('See Tasks'), position: 'right', +}, { + mobile: true, + trigger: '.oe_button_box .oe_stat_button:has(div[name=tasks_count])', + extra_trigger: '.o_control_panel .breadcrumb:contains("S0")', + content: _t('See Tasks'), + position: 'bottom', }, { trigger: '.o_field_widget[name=project_id]', content: _t('See Project'), position: 'right', -}, { +}, +tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("the_flow.project")')), +{ trigger: '.oe_button_box .oe_stat_button:has(span:contains("Timesheets"))', extra_trigger: '.o_form_readonly', content: _t('See Timesheets'), @@ -634,6 +869,7 @@ tour.register('main_flow_tour', { content: _t('Add a Timesheet'), position: 'bottom', }, { + // FIXME replace list by kanban + form trigger: '.o_selected_row input[name=name]', content: _t('Set description'), position: 'bottom', @@ -644,8 +880,15 @@ tour.register('main_flow_tour', { position: 'bottom', run: 'click', }, { + mobile: false, trigger: ".ui-menu-item > a", auto: true, +}, { + mobile: true, + trigger: ".o_kanban_record .o_kanban_record_title :contains('the_flow.service')", + extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Task')", + content: _t("Select the the_flow.vendor"), + position: "bottom", }, { trigger: '.o_selected_row input[name=unit_amount]', content: _t('Set time'), @@ -655,28 +898,29 @@ tour.register('main_flow_tour', { trigger: '.o_list_button_save', content: _t('Save'), position: 'bottom', -}, { +}, +...tour.stepUtils.goBackBreadcrumbsMobile( + _t('Back to the sale order'), + undefined, + ".breadcrumb-item.active:contains('the_flow.project')", + ".breadcrumb-item.active:contains('the_flow.service')" + ), +{ + mobile: false, trigger: '.breadcrumb-item:nth-child(2) a', extra_trigger: '.o_list_button_add', // Waiting save content: _t('Back to the sale order'), position: 'bottom', -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Create Invoice')", - content: _t("Validate"), - position: "bottom", -}, { +}, +...tour.stepUtils.statusbarButtonsSteps('Create Invoice', _t("Validate"), ".o_field_widget[name=order_line]"), +{ trigger: ".modal-footer .btn-primary", content: _t("Create and View Invoices"), position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Post')", - content: _t("Validate"), - position: "bottom", -}, { - trigger: ".o_statusbar_buttons > button:enabled:contains('Register Payment')", - content: _t("Register Payment"), - position: "bottom", -}, { +}, +...tour.stepUtils.statusbarButtonsSteps('Post', _t("Validate"), ".breadcrumb-item.active:contains('Draft Invoice')"), +...tour.stepUtils.statusbarButtonsSteps('Register Payment', _t("Register Payment"), ".breadcrumb-item.active:contains('INV/')"), +{ trigger: ".modal-footer .btn-primary", content: _t("Validate"), position: "bottom", @@ -718,18 +962,19 @@ tour.register('main_flow_tour', { }, { edition: "enterprise", trigger: 'input[name=name]', - content: _t('Let\'s enter the reference.'), + content: _t("Let's enter the reference."), position: 'left', run: 'text the_flow.statement', }, { edition: "enterprise", - trigger: ".o_field_widget[name=balance_end_real] input", - content: _t('Let\'s calculate the ending balance.'), + trigger: ".o_field_widget[name=balance_end_real] input", + content: _t("Let's calculate the ending balance."), position: 'right', run: 'text 9010.85', // + 12.65 }, { + // FIXME convert list in kanban + form edition: "enterprise", - trigger: ".o_field_widget[name=line_ids] .o_field_x2many_list_row_add > a", + trigger: ".o_field_widget[name=line_ids] .o_field_x2many_list_row_add > a", content: _t("Click here to add some lines."), position: "bottom", }, { @@ -739,20 +984,34 @@ tour.register('main_flow_tour', { position: "bottom", run: "text 11.00", }, { + mobile: false, edition: "enterprise", trigger: ".o_selected_row .o_field_widget[name=partner_id] input", content: _t("Write the name of your customer."), position: "bottom", run: "text the_flow.customer", }, { + mobile: false, edition: "enterprise", trigger: ".ui-menu-item > a:contains('the_flow.customer')", in_modal: false, auto: true, +}, { + mobile: true, + edition: "enterprise", + trigger: ".o_selected_row .o_field_widget[name=partner_id] input", + content: _t("Write the name of your customer."), + position: "bottom", +}, { + mobile: true, + trigger: ".o_kanban_record .o_kanban_record_title :contains('the_flow.customer')", + extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Partner')", + content: _t('Save'), + position: 'right', }, { edition: "enterprise", trigger: ".o_selected_row .o_field_widget[name=name]", - content: _t('Let\'s enter a name.'), + content: _t("Let's enter a name."), position: "bottom", run: "text the_flow.statement.line", }, { @@ -760,12 +1019,9 @@ tour.register('main_flow_tour', { trigger: '.o_form_button_save', content: _t('Save.'), position: 'bottom', -}, { - edition: "enterprise", - trigger: ".o_statusbar_buttons > button:enabled:contains('Reconcile')", - content: _t('Reconcile
'), - position: "bottom", -}, { +}, +...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile
'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier), +{ edition: "enterprise", trigger: "button.o_reconcile, button.o_validate", content: _t('Click on Reconcile
'), diff --git a/odoo/addons/test_main_flows/tests/test_flow.py b/odoo/addons/test_main_flows/tests/test_flow.py index 2f359d4cccc..d450c335253 100644 --- a/odoo/addons/test_main_flows/tests/test_flow.py +++ b/odoo/addons/test_main_flows/tests/test_flow.py @@ -1,12 +1,12 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +import odoo import odoo.tests +import unittest +class BaseTestUi(odoo.tests.HttpCase): -@odoo.tests.tagged('post_install', '-at_install') -class TestUi(odoo.tests.HttpCase): - - def test_01_main_flow_tour(self): + def main_flow_tour(self): # Define minimal accounting data to run without CoA a_expense = self.env['account.account'].create({ 'code': 'X2120', @@ -68,3 +68,21 @@ class TestUi(odoo.tests.HttpCase): }) self.start_tour("/web", 'main_flow_tour', login="admin", timeout=180) + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(BaseTestUi): + + def test_01_main_flow_tour(self): + self.main_flow_tour() + +@odoo.tests.tagged('post_install', '-at_install') +class TestUiMobile(BaseTestUi): + + browser_size = '375x667' + + def test_01_main_flow_tour_mobile(self): + + if odoo.release.version_info[-1] == 'e': + self.main_flow_tour() + else: + raise unittest.SkipTest("Mobile testing not needed in community")