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