[MERGE] forward port branch saas-14 up to 0c5cbbe0bb
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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,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()
|
||||
|
||||
Reference in New Issue
Block a user