+
+
+
diff --git a/addons/auth_signup/models/base_config_settings.py b/addons/auth_signup/models/base_config_settings.py
index eb830c24e37..8ea0a8f2041 100644
--- a/addons/auth_signup/models/base_config_settings.py
+++ b/addons/auth_signup/models/base_config_settings.py
@@ -2,24 +2,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
-from odoo.tools.safe_eval import safe_eval
+
class BaseConfigSettings(models.TransientModel):
_inherit = 'base.config.settings'
auth_signup_reset_password = fields.Boolean(string='Enable password reset from Login page')
- auth_signup_uninvited = fields.Boolean(string='Allow external users to sign up')
+ auth_signup_uninvited = fields.Selection([
+ ('b2b', 'On invitation (B2B)'),
+ ('b2c', 'Free sign up (B2C)'),
+ ], string='Customer Account')
auth_signup_template_user_id = fields.Many2one('res.users', string='Template user for new users created through signup')
@api.model
def get_values(self):
res = super(BaseConfigSettings, self).get_values()
get_param = self.env['ir.config_parameter'].sudo().get_param
- # we use safe_eval on the result, since the value of the parameter is a nonempty string
+ # the value of the parameter is a nonempty string
res.update(
- auth_signup_reset_password=safe_eval(get_param('auth_signup.reset_password', 'False')),
- auth_signup_uninvited=safe_eval(get_param('auth_signup.allow_uninvited', 'False')),
- auth_signup_template_user_id=safe_eval(get_param('auth_signup.template_user_id', 'False')),
+ auth_signup_reset_password=get_param('auth_signup.reset_password', 'False').lower() == 'true',
+ auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b',
+ auth_signup_template_user_id=get_param('auth_signup.template_user_id', 'False').lower() == 'true',
)
return res
@@ -29,7 +32,7 @@ class BaseConfigSettings(models.TransientModel):
set_param = self.env['ir.config_parameter'].sudo().set_param
# we store the repr of the values, since the value of the parameter is a required string
set_param('auth_signup.reset_password', repr(self.auth_signup_reset_password))
- set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited))
+ set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c'))
set_param('auth_signup.template_user_id', repr(self.auth_signup_template_user_id.id))
@api.multi
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index d8111420a3c..302d69f173e 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -4,9 +4,10 @@
import random
import werkzeug.urls
+from collections import defaultdict
from datetime import datetime, timedelta
-from odoo import api, fields, models, _
+from odoo import api, exceptions, fields, models, _
from odoo.tools import pycompat
@@ -97,6 +98,21 @@ class ResPartner(models.Model):
def action_signup_prepare(self):
return self.signup_prepare()
+ def signup_get_auth_param(self):
+ """ Get a signup token related to the partner if signup is enabled.
+ If the partner already has a user, get the login parameter.
+ """
+ res = defaultdict(dict)
+
+ allow_signup = self.env['ir.config_parameter'].get_param('auth_signup.allow_uninvited', 'False').lower() == 'true'
+ for partner in self:
+ if allow_signup and not partner.user_ids:
+ partner.signup_prepare()
+ res[partner.id]['auth_signup_token'] = partner.signup_token
+ elif partner.user_ids:
+ res[partner.id]['auth_login'] = partner.user_ids[0].login
+ return res
+
@api.multi
def signup_cancel(self):
return self.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False})
@@ -125,11 +141,11 @@ class ResPartner(models.Model):
partner = self.search([('signup_token', '=', token)], limit=1)
if not partner:
if raise_exception:
- raise SignupError("Signup token '%s' is not valid" % token)
+ raise exceptions.UserError(_("Signup token '%s' is not valid") % token)
return False
if check_validity and not partner.signup_valid:
if raise_exception:
- raise SignupError("Signup token '%s' is no longer valid" % token)
+ raise exceptions.UserError(_("Signup token '%s' is no longer valid") % token)
return False
return partner
diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py
index 05d52596c85..b58606b73ab 100644
--- a/addons/auth_signup/models/res_users.py
+++ b/addons/auth_signup/models/res_users.py
@@ -85,7 +85,7 @@ class ResUsers(models.Model):
# check that uninvited users may sign up
if 'partner_id' not in values:
if not literal_eval(get_param('auth_signup.allow_uninvited', 'False')):
- raise SignupError('Signup is not allowed for uninvited users')
+ raise SignupError(_('Signup is not allowed for uninvited users'))
assert values.get('login'), "Signup: no login given for new user"
assert values.get('partner_id') or values.get('name'), "Signup: no name or partner given for new user"
diff --git a/addons/auth_signup/views/auth_signup_login_templates.xml b/addons/auth_signup/views/auth_signup_login_templates.xml
index b3e17efac90..a38596abe9c 100644
--- a/addons/auth_signup/views/auth_signup_login_templates.xml
+++ b/addons/auth_signup/views/auth_signup_login_templates.xml
@@ -2,7 +2,7 @@
- Sign up
+ Don't have an account?Reset Password
@@ -40,7 +40,7 @@
-
+
Dear ${object.partner_id.name}
% set access_action = object.with_context(force_website=True).get_access_action()
% set is_online = access_action and access_action['type'] == 'ir.actions.act_url'
-% set access_url = is_online and '/mail/view?model=account.invoice&res_id=%d' % (object.id) or '/report/pdf/account.report_invoice/' + str(object.id)
+% set access_url = is_online and object.get_mail_url() or '/report/pdf/account.report_invoice/' + str(object.id)
% if object.partner_id.parent_id:
(${object.partner_id.parent_id.name})
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index de58beed4cd..95165441dfd 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -52,7 +52,7 @@ class MailController(http.Controller):
return comparison, record, redirect
@classmethod
- def _redirect_to_record(cls, model, res_id):
+ def _redirect_to_record(cls, model, res_id, access_token=None):
uid = request.session.uid
# no model / res_id, meaning no possible record -> redirect to login
@@ -68,7 +68,6 @@ class MailController(http.Controller):
# the record has a window redirection: check access rights
if uid is not None:
- record = record_sudo.sudo(uid)
if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False):
return cls._redirect_to_messaging()
try:
@@ -76,7 +75,7 @@ class MailController(http.Controller):
except AccessError:
return cls._redirect_to_messaging()
else:
- record_action = record.get_access_action()
+ record_action = record_sudo.get_access_action(access_uid=uid)
else:
record_action = record_sudo.get_access_action()
@@ -162,15 +161,17 @@ class MailController(http.Controller):
return subtypes_list
@http.route('/mail/view', type='http', auth='none')
- def mail_action_view(self, model=None, res_id=None, message_id=None):
+ def mail_action_view(self, model=None, res_id=None, message_id=None, access_token=None, **kwargs):
""" Generic access point from notification emails. The heuristic to
- choose where to redirect the user is the following :
+ choose where to redirect the user is the following :
- find a public URL
- if none found
- users with a read access are redirected to the document
- users without read access are redirected to the Messaging
- not logged users are redirected to the login page
+
+ models that have an access_token may apply variations on this.
"""
if message_id:
try:
@@ -185,7 +186,7 @@ class MailController(http.Controller):
elif res_id and isinstance(res_id, basestring):
res_id = int(res_id)
- return self._redirect_to_record(model, res_id)
+ return self._redirect_to_record(model, res_id, access_token)
@http.route('/mail/follow', type='http', auth='user', methods=['GET'])
def mail_action_follow(self, model, res_id, token=None):
diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml
index 03de4047962..49c205f102b 100644
--- a/addons/sale/data/mail_template_data.xml
+++ b/addons/sale/data/mail_template_data.xml
@@ -20,7 +20,7 @@
% set doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order confirmation'
% set pay_sign_name = ('require_payment' in object and object.require_payment and 'pay') or 'sign'
% set access_name = is_online and object.template_id and object.state in ('draft', 'sent') and 'Accept and %s online' % pay_sign_name or 'View %s' % doc_name
-% set access_url = is_online and access_action['url'] or ''
+% set access_url = is_online and object.get_mail_url() or ''
% if object.partner_id.parent_id:
(${object.partner_id.parent_id.name})
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 18bc98806fc..e2f53d70819 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -293,12 +293,19 @@ class SaleOrder(models.Model):
@api.model_cr_context
def _init_column(self, column_name):
""" Initialize the value of the given column for existing rows.
- Overridden here because we skip generating unique access tokens
- for potentially tons of existing sale orders, should they be needed,
- they will be generated on the fly.
+
+ Overridden here because we need to generate different access tokens
+ and by default _init_column calls the default method once and applies
+ it for every record.
"""
if column_name != 'access_token':
super(SaleOrder, self)._init_column(column_name)
+ else:
+ query = """UPDATE %(table_name)s
+ SET %(column_name)s = md5(md5(random()::varchar || id::varchar) || clock_timestamp()::varchar)::uuid::varchar
+ WHERE %(column_name)s IS NULL
+ """ % {'table_name': self._name, 'column_name': column_name}
+ self.env.cr.execute(query)
def _generate_access_token(self):
for order in self:
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 4184d8b4987..e3265bd08a2 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -488,6 +488,9 @@ class Home(http.Controller):
if 'error' in request.params and request.params.get('error') == 'access':
values['error'] = _('Only employee can access this database. Please contact the administrator.')
+ if 'login' not in values and request.session.get('auth_login'):
+ values['login'] = request.session.get('auth_login')
+
response = request.render('web.login', values)
response.headers['X-Frame-Options'] = 'DENY'
return response
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index a97cebe76d5..4349eaa1774 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -134,6 +134,13 @@ class Http(models.AbstractModel):
first_pass = not hasattr(request, 'website')
request.website = None
func = None
+
+ # add signup token or login to the session if given
+ if 'auth_signup_token' in request.params:
+ request.session['auth_signup_token'] = request.params['auth_signup_token']
+ if 'auth_login' in request.params:
+ request.session['auth_login'] = request.params['auth_login']
+
try:
if request.httprequest.method == 'GET' and '//' in request.httprequest.path:
new_url = request.httprequest.path.replace('//', '/') + '?' + request.httprequest.query_string
diff --git a/addons/website/models/website_config_settings.py b/addons/website/models/website_config_settings.py
index bc5901cdc47..35e20e77290 100644
--- a/addons/website/models/website_config_settings.py
+++ b/addons/website/models/website_config_settings.py
@@ -53,6 +53,10 @@ class WebsiteConfigSettings(models.TransientModel):
has_google_analytics = fields.Boolean("Google Analytics")
has_google_analytics_dashboard = fields.Boolean("Google Analytics in Dashboard")
has_google_maps = fields.Boolean("Google Maps")
+ auth_signup_uninvited = fields.Selection([
+ ('b2b', 'On invitation (B2B)'),
+ ('b2c', 'Free sign up (B2C)'),
+ ], string='Customer Account')
@api.onchange('has_google_analytics')
def onchange_has_google_analytics(self):
@@ -77,12 +81,13 @@ class WebsiteConfigSettings(models.TransientModel):
if not self.user_has_groups('website.group_website_designer'):
raise AccessDenied()
res = super(WebsiteConfigSettings, self).get_values()
- params = self.env['ir.config_parameter'].sudo()
+ get_param = self.env['ir.config_parameter'].sudo().get_param
res.update(
- has_google_analytics=params.get_param('website.has_google_analytics'),
- has_google_analytics_dashboard=params.get_param('website.has_google_analytics_dashboard'),
- has_google_maps=params.get_param('website.has_google_maps'),
- google_maps_api_key=params.get_param('google_maps_api_key', default=''),
+ auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b',
+ has_google_analytics=get_param('website.has_google_analytics'),
+ has_google_analytics_dashboard=get_param('website.has_google_analytics_dashboard'),
+ has_google_maps=get_param('website.has_google_maps'),
+ google_maps_api_key=get_param('google_maps_api_key', default=''),
)
return res
@@ -90,7 +95,9 @@ class WebsiteConfigSettings(models.TransientModel):
if not self.user_has_groups('website.group_website_designer'):
raise AccessDenied()
super(WebsiteConfigSettings, self).set_values()
- self.env['ir.config_parameter'].sudo().set_param('website.has_google_analytics', self.has_google_analytics)
- self.env['ir.config_parameter'].sudo().set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard)
- self.env['ir.config_parameter'].sudo().set_param('website.has_google_maps', self.has_google_maps)
- self.env['ir.config_parameter'].sudo().set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
+ set_param = self.env['ir.config_parameter'].sudo().set_param
+ set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c'))
+ set_param('website.has_google_analytics', self.has_google_analytics)
+ set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard)
+ set_param('website.has_google_maps', self.has_google_maps)
+ set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
diff --git a/addons/website/views/website_config_settings_views.xml b/addons/website/views/website_config_settings_views.xml
index 32f0a61e2d9..fe04057717b 100644
--- a/addons/website/views/website_config_settings_views.xml
+++ b/addons/website/views/website_config_settings_views.xml
@@ -147,6 +147,19 @@
+
+
+
+
+
+
+ Let your customers log in to see their documents
+
+
+
+
+
+
Social Media
diff --git a/addons/website_account/models/account_invoice.py b/addons/website_account/models/account_invoice.py
index 088131d83d5..3c2c086f454 100644
--- a/addons/website_account/models/account_invoice.py
+++ b/addons/website_account/models/account_invoice.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, models
+from werkzeug.urls import url_encode
class AccountInvoice(models.Model):
@@ -17,22 +18,36 @@ class AccountInvoice(models.Model):
return groups
@api.multi
- def get_access_action(self):
+ def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the online invoice for portal users. """
self.ensure_one()
- if self.env.user.share or self.env.context.get('force_website'):
+ user, record = self.env.user, self
+ if access_uid:
+ user = self.env['res.users'].sudo().browse(access_uid)
+ record = self.sudo(user)
+
+ if user.share or self.env.context.get('force_website'):
try:
- self.check_access_rule('read')
+ record.check_access_rule('read')
except exceptions.AccessError:
pass
else:
return {
'type': 'ir.actions.act_url',
- 'url': '/my/invoices', # No controller /my/invoices/, only a report pdf
+ 'url': '/my/invoices?', # No controller /my/invoices/, only a report pdf
'target': 'self',
'res_id': self.id,
}
- return super(AccountInvoice, self).get_access_action()
+ return super(AccountInvoice, self).get_access_action(access_uid)
+
+ def get_mail_url(self):
+ self.ensure_one()
+ params = {
+ 'model': self._name,
+ 'res_id': self.id,
+ }
+ params.update(self.partner_id.signup_get_auth_param()[self.partner_id.id])
+ return '/mail/view?' + url_encode(params)
@api.multi
def get_signup_url(self):
diff --git a/addons/website_account/views/account_templates.xml b/addons/website_account/views/account_templates.xml
index ad9cd8c46b6..0d0a1b98daa 100644
--- a/addons/website_account/views/account_templates.xml
+++ b/addons/website_account/views/account_templates.xml
@@ -1,19 +1,17 @@
-
-
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index f10f3fe0745..22456302949 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -216,12 +216,13 @@ class BlogPost(models.Model):
return result
@api.multi
- def get_access_action(self):
+ def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to the post on website
directly if user is an employee or if the post is published. """
self.ensure_one()
- if self.env.user.share and not self.sudo().website_published:
- return super(BlogPost, self).get_access_action()
+ user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
+ if user.share and not self.sudo().website_published:
+ return super(BlogPost, self).get_access_action(access_uid)
return {
'type': 'ir.actions.act_url',
'url': self.url,
diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
index c5db1490a02..8b92d401113 100644
--- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
+++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
@@ -193,17 +193,29 @@
-
-
-
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index 6607e5cb4ac..83252665e96 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -434,7 +434,7 @@ class Slide(models.Model):
return fields
@api.multi
- def get_access_action(self):
+ def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website if it is published. """
self.ensure_one()
if self.website_published:
@@ -444,7 +444,7 @@ class Slide(models.Model):
'target': 'self',
'res_id': self.id,
}
- return super(Slide, self).get_access_action()
+ return super(Slide, self).get_access_action(access_uid)
@api.multi
def _notification_recipients(self, message, groups):
diff --git a/odoo/models.py b/odoo/models.py
index 6be8a3d443e..fa644aac363 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1339,12 +1339,14 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return False
@api.multi
- def get_formview_action(self):
+ def get_formview_action(self, access_uid=None):
""" Return an action to open the document ``self``. This method is meant
to be overridden in addons that want to give specific view ids for
example.
- """
- view_id = self.sudo().get_formview_id(access_uid=self.env.uid)
+
+ An optional access_uid holds the user that will access the document
+ that could be different from the current user. """
+ view_id = self.sudo().get_formview_id(access_uid=access_uid)
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
@@ -1357,12 +1359,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
}
@api.multi
- def get_access_action(self):
+ def get_access_action(self, access_uid=None):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific access to the document.
By default it opens the formview of the document.
+
+ An optional access_uid holds the user that will access the document
+ that could be different from the current user.
"""
- return self[0].get_formview_action()
+ return self[0].get_formview_action(access_uid=access_uid)
@api.model
def search_count(self, args):