summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-10-02 13:05:17 +0200
committerChristophe Simonis <chs@odoo.com>2017-10-02 13:05:17 +0200
commit5ce21c7355b6f8f4c587f011417af3bcca7603a1 (patch)
treeacf66dcf1b1fd0e19b34e2d48bbd214a00b41abd
parentabfac77a0644cad5eafe5a261b693ce4549bb0b5 (diff)
parentb3d0897f2d80352184874098d0f9fe2a6737d7a8 (diff)
[MERGE] forward port branch saas-16 up to b3d0897f2d8
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js35
-rw-r--r--addons/calendar/models/calendar.py2
-rw-r--r--addons/calendar/tests/test_calendar.py44
-rw-r--r--addons/hr_payroll/models/hr_payslip.py8
-rw-r--r--addons/hr_payroll/models/hr_salary_rule.py4
-rw-r--r--addons/sale/views/sale_views.xml2
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js47
-rw-r--r--addons/website_slides/models/slides.py3
-rw-r--r--doc/reference/guidelines.rst2
-rw-r--r--odoo/addons/base/module/module.py42
-rw-r--r--odoo/addons/base/module/wizard/base_language_install.py2
-rw-r--r--odoo/modules/loading.py4
-rw-r--r--odoo/service/db.py2
13 files changed, 160 insertions, 37 deletions
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index a16594d69cb..b35802422e4 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -717,26 +717,27 @@ var StatementModel = BasicModel.extend({
args: args,
})
.then(function (result) {
- var tax = result.taxes[0];
- var tax_prop = self._formatQuickCreate(line, {
- 'link': prop.id,
- 'tax_id': tax.id,
- 'amount': tax.amount,
- 'label': tax.name,
- 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,
- 'analytic': tax.analytic,
- 'is_tax': true,
- '__focus': false
- });
+ _.each(result.taxes, function(tax){
+ var tax_prop = self._formatQuickCreate(line, {
+ 'link': prop.id,
+ 'tax_id': tax.id,
+ 'amount': tax.amount,
+ 'label': tax.name,
+ 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,
+ 'analytic': tax.analytic,
+ 'is_tax': true,
+ '__focus': false
+ });
- prop.amount = tax.base;
- prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
- prop.invalid = !self._isValid(prop);
+ prop.amount = tax.base;
+ prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
+ prop.invalid = !self._isValid(prop);
- tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);
- tax_prop.invalid = prop.invalid;
+ tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);
+ tax_prop.invalid = prop.invalid;
- reconciliation_proposition.push(tax_prop);
+ reconciliation_proposition.push(tax_prop);
+ });
}));
} else {
var currencyData = {
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 6b061c6b79a..6de011b2915 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -1296,8 +1296,6 @@ class Meeting(models.Model):
meeting_origin = self.browse(real_id)
data = self.read(['allday', 'start', 'stop', 'rrule', 'duration'])[0]
- data['start_date' if data['allday'] else 'start_datetime'] = data['start']
- data['stop_date' if data['allday'] else 'stop_datetime'] = data['stop']
if data.get('rrule'):
data.update(
values,
diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py
index 5d3ffcc5e23..7fabe934a94 100644
--- a/addons/calendar/tests/test_calendar.py
+++ b/addons/calendar/tests/test_calendar.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import datetime
from datetime import datetime, timedelta
@@ -100,6 +101,49 @@ class TestCalendar(TransactionCase):
self.assertEqual(calendar_event_sprint_review.byday, '1', 'rrule_type should be mothly')
self.assertEqual(calendar_event_sprint_review.week_list, 'MO', 'rrule_type should be mothly')
+ def test_validation_error(self):
+ """
+ Ideally this should build the base event in such a way that calling
+ write() triggers detach_recurring_event, but I've no idea how that
+ actually works so just calling it directly for now
+ """
+ m = self.CalendarEvent.create({
+ 'name': "wheee",
+ 'start': '2017-07-12 14:30:00',
+ 'allday': False,
+ 'rrule': u'FREQ=WEEKLY;BYDAY=WE;INTERVAL=1;COUNT=100',
+ 'duration': 0.5,
+ 'stop': '2017-07-12 15:00:00',
+ })
+
+ values = {
+ 'allday': False,
+ 'name': u'wheee',
+ 'attendee_ids': [
+ (0, 0, {'state': u'needsAction', 'partner_id': 8, 'email': u'bob@example.com'}),
+ (0, 0, {'state': u'needsAction', 'partner_id': 10, 'email': u'ed@example.com'}),
+ ],
+ 'recurrency': True,
+ 'privacy': u'public',
+ 'stop': '2017-07-10 16:00:00',
+ 'alarm_ids': [(6, 0, [])],
+ 'start': '2017-07-10 15:30:00',
+ 'location': u"XXX",
+ 'duration': 0.5,
+ 'partner_ids': [(4, 10), (4, 8)],
+ 'description': u"A thing"
+ }
+
+ records = m.detach_recurring_event(values)
+ self.assertEqual(
+ (m.start_datetime, m.stop_datetime),
+ (u'2017-07-12 14:30:00', u'2017-07-12 15:00:00'),
+ )
+ self.assertEquals(
+ (records.start_datetime, records.stop_datetime),
+ (u'2017-07-10 15:30:00', u'2017-07-10 16:00:00'),
+ )
+
def test_event_order(self):
""" check the ordering of events when searching """
def create_event(name, date):
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index 562604a591d..3aaa01910ee 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -156,7 +156,7 @@ class HrPayslip(models.Model):
# if we don't give the contract, then the rules to apply should be for all current contracts of the employee
contract_ids = payslip.contract_id.ids or \
self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to)
- lines = [(0, 0, line) for line in self.get_payslip_lines(contract_ids, payslip.id)]
+ lines = [(0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id)]
payslip.write({'line_ids': lines, 'number': number})
return True
@@ -226,7 +226,7 @@ class HrPayslip(models.Model):
return res
@api.model
- def get_payslip_lines(self, contract_ids, payslip_id):
+ def _get_payslip_lines(self, contract_ids, payslip_id):
def _sum_salary_rule_category(localdict, category, amount):
if category.parent_id:
localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
@@ -327,9 +327,9 @@ class HrPayslip(models.Model):
localdict['result_qty'] = 1.0
localdict['result_rate'] = 100
#check if the rule can be applied
- if rule.satisfy_condition(localdict) and rule.id not in blacklist:
+ if rule._satisfy_condition(localdict) and rule.id not in blacklist:
#compute the amount of the rule
- amount, qty, rate = rule.compute_rule(localdict)
+ amount, qty, rate = rule._compute_rule(localdict)
#check if there is already a rule computed with that code
previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
#set/overwrite the amount computed for this rule in the localdict
diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py
index cf1b01fa60c..5d6c49a0bf7 100644
--- a/addons/hr_payroll/models/hr_salary_rule.py
+++ b/addons/hr_payroll/models/hr_salary_rule.py
@@ -177,7 +177,7 @@ class HrSalaryRule(models.Model):
#TODO should add some checks on the type of result (should be float)
@api.multi
- def compute_rule(self, localdict):
+ def _compute_rule(self, localdict):
"""
:param localdict: dictionary containing the environement in which to compute the rule
:return: returns a tuple build as the base/amount computed, the quantity and the rate
@@ -204,7 +204,7 @@ class HrSalaryRule(models.Model):
raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))
@api.multi
- def satisfy_condition(self, localdict):
+ def _satisfy_condition(self, localdict):
"""
@param contract_id: id of hr.contract to be tested
@return: returns True if the given rule match the condition for the given contract. Return False otherwise.
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 09a4a37b1ce..32ef3464bae 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -185,7 +185,7 @@
<form string="Sales Order" class="o_sale_order">
<header>
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary"/>
- <button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" class="btn-primary" attrs="{'invisible': ['|', ('state', 'in', ['sent', 'sale']), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
+ <button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" class="btn-primary" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="print_quotation" string="Print" type="object" states="draft" class="btn-primary o_sale_print"/>
<button name="action_confirm" id="action_confirm"
string="Confirm Sale" class="btn-primary" type="object"
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index bb7dc5db00e..0c910e5b4de 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6791,6 +6791,53 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2many checkboxes with default values', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.turtles.default = [3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles" widget="many2many_checkboxes">' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].turtles, [[6, false, [1]]],
+ "correct values should have been sent to create");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.notOk(form.$('.o_checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('.o_checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.ok(form.$('.o_checkbox input').eq(2).prop('checked'),
+ "third checkbox should be checked");
+
+ form.$('.o_checkbox input:checked').click(); // Uncheck default record
+ form.$('.o_checkbox input').first().click(); // Check first record
+ form.$('.o_checkbox input').first().click(); // Uncheck first record
+ form.$('.o_checkbox input').first().click(); // Recheck first record
+
+ assert.ok(form.$('.o_checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('.o_checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.notOk(form.$('.o_checkbox input').eq(2).prop('checked'),
+ "third checkbox should not be checked");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
QUnit.test('many2many list with x2many: add a record', function (assert) {
assert.expect(18);
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index fbf48b596ad..b7632e96527 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -354,7 +354,7 @@ class Slide(models.Model):
record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" allowFullScreen="true" frameborder="0"></iframe>' % (record.document_id)
else:
# embed google doc video
- record.embed_code = '<embed src="https://video.google.com/get_player?ps=docs&partnerid=30&docid=%s" type="application/x-shockwave-flash"></embed>' % (record.document_id)
+ record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % (record.document_id)
else:
record.embed_code = False
@@ -557,6 +557,7 @@ class Slide(models.Model):
'name': snippet['title'],
'image': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'],
'description': snippet['description'],
+ 'mime_type': False,
})
return {'values': values}
diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst
index fc15c10f7ab..92f2a223d78 100644
--- a/doc/reference/guidelines.rst
+++ b/doc/reference/guidelines.rst
@@ -346,7 +346,7 @@ Idiomatics Python Programming
new_dict = dict(my_dict)
new_list = list(old_list)
-- Python dictionnary : creation and update
+- Python dictionary : creation and update
.. code-block:: python
diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py
index 0fdb51ffce2..d28b56584e5 100644
--- a/odoo/addons/base/module/module.py
+++ b/odoo/addons/base/module/module.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from collections import defaultdict
+from decorator import decorator
from operator import attrgetter
import importlib
import io
@@ -25,6 +26,7 @@ import odoo
from odoo import api, fields, models, modules, tools, _
from odoo.exceptions import AccessDenied, UserError
from odoo.tools.parse_version import parse_version
+from odoo.http import request
_logger = logging.getLogger(__name__)
@@ -51,6 +53,23 @@ def backup(path, raise_exception=True):
cnt += 1
+def assert_log_admin_access(method):
+ """Decorator checking that the calling user is an administrator, and logging the call.
+
+ Raises an AccessDenied error if the user does not have administrator privileges, according
+ to `user._is_admin()`.
+ """
+ def check_and_log(method, self, *args, **kwargs):
+ user = self.env.user
+ origin = request.httprequest.remote_addr if request else 'n/a'
+ log_data = (method.__name__, self.sudo().mapped('name'), user.login, user.id, origin)
+ if not self.env.user._is_admin():
+ _logger.warning('DENY access to module.%s on %s to user %s ID #%s via %s', *log_data)
+ raise AccessDenied()
+ _logger.info('ALLOW access to module.%s on %s to user %s #%s via %s', *log_data)
+ return method(self, *args, **kwargs)
+ return decorator(check_and_log, method)
+
class ModuleCategory(models.Model):
_name = "ir.module.category"
_description = "Application"
@@ -319,7 +338,7 @@ class Module(models.Model):
raise UserError(msg % (module_name, e.args[0]))
@api.multi
- def state_update(self, newstate, states_to_update, level=100):
+ def _state_update(self, newstate, states_to_update, level=100):
if level < 1:
raise UserError(_('Recursion error in modules dependencies !'))
@@ -338,7 +357,7 @@ class Module(models.Model):
update_mods += dep.depend_id
# update dependency modules that require it, and determine demo for module
- update_demo = update_mods.state_update(newstate, states_to_update, level=level-1)
+ update_demo = update_mods._state_update(newstate, states_to_update, level=level-1)
module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods)
demo = demo or module_demo
@@ -349,6 +368,7 @@ class Module(models.Model):
return demo
+ @assert_log_admin_access
@api.multi
def button_install(self):
# domain to select auto-installable (but not yet installed) modules
@@ -365,7 +385,7 @@ class Module(models.Model):
modules = self
while modules:
# Mark the given modules and their dependencies to be installed.
- modules.state_update('to install', ['uninstalled'])
+ modules._state_update('to install', ['uninstalled'])
# Determine which auto-installable modules must be installed.
modules = self.search(auto_domain).filtered(must_install)
@@ -404,6 +424,7 @@ class Module(models.Model):
return dict(ACTION_DICT, name=_('Install'))
+ @assert_log_admin_access
@api.multi
def button_immediate_install(self):
""" Installs the selected module(s) immediately and fully,
@@ -415,11 +436,13 @@ class Module(models.Model):
_logger.info('User #%d triggered module installation', self.env.uid)
return self._button_immediate_function(type(self).button_install)
+ @assert_log_admin_access
@api.multi
def button_install_cancel(self):
self.write({'state': 'uninstalled', 'demo': False})
return True
+ @assert_log_admin_access
@api.multi
def module_uninstall(self):
""" Perform the various steps required to uninstall a module completely
@@ -521,6 +544,7 @@ class Module(models.Model):
'params': {'menu_id': menu.id},
}
+ @assert_log_admin_access
@api.multi
def button_immediate_uninstall(self):
"""
@@ -530,6 +554,7 @@ class Module(models.Model):
_logger.info('User #%d triggered module uninstallation', self.env.uid)
return self._button_immediate_function(type(self).button_uninstall)
+ @assert_log_admin_access
@api.multi
def button_uninstall(self):
if 'base' in self.mapped('name'):
@@ -538,6 +563,7 @@ class Module(models.Model):
(self + deps).write({'state': 'to remove'})
return dict(ACTION_DICT, name=_('Uninstall'))
+ @assert_log_admin_access
@api.multi
def button_uninstall_wizard(self):
""" Launch the wizard to uninstall the given module. """
@@ -555,6 +581,7 @@ class Module(models.Model):
self.write({'state': 'installed'})
return True
+ @assert_log_admin_access
@api.multi
def button_immediate_upgrade(self):
"""
@@ -563,6 +590,7 @@ class Module(models.Model):
"""
return self._button_immediate_function(type(self).button_upgrade)
+ @assert_log_admin_access
@api.multi
def button_upgrade(self):
Dependency = self.env['ir.module.module.dependency']
@@ -593,6 +621,7 @@ class Module(models.Model):
self.browse(to_install).button_install()
return dict(ACTION_DICT, name=_('Apply Schedule Upgrade'))
+ @assert_log_admin_access
@api.multi
def button_upgrade_cancel(self):
self.write({'state': 'installed'})
@@ -630,6 +659,7 @@ class Module(models.Model):
return new
# update the list of available packages
+ @assert_log_admin_access
@api.model
def update_list(self):
res = [0, 0] # [update, add]
@@ -672,10 +702,12 @@ class Module(models.Model):
return res
+ @assert_log_admin_access
@api.multi
def download(self, download=True):
return []
+ @assert_log_admin_access
@api.model
def install_from_urls(self, urls):
if not self.env.user.has_group('base.group_system'):
@@ -806,7 +838,7 @@ class Module(models.Model):
self.write({'category_id': cat_id})
@api.multi
- def update_translations(self, filter_lang=None):
+ def _update_translations(self, filter_lang=None):
if not filter_lang:
langs = self.env['res.lang'].search([('translatable', '=', True)])
filter_lang = [lang.code for lang in langs]
@@ -816,7 +848,7 @@ class Module(models.Model):
self.env['ir.translation'].load_module_terms(mod_names, filter_lang)
@api.multi
- def check(self):
+ def _check(self):
for module in self:
if not module.description_html:
_logger.warning('module %s: description is empty !', module.name)
diff --git a/odoo/addons/base/module/wizard/base_language_install.py b/odoo/addons/base/module/wizard/base_language_install.py
index 4c93be19bee..ae08af37587 100644
--- a/odoo/addons/base/module/wizard/base_language_install.py
+++ b/odoo/addons/base/module/wizard/base_language_install.py
@@ -22,7 +22,7 @@ class BaseLanguageInstall(models.TransientModel):
def lang_install(self):
self.ensure_one()
mods = self.env['ir.module.module'].search([('state', '=', 'installed')])
- mods.with_context(overwrite=self.overwrite).update_translations(self.lang)
+ mods.with_context(overwrite=self.overwrite)._update_translations(self.lang)
self.state = 'done'
return {
'name': _('Language Pack'),
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index ddabe6c188d..b56377bcf3e 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -148,7 +148,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
module = env['ir.module.module'].browse(module_id)
if perform_checks:
- module.check()
+ module._check()
if package.state=='to upgrade':
# upgrading the module information
@@ -164,7 +164,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
# Update translations for all installed languages
overwrite = odoo.tools.config["overwrite_existing_translations"]
- module.with_context(overwrite=overwrite).update_translations()
+ module.with_context(overwrite=overwrite)._update_translations()
registry._init_modules.add(package.name)
diff --git a/odoo/service/db.py b/odoo/service/db.py
index 4c6c23822ab..43760b97967 100644
--- a/odoo/service/db.py
+++ b/odoo/service/db.py
@@ -56,7 +56,7 @@ def _initialize_db(id, db_name, demo, lang, user_password, login='admin', countr
if lang:
modules = env['ir.module.module'].search([('state', '=', 'installed')])
- modules.update_translations(lang)
+ modules._update_translations(lang)
if country_code:
countries = env['res.country'].search([('code', 'ilike', country_code)])