diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index dbf6c2fec53..da2db6041db 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -6,7 +6,7 @@ import werkzeug from odoo import http, _ from odoo.addons.auth_signup.models.res_users import SignupError from odoo.addons.web.controllers.main import ensure_db, Home -from odoo.addons.web_settings_dashboard.controllers.main import WebSettingsDashboard as Dashboard +from odoo.addons.base_setup.controllers.main import BaseSetup from odoo.exceptions import UserError from odoo.http import request @@ -136,9 +136,9 @@ class AuthSignupHome(Home): if not uid: raise SignupError(_('Authentication Failed.')) -class WebSettingsDashboard(Dashboard): - @http.route('/web_settings_dashboard/data', type='json', auth='user') - def web_settings_dashboard_data(self, **kw): - res = super(WebSettingsDashboard, self).web_settings_dashboard_data(**kw) - res['users_info'].update({'resend_invitation': True}) +class AuthBaseSetup(BaseSetup): + @http.route('/base_setup/data', type='json', auth='user') + def base_setup_data(self, **kwargs): + res = super().base_setup_data(**kwargs) + res.update({'resend_invitation': True}) return res diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index d2f40693f24..6564129253a 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -222,10 +222,10 @@ class ResUsers(models.Model): template.send_mail(user, notif_layout='mail.mail_notification_light', force_send=False) @api.model - def web_dashboard_create_users(self, emails): + def web_create_users(self, emails): inactive_users = self.search([('state', '=', 'new'), '|', ('login', 'in', emails), ('email', 'in', emails)]) new_emails = set(emails) - set(inactive_users.mapped('email')) - res = super(ResUsers, self).web_dashboard_create_users(list(new_emails)) + res = super(ResUsers, self).web_create_users(list(new_emails)) if inactive_users: inactive_users.with_context(create_user=True).action_reset_password() return res diff --git a/addons/base_setup/__init__.py b/addons/base_setup/__init__.py index dc5e6b693d1..373b1d07b83 100644 --- a/addons/base_setup/__init__.py +++ b/addons/base_setup/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import models +from . import models, controllers diff --git a/addons/base_setup/__manifest__.py b/addons/base_setup/__manifest__.py index df9fb1ec1a8..1afdc984581 100644 --- a/addons/base_setup/__manifest__.py +++ b/addons/base_setup/__manifest__.py @@ -16,8 +16,17 @@ Shows you a list of applications features to install from. 'data/base_setup_data.xml', 'views/res_config_settings_views.xml', 'views/res_partner_views.xml', + 'views/assets.xml', ], 'demo': [], 'installable': True, 'auto_install': False, + + 'qweb': [ + 'static/src/xml/res_config_dev_tool.xml', + 'static/src/xml/res_config_edition.xml', + 'static/src/xml/res_config_invite_users.xml', + ], + + } diff --git a/addons/base_setup/controllers/__init__.py b/addons/base_setup/controllers/__init__.py new file mode 100644 index 00000000000..757b12a1f17 --- /dev/null +++ b/addons/base_setup/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/addons/base_setup/controllers/main.py b/addons/base_setup/controllers/main.py new file mode 100644 index 00000000000..317cb6bd754 --- /dev/null +++ b/addons/base_setup/controllers/main.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, http +from odoo.exceptions import AccessError +from odoo.http import request + + +class BaseSetup(http.Controller): + @http.route('/base_setup/data', type='json', auth='user') + def base_setup_data(self, **kw): + if not request.env.user.has_group('base.group_erp_manager'): + raise AccessError(_("Access Denied")) + + cr = request.cr + cr.execute(""" + SELECT count(*) + FROM res_users + WHERE active=true AND + share=false + """) + active_count = cr.dictfetchall()[0].get('count') + + cr.execute(""" + SELECT count(u.*) + FROM res_users u + WHERE active=true AND + share=false AND + NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id) + """) + pending_count = cr.dictfetchall()[0].get('count') + + cr.execute(""" + SELECT id, login + FROM res_users u + WHERE active=true AND + share=false AND + NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id) + ORDER BY id desc + LIMIT 10 + """) + pending_users = cr.fetchall() + + return { + 'active_users': active_count, + 'pending_count': pending_count, + 'pending_users': pending_users, + } + + @http.route('/base_setup/demo_active', type='json', auth='user') + def base_setup_is_demo(self, **kwargs): + # We assume that if there's at least one module with demo data active, then the db was + # initialized with demo=True or it has been force-activated by the `Load demo data` button + # in the settings dashboard. + demo_active = bool(request.env['ir.module.module'].search_count([('demo', '=', True)])) + return demo_active diff --git a/addons/base_setup/models/__init__.py b/addons/base_setup/models/__init__.py index ecb5f3ba7ea..ce534721577 100644 --- a/addons/base_setup/models/__init__.py +++ b/addons/base_setup/models/__init__.py @@ -3,4 +3,4 @@ from . import ir_http from . import res_config_settings - +from . import res_users diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index 9cd9289a929..a3f9cb1ec84 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -44,13 +44,15 @@ class ResConfigSettings(models.TransientModel): paperformat_id = fields.Many2one(related="company_id.paperformat_id", string='Paper format', readonly=False) external_report_layout_id = fields.Many2one(related="company_id.external_report_layout_id", readonly=False) show_effect = fields.Boolean(string="Show Effect", config_parameter='base_setup.show_effect') + company_count = fields.Integer('Number of Companies', readonly=True, compute="_compute_company_count") + language_count = fields.Integer('Number of Languages', compute="_compute_language_count") @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() - res.update( - company_share_partner=not self.env.ref('base.res_partner_rule').active, - ) + res.update({ + 'company_share_partner': not self.env.ref('base.res_partner_rule').active, + }) return res def set_values(self): @@ -100,3 +102,19 @@ class ResConfigSettings(models.TransientModel): 'view_id': template.id, 'target': 'new', } + + # NOTE: These fields depend on the context, if we want them to be computed + # we have to make them depend on a field. This is because we are on a TransientModel. + @api.depends('company_id') + def _compute_company_count(self): + company_count = self.env['res.company'].sudo().search_count([]) + for record in self: + record.company_count = company_count + + @api.depends('company_id') + def _compute_language_count(self): + language_count = self.env['res.lang'].search_count([ + ('active', '=', True), + ]) + for record in self: + record.language_count = language_count diff --git a/addons/web_settings_dashboard/models/res_users.py b/addons/base_setup/models/res_users.py similarity index 94% rename from addons/web_settings_dashboard/models/res_users.py rename to addons/base_setup/models/res_users.py index 3c83072de6f..112c55c188a 100644 --- a/addons/web_settings_dashboard/models/res_users.py +++ b/addons/base_setup/models/res_users.py @@ -8,7 +8,7 @@ class ResUsers(models.Model): _inherit = 'res.users' @api.model - def web_dashboard_create_users(self, emails): + def web_create_users(self, emails): # Reactivate already existing users if needed deactivated_users = self.with_context(active_test=False).search([('active', '=', False), '|', ('login', 'in', emails), ('email', 'in', emails)]) diff --git a/addons/base_setup/static/src/img/app_store.png b/addons/base_setup/static/src/img/app_store.png new file mode 100644 index 00000000000..65320714c06 Binary files /dev/null and b/addons/base_setup/static/src/img/app_store.png differ diff --git a/addons/base_setup/static/src/img/google_play.png b/addons/base_setup/static/src/img/google_play.png new file mode 100644 index 00000000000..229d939d728 Binary files /dev/null and b/addons/base_setup/static/src/img/google_play.png differ diff --git a/addons/base_setup/static/src/js/res_config_dev_tool.js b/addons/base_setup/static/src/js/res_config_dev_tool.js new file mode 100644 index 00000000000..b407a731a35 --- /dev/null +++ b/addons/base_setup/static/src/js/res_config_dev_tool.js @@ -0,0 +1,31 @@ +odoo.define('base_setup.ResConfigDevTool', function (require) { + "use strict"; + + var config = require('web.config'); + var Widget = require('web.Widget'); + var widget_registry = require('web.widget_registry'); + + var ResConfigDevTool = Widget.extend({ + template: 'res_config_dev_tool', + + init: function () { + this._super.apply(this, arguments); + this.isDebug = config.isDebug(); + this.isAssets = config.isDebug("assets"); + this.isTests = config.isDebug("tests"); + }, + + willStart: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + return self._rpc({ + route: '/base_setup/demo_active', + }).then(function (demo_active) { + self.demo_active = demo_active; + }); + }); + }, + }); + + widget_registry.add('res_config_dev_tool', ResConfigDevTool); +}); diff --git a/addons/base_setup/static/src/js/res_config_edition.js b/addons/base_setup/static/src/js/res_config_edition.js new file mode 100644 index 00000000000..bdc3f434f88 --- /dev/null +++ b/addons/base_setup/static/src/js/res_config_edition.js @@ -0,0 +1,21 @@ +odoo.define('base_setup.ResConfigEdition', function (require) { + "use strict"; + + var Widget = require('web.Widget'); + var widget_registry = require('web.widget_registry'); + var session = require ('web.session'); + + var ResConfigEdition = Widget.extend({ + template: 'res_config_edition', + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.server_version = session.server_version; + }, + }); + + widget_registry.add('res_config_edition', ResConfigEdition); +}); diff --git a/addons/base_setup/static/src/js/res_config_invite_users.js b/addons/base_setup/static/src/js/res_config_invite_users.js new file mode 100644 index 00000000000..653a130868c --- /dev/null +++ b/addons/base_setup/static/src/js/res_config_invite_users.js @@ -0,0 +1,214 @@ +odoo.define('base_setup.ResConfigInviteUsers', function (require) { + "use strict"; + + var Widget = require('web.Widget'); + var widget_registry = require('web.widget_registry'); + var core = require('web.core'); + var framework = require('web.framework'); + + var QWeb = core.qweb; + var _t = core._t; + + var ResConfigInviteUsers = Widget.extend({ + template: 'res_config_invite_users', + + events: { + 'click .o_web_settings_invite': '_onClickInvite', + 'click .o_web_settings_access_rights': '_onClickAccessRights', + 'click .o_web_settings_user': '_onClickUser', + 'click .o_web_settings_more': '_onClickMore', + 'click .o_badge_remove': '_onClickBadgeRemove', + 'keydown .o_user_emails': '_onKeydownUserEmails', + }, + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.emails = []; + }, + + willStart: function () { + var self = this; + + return this._super.apply(this, arguments).then(function () { + return self.load(); + }); + }, + + load: function () { + var self = this; + + return this._rpc({ + route: '/base_setup/data', + }).then(function (data) { + self.active_users = data.active_users; + self.pending_users = data.pending_users; + self.pending_count = data.pending_count; + self.resend_invitation = data.resend_invitation || false; + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Creates and appends badges for valid and unique email addresses + * + * @private + */ + _createBadges: function () { + var $userEmails = this.$('.o_user_emails'); + var value = $userEmails.val().trim(); + if (value) { + // filter out duplicates + var emails = _.uniq(value.split(/[ ,;\n]+/)); + + // filter out invalid email addresses + var invalidEmails = _.reject(emails, this._validateEmail); + if (invalidEmails.length) { + this.do_warn(_.str.sprintf(_t('The following email addresses are invalid: %s.'), invalidEmails.join(', '))); + } + emails = _.difference(emails, invalidEmails); + + if (!this.resend_invitation) { + // filter out already processed or pending addresses + var pendingEmails = _.map(this.pending_users, function (info) { + return info[1]; + }); + var existingEmails = _.intersection(emails, this.emails.concat(pendingEmails)); + if (existingEmails.length) { + this.do_warn(_.str.sprintf(_t('The following email addresses already exist: %s.'), existingEmails.join(', '))); + } + emails = _.difference(emails, existingEmails); + } + + // add valid email addresses, if any + if (emails.length) { + this.emails = this.emails.concat(emails); + this.$('.o_web_settings_invitation_form').append(QWeb.render('EmailBadge', {emails: emails})); + $userEmails.val(''); + } + } + }, + /** + * Removes a given badge from the DOM, and its associated email address + * + * @private + * @param {jQueryElement} $badge + */ + _removeBadge: function ($badge) { + var email = $badge.text().trim(); + this.emails = _.without(this.emails, email); + $badge.remove(); + }, + /** + * @private + * @param {string} email + * @returns {boolean} true if the given email address is valid + */ + _validateEmail: function (email) { + var re = /^([a-z0-9][-a-z0-9_\+\.]*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,63}(?:\.[a-z]{2})?)$/i; + return re.test(email); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAccessRights: function (e) { + e.preventDefault(); + this.do_action('base.action_res_users'); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickBadgeRemove: function (ev) { + var $badge = $(ev.target).closest('.badge'); + this._removeBadge($badge); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickInvite: function (ev) { + var self = this; + this._createBadges(); + if (this.emails.length) { + var $button = $(ev.target); + $button.button('loading'); + this._rpc({ + model: 'res.users', + method: 'web_create_users', + args: [this.emails], + }).then(function () { + return self.load().then(function () { + self.renderElement(); + }); + }).guardedCatch(function () { + // NOTE Not sure that this is needed + $button.button('reset'); + }); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMore: function(ev) { + ev.preventDefault(); + this.do_action({ + name: _t('Users'), + type: 'ir.actions.act_window', + view_mode: 'tree,form', + res_model: 'res.users', + domain: [['log_ids', '=', false]], + context: { + search_default_no_share: true, + }, + views: [[false, 'list'], [false, 'form']], + }); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickUser: function (ev) { + ev.preventDefault(); + var user_id = $(e.currentTarget).data('user-id'); + this.do_action({ + type: 'ir.actions.act_window', + res_model: 'res.users', + view_mode: 'form', + views: [[false, 'form']], + res_id: user_id, + }); + }, + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydownUserEmails: function (ev) { + var $userEmails = this.$('.o_user_emails'); + var keyCodes = [$.ui.keyCode.TAB, $.ui.keyCode.COMMA, $.ui.keyCode.ENTER, $.ui.keyCode.SPACE]; + if (_.contains(keyCodes, ev.which)) { + ev.preventDefault(); + this._createBadges(); + } + // remove last badge on backspace + if (ev.which === $.ui.keyCode.BACKSPACE && this.emails.length && !$userEmails.val()) { + this._removeBadge(this.$('.o_web_settings_invitation_form .badge:last')); + } + }, + }); + + widget_registry.add('res_config_invite_users', ResConfigInviteUsers); + +}); diff --git a/addons/web_settings_dashboard/static/src/scss/dashboard.scss b/addons/base_setup/static/src/scss/settings.scss similarity index 65% rename from addons/web_settings_dashboard/static/src/scss/dashboard.scss rename to addons/base_setup/static/src/scss/settings.scss index 82f2a1e09d2..2f385a3ad10 100644 --- a/addons/web_settings_dashboard/static/src/scss/dashboard.scss +++ b/addons/base_setup/static/src/scss/settings.scss @@ -1,8 +1,8 @@ -.o_web_settings_dashboard { +.o_web_settings { $bg-dashboard-col: #FFF; - @mixin o-web-settings-dashboard-card { + @mixin o-web-settings-card-card { box-shadow: 0 1px 2px 0 rgba(0,0,0,.14); background: $bg-dashboard-col; border-radius: 2px; @@ -11,30 +11,30 @@ background: #F7F7F7; height: 100%; - .o_web_settings_dashboard_col { + .o_web_settings_col { > div > hr { margin: 12px -18px 15px -18px; border-color: #CBCBCB; } > div { - @include o-web-settings-dashboard-card; + @include o-web-settings-card-card; padding: 25px 18px 18px 18px; - .o_web_settings_dashboard_header { + .o_web_settings_header { font-size: 22px; margin-bottom: 8px; } } - .btn.o_browse_apps, .btn.o_web_settings_dashboard_invitations { + .btn.o_browse_apps, .btn.o_web_settings_invitations { margin-top: 12px; } - .o_web_settings_dashboard_compact_subtitle { + .o_web_settings_compact_subtitle { line-height: 12px; display: block; } - .o_web_settings_dashboard_invitations { - .o_web_settings_dashboard_invitation_form { + .o_web_settings_invitations { + .o_web_settings_invitation_form { text-align: left; > textarea { resize: vertical; @@ -42,22 +42,22 @@ } } } - .o_web_settings_dashboard_planner { + .o_web_settings_planner { .progress { height: 10px; background-color: darken($bg-dashboard-col, 15%); cursor: pointer; margin-bottom: 30px; } - .o_web_settings_dashboard_planner_progress_text { + .o_web_settings_planner_progress_text { font-size: 18px; padding-right: 0px; padding-top: 3px; } - .o_web_settings_dashboard_planner_progress_bar { + .o_web_settings_planner_progress_bar { padding-left: 10px; } - .o_web_settings_dashboard_progress_title { + .o_web_settings_progress_title { font-weight: bold; cursor: pointer; color: $o-brand-primary; diff --git a/addons/base_setup/static/src/xml/res_config_dev_tool.xml b/addons/base_setup/static/src/xml/res_config_dev_tool.xml new file mode 100644 index 00000000000..ab108bd2518 --- /dev/null +++ b/addons/base_setup/static/src/xml/res_config_dev_tool.xml @@ -0,0 +1,19 @@ + + diff --git a/addons/base_setup/static/src/xml/res_config_edition.xml b/addons/base_setup/static/src/xml/res_config_edition.xml new file mode 100644 index 00000000000..31485c33ab9 --- /dev/null +++ b/addons/base_setup/static/src/xml/res_config_edition.xml @@ -0,0 +1,24 @@ + + + + diff --git a/addons/base_setup/static/src/xml/res_config_invite_users.xml b/addons/base_setup/static/src/xml/res_config_invite_users.xml new file mode 100644 index 00000000000..ba9bd5fbc7e --- /dev/null +++ b/addons/base_setup/static/src/xml/res_config_invite_users.xml @@ -0,0 +1,54 @@ + + +
+
+
+
+ +
+ + Active User + Active Users +
+
+
+ + + Manage Users + +
+
+
+
+ +

Pending Invitations:

+ + + + +
+ more +
+
+
+
+
+
+ Invite New Users: + + + +
+
+
+
+ + + + + + + + + +
diff --git a/addons/base_setup/views/assets.xml b/addons/base_setup/views/assets.xml new file mode 100644 index 00000000000..340e4a52445 --- /dev/null +++ b/addons/base_setup/views/assets.xml @@ -0,0 +1,10 @@ + + + + diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index 5acfd343678..2398075bf0e 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -3,11 +3,72 @@ res.config.settings.view.form.inherit.base.setup res.config.settings - +
+
+

About

+
+
+
+

Users

+ +
+ +
+

Companies

+
+
+
+ +
+ + + companies + +
+
+
+
+
+ +
+

Languages

+
+
+
+ +
+ + + Languages + +
+
+
+
+
+

Business Documents

@@ -48,6 +109,13 @@
+ +
+

Statistics

+
+
+
+

Contacts

@@ -72,55 +140,55 @@ Send texts to your contacts
+ + + +
+

Users

+
+
+
+ +
+
+
-
-

Users

-
-
-
- -
-
-