diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-10-02 13:05:17 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-10-02 13:05:17 +0200 |
| commit | 5ce21c7355b6f8f4c587f011417af3bcca7603a1 (patch) | |
| tree | acf66dcf1b1fd0e19b34e2d48bbd214a00b41abd | |
| parent | abfac77a0644cad5eafe5a261b693ce4549bb0b5 (diff) | |
| parent | b3d0897f2d80352184874098d0f9fe2a6737d7a8 (diff) | |
[MERGE] forward port branch saas-16 up to b3d0897f2d8
| -rw-r--r-- | addons/account/static/src/js/reconciliation/reconciliation_model.js | 35 | ||||
| -rw-r--r-- | addons/calendar/models/calendar.py | 2 | ||||
| -rw-r--r-- | addons/calendar/tests/test_calendar.py | 44 | ||||
| -rw-r--r-- | addons/hr_payroll/models/hr_payslip.py | 8 | ||||
| -rw-r--r-- | addons/hr_payroll/models/hr_salary_rule.py | 4 | ||||
| -rw-r--r-- | addons/sale/views/sale_views.xml | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/relational_fields_tests.js | 47 | ||||
| -rw-r--r-- | addons/website_slides/models/slides.py | 3 | ||||
| -rw-r--r-- | doc/reference/guidelines.rst | 2 | ||||
| -rw-r--r-- | odoo/addons/base/module/module.py | 42 | ||||
| -rw-r--r-- | odoo/addons/base/module/wizard/base_language_install.py | 2 | ||||
| -rw-r--r-- | odoo/modules/loading.py | 4 | ||||
| -rw-r--r-- | odoo/service/db.py | 2 |
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','>=',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','>=',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)]) |
