diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/portal/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/portal/models')
| -rw-r--r-- | addons/portal/models/__init__.py | 9 | ||||
| -rw-r--r-- | addons/portal/models/ir_http.py | 20 | ||||
| -rw-r--r-- | addons/portal/models/ir_ui_view.py | 28 | ||||
| -rw-r--r-- | addons/portal/models/mail_message.py | 24 | ||||
| -rw-r--r-- | addons/portal/models/mail_thread.py | 39 | ||||
| -rw-r--r-- | addons/portal/models/portal_mixin.py | 150 | ||||
| -rw-r--r-- | addons/portal/models/res_partner.py | 14 |
7 files changed, 284 insertions, 0 deletions
diff --git a/addons/portal/models/__init__.py b/addons/portal/models/__init__.py new file mode 100644 index 00000000..1c70d123 --- /dev/null +++ b/addons/portal/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ir_http +from . import ir_ui_view +from . import mail_thread +from . import mail_message +from . import portal_mixin +from . import res_partner diff --git a/addons/portal/models/ir_http.py b/addons/portal/models/ir_http.py new file mode 100644 index 00000000..9320f6d4 --- /dev/null +++ b/addons/portal/models/ir_http.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.http import request + + +class IrHttp(models.AbstractModel): + _inherit = 'ir.http' + + @classmethod + def _get_translation_frontend_modules_name(cls): + mods = super(IrHttp, cls)._get_translation_frontend_modules_name() + return mods + ['portal'] + + @classmethod + def _get_frontend_langs(cls): + if request and request.is_frontend: + return [lang[0] for lang in filter(lambda l: l[3], request.env['res.lang'].get_available())] + return super()._get_frontend_langs() diff --git a/addons/portal/models/ir_ui_view.py b/addons/portal/models/ir_ui_view.py new file mode 100644 index 00000000..ca405175 --- /dev/null +++ b/addons/portal/models/ir_ui_view.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields +from odoo.http import request +from odoo.addons.http_routing.models.ir_http import url_for + + +class View(models.Model): + _inherit = "ir.ui.view" + + customize_show = fields.Boolean("Show As Optional Inherit", default=False) + + @api.model + def _prepare_qcontext(self): + """ Returns the qcontext : rendering context with portal specific value (required + to render portal layout template) + """ + qcontext = super(View, self)._prepare_qcontext() + if request and getattr(request, 'is_frontend', False): + Lang = request.env['res.lang'] + portal_lang_code = request.env['ir.http']._get_frontend_langs() + qcontext.update(dict( + self._context.copy(), + languages=[lang for lang in Lang.get_available() if lang[0] in portal_lang_code], + url_for=url_for, + )) + return qcontext diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py new file mode 100644 index 00000000..91d40d7a --- /dev/null +++ b/addons/portal/models/mail_message.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class MailMessage(models.Model): + _inherit = 'mail.message' + + def portal_message_format(self): + return self._portal_message_format([ + 'id', 'body', 'date', 'author_id', 'email_from', # base message fields + 'message_type', 'subtype_id', 'is_internal', 'subject', # message specific + 'model', 'res_id', 'record_name', # document related + ]) + + def _portal_message_format(self, fields_list): + vals_list = self._message_format(fields_list) + IrAttachmentSudo = self.env['ir.attachment'].sudo() + for vals in vals_list: + for attachment in vals.get('attachment_ids', []): + if not attachment.get('access_token'): + attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0] + return vals_list diff --git a/addons/portal/models/mail_thread.py b/addons/portal/models/mail_thread.py new file mode 100644 index 00000000..0e9b58b9 --- /dev/null +++ b/addons/portal/models/mail_thread.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hashlib +import hmac + +from odoo import api, fields, models, _ + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + _mail_post_token_field = 'access_token' # token field for external posts, to be overridden + + website_message_ids = fields.One2many('mail.message', 'res_id', string='Website Messages', + domain=lambda self: [('model', '=', self._name), '|', ('message_type', '=', 'comment'), ('message_type', '=', 'email')], auto_join=True, + help="Website communication history") + + def _sign_token(self, pid): + """Generate a secure hash for this record with the email of the recipient with whom the record have been shared. + + This is used to determine who is opening the link + to be able for the recipient to post messages on the document's portal view. + + :param str email: + Email of the recipient that opened the link. + """ + self.ensure_one() + # check token field exists + if self._mail_post_token_field not in self._fields: + raise NotImplementedError(_( + "Model %(model_name)s does not support token signature, as it does not have %(field_name)s field.", + model_name=self._name, + field_name=self._mail_post_token_field + )) + # sign token + secret = self.env["ir.config_parameter"].sudo().get_param("database.secret") + token = (self.env.cr.dbname, self[self._mail_post_token_field], pid) + return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha256).hexdigest() diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py new file mode 100644 index 00000000..bbd9576c --- /dev/null +++ b/addons/portal/models/portal_mixin.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import uuid +from werkzeug.urls import url_encode +from odoo import api, exceptions, fields, models, _ + + +class PortalMixin(models.AbstractModel): + _name = "portal.mixin" + _description = 'Portal Mixin' + + access_url = fields.Char( + 'Portal Access URL', compute='_compute_access_url', + help='Customer Portal URL') + access_token = fields.Char('Security Token', copy=False) + + # to display the warning from specific model + access_warning = fields.Text("Access warning", compute="_compute_access_warning") + + def _compute_access_warning(self): + for mixin in self: + mixin.access_warning = '' + + def _compute_access_url(self): + for record in self: + record.access_url = '#' + + def _portal_ensure_token(self): + """ Get the current record access token """ + if not self.access_token: + # we use a `write` to force the cache clearing otherwise `return self.access_token` will return False + self.sudo().write({'access_token': str(uuid.uuid4())}) + return self.access_token + + def _get_share_url(self, redirect=False, signup_partner=False, pid=None, share_token=True): + """ + Build the url of the record that will be sent by mail and adds additional parameters such as + access_token to bypass the recipient's rights, + signup_partner to allows the user to create easily an account, + hash token to allow the user to be authenticated in the chatter of the record portal view, if applicable + :param redirect : Send the redirect url instead of the direct portal share url + :param signup_partner: allows the user to create an account with pre-filled fields. + :param pid: = partner_id - when given, a hash is generated to allow the user to be authenticated + in the portal chatter, if any in the target page, + if the user is redirected to the portal instead of the backend. + :return: the url of the record with access parameters, if any. + """ + self.ensure_one() + params = { + 'model': self._name, + 'res_id': self.id, + } + if share_token and hasattr(self, 'access_token'): + params['access_token'] = self._portal_ensure_token() + if pid: + params['pid'] = pid + params['hash'] = self._sign_token(pid) + if signup_partner and hasattr(self, 'partner_id') and self.partner_id: + params.update(self.partner_id.signup_get_auth_param()[self.partner_id.id]) + + return '%s?%s' % ('/mail/view' if redirect else self.access_url, url_encode(params)) + + def _notify_get_groups(self, msg_vals=None): + access_token = self._portal_ensure_token() + groups = super(PortalMixin, self)._notify_get_groups(msg_vals=msg_vals) + local_msg_vals = dict(msg_vals or {}) + + if access_token and 'partner_id' in self._fields and self['partner_id']: + customer = self['partner_id'] + local_msg_vals['access_token'] = self.access_token + local_msg_vals.update(customer.signup_get_auth_param()[customer.id]) + access_link = self._notify_get_action_link('view', **local_msg_vals) + + new_group = [ + ('portal_customer', lambda pdata: pdata['id'] == customer.id, { + 'has_button_access': False, + 'button_access': { + 'url': access_link, + }, + 'notification_is_customer': True, + }) + ] + else: + new_group = [] + return new_group + groups + + def get_access_action(self, access_uid=None): + """ Instead of the classic form view, redirect to the online document for + portal users or if force_website=True in the context. """ + self.ensure_one() + + user, record = self.env.user, self + if access_uid: + try: + record.check_access_rights('read') + record.check_access_rule("read") + except exceptions.AccessError: + return super(PortalMixin, self).get_access_action(access_uid) + user = self.env['res.users'].sudo().browse(access_uid) + record = self.with_user(user) + if user.share or self.env.context.get('force_website'): + try: + record.check_access_rights('read') + record.check_access_rule('read') + except exceptions.AccessError: + if self.env.context.get('force_website'): + return { + 'type': 'ir.actions.act_url', + 'url': record.access_url, + 'target': 'self', + 'res_id': record.id, + } + else: + pass + else: + return { + 'type': 'ir.actions.act_url', + 'url': record._get_share_url(), + 'target': 'self', + 'res_id': record.id, + } + return super(PortalMixin, self).get_access_action(access_uid) + + @api.model + def action_share(self): + action = self.env["ir.actions.actions"]._for_xml_id("portal.portal_share_action") + action['context'] = {'active_id': self.env.context['active_id'], + 'active_model': self.env.context['active_model']} + return action + + def get_portal_url(self, suffix=None, report_type=None, download=None, query_string=None, anchor=None): + """ + Get a portal url for this model, including access_token. + The associated route must handle the flags for them to have any effect. + - suffix: string to append to the url, before the query string + - report_type: report_type query string, often one of: html, pdf, text + - download: set the download query string to true + - query_string: additional query string + - anchor: string to append after the anchor # + """ + self.ensure_one() + url = self.access_url + '%s?access_token=%s%s%s%s%s' % ( + suffix if suffix else '', + self._portal_ensure_token(), + '&report_type=%s' % report_type if report_type else '', + '&download=true' if download else '', + query_string if query_string else '', + '#%s' % anchor if anchor else '' + ) + return url diff --git a/addons/portal/models/res_partner.py b/addons/portal/models/res_partner.py new file mode 100644 index 00000000..064fa2ff --- /dev/null +++ b/addons/portal/models/res_partner.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + def can_edit_vat(self): + ''' `vat` is a commercial field, synced between the parent (commercial + entity) and the children. Only the commercial entity should be able to + edit it (as in backend). ''' + return not self.parent_id |
