mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 25062022
This commit is contained in:
@@ -54,8 +54,7 @@
|
||||
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>
|
||||
</group>
|
||||
<group name="group3"
|
||||
attrs="{'invisible': ['|', ('payment_difference', '=', 0.0), '|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"
|
||||
groups="account.group_account_readonly">
|
||||
attrs="{'invisible': ['|', ('payment_difference', '=', 0.0), '|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}">
|
||||
<label for="payment_difference"/>
|
||||
<div>
|
||||
<field name="payment_difference"/>
|
||||
|
||||
@@ -105,6 +105,15 @@ class AccountEdiFormat(models.Model):
|
||||
self.ensure_one()
|
||||
return journal.type == 'sale'
|
||||
|
||||
def _is_enabled_by_default_on_journal(self, journal):
|
||||
""" Indicate if the EDI format should be selected by default on the journal passed as parameter.
|
||||
If True, this EDI format will be selected by default on the journal.
|
||||
|
||||
:param journal: The journal.
|
||||
:returns: True if this format should be enabled by default on the journal, False otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
def _is_embedding_to_invoice_pdf_needed(self):
|
||||
""" Indicate if the EDI must be embedded inside the PDF report.
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
from flectra import api, models, fields, _
|
||||
from flectra.exceptions import UserError
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
@@ -47,11 +49,34 @@ class AccountJournal(models.Model):
|
||||
|
||||
for journal in self:
|
||||
compatible_edis = edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal))
|
||||
journal.compatible_edi_ids += compatible_edis
|
||||
journal.compatible_edi_ids = compatible_edis
|
||||
|
||||
@api.depends('type', 'company_id', 'company_id.country_id')
|
||||
def _compute_edi_format_ids(self):
|
||||
edi_formats = self.env['account.edi.format'].search([])
|
||||
journal_ids = self.ids
|
||||
|
||||
if journal_ids:
|
||||
self._cr.execute('''
|
||||
SELECT
|
||||
move.journal_id,
|
||||
ARRAY_AGG(doc.edi_format_id) AS edi_format_ids
|
||||
FROM account_edi_document doc
|
||||
JOIN account_move move ON move.id = doc.move_id
|
||||
WHERE doc.state IN ('to_cancel', 'to_send')
|
||||
AND move.journal_id IN %s
|
||||
GROUP BY move.journal_id
|
||||
''', [tuple(journal_ids)])
|
||||
protected_edi_formats_per_journal = {r[0]: set(r[1]) for r in self._cr.fetchall()}
|
||||
else:
|
||||
protected_edi_formats_per_journal = defaultdict(set)
|
||||
|
||||
for journal in self:
|
||||
journal.edi_format_ids += edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal))
|
||||
enabled_edi_formats = edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal) and
|
||||
e._is_enabled_by_default_on_journal(journal))
|
||||
|
||||
# The existing edi formats that are already in use so we can't remove it.
|
||||
protected_edi_format_ids = protected_edi_formats_per_journal.get(journal.id, set())
|
||||
protected_edi_formats = journal.edi_format_ids.filtered(lambda e: e.id in protected_edi_format_ids)
|
||||
|
||||
journal.edi_format_ids = enabled_edi_formats + protected_edi_formats
|
||||
|
||||
@@ -10,18 +10,30 @@
|
||||
<field name="use_google_gmail_service" string="Gmail" attrs="{'readonly': [('state', '=', 'done')]}"/>
|
||||
</field>
|
||||
<field name="user" position="after">
|
||||
<field string="Authorization Code" name="google_gmail_authorization_code" password="True"
|
||||
attrs="{'required': [('use_google_gmail_service', '=', True)], 'invisible': [('use_google_gmail_service', '=', False)], 'readonly': [('state', '=', 'done')]}"
|
||||
style="word-break: break-word;"/>
|
||||
<field name="google_gmail_uri"
|
||||
class="fa fa-arrow-right oe_edit_only"
|
||||
widget="url"
|
||||
text=" Get an Authorization Code"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '=', False)]}"
|
||||
nolabel="1"/>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '!=', False)]}">
|
||||
Setup your Gmail API credentials in the general settings to link a Gmail account.
|
||||
<field name="google_gmail_uri" invisible="1"/>
|
||||
<field name="google_gmail_refresh_token" invisible="1"/>
|
||||
<div></div>
|
||||
<div attrs="{'invisible': [('use_google_gmail_service', '=', False)]}">
|
||||
<span attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '=', False)]}"
|
||||
class="badge badge-success">
|
||||
Gmail Token Valid
|
||||
</span>
|
||||
<button type="object"
|
||||
name="open_google_gmail_uri" class="btn-link px-0"
|
||||
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '!=', False)]}">
|
||||
<i class="fa fa-arrow-right"/>
|
||||
Connect your Gmail account
|
||||
</button>
|
||||
<button type="object"
|
||||
name="open_google_gmail_uri" class="btn-link px-0"
|
||||
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '=', False)]}">
|
||||
<i class="fa fa-cog"/>
|
||||
Edit Settings
|
||||
</button>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '!=', False)]}">
|
||||
Setup your Gmail API credentials in the general settings to link a Gmail account.
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="password" position="attributes">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
4
addons/google_gmail/controllers/__init__.py
Normal file
4
addons/google_gmail/controllers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
76
addons/google_gmail/controllers/main.py
Normal file
76
addons/google_gmail/controllers/main.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from flectra import _, http
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.http import request
|
||||
from flectra.tools import consteq
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoogleGmailController(http.Controller):
|
||||
@http.route('/google_gmail/confirm', type='http', auth='user')
|
||||
def google_gmail_callback(self, code=None, state=None, error=None, **kwargs):
|
||||
"""Callback URL during the OAuth process.
|
||||
|
||||
Gmail redirects the user browser to this endpoint with the authorization code.
|
||||
We will fetch the refresh token and the access token thanks to this authorization
|
||||
code and save those values on the given mail server.
|
||||
"""
|
||||
if not request.env.user.has_group('base.group_system'):
|
||||
_logger.error('Google Gmail: non-system user trying to link an Gmail account.')
|
||||
raise Forbidden()
|
||||
|
||||
if error:
|
||||
return _('An error occur during the authentication process: %s.') % error
|
||||
|
||||
try:
|
||||
state = json.loads(state)
|
||||
model_name = state['model']
|
||||
rec_id = state['id']
|
||||
csrf_token = state['csrf_token']
|
||||
except Exception:
|
||||
_logger.error('Google Gmail: Wrong state value %r.', state)
|
||||
raise Forbidden()
|
||||
|
||||
model = request.env[model_name]
|
||||
|
||||
if not issubclass(type(model), request.env.registry['google.gmail.mixin']):
|
||||
# The model must inherits from the "google.gmail.mixin" mixin
|
||||
raise Forbidden()
|
||||
|
||||
record = model.browse(rec_id).exists()
|
||||
if not record:
|
||||
raise Forbidden()
|
||||
|
||||
if not csrf_token or not consteq(csrf_token, record._get_gmail_csrf_token()):
|
||||
_logger.error('Google Gmail: Wrong CSRF token during Gmail authentication.')
|
||||
raise Forbidden()
|
||||
|
||||
try:
|
||||
refresh_token, access_token, expiration = record._fetch_gmail_refresh_token(code)
|
||||
except UserError as e:
|
||||
return _('An error occur during the authentication process: %s.') % str(e.name)
|
||||
|
||||
record.write({
|
||||
'google_gmail_access_token': access_token,
|
||||
'google_gmail_access_token_expiration': expiration,
|
||||
'google_gmail_authorization_code': code,
|
||||
'google_gmail_refresh_token': refresh_token,
|
||||
})
|
||||
|
||||
url_params = {
|
||||
'id': rec_id,
|
||||
'model': model_name,
|
||||
'view_type': 'form'
|
||||
}
|
||||
url = '/web?#' + url_encode(url_params)
|
||||
return werkzeug.utils.redirect(url, 303)
|
||||
@@ -1,10 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
|
||||
from flectra import api, fields, models
|
||||
from werkzeug.urls import url_encode, url_join
|
||||
|
||||
from flectra import _, api, fields, models, tools
|
||||
from flectra.exceptions import AccessError, UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,37 +33,107 @@ class GoogleGmailMixin(models.AbstractModel):
|
||||
Config = self.env['ir.config_parameter'].sudo()
|
||||
google_gmail_client_id = Config.get_param('google_gmail_client_id')
|
||||
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
|
||||
base_url = self.get_base_url()
|
||||
|
||||
redirect_uri = url_join(base_url, '/google_gmail/confirm')
|
||||
|
||||
if not google_gmail_client_id or not google_gmail_client_secret:
|
||||
self.google_gmail_uri = False
|
||||
else:
|
||||
google_gmail_uri = self.env['google.service']._get_google_token_uri('gmail', scope=self._SERVICE_SCOPE)
|
||||
self.google_gmail_uri = google_gmail_uri
|
||||
for record in self:
|
||||
google_gmail_uri = 'https://accounts.google.com/o/oauth2/v2/auth?%s' % url_encode({
|
||||
'client_id': google_gmail_client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'response_type': 'code',
|
||||
'scope': self._SERVICE_SCOPE,
|
||||
# access_type and prompt needed to get a refresh token
|
||||
'access_type': 'offline',
|
||||
'prompt': 'consent',
|
||||
'state': json.dumps({
|
||||
'model': record._name,
|
||||
'id': record.id or False,
|
||||
'csrf_token': record._get_gmail_csrf_token() if record.id else False,
|
||||
})
|
||||
})
|
||||
record.google_gmail_uri = google_gmail_uri
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if values.get('google_gmail_authorization_code'):
|
||||
# Generate the refresh token
|
||||
values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token(
|
||||
'gmail', values['google_gmail_authorization_code'])
|
||||
values['google_gmail_access_token'] = False
|
||||
values['google_gmail_access_token_expiration'] = False
|
||||
def open_google_gmail_uri(self):
|
||||
"""Open the URL to accept the Gmail permission.
|
||||
|
||||
return super(GoogleGmailMixin, self).create(values)
|
||||
This is done with an action, so we can force the user the save the form.
|
||||
We need him to save the form so the current mail server record exist in DB, and
|
||||
we can include the record ID in the URL.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
def write(self, values):
|
||||
authorization_code = values.get('google_gmail_authorization_code')
|
||||
if (
|
||||
authorization_code
|
||||
and not all(authorization_code == code for code in self.mapped('google_gmail_authorization_code'))
|
||||
):
|
||||
# Update the refresh token
|
||||
values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token(
|
||||
'gmail', authorization_code)
|
||||
values['google_gmail_access_token'] = False
|
||||
values['google_gmail_access_token_expiration'] = False
|
||||
if not self.env.user.has_group('base.group_system'):
|
||||
raise AccessError(_('Only the administrator can link a Gmail mail server.'))
|
||||
|
||||
return super(GoogleGmailMixin, self).write(values)
|
||||
if not self.google_gmail_uri:
|
||||
raise UserError(_('Please configure your Gmail credentials.'))
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.google_gmail_uri,
|
||||
}
|
||||
|
||||
def _fetch_gmail_refresh_token(self, authorization_code):
|
||||
"""Request the refresh token and the initial access token from the authorization code.
|
||||
|
||||
:return:
|
||||
refresh_token, access_token, access_token_expiration
|
||||
"""
|
||||
response = self._fetch_gmail_token('authorization_code', code=authorization_code)
|
||||
|
||||
return (
|
||||
response['refresh_token'],
|
||||
response['access_token'],
|
||||
int(time.time()) + response['expires_in'],
|
||||
)
|
||||
|
||||
def _fetch_gmail_access_token(self, refresh_token):
|
||||
"""Refresh the access token thanks to the refresh token.
|
||||
|
||||
:return:
|
||||
access_token, access_token_expiration
|
||||
"""
|
||||
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
|
||||
|
||||
return (
|
||||
response['access_token'],
|
||||
int(time.time()) + response['expires_in'],
|
||||
)
|
||||
|
||||
def _fetch_gmail_token(self, grant_type, **values):
|
||||
"""Generic method to request an access token or a refresh token.
|
||||
|
||||
Return the JSON response of the GMail API and manage the errors which can occur.
|
||||
|
||||
:param grant_type: Depends the action we want to do (refresh_token or authorization_code)
|
||||
:param values: Additional parameters that will be given to the GMail endpoint
|
||||
"""
|
||||
Config = self.env['ir.config_parameter'].sudo()
|
||||
google_gmail_client_id = Config.get_param('google_gmail_client_id')
|
||||
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
|
||||
base_url = self.get_base_url()
|
||||
redirect_uri = url_join(base_url, '/google_gmail/confirm')
|
||||
|
||||
response = requests.post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
data={
|
||||
'client_id': google_gmail_client_id,
|
||||
'client_secret': google_gmail_client_secret,
|
||||
'grant_type': grant_type,
|
||||
'redirect_uri': redirect_uri,
|
||||
**values,
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
raise UserError(_('An error occurred when fetching the access token.'))
|
||||
|
||||
return response.json()
|
||||
|
||||
def _generate_oauth2_string(self, user, refresh_token):
|
||||
"""Generate a OAuth2 string which can be used for authentication.
|
||||
@@ -73,14 +148,33 @@ class GoogleGmailMixin(models.AbstractModel):
|
||||
if not self.google_gmail_access_token \
|
||||
or not self.google_gmail_access_token_expiration \
|
||||
or self.google_gmail_access_token_expiration < now_timestamp:
|
||||
self.google_gmail_access_token, expires_in = self.env['google.service']._get_access_token(
|
||||
refresh_token, 'gmail', self._SERVICE_SCOPE)
|
||||
self.google_gmail_access_token_expiration = now_timestamp + expires_in
|
||||
|
||||
_logger.info('Google Gmail: fetch new access token. Expire in %i minutes', expires_in // 60)
|
||||
access_token, expiration = self._fetch_gmail_access_token(self.google_gmail_refresh_token)
|
||||
|
||||
self.write({
|
||||
'google_gmail_access_token': access_token,
|
||||
'google_gmail_access_token_expiration': expiration,
|
||||
})
|
||||
|
||||
_logger.info(
|
||||
'Google Gmail: fetch new access token. Expires in %i minutes',
|
||||
(self.google_gmail_access_token_expiration - now_timestamp) // 60)
|
||||
else:
|
||||
_logger.info(
|
||||
'Google Gmail: reuse existing access token. Expire in %i minutes',
|
||||
(self.google_gmail_access_token_expiration - now_timestamp) // 60)
|
||||
|
||||
return 'user=%s\1auth=Bearer %s\1\1' % (user, self.google_gmail_access_token)
|
||||
|
||||
def _get_gmail_csrf_token(self):
|
||||
"""Generate a CSRF token that will be verified in `google_gmail_callback`.
|
||||
|
||||
This will prevent a malicious person to make an admin user disconnect the mail servers.
|
||||
"""
|
||||
self.ensure_one()
|
||||
_logger.info('Google Gmail: generate CSRF token for %s #%i', self._name, self.id)
|
||||
return tools.misc.hmac(
|
||||
env=self.env(su=True),
|
||||
scope='google_gmail_oauth',
|
||||
message=(self._name, self.id),
|
||||
)
|
||||
|
||||
@@ -9,18 +9,30 @@
|
||||
<field name="use_google_gmail_service" string="Gmail"/>
|
||||
</field>
|
||||
<field name="smtp_user" position="after">
|
||||
<field string="Authorization Code" name="google_gmail_authorization_code" password="True"
|
||||
attrs="{'required': [('use_google_gmail_service', '=', True)], 'invisible': [('use_google_gmail_service', '=', False)]}"
|
||||
style="word-break: break-word;"/>
|
||||
<field name="google_gmail_uri"
|
||||
class="fa fa-arrow-right oe_edit_only"
|
||||
widget="url"
|
||||
text=" Get an Authorization Code"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '=', False)]}"
|
||||
nolabel="1"/>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '!=', False)]}">
|
||||
Setup your Gmail API credentials in the general settings to link a Gmail account.
|
||||
<field name="google_gmail_uri" invisible="1"/>
|
||||
<field name="google_gmail_refresh_token" invisible="1"/>
|
||||
<div></div>
|
||||
<div attrs="{'invisible': [('use_google_gmail_service', '=', False)]}">
|
||||
<span attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '=', False)]}"
|
||||
class="badge badge-success">
|
||||
Gmail Token Valid
|
||||
</span>
|
||||
<button type="object"
|
||||
name="open_google_gmail_uri" class="btn-link px-0"
|
||||
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '!=', False)]}">
|
||||
<i class="fa fa-arrow-right"/>
|
||||
Connect your Gmail account
|
||||
</button>
|
||||
<button type="object"
|
||||
name="open_google_gmail_uri" class="btn-link px-0"
|
||||
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('use_google_gmail_service', '=', False), ('google_gmail_refresh_token', '=', False)]}">
|
||||
<i class="fa fa-cog"/>
|
||||
Edit Settings
|
||||
</button>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '!=', False)]}">
|
||||
Setup your Gmail API credentials in the general settings to link a Gmail account.
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="smtp_pass" position="attributes">
|
||||
|
||||
@@ -8,8 +8,13 @@ from flectra.exceptions import AccessError
|
||||
class User(models.Model):
|
||||
_inherit = ['res.users']
|
||||
|
||||
def _employee_ids_domain(self):
|
||||
# employee_ids is considered a safe field and as such will be fetched as sudo.
|
||||
# So try to enforce the security rules on the field to make sure we do not load employees outside of active companies
|
||||
return [('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))]
|
||||
|
||||
# note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´)
|
||||
employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employee')
|
||||
employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employee', domain=_employee_ids_domain)
|
||||
employee_id = fields.Many2one('hr.employee', string="Company employee",
|
||||
compute='_compute_company_employee', search='_search_company_employee', store=False)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<record id="hr_employee_public_comp_rule" model="ir.rule">
|
||||
<field name="name">Employee multi company rule</field>
|
||||
<field name="model_id" ref="model_hr_employee_public"/>
|
||||
<field name="domain_force">['|','|',('user_id', '=', user.id),('company_id', '=',False),('company_id', 'in', company_ids)]</field>
|
||||
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_job_comp_rule" model="ir.rule">
|
||||
|
||||
@@ -208,6 +208,10 @@ class HrExpense(models.Model):
|
||||
self.analytic_account_id = self.analytic_account_id or rec.analytic_id.id
|
||||
self.analytic_tag_ids = self.analytic_tag_ids or rec.analytic_tag_ids.ids
|
||||
|
||||
@api.constrains('payment_mode')
|
||||
def _check_payment_mode(self):
|
||||
self.sheet_id._check_payment_mode()
|
||||
|
||||
@api.constrains('product_id', 'product_uom_id')
|
||||
def _check_product_uom_category(self):
|
||||
if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id:
|
||||
@@ -284,7 +288,7 @@ class HrExpense(models.Model):
|
||||
|
||||
@api.model
|
||||
def get_empty_list_help(self, help_message):
|
||||
return super(HrExpense, self).get_empty_list_help(help_message + self._get_empty_list_mail_alias())
|
||||
return super(HrExpense, self).get_empty_list_help(help_message or '' + self._get_empty_list_mail_alias())
|
||||
|
||||
@api.model
|
||||
def _get_empty_list_mail_alias(self):
|
||||
|
||||
@@ -179,7 +179,7 @@ class AccountAnalyticLine(models.Model):
|
||||
if partner_id:
|
||||
vals['partner_id'] = partner_id
|
||||
# set timesheet UoM from the AA company (AA implies uom)
|
||||
if 'product_uom_id' not in vals and all(v in vals for v in ['account_id', 'project_id']): # project_id required to check this is timesheet flow
|
||||
if not vals.get('product_uom_id') and all(v in vals for v in ['account_id', 'project_id']): # project_id required to check this is timesheet flow
|
||||
analytic_account = self.env['account.analytic.account'].sudo().browse(vals['account_id'])
|
||||
vals['product_uom_id'] = analytic_account.company_id.project_time_mode_id.id
|
||||
return vals
|
||||
|
||||
@@ -367,3 +367,29 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
})
|
||||
|
||||
self.assertEqual(timesheet.project_id, second_project, 'The project_id of timesheet should be second_project')
|
||||
|
||||
def test_ensure_product_uom_set_in_timesheet(self):
|
||||
self.assertFalse(self.project_customer.timesheet_ids, 'No timesheet should be recorded in this project')
|
||||
self.assertFalse(self.project_customer.total_timesheet_time, 'The total time recorded should be equal to 0 since no timesheet is recorded.')
|
||||
|
||||
timesheet1, timesheet2 = self.env['account.analytic.line'].create([
|
||||
{'unit_amount': 1.0, 'project_id': self.project_customer.id},
|
||||
{'unit_amount': 3.0, 'project_id': self.project_customer.id, 'product_uom_id': False},
|
||||
])
|
||||
self.assertEqual(
|
||||
timesheet1.product_uom_id,
|
||||
self.project_customer.analytic_account_id.company_id.timesheet_encode_uom_id,
|
||||
'The default UoM set on the timesheet should be the one set on the company of AA.'
|
||||
)
|
||||
self.assertEqual(
|
||||
timesheet2.product_uom_id,
|
||||
self.project_customer.analytic_account_id.company_id.timesheet_encode_uom_id,
|
||||
'Even if the product_uom_id field is empty in the vals, the product_uom_id should have a UoM by default,'
|
||||
' otherwise the `total_timesheet_time` in project should not included the timesheet.'
|
||||
)
|
||||
self.assertEqual(self.project_customer.timesheet_ids, timesheet1 + timesheet2)
|
||||
self.assertEqual(
|
||||
self.project_customer.total_timesheet_time,
|
||||
timesheet1.unit_amount + timesheet2.unit_amount,
|
||||
'The total timesheet time of this project should be equal to 4.'
|
||||
)
|
||||
|
||||
@@ -1947,7 +1947,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_02')],
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_05')],
|
||||
}),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
@@ -1958,7 +1958,7 @@
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_02')],
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_05')],
|
||||
}),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
|
||||
@@ -713,10 +713,6 @@ var KanbanActivity = BasicActivity.extend({
|
||||
*/
|
||||
_renderDropdown: function () {
|
||||
var self = this;
|
||||
this.$el.dropdown({
|
||||
boundary: 'viewport',
|
||||
flip: false,
|
||||
});
|
||||
this.$('.o_activity')
|
||||
.toggleClass('dropdown-menu-right', config.device.isMobile)
|
||||
.html(QWeb.render('mail.KanbanActivityLoading'));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.o_activity_view {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
> table {
|
||||
background-color: white;
|
||||
thead > tr > th:first-of-type {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="mail.KanbanActivity">
|
||||
<div class="o_kanban_inline_block dropdown o_mail_activity">
|
||||
<!-- Dropdowns are created in JS to avoid some bugs, that's why the <a/> contains no args for the dropdown creation -->
|
||||
<a class="dropdown-toggle o-no-caret o_activity_btn" data-toggle="dropdown" role="button">
|
||||
<a class="dropdown-toggle o-no-caret o_activity_btn" data-boundary="viewport" data-flip="false" data-toggle="dropdown" role="button">
|
||||
<!-- span classes are generated dynamically (see _render) -->
|
||||
<span t-att-title="widget.selection[widget.activityState]" role="img" t-att-aria-label="widget.selection[widget.activity_state]"/>
|
||||
</a>
|
||||
|
||||
@@ -219,7 +219,7 @@ class MrpUnbuild(models.Model):
|
||||
finished_moves = unbuild.mo_id.move_finished_ids.filtered(lambda move: move.state == 'done')
|
||||
factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
|
||||
for finished_move in finished_moves:
|
||||
moves += unbuild._generate_move_from_existing_move(finished_move, factor, finished_move.location_dest_id, finished_move.location_id)
|
||||
moves += unbuild._generate_move_from_existing_move(finished_move, factor, unbuild.location_id, finished_move.location_id)
|
||||
else:
|
||||
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
|
||||
moves += unbuild._generate_move_from_bom_line(self.product_id, self.product_uom_id, unbuild.product_qty)
|
||||
|
||||
@@ -686,3 +686,54 @@ class TestUnbuild(TestMrpCommon):
|
||||
uo.action_unbuild()
|
||||
|
||||
self.assertEqual(uo.produce_line_ids.filtered(lambda sm: sm.product_id == compo).lot_ids, lot01 + lot02)
|
||||
|
||||
def test_unbuild_and_multilocations(self):
|
||||
"""
|
||||
Basic flow: produce p_final, transfer it to a sub-location and then
|
||||
unbuild it. The test ensures that the source/destination locations of an
|
||||
unbuild order are applied on the stock moves
|
||||
"""
|
||||
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
||||
self.env.user.write({'groups_id': [(4, grp_multi_loc.id, 0)]})
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
prod_location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.user.id)])
|
||||
subloc01, subloc02, = self.stock_location.child_ids[:2]
|
||||
|
||||
mo, _, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1.0
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
# Transfer the finished product from WH/Stock to `subloc01`
|
||||
internal_form = Form(self.env['stock.picking'])
|
||||
internal_form.picking_type_id = warehouse.int_type_id
|
||||
internal_form.location_id = self.stock_location
|
||||
internal_form.location_dest_id = subloc01
|
||||
with internal_form.move_ids_without_package.new() as move:
|
||||
move.product_id = p_final
|
||||
move.product_uom_qty = 1.0
|
||||
internal_transfer = internal_form.save()
|
||||
internal_transfer.action_confirm()
|
||||
internal_transfer.action_assign()
|
||||
internal_transfer.move_line_ids.qty_done = 1.0
|
||||
internal_transfer.button_validate()
|
||||
|
||||
unbuild_order_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_order_form.mo_id = mo
|
||||
unbuild_order_form.location_id = subloc01
|
||||
unbuild_order_form.location_dest_id = subloc02
|
||||
unbuild_order = unbuild_order_form.save()
|
||||
unbuild_order.action_unbuild()
|
||||
|
||||
self.assertRecordValues(unbuild_order.produce_line_ids, [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'product_id': p_final.id, 'location_id': subloc01.id, 'location_dest_id': prod_location.id},
|
||||
{'product_id': p2.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
|
||||
{'product_id': p1.id, 'location_id': prod_location.id, 'location_dest_id': subloc02.id},
|
||||
])
|
||||
|
||||
@@ -1931,7 +1931,9 @@ exports.Orderline = Backbone.Model.extend({
|
||||
}
|
||||
|
||||
// Set the quantity of the line based on number of pack lots.
|
||||
this.pack_lot_lines.set_quantity_by_lot();
|
||||
if(!this.product.to_weight){
|
||||
this.pack_lot_lines.set_quantity_by_lot();
|
||||
}
|
||||
},
|
||||
set_product_lot: function(product){
|
||||
this.has_product_lot = product.tracking !== 'none';
|
||||
|
||||
@@ -16,11 +16,11 @@ from flectra.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
|
||||
if _hash and pid: # Signed Token Case: hash implies token is signed by partner pid
|
||||
return consteq(_hash, record._sign_token(pid))
|
||||
elif 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()
|
||||
|
||||
@@ -65,8 +65,11 @@ def _message_post_helper(res_model, res_id, message, token='', _hash=False, pid=
|
||||
# deduce author of message
|
||||
author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
|
||||
|
||||
# Signed Token Case: author_id is forced
|
||||
if _hash and pid:
|
||||
author_id = pid
|
||||
# Token Case: author is document customer (if not logged) or itself even if user has not the access
|
||||
if token:
|
||||
elif 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')
|
||||
@@ -74,9 +77,6 @@ def _message_post_helper(res_model, res_id, message, token='', _hash=False, pid=
|
||||
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:
|
||||
@@ -102,7 +102,7 @@ def _message_post_helper(res_model, res_id, message, token='', _hash=False, pid=
|
||||
class PortalChatter(http.Controller):
|
||||
|
||||
def _portal_post_filter_params(self):
|
||||
return ['token', 'hash', 'pid']
|
||||
return ['token', 'pid']
|
||||
|
||||
def _portal_post_check_attachments(self, attachment_ids, attachment_tokens):
|
||||
if len(attachment_tokens) != len(attachment_ids):
|
||||
@@ -142,6 +142,7 @@ class PortalChatter(http.Controller):
|
||||
'attachment_ids': False, # will be added afterward
|
||||
}
|
||||
post_values.update((fname, kw.get(fname)) for fname in self._portal_post_filter_params())
|
||||
post_values['_hash'] = kw.get('hash')
|
||||
message = _message_post_helper(**post_values)
|
||||
|
||||
if attachment_ids:
|
||||
|
||||
@@ -206,9 +206,7 @@ class Project(models.Model):
|
||||
"A user with the project > administrator access right level can still access this project and its tasks, even if they are not explicitly part of the followers.\n\n"
|
||||
"- All internal users: all internal users can access the project and all of its tasks without distinction.\n\n"
|
||||
"- Invited portal users and all internal users: all internal users can access the project and all of its tasks without distinction.\n"
|
||||
"When following a project, portal users will get access to all of its tasks without distinction. Otherwise, they will only get access to the specific tasks they are following.\n\n"
|
||||
"In any case, an internal user with no project access rights can still access a task, "
|
||||
"provided that they are given the corresponding URL (and that they are part of the followers if the project is private).")
|
||||
"When following a project, portal users will get access to all of its tasks without distinction. Otherwise, they will only get access to the specific tasks they are following.")
|
||||
|
||||
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users', inverse='_inverse_allowed_user')
|
||||
allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel',
|
||||
|
||||
@@ -27,6 +27,8 @@ class TestAngloSaxonValuationPurchaseMRP(SavepointCase):
|
||||
'property_stock_valuation_account_id': cls.stock_valuation_account.id,
|
||||
})
|
||||
|
||||
cls.env.company.anglo_saxon_accounting = True
|
||||
|
||||
def test_kit_anglo_saxo_price_diff(self):
|
||||
"""
|
||||
Suppose an automated-AVCO configuration and a Price Difference Account defined on
|
||||
|
||||
@@ -42,8 +42,7 @@ class AccountMove(models.Model):
|
||||
continue
|
||||
|
||||
move = move.with_company(move.company_id)
|
||||
for line in move.invoice_line_ids.filtered(lambda line: line.product_id.type == 'product' and line.product_id.valuation == 'real_time'):
|
||||
|
||||
for line in move.invoice_line_ids:
|
||||
# Filter out lines being not eligible for price difference.
|
||||
if line.product_id.type != 'product' or line.product_id.valuation != 'real_time':
|
||||
continue
|
||||
@@ -100,19 +99,16 @@ class AccountMove(models.Model):
|
||||
|
||||
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
if line.tax_ids:
|
||||
if line.discount and line.quantity:
|
||||
# We do not want to round the price unit since :
|
||||
# - It does not follow the currency precision
|
||||
# - It may include a discount
|
||||
# Since compute_all still rounds the total, we use an ugly workaround:
|
||||
# multiply then divide the price unit.
|
||||
price_unit *= line.quantity
|
||||
price_unit = line.tax_ids.with_context(round=False, force_sign=move._get_tax_force_sign()).compute_all(
|
||||
price_unit, currency=move.currency_id, quantity=1.0, is_refund=move.move_type == 'in_refund')['total_excluded']
|
||||
price_unit /= line.quantity
|
||||
else:
|
||||
price_unit = line.tax_ids.compute_all(
|
||||
price_unit, currency=move.currency_id, quantity=1.0, is_refund=move.move_type == 'in_refund')['total_excluded']
|
||||
# We do not want to round the price unit since :
|
||||
# - It does not follow the currency precision
|
||||
# - It may include a discount
|
||||
# Since compute_all still rounds the total, we use an ugly workaround:
|
||||
# shift the decimal part using a fixed quantity to avoid rounding issues
|
||||
prec = 1e+6
|
||||
price_unit *= prec
|
||||
price_unit = line.tax_ids.with_context(round=False, force_sign=move._get_tax_force_sign()).compute_all(
|
||||
price_unit, currency=move.currency_id, quantity=1.0, is_refund=move.move_type == 'in_refund')['total_excluded']
|
||||
price_unit /= prec
|
||||
|
||||
price_unit_val_dif = price_unit - valuation_price_unit
|
||||
price_subtotal = line.quantity * price_unit_val_dif
|
||||
|
||||
@@ -573,4 +573,4 @@ class PurchaseOrderLine(models.Model):
|
||||
@api.model
|
||||
def _update_qty_received_method(self):
|
||||
"""Update qty_received_method for old PO before install this module."""
|
||||
self.search([])._compute_qty_received_method()
|
||||
self.search(['!', ('state', 'in', ['purchase', 'done'])])._compute_qty_received_method()
|
||||
|
||||
@@ -1351,3 +1351,30 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
|
||||
# Check if something was posted in the price difference account
|
||||
price_diff_aml = invoice.line_ids.filtered(lambda l: l.account_id == self.price_diff_account)
|
||||
self.assertEqual(len(price_diff_aml), 0, "No line should have been generated in the price difference account.")
|
||||
|
||||
def test_anglosaxon_valuation_price_unit_diff_standard(self):
|
||||
"""
|
||||
Check the price unit difference account is hit with the correct amount
|
||||
"""
|
||||
self.env.ref("product.decimal_price").digits = 6
|
||||
self.env.company.anglo_saxon_accounting = True
|
||||
self.product1.categ_id.property_cost_method = 'standard'
|
||||
self.product1.categ_id.property_valuation = 'real_time'
|
||||
self.product1.categ_id.property_account_creditor_price_difference_categ = self.price_diff_account
|
||||
self.product1.standard_price = 0.01719
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'invoice_date': '2022-03-31',
|
||||
'partner_id': self.partner_id.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {'product_id': self.product1.id, 'quantity': 30000, 'price_unit': 0.01782, 'tax_ids': self.tax_purchase_a.ids})
|
||||
]
|
||||
})
|
||||
|
||||
invoice.action_post()
|
||||
|
||||
# Check if something was posted in the price difference account
|
||||
price_diff_aml = invoice.line_ids.filtered(lambda l: l.account_id == self.price_diff_account)
|
||||
self.assertEqual(len(price_diff_aml), 1, "A line should have been generated in the price difference account.")
|
||||
self.assertAlmostEqual(price_diff_aml.balance, 18.90)
|
||||
|
||||
@@ -10,6 +10,7 @@ class ProductionLot(models.Model):
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_description = 'Lot/Serial'
|
||||
_check_company_auto = True
|
||||
_order = 'name, id'
|
||||
|
||||
name = fields.Char(
|
||||
'Lot/Serial Number', default=lambda self: self.env['ir.sequence'].next_by_code('stock.lot.serial'),
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<field name="product_id"/>
|
||||
<field name="show_details_visible"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_qty" readonly="1"/>
|
||||
<field name="quantity_done"/>
|
||||
<field name="reserved_availability"/>
|
||||
<field name="inventory_id"/>
|
||||
|
||||
@@ -17,7 +17,7 @@ real applications. """,
|
||||
'mail_bot',
|
||||
# 'snailmail',
|
||||
'mass_mailing',
|
||||
'mass_mailing_sms',
|
||||
'mass_mailing_sms', # adds portal
|
||||
'phone_validation',
|
||||
'sms',
|
||||
],
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
from flectra import fields, models
|
||||
|
||||
|
||||
class MailTestPortal(models.Model):
|
||||
""" A model intheriting from mail.thread with some fields used for portal
|
||||
sharing, like a partner, ..."""
|
||||
_description = 'Chatter Model for Portal'
|
||||
_name = 'mail.test.portal'
|
||||
_inherit = [
|
||||
'mail.thread',
|
||||
'portal.mixin',
|
||||
]
|
||||
|
||||
name = fields.Char()
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
def _compute_access_url(self):
|
||||
self.access_url = False
|
||||
for record in self.filtered('id'):
|
||||
record.access_url = '/my/test_portal/%s' % self.id
|
||||
|
||||
|
||||
class MailTestSMS(models.Model):
|
||||
""" A model inheriting from mail.thread with some fields used for SMS
|
||||
gateway, like a partner, a specific mobile phone, ... """
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mail_test_portal_all,mail.test.portal.all,model_mail_test_portal,,1,0,0,0
|
||||
access_mail_test_portal_user,mail.test.portal.user,model_mail_test_portal,base.group_user,1,1,1,1
|
||||
access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0
|
||||
access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1
|
||||
access_mail_test_sms_bl_all,mail.test.sms.bl.all,model_mail_test_sms_bl,,0,0,0,0
|
||||
|
||||
|
@@ -5,6 +5,7 @@ from . import test_flectrabot
|
||||
from . import test_phone_blacklist
|
||||
from . import test_mass_mailing
|
||||
from . import test_mass_sms
|
||||
from . import test_portal
|
||||
from . import test_sms_composer
|
||||
from . import test_sms_management
|
||||
from . import test_sms_performance
|
||||
|
||||
77
addons/test_mail_full/tests/test_portal.py
Normal file
77
addons/test_mail_full/tests/test_portal.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import http
|
||||
from flectra.addons.mail.tests.common import mail_new_test_user
|
||||
from flectra.tests import tagged, users
|
||||
from flectra.tests.common import HttpCase
|
||||
|
||||
|
||||
@tagged('portal')
|
||||
class TestPortal(HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPortal, self).setUp()
|
||||
|
||||
self.user_employee = mail_new_test_user(
|
||||
self.env,
|
||||
groups='base.group_user',
|
||||
login='employee',
|
||||
name='Ernest Employee',
|
||||
signature='--\nErnest',
|
||||
)
|
||||
self.partner_1, self.partner_2 = self.env['res.partner'].create([
|
||||
{'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'mobile': '0456001122'},
|
||||
{'name': 'Valid Poilvache',
|
||||
'email': 'valid.other@gmail.com',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'mobile': '+32 456 22 11 00'}
|
||||
])
|
||||
|
||||
self.record_portal = self.env['mail.test.portal'].create({
|
||||
'partner_id': self.partner_1.id,
|
||||
'name': 'Test Portal Record',
|
||||
})
|
||||
|
||||
self.record_portal._portal_ensure_token()
|
||||
|
||||
@users('employee')
|
||||
def test_portal_mixin(self):
|
||||
""" Test internals of portal mixin """
|
||||
customer = self.partner_1.with_env(self.env)
|
||||
record_portal = self.env['mail.test.portal'].create({
|
||||
'partner_id': customer.id,
|
||||
'name': 'Test Portal Record',
|
||||
})
|
||||
|
||||
self.assertFalse(record_portal.access_token)
|
||||
self.assertEqual(record_portal.access_url, '/my/test_portal/%s' % record_portal.id)
|
||||
|
||||
record_portal._portal_ensure_token()
|
||||
self.assertTrue(record_portal.access_token)
|
||||
|
||||
def test_portal_share_comment(self):
|
||||
""" Test posting through portal controller allowing to use a hash to
|
||||
post wihtout access rights. """
|
||||
self.authenticate(None, None)
|
||||
post_url = "/mail/chatter_post"
|
||||
post_data = {
|
||||
'csrf_token': http.WebRequest.csrf_token(self),
|
||||
'hash': self.record_portal._sign_token(self.partner_2.id),
|
||||
'message': 'Test',
|
||||
'pid': self.partner_2.id,
|
||||
'redirect': '/',
|
||||
'res_model': self.record_portal._name,
|
||||
'res_id': self.record_portal.id,
|
||||
'token': self.record_portal.access_token,
|
||||
}
|
||||
|
||||
# test as not logged
|
||||
self.url_open(url=post_url, data=post_data)
|
||||
message = self.record_portal.message_ids[0]
|
||||
|
||||
self.assertIn('Test', message.body)
|
||||
self.assertEqual(message.author_id, self.partner_2)
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_graph_measures_list {
|
||||
.o_graph_measures_list .o_dropdown_menu {
|
||||
max-height: calc(100vh - #{$o-navbar-height} - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
const { browser } = owl;
|
||||
|
||||
// Summernote Lib (neek change to make accessible: method and object)
|
||||
var dom = summernote.core.dom;
|
||||
var range = summernote.core.range;
|
||||
|
||||
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
||||
class Web_Unsplash(http.Controller):
|
||||
|
||||
def _get_access_key(self):
|
||||
if request.env.user._has_unsplash_key_rights():
|
||||
if request.env.user._has_unsplash_key_rights(mode='read'):
|
||||
return request.env['ir.config_parameter'].sudo().get_param('unsplash.access_key')
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
@@ -142,7 +142,7 @@ class Web_Unsplash(http.Controller):
|
||||
|
||||
@http.route("/web_unsplash/save_unsplash", type='json', auth="user")
|
||||
def save_unsplash(self, **post):
|
||||
if request.env.user._has_unsplash_key_rights():
|
||||
if request.env.user._has_unsplash_key_rights(mode='write'):
|
||||
request.env['ir.config_parameter'].sudo().set_param('unsplash.app_id', post.get('appId'))
|
||||
request.env['ir.config_parameter'].sudo().set_param('unsplash.access_key', post.get('key'))
|
||||
return True
|
||||
|
||||
@@ -6,10 +6,12 @@ from flectra import models
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def _has_unsplash_key_rights(self):
|
||||
def _has_unsplash_key_rights(self, mode='write'):
|
||||
self.ensure_one()
|
||||
# Website has no dependency to web_unsplash, we cannot warranty the order of the execution
|
||||
# of the overwrite done in 5ef8300.
|
||||
# So to avoid to create a new module bridge, with a lot of code, we prefer to make a check
|
||||
# here for website's user.
|
||||
return self.has_group('base.group_erp_manager') or self.has_group('website.group_website_designer')
|
||||
assert mode in ('read', 'write')
|
||||
website_group_required = (mode == 'write') and 'website.group_website_designer' or 'website.group_website_publisher'
|
||||
return self.has_group('base.group_erp_manager') or self.has_group(website_group_required)
|
||||
|
||||
Reference in New Issue
Block a user