summaryrefslogtreecommitdiff
path: root/addons/portal/controllers
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/portal/controllers
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/portal/controllers')
-rw-r--r--addons/portal/controllers/__init__.py6
-rw-r--r--addons/portal/controllers/mail.py247
-rw-r--r--addons/portal/controllers/portal.py437
-rw-r--r--addons/portal/controllers/web.py26
4 files changed, 716 insertions, 0 deletions
diff --git a/addons/portal/controllers/__init__.py b/addons/portal/controllers/__init__.py
new file mode 100644
index 00000000..c1b076c5
--- /dev/null
+++ b/addons/portal/controllers/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import web
+from . import portal
+from . import mail
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
new file mode 100644
index 00000000..612191b7
--- /dev/null
+++ b/addons/portal/controllers/mail.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import werkzeug
+from werkzeug import urls
+from werkzeug.exceptions import NotFound, Forbidden
+
+from odoo import http, _
+from odoo.http import request
+from odoo.osv import expression
+from odoo.tools import consteq, plaintext2html
+from odoo.addons.mail.controllers.main import MailController
+from odoo.addons.portal.controllers.portal import CustomerPortal
+from odoo.exceptions import AccessError, MissingError, UserError
+
+
+def _check_special_access(res_model, res_id, token='', _hash='', pid=False):
+ record = request.env[res_model].browse(res_id).sudo()
+ if token: # Token Case: token is the global one of the document
+ token_field = request.env[res_model]._mail_post_token_field
+ return (token and record and consteq(record[token_field], token))
+ elif _hash and pid: # Signed Token Case: hash implies token is signed by partner pid
+ return consteq(_hash, record._sign_token(pid))
+ else:
+ raise Forbidden()
+
+
+def _message_post_helper(res_model, res_id, message, token='', _hash=False, pid=False, nosubscribe=True, **kw):
+ """ Generic chatter function, allowing to write on *any* object that inherits mail.thread. We
+ distinguish 2 cases:
+ 1/ If a token is specified, all logged in users will be able to write a message regardless
+ of access rights; if the user is the public user, the message will be posted under the name
+ of the partner_id of the object (or the public user if there is no partner_id on the object).
+
+ 2/ If a signed token is specified (`hash`) and also a partner_id (`pid`), all post message will
+ be done under the name of the partner_id (as it is signed). This should be used to avoid leaking
+ token to all users.
+
+ Required parameters
+ :param string res_model: model name of the object
+ :param int res_id: id of the object
+ :param string message: content of the message
+
+ Optional keywords arguments:
+ :param string token: access token if the object's model uses some kind of public access
+ using tokens (usually a uuid4) to bypass access rules
+ :param string hash: signed token by a partner if model uses some token field to bypass access right
+ post messages.
+ :param string pid: identifier of the res.partner used to sign the hash
+ :param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True)
+
+ The rest of the kwargs are passed on to message_post()
+ """
+ record = request.env[res_model].browse(res_id)
+
+ # check if user can post with special token/signed token. The "else" will try to post message with the
+ # current user access rights (_mail_post_access use case).
+ if token or (_hash and pid):
+ pid = int(pid) if pid else False
+ if _check_special_access(res_model, res_id, token=token, _hash=_hash, pid=pid):
+ record = record.sudo()
+ else:
+ raise Forbidden()
+
+ # deduce author of message
+ author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
+
+ # Token Case: author is document customer (if not logged) or itself even if user has not the access
+ if token:
+ if request.env.user._is_public():
+ # TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line
+ # TODO : Author must be Public User (to rename to 'Anonymous')
+ author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id
+ else:
+ if not author_id:
+ raise NotFound()
+ # Signed Token Case: author_id is forced
+ elif _hash and pid:
+ author_id = pid
+
+ email_from = None
+ if author_id and 'email_from' not in kw:
+ partner = request.env['res.partner'].sudo().browse(author_id)
+ email_from = partner.email_formatted if partner.email else None
+
+ message_post_args = dict(
+ body=message,
+ message_type=kw.pop('message_type', "comment"),
+ subtype_xmlid=kw.pop('subtype_xmlid', "mail.mt_comment"),
+ author_id=author_id,
+ **kw
+ )
+
+ # This is necessary as mail.message checks the presence
+ # of the key to compute its default email from
+ if email_from:
+ message_post_args['email_from'] = email_from
+
+ return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(**message_post_args)
+
+
+class PortalChatter(http.Controller):
+
+ def _portal_post_filter_params(self):
+ return ['token', 'hash', 'pid']
+
+ def _portal_post_check_attachments(self, attachment_ids, attachment_tokens):
+ if len(attachment_tokens) != len(attachment_ids):
+ raise UserError(_("An access token must be provided for each attachment."))
+ for (attachment_id, access_token) in zip(attachment_ids, attachment_tokens):
+ try:
+ CustomerPortal._document_check_access(self, 'ir.attachment', attachment_id, access_token)
+ except (AccessError, MissingError):
+ raise UserError(_("The attachment %s does not exist or you do not have the rights to access it.", attachment_id))
+
+ @http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
+ def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids='', attachment_tokens='', **kw):
+ """Create a new `mail.message` with the given `message` and/or
+ `attachment_ids` and redirect the user to the newly created message.
+
+ The message will be associated to the record `res_id` of the model
+ `res_model`. The user must have access rights on this target document or
+ must provide valid identifiers through `kw`. See `_message_post_helper`.
+ """
+ url = redirect or (request.httprequest.referrer and request.httprequest.referrer + "#discussion") or '/my'
+
+ res_id = int(res_id)
+
+ attachment_ids = [int(attachment_id) for attachment_id in attachment_ids.split(',') if attachment_id]
+ attachment_tokens = [attachment_token for attachment_token in attachment_tokens.split(',') if attachment_token]
+ self._portal_post_check_attachments(attachment_ids, attachment_tokens)
+
+ if message or attachment_ids:
+ # message is received in plaintext and saved in html
+ if message:
+ message = plaintext2html(message)
+ post_values = {
+ 'res_model': res_model,
+ 'res_id': res_id,
+ 'message': message,
+ 'send_after_commit': False,
+ 'attachment_ids': False, # will be added afterward
+ }
+ post_values.update((fname, kw.get(fname)) for fname in self._portal_post_filter_params())
+ message = _message_post_helper(**post_values)
+
+ if attachment_ids:
+ # sudo write the attachment to bypass the read access
+ # verification in mail message
+ record = request.env[res_model].browse(res_id)
+ message_values = {'res_id': res_id, 'model': res_model}
+ attachments = record._message_post_process_attachments([], attachment_ids, message_values)
+
+ if attachments.get('attachment_ids'):
+ message.sudo().write(attachments)
+
+ return request.redirect(url)
+
+ @http.route('/mail/chatter_init', type='json', auth='public', website=True)
+ def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
+ is_user_public = request.env.user.has_group('base.group_public')
+ message_data = self.portal_message_fetch(res_model, res_id, domain=domain, limit=limit, **kwargs)
+ display_composer = False
+ if kwargs.get('allow_composer'):
+ display_composer = kwargs.get('token') or not is_user_public
+ return {
+ 'messages': message_data['messages'],
+ 'options': {
+ 'message_count': message_data['message_count'],
+ 'is_user_public': is_user_public,
+ 'is_user_employee': request.env.user.has_group('base.group_user'),
+ 'is_user_publisher': request.env.user.has_group('website.group_website_publisher'),
+ 'display_composer': display_composer,
+ 'partner_id': request.env.user.partner_id.id
+ }
+ }
+
+ @http.route('/mail/chatter_fetch', type='json', auth='public', website=True)
+ def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
+ if not domain:
+ domain = []
+ # Only search into website_message_ids, so apply the same domain to perform only one search
+ # extract domain from the 'website_message_ids' field
+ model = request.env[res_model]
+ field = model._fields['website_message_ids']
+ field_domain = field.get_domain_list(model)
+ domain = expression.AND([domain, field_domain, [('res_id', '=', res_id)]])
+
+ # Check access
+ Message = request.env['mail.message']
+ if kw.get('token'):
+ access_as_sudo = _check_special_access(res_model, res_id, token=kw.get('token'))
+ if not access_as_sudo: # if token is not correct, raise Forbidden
+ raise Forbidden()
+ # Non-employee see only messages with not internal subtype (aka, no internal logs)
+ if not request.env['res.users'].has_group('base.group_user'):
+ domain = expression.AND([Message._get_search_domain_share(), domain])
+ Message = request.env['mail.message'].sudo()
+ return {
+ 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
+ 'message_count': Message.search_count(domain)
+ }
+
+ @http.route(['/mail/update_is_internal'], type='json', auth="user", website=True)
+ def portal_message_update_is_internal(self, message_id, is_internal):
+ message = request.env['mail.message'].browse(int(message_id))
+ message.write({'is_internal': is_internal})
+ return message.is_internal
+
+
+class MailController(MailController):
+
+ @classmethod
+ def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
+ """ If the current user doesn't have access to the document, but provided
+ a valid access token, redirect him to the front-end view.
+ If the partner_id and hash parameters are given, add those parameters to the redirect url
+ to authentify the recipient in the chatter, if any.
+
+ :param model: the model name of the record that will be visualized
+ :param res_id: the id of the record
+ :param access_token: token that gives access to the record
+ bypassing the rights and rules restriction of the user.
+ :param kwargs: Typically, it can receive a partner_id and a hash (sign_token).
+ If so, those two parameters are used to authentify the recipient in the chatter, if any.
+ :return:
+ """
+ if issubclass(type(request.env[model]), request.env.registry['portal.mixin']):
+ uid = request.session.uid or request.env.ref('base.public_user').id
+ record_sudo = request.env[model].sudo().browse(res_id).exists()
+ try:
+ record_sudo.with_user(uid).check_access_rights('read')
+ record_sudo.with_user(uid).check_access_rule('read')
+ except AccessError:
+ if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token):
+ record_action = record_sudo.with_context(force_website=True).get_access_action()
+ if record_action['type'] == 'ir.actions.act_url':
+ pid = kwargs.get('pid')
+ hash = kwargs.get('hash')
+ url = record_action['url']
+ if pid and hash:
+ url = urls.url_parse(url)
+ url_params = url.decode_query()
+ url_params.update([("pid", pid), ("hash", hash)])
+ url = url.replace(query=urls.url_encode(url_params)).to_url()
+ return werkzeug.utils.redirect(url)
+ return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token)
diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py
new file mode 100644
index 00000000..f0f4920a
--- /dev/null
+++ b/addons/portal/controllers/portal.py
@@ -0,0 +1,437 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import functools
+import json
+import logging
+import math
+import re
+
+from werkzeug import urls
+
+from odoo import fields as odoo_fields, http, tools, _, SUPERUSER_ID
+from odoo.exceptions import ValidationError, AccessError, MissingError, UserError, AccessDenied
+from odoo.http import content_disposition, Controller, request, route
+from odoo.tools import consteq
+
+# --------------------------------------------------
+# Misc tools
+# --------------------------------------------------
+
+_logger = logging.getLogger(__name__)
+def pager(url, total, page=1, step=30, scope=5, url_args=None):
+ """ Generate a dict with required value to render `website.pager` template. This method compute
+ url, page range to display, ... in the pager.
+ :param url : base url of the page link
+ :param total : number total of item to be splitted into pages
+ :param page : current page
+ :param step : item per page
+ :param scope : number of page to display on pager
+ :param url_args : additionnal parameters to add as query params to page url
+ :type url_args : dict
+ :returns dict
+ """
+ # Compute Pager
+ page_count = int(math.ceil(float(total) / step))
+
+ page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
+ scope -= 1
+
+ pmin = max(page - int(math.floor(scope/2)), 1)
+ pmax = min(pmin + scope, page_count)
+
+ if pmax - pmin < scope:
+ pmin = pmax - scope if pmax - scope > 0 else 1
+
+ def get_url(page):
+ _url = "%s/page/%s" % (url, page) if page > 1 else url
+ if url_args:
+ _url = "%s?%s" % (_url, urls.url_encode(url_args))
+ return _url
+
+ return {
+ "page_count": page_count,
+ "offset": (page - 1) * step,
+ "page": {
+ 'url': get_url(page),
+ 'num': page
+ },
+ "page_first": {
+ 'url': get_url(1),
+ 'num': 1
+ },
+ "page_start": {
+ 'url': get_url(pmin),
+ 'num': pmin
+ },
+ "page_previous": {
+ 'url': get_url(max(pmin, page - 1)),
+ 'num': max(pmin, page - 1)
+ },
+ "page_next": {
+ 'url': get_url(min(pmax, page + 1)),
+ 'num': min(pmax, page + 1)
+ },
+ "page_end": {
+ 'url': get_url(pmax),
+ 'num': pmax
+ },
+ "page_last": {
+ 'url': get_url(page_count),
+ 'num': page_count
+ },
+ "pages": [
+ {'url': get_url(page_num), 'num': page_num} for page_num in range(pmin, pmax+1)
+ ]
+ }
+
+
+def get_records_pager(ids, current):
+ if current.id in ids and (hasattr(current, 'website_url') or hasattr(current, 'access_url')):
+ attr_name = 'access_url' if hasattr(current, 'access_url') else 'website_url'
+ idx = ids.index(current.id)
+ return {
+ 'prev_record': idx != 0 and getattr(current.browse(ids[idx - 1]), attr_name),
+ 'next_record': idx < len(ids) - 1 and getattr(current.browse(ids[idx + 1]), attr_name),
+ }
+ return {}
+
+
+def _build_url_w_params(url_string, query_params, remove_duplicates=True):
+ """ Rebuild a string url based on url_string and correctly compute query parameters
+ using those present in the url and those given by query_params. Having duplicates in
+ the final url is optional. For example:
+
+ * url_string = '/my?foo=bar&error=pay'
+ * query_params = {'foo': 'bar2', 'alice': 'bob'}
+ * if remove duplicates: result = '/my?foo=bar2&error=pay&alice=bob'
+ * else: result = '/my?foo=bar&foo=bar2&error=pay&alice=bob'
+ """
+ url = urls.url_parse(url_string)
+ url_params = url.decode_query()
+ if remove_duplicates: # convert to standard dict instead of werkzeug multidict to remove duplicates automatically
+ url_params = url_params.to_dict()
+ url_params.update(query_params)
+ return url.replace(query=urls.url_encode(url_params)).to_url()
+
+
+class CustomerPortal(Controller):
+
+ MANDATORY_BILLING_FIELDS = ["name", "phone", "email", "street", "city", "country_id"]
+ OPTIONAL_BILLING_FIELDS = ["zipcode", "state_id", "vat", "company_name"]
+
+ _items_per_page = 20
+
+ def _prepare_portal_layout_values(self):
+ """Values for /my/* templates rendering.
+
+ Does not include the record counts.
+ """
+ # get customer sales rep
+ sales_user = False
+ partner = request.env.user.partner_id
+ if partner.user_id and not partner.user_id._is_public():
+ sales_user = partner.user_id
+
+ return {
+ 'sales_user': sales_user,
+ 'page_name': 'home',
+ }
+
+ def _prepare_home_portal_values(self, counters):
+ """Values for /my & /my/home routes template rendering.
+
+ Includes the record count for the displayed badges.
+ where 'coutners' is the list of the displayed badges
+ and so the list to compute.
+ """
+ return {}
+
+ @route(['/my/counters'], type='json', auth="user", website=True)
+ def counters(self, counters, **kw):
+ return self._prepare_home_portal_values(counters)
+
+ @route(['/my', '/my/home'], type='http', auth="user", website=True)
+ def home(self, **kw):
+ values = self._prepare_portal_layout_values()
+ return request.render("portal.portal_my_home", values)
+
+ @route(['/my/account'], type='http', auth='user', website=True)
+ def account(self, redirect=None, **post):
+ values = self._prepare_portal_layout_values()
+ partner = request.env.user.partner_id
+ values.update({
+ 'error': {},
+ 'error_message': [],
+ })
+
+ if post and request.httprequest.method == 'POST':
+ error, error_message = self.details_form_validate(post)
+ values.update({'error': error, 'error_message': error_message})
+ values.update(post)
+ if not error:
+ values = {key: post[key] for key in self.MANDATORY_BILLING_FIELDS}
+ values.update({key: post[key] for key in self.OPTIONAL_BILLING_FIELDS if key in post})
+ for field in set(['country_id', 'state_id']) & set(values.keys()):
+ try:
+ values[field] = int(values[field])
+ except:
+ values[field] = False
+ values.update({'zip': values.pop('zipcode', '')})
+ partner.sudo().write(values)
+ if redirect:
+ return request.redirect(redirect)
+ return request.redirect('/my/home')
+
+ countries = request.env['res.country'].sudo().search([])
+ states = request.env['res.country.state'].sudo().search([])
+
+ values.update({
+ 'partner': partner,
+ 'countries': countries,
+ 'states': states,
+ 'has_check_vat': hasattr(request.env['res.partner'], 'check_vat'),
+ 'redirect': redirect,
+ 'page_name': 'my_details',
+ })
+
+ response = request.render("portal.portal_my_details", values)
+ response.headers['X-Frame-Options'] = 'DENY'
+ return response
+
+ @route('/my/security', type='http', auth='user', website=True, methods=['GET', 'POST'])
+ def security(self, **post):
+ values = self._prepare_portal_layout_values()
+ values['get_error'] = get_error
+
+ if request.httprequest.method == 'POST':
+ values.update(self._update_password(
+ post['old'].strip(),
+ post['new1'].strip(),
+ post['new2'].strip()
+ ))
+
+ return request.render('portal.portal_my_security', values, headers={
+ 'X-Frame-Options': 'DENY'
+ })
+
+ def _update_password(self, old, new1, new2):
+ for k, v in [('old', old), ('new1', new1), ('new2', new2)]:
+ if not v:
+ return {'errors': {'password': {k: _("You cannot leave any password empty.")}}}
+
+ if new1 != new2:
+ return {'errors': {'password': {'new2': _("The new password and its confirmation must be identical.")}}}
+
+ try:
+ request.env['res.users'].change_password(old, new1)
+ except UserError as e:
+ return {'errors': {'password': e.name}}
+ except AccessDenied as e:
+ msg = e.args[0]
+ if msg == AccessDenied().args[0]:
+ msg = _('The old password you provided is incorrect, your password was not changed.')
+ return {'errors': {'password': {'old': msg}}}
+
+ # update session token so the user does not get logged out (cache cleared by passwd change)
+ new_token = request.env.user._compute_session_token(request.session.sid)
+ request.session.session_token = new_token
+
+ return {'success': {'password': True}}
+
+ @http.route('/portal/attachment/add', type='http', auth='public', methods=['POST'], website=True)
+ def attachment_add(self, name, file, res_model, res_id, access_token=None, **kwargs):
+ """Process a file uploaded from the portal chatter and create the
+ corresponding `ir.attachment`.
+
+ The attachment will be created "pending" until the associated message
+ is actually created, and it will be garbage collected otherwise.
+
+ :param name: name of the file to save.
+ :type name: string
+
+ :param file: the file to save
+ :type file: werkzeug.FileStorage
+
+ :param res_model: name of the model of the original document.
+ To check access rights only, it will not be saved here.
+ :type res_model: string
+
+ :param res_id: id of the original document.
+ To check access rights only, it will not be saved here.
+ :type res_id: int
+
+ :param access_token: access_token of the original document.
+ To check access rights only, it will not be saved here.
+ :type access_token: string
+
+ :return: attachment data {id, name, mimetype, file_size, access_token}
+ :rtype: dict
+ """
+ try:
+ self._document_check_access(res_model, int(res_id), access_token=access_token)
+ except (AccessError, MissingError) as e:
+ raise UserError(_("The document does not exist or you do not have the rights to access it."))
+
+ IrAttachment = request.env['ir.attachment']
+ access_token = False
+
+ # Avoid using sudo or creating access_token when not necessary: internal
+ # users can create attachments, as opposed to public and portal users.
+ if not request.env.user.has_group('base.group_user'):
+ IrAttachment = IrAttachment.sudo().with_context(binary_field_real_user=IrAttachment.env.user)
+ access_token = IrAttachment._generate_access_token()
+
+ # At this point the related message does not exist yet, so we assign
+ # those specific res_model and res_is. They will be correctly set
+ # when the message is created: see `portal_chatter_post`,
+ # or garbage collected otherwise: see `_garbage_collect_attachments`.
+ attachment = IrAttachment.create({
+ 'name': name,
+ 'datas': base64.b64encode(file.read()),
+ 'res_model': 'mail.compose.message',
+ 'res_id': 0,
+ 'access_token': access_token,
+ })
+ return request.make_response(
+ data=json.dumps(attachment.read(['id', 'name', 'mimetype', 'file_size', 'access_token'])[0]),
+ headers=[('Content-Type', 'application/json')]
+ )
+
+ @http.route('/portal/attachment/remove', type='json', auth='public')
+ def attachment_remove(self, attachment_id, access_token=None):
+ """Remove the given `attachment_id`, only if it is in a "pending" state.
+
+ The user must have access right on the attachment or provide a valid
+ `access_token`.
+ """
+ try:
+ attachment_sudo = self._document_check_access('ir.attachment', int(attachment_id), access_token=access_token)
+ except (AccessError, MissingError) as e:
+ raise UserError(_("The attachment does not exist or you do not have the rights to access it."))
+
+ if attachment_sudo.res_model != 'mail.compose.message' or attachment_sudo.res_id != 0:
+ raise UserError(_("The attachment %s cannot be removed because it is not in a pending state.", attachment_sudo.name))
+
+ if attachment_sudo.env['mail.message'].search([('attachment_ids', 'in', attachment_sudo.ids)]):
+ raise UserError(_("The attachment %s cannot be removed because it is linked to a message.", attachment_sudo.name))
+
+ return attachment_sudo.unlink()
+
+ def details_form_validate(self, data):
+ error = dict()
+ error_message = []
+
+ # Validation
+ for field_name in self.MANDATORY_BILLING_FIELDS:
+ if not data.get(field_name):
+ error[field_name] = 'missing'
+
+ # email validation
+ if data.get('email') and not tools.single_email_re.match(data.get('email')):
+ error["email"] = 'error'
+ error_message.append(_('Invalid Email! Please enter a valid email address.'))
+
+ # vat validation
+ partner = request.env.user.partner_id
+ if data.get("vat") and partner and partner.vat != data.get("vat"):
+ if partner.can_edit_vat():
+ if hasattr(partner, "check_vat"):
+ if data.get("country_id"):
+ data["vat"] = request.env["res.partner"].fix_eu_vat_number(int(data.get("country_id")), data.get("vat"))
+ partner_dummy = partner.new({
+ 'vat': data['vat'],
+ 'country_id': (int(data['country_id'])
+ if data.get('country_id') else False),
+ })
+ try:
+ partner_dummy.check_vat()
+ except ValidationError:
+ error["vat"] = 'error'
+ else:
+ error_message.append(_('Changing VAT number is not allowed once document(s) have been issued for your account. Please contact us directly for this operation.'))
+
+ # error message for empty required fields
+ if [err for err in error.values() if err == 'missing']:
+ error_message.append(_('Some required fields are empty.'))
+
+ unknown = [k for k in data if k not in self.MANDATORY_BILLING_FIELDS + self.OPTIONAL_BILLING_FIELDS]
+ if unknown:
+ error['common'] = 'Unknown field'
+ error_message.append("Unknown field '%s'" % ','.join(unknown))
+
+ return error, error_message
+
+ def _document_check_access(self, model_name, document_id, access_token=None):
+ document = request.env[model_name].browse([document_id])
+ document_sudo = document.with_user(SUPERUSER_ID).exists()
+ if not document_sudo:
+ raise MissingError(_("This document does not exist."))
+ try:
+ document.check_access_rights('read')
+ document.check_access_rule('read')
+ except AccessError:
+ if not access_token or not document_sudo.access_token or not consteq(document_sudo.access_token, access_token):
+ raise
+ return document_sudo
+
+ def _get_page_view_values(self, document, access_token, values, session_history, no_breadcrumbs, **kwargs):
+ if access_token:
+ # if no_breadcrumbs = False -> force breadcrumbs even if access_token to `invite` users to register if they click on it
+ values['no_breadcrumbs'] = no_breadcrumbs
+ values['access_token'] = access_token
+ values['token'] = access_token # for portal chatter
+
+ # Those are used notably whenever the payment form is implied in the portal.
+ if kwargs.get('error'):
+ values['error'] = kwargs['error']
+ if kwargs.get('warning'):
+ values['warning'] = kwargs['warning']
+ if kwargs.get('success'):
+ values['success'] = kwargs['success']
+ # Email token for posting messages in portal view with identified author
+ if kwargs.get('pid'):
+ values['pid'] = kwargs['pid']
+ if kwargs.get('hash'):
+ values['hash'] = kwargs['hash']
+
+ history = request.session.get(session_history, [])
+ values.update(get_records_pager(history, document))
+
+ return values
+
+ def _show_report(self, model, report_type, report_ref, download=False):
+ if report_type not in ('html', 'pdf', 'text'):
+ raise UserError(_("Invalid report type: %s", report_type))
+
+ report_sudo = request.env.ref(report_ref).with_user(SUPERUSER_ID)
+
+ if not isinstance(report_sudo, type(request.env['ir.actions.report'])):
+ raise UserError(_("%s is not the reference of a report", report_ref))
+
+ if hasattr(model, 'company_id'):
+ report_sudo = report_sudo.with_company(model.company_id)
+
+ method_name = '_render_qweb_%s' % (report_type)
+ report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0]
+ reporthttpheaders = [
+ ('Content-Type', 'application/pdf' if report_type == 'pdf' else 'text/html'),
+ ('Content-Length', len(report)),
+ ]
+ if report_type == 'pdf' and download:
+ filename = "%s.pdf" % (re.sub('\W+', '-', model._get_report_base_filename()))
+ reporthttpheaders.append(('Content-Disposition', content_disposition(filename)))
+ return request.make_response(report, headers=reporthttpheaders)
+
+def get_error(e, path=''):
+ """ Recursively dereferences `path` (a period-separated sequence of dict
+ keys) in `e` (an error dict or value), returns the final resolution IIF it's
+ an str, otherwise returns None
+ """
+ for k in (path.split('.') if path else []):
+ if not isinstance(e, dict):
+ return None
+ e = e.get(k)
+
+ return e if isinstance(e, str) else None
diff --git a/addons/portal/controllers/web.py b/addons/portal/controllers/web.py
new file mode 100644
index 00000000..9e6e6175
--- /dev/null
+++ b/addons/portal/controllers/web.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+from odoo.addons.web.controllers.main import Home
+from odoo.http import request
+
+
+class Home(Home):
+
+ @http.route()
+ def index(self, *args, **kw):
+ if request.session.uid and not request.env['res.users'].sudo().browse(request.session.uid).has_group('base.group_user'):
+ return http.local_redirect('/my', query=request.params, keep_hash=True)
+ return super(Home, self).index(*args, **kw)
+
+ def _login_redirect(self, uid, redirect=None):
+ if not redirect and not request.env['res.users'].sudo().browse(uid).has_group('base.group_user'):
+ redirect = '/my'
+ return super(Home, self)._login_redirect(uid, redirect=redirect)
+
+ @http.route('/web', type='http', auth="none")
+ def web_client(self, s_action=None, **kw):
+ if request.session.uid and not request.env['res.users'].sudo().browse(request.session.uid).has_group('base.group_user'):
+ return http.local_redirect('/my', query=request.params, keep_hash=True)
+ return super(Home, self).web_client(s_action, **kw)