[MERGE] forward port branch saas-14 up to 0c5cbbe0bb

This commit is contained in:
Christophe Simonis
2018-08-07 17:01:16 +02:00
21 changed files with 141 additions and 49 deletions
+7 -5
View File
@@ -26,9 +26,9 @@ def now(**kwargs):
class ResPartner(models.Model):
_inherit = 'res.partner'
signup_token = fields.Char(copy=False)
signup_type = fields.Char(string='Signup Token Type', copy=False)
signup_expiration = fields.Datetime(copy=False)
signup_token = fields.Char(copy=False, groups="base.group_erp_manager")
signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager")
signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager")
signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid')
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
@@ -36,15 +36,17 @@ class ResPartner(models.Model):
@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self):
dt = now()
for partner in self:
for partner in self.sudo():
partner.signup_valid = bool(partner.signup_token) and \
(not partner.signup_expiration or dt <= partner.signup_expiration)
@api.multi
def _compute_signup_url(self):
""" proxy for function field towards actual implementation """
result = self._get_signup_url_for_action()
result = self.sudo()._get_signup_url_for_action()
for partner in self:
if any(u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user):
self.env['res.users'].check_access_rights('write')
partner.signup_url = result.get(partner.id, False)
@api.multi
+11 -1
View File
@@ -132,10 +132,20 @@ class ResUsers(models.Model):
template = self.env.ref('auth_signup.reset_password_email')
assert template._name == 'mail.template'
template_values = {
'email_to': '${object.email|safe}',
'email_cc': False,
'auto_delete': True,
'partner_to': False,
'scheduled_date': False,
}
template.write(template_values)
for user in self:
if not user.email:
raise UserError(_("Cannot send email: user %s has no email address.") % user.name)
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
with self.env.cr.savepoint():
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
_logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
@api.model
+1 -1
View File
@@ -38,7 +38,7 @@ class TestLead2opportunity2win(TestCrmCases):
crm_case_3.message_post(subject='Test note', body='Détails envoyés par le client sur ​​le FAX pour la qualité')
# I convert mass lead into opportunity customer.
mass = CrmLead2OpportunityPartnerMass.with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
mass = CrmLead2OpportunityPartnerMass.sudo(self.crm_salemanager.id).with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
'user_ids': [(6, 0, self.env.ref('base.user_root').ids)],
'team_id': self.env.ref("sales_team.team_sales_department").id
})
+6 -5
View File
@@ -8,12 +8,13 @@ class SaleOrder(models.Model):
@api.multi
def action_confirm(self):
self.ensure_one()
res = super(SaleOrder, self).action_confirm()
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
self.order_line._update_registrations(confirm=self.amount_total == 0, cancel_to_draft=False)
if any(self.order_line.filtered(lambda line: line.event_id)):
return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration')
for order in self:
# confirm registration if it was free (otherwise it will be confirmed once invoice fully paid)
order.order_line._update_registrations(confirm=order.amount_total == 0, cancel_to_draft=False)
if any(order.order_line.filtered(lambda line: line.event_id)):
return self.env['ir.actions.act_window'].with_context(default_sale_order_id=order.id).for_xml_id(
'event_sale', 'action_sale_order_event_registration')
return res
+6 -6
View File
@@ -255,22 +255,22 @@ class MailTemplate(models.Model):
def unlink_action(self):
for template in self:
if template.ref_ir_act_window:
template.ref_ir_act_window.sudo().unlink()
template.ref_ir_act_window.unlink()
if template.ref_ir_value:
template.ref_ir_value.sudo().unlink()
template.ref_ir_value.unlink()
return True
@api.multi
def create_action(self):
ActWindowSudo = self.env['ir.actions.act_window'].sudo()
IrValuesSudo = self.env['ir.values'].sudo()
ActWindow = self.env['ir.actions.act_window']
IrValues = self.env['ir.values']
view = self.env.ref('mail.email_compose_message_wizard_form')
for template in self:
src_obj = template.model_id.model
button_name = _('Send Mail (%s)') % template.name
action = ActWindowSudo.create({
action = ActWindow.create({
'name': button_name,
'type': 'ir.actions.act_window',
'res_model': 'mail.compose.message',
@@ -281,7 +281,7 @@ class MailTemplate(models.Model):
'view_id': view.id,
'target': 'new',
})
ir_value = IrValuesSudo.create({
ir_value = IrValues.create({
'name': button_name,
'model': src_obj,
'key2': 'client_action_multi',
+5 -2
View File
@@ -9,7 +9,9 @@
<sheet>
<div class="oe_button_box" name="button_box">
<field name="ref_ir_act_window" invisible="1"/>
<button class="oe_stat_button" name="create_action" type="object"
<button class="oe_stat_button"
groups="base.group_system"
name="create_action" type="object"
attrs="{'invisible':[('ref_ir_act_window','!=',False)]}" icon="fa-plus"
help="Display an option on related documents to open a composition wizard with this template">
<div class="o_form_field o_stat_info">
@@ -17,7 +19,8 @@
<span class="o_stat_text">Context Action</span>
</div>
</button>
<button name="unlink_action" type="object"
<button name="unlink_action" type="object"
groups="base.group_system"
class="oe_stat_button" icon="fa-minus"
attrs="{'invisible':[('ref_ir_act_window','=',False)]}"
help="Remove the contextual action to use this template on related documents" widget="statinfo">
@@ -1317,6 +1317,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
reload_partners: function(){
var self = this;
return this.pos.load_new_partners().then(function(){
// partners may have changed in the backend
self.partner_cache = new DomCache();
self.render_list(self.pos.db.get_partners_sorted(1000));
// update the currently assigned client if it has been changed in db.
+1 -1
View File
@@ -531,7 +531,7 @@ class Report(models.Model):
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string representing the wkhtmltopdf arguments
"""
command_args = []
command_args = ['--disable-local-file-access']
if paperformat.format and paperformat.format != 'custom':
command_args.extend(['--page-size', paperformat.format])
+1 -1
View File
@@ -1018,13 +1018,13 @@ class SaleOrderLine(models.Model):
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
self.discount = 0.0
if not (self.product_id and self.product_uom and
self.order_id.partner_id and self.order_id.pricelist_id and
self.order_id.pricelist_id.discount_policy == 'without_discount' and
self.env.user.has_group('sale.group_discount_per_so_line')):
return
self.discount = 0.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id.id,
+4 -1
View File
@@ -714,6 +714,8 @@ class Database(http.Controller):
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
def restore(self, master_pwd, backup_file, name, copy=False):
try:
data_file = None
db.check_super(master_pwd)
with tempfile.NamedTemporaryFile(delete=False) as data_file:
backup_file.save(data_file)
db.restore_db(name, data_file.name, str2bool(copy))
@@ -722,7 +724,8 @@ class Database(http.Controller):
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
finally:
os.unlink(data_file.name)
if data_file:
os.unlink(data_file.name)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
def change_password(self, master_pwd, master_pwd_new):
+5 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class Attachment(models.Model):
@@ -10,3 +10,7 @@ class Attachment(models.Model):
# related for backward compatibility with saas-6
website_url = fields.Char(string="Attachment URL", related='local_url', deprecated=True)
@api.model
def get_serving_groups(self):
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
+3 -2
View File
@@ -20,14 +20,14 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_
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 token_field: name of the field that contains the token on the object (defaults to 'token')
:param string token_field: name of the field that contains the token on the object (deprecated, use _mail_post_token_field)
: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)
author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
if token and record and token == getattr(record.sudo(), token_field, None):
if token and record and token == getattr(record.sudo(), record._mail_post_token_field, None):
record = record.sudo()
if request.env.user == request.env.ref('base.public_user'):
author_id = record.partner_id.id if hasattr(record, 'partner_id') else author_id
@@ -35,6 +35,7 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_
if not author_id:
raise NotFound()
kw.pop('csrf_token', None)
kw.pop('attachment_ids', None)
return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message,
message_type=kw.pop('message_type', "comment"),
subtype=kw.pop('subtype', "mt_comment"),
@@ -56,3 +56,9 @@ class MailMessage(models.Model):
if self.env.cr.fetchall():
raise AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation))
return super(MailMessage, self).check_access_rule(operation=operation)
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
_mail_post_token_field = 'access_token' # token field for external posts, to be overridden
+11 -1
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import dateutil
import logging
import os
import time
@@ -13,9 +12,20 @@ from odoo import api, fields, models, tools, _
from odoo.exceptions import MissingError, UserError, ValidationError
from odoo.report.report_sxw import report_sxw, report_rml
from odoo.tools.safe_eval import safe_eval, test_python_expr
from odoo.tools.misc import wrap_module
_logger = logging.getLogger(__name__)
# build dateutil helper, starting with the relevant *lazy* imports
import dateutil
import dateutil.parser
import dateutil.relativedelta
import dateutil.rrule
import dateutil.tz
mods = {'parser', 'relativedelta', 'rrule', 'tz'}
attribs = {atr for m in mods for atr in getattr(dateutil, m).__all__}
dateutil = wrap_module(dateutil, mods | attribs)
class IrActions(models.Model):
_name = 'ir.actions.actions'
+20 -1
View File
@@ -10,7 +10,7 @@ import re
from collections import defaultdict
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessError
from odoo.exceptions import AccessError, ValidationError
from odoo.tools import config, human_size, ustr, html_escape
from odoo.tools.mimetypes import guess_mimetype
@@ -262,6 +262,15 @@ class IrAttachment(models.Model):
index_content = ustr("\n".join(words))
return index_content
@api.model
def get_serving_groups(self):
""" An ir.attachment record may be used as a fallback in the
http dispatch if its type field is set to "binary" and its url
field is set as the request's url. Only the groups returned by
this method are allowed to create and write on such records.
"""
return ['base.group_system']
name = fields.Char('Attachment Name', required=True)
datas_fname = fields.Char('File Name')
description = fields.Text('Description')
@@ -295,6 +304,16 @@ class IrAttachment(models.Model):
self._table, ['res_model', 'res_id'])
return res
@api.one
@api.constrains('type', 'url')
def _check_serving_attachments(self):
# restrict writing on attachments that could be served by the
# ir.http's dispatch exception handling
if self.type == 'binary' and self.url:
has_group = self.env.user.has_group
if not any([has_group(g) for g in self.get_serving_groups()]):
raise ValidationError("Sorry, you are not allowed to write on this document")
@api.model
def check(self, mode, values=None):
"""Restricts the access to an ir.attachment, according to referred model
+4 -3
View File
@@ -8,7 +8,7 @@ import textwrap
import uuid
from datetime import datetime
from subprocess import Popen, PIPE
from odoo import fields, tools
from odoo import fields, tools, SUPERUSER_ID
from odoo.http import request
from odoo.modules.module import get_resource_path
import psycopg2
@@ -190,10 +190,11 @@ class AssetsBundle(object):
self.env.cr.execute("""
SELECT max(id)
FROM ir_attachment
WHERE url like %s
WHERE create_uid = %s
AND url like %s
GROUP BY datas_fname
ORDER BY datas_fname
""", [url_pattern])
""", [SUPERUSER_ID, url_pattern])
attachment_ids = [r[0] for r in self.env.cr.fetchall()]
return self.env['ir.attachment'].sudo().browse(attachment_ids)
+1
View File
@@ -169,6 +169,7 @@ class Module(models.Model):
'doctitle_xform': False,
'output_encoding': 'unicode',
'xml_declaration': False,
'file_insertion_enabled': False,
}
output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter())
module.description_html = tools.html_sanitize(output)
+14 -13
View File
@@ -282,6 +282,19 @@ class Users(models.Model):
if any(user.company_ids and user.company_id not in user.company_ids for user in self):
raise ValidationError(_('The chosen company is not in the allowed companies for this user'))
def _read_from_database(self, field_names, inherited_field_names=[]):
super(Users, self)._read_from_database(field_names, inherited_field_names)
canwrite = self.check_access_rights('write', raise_exception=False)
if not canwrite and set(USER_PRIVATE_FIELDS).intersection(field_names):
for record in self:
for f in USER_PRIVATE_FIELDS:
try:
record._cache[f]
record._cache[f] = '********'
except Exception:
# skip SpecialValue (e.g. for missing record or access right)
pass
@api.multi
@api.constrains('action_id')
def _check_action_id(self):
@@ -299,19 +312,7 @@ class Users(models.Model):
# safe fields only, so we read as super-user to bypass access rights
self = self.sudo()
result = super(Users, self).read(fields=fields, load=load)
canwrite = self.env['ir.model.access'].check('res.users', 'write', False)
if not canwrite:
def override_password(vals):
if (vals['id'] != self._uid):
for key in USER_PRIVATE_FIELDS:
if key in vals:
vals[key] = '********'
return vals
result = map(override_password, result)
return result
return super(Users, self).read(fields=fields, load=load)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+1
View File
@@ -1523,6 +1523,7 @@ def db_filter(dbs, httprequest=None):
if d == "www" and r:
d = r.partition('.')[0]
if odoo.tools.config['dbfilter']:
d, h = re.escape(d), re.escape(h)
r = odoo.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
dbs = [i for i in dbs if re.match(r, i)]
elif odoo.tools.config['db_name']:
+10 -5
View File
@@ -60,7 +60,7 @@ _unlink = logging.getLogger(__name__ + '.unlink')
regex_order = re.compile('^(\s*([a-z0-9:_]+|"[a-z0-9:_]+")(\s+(desc|asc))?\s*(,|$))+(?<!,)$', re.I)
regex_object_name = re.compile(r'^[a-z0-9_.]+$')
regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
onchange_v7 = re.compile(r"^(\w+)\((.*)\)$")
onchange_v7 = re.compile(r"^([a-zA-Z]\w+)\((.*)\)$")
AUTOINIT_RECALCULATE_STORED_FIELDS = 1000
@@ -3541,6 +3541,15 @@ class BaseModel(object):
if table not in query.tables:
query.tables.append(table)
if self._transient:
# One single implicit access rule for transient models: owner only!
# This is ok because we assert that TransientModels always have
# log_access enabled, so that 'create_uid' is always there.
domain = [('create_uid', '=', self._uid)]
tquery = self._where_calc(domain, active_test=False)
apply_rule(tquery.where_clause, tquery.where_clause_params, tquery.tables)
return
# apply main rules on the object
Rule = self.env['ir.rule']
where_clause, where_params, tables = Rule.domain_get(self._name, mode)
@@ -3688,10 +3697,6 @@ class BaseModel(object):
"""
self.sudo(access_rights_uid or self._uid).check_access_rights('read')
# For transient models, restrict access to the current user, except for the super-user
if self.is_transient() and self._log_access and self._uid != SUPERUSER_ID:
args = expression.AND(([('create_uid', '=', self._uid)], args or []))
query = self._where_calc(args)
self._apply_ir_rules(query, 'read')
order_by = self._generate_order_by(order, query)
+21
View File
@@ -21,6 +21,7 @@ import socket
import sys
import threading
import time
import types
import werkzeug.utils
import zipfile
from cStringIO import StringIO
@@ -1253,3 +1254,23 @@ class Pickle(object):
dump = cPickle.dump
pickle = Pickle
def wrap_module(module, attr_list):
"""Helper for wrapping a package/module to expose selected attributes
:param Module module: the actual package/module to wrap, as returned by ``import <module>``
:param iterable attr_list: a global list of attributes to expose, usually the top-level
attributes and their own main attributes. No support for hiding attributes in case
of name collision at different levels.
"""
attr_list = set(attr_list)
class WrappedModule(object):
def __getattr__(self, attrib):
if attrib in attr_list:
target = getattr(module, attrib)
if isinstance(target, types.ModuleType):
return wrap_module(target, attr_list)
return target
raise AttributeError(attrib)
# module and attr_list are in the closure
return WrappedModule()