summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/services
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/services
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/services')
-rw-r--r--addons/web/static/src/js/services/ajax_service.js41
-rw-r--r--addons/web/static/src/js/services/config.js122
-rw-r--r--addons/web/static/src/js/services/core.js49
-rw-r--r--addons/web/static/src/js/services/crash_manager.js412
-rw-r--r--addons/web/static/src/js/services/crash_manager_service.js9
-rw-r--r--addons/web/static/src/js/services/data_manager.js225
-rw-r--r--addons/web/static/src/js/services/local_storage_service.js20
-rw-r--r--addons/web/static/src/js/services/notification_service.js111
-rw-r--r--addons/web/static/src/js/services/report_service.js35
-rw-r--r--addons/web/static/src/js/services/session.js12
-rw-r--r--addons/web/static/src/js/services/session_storage_service.js20
11 files changed, 1056 insertions, 0 deletions
diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js
new file mode 100644
index 00000000..da3d436c
--- /dev/null
+++ b/addons/web/static/src/js/services/ajax_service.js
@@ -0,0 +1,41 @@
+odoo.define('web.AjaxService', function (require) {
+"use strict";
+
+var AbstractService = require('web.AbstractService');
+var ajax = require('web.ajax');
+var core = require('web.core');
+var session = require('web.session');
+
+var AjaxService = AbstractService.extend({
+ /**
+ * @param {Object} libs - @see ajax.loadLibs
+ * @param {Object} [context] - @see ajax.loadLibs
+ * @param {Object} [tplRoute] - @see ajax.loadLibs
+ */
+ loadLibs: function (libs, context, tplRoute) {
+ return ajax.loadLibs(libs, context, tplRoute);
+ },
+ rpc: function (route, args, options, target) {
+ var rpcPromise;
+ var promise = new Promise(function (resolve, reject) {
+ rpcPromise = session.rpc(route, args, options);
+ rpcPromise.then(function (result) {
+ if (!target.isDestroyed()) {
+ resolve(result);
+ }
+ }).guardedCatch(function (reason) {
+ if (!target.isDestroyed()) {
+ reject(reason);
+ }
+ });
+ });
+ promise.abort = rpcPromise.abort.bind(rpcPromise);
+ return promise;
+ },
+});
+
+core.serviceRegistry.add('ajax', AjaxService);
+
+return AjaxService;
+
+});
diff --git a/addons/web/static/src/js/services/config.js b/addons/web/static/src/js/services/config.js
new file mode 100644
index 00000000..6c576c35
--- /dev/null
+++ b/addons/web/static/src/js/services/config.js
@@ -0,0 +1,122 @@
+odoo.define('web.config', function (require) {
+"use strict";
+
+const Bus = require('web.Bus');
+
+const bus = new Bus();
+
+/**
+ * This module contains all the (mostly) static 'environmental' information.
+ * This is often necessary to allow the rest of the web client to properly
+ * render itself.
+ *
+ * Note that many information currently stored in session should be moved to
+ * this file someday.
+ */
+
+var config = {
+ device: {
+ /**
+ * bus to use in order to be able to handle device config related events
+ * - 'size_changed' : triggered when window size is
+ * corresponding to a new bootstrap breakpoint. The new size_class
+ * is provided.
+ */
+ bus: bus,
+ /**
+ * touch is a boolean, true if the device supports touch interaction
+ *
+ * @type Boolean
+ */
+ touch: 'ontouchstart' in window || 'onmsgesturechange' in window,
+ /**
+ * size_class is an integer: 0, 1, 2, 3 or 4, depending on the (current)
+ * size of the device. This is a dynamic property, updated whenever the
+ * browser is resized
+ *
+ * @type Number
+ */
+ size_class: null,
+ /**
+ * A frequent use case is to have a different render in 'mobile' mode,
+ * meaning when the screen is small. This flag (boolean) is true when
+ * the size is XS/VSM/SM. It is also updated dynamically.
+ *
+ * @type Boolean
+ */
+ isMobile: null,
+ /**
+ * Mobile device detection using userAgent.
+ * This flag doesn't depend on the size/resolution of the screen.
+ * It targets mobile devices which suggests that there is a virtual keyboard.
+ *
+ * @return {boolean}
+ */
+ isMobileDevice: navigator.userAgent.match(/Android/i) ||
+ navigator.userAgent.match(/webOS/i) ||
+ navigator.userAgent.match(/iPhone/i) ||
+ navigator.userAgent.match(/iPad/i) ||
+ navigator.userAgent.match(/iPod/i) ||
+ navigator.userAgent.match(/BlackBerry/i) ||
+ navigator.userAgent.match(/Windows Phone/i),
+ /**
+ * Mapping between the numbers 0,1,2,3,4,5,6 and some descriptions
+ */
+ SIZES: { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 },
+ },
+ /**
+ * States whether the current environment is in debug or not.
+ *
+ * @param debugMode the debug mode to check, empty for simple debug mode
+ * @returns {boolean}
+ */
+ isDebug: function (debugMode) {
+ if (debugMode) {
+ return odoo.debug && odoo.debug.indexOf(debugMode) !== -1;
+ }
+ return odoo.debug;
+ },
+};
+
+
+var medias = [
+ window.matchMedia('(max-width: 474px)'),
+ window.matchMedia('(min-width: 475px) and (max-width: 575px)'),
+ window.matchMedia('(min-width: 576px) and (max-width: 767px)'),
+ window.matchMedia('(min-width: 768px) and (max-width: 991px)'),
+ window.matchMedia('(min-width: 992px) and (max-width: 1199px)'),
+ window.matchMedia('(min-width: 1200px) and (max-width: 1533px)'),
+ window.matchMedia('(min-width: 1534px)'),
+];
+
+/**
+ * Return the current size class
+ *
+ * @returns {integer} a number between 0 and 5, included
+ */
+function _getSizeClass() {
+ for (var i = 0 ; i < medias.length ; i++) {
+ if (medias[i].matches) {
+ return i;
+ }
+ }
+}
+/**
+ * Update the size dependant properties in the config object. This method
+ * should be called every time the size class changes.
+ */
+function _updateSizeProps() {
+ var sc = _getSizeClass();
+ if (sc !== config.device.size_class) {
+ config.device.size_class = sc;
+ config.device.isMobile = config.device.size_class <= config.device.SIZES.SM;
+ config.device.bus.trigger('size_changed', config.device.size_class);
+ }
+}
+
+_.invoke(medias, 'addListener', _updateSizeProps);
+_updateSizeProps();
+
+return config;
+
+});
diff --git a/addons/web/static/src/js/services/core.js b/addons/web/static/src/js/services/core.js
new file mode 100644
index 00000000..1d4619f6
--- /dev/null
+++ b/addons/web/static/src/js/services/core.js
@@ -0,0 +1,49 @@
+odoo.define('web.core', function (require) {
+"use strict";
+
+var Bus = require('web.Bus');
+var config = require('web.config');
+var Class = require('web.Class');
+var QWeb = require('web.QWeb');
+var Registry = require('web.Registry');
+var translation = require('web.translation');
+
+/**
+ * Whether the client is currently in "debug" mode
+ *
+ * @type Boolean
+ */
+var bus = new Bus();
+
+_.each('click,dblclick,keydown,keypress,keyup'.split(','), function (evtype) {
+ $('html').on(evtype, function (ev) {
+ bus.trigger(evtype, ev);
+ });
+});
+_.each('resize,scroll'.split(','), function (evtype) {
+ $(window).on(evtype, function (ev) {
+ bus.trigger(evtype, ev);
+ });
+});
+
+return {
+ qweb: new QWeb(config.isDebug()),
+
+ // core classes and functions
+ Class: Class,
+ bus: bus,
+ main_bus: new Bus(),
+ _t: translation._t,
+ _lt: translation._lt,
+
+ // registries
+ action_registry: new Registry(),
+ crash_registry: new Registry(),
+ serviceRegistry: new Registry(),
+ /**
+ * @type {String}
+ */
+ csrf_token: odoo.csrf_token,
+};
+
+});
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
new file mode 100644
index 00000000..abd40e93
--- /dev/null
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -0,0 +1,412 @@
+odoo.define('web.ErrorDialogRegistry', function (require) {
+"use strict";
+
+var Registry = require('web.Registry');
+
+return new Registry();
+});
+
+odoo.define('web.CrashManager', function (require) {
+"use strict";
+
+const AbstractService = require('web.AbstractService');
+var ajax = require('web.ajax');
+const BrowserDetection = require('web.BrowserDetection');
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var ErrorDialogRegistry = require('web.ErrorDialogRegistry');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var _lt = core._lt;
+
+// Register this eventlistener before qunit does.
+// Some errors needs to be negated by the crash_manager.
+window.addEventListener('unhandledrejection', ev =>
+ core.bus.trigger('crash_manager_unhandledrejection', ev)
+);
+
+let active = true;
+
+/**
+ * An extension of Dialog Widget to render the warnings and errors on the website.
+ * Extend it with your template of choice like ErrorDialog/WarningDialog
+ */
+var CrashManagerDialog = Dialog.extend({
+ xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
+ ['/web/static/src/xml/crash_manager.xml']
+ ),
+
+ /**
+ * @param {Object} error
+ * @param {string} error.message the message in Warning/Error Dialog
+ * @param {string} error.traceback the traceback in ErrorDialog
+ *
+ * @constructor
+ */
+ init: function (parent, options, error) {
+ this._super.apply(this, [parent, options]);
+ this.message = error.message;
+ this.traceback = error.traceback;
+ core.bus.off('close_dialogs', this);
+ },
+});
+
+var ErrorDialog = CrashManagerDialog.extend({
+ template: 'CrashManager.error',
+});
+
+var WarningDialog = CrashManagerDialog.extend({
+ template: 'CrashManager.warning',
+
+ /**
+ * Sets size to medium by default.
+ *
+ * @override
+ */
+ init: function (parent, options, error) {
+ this._super(parent, _.extend({
+ size: 'medium',
+ }, options), error);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Focuses the ok button.
+ *
+ * @override
+ */
+ open: function () {
+ this._super({shouldFocusButtons: true});
+ },
+});
+
+var CrashManager = AbstractService.extend({
+ init: function () {
+ var self = this;
+ active = true;
+ this.isConnected = true;
+ this.odooExceptionTitleMap = {
+ 'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"),
+ 'odoo.exceptions.AccessDenied': _lt("Access Denied"),
+ 'odoo.exceptions.AccessError': _lt("Access Error"),
+ 'odoo.exceptions.MissingError': _lt("Missing Record"),
+ 'odoo.exceptions.UserError': _lt("User Error"),
+ 'odoo.exceptions.ValidationError': _lt("Validation Error"),
+ 'odoo.exceptions.Warning': _lt("Warning"),
+ };
+
+ this.browserDetection = new BrowserDetection();
+ this._super.apply(this, arguments);
+
+ // crash manager integration
+ core.bus.on('rpc_error', this, this.rpc_error);
+ window.onerror = function (message, file, line, col, error) {
+ // Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
+ // The browser will just give you a 'Script error.' as message and nothing else for security issue.
+ // To enable onerror to work properly with CORS file, you should:
+ // 1. add crossorigin="anonymous" to your <script> tag loading the file
+ // 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
+ // Since in some case it wont be possible to to this, this handle should have the possibility to be
+ // handled by the script manipulating the injected file. For this, you will use window.onOriginError
+ // If it is not handled, we should display something clearer than the common crash_manager error dialog
+ // since it won't show anything except "Script error."
+ // This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
+ if (!file && !line && !col) {
+ // Chrome and Opera set "Script error." on the `message` and hide the `error`
+ // Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
+ if (window.onOriginError) {
+ window.onOriginError();
+ delete window.onOriginError;
+ } else {
+ self.show_error({
+ type: _t("Odoo Client Error"),
+ message: _t("Unknown CORS error"),
+ data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
+ });
+ }
+ } else {
+ // ignore Chrome video internal error: https://crbug.com/809574
+ if (!error && message === 'ResizeObserver loop limit exceeded') {
+ return;
+ }
+ var traceback = error ? error.stack : '';
+ self.show_error({
+ type: _t("Odoo Client Error"),
+ message: message,
+ data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback},
+ });
+ }
+ };
+
+ // listen to unhandled rejected promises, and throw an error when the
+ // promise has been rejected due to a crash
+ core.bus.on('crash_manager_unhandledrejection', this, function (ev) {
+ if (ev.reason && ev.reason instanceof Error) {
+ // Error.prototype.stack is non-standard.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
+ // However, most engines provide an implementation.
+ // In particular, Chrome formats the contents of Error.stack
+ // https://v8.dev/docs/stack-trace-api#compatibility
+ let traceback;
+ if (self.browserDetection.isBrowserChrome()) {
+ traceback = ev.reason.stack;
+ } else {
+ traceback = `${_t("Error:")} ${ev.reason.message}\n${ev.reason.stack}`;
+ }
+ self.show_error({
+ type: _t("Odoo Client Error"),
+ message: '',
+ data: {debug: _t('Traceback:') + "\n" + traceback},
+ });
+ } else {
+ // the rejection is not due to an Error, so prevent the browser
+ // from displaying an 'unhandledrejection' error in the console
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ ev.preventDefault();
+ }
+ });
+ },
+ enable: function () {
+ active = true;
+ },
+ disable: function () {
+ active = false;
+ },
+ handleLostConnection: function () {
+ var self = this;
+ if (!this.isConnected) {
+ // already handled, nothing to do. This can happen when several
+ // rpcs are done in parallel and fail because of a lost connection.
+ return;
+ }
+ this.isConnected = false;
+ var delay = 2000;
+ core.bus.trigger('connection_lost');
+
+ setTimeout(function checkConnection() {
+ ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
+ core.bus.trigger('connection_restored');
+ self.isConnected = true;
+ }).guardedCatch(function () {
+ // exponential backoff, with some jitter
+ delay = (delay * 1.5) + 500*Math.random();
+ setTimeout(checkConnection, delay);
+ });
+ }, delay);
+ },
+ rpc_error: function(error) {
+ // Some qunit tests produces errors before the DOM is set.
+ // This produces an error loop as the modal/toast has no DOM to attach to.
+ if (!document.body || !active || this.connection_lost) return;
+
+ // Connection lost error
+ if (error.code === -32098) {
+ this.handleLostConnection();
+ return;
+ }
+
+ // Special exception handlers, see crash_registry bellow
+ var handler = core.crash_registry.get(error.data.name, true);
+ if (handler) {
+ new (handler)(this, error).display();
+ return;
+ }
+
+ // Odoo custom exception: UserError, AccessError, ...
+ if (_.has(this.odooExceptionTitleMap, error.data.name)) {
+ error = _.extend({}, error, {
+ data: _.extend({}, error.data, {
+ message: error.data.arguments[0],
+ title: this.odooExceptionTitleMap[error.data.name],
+ }),
+ });
+ this.show_warning(error);
+ return;
+ }
+
+ // Any other Python exception
+ this.show_error(error);
+ },
+ show_warning: function (error, options) {
+ if (!active) {
+ return;
+ }
+ var message = error.data ? error.data.message : error.message;
+ var title = _t("Something went wrong !");
+ if (error.type) {
+ title = _.str.capitalize(error.type);
+ } else if (error.data && error.data.title) {
+ title = _.str.capitalize(error.data.title);
+ }
+ return this._displayWarning(message, title, options);
+ },
+ show_error: function (error) {
+ if (!active) {
+ return;
+ }
+ error.traceback = error.data.debug;
+ var dialogClass = error.data.context && ErrorDialogRegistry.get(error.data.context.exception_class) || ErrorDialog;
+ var dialog = new dialogClass(this, {
+ title: _.str.capitalize(error.type) || _t("Odoo Error"),
+ }, error);
+
+
+ // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
+ var $clipboardBtn;
+ var clipboard;
+ dialog.opened(function () {
+ // When the full traceback is shown, scroll it to the end (useful for better python error reporting)
+ dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) {
+ e.target.scrollTop = e.target.scrollHeight;
+ });
+
+ $clipboardBtn = dialog.$(".o_clipboard_button");
+ $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
+ clipboard = new window.ClipboardJS($clipboardBtn[0], {
+ text: function () {
+ return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim();
+ },
+ // Container added because of Bootstrap modal that give the focus to another element.
+ // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
+ // https://github.com/zenorocha/clipboard.js/issues/155
+ container: dialog.el,
+ });
+ clipboard.on("success", function (e) {
+ _.defer(function () {
+ $clipboardBtn.tooltip("show");
+ _.delay(function () {
+ $clipboardBtn.tooltip("hide");
+ }, 800);
+ });
+ });
+ });
+ dialog.on("closed", this, function () {
+ $clipboardBtn.tooltip('dispose');
+ clipboard.destroy();
+ });
+
+ return dialog.open();
+ },
+ show_message: function(exception) {
+ return this.show_error({
+ type: _t("Odoo Client Error"),
+ message: exception,
+ data: {debug: ""}
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} message
+ * @param {string} title
+ * @param {Object} options
+ */
+ _displayWarning: function (message, title, options) {
+ return new WarningDialog(this, Object.assign({}, options, {
+ title,
+ }), {
+ message,
+ }).open();
+ },
+});
+
+/**
+ * An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
+*/
+var ExceptionHandler = {
+ /**
+ * @param parent The parent.
+ * @param error The error object as returned by the JSON-RPC implementation.
+ */
+ init: function(parent, error) {},
+ /**
+ * Called to inform to display the widget, if necessary. A typical way would be to implement
+ * this interface in a class extending instance.web.Dialog and simply display the dialog in this
+ * method.
+ */
+ display: function() {},
+};
+
+
+/**
+ * Handle redirection warnings, which behave more or less like a regular
+ * warning, with an additional redirection button.
+ */
+var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
+ init: function(parent, error) {
+ this._super(parent);
+ this.error = error;
+ },
+ display: function() {
+ var self = this;
+ var error = this.error;
+ var additional_context = _.extend({}, this.context, error.data.arguments[3]);
+
+ new WarningDialog(this, {
+ title: _.str.capitalize(error.type) || _t("Odoo Warning"),
+ buttons: [
+ {text: error.data.arguments[2], classes : "btn-primary", click: function() {
+ self.do_action(
+ error.data.arguments[1],
+ {
+ additional_context: additional_context,
+ });
+ self.destroy();
+ }, close: true},
+ {text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
+ ]
+ }, {
+ message: error.data.arguments[0],
+ }).open();
+ }
+});
+
+core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
+
+function session_expired(cm) {
+ return {
+ display: function () {
+ const notif = {
+ type: _t("Odoo Session Expired"),
+ message: _t("Your Odoo session expired. The current page is about to be refreshed."),
+ };
+ const options = {
+ buttons: [{
+ text: _t("Ok"),
+ click: () => window.location.reload(true),
+ close: true
+ }],
+ };
+ cm.show_warning(notif, options);
+ }
+ };
+}
+core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
+core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
+
+core.crash_registry.add('504', function (cm) {
+ return {
+ display: function () {
+ cm.show_warning({
+ type: _t("Request timeout"),
+ message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
+ }
+ };
+});
+
+return {
+ CrashManager: CrashManager,
+ ErrorDialog: ErrorDialog,
+ WarningDialog: WarningDialog,
+ disable: () => active = false,
+};
+});
diff --git a/addons/web/static/src/js/services/crash_manager_service.js b/addons/web/static/src/js/services/crash_manager_service.js
new file mode 100644
index 00000000..44182561
--- /dev/null
+++ b/addons/web/static/src/js/services/crash_manager_service.js
@@ -0,0 +1,9 @@
+odoo.define('crash_manager.service', function (require) {
+'use strict';
+
+const core = require('web.core');
+const CrashManager = require('web.CrashManager').CrashManager;
+
+core.serviceRegistry.add('crash_manager', CrashManager);
+
+});
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
new file mode 100644
index 00000000..f5bb482a
--- /dev/null
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -0,0 +1,225 @@
+odoo.define('web.DataManager', function (require) {
+"use strict";
+
+var config = require('web.config');
+var core = require('web.core');
+var rpc = require('web.rpc');
+var session = require('web.session');
+var utils = require('web.utils');
+
+return core.Class.extend({
+ init: function () {
+ this._init_cache();
+ core.bus.on('clear_cache', this, this.invalidate.bind(this));
+ },
+
+ _init_cache: function () {
+ this._cache = {
+ actions: {},
+ filters: {},
+ views: {},
+ };
+ },
+
+ /**
+ * Invalidates the whole cache
+ * Suggestion: could be refined to invalidate some part of the cache
+ */
+ invalidate: function () {
+ session.invalidateCacheKey('load_menus');
+ this._init_cache();
+ },
+
+ /**
+ * Loads an action from its id or xmlid.
+ *
+ * @param {int|string} [action_id] the action id or xmlid
+ * @param {Object} [additional_context] used to load the action
+ * @return {Promise} resolved with the action whose id or xmlid is action_id
+ */
+ load_action: function (action_id, additional_context) {
+ var self = this;
+ var key = this._gen_key(action_id, additional_context || {});
+
+ if (config.isDebug('assets') || !this._cache.actions[key]) {
+ this._cache.actions[key] = rpc.query({
+ route: "/web/action/load",
+ params: {
+ action_id: action_id,
+ additional_context: additional_context,
+ },
+ }).then(function (action) {
+ self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
+ return action;
+ }).guardedCatch(() => this._invalidate('actions', key));
+ }
+
+ return this._cache.actions[key].then(function (action) {
+ return $.extend(true, {}, action);
+ });
+ },
+
+ /**
+ * Loads various information concerning views: fields_view for each view,
+ * the fields of the corresponding model, and optionally the filters.
+ *
+ * @param {Object} params
+ * @param {String} params.model
+ * @param {Object} params.context
+ * @param {Array} params.views_descr array of [view_id, view_type]
+ * @param {Object} [options={}] dictionary of various options:
+ * - options.load_filters: whether or not to load the filters,
+ * - options.action_id: the action_id (required to load filters),
+ * - options.toolbar: whether or not a toolbar will be displayed,
+ * @return {Promise} resolved with the requested views information
+ */
+ load_views: async function ({ model, context, views_descr } , options = {}) {
+ const viewsKey = this._gen_key(model, views_descr, options, context);
+ const filtersKey = this._gen_key(model, options.action_id);
+ const withFilters = Boolean(options.load_filters);
+ const shouldLoadViews = config.isDebug('assets') || !this._cache.views[viewsKey];
+ const shouldLoadFilters = config.isDebug('assets') || (
+ withFilters && !this._cache.filters[filtersKey]
+ );
+ if (shouldLoadViews) {
+ // Views info should be loaded
+ options.load_filters = shouldLoadFilters;
+ this._cache.views[viewsKey] = rpc.query({
+ args: [],
+ kwargs: { context, options, views: views_descr },
+ model,
+ method: 'load_views',
+ }).then(result => {
+ // Freeze the fields dict as it will be shared between views and
+ // no one should edit it
+ utils.deepFreeze(result.fields);
+ for (const [viewId, viewType] of views_descr) {
+ const fvg = result.fields_views[viewType];
+ fvg.viewFields = fvg.fields;
+ fvg.fields = result.fields;
+ }
+
+ // Insert filters, if any, into the filters cache
+ if (shouldLoadFilters) {
+ this._cache.filters[filtersKey] = Promise.resolve(result.filters);
+ }
+ return result.fields_views;
+ }).guardedCatch(() => this._invalidate('views', viewsKey));
+ }
+ const result = await this._cache.views[viewsKey];
+ if (withFilters && result.search) {
+ if (shouldLoadFilters) {
+ await this.load_filters({
+ actionId: options.action_id,
+ context,
+ forceReload: false,
+ modelName: model,
+ });
+ }
+ result.search.favoriteFilters = await this._cache.filters[filtersKey];
+ }
+ return result;
+ },
+
+ /**
+ * Loads the filters of a given model and optional action id.
+ *
+ * @param {Object} params
+ * @param {number} params.actionId
+ * @param {Object} params.context
+ * @param {boolean} [params.forceReload=true] can be set to false to prevent forceReload
+ * @param {string} params.modelName
+ * @return {Promise} resolved with the requested filters
+ */
+ load_filters: function (params) {
+ const key = this._gen_key(params.modelName, params.actionId);
+ const forceReload = params.forceReload !== false && config.isDebug('assets');
+ if (forceReload || !this._cache.filters[key]) {
+ this._cache.filters[key] = rpc.query({
+ args: [params.modelName, params.actionId],
+ kwargs: {
+ context: params.context || {},
+ // get_context() de dataset
+ },
+ model: 'ir.filters',
+ method: 'get_filters',
+ }).guardedCatch(() => this._invalidate('filters', key));
+ }
+ return this._cache.filters[key];
+ },
+
+ /**
+ * Calls 'create_or_replace' on 'ir_filters'.
+ *
+ * @param {Object} [filter] the filter description
+ * @return {Promise} resolved with the id of the created or replaced filter
+ */
+ create_filter: function (filter) {
+ return rpc.query({
+ args: [filter],
+ model: 'ir.filters',
+ method: 'create_or_replace',
+ })
+ .then(filterId => {
+ const filtersKey = this._gen_key(filter.model_id, filter.action_id);
+ this._invalidate('filters', filtersKey);
+ return filterId;
+ });
+ },
+
+ /**
+ * Calls 'unlink' on 'ir_filters'.
+ *
+ * @param {integer} filterId Id of the filter to remove
+ * @return {Promise}
+ */
+ delete_filter: function (filterId) {
+ return rpc.query({
+ args: [filterId],
+ model: 'ir.filters',
+ method: 'unlink',
+ })
+ // Invalidate the whole cache since we have no idea where the filter came from.
+ .then(() => this._invalidate('filters'));
+ },
+
+ /**
+ * Private function that generates a cache key from its arguments
+ */
+ _gen_key: function () {
+ return _.map(Array.prototype.slice.call(arguments), function (arg) {
+ if (!arg) {
+ return false;
+ }
+ return _.isObject(arg) ? JSON.stringify(arg) : arg;
+ }).join(',');
+ },
+
+ /**
+ * Invalidate a cache entry or a whole cache section.
+ *
+ * @private
+ * @param {string} section
+ * @param {string} key
+ */
+ _invalidate(section, key) {
+ if (key) {
+ delete this._cache[section][key];
+ } else {
+ this._cache[section] = {};
+ }
+ },
+});
+
+});
+
+odoo.define('web.data_manager', function (require) {
+"use strict";
+
+var DataManager = require('web.DataManager');
+
+var data_manager = new DataManager();
+
+return data_manager;
+
+});
diff --git a/addons/web/static/src/js/services/local_storage_service.js b/addons/web/static/src/js/services/local_storage_service.js
new file mode 100644
index 00000000..694dc6da
--- /dev/null
+++ b/addons/web/static/src/js/services/local_storage_service.js
@@ -0,0 +1,20 @@
+odoo.define('web.LocalStorageService', function (require) {
+'use strict';
+
+/**
+ * This module defines a service to access the localStorage object.
+ */
+
+var AbstractStorageService = require('web.AbstractStorageService');
+var core = require('web.core');
+var localStorage = require('web.local_storage');
+
+var LocalStorageService = AbstractStorageService.extend({
+ storage: localStorage,
+});
+
+core.serviceRegistry.add('local_storage', LocalStorageService);
+
+return LocalStorageService;
+
+});
diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js
new file mode 100644
index 00000000..295b49d7
--- /dev/null
+++ b/addons/web/static/src/js/services/notification_service.js
@@ -0,0 +1,111 @@
+odoo.define('web.NotificationService', function (require) {
+'use strict';
+
+var AbstractService = require('web.AbstractService');
+var Notification = require('web.Notification');
+var core = require('web.core');
+
+var id = 0;
+
+/**
+ * Notification Service
+ *
+ * The Notification Service is simply a service used to display notifications in
+ * the top/right part of the screen.
+ *
+ * If you want to display such a notification, you probably do not want to do it
+ * by using this file. The proper way is to use the do_warn or do_notify
+ * methods on the Widget class.
+ */
+var NotificationService = AbstractService.extend({
+ custom_events: {
+ close: '_onCloseNotification',
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ this._super.apply(this, arguments);
+ this.notifications = {};
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * It may sometimes be useful to close programmatically a notification. For
+ * example, when there is a sticky notification warning the user about some
+ * condition (connection lost), but the condition does not apply anymore.
+ *
+ * @param {number} notificationId
+ * @param {boolean} [silent=false] if true, the notification does not call
+ * onClose callback
+ */
+ close: function (notificationId, silent) {
+ var notification = this.notifications[notificationId];
+ if (!notification) {
+ return;
+ }
+ notification.close(silent);
+ },
+ /**
+ * Display a notification at the appropriate location, and returns the
+ * reference id to the same widget.
+ *
+ * Note that this method does not wait for the appendTo method to complete.
+ *
+ * @param {Object} params
+ * @param {function} [params.Notification] javascript class of a notification
+ * to instantiate by default use 'web.Notification'
+ * @param {string} params.title notification title
+ * @param {string} params.subtitle notification subtitle
+ * @param {string} params.message notification main message
+ * @param {string} params.type 'notification' or 'warning'
+ * @param {boolean} [params.sticky=false] if true, the notification will stay
+ * visible until the user clicks on it.
+ * @param {string} [params.className] className to add on the dom
+ * @param {function} [params.onClose] callback when the user click on the x
+ * or when the notification is auto close (no sticky)
+ * @param {Object[]} params.buttons
+ * @param {function} params.buttons[0].click callback on click
+ * @param {Boolean} [params.buttons[0].primary] display the button as primary
+ * @param {string} [params.buttons[0].text] button label
+ * @param {string} [params.buttons[0].icon] font-awsome className or image src
+ * @returns {Number} notification id
+ */
+ notify: function (params) {
+ if (!this.$el) {
+ this.$el = $('<div class="o_notification_manager"/>');
+ this.$el.prependTo('body');
+ }
+ var NotificationWidget = params.Notification || Notification;
+ var notification = this.notifications[++id] = new NotificationWidget(this, params);
+ notification.appendTo(this.$el);
+ return id;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onCloseNotification: function (ev) {
+ ev.stopPropagation();
+ for (var notificationId in this.notifications) {
+ if (this.notifications[notificationId] === ev.target) {
+ delete this.notifications[notificationId];
+ break;
+ }
+ }
+ },
+});
+
+core.serviceRegistry.add('notification', NotificationService);
+
+return NotificationService;
+});
diff --git a/addons/web/static/src/js/services/report_service.js b/addons/web/static/src/js/services/report_service.js
new file mode 100644
index 00000000..ed4907a6
--- /dev/null
+++ b/addons/web/static/src/js/services/report_service.js
@@ -0,0 +1,35 @@
+odoo.define('web.ReportService', function (require) {
+"use strict";
+
+/**
+ * This file defines the service for the report generation in Odoo.
+ */
+
+var AbstractService = require('web.AbstractService');
+var core = require('web.core');
+
+var ReportService = AbstractService.extend({
+ dependencies: ['ajax'],
+
+ /**
+ * Checks the state of the installation of wkhtmltopdf on the server.
+ * Implements an internal cache to do the request only once.
+ *
+ * @returns {Promise} resolved with the state of wkhtmltopdf on the server
+ * (possible values are 'ok', 'broken', 'install', 'upgrade', 'workers').
+ */
+ checkWkhtmltopdf: function () {
+ if (!this.wkhtmltopdfState) {
+ this.wkhtmltopdfState = this._rpc({
+ route:'/report/check_wkhtmltopdf'
+ });
+ }
+ return this.wkhtmltopdfState;
+ },
+});
+
+core.serviceRegistry.add('report', ReportService);
+
+return ReportService;
+
+});
diff --git a/addons/web/static/src/js/services/session.js b/addons/web/static/src/js/services/session.js
new file mode 100644
index 00000000..35720f81
--- /dev/null
+++ b/addons/web/static/src/js/services/session.js
@@ -0,0 +1,12 @@
+odoo.define('web.session', function (require) {
+"use strict";
+
+var Session = require('web.Session');
+var modules = odoo._modules;
+
+var session = new Session(undefined, undefined, {modules: modules, use_cors: false});
+session.is_bound = session.session_bind();
+
+return session;
+
+});
diff --git a/addons/web/static/src/js/services/session_storage_service.js b/addons/web/static/src/js/services/session_storage_service.js
new file mode 100644
index 00000000..41c47af3
--- /dev/null
+++ b/addons/web/static/src/js/services/session_storage_service.js
@@ -0,0 +1,20 @@
+odoo.define('web.SessionStorageService', function (require) {
+'use strict';
+
+/**
+ * This module defines a service to access the sessionStorage object.
+ */
+
+var AbstractStorageService = require('web.AbstractStorageService');
+var core = require('web.core');
+var sessionStorage = require('web.sessionStorage');
+
+var SessionStorageService = AbstractStorageService.extend({
+ storage: sessionStorage,
+});
+
+core.serviceRegistry.add('session_storage', SessionStorageService);
+
+return SessionStorageService;
+
+});