Note: If you did not ask for a password reset, you can safely ignore this email.
]]>
-
-
- auth.reset_password.form
- auth.reset_password
-
-
-
-
-
-
- Reset Password
- ir.actions.act_window
- auth.reset_password
- form
- form
- new
-
-
-
diff --git a/addons/auth_reset_password/controllers/__init__.py b/addons/auth_reset_password/controllers/__init__.py
new file mode 100644
index 00000000000..f40f0ee976e
--- /dev/null
+++ b/addons/auth_reset_password/controllers/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import main
+
+# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/auth_reset_password/controllers/main.py b/addons/auth_reset_password/controllers/main.py
new file mode 100644
index 00000000000..5185a65c3ce
--- /dev/null
+++ b/addons/auth_reset_password/controllers/main.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.modules.registry import RegistryManager
+import openerp.addons.web.common.http as openerpweb
+
+import werkzeug
+
+import logging
+_logger = logging.getLogger(__name__)
+
+class Controller(openerpweb.Controller):
+ _cp_path = '/auth_reset_password'
+
+ @openerpweb.httprequest
+ def reset_password(self, req, dbname, login):
+ """ retrieve user, and perform reset password """
+ url = '/'
+ registry = RegistryManager.get(dbname)
+ with registry.cursor() as cr:
+ try:
+ res_users = registry.get('res.users')
+ res_users.reset_password(cr, SUPERUSER_ID, login)
+ cr.commit()
+ message = 'An email has been sent with credentials to reset your password'
+ except Exception as e:
+ # signup error
+ _logger.exception('error when resetting password')
+ message = e.message
+ url = "/#action=login&error_message=%s" % werkzeug.urls.url_quote(message)
+ return werkzeug.utils.redirect(url)
+
+# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/auth_reset_password/res_users.py b/addons/auth_reset_password/res_users.py
new file mode 100644
index 00000000000..b1039031812
--- /dev/null
+++ b/addons/auth_reset_password/res_users.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
+
+from datetime import datetime, timedelta
+
+def now(**kwargs):
+ dt = datetime.now() + timedelta(**kwargs)
+ return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
+
+class res_users(osv.osv):
+ _inherit = 'res.users'
+
+ def reset_password(self, cr, uid, login, context=None):
+ """ retrieve the user corresponding to login (login or email),
+ and reset their password
+ """
+ user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
+ if not user_ids:
+ user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
+ if len(user_ids) != 1:
+ raise Exception('Reset password: invalid username or email')
+ return self.action_reset_password(cr, uid, user_ids, context=context)
+
+ def action_reset_password(self, cr, uid, ids, context=None):
+ """ create signup token for each user, and send their signup url by email """
+ # prepare reset password signup
+ res_partner = self.pool.get('res.partner')
+ partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
+ res_partner.signup_prepare(cr, uid, partner_ids, expiration=now(days=+1), context=context)
+
+ # send email to users with their signup url
+ template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_reset_password', 'reset_password_email')
+ assert template._name == 'email.template'
+ for user in self.browse(cr, uid, ids, context):
+ self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, context=context)
+
+ return True
diff --git a/addons/auth_reset_password/res_users_view.xml b/addons/auth_reset_password/res_users_view.xml
new file mode 100644
index 00000000000..9952162ae65
--- /dev/null
+++ b/addons/auth_reset_password/res_users_view.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ user.form.reset_password
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/auth_reset_password/static/src/css/reset_password.css b/addons/auth_reset_password/static/src/css/reset_password.css
deleted file mode 100644
index dd590365918..00000000000
--- a/addons/auth_reset_password/static/src/css/reset_password.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.openerp .oe_login .oe_login_pane {
- height: 152px;
-}
-.openerp .oe_login .oe_login_pane ul.oe_login_switch a {
- color: #eeeeee;
- margin: 0 8px;
-}
-.openerp .oe_login .oe_login_pane ul.oe_login_switch a:hover {
- text-decoration: underline;
-}
-
-
diff --git a/addons/auth_reset_password/static/src/js/reset_password.js b/addons/auth_reset_password/static/src/js/reset_password.js
index 4e09b5c5318..13fe2a39ce5 100644
--- a/addons/auth_reset_password/static/src/js/reset_password.js
+++ b/addons/auth_reset_password/static/src/js/reset_password.js
@@ -1,72 +1,30 @@
openerp.auth_reset_password = function(instance) {
var _t = instance.web._t;
+
instance.web.Login.include({
start: function() {
- var $e = this.$el;
- $e.find('a.oe_login_switch').click(function() {
- $e.find('ul.oe_login_switch').toggle();
- var $m = $e.find('form input[name=is_reset_pw]');
- $m.attr('checked', !$m.is(':checked'));
- });
+ this.$('a.oe_reset_password').click(this.do_reset_password);
return this._super();
},
- on_submit: function(ev) {
- if(ev) {
+ do_reset_password: function(ev) {
+ if (ev) {
ev.preventDefault();
}
-
- var $e = this.$el;
- var db = $e.find("form [name=db]").val();
+ var db = this.$("form [name=db]").val();
+ var login = this.$("form input[name=login]").val();
if (!db) {
- this.do_warn(_t("Login"), _t("No database selected !"));
+ this.do_warn("Login", "No database selected !");
+ return false;
+ } else if (!login) {
+ this.do_warn("Login", "Please enter a username or email address.")
return false;
}
- var $m = $e.find('form input[name=is_reset_pw]');
- if ($m.is(':checked')) {
- var email = $e.find('form input[name=email]').val();
- return this.do_reset_password(db, email);
- } else {
- return this._super(ev);
- }
- },
- do_reset_password: function(db, email) {
- var self = this;
- instance.session.session_authenticate(db, 'anonymous', 'anonymous', true).pipe(function () {
- var func = new instance.web.Model("res.users").get_func("send_reset_password_request");
- return func(email).then(function(res) {
- // show message
- self.do_notify(_t('Reset Password'), _.str.sprintf(_t('We have sent an email to %s with further instructions'), email), true);
- }, function(error, event) {
- // no traceback please
- event.preventDefault();
- });
- }).fail(function(error, event) {
- // cannot log as anonymous or reset_password not installed
- self.do_warn(_t('Reset Password'), _.str.sprintf(_t('Reset Password functionnality is not available for database %s'), db), true);
- });
+ var params = {
+ dbname : db,
+ login: login,
+ };
+ var url = "/auth_reset_password/reset_password?" + $.param(params);
+ window.location = url;
}
});
-
- instance.reset_password = {};
- instance.reset_password.ResetPassword = instance.web.Widget.extend({
- init: function(parent, params) {
- this._super(parent);
- this.token = (params && params.token) || false;
- },
- start: function() {
- this.do_action({
- name: 'Reset Password',
- type: 'ir.actions.act_window',
- context: {default_token: this.token},
- res_model: 'auth.reset_password',
- target: 'new',
- views: [[false, 'form']]
- });
- }
- });
-
-
- instance.web.client_actions.add("reset_password", "instance.reset_password.ResetPassword");
-
-
};
diff --git a/addons/auth_reset_password/static/src/xml/reset_password.xml b/addons/auth_reset_password/static/src/xml/reset_password.xml
index e57334b9c15..841f3906622 100644
--- a/addons/auth_reset_password/static/src/xml/reset_password.xml
+++ b/addons/auth_reset_password/static/src/xml/reset_password.xml
@@ -1,26 +1,11 @@
-
+
-
- // addClass does not work :(
- this.attr('class', (this.attr('class') || '') + ' oe_login_switch');
-
-
+
diff --git a/addons/auth_signup/__openerp__.py b/addons/auth_signup/__openerp__.py
index 9cfe89cdc1c..9cbeb60638f 100644
--- a/addons/auth_signup/__openerp__.py
+++ b/addons/auth_signup/__openerp__.py
@@ -34,7 +34,9 @@ Allow users to sign up.
'data': [
'auth_signup_data.xml',
'res_config.xml',
+ 'res_users_view.xml',
],
'js': ['static/src/js/auth_signup.js'],
+ 'css' : ['static/src/css/base.css'],
'qweb': ['static/src/xml/auth_signup.xml'],
}
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 821ef44f459..56e5e910212 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -1,35 +1,63 @@
-import logging
-
-import werkzeug.urls
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+from openerp import SUPERUSER_ID
from openerp.modules.registry import RegistryManager
from openerp.addons.web.controllers.main import login_and_redirect
import openerp.addons.web.common.http as openerpweb
-from openerp import SUPERUSER_ID
+import werkzeug
+
+import logging
_logger = logging.getLogger(__name__)
-class OpenIDController(openerpweb.Controller):
+class Controller(openerpweb.Controller):
_cp_path = '/auth_signup'
+ @openerpweb.jsonrequest
+ def retrieve(self, req, dbname, token):
+ """ retrieve the user info (name, login or email) corresponding to a signup token """
+ registry = RegistryManager.get(dbname)
+ user_info = None
+ with registry.cursor() as cr:
+ res_partner = registry.get('res.partner')
+ user_info = res_partner.signup_retrieve_info(cr, SUPERUSER_ID, token)
+ return user_info
+
@openerpweb.httprequest
- def signup(self, req, dbname, name, login, password):
+ def signup(self, req, dbname, token, name, login, password):
+ """ sign up a user (new or existing), and log it in """
url = '/'
registry = RegistryManager.get(dbname)
with registry.cursor() as cr:
try:
- Users = registry.get('res.users')
- credentials = Users.auth_signup(cr, SUPERUSER_ID, name, login, password)
+ res_users = registry.get('res.users')
+ values = {'name': name, 'login': login, 'password': password}
+ credentials = res_users.signup(cr, SUPERUSER_ID, values, token)
cr.commit()
return login_and_redirect(req, *credentials)
- except AttributeError:
- # auth_signup is not installed
- _logger.exception('attribute error when signup')
- url = "/#action=auth_signup&error=NA" # Not Available
- except Exception:
+ except Exception as e:
# signup error
_logger.exception('error when signup')
- url = "/#action=auth_signup&error=UE" # Unexcpected Error
+ url = "/#action=login&error_message=%s" % werkzeug.urls.url_quote(e.message)
return werkzeug.utils.redirect(url)
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/auth_signup/res_config.py b/addons/auth_signup/res_config.py
index 8bfe1fc3ddb..21fce19745c 100644
--- a/addons/auth_signup/res_config.py
+++ b/addons/auth_signup/res_config.py
@@ -20,22 +20,27 @@
##############################################################################
from openerp.osv import osv, fields
+from openerp.tools.safe_eval import safe_eval
class base_config_settings(osv.TransientModel):
_inherit = 'base.config.settings'
_columns = {
- 'auth_signup_uninvited': fields.boolean('Allow public users to sign up', help="If unchecked only invited users may sign up"),
+ 'auth_signup_uninvited': fields.boolean('Allow external users to sign up', help="If unchecked, only invited users may sign up"),
'auth_signup_template_user_id': fields.many2one('res.users', 'Template user for new users created through signup'),
}
def get_default_auth_signup_template_user_id(self, cr, uid, fields, context=None):
icp = self.pool.get('ir.config_parameter')
+ # we use safe_eval on the result, since the value of the parameter is a nonempty string
return {
- 'auth_signup_template_user_id': icp.get_param(cr, uid, 'auth_signup.template_user_id', 0) or False
+ 'auth_signup_uninvited': safe_eval(icp.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')),
+ 'auth_signup_template_user_id': safe_eval(icp.get_param(cr, uid, 'auth_signup.template_user_id', 'False')),
}
def set_auth_signup_template_user_id(self, cr, uid, ids, context=None):
config = self.browse(cr, uid, ids[0], context=context)
icp = self.pool.get('ir.config_parameter')
- icp.set_param(cr, uid, 'auth_signup.template_user_id', config.auth_signup_template_user_id.id)
+ # we store the repr of the values, since the value of the parameter is a required string
+ icp.set_param(cr, uid, 'auth_signup.allow_uninvited', repr(config.auth_signup_uninvited))
+ icp.set_param(cr, uid, 'auth_signup.template_user_id', repr(config.auth_signup_template_user_id.id))
diff --git a/addons/auth_signup/res_config.xml b/addons/auth_signup/res_config.xml
index c17634ececb..9d3eb4eaa5a 100644
--- a/addons/auth_signup/res_config.xml
+++ b/addons/auth_signup/res_config.xml
@@ -14,7 +14,9 @@
-
+
diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py
index 5f6463508b6..7b663bd0e33 100644
--- a/addons/auth_signup/res_users.py
+++ b/addons/auth_signup/res_users.py
@@ -1,47 +1,205 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
import openerp
-from openerp.osv import osv
+from openerp.osv import osv, fields
from openerp import SUPERUSER_ID
+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
+from openerp.tools.safe_eval import safe_eval
+
+import time
+import random
+import urllib
+import urlparse
+
+def random_token():
+ # the token has an entropy of about 120 bits (6 bits/char * 20 chars)
+ chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ return ''.join(random.choice(chars) for i in xrange(20))
+
+def now():
+ return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
+
+class res_partner(osv.Model):
+ _inherit = 'res.partner'
+
+ def _get_signup_valid(self, cr, uid, ids, name, arg, context=None):
+ dt = now()
+ res = {}
+ for partner in self.browse(cr, uid, ids, context):
+ res[partner.id] = bool(partner.signup_token) and \
+ (not partner.signup_expiration or dt <= partner.signup_expiration)
+ return res
+
+ def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
+ """ determine a signup url for a given partner """
+ base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+
+ # if required, make sure that every partner without user has a valid signup token
+ if context and context.get('signup_valid'):
+ unsigned_ids = [p.id for p in self.browse(cr, uid, ids, context) if not p.user_ids]
+ self.signup_prepare(cr, uid, unsigned_ids, context=context)
+
+ res = dict.fromkeys(ids, False)
+ for partner in self.browse(cr, uid, ids, context):
+ if partner.signup_token:
+ params = (urllib.quote(cr.dbname), urllib.quote(partner.signup_token))
+ res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&token=%s" % params)
+ elif partner.user_ids:
+ user = partner.user_ids[0]
+ params = (urllib.quote(cr.dbname), urllib.quote(user.login))
+ res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&login=%s" % params)
+ return res
+
+ _columns = {
+ 'signup_token': fields.char(size=24, string='Signup Token'),
+ 'signup_expiration': fields.datetime(string='Signup Expiration'),
+ 'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'),
+ 'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'),
+ }
+
+ def action_signup_prepare(self, cr, uid, ids, context=None):
+ return self.signup_prepare(cr, uid, ids, context=context)
+
+ def signup_prepare(self, cr, uid, ids, expiration=False, context=None):
+ """ generate a new token for the partners with the given validity, if necessary
+ :param expiration: the expiration datetime of the token (string, optional)
+ """
+ for partner in self.browse(cr, uid, ids, context):
+ if expiration or not partner.signup_valid:
+ token = random_token()
+ while self._signup_retrieve_partner(cr, uid, token, context=context):
+ token = random_token()
+ partner.write({'signup_token': token, 'signup_expiration': expiration})
+ return True
+
+ def _signup_retrieve_partner(self, cr, uid, token,
+ check_validity=False, raise_exception=False, context=None):
+ """ find the partner corresponding to a token, and possibly check its validity
+ :param token: the token to resolve
+ :param check_validity: if True, also check validity
+ :param raise_exception: if True, raise exception instead of returning False
+ :return: partner (browse record) or False (if raise_exception is False)
+ """
+ partner_ids = self.search(cr, uid, [('signup_token', '=', token)], context=context)
+ if not partner_ids:
+ if raise_exception:
+ raise Exception("Signup token '%s' is not valid" % token)
+ return False
+ partner = self.browse(cr, uid, partner_ids[0], context)
+ if check_validity and not partner.signup_valid:
+ if raise_exception:
+ raise Exception("Signup token '%s' is no longer valid" % token)
+ return False
+ return partner
+
+ def signup_retrieve_info(self, cr, uid, token, context=None):
+ """ retrieve the user info about the token
+ :return: a dictionary with the user information:
+ - 'db': the name of the database
+ - 'token': the token, if token is valid
+ - 'name': the name of the partner, if token is valid
+ - 'login': the user login, if the user already exists
+ - 'email': the partner email, if the user does not exist
+ """
+ partner = self._signup_retrieve_partner(cr, uid, token, raise_exception=True, context=None)
+ res = {'db': cr.dbname}
+ if partner.signup_valid:
+ res['token'] = token
+ res['name'] = partner.name
+ if partner.user_ids:
+ res['login'] = partner.user_ids[0].login
+ else:
+ res['email'] = partner.email or ''
+ return res
+
+
class res_users(osv.Model):
_inherit = 'res.users'
- def auth_signup_create(self, cr, uid, new_user, context=None):
- # new_user:
- # login
- # email
- # name (optional)
- # partner_id (optional)
- # groups (optional)
- # sign (for partner_id and groups)
- #
- user_template_id = self.pool.get('ir.config_parameter').get_param(cr, uid, 'auth.signup_template_user_id', 0)
- if user_template_id:
- self.pool.get('res.users').copy(cr, SUPERUSER_ID, user_template_id, new_user, context=context)
- else:
- self.pool.get('res.users').create(cr, SUPERUSER_ID, new_user, context=context)
+ def _get_state(self, cr, uid, ids, name, arg, context=None):
+ return dict((user.id, 'new' if not user.login_date else 'reset' if user.signup_token else 'active')
+ for user in self.browse(cr, uid, ids, context))
- def auth_signup(self, cr, uid, name, login, password, context=None):
- r = (cr.dbname, login, password)
- res = self.search(cr, uid, [("login", "=", login)])
- if res:
- # Existing user
- user_id = res[0]
- try:
- self.check(cr.dbname, user_id, password)
- # Same password
- except openerp.exceptions.AccessDenied:
- # Different password
- raise
- else:
- # New user
- new_user = {
- 'name': name,
- 'login': login,
- 'user_email': login,
- 'password': password,
- 'active': True,
- }
- self.auth_signup_create(cr, uid, new_user)
- return r
+ _columns = {
+ 'state': fields.function(_get_state, string='State', type='selection',
+ selection=[('new', 'New'), ('active', 'Active'), ('reset', 'Resetting Password')]),
+ }
-#
+ def signup(self, cr, uid, values, token=None, context=None):
+ """ signup a user, to either:
+ - create a new user (no token), or
+ - create a user for a partner (with token, but no user for partner), or
+ - change the password of a user (with token, and existing user).
+ :param values: a dictionary with field values
+ :param token: signup token (optional)
+ :return: (dbname, login, password) for the signed up user
+ """
+ assert values.get('login') and values.get('password')
+ result = (cr.dbname, values['login'], values['password'])
+
+ if token:
+ # signup with a token: find the corresponding partner id
+ res_partner = self.pool.get('res.partner')
+ partner = res_partner._signup_retrieve_partner(cr, uid, token,
+ check_validity=True, raise_exception=True, context=None)
+ # invalidate signup token
+ partner.write({'signup_token': False, 'signup_expiration': False})
+ if partner.user_ids:
+ # user exists, modify its password
+ partner.user_ids[0].write({'password': values['password']})
+ else:
+ # user does not exist: sign up invited user
+ self._signup_create_user(cr, uid, {
+ 'name': partner.name,
+ 'login': values['login'],
+ 'password': values['password'],
+ 'email': values['login'],
+ 'partner_id': partner.id,
+ }, context=context)
+ return result
+
+ # sign up an external user
+ assert values.get('name'), 'Signup: no name given for new user'
+ self._signup_create_user(cr, uid, {
+ 'name': values['name'],
+ 'login': values['login'],
+ 'password': values['password'],
+ 'email': values['login'],
+ }, context=context)
+ return result
+
+ def _signup_create_user(self, cr, uid, values, context=None):
+ """ create a new user from the template user """
+ ir_config_parameter = self.pool.get('ir.config_parameter')
+ template_user_id = safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.template_user_id', 'False'))
+ assert template_user_id and self.exists(cr, uid, template_user_id, context=context), 'Signup: invalid template user'
+
+ # check that uninvited users may sign up
+ if 'partner_id' not in values:
+ if not safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')):
+ raise Exception('Signup is not allowed for uninvited users')
+
+ # create a copy of the template user (attached to a specific partner_id if given)
+ values['active'] = True
+ return self.copy(cr, uid, template_user_id, values, context=context)
diff --git a/addons/auth_signup/res_users_view.xml b/addons/auth_signup/res_users_view.xml
new file mode 100644
index 00000000000..c42e1ab5f41
--- /dev/null
+++ b/addons/auth_signup/res_users_view.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ user.form.state
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/auth_signup/static/src/css/Makefile b/addons/auth_signup/static/src/css/Makefile
new file mode 100644
index 00000000000..d6b4f4b2fc8
--- /dev/null
+++ b/addons/auth_signup/static/src/css/Makefile
@@ -0,0 +1,3 @@
+base.css: base.sass
+ sass --trace -t expanded base.sass base.css
+
diff --git a/addons/auth_signup/static/src/css/base.css b/addons/auth_signup/static/src/css/base.css
new file mode 100644
index 00000000000..3f97e7a9301
--- /dev/null
+++ b/addons/auth_signup/static/src/css/base.css
@@ -0,0 +1,10 @@
+@charset "utf-8";
+.openerp .oe_login .oe_signup_show {
+ display: none;
+}
+.openerp .oe_login_signup .oe_signup_show {
+ display: block !important;
+}
+.openerp .oe_login_signup .oe_signup_hide {
+ display: none;
+}
diff --git a/addons/auth_signup/static/src/css/base.sass b/addons/auth_signup/static/src/css/base.sass
new file mode 100644
index 00000000000..f97665d192c
--- /dev/null
+++ b/addons/auth_signup/static/src/css/base.sass
@@ -0,0 +1,13 @@
+@charset "utf-8"
+
+.openerp
+ // Regular login form
+ .oe_login
+ .oe_signup_show
+ display: none
+ // Signup form
+ .oe_login_signup
+ .oe_signup_show
+ display: block !important
+ .oe_signup_hide
+ display: none
diff --git a/addons/auth_signup/static/src/js/auth_signup.js b/addons/auth_signup/static/src/js/auth_signup.js
index 2ab13aa7598..509fafa35cc 100644
--- a/addons/auth_signup/static/src/js/auth_signup.js
+++ b/addons/auth_signup/static/src/js/auth_signup.js
@@ -5,56 +5,99 @@ openerp.auth_signup = function(instance) {
instance.web.Login.include({
start: function() {
var self = this;
- this.$('a.oe_signup').click(function() {
- var dbname = self.$("form [name=db]").val();
- self.do_action({
- type: 'ir.actions.client',
- tag: 'auth_signup.signup',
- params: {'dbname': dbname},
- target: 'new',
- name: 'Sign up'
- });
- return true;
+ var d = this._super();
+
+ // to switch between the signup and regular login form
+ this.$('a.oe_signup_signup').click(function() {
+ self.$el.addClass("oe_login_signup");
+ });
+ this.$('a.oe_signup_back').click(function() {
+ self.$el.removeClass("oe_login_signup");
+ delete self.params.token;
});
- return this._super();
- },
- });
+ // if there is an error message in params, show it then forget it
+ if (self.params.error_message) {
+ this.show_error(self.params.error_message);
+ delete self.params.error_message;
+ }
- instance.auth_signup.Signup = instance.web.Widget.extend({
- template: 'auth_signup.signup',
- init: function(parent, params) {
- this.params = params;
- return this._super();
+ // in case of a signup, retrieve the user information from the token
+ if (self.params.db && self.params.token) {
+ d = self.rpc("/auth_signup/retrieve", {dbname: self.params.db, token: self.params.token})
+ .done(self.on_token_loaded)
+ .fail(self.on_token_failed);
+ }
+ return d;
},
- start: function() {
- var self = this;
- this.$('input[name=password_confirmation]').keyup(function() {
- var v = $(this).val();
- var $b = self.$('button');
- if (_.isEmpty(v) || self.$('input[name=password]').val() === v) {
- $b.removeAttr('disabled');
+ on_token_loaded: function(result) {
+ // select the right the database
+ this.selected_db = result.db;
+ this.on_db_loaded({db_list: [result.db]});
+ if (result.token) {
+ // switch to signup mode, set user name and login
+ this.$el.addClass("oe_login_signup");
+ this.$("form input[name=name]").val(result.name).attr("readonly", "readonly");
+ if (result.login) {
+ this.$("form input[name=login]").val(result.login).attr("readonly", "readonly");
} else {
- $b.attr('disabled', 'disabled');
+ this.$("form input[name=login]").val(result.email);
}
- });
-
- this.$('form').submit(function(ev) {
- if(ev) {
- ev.preventDefault();
+ } else {
+ // remain in login mode, set login if present
+ delete this.params.token;
+ this.$("form input[name=login]").val(result.login || "");
+ }
+ },
+ on_token_failed: function(result, ev) {
+ if (ev) {
+ ev.preventDefault();
+ }
+ this.show_error("Invalid signup token");
+ delete this.params.db;
+ delete this.params.token;
+ },
+ on_submit: function(ev) {
+ if (ev) {
+ ev.preventDefault();
+ }
+ if (this.$el.hasClass("oe_login_signup")) {
+ // signup user (or reset password)
+ var db = this.$("form [name=db]").val();
+ var name = this.$("form input[name=name]").val();
+ var login = this.$("form input[name=login]").val();
+ var password = this.$("form input[name=password]").val();
+ var confirm_password = this.$("form input[name=confirm_password]").val();
+ if (!db) {
+ this.do_warn("Login", "No database selected !");
+ return false;
+ } else if (!name) {
+ this.do_warn("Login", "Please enter a name.")
+ return false;
+ } else if (!login) {
+ this.do_warn("Login", "Please enter a username.")
+ return false;
+ } else if (!password || !confirm_password) {
+ this.do_warn("Login", "Please enter a password and confirm it.")
+ return false;
+ } else if (password !== confirm_password) {
+ this.do_warn("Login", "Passwords do not match; please retype them.")
+ return false;
}
var params = {
- dbname : self.params.dbname,
- name: self.$('input[name=name]').val(),
- login: self.$('input[name=email]').val(),
- password: self.$('input[name=password]').val(),
+ dbname : db,
+ token: this.params.token || "",
+ name: name,
+ login: login,
+ password: password,
};
var url = "/auth_signup/signup?" + $.param(params);
window.location = url;
- });
- return this._super();
- }
+ } else {
+ // regular login
+ this._super(ev);
+ }
+ },
});
- instance.web.client_actions.add("auth_signup.signup", "instance.auth_signup.Signup");
};
diff --git a/addons/auth_signup/static/src/xml/auth_signup.xml b/addons/auth_signup/static/src/xml/auth_signup.xml
index c2eec846d15..e63bd5863b0 100644
--- a/addons/auth_signup/static/src/xml/auth_signup.xml
+++ b/addons/auth_signup/static/src/xml/auth_signup.xml
@@ -1,28 +1,28 @@
-
+
-
-
-
-
-
-
diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml
index 37797a012aa..4df0a8a22b2 100644
--- a/addons/base_calendar/crm_meeting_view.xml
+++ b/addons/base_calendar/crm_meeting_view.xml
@@ -280,7 +280,7 @@
-
+
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index f91a8756598..6a152f44b67 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -356,10 +356,10 @@
-
+
-
+
@@ -569,10 +569,10 @@
-
+
-
+
@@ -580,9 +580,8 @@
domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Opportunities that are assigned to either me or one of the sale teams I manage" />
-
-
+
diff --git a/addons/crm/res_partner_view.xml b/addons/crm/res_partner_view.xml
index 84a0bd4cf49..b7aa334a9c3 100644
--- a/addons/crm/res_partner_view.xml
+++ b/addons/crm/res_partner_view.xml
@@ -15,31 +15,12 @@
-
- view.res.partner.tree.crm.inherited2
- res.partner
-
-
-
-
-
-
-
-
-
view.res.partner.search.crm.inherited3res.partner
-
-
-
-
-
-
-
diff --git a/addons/crm_partner_assign/crm_lead_view.xml b/addons/crm_partner_assign/crm_lead_view.xml
index cd2e3f08c9a..2cffb6fb899 100644
--- a/addons/crm_partner_assign/crm_lead_view.xml
+++ b/addons/crm_partner_assign/crm_lead_view.xml
@@ -56,7 +56,7 @@
-
+
diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml
index b46fafe270f..0155846fec3 100644
--- a/addons/event/event_view.xml
+++ b/addons/event/event_view.xml
@@ -339,7 +339,7 @@
-
+
@@ -529,7 +529,7 @@
-
+
diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml
index 69567902763..89c327f4d13 100644
--- a/addons/hr/hr_view.xml
+++ b/addons/hr/hr_view.xml
@@ -112,8 +112,6 @@
-
-
diff --git a/addons/hr_expense/hr_expense_view.xml b/addons/hr_expense/hr_expense_view.xml
index 388e5262f6e..67d629eee6a 100644
--- a/addons/hr_expense/hr_expense_view.xml
+++ b/addons/hr_expense/hr_expense_view.xml
@@ -155,11 +155,12 @@
-
+
+
+
-
diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml
index 08e5f31a2d2..d48086f5663 100644
--- a/addons/hr_recruitment/hr_recruitment_view.xml
+++ b/addons/hr_recruitment/hr_recruitment_view.xml
@@ -208,7 +208,7 @@
-
+
diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py
index 9aab412d425..fd025491588 100644
--- a/addons/hr_timesheet_sheet/__openerp__.py
+++ b/addons/hr_timesheet_sheet/__openerp__.py
@@ -65,5 +65,8 @@ The validation can be configured in the company:
'installable': True,
'auto_install': False,
'application': True,
+ 'js': ['static/src/js/timesheet.js',],
+ 'css': ['static/src/css/timesheet.css',],
+ 'qweb': ['static/src/xml/timesheet.xml',],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
index cd58f52f959..96cb15ddce1 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
@@ -27,80 +27,6 @@ from osv import fields, osv
from tools.translate import _
import netsvc
-class one2many_mod2(fields.one2many):
- def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
- if context is None:
- context = {}
-
- if values is None:
- values = {}
-
- # res6 = {id: date_current, ...}
- res6 = dict([(rec['id'], rec['date_current'])
- for rec in obj.read(cr, user, ids, ['date_current'], context=context)])
-
- dom = []
- for c, id in enumerate(ids):
- if id in res6:
- if c: # skip first
- dom.insert(0 ,'|')
- dom.append('&')
- dom.append('&')
- dom.append(('name', '>=', res6[id]))
- dom.append(('name', '<=', res6[id]))
- dom.append(('sheet_id', '=', id))
-
- ids2 = obj.pool.get(self._obj).search(cr, user, dom, limit=self._limit)
-
- res = {}
- for i in ids:
- res[i] = []
-
- for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_read'):
- if r[self._fields_id]:
- res[r[self._fields_id][0]].append(r['id'])
- return res
-
- def set(self, cr, obj, id, field, values, user=None, context=None):
- if context is None:
- context = {}
-
- context = context.copy()
- context['sheet_id'] = id
- return super(one2many_mod2, self).set(cr, obj, id, field, values, user=user, context=context)
-
-
-class one2many_mod(fields.one2many):
- def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
- if context is None:
- context = {}
-
- if values is None:
- values = {}
-
-
- res5 = obj.read(cr, user, ids, ['date_current'], context=context)
- res6 = {}
- for r in res5:
- res6[r['id']] = r['date_current']
-
- ids2 = []
- for id in ids:
- dom = []
- if id in res6:
- dom = [('date', '=', res6[id]), ('sheet_id', '=', id)]
- ids2.extend(obj.pool.get(self._obj).search(cr, user,
- dom, limit=self._limit))
- res = {}
- for i in ids:
- res[i] = []
- for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2,
- [self._fields_id], context=context, load='_classic_read'):
- if r[self._fields_id]:
- res[r[self._fields_id][0]].append(r['id'])
-
- return res
-
class hr_timesheet_sheet(osv.osv):
_name = "hr_timesheet_sheet.sheet"
_inherit = "mail.thread"
@@ -111,8 +37,7 @@ class hr_timesheet_sheet(osv.osv):
def _total_attendances(self, cr, uid, ids, name, args, context=None):
""" Get the total attendance for the timesheets
Returns a dict like :
- {id: {'date_current': '2011-06-17',
- 'total_per_day': {day: timedelta, ...},
+ {id: {'total_per_day': {day: timedelta, ...},
},
...
}
@@ -122,7 +47,6 @@ class hr_timesheet_sheet(osv.osv):
res = {}
for sheet_id in ids:
sheet = self.browse(cr, uid, sheet_id, context=context)
- date_current = sheet.date_current
# field attendances_ids of hr_timesheet_sheet.sheet only
# returns attendances of timesheet's current date
attendance_ids = attendance_obj.search(cr, uid, [('sheet_id', '=', sheet_id)], context=context)
@@ -143,20 +67,7 @@ class hr_timesheet_sheet(osv.osv):
else:
total_attendance[day] += attendance_interval
- # if the delta is negative, it means that a sign out is missing
- # in a such case, we want to have the time to the end of the day
- # for a past date, and the time to now for the current date
- if total_attendance[day] < timedelta(0):
- if day == date_current:
- now = datetime.now()
- total_attendance[day] += timedelta(hours=now.hour,
- minutes=now.minute,
- seconds=now.second)
- else:
- total_attendance[day] += timedelta(days=1)
-
- res[sheet_id] = {'date_current': date_current,
- 'total_per_day': total_attendance}
+ res[sheet_id] = {'total_per_day': total_attendance}
return res
def _total_timesheet(self, cr, uid, ids, name, args, context=None):
@@ -210,24 +121,16 @@ class hr_timesheet_sheet(osv.osv):
all_attendances_sheet = all_timesheet_attendances[id]
- date_current = all_attendances_sheet['date_current']
total_attendances_sheet = all_attendances_sheet['total_per_day']
total_attendances_all_days = sum_all_days(total_attendances_sheet)
- total_attendances_day = total_attendances_sheet.get(date_current, timedelta(seconds=0))
total_timesheets_sheet = all_timesheet_lines[id]
total_timesheets_all_days = sum_all_days(total_timesheets_sheet)
- total_timesheets_day = total_timesheets_sheet.get(date_current, timedelta(seconds=0))
total_difference_all_days = total_attendances_all_days - total_timesheets_all_days
- total_difference_day = total_attendances_day - total_timesheets_day
res[id]['total_attendance'] = timedelta_to_hours(total_attendances_all_days)
res[id]['total_timesheet'] = timedelta_to_hours(total_timesheets_all_days)
res[id]['total_difference'] = timedelta_to_hours(total_difference_all_days)
-
- res[id]['total_attendance_day'] = timedelta_to_hours(total_attendances_day)
- res[id]['total_timesheet_day'] = timedelta_to_hours(total_timesheets_day)
- res[id]['total_difference_day'] = timedelta_to_hours(total_difference_day)
return res
def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
@@ -277,44 +180,6 @@ class hr_timesheet_sheet(osv.osv):
raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
return True
- def date_today(self, cr, uid, ids, context=None):
- for sheet in self.browse(cr, uid, ids, context=context):
- if datetime.today() <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
- elif datetime.now() >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
- else:
- self.write(cr, uid, [sheet.id], {'date_current': time.strftime('%Y-%m-%d')}, context=context)
- return True
-
- def date_previous(self, cr, uid, ids, context=None):
- for sheet in self.browse(cr, uid, ids, context=context):
- if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
- else:
- self.write(cr, uid, [sheet.id], {
- 'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=-1)).strftime('%Y-%m-%d'),
- }, context=context)
- return True
-
- def date_next(self, cr, uid, ids, context=None):
- for sheet in self.browse(cr, uid, ids, context=context):
- if datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
- else:
- self.write(cr, uid, [sheet.id], {
- 'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=1)).strftime('%Y-%m-%d'),
- }, context=context)
- return True
-
- def button_dummy(self, cr, uid, ids, context=None):
- for sheet in self.browse(cr, uid, ids, context=context):
- if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
- elif datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
- self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
- return True
-
def attendance_action_change(self, cr, uid, ids, context=None):
hr_employee = self.pool.get('hr.employee')
employee_ids = []
@@ -329,14 +194,13 @@ class hr_timesheet_sheet(osv.osv):
'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
- 'date_current': fields.date('Current date', required=True, select=1),
- 'timesheet_ids' : one2many_mod('hr.analytic.timesheet', 'sheet_id',
- 'Timesheet lines', domain=[('date', '=', time.strftime('%Y-%m-%d'))],
+ 'timesheet_ids' : fields.one2many('hr.analytic.timesheet', 'sheet_id',
+ 'Timesheet lines',
readonly=True, states={
'draft': [('readonly', False)],
'new': [('readonly', False)]}
),
- 'attendances_ids' : one2many_mod2('hr.attendance', 'sheet_id', 'Attendances'),
+ 'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
'state' : fields.selection([
('new', 'New'),
('draft','Open'),
@@ -346,9 +210,6 @@ class hr_timesheet_sheet(osv.osv):
\n* The \'Confirmed\' state is used for to confirm the timesheet by user. \
\n* The \'Done\' state is used when users timesheet is accepted by his/her senior.'),
'state_attendance' : fields.related('employee_id', 'state', type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Current Status', readonly=True),
- 'total_attendance_day': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
- 'total_timesheet_day': fields.function(_total, method=True, string='Total Timesheet', multi="_total"),
- 'total_difference_day': fields.function(_total, method=True, string='Difference', multi="_total"),
'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total"),
'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"),
@@ -386,7 +247,6 @@ class hr_timesheet_sheet(osv.osv):
_defaults = {
'date_from' : _default_date_from,
- 'date_current' : lambda *a: time.strftime('%Y-%m-%d'),
'date_to' : _default_date_to,
'state': 'new',
'employee_id': _default_employee,
@@ -406,16 +266,9 @@ class hr_timesheet_sheet(osv.osv):
return False
return True
- def _date_current_check(self, cr, uid, ids, context=None):
- for sheet in self.browse(cr, uid, ids, context=context):
- if sheet.date_current < sheet.date_from or sheet.date_current > sheet.date_to:
- return False
- return True
-
_constraints = [
(_sheet_date, 'You cannot have 2 timesheets that overlaps !\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
- (_date_current_check, 'You must select a Current date which is in the timesheet dates !', ['date_current']),
]
def action_set_to_draft(self, cr, uid, ids, *args):
@@ -498,7 +351,7 @@ class hr_timesheet_line(osv.osv):
_columns = {
'sheet_id': fields.function(_sheet, string='Sheet',
- type='many2one', relation='hr_timesheet_sheet.sheet',
+ type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
store={
'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
'account.analytic.line': (_get_account_analytic_line, ['user_id', 'date'], 10),
@@ -534,6 +387,10 @@ class hr_timesheet_line(osv.osv):
raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet.'))
return True
+ def multi_on_change_account_id(self, cr, uid, ids, account_ids, context=None):
+ return dict([(el, self.on_change_account_id(cr, uid, ids, el)) for el in account_ids])
+
+
hr_timesheet_line()
class hr_attendance(osv.osv):
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml
index 42497eb537e..4e0e1becbe9 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_demo.xml
@@ -6,7 +6,6 @@
Sheet 1
-
-->
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
index 9a7416464af..7dd2b9a1b88 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
@@ -71,20 +71,13 @@
+
+
+
+
-
-
- :
-
-
-
-
-
-
-
-
-
+
@@ -101,9 +94,9 @@
-
+
-
+
diff --git a/addons/hr_timesheet_sheet/report/timesheet_report.py b/addons/hr_timesheet_sheet/report/timesheet_report.py
index 3db7ff8feb2..497fe8da117 100644
--- a/addons/hr_timesheet_sheet/report/timesheet_report.py
+++ b/addons/hr_timesheet_sheet/report/timesheet_report.py
@@ -46,7 +46,6 @@ class timesheet_report(osv.osv):
'department_id':fields.many2one('hr.department','Department',readonly=True),
'date_from': fields.date('Date from',readonly=True,),
'date_to': fields.date('Date to',readonly=True),
- 'date_current': fields.date('Current date', required=True),
'state' : fields.selection([
('new', 'New'),
('draft','Draft'),
@@ -62,13 +61,9 @@ class timesheet_report(osv.osv):
create or replace view timesheet_report as (
select
min(aal.id) as id,
- htss.date_current,
htss.name,
htss.date_from,
htss.date_to,
- to_char(htss.date_current,'YYYY') as year,
- to_char(htss.date_current,'MM') as month,
- to_char(htss.date_current, 'YYYY-MM-DD') as day,
count(*) as nbr,
aal.unit_amount as quantity,
aal.amount as cost,
@@ -77,18 +72,15 @@ class timesheet_report(osv.osv):
(SELECT sum(day.total_difference)
FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day
- ON (sheet.id = day.sheet_id
- AND day.name = sheet.date_current) where sheet.id=htss.id) as total_diff,
+ ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_diff,
(SELECT sum(day.total_timesheet)
FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day
- ON (sheet.id = day.sheet_id
- AND day.name = sheet.date_current) where sheet.id=htss.id) as total_timesheet,
+ ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_timesheet,
(SELECT sum(day.total_attendance)
FROM hr_timesheet_sheet_sheet AS sheet
LEFT JOIN hr_timesheet_sheet_sheet_day AS day
- ON (sheet.id = day.sheet_id
- AND day.name = sheet.date_current) where sheet.id=htss.id) as total_attendance,
+ ON (sheet.id = day.sheet_id) where sheet.id=htss.id) as total_attendance,
aal.to_invoice,
aal.general_account_id,
htss.user_id,
@@ -99,15 +91,11 @@ class timesheet_report(osv.osv):
left join hr_analytic_timesheet as hat ON (hat.line_id=aal.id)
left join hr_timesheet_sheet_sheet as htss ON (hat.line_id=htss.id)
group by
- to_char(htss.date_current,'YYYY'),
- to_char(htss.date_current,'MM'),
- to_char(htss.date_current, 'YYYY-MM-DD'),
aal.account_id,
htss.date_from,
htss.date_to,
aal.unit_amount,
aal.amount,
- htss.date_current,
aal.to_invoice,
aal.product_id,
aal.general_account_id,
diff --git a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
index 7b31f72235b..711bbaf2a35 100644
--- a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
+++ b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
@@ -17,7 +17,6 @@
timesheet.report
-
@@ -56,7 +55,6 @@
-
diff --git a/addons/hr_timesheet_sheet/static/src/css/timesheet.css b/addons/hr_timesheet_sheet/static/src/css/timesheet.css
new file mode 100644
index 00000000000..b486f76694d
--- /dev/null
+++ b/addons/hr_timesheet_sheet/static/src/css/timesheet.css
@@ -0,0 +1,79 @@
+
+.openerp .oe_timesheet_weekly {
+ overflow-x: auto;
+}
+
+.openerp .oe_timesheet_weekly table {
+ width: 100%;
+}
+
+.openerp .oe_timesheet_weekly td {
+ padding-top: 15px;
+}
+
+.openerp .oe_timesheet_weekly th {
+ text-align: right;
+ color: #069;
+ font-family: 'Helvetica Neue', Arial, Verdana, 'Nimbus Sans L', sans-serif;
+ font-size: 10px;
+}
+
+.openerp .oe_timesheet_weekly th.oe_timesheet_weekly_date_head {
+ width: 60px;
+}
+
+.openerp .oe_timesheet_weekly td {
+ text-align: right;
+ vertical-align: middle;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_account {
+ text-align: left;
+ padding-right: 30px;
+}
+
+.openerp .oe_timesheet_weekly td input.oe_timesheet_weekly_input {
+ border: 1px solid #CCC;
+ padding: 5px 2px !important;
+ color: #666 !important;
+ font-size: 14px;
+ font-weight: bold;
+ width: 38px;
+ text-align: right;
+ min-width: 0 !important;
+}
+
+.openerp .oe_timesheet_weekly td .oe_timesheet_weekly_box {
+ padding: 5px 2px !important;
+ color: #666 !important;
+ font-size: 14px;
+ font-weight: bold;
+ width: 38px;
+ display: inline-block;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_adding_tot {
+ display: table;
+ width: 100%;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_adding {
+ display: table-cell;
+ text-align: left;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_tottot {
+ display: table-cell;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_add_row td {
+ text-align: left;
+}
+
+.openerp .oe_timesheet_weekly .oe_timesheet_weekly_add_row .oe_form_field_many2one {
+ display: inline-block;
+ width: 200px;
+}
+
+.openerp .oe_timesheet_weekly_today {
+}
diff --git a/addons/hr_timesheet_sheet/static/src/js/timesheet.js b/addons/hr_timesheet_sheet/static/src/js/timesheet.js
new file mode 100644
index 00000000000..1be965fd63d
--- /dev/null
+++ b/addons/hr_timesheet_sheet/static/src/js/timesheet.js
@@ -0,0 +1,299 @@
+
+openerp.hr_timesheet_sheet = function(instance) {
+ var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
+
+ instance.hr_timesheet_sheet.WeeklyTimesheet = instance.web.form.FormWidget.extend(instance.web.form.ReinitializeWidgetMixin, {
+ init: function() {
+ this._super.apply(this, arguments);
+ this.set({
+ sheets: [],
+ date_to: false,
+ date_from: false,
+ });
+ this.field_manager.on("field_changed:timesheet_ids", this, this.query_sheets);
+ this.field_manager.on("field_changed:date_from", this, function() {
+ this.set({"date_from": instance.web.str_to_date(this.field_manager.get_field_value("date_from"))});
+ });
+ this.field_manager.on("field_changed:date_to", this, function() {
+ this.set({"date_to": instance.web.str_to_date(this.field_manager.get_field_value("date_to"))});
+ });
+ this.field_manager.on("field_changed:user_id", this, function() {
+ this.set({"user_id": this.field_manager.get_field_value("user_id")});
+ });
+ this.on("change:sheets", this, this.update_sheets);
+ this.res_o2m_drop = new instance.web.DropMisordered();
+ this.render_drop = new instance.web.DropMisordered();
+ this.description_line = _t("/");
+ },
+ query_sheets: function() {
+ var self = this;
+ if (self.updating)
+ return;
+ var commands = this.field_manager.get_field_value("timesheet_ids");
+ this.res_o2m_drop.add(new instance.web.Model(this.view.model).call("resolve_2many_commands", ["timesheet_ids", commands, [],
+ new instance.web.CompoundContext()]))
+ .then(function(result) {
+ self.querying = true;
+ self.set({sheets: result});
+ self.querying = false;
+ });
+ },
+ update_sheets: function() {
+ var self = this;
+ if (self.querying)
+ return;
+ self.updating = true;
+ self.field_manager.set_values({timesheet_ids: self.get("sheets")}).then(function() {
+ self.updating = false;
+ });
+ },
+ initialize_field: function() {
+ instance.web.form.ReinitializeWidgetMixin.initialize_field.call(this);
+ var self = this;
+ self.on("change:sheets", self, self.initialize_content);
+ self.on("change:date_to", self, self.initialize_content);
+ self.on("change:date_from", self, self.initialize_content);
+ self.on("change:user_id", self, self.initialize_content);
+ },
+ initialize_content: function() {
+ var self = this;
+ if (self.setting)
+ return;
+ // don't render anything until we have date_to and date_from
+ if (!self.get("date_to") || !self.get("date_from"))
+ return;
+ this.destroy_content();
+
+ // it's important to use those vars to avoid race conditions
+ var dates;
+ var accounts;
+ var account_names;
+ var default_get;
+ return this.render_drop.add(new instance.web.Model("hr.analytic.timesheet").call("default_get", [
+ ['account_id','general_account_id', 'journal_id','date','name','user_id','product_id','product_uom_id','to_invoice','amount','unit_amount'],
+ new instance.web.CompoundContext({'user_id': self.get('user_id')})]).pipe(function(result) {
+ default_get = result;
+ // calculating dates
+ dates = [];
+ var start = self.get("date_from");
+ var end = self.get("date_to");
+ while (start <= end) {
+ dates.push(start);
+ start = start.clone().addDays(1);
+ }
+ // group by account
+ accounts = _(self.get("sheets")).chain()
+ .map(function(el) {
+ // much simpler to use only the id in all cases
+ if (typeof(el.account_id) === "object")
+ el.account_id = el.account_id[0];
+ return el;
+ })
+ .groupBy("account_id").value();
+
+ var account_ids = _.map(_.keys(accounts), function(el) { return el === "false" ? false : Number(el) });
+
+ return new instance.web.Model("hr.analytic.timesheet").call("multi_on_change_account_id", [[], account_ids,
+ new instance.web.CompoundContext({'user_id': self.get('user_id')})]).pipe(function(accounts_defaults) {
+ accounts = _(accounts).chain().map(function(lines, account_id) {
+ account_defaults = _.extend({}, default_get, accounts_defaults[account_id]);
+ // group by days
+ account_id = account_id === "false" ? false : Number(account_id);
+ var index = _.groupBy(lines, "date");
+ var days = _.map(dates, function(date) {
+ var day = {day: date, lines: index[instance.web.date_to_str(date)] || []};
+ // add line where we will insert/remove hours
+ var to_add = _.find(day.lines, function(line) { return line.name === self.description_line });
+ if (to_add) {
+ day.lines = _.without(day.lines, to_add);
+ day.lines.unshift(to_add);
+ } else {
+ day.lines.unshift(_.extend(_.clone(account_defaults), {
+ name: self.description_line,
+ unit_amount: 0,
+ date: instance.web.date_to_str(date),
+ account_id: account_id,
+ }));
+ }
+ return day;
+ });
+ return {account: account_id, days: days, account_defaults: account_defaults};
+ }).value();
+
+ // we need the name_get of the analytic accounts
+ return new instance.web.Model("account.analytic.account").call("name_get", [_.pluck(accounts, "account"),
+ new instance.web.CompoundContext()]).pipe(function(result) {
+ account_names = {};
+ _.each(result, function(el) {
+ account_names[el[0]] = el[1];
+ });
+ accounts = _.sortBy(accounts, function(el) {
+ return account_names[el.account];
+ });
+ });;
+ });
+ })).pipe(function(result) {
+ // we put all the gathered data in self, then we render
+ self.dates = dates;
+ self.accounts = accounts;
+ self.account_names = account_names;
+ self.default_get = default_get;
+ //real rendering
+ self.display_data();
+ });
+ },
+ destroy_content: function() {
+ if (this.dfm) {
+ this.dfm.destroy();
+ this.dfm = undefined;
+ }
+ },
+ display_data: function() {
+ var self = this;
+ self.$el.html(QWeb.render("hr_timesheet_sheet.WeeklyTimesheet", {widget: self}));
+ _.each(self.accounts, function(account) {
+ _.each(_.range(account.days.length), function(day_count) {
+ if (!self.get('effective_readonly')) {
+ self.get_box(account, day_count).val(self.sum_box(account, day_count)).change(function() {
+ var num = Number($(this).val());
+ if (isNaN(num)) {
+ $(this).val(self.sum_box(account, day_count));
+ } else {
+ account.days[day_count].lines[0].unit_amount += num - self.sum_box(account, day_count);
+ self.display_totals();
+ self.sync();
+ }
+ });
+ } else {
+ self.get_box(account, day_count).html(self.sum_box(account, day_count));
+ }
+ });
+ });
+ self.display_totals();
+ self.$(".oe_timesheet_weekly_adding button").click(_.bind(this.init_add_account, this));
+ },
+ init_add_account: function() {
+ var self = this;
+ if (self.dfm)
+ return;
+ self.$(".oe_timesheet_weekly_add_row").show();
+ self.dfm = new instance.web.form.DefaultFieldManager(self);
+ self.dfm.extend_field_desc({
+ account: {
+ relation: "account.analytic.account",
+ },
+ });
+ self.account_m2o = new instance.web.form.FieldMany2One(self.dfm, {
+ attrs: {
+ name: "account",
+ type: "many2one",
+ domain: [
+ ['type','in',['normal', 'contract']],
+ ['state', '<>', 'close'],
+ ['use_timesheets','=',1],
+ ['id', 'not in', _.pluck(self.accounts, "account")],
+ ],
+ modifiers: '{"required": true}',
+ },
+ });
+ self.account_m2o.prependTo(self.$(".oe_timesheet_weekly_add_row td"));
+ self.$(".oe_timesheet_weekly_add_row button").click(function() {
+ var id = self.account_m2o.get_value();
+ if (id === false) {
+ self.dfm.set({display_invalid_fields: true});
+ return;
+ }
+ var ops = self.generate_o2m_value();
+ new instance.web.Model("hr.analytic.timesheet").call("on_change_account_id", [[], id]).pipe(function(res) {
+ var def = _.extend({}, self.default_get, res.value, {
+ name: self.description_line,
+ unit_amount: 0,
+ date: instance.web.date_to_str(self.dates[0]),
+ account_id: id,
+ });
+ ops.push(def);
+ self.set({"sheets": ops});
+ });
+ });
+ },
+ get_box: function(account, day_count) {
+ return this.$('[data-account="' + account.account + '"][data-day-count="' + day_count + '"]');
+ },
+ get_total: function(account) {
+ return this.$('[data-account-total="' + account.account + '"]');
+ },
+ get_day_total: function(day_count) {
+ return this.$('[data-day-total="' + day_count + '"]');
+ },
+ get_super_total: function() {
+ return this.$('.oe_timesheet_weekly_supertotal');
+ },
+ sum_box: function(account, day_count) {
+ var line_total = 0;
+ _.each(account.days[day_count].lines, function(line) {
+ line_total += line.unit_amount;
+ });
+ return line_total;
+ },
+ display_totals: function() {
+ var self = this;
+ var day_tots = _.map(_.range(self.dates.length), function() { return 0 });
+ var super_tot = 0;
+ _.each(self.accounts, function(account) {
+ var acc_tot = 0;
+ _.each(_.range(self.dates.length), function(day_count) {
+ var sum = self.sum_box(account, day_count);
+ acc_tot += sum;
+ day_tots[day_count] += sum;
+ super_tot += sum;
+ });
+ self.get_total(account).html(acc_tot);
+ });
+ _.each(_.range(self.dates.length), function(day_count) {
+ self.get_day_total(day_count).html(day_tots[day_count]);
+ });
+ self.get_super_total().html(super_tot);
+ },
+ sync: function() {
+ var self = this;
+ self.setting = true;
+ self.set({sheets: this.generate_o2m_value()});
+ self.setting = false;
+ },
+ generate_o2m_value: function() {
+ var self = this;
+ var ops = [];
+
+ _.each(self.accounts, function(account) {
+ var auth_keys = _.extend(_.clone(account.account_defaults), {
+ name: true, unit_amount: true, date: true, account_id:true,
+ });
+ _.each(account.days, function(day) {
+ _.each(day.lines, function(line) {
+ if (line.unit_amount !== 0) {
+ var tmp = _.clone(line);
+ tmp.id = undefined;
+ _.each(line, function(v, k) {
+ if (v instanceof Array) {
+ tmp[k] = v[0];
+ }
+ });
+ // we have to remove some keys, because analytic lines are shitty
+ _.each(_.keys(tmp), function(key) {
+ if (auth_keys[key] === undefined) {
+ tmp[key] = undefined;
+ }
+ });
+ ops.push(tmp);
+ }
+ });
+ });
+ });
+ return ops;
+ },
+ });
+
+ instance.web.form.custom_widgets.add('weekly_timesheet', 'instance.hr_timesheet_sheet.WeeklyTimesheet');
+
+};
diff --git a/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml b/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml
new file mode 100644
index 00000000000..0c4ad857d3b
--- /dev/null
+++ b/addons/hr_timesheet_sheet/static/src/xml/timesheet.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TOTAL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TOTAL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
index 070079a4b80..71776af61ba 100644
--- a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
+++ b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
@@ -16,7 +16,6 @@
I create a timesheet for employee "Quentin Paolinon".
-
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
- date_current: !eval time.strftime('%Y-%m-%d')
date_from: !eval time.strftime('%Y-%m-01')
name: Quentin Paolinon
state: new
@@ -34,30 +33,6 @@
-
!assert {model: hr.employee, id: hr.employee_qdp}:
- state == 'present'
--
- I want to check attendance and work of yesterday. I click on <- button.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- date_prev = self.date_previous(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None)
- assert date_prev == True, "I See Previous Date Timesheet"
--
- Then I click on "Today" button to fill today's timesheet.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- date_to = self.date_today(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None)
- assert date_to == True, "I See Today Date Timesheet"
--
- I can also move to next day by clicking on -> button.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- date_next = self.date_next(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None)
- assert date_next == True, "I See Next Date Timesheet"
--
- I want to go to a particular date and see attendance then I select the date and click on "Go to:" button.
--
- !python {model: hr_timesheet_sheet.sheet}: |
- button_dumy = self.button_dummy(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], None)
- assert button_dumy == True, "I See Particular Date Attendance"
-
At the time of logout, I create attendance and perform "Sign Out".
-
diff --git a/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py b/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py
index a6974fc2ccf..3bd495036d5 100644
--- a/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py
+++ b/addons/hr_timesheet_sheet/wizard/hr_timesheet_current.py
@@ -42,7 +42,6 @@ class hr_timesheet_current_open(osv.osv_memory):
view_type = 'tree,form'
domain = "[('id','in',["+','.join(map(str, ids))+"]),('user_id', '=', uid)]"
elif len(ids)==1:
- ts.write(cr, uid, ids, {'date_current': time.strftime('%Y-%m-%d')}, context=context)
domain = "[('user_id', '=', uid)]"
else:
domain = "[('user_id', '=', uid)]"
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 2261b940944..529ee8c65cc 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -39,63 +39,6 @@ _logger = logging.getLogger(__name__)
def decode_header(message, header, separator=' '):
return separator.join(map(decode, message.get_all(header, [])))
-class many2many_reference(fields.many2many):
- """ many2many_reference manages many2many fields where one id is found
- by a reference-like key (a char column in addition to the foreign id).
- The reference_column attribute on the many2many fields is used;
- if not defined, ``res_model`` is used. """
-
- def _get_query_and_where_params(self, cr, model, ids, values, where_params):
- """ Add in where condition like mail_followers.res_model = 'crm.lead' """
- reference_column = self.reference_column if self.reference_column else 'res_model'
- values.update(reference_column=reference_column, reference_value=model._name)
- query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
- FROM %(rel)s, %(from_c)s \
- WHERE %(rel)s.%(id1)s IN %%s \
- AND %(rel)s.%(id2)s = %(tbl)s.id \
- AND %(rel)s.%(reference_column)s = \'%(reference_value)s\' \
- %(where_c)s \
- %(order_by)s \
- %(limit)s \
- OFFSET %(offset)d' \
- % values
- return query, where_params
-
- def set(self, cr, model, id, name, values, user=None, context=None):
- """ Override to add the reference field in queries. """
- if not values: return
- rel, id1, id2 = self._sql_names(model)
- obj = model.pool.get(self._obj)
- # reference column name: given by attribute or res_model
- reference_column = self.reference_column if self.reference_column else 'res_model'
- for act in values:
- if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
- continue
- if act[0] == 0:
- idnew = obj.create(cr, user, act[2], context=context)
- cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, idnew, model._name))
- elif act[0] == 3:
- cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
- elif act[0] == 4:
- # following queries are in the same transaction - so should be relatively safe
- cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
- if not cr.fetchone():
- cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act[1], model._name))
- elif act[0] == 5:
- cr.execute('delete from '+rel+' where '+id1+' = %s AND '+reference_column+'=%s', (id, model._name))
- elif act[0] == 6:
- d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
- if d1:
- d1 = ' and ' + ' and '.join(d1)
- else:
- d1 = ''
- cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+reference_column+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2)
- for act_nbr in act[2]:
- cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act_nbr, model._name))
- # cases 1, 2: performs write and unlink -> default implementation is ok
- else:
- return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
-
class mail_thread(osv.AbstractModel):
''' mail_thread model is meant to be inherited by any model that needs to
@@ -186,6 +129,62 @@ class mail_thread(osv.AbstractModel):
res[notif.message_id.res_id] = True
return [('id', 'in', res.keys())]
+ def _get_followers(self, cr, uid, ids, name, arg, context=None):
+ fol_obj = self.pool.get('mail.followers')
+ fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids)])
+ res = dict((res_id, []) for res_id in ids)
+ for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids):
+ res[fol.res_id].append(fol.partner_id.id)
+ return res
+
+ def _set_followers(self, cr, uid, id, name, value, arg, context=None):
+ partner_obj = self.pool.get('res.partner')
+ fol_obj = self.pool.get('mail.followers')
+
+ # read the old set of followers, and determine the new set of followers
+ fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', '=', id)])
+ old = set(fol.partner_id.id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids))
+ new = set(old)
+
+ for command in value:
+ if isinstance(command, (int, long)):
+ new.add(command)
+ elif command[0] == 0:
+ new.add(partner_obj.create(cr, uid, command[2], context=context))
+ elif command[0] == 1:
+ partner_obj.write(cr, uid, [command[1]], command[2], context=context)
+ new.add(command[1])
+ elif command[0] == 2:
+ partner_obj.unlink(cr, uid, [command[1]], context=context)
+ new.discard(command[1])
+ elif command[0] == 3:
+ new.discard(command[1])
+ elif command[0] == 4:
+ new.add(command[1])
+ elif command[0] == 5:
+ new.clear()
+ elif command[0] == 6:
+ new = set(command[2])
+
+ # remove partners that are no longer followers
+ fol_ids = fol_obj.search(cr, SUPERUSER_ID,
+ [('res_model', '=', self._name), ('res_id', '=', id), ('partner_id', 'not in', list(new))])
+ fol_obj.unlink(cr, SUPERUSER_ID, fol_ids)
+
+ # add new followers
+ for partner_id in new - old:
+ fol_obj.create(cr, SUPERUSER_ID, {'res_model': self._name, 'res_id': id, 'partner_id': partner_id})
+
+ def _search_followers(self, cr, uid, obj, name, args, context):
+ fol_obj = self.pool.get('mail.followers')
+ res = []
+ for field, operator, value in args:
+ assert field == name
+ fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
+ res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
+ res.append(('id', 'in', res_ids))
+ return res
+
_columns = {
'message_is_follower': fields.function(_get_subscription_data,
type='boolean', string='Is a Follower', multi='_get_subscription_data,'),
@@ -194,9 +193,8 @@ class mail_thread(osv.AbstractModel):
help="Holds data about the subtypes. The content of this field "\
"is a structure holding the current model subtypes, and the "\
"current document followed subtypes."),
- 'message_follower_ids': many2many_reference('res.partner',
- 'mail_followers', 'res_id', 'partner_id',
- reference_column='res_model', string='Followers'),
+ 'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
+ fnct_search=_search_followers, type='many2many', obj='res.partner', string='Followers'),
'message_comment_ids': fields.one2many('mail.message', 'res_id',
domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))],
string='Comments and emails',
diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml
index b503565bd11..be3bb667628 100644
--- a/addons/mail/security/mail_security.xml
+++ b/addons/mail/security/mail_security.xml
@@ -7,7 +7,7 @@
Mail.group: access only public and joined groups
- ['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.id]), '&', ('public','=','groups'), ('group_public_id','in', [x.id for x in user.groups_id])]
+ ['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.partner_id.id]), '&', ('public','=','groups'), ('group_public_id','in', [g.id for g in user.groups_id])]
diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py
index 10392be53ff..83b10b39c28 100644
--- a/addons/portal/tests/test_portal.py
+++ b/addons/portal/tests/test_portal.py
@@ -33,6 +33,7 @@ class test_portal(test_mail.TestMailMockups):
self.mail_group = self.registry('mail.group')
self.mail_mail = self.registry('mail.mail')
self.mail_message = self.registry('mail.message')
+ self.mail_invite = self.registry('mail.wizard.invite')
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
@@ -41,13 +42,13 @@ class test_portal(test_mail.TestMailMockups):
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
# Find Portal group
- group_portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal')
- self.group_portal_id = group_portal_ref and group_portal_ref[1] or False
+ group_portal = self.registry('ir.model.data').get_object(cr, uid, 'portal', 'group_portal')
+ self.group_portal_id = group_portal.id
# Create Chell (portal user)
self.user_chell_id = self.res_users.create(cr, uid, {'name': 'Chell Gladys', 'login': 'chell', 'groups_id': [(6, 0, [self.group_portal_id])]})
- self.user_chell = self.res_users.browse(cr, uid, self.user_chell_id)
- self.partner_chell_id = self.user_chell.partner_id.id
+ user_chell = self.res_users.browse(cr, uid, self.user_chell_id)
+ self.partner_chell_id = user_chell.partner_id.id
# Set an email address for the user running the tests, used as Sender for outgoing mails
self.res_users.write(cr, uid, uid, {'email': 'test@localhost'})
@@ -55,8 +56,6 @@ class test_portal(test_mail.TestMailMockups):
def test_00_access_rights(self):
""" Test basic mail_message and mail_group access rights for portal users. """
cr, uid = self.cr, self.uid
- partner_chell_id = self.partner_chell_id
- user_chell_id = self.user_chell_id
# Prepare group: Pigs (portal)
self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message')
@@ -67,56 +66,56 @@ class test_portal(test_mail.TestMailMockups):
# ----------------------------------------
# Do: Chell reads Pigs messages, ok because restricted to portal group
- message_ids = self.mail_group.read(cr, user_chell_id, self.group_pigs_id, ['message_ids'])['message_ids']
- self.mail_message.read(cr, user_chell_id, message_ids)
+ message_ids = self.mail_group.read(cr, self.user_chell_id, self.group_pigs_id, ['message_ids'])['message_ids']
+ self.mail_message.read(cr, self.user_chell_id, message_ids)
+
# Do: Chell posts a message on Pigs, crash because can not write on group or is not in the followers
- self.assertRaises(except_orm,
- self.mail_group.message_post,
- cr, user_chell_id, self.group_pigs_id, body='Message')
+ with self.assertRaises(except_orm):
+ self.mail_group.message_post(cr, self.user_chell_id, self.group_pigs_id, body='Message')
+
# Do: Chell is added to Pigs followers
- self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [partner_chell_id])
+ self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [self.partner_chell_id])
+
# Test: Chell posts a message on Pigs, ok because in the followers
- self.mail_group.message_post(cr, user_chell_id, self.group_pigs_id, body='Message')
+ self.mail_group.message_post(cr, self.user_chell_id, self.group_pigs_id, body='Message')
def test_50_mail_invite(self):
cr, uid = self.cr, self.uid
user_admin = self.res_users.browse(cr, uid, uid)
- self.mail_invite = self.registry('mail.wizard.invite')
base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='')
- portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_portal')
- portal_id = portal_ref and portal_ref[1] or False
-
# 0 - Admin
- p_a_id = user_admin.partner_id.id
+ partner_admin_id = user_admin.partner_id.id
# 1 - Bert Tartopoils, with email, should receive emails for comments and emails
- p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
+ partner_bert_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
# ----------------------------------------
- # CASE1: generated URL
+ # CASE: invite Bert to follow Pigs
# ----------------------------------------
- url = self.mail_mail._generate_signin_url(cr, uid, p_b_id, portal_id, 1234)
- self.assertEqual(url, base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (p_b_id, portal_id, 1234),
- 'generated signin URL incorrect')
-
- # ----------------------------------------
- # CASE2: invite Bert
- # ----------------------------------------
-
- _sent_email_subject = 'Invitation to follow Pigs'
- _sent_email_body = append_content_to_html('
You have been invited to follow Pigs.
', url)
-
# Do: create a mail_wizard_invite, validate it
self._init_mock_build_email()
- mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, p_b_id)]}, {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id})
+ context = {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id}
+ mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, partner_bert_id)]}, context)
self.mail_invite.add_followers(cr, uid, [mail_invite_id])
- group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
# Test: Pigs followers should contain Admin and Bert
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
- self.assertEqual(set(follower_ids), set([p_a_id, p_b_id]), 'Pigs followers after invite is incorrect')
- # Test: sent email subject, body
+ self.assertEqual(set(follower_ids), set([partner_admin_id, partner_bert_id]), 'Pigs followers after invite is incorrect')
+
+ # Test: partner must have been prepared for signup
+ partner_bert = self.res_partner.browse(cr, uid, partner_bert_id)
+ self.assertTrue(partner_bert.signup_valid, 'partner has not been prepared for signup')
+ self.assertTrue(base_url in partner_bert.signup_url, 'signup url is incorrect')
+ self.assertTrue(cr.dbname in partner_bert.signup_url, 'signup url is incorrect')
+ self.assertTrue(partner_bert.signup_token in partner_bert.signup_url, 'signup url is incorrect')
+
+ # Test: (pretend to) send email and check subject, body
self.assertEqual(len(self._build_email_kwargs_list), 1, 'sent email number incorrect, should be only for Bert')
for sent_email in self._build_email_kwargs_list:
- self.assertEqual(sent_email.get('subject'), _sent_email_subject, 'sent email subject incorrect')
- self.assertEqual(sent_email.get('body'), _sent_email_body, 'sent email body incorrect')
+ self.assertEqual(sent_email.get('subject'), 'Invitation to follow Pigs',
+ 'subject of invitation email is incorrect')
+ self.assertTrue('You have been invited to follow Pigs' in sent_email.get('body'),
+ 'body of invitation email is incorrect')
+ self.assertTrue(partner_bert.signup_url in sent_email.get('body'),
+ 'body of invitation email does not contain signup url')
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index 3cc706c2129..c94d6509f63 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -30,17 +30,19 @@ from openerp import SUPERUSER_ID
from base.res.res_partner import _lang_get
_logger = logging.getLogger(__name__)
-# welcome/goodbye email sent to portal users
+# welcome email sent to portal users
# (note that calling '_' has no effect except exporting those strings for translation)
WELCOME_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s")
WELCOME_EMAIL_BODY = _("""Dear %(name)s,
-You have been given access to %(portal)s at %(url)s.
+You have been given access to %(portal)s.
Your login account data is:
Database: %(db)s
-User: %(login)s
-Password: %(password)s
+Username: %(login)s
+
+In order to complete the signin process, click on the following url:
+%(url)s
%(welcome_message)s
@@ -49,28 +51,10 @@ OpenERP - Open Source Business Applications
http://www.openerp.com
""")
-GOODBYE_EMAIL_SUBJECT = _("Your OpenERP account at %(company)s")
-GOODBYE_EMAIL_BODY = _("""Dear %(name)s,
-
-Your access to %(portal)s has been withdrawn.
-
-%(goodbye_message)s
-
---
-OpenERP - Open Source Business Applications
-http://www.openerp.com
-""")
-
-# character sets for passwords, excluding 0, O, o, 1, I, l
-_PASSU = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'
-_PASSL = 'abcdefghijkmnpqrstuvwxyz'
-_PASSD = '23456789'
-
def random_password():
- # get 3 uppercase letters, 3 lowercase letters, 2 digits, and shuffle them
- chars = map(random.choice, [_PASSU] * 3 + [_PASSL] * 3 + [_PASSD] * 2)
- random.shuffle(chars)
- return ''.join(chars)
+ # temporary random stuff; user password is reset by signup process
+ chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ return ''.join(random.choice(chars) for i in xrange(12))
def extract_email(email):
""" extract the email address from a user-friendly email address """
@@ -92,8 +76,6 @@ class wizard(osv.osv_memory):
'user_ids': fields.one2many('portal.wizard.user', 'wizard_id', string='Users'),
'welcome_message': fields.text(string='Invitation Message',
help="This text is included in the email sent to new users of the portal."),
- 'goodbye_message': fields.text(string='Withdrawal Message',
- help="This text is included in the email sent to users withdrawn from the portal."),
}
def _default_portal(self, cr, uid, context):
@@ -164,6 +146,8 @@ class wizard_user(osv.osv_memory):
user = self._create_user(cr, SUPERUSER_ID, wizard_user, context)
if (not user.active) or (portal not in user.groups_id):
user.write({'active': True, 'groups_id': [(4, portal.id)]})
+ # prepare for the signup process
+ user.partner_id.signup_prepare()
wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context)
self._send_email(cr, uid, wizard_user, context)
else:
@@ -174,8 +158,6 @@ class wizard_user(osv.osv_memory):
user.write({'groups_id': [(3, portal.id)], 'active': False})
else:
user.write({'groups_id': [(3, portal.id)]})
- wizard_user = self.browse(cr, SUPERUSER_ID, wizard_user.id, context)
- self._send_email(cr, uid, wizard_user, context)
def _retrieve_user(self, cr, uid, wizard_user, context=None):
""" retrieve the (possibly inactive) user corresponding to wizard_user.partner_id
@@ -208,7 +190,7 @@ class wizard_user(osv.osv_memory):
return res_users.browse(cr, uid, user_id, context)
def _send_email(self, cr, uid, wizard_user, context=None):
- """ send notification email to a new/former portal user
+ """ send notification email to a new portal user
@param wizard_user: browse record of model portal.wizard.user
@return: the id of the created mail.mail record
"""
@@ -219,33 +201,23 @@ class wizard_user(osv.osv_memory):
_('You must have an email address in your User Preferences to send emails.'))
# determine subject and body in the portal user's language
- url = self.pool.get('ir.config_parameter').get_param(cr, SUPERUSER_ID, 'web.base.url', context=this_context)
user = self._retrieve_user(cr, SUPERUSER_ID, wizard_user, context)
context = dict(this_context or {}, lang=user.lang)
data = {
'company': this_user.company_id.name,
'portal': wizard_user.wizard_id.portal_id.name,
'welcome_message': wizard_user.wizard_id.welcome_message or "",
- 'goodbye_message': wizard_user.wizard_id.goodbye_message or "",
- 'url': url or _("(missing url)"),
'db': cr.dbname,
+ 'name': user.name,
'login': user.login,
- 'password': user.password,
- 'name': user.name
+ 'url': user.signup_url,
}
- if wizard_user.in_portal:
- subject = _(WELCOME_EMAIL_SUBJECT) % data
- body = _(WELCOME_EMAIL_BODY) % data
- else:
- subject = _(GOODBYE_EMAIL_SUBJECT) % data
- body = _(GOODBYE_EMAIL_BODY) % data
-
mail_mail = self.pool.get('mail.mail')
mail_values = {
'email_from': this_user.email,
'email_to': user.email,
- 'subject': subject,
- 'body_html': '