[MERGE] forward port branch 11.0 up to ced9156a97

This commit is contained in:
Christophe Simonis
2018-08-07 19:08:07 +02:00
21 changed files with 133 additions and 46 deletions
+7 -5
View File
@@ -25,9 +25,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')
@@ -35,15 +35,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
+3 -3
View File
@@ -283,17 +283,17 @@ 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()
return True
@api.multi
def create_action(self):
ActWindowSudo = self.env['ir.actions.act_window'].sudo()
ActWindow = self.env['ir.actions.act_window']
view = self.env.ref('mail.email_compose_message_wizard_form')
for template in self:
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',
+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_field_widget 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">
@@ -1372,6 +1372,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.
+5 -2
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import float_is_zero
class PosMakePayment(models.TransientModel):
@@ -30,7 +31,7 @@ class PosMakePayment(models.TransientModel):
session_id = fields.Many2one('pos.session', required=True, default=_default_session)
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
amount = fields.Float(digits=(16, 2), required=True, default=_default_amount)
amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_name = fields.Char(string='Payment Reference')
payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Datetime.now())
@@ -49,11 +50,13 @@ class PosMakePayment(models.TransientModel):
"""
self.ensure_one()
order = self.env['pos.order'].browse(self.env.context.get('active_id', False))
currency = order.pricelist_id.currency_id
amount = order.amount_total - order.amount_paid
data = self.read()[0]
# add_payment expect a journal key
data['journal'] = data['journal_id'][0]
if amount != 0.0:
data['amount'] = currency.round(data['amount']) if currency else data['amount']
if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01):
order.add_payment(data)
if order.test_paid():
order.action_pos_order_paid()
+1 -1
View File
@@ -1219,13 +1219,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
@@ -744,6 +744,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))
@@ -752,7 +754,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="Website URL", related='local_url', deprecated=True)
@api.model
def get_serving_groups(self):
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
-1
View File
@@ -790,7 +790,6 @@ class Page(models.Model):
@api.multi
def write(self, vals):
self.ensure_one()
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
result = super(Page, self).write(vals)
+4 -3
View File
@@ -10,7 +10,7 @@ import uuid
from datetime import datetime
from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools
from odoo import fields, tools, SUPERUSER_ID
from odoo.tools.pycompat import string_types, to_text
from odoo.http import request
from odoo.modules.module import get_resource_path
@@ -230,10 +230,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)
+11 -2
View File
@@ -5,13 +5,12 @@ import odoo
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import MissingError, UserError, ValidationError, AccessError
from odoo.tools.safe_eval import safe_eval, test_python_expr
from odoo.tools import pycompat
from odoo.tools import pycompat, wrap_module
from odoo.http import request
import base64
from collections import defaultdict
import datetime
import dateutil
import logging
import time
@@ -19,6 +18,16 @@ from pytz import timezone
_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'
+1 -1
View File
@@ -206,7 +206,7 @@ class IrActionsReport(models.Model):
:param set_viewport_size: Enable a viewport sized '1024x1280' or '1280x1024' depending of landscape arg.
:return: A list of string representing the wkhtmltopdf process command args.
'''
command_args = []
command_args = ['--disable-local-file-access']
if set_viewport_size:
command_args.extend(['--viewport-size', landscape and '1024x1280' or '1280x1024'])
+20 -1
View File
@@ -11,7 +11,7 @@ from collections import defaultdict
import uuid
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
@@ -263,6 +263,15 @@ class IrAttachment(models.Model):
index_content = b"\n".join(words).decode('ascii')
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')
@@ -299,6 +308,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
+1
View File
@@ -188,6 +188,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 -11
View File
@@ -280,6 +280,19 @@ class Users(models.Model):
def onchange_parent_id(self):
return self.mapped('partner_id').onchange_parent_id()
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('company_id', 'company_ids')
def _check_company(self):
@@ -303,17 +316,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:
for vals in result:
if vals['id'] != self._uid:
for key in USER_PRIVATE_FIELDS:
if key in vals:
vals[key] = '********'
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
@@ -1494,6 +1494,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
@@ -66,7 +66,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
@@ -3623,6 +3623,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
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)
@@ -3769,10 +3778,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
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 []))
if expression.is_false(self, args):
# optimization: no need to query, as no record satisfies the domain
return 0 if count else []
+20
View File
@@ -1221,3 +1221,23 @@ pickle.load = _pickle_load
pickle.loads = lambda text, encoding='ASCII': _pickle_load(io.BytesIO(text), encoding=encoding)
pickle.dump = pickle_.dump
pickle.dumps = pickle_.dumps
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()