summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-02-01 18:15:21 +0100
committerChristophe Simonis <chs@odoo.com>2018-02-01 18:15:21 +0100
commit7e469eb77aae1f7e05a3ce0f6575b5149fed3ca1 (patch)
tree25023238fd6ccde1f3b8e805b03fcf228798a7b0
parentc4895c2f2ba0fcd8fc5aef210003c20ba2154979 (diff)
parentf5e7b866860436782ade53308da1ff725be1f124 (diff)
[MERGE] forward port branch 11.0 up to f5e7b866860
-rw-r--r--addons/account/models/account_payment.py21
-rw-r--r--addons/account/views/res_config_settings_views.xml4
-rw-r--r--addons/account_asset/models/account_invoice.py3
-rw-r--r--addons/account_check_printing/models/account_payment.py2
-rw-r--r--addons/barcodes/models/__init__.py1
-rw-r--r--addons/barcodes/models/ir_http.py14
-rw-r--r--addons/barcodes/static/src/js/barcode_events.js3
-rw-r--r--addons/event/models/event_mail.py6
-rw-r--r--addons/hr/views/hr_views.xml6
-rw-r--r--addons/hr_timesheet/models/project.py2
-rw-r--r--addons/http_routing/models/ir_http.py2
-rw-r--r--addons/iap/static/src/xml/iap_templates.xml8
-rw-r--r--addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml6
-rw-r--r--addons/mail/models/mail_template.py2
-rw-r--r--addons/mail/static/src/js/client_action.js2
-rw-r--r--addons/maintenance/models/maintenance.py2
-rw-r--r--addons/mass_mailing/static/src/js/mass_mailing_editor.js10
-rw-r--r--addons/mass_mailing/views/snippets_themes_options.xml23
-rw-r--r--addons/mrp/models/stock_move.py2
-rw-r--r--addons/mrp/views/mrp_production_views.xml2
-rw-r--r--addons/pad/models/pad.py2
-rw-r--r--addons/payment_sips/models/payment.py2
-rw-r--r--addons/point_of_sale/models/pos_order.py5
-rw-r--r--addons/point_of_sale/static/src/js/models.js6
-rw-r--r--addons/purchase/models/account_invoice.py6
-rw-r--r--addons/resource/models/resource.py10
-rw-r--r--addons/resource/tests/test_resource.py6
-rw-r--r--addons/sale/models/sale.py3
-rw-r--r--addons/sms/wizard/send_sms.py2
-rw-r--r--addons/stock/views/stock_picking_views.xml2
-rw-r--r--addons/stock_account/views/stock_account_views.xml4
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js1
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js1
-rw-r--r--addons/web/static/src/js/fields/special_fields.js3
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js2
-rw-r--r--addons/web/static/src/less/webclient.less50
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js4
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js87
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js12
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js34
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js3
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js20
-rw-r--r--addons/web_editor/static/src/js/widgets/ace.js2
-rw-r--r--addons/web_editor/static/tests/web_editor_tests.js16
-rw-r--r--addons/website/models/ir_http.py15
-rw-r--r--addons/website_blog/models/website_blog.py2
-rw-r--r--addons/website_sale/views/templates.xml14
-rw-r--r--doc/cla/corporate/vauxoo.md1
-rw-r--r--doc/cla/individual/kaberer.md11
-rw-r--r--doc/cla/individual/moutio.md9
-rw-r--r--doc/cla/individual/phucngta.md11
-rw-r--r--doc/reference/module.rst45
-rw-r--r--doc/setup/deploy.rst36
-rw-r--r--doc/webservices/localization.rst9
-rw-r--r--odoo/addons/base/models/ir_actions.py5
-rw-r--r--odoo/addons/base/models/res_users.py8
-rw-r--r--odoo/addons/base/tests/test_float.py18
-rw-r--r--odoo/http.py28
-rw-r--r--odoo/tools/float_utils.py27
59 files changed, 444 insertions, 199 deletions
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index b6bc2732231..8764d190991 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -332,7 +332,7 @@ class account_payment(models.Model):
else:
if self.payment_type == 'inbound':
domain.append(('at_least_one_inbound', '=', True))
- else:
+ elif self.payment_type == 'outbound':
domain.append(('at_least_one_outbound', '=', True))
return {'domain': domain, 'journal_types': set(journal_type)}
@@ -341,9 +341,16 @@ class account_payment(models.Model):
jrnl_filters = self._compute_journal_domain_and_types()
journal_types = jrnl_filters['journal_types']
domain_on_types = [('type', 'in', list(journal_types))]
- if self.journal_id.type not in journal_types:
- self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
- return {'domain': {'journal_id': jrnl_filters['domain'] + domain_on_types}}
+
+ journal_domain = jrnl_filters['domain'] + domain_on_types
+ default_journal_id = self.env.context.get('default_journal_id')
+ if not default_journal_id:
+ if self.journal_id.type not in journal_types:
+ self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
+ else:
+ journal_domain = journal_domain.append(('id', '=', default_journal_id))
+
+ return {'domain': {'journal_id': journal_domain}}
@api.one
@api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id')
@@ -374,6 +381,8 @@ class account_payment(models.Model):
self.partner_type = 'customer'
elif self.payment_type == 'outbound':
self.partner_type = 'supplier'
+ else:
+ self.partner_type = False
# Set payment method domain
res = self._onchange_journal()
if not res.get('domain', {}):
@@ -505,7 +514,7 @@ class account_payment(models.Model):
@api.multi
def action_draft(self):
return self.write({'state': 'draft'})
-
+
def action_validate_invoice_payment(self):
""" Posts a payment used to pay an invoice. This function only posts the
payment by default but can be overridden to apply specific post or pre-processing.
@@ -669,7 +678,7 @@ class account_payment(models.Model):
for inv in invoice:
if inv.move_id:
name += inv.number + ', '
- name = name[:len(name)-2]
+ name = name[:len(name)-2]
return {
'name': name,
'account_id': self.destination_account_id.id,
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 7a878d265c7..22726578d1b 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -168,10 +168,6 @@
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
<field name="currency_exchange_journal_id"/>
</div>
- <div class="row mt16">
- <label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
- <field name="currency_exchange_journal_id"/>
- </div>
</div>
</div>
</div>
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py
index dd22ea851f3..1c8333f550a 100644
--- a/addons/account_asset/models/account_invoice.py
+++ b/addons/account_asset/models/account_invoice.py
@@ -3,7 +3,8 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
-from odoo import api, fields, models
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from odoo.addons import decimal_precision as dp
diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py
index aa49b0f1993..65d734fd11a 100644
--- a/addons/account_check_printing/models/account_payment.py
+++ b/addons/account_check_printing/models/account_payment.py
@@ -32,7 +32,7 @@ class AccountRegisterPayments(models.TransientModel):
res = super(AccountRegisterPayments, self)._prepare_payment_vals(invoices)
if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
res.update({
- 'check_amount_in_words': self.check_amount_in_words,
+ 'check_amount_in_words': self.currency_id.amount_to_text(res['amount']) if self.multi else self.check_amount_in_words,
'check_manual_sequencing': self.check_manual_sequencing,
})
return res
diff --git a/addons/barcodes/models/__init__.py b/addons/barcodes/models/__init__.py
index 05b9acf6f97..e3f49cb14ed 100644
--- a/addons/barcodes/models/__init__.py
+++ b/addons/barcodes/models/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import barcodes
from . import barcode_events_mixin
+from . import ir_http
diff --git a/addons/barcodes/models/ir_http.py b/addons/barcodes/models/ir_http.py
new file mode 100644
index 00000000000..7ff6aa7c8f2
--- /dev/null
+++ b/addons/barcodes/models/ir_http.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class IrHttp(models.AbstractModel):
+ _inherit = 'ir.http'
+
+ def session_info(self):
+ res = super(IrHttp, self).session_info()
+ res['max_time_between_keys_in_ms'] = int(
+ self.env['ir.config_parameter'].sudo().get_param('barcode.max_time_between_keys_in_ms', default='55'))
+ return res
diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js
index f26748ac7b3..e02d3aafdb7 100644
--- a/addons/barcodes/static/src/js/barcode_events.js
+++ b/addons/barcodes/static/src/js/barcode_events.js
@@ -3,6 +3,7 @@ odoo.define('barcodes.BarcodeEvents', function(require) {
var core = require('web.core');
var mixins = require('web.mixins');
+var session = require('web.session');
// For IE >= 9, use this, new CustomEvent(), instead of new Event()
@@ -26,7 +27,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
suffix: /[\n\r\t]+/,
// Keys from a barcode scanner are usually processed as quick as possible,
// but some scanners can use an intercharacter delay (we support <= 50 ms)
- max_time_between_keys_in_ms: 55,
+ max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
init: function() {
mixins.PropertiesMixin.init.call(this);
diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py
index d87c24dd663..1013f5a083b 100644
--- a/addons/event/models/event_mail.py
+++ b/addons/event/models/event_mail.py
@@ -96,6 +96,7 @@ class EventMailScheduler(models.Model):
@api.one
def execute(self):
+ now = fields.Datetime.now()
if self.interval_type == 'after_sub':
# update registration lines
lines = [
@@ -105,9 +106,10 @@ class EventMailScheduler(models.Model):
if lines:
self.write({'mail_registration_ids': lines})
# execute scheduler on registrations
- self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT)).execute()
+ self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= now).execute()
else:
- if not self.mail_sent:
+ # Do not send emails if the mailing was scheduled before the event but the event is over
+ if not self.mail_sent and (self.interval_type != 'before_event' or self.event_id.date_end > now):
self.event_id.mail_attendees(self.template_id.id)
self.write({'mail_sent': True})
return True
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index fc75dbe232e..d3351e1b5f2 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -290,12 +290,14 @@
</field>
</record>
+ <!-- TODO remove me in master -->
<record id="hr_employee_action_subordinate_hierachy" model="ir.actions.act_window">
<field name="name">Subordinate Hierarchy</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">hr.employee</field>
- <field name="domain">[('id','in',active_ids)]</field>
- <field name="view_type">tree</field>
+ <field name="domain">['|', ('id','in',active_ids), ('parent_id', 'in', active_ids)]</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
<field name="view_id" ref="view_partner_tree2"/>
<field name="binding_model_id" ref="hr.model_hr_employee"/>
</record>
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index af27e3094a4..237e280ed65 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -67,7 +67,7 @@ class Task(models.Model):
if 'project_id' in values:
project_id = values.get('project_id')
# a timesheet must have an analytic account (and a project)
- if not project_id:
+ if self and not project_id:
raise UserError(_('This task must have a project since they are linked to timesheets.'))
self.sudo().mapped('timesheet_ids').write({
'project_id': project_id,
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 34e100cea2e..4011f4987d7 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -418,6 +418,8 @@ class IrHttp(models.AbstractModel):
try:
_, path = rule.build(arguments)
assert path is not None
+ except odoo.exceptions.MissingError:
+ return cls._handle_exception(werkzeug.exceptions.NotFound())
except Exception as e:
return cls._handle_exception(e)
diff --git a/addons/iap/static/src/xml/iap_templates.xml b/addons/iap/static/src/xml/iap_templates.xml
index 9b52a4ec445..4995c4527e3 100644
--- a/addons/iap/static/src/xml/iap_templates.xml
+++ b/addons/iap/static/src/xml/iap_templates.xml
@@ -17,9 +17,11 @@
</div>
<t t-extend="DashboardApps">
- <t t-jquery=".o_web_settings_dashboard_pills" t-operation="append">
- <a class="pull-right" t-att-href="widget.data.url" target="_blank">
- <i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
+ <t t-jquery=".o_web_settings_dashboard_pills" t-operation="after">
+ <div class="text-center" style="display: inline-block">
+ <a t-att-href="widget.data.url" target="_blank">
+ <i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
+ </div>
</t>
</t>
diff --git a/addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml b/addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml
index 72bc16378ab..44137ab88f5 100644
--- a/addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml
+++ b/addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml
@@ -62,7 +62,8 @@
<strong><span t-esc="h['rule_category']"/></strong>
</td>
<td>
- <strong><span t-esc="format(h['total'], 'monetary', o.company_id.currency_id)"/></strong>
+ <strong><span t-esc="h['total']"
+ t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/></strong>
</td>
</tr>
</t>
@@ -75,7 +76,8 @@
<span t-esc="'..'*h['level']"/><span t-esc="h['rule_category']"/>
</td>
<td>
- <span t-esc="format(h['total'], 'monetary', o.company_id.currency_id)"/>
+ <span t-esc="h['total']"
+ t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
</t>
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index e0e16eeef7c..65ffe08e1e1 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -300,7 +300,7 @@ class MailTemplate(models.Model):
def _process_link(url):
new_url = urls.url_parse(url)
- if new_url.scheme and new_url.netloc:
+ if new_url.scheme and (new_url.netloc or new_url.scheme == 'mailto'):
return url
return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url()
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index 2b5b342787b..61650ceaa73 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -890,7 +890,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var def = window.Notification && window.Notification.requestPermission();
if (def) {
def.then(function (value) {
- if (value === 'denied') {
+ if (value !== 'granted') {
utils.send_notification(self, _t('Permission denied'),
_t('Odoo will not have the permission to send native notifications on this device.'));
} else {
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index d46b688ca79..53b450baee7 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -368,7 +368,7 @@ class MaintenanceTeam(models.Model):
_name = 'maintenance.team'
_description = 'Maintenance Teams'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
member_ids = fields.Many2many('res.users', 'maintenance_team_users_rel', string="Team Members")
color = fields.Integer("Color Index", default=0)
request_ids = fields.One2many('maintenance.request', 'maintenance_team_id', copy=False)
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index 7705e62fa9e..c40f31c258f 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -323,20 +323,20 @@ snippets_editor.Class.include({
$body.removeClass(all_classes).addClass(theme_params.className);
var $old_layout = $editable_area.find(".o_layout");
- // This wrapper structure is the only way to have a responsive and
- // centered fixed-width content column on all mail clients
var $new_wrapper, $new_wrapper_content;
if (theme_params.nowrap) {
$new_wrapper = $new_wrapper_content = $("<div/>", {"class": "oe_structure"});
}
else {
+ // This wrapper structure is the only way to have a responsive and
+ // centered fixed-width content column on all mail clients
$new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
- $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
+ $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_no_colorpicker o_mail_wrapper_td oe_structure'});
$new_wrapper.append($('<tr/>').append(
- $("<td/>", {class: 'o_mail_no_resize'}),
+ $("<td/>", {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'}),
$new_wrapper_content,
- $("<td/>", {class: 'o_mail_no_resize'})
+ $("<td/>", {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'})
));
}
var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);
diff --git a/addons/mass_mailing/views/snippets_themes_options.xml b/addons/mass_mailing/views/snippets_themes_options.xml
index 34d973369e8..88c3ea889e7 100644
--- a/addons/mass_mailing/views/snippets_themes_options.xml
+++ b/addons/mass_mailing/views/snippets_themes_options.xml
@@ -6,10 +6,12 @@
<t t-raw="0"/>
<div data-js="width-x"
- data-selector="img, .mv, .col_mv, td:not(.o_mail_no_resize), th">
- </div>
+ data-selector="img, .mv, .col_mv, td, th"
+ data-exclude=".o_mail_no_resize, .o_mail_wrapper_td"/>
- <div data-js="table_item" data-selector="td, th"/>
+ <div data-js="table_item"
+ data-selector="td, th"
+ data-exclude=".o_mail_wrapper_td"/>
<div data-js="table_row"
data-selector="tr:has(> .row), tr:has(> .col_mv)"
@@ -18,11 +20,13 @@
<div data-js="table_column"
data-selector=".col>td, .col>th"
+ data-exclude=".o_mail_wrapper_td"
data-drop-near=".col>td, .col>th">
</div>
<div data-js="table_column_mv"
data-selector=".col_mv, td, th"
+ data-exclude=".o_mail_wrapper_td"
data-drop-near=".col_mv, td, th">
</div>
@@ -33,19 +37,22 @@
</div>
<div data-js="resize"
- data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div">
+ data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div"
+ data-exclude=".o_mail_no_resize, .o_mail_wrapper_td">
</div>
<div data-js="margin-y"
- data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td:not(.o_mail_no_resize), th">
+ data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td, th"
+ data-exclude=".o_mail_no_resize, .o_mail_wrapper_td">
</div>
<div data-js="colorpicker"
data-selector="
- [data-oe-field='body_html'] > div:not(.o_layout):not(.o_mail_no_colorpicker),
- [data-oe-field='body_html'] .oe_structure > div:not(.o_mail_no_colorpicker),
+ [data-oe-field='body_html'] > div:not(.o_layout),
+ [data-oe-field='body_html'] .oe_structure > div,
td, th
- ">
+ "
+ data-exclude=".o_mail_no_colorpicker">
<li class="dropdown-submenu">
<a tabindex="-1" href="#"><i class="fa fa-eyedropper"/>Background Color</a>
<ul class="dropdown-menu">
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 73f4c861727..ccba058f291 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -151,7 +151,7 @@ class StockMove(models.Model):
# all grouped in the same picking.
if not self.picking_type_id:
return self
- bom = self.env['mrp.bom'].sudo()._bom_find(product=self.product_id)
+ bom = self.env['mrp.bom'].sudo()._bom_find(product=self.product_id, company_id=self.company_id.id)
if not bom or bom.type != 'phantom':
return self
phantom_moves = self.env['stock.move']
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 1d6136d7e95..9de067caf0d 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -48,7 +48,7 @@
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
<span class="label label-danger" attrs="{'invisible': ['|',('availability', 'in', ('assigned', 'none')), ('state', 'not in', ('confirmed','progress'))]}">Raw materials not available!</span>
<field name="state" widget="statusbar" statusbar_visible="confirmed,progress,done"/>
- <button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', '=', 'cancel'), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="If the manufacturing order is unlocked you can add to the initial demand."/>
+ <button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', '=', 'cancel'), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to correct what has been consumed or produced."/>
<button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="mrp.group_mrp_manager" type="object"/>
</header>
<sheet>
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index 60eaa5fc2bb..aebcff24918 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -132,7 +132,7 @@ class PadCommon(models.AbstractModel):
# Update the `pad_content_field` if the pad is modified
for k, v in list(vals.items()):
- field = self._fields[k]
+ field = self._fields.get(k)
if hasattr(field, 'pad_content_field'):
vals[field.pad_content_field] = self.pad_get_content(v)
diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py
index f06b72461f9..46464b7d60c 100644
--- a/addons/payment_sips/models/payment.py
+++ b/addons/payment_sips/models/payment.py
@@ -76,7 +76,7 @@ class AcquirerSips(models.Model):
if self.environment == 'prod':
# For production environment, key version 2 is required
merchant_id = getattr(self, 'sips_merchant_id')
- key_version = '2'
+ key_version = self.env['ir.config_parameter'].sudo().get_param('sips.key_version', '2')
else:
# Test key provided by Atos Wordline works only with version 1
merchant_id = '002001000000001'
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ba856c4cfab..080d7277657 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -1018,7 +1018,10 @@ class ReportSaleDetails(models.AbstractModel):
for tax in line_taxes['taxes']:
taxes.setdefault(tax['id'], {'name': tax['name'], 'tax_amount':0.0, 'base_amount':0.0})
taxes[tax['id']]['tax_amount'] += tax['amount']
- taxes[tax['id']]['base_amount'] += line.price_subtotal
+ taxes[tax['id']]['base_amount'] += tax['base']
+ else:
+ taxes.setdefault(0, {'name': _('No Taxes'), 'tax_amount':0.0, 'base_amount':0.0})
+ taxes[0]['base_amount'] += line.price_subtotal_incl
st_line_ids = self.env["account.bank.statement.line"].search([('pos_statement_id', 'in', orders.ids)]).ids
if st_line_ids:
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 14b9285f38d..99fdc1ec7db 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -331,7 +331,7 @@ exports.PosModel = Backbone.Model.extend({
ids: function(self){ return [self.config.currency_id[0]]; },
loaded: function(self, currencies){
self.currency = currencies[0];
- if (self.currency.rounding > 0) {
+ if (self.currency.rounding > 0 && self.currency.rounding < 1) {
self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10));
} else {
self.currency.decimals = 0;
@@ -528,7 +528,7 @@ exports.PosModel = Backbone.Model.extend({
var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields;
var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain;
- var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context;
+ var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {};
var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids;
var order = typeof model.order === 'function' ? model.order(self,tmp): model.order;
progress += progress_step;
@@ -536,7 +536,7 @@ exports.PosModel = Backbone.Model.extend({
if( model.model ){
var params = {
model: model.model,
- context: context,
+ context: _.extend(context, session.user_context || {}),
};
if (model.ids) {
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 02d02844a82..ef23444c32c 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -94,13 +94,11 @@ class AccountInvoice(models.Model):
self.purchase_id = False
return {}
- @api.onchange('currency_id', 'date_invoice')
+ @api.onchange('currency_id')
def _onchange_currency_id(self):
if self.currency_id:
for line in self.invoice_line_ids.filtered(lambda r: r.purchase_line_id):
- if line.purchase_id.currency_id != self.currency_id:
- line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice)\
- .compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
+ line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice).compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
@api.onchange('invoice_line_ids')
def _onchange_origin(self):
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index bcf7799f161..50cb684699f 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -128,6 +128,8 @@ class ResourceCalendar(models.Model):
)
def _interval_and(self, interval, interval_dst):
+ if interval.start_datetime > interval_dst.end_datetime or interval.end_datetime < interval_dst.start_datetime:
+ return None
return self._interval_obj(
interval.start_datetime > interval_dst.start_datetime and interval.start_datetime or interval_dst.start_datetime,
interval.end_datetime < interval_dst.end_datetime and interval.end_datetime or interval_dst.end_datetime,
@@ -392,10 +394,10 @@ class ResourceCalendar(models.Model):
start_datetime=datetime.datetime.combine(day_date, start_time),
end_datetime=datetime.datetime.combine(day_date, end_time))
- final_intervals = [
- self._interval_and(leave_interval, work_interval)
- for leave_interval in leaves_intervals
- for work_interval in working_intervals]
+ final_intervals = [i for i in
+ [self._interval_and(leave_interval, work_interval)
+ for leave_interval in leaves_intervals
+ for work_interval in working_intervals] if i]
# adapt tz
return [self._interval_new(
diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py
index 617d6f9b79e..f31a89399dd 100644
--- a/addons/resource/tests/test_resource.py
+++ b/addons/resource/tests/test_resource.py
@@ -55,6 +55,12 @@ class TestIntervals(TestResourceCommon):
# Third interval: 04, 17-21, 18-19 being inside 17-21
self.assertEqual(cleaned_intervals[2][:2], (Datetime.from_string('2013-02-04 17:00:00'), Datetime.from_string('2013-02-04 21:00:00')))
+ def test_interval_and(self):
+ self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[0], self.intervals[1]),
+ self.calendar._interval_new(Datetime.from_string('2013-02-04 09:00:00'), Datetime.from_string('2013-02-04 11:00:00')))
+ self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[2], self.intervals[3]),
+ None)
+
def test_interval_remove(self):
working_interval = self.calendar._interval_new(Datetime.from_string('2013-02-04 08:00:00'), Datetime.from_string('2013-02-04 18:00:00'))
result = self.env['resource.calendar']._interval_remove_leaves(working_interval, self.intervals)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 9f07d83cbe5..1c460150555 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -830,8 +830,9 @@ class SaleOrderLine(models.Model):
# Prevent writing on a locked SO.
protected_fields = self._get_protected_fields()
if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields):
+ protected_fields_modified = list(set(protected_fields) & set(values.keys()))
fields = self.env['ir.model.fields'].search([
- ('name', 'in', protected_fields), ('model', '=', self._name)
+ ('name', 'in', protected_fields_modified), ('model', '=', self._name)
])
raise UserError(
_('It is forbidden to modify the following fields in a locked order:\n%s')
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index 844933d092a..139f542e597 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -35,7 +35,7 @@ class SendSMS(models.TransientModel):
def _sms_sanitization(self, partner, field_name):
number = partner[field_name]
- if _sms_phonenumbers_lib_imported:
+ if number and _sms_phonenumbers_lib_imported:
country = self._phone_get_country(partner)
country_code = country.code if country else None
try:
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 126da0a781d..167f1089c8f 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -246,7 +246,7 @@
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
</group>
<group>
- <field name="scheduled_date" attrs="{'readonly': [('id', '=', False)]}"/>
+ <field name="scheduled_date" attrs="{'readonly': [('id', '=', False)], 'required': [('id', '!=', False)]}"/>
<field name="origin" placeholder="e.g. PO0032"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<div groups="stock.group_tracking_owner" colspan="2" col="2">
diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml
index e169a2b7a8d..bd9f7f523f8 100644
--- a/addons/stock_account/views/stock_account_views.xml
+++ b/addons/stock_account/views/stock_account_views.xml
@@ -43,9 +43,9 @@
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="model">stock.return.picking</field>
<field name="arch" type="xml">
- <field name="uom_id" position="after">
+ <xpath expr="//field[@name='product_return_moves']/tree" position="inside">
<field name="to_refund"/>
- </field>
+ </xpath>
</field>
</record>
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 94facd80ceb..276c9e12544 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1082,6 +1082,7 @@ var UrlWidget = InputField.extend({
_renderReadonly: function () {
this.$el.text(this.attrs.text || this.value)
.addClass('o_form_uri o_text_overflow')
+ .attr('target', '_blank')
.attr('href', this.value);
}
});
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index bdafcee04da..7006ef03471 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1104,6 +1104,7 @@ var FieldX2Many = AbstractField.extend({
* @param {OdooEvent} event
*/
_onResequence: function (event) {
+ event.stopPropagation();
var self = this;
this.trigger_up('freeze_order', {id: this.value.id});
var rowIDs = event.data.rowIDs.slice();
diff --git a/addons/web/static/src/js/fields/special_fields.js b/addons/web/static/src/js/fields/special_fields.js
index 19db97fe985..6bd5d03ac69 100644
--- a/addons/web/static/src/js/fields/special_fields.js
+++ b/addons/web/static/src/js/fields/special_fields.js
@@ -20,7 +20,8 @@ var FieldTimezoneMismatch = FieldSelection.extend({
* @override
*/
start: function () {
- this._datetime = setInterval(this._renderDateTimeTimezone.bind(this), 1000);
+ var interval = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? 60000 : 1000;
+ this._datetime = setInterval(this._renderDateTimeTimezone.bind(this), interval);
return this._super.apply(this, arguments);
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index d3553d8db12..e3ea8a82343 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -103,7 +103,7 @@ var KanbanColumn = Widget.extend({
// should find a way to use the touch events to make sortable work.
this.$el.sortable({
connectWith: '.o_kanban_group',
- containment: this.draggable ? '.o_kanban_view' : 'parent',
+ containment: this.draggable ? false : 'parent',
revert: 0,
delay: 0,
items: '> .o_kanban_record:not(.o_updating)',
diff --git a/addons/web/static/src/less/webclient.less b/addons/web/static/src/less/webclient.less
index dfc26b11f5a..215b86cef83 100644
--- a/addons/web/static/src/less/webclient.less
+++ b/addons/web/static/src/less/webclient.less
@@ -86,39 +86,37 @@ div.o_boolean_toggle {
width: 32px !important;
height: 18px;
- & > label {
- & > input {
+ > input {
display: none;
}
- & > .slider {
+ .slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #a0a0a0;
+ border-radius: 24px;
+
+ &:before {
position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: #a0a0a0;
- border-radius: 24px;
-
- &:before {
- position: absolute;
- content: "";
- height: 12px;
- width: 12px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- border-radius: 50%;
- }
+ content: "";
+ height: 12px;
+ width: 12px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ border-radius: 50%;
}
+ }
- & > input:checked + .slider {
- background-color: @odoo-brand-optional;
+ > input:checked + .slider {
+ background-color: @odoo-brand-optional;
- &:before {
- .translate(14px; 0px);
- }
+ &:before {
+ .translate(14px; 0px);
}
}
}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 4fd9d6ee84e..4f3f4b413df 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1110,7 +1110,7 @@ QUnit.module('basic_fields', {
QUnit.module('UrlWidget');
QUnit.test('url widget in form view', function (assert) {
- assert.expect(8);
+ assert.expect(9);
var form = createView({
View: FormView,
@@ -1130,6 +1130,8 @@ QUnit.module('basic_fields', {
"should have a anchor with correct classes");
assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').attr('href'), 'yop',
"should have proper href link");
+ assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').attr('target'), '_blank',
+ "should have target attribute set to _blank");
assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'yop',
"the value should be displayed properly");
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 8938755a8f3..0782d667690 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1321,6 +1321,93 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('X2Many sequence list in modal', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.sequence = {string: 'Sequence', type: 'integer'};
+ this.data.partner.records[0].sequence = 1;
+ this.data.partner.records[1].sequence = 2;
+ this.data.partner.onchanges = {
+ sequence: function (obj) {
+ if (obj.id === 2) {
+ obj.sequence = 1;
+ assert.step('onchange sequence');
+ }
+ },
+ };
+
+ this.data.product.fields.turtle_ids = {string: 'Turtles', type: 'one2many', relation: 'turtle'};
+ this.data.product.records[0].turtle_ids = [1];
+
+ this.data.turtle.fields.partner_types_ids = {string: "Partner", type: "one2many", relation: 'partner'};
+ this.data.turtle.fields.type_id = {string: "Partner Type", type: "many2one", relation: 'partner_type'};
+
+ this.data.partner_type.fields.partner_ids = {string: "Partner", type: "one2many", relation: 'partner'};
+ this.data.partner_type.records[0].partner_ids = [1,2];
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'product',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="turtle_ids" widget="one2many_list">' +
+ '<tree string="Turtles" editable="bottom">' +
+ '<field name="type_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,form': '<form><field name="partner_ids"/></form>',
+ 'partner,false,list': '<tree string="Vendors">' +
+ '<field name="display_name"/>' +
+ '<field name="sequence" widget="handle"/>' +
+ '</tree>',
+ },
+ res_id: 37,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/read') {
+ return $.when([{id: 37, name: 'xphone', display_name: 'leonardo', turtle_ids: [1]}]);
+ }
+ if (route === '/web/dataset/call_kw/turtle/read') {
+ return $.when([{id: 1, type_id: [12, 'gold']}]);
+ }
+ if (route === '/web/dataset/call_kw/partner_type/get_formview_id') {
+ return $.when(false)
+ }
+ if (route === '/web/dataset/call_kw/partner_type/read') {
+ return $.when([{id: 12, partner_ids: [1,2], display_name: 'gold'}])
+ }
+ if (route === '/web/dataset/call_kw/partner_type/write') {
+ assert.step('partner_type write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function(form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_cell').click();
+ form.$('.o_external_button').click();
+
+ var $modal = $('.modal-dialog');
+ assert.equal($modal.length, 1,
+ 'There should be 1 modal opened');
+
+ var $handles = $modal.find('.ui-sortable-handle');
+ assert.equal($handles.length, 2,
+ 'There should be 2 sequence handlers');
+
+ testUtils.dragAndDrop($handles.eq(1), $modal.find('tbody tr').first());
+
+ // Saving the modal and then the original model
+ $modal.find('.modal-footer .btn-primary').click();
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['onchange sequence', 'partner_type write']);
+
+ form.destroy();
+ });
+ });
+
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index cbdb6cbb88b..b84fcd21c91 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -409,7 +409,19 @@ var FieldTextHtml = AbstractField.extend({
var layoutInfo = self.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
+ var $ancestors = self.$iframe.filter(':not(:visible)').parentsUntil(':visible').addBack();
+ var ancestorsStyle = [];
+ // temporarily force displaying iframe (needed for firefox)
+ _.each($ancestors, function (el) {
+ var $el = $(el);
+ ancestorsStyle.unshift($el.attr('style') || null);
+ $el.css({display: 'initial', visibility: 'hidden', height: 1});
+ });
self.editor.snippetsMenu && self.editor.snippetsMenu.cleanForSave();
+ _.each($ancestors, function (el) {
+ var $el = $(el);
+ $el.attr('style', ancestorsStyle.pop());
+ });
self._setValue(self.$content.html());
});
},
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index d2bcd29e5ce..ee0ec7932c0 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -281,8 +281,10 @@ var RTEWidget = Widget.extend({
.each(function () {
var $node = $(this);
+ // fallback for firefox iframe display:none see https://github.com/odoo/odoo/pull/22610
+ var computedStyles = window.getComputedStyle(this) || window.parent.getComputedStyle(this);
// add class to display inline-block for empty t-field
- if (window.getComputedStyle(this).display === 'inline' && $node.data('oe-type') !== 'image') {
+ if (computedStyles.display === 'inline' && $node.data('oe-type') !== 'image') {
$node.addClass('o_is_inline_editable');
}
});
@@ -651,26 +653,16 @@ var RTEWidget = Widget.extend({
self.historyRecordUndo($target, 'activate', true);
});
- // To Fix Google Chrome Tripleclick Issue, which selects the ending
- // whitespace characters (so Tripleclicking then typing text will remove
- // the whole paragraph instead of its content).
- // http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
- if ($.browser.chrome === true && ev.originalEvent.detail === 3) {
- var currentSelection = range.create();
- if (currentSelection.sc.parentNode === currentSelection.ec) {
- _selectSC(currentSelection);
- } else if (currentSelection.eo === 0) {
- var $hasNext = $(currentSelection.sc).parent();
- while (!$hasNext.next().length && !$hasNext.is('body')) {
- $hasNext = $hasNext.parent();
- }
- if ($hasNext.next()[0] === currentSelection.ec) {
- _selectSC(currentSelection);
- }
- }
- }
- function _selectSC(selection) {
- range.create(selection.sc, selection.so, selection.sc, selection.sc.length).select();
+ // Browsers select different content from one to another after a
+ // triple click (especially: if triple-clicking on a paragraph on
+ // Chrome, blank characters of the element following the paragraph are
+ // selected too)
+ //
+ // The triple click behavior is reimplemented for all browsers here
+ if (ev.originalEvent.detail === 3) {
+ // Select the whole content inside the deepest DOM element that was
+ // triple-clicked
+ range.create(ev.target, 0, ev.target, ev.target.childNodes.length).select();
}
},
});
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index fb1ce3e12b4..2ac9df2d693 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -417,6 +417,9 @@ dom.isImg = function (node) {
};
var fn_is_forbidden_node = dom.isForbiddenNode || function () {};
dom.isForbiddenNode = function (node) {
+ if (node.tagName === "BR") {
+ return false;
+ }
return fn_is_forbidden_node(node) || $(node).is(".media_iframe_video");
};
var fn_is_img_font = dom.isImgFont || function () {};
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index 8ce2ed692da..59609eab8a5 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -694,10 +694,16 @@ dom.isRemovableEmptyNode = function (node) {
dom.isForbiddenNode = function (node) {
return node.tagName === "BR" || $(node).is(".fa, img");
};
-dom.listBetween = function (sc, ec) {
+/**
+ * @todo 'so' and 'eo' were added as a bugfix and are not given everytime. They
+ * however should be as the function may be wrong without them (for example,
+ * when asking the list between an element and its parent, as there is no path
+ * from the beginning of the former to the beginning of the later).
+ */
+dom.listBetween = function (sc, ec, so, eo) {
var nodes = [];
var ancestor = dom.commonAncestor(sc, ec);
- dom.walkPoint({'node': sc, 'offset': 0}, {'node': ec, 'offset': 0}, function (point) {
+ dom.walkPoint({'node': sc, 'offset': so || 0}, {'node': ec, 'offset': eo || 0}, function (point) {
if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
nodes.push(point.node);
}
@@ -1869,7 +1875,7 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
}
// fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
- var nodes = dom.listBetween(r.sc, r.ec);
+ var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
for (var i=0; i<nodes.length; i++) {
if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
var ancestor = dom.ancestor(nodes[i], isFormatNode);
@@ -1984,6 +1990,14 @@ eventHandler.modules.editor.currentStyle = function (target) {
if (r)
styleInfo.image = r.isOnImg();
}
+ // Fix when the target is a link: the text-align buttons state should
+ // indicate the alignment of the link in the parent, not the text inside
+ // the link (which is not possible to customize with summernote). Summernote fixed
+ // this in their newest version... by just not showing the active button
+ // for alignments.
+ if (styleInfo.anchor) {
+ styleInfo['text-align'] = $(styleInfo.anchor).parent().css('text-align');
+ }
return styleInfo;
};
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js
index 80bb28a4c01..6c28c1e2332 100644
--- a/addons/web_editor/static/src/js/widgets/ace.js
+++ b/addons/web_editor/static/src/js/widgets/ace.js
@@ -742,7 +742,7 @@ var ViewEditor = Widget.extend({
var text = data.text || '';
if (!isSelected) {
- text = Array($elem.data('level')).join('-') + ' ' + text;
+ text = Array($elem.data('level') + 1).join('-') + ' ' + text;
}
var $div = $('<div/>', {
text: text,
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
index 3f46edde64c..2ea6078c44b 100644
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ b/addons/web_editor/static/tests/web_editor_tests.js
@@ -136,12 +136,12 @@ QUnit.test('field html_frame widget', function (assert) {
model: 'mass.mailing',
data: this.data,
arch: '<form string="Partners">' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
+ '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
'</form>',
res_id: 1,
session: {user_context: {lang: "en_us"}},
mockRPC: function (route) {
- if (_.str.startsWith(route, '/test')) {
+ if (_.str.startsWith(route, '/logo')) {
// those tests will be executed twice, once in readonly and once in edit
assert.ok(route.search('model=mass.mailing') > 0,
"the route should specify the correct model");
@@ -200,7 +200,7 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
+ '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
@@ -208,7 +208,7 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
if (args.method) {
assert.step(args.method);
}
- if (_.str.startsWith(route, '/test')) {
+ if (_.str.startsWith(route, '/logo')) {
// manually call the callback to simulate that the iframe has
// been loaded (note: just the content, not the editor)
window.odoo[$.deparam(route).callback + '_content'].call();
@@ -237,7 +237,7 @@ QUnit.test('html_frame does not crash when saving in edit mode (editor not loade
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
+ '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
@@ -245,7 +245,7 @@ QUnit.test('html_frame does not crash when saving in edit mode (editor not loade
if (args.method) {
assert.step(args.method);
}
- if (_.str.startsWith(route, '/test')) {
+ if (_.str.startsWith(route, '/logo')) {
// manually call the callback to simulate that the iframe has
// been partially loaded (just the content, not the editor)
window.odoo[$.deparam(route).callback + '_content']();
@@ -279,7 +279,7 @@ QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', f
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
+ '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
@@ -290,7 +290,7 @@ QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', f
writeDeferred.resolve();
}
}
- if (_.str.startsWith(route, '/test')) {
+ if (_.str.startsWith(route, '/logo')) {
// manually call the callback to simulate that the iframe has
// been fully loaded (content + editor)
var callback = $.deparam(route).callback;
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 84813d7eca5..c87d7226893 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -124,12 +124,6 @@ class Http(models.AbstractModel):
return False
@classmethod
- def _serve_404(cls):
- req_page = request.httprequest.path
- page404 = 'website.%s' % (request.website.is_publisher() and 'page_404' or '404')
- return request.render(page404, {'path': req_page[1:]})
-
- @classmethod
def _serve_redirect(cls):
req_page = request.httprequest.path
domain = [
@@ -153,7 +147,7 @@ class Http(models.AbstractModel):
if redirect:
return request.redirect(redirect.url_to, code=redirect.type)
- return cls._serve_404()
+ return False
@classmethod
def _handle_exception(cls, exception):
@@ -212,11 +206,16 @@ class Http(models.AbstractModel):
status_code=code,
)
+ view_id = code
+ if request.website.is_publisher() and isinstance(exception, werkzeug.exceptions.NotFound):
+ view_id = 'page_404'
+ values['path'] = request.httprequest.path[1:]
+
if not request.uid:
cls._auth_method_public()
try:
- html = request.env['ir.ui.view'].render_template('website.%s' % code, values)
+ html = request.env['ir.ui.view'].render_template('website.%s' % view_id, values)
except Exception:
html = request.env['ir.ui.view'].render_template('website.http_error', values)
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index 64928682533..87e2ccd5999 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -209,7 +209,7 @@ class BlogPost(models.Model):
def write(self, vals):
result = True
for post in self:
- copy_vals = vals
+ copy_vals = dict(vals)
if 'website_published' in vals and 'published_date' not in vals and (post.published_date or '') <= fields.Datetime.now():
copy_vals['published_date'] = vals['website_published'] and fields.Datetime.now() or False
result &= super(BlogPost, self).write(copy_vals)
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 6e9781da5f3..f0393cfb834 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -417,8 +417,8 @@
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
<div class="carousel-outer">
<div class="carousel-inner">
- <div class="item active" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
- <div t-if="variant_img" class="item" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
+ <div t-if="variant_img" class="item active" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
+ <div t-attf-class="item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
<div class="item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
</t>
@@ -437,11 +437,11 @@
</div>
<ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
- <li data-target="#o-carousel-product" data-slide-to="0" class="active">
- <img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
+ <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
+ <img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
</li>
- <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="1" class="">
- <img class="img img-responsive" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
+ <li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else 0" t-att-class="'' if variant_img else 'active'">
+ <img class="img img-responsive" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
</li>
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
@@ -518,7 +518,7 @@
<div t-field="alt_product.image_small" t-options="{'widget': 'image', 'class': 'img-rounded shadow o_alternative_product' }" />
<h5>
<a t-attf-href="/shop/product/#{ slug(alt_product) }" style="display: block">
- <span t-att-title="alt_product.name" t-field="alt_product.name" style="display: block;" />
+ <span t-att-title="alt_product.name" t-field="alt_product.name" class="o_text_overflow" style="display: block;" />
</a>
</h5>
</div>
diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md
index f250546a6ef..0d8c8b623e9 100644
--- a/doc/cla/corporate/vauxoo.md
+++ b/doc/cla/corporate/vauxoo.md
@@ -42,3 +42,4 @@ Edgar Rivero edgar@vauxoo.com https://github.com/egrivero
Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
+Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
diff --git a/doc/cla/individual/kaberer.md b/doc/cla/individual/kaberer.md
new file mode 100644
index 00000000000..e17852fe338
--- /dev/null
+++ b/doc/cla/individual/kaberer.md
@@ -0,0 +1,11 @@
+Switzerland, 2018-01-30
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Kumar Aberer office@aberer-solutions.ch https://github.com/kaberer
diff --git a/doc/cla/individual/moutio.md b/doc/cla/individual/moutio.md
new file mode 100644
index 00000000000..4a1b5b4b955
--- /dev/null
+++ b/doc/cla/individual/moutio.md
@@ -0,0 +1,9 @@
+Spain, 2018-01-29
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Nacho Muñoz nacmuro@gmail.com https://github.com/moutio
diff --git a/doc/cla/individual/phucngta.md b/doc/cla/individual/phucngta.md
new file mode 100644
index 00000000000..c53fe2b7136
--- /dev/null
+++ b/doc/cla/individual/phucngta.md
@@ -0,0 +1,11 @@
+Vietnam, 2018.01.17
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Nguyen Phuc <phucngta@gmail.com> https://github.com/phucngta \ No newline at end of file
diff --git a/doc/reference/module.rst b/doc/reference/module.rst
index aecb8de8a8f..9ec0ef39c93 100644
--- a/doc/reference/module.rst
+++ b/doc/reference/module.rst
@@ -1,5 +1,3 @@
-:banner: banners/module.jpg
-
=======
Modules
=======
@@ -84,6 +82,49 @@ Available manifest fields are:
to ``auto_install``. When both ``sale`` and ``crm`` are installed, it
automatically adds CRM campaigns tracking to sale orders without either
``sale`` or ``crm`` being aware of one another
+``external_dependencies`` (``dict(key=list(str))``)
+ A dictionary containing python and/or binary dependencies.
+
+ For python dependencies, the ``python`` key must be defined for this
+ dictionary and a list of python modules to be imported should be assigned
+ to it.
+
+ For binary dependencies, the ``bin`` key must be defined for this
+ dictionary and a list of binary executable names should be assigned to it.
+
+ The module won't be installed if either the python module is not installed
+ in the host machine or the binary executable is not found within the
+ host machine's PATH environment variable.
+``application`` (``bool``, default: ``False``)
+ Whether the module should be considered as a fully-fledged application
+ (``True``) or is just a technical module (``False``) that provides some
+ extra functionality to an existing application module.
+``css`` (``list(str)``)
+ Specify css files with custom rules to be imported, these files should be
+ located in ``static/src/css`` inside the module.
+``images`` (``list(str)``)
+ Specify image files to be used by the module.
+``installable`` (``bool`` default: ``False``)
+ Whether a user should be able to install the module from the Web UI or not.
+``maintainer`` (``str``)
+ Person or entity in charge of the maintenance of this module, by default
+ it is assumed that the author is the maintainer.
+``{pre_init, post_init, uninstall}_hook`` (``str``)
+ Hooks for module installation/uninstallation, their value should be a
+ string representing the name of a function defined inside the module's
+ ``__init__.py``.
+
+ ``pre_init_hook`` takes a cursor as its only argument, this function is
+ executed prior to the module's installation.
+
+ ``post_init_hook`` takes a cursor and a registry as its arguments, this
+ function is executed right after the module's installation.
+
+ ``uninstall_hook`` takes a cursor and a registry as its arguments, this
+ function is executed after the module's uninstallation.
+
+ These hooks should only be used when setup/cleanup required for this module
+ is either extremely difficult or impossible through the api.
.. _semantic versioning: http://semver.org
.. _existing categories:
diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst
index 0d7303a795a..b0a5cf3224e 100644
--- a/doc/setup/deploy.rst
+++ b/doc/setup/deploy.rst
@@ -44,7 +44,7 @@ correctly.
Configuration samples
---------------------
-* show only databases with names beginning with 'mycompany'
+* Show only databases with names beginning with 'mycompany'
in ``/etc/odoo.conf`` set:
@@ -87,18 +87,18 @@ machine, and is the default when no host is provided, but if you want Odoo and
PostgreSQL to execute on different machines [#different-machines]_ it will
need to `listen to network interfaces`_ [#remote-socket]_, either:
-* only accept loopback connections and `use an SSH tunnel`_ between the
+* Only accept loopback connections and `use an SSH tunnel`_ between the
machine on which Odoo runs and the one on which PostgreSQL runs, then
configure Odoo to connect to its end of the tunnel
-* accept connections to the machine on which Odoo is installed, possibly
+* Accept connections to the machine on which Odoo is installed, possibly
over ssl (see `PostgreSQL connection settings`_ for details), then configure
Odoo to connect over the network
Configuration sample
--------------------
-* allow tcp connection on localhost
-* allow tcp connection from 192.168.1.x network
+* Allow tcp connection on localhost
+* Allow tcp connection from 192.168.1.x network
in ``/etc/postgresql/9.5/main/pg_hba.conf`` set:
@@ -134,11 +134,11 @@ create a new user (``odoo``) and set it as the database user.
simply checked before performing database alterations. It should be set to
a randomly generated value to ensure third parties can not use this
interface.
-* all database operations use the :ref:`database options
+* All database operations use the :ref:`database options
<reference/cmdline/server/database>`, including the database management
screen. For the database management screen to work requires that the PostgreSQL user
have ``createdb`` right.
-* users can always drop databases they own. For the database management screen
+* Users can always drop databases they own. For the database management screen
to be completely non-functional, the PostgreSQL user needs to be created with
``no-createdb`` and the database must be owned by a different PostgreSQL user.
@@ -248,10 +248,10 @@ authentication information in cleartext. This means a secure deployment of
Odoo must use HTTPS\ [#switching]_. SSL termination can be implemented via
just about any SSL termination proxy, but requires the following setup:
-* enable Odoo's :option:`proxy mode <odoo-bin --proxy-mode>`. This should only be enabled when Odoo is behind a reverse proxy
-* set up the SSL termination proxy (`Nginx termination example`_)
-* set up the proxying itself (`Nginx proxying example`_)
-* your SSL termination proxy should also automatically redirect non-secure
+* Enable Odoo's :option:`proxy mode <odoo-bin --proxy-mode>`. This should only be enabled when Odoo is behind a reverse proxy
+* Set up the SSL termination proxy (`Nginx termination example`_)
+* Set up the proxying itself (`Nginx proxying example`_)
+* Your SSL termination proxy should also automatically redirect non-secure
connections to the secure port
.. warning::
@@ -263,8 +263,8 @@ just about any SSL termination proxy, but requires the following setup:
Configuration sample
--------------------
-* redirect http requests to https
-* proxy requests to odoo
+* Redirect http requests to https
+* Proxy requests to odoo
in ``/etc/odoo.conf`` set:
@@ -349,10 +349,10 @@ Cron Workers
To run cron jobs for an Odoo deployment as a WSGI application requires
-* a classical Odoo (run via ``odoo-bin``)
-* connected to the database in which cron jobs have to be run (via
+* A classical Odoo (run via ``odoo-bin``)
+* Connected to the database in which cron jobs have to be run (via
:option:`odoo-bin -d`)
-* which should not be exposed to the network. To ensure cron runners are not
+* Which should not be exposed to the network. To ensure cron runners are not
network-accessible, it is possible to disable the built-in HTTP server
entirely with :option:`odoo-bin --no-http` or setting ``http_enable = False``
in the configuration file
@@ -372,11 +372,11 @@ notifications.
The solutions to support livechat/motifications in a WSGI application are:
-* deploy a threaded version of Odoo (instread of a process-based preforking
+* Deploy a threaded version of Odoo (instread of a process-based preforking
one) and redirect only requests to URLs starting with ``/longpolling/`` to
that Odoo, this is the simplest and the longpolling URL can double up as
the cron instance.
-* deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
+* Deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
with ``/longpolling/`` to
:option:`the longpolling port <odoo-bin --longpolling-port>`.
diff --git a/doc/webservices/localization.rst b/doc/webservices/localization.rst
index 17559b2a01a..39e1c98181c 100644
--- a/doc/webservices/localization.rst
+++ b/doc/webservices/localization.rst
@@ -1,12 +1,13 @@
:banner: banners/localization.jpg
-==========================
-Making my Own Localization
-==========================
+=======================
+Creating a Localization
+=======================
.. warning::
- This tutorial requires knowledges about how to build a module in Odoo.
+ This tutorial requires knowledges about how to build a module in Odoo (see
+ :doc:`../../howtos/backend`).
Building a localization module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index d857c14593b..646615cb593 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -8,6 +8,7 @@ from odoo.tools.safe_eval import safe_eval, test_python_expr
from odoo.tools import pycompat
from odoo.http import request
+import base64
from collections import defaultdict
import datetime
import dateutil
@@ -76,6 +77,8 @@ class IrActions(models.Model):
'datetime': datetime,
'dateutil': dateutil,
'timezone': timezone,
+ 'b64encode': base64.b64encode,
+ 'b64decode': base64.b64decode,
}
@api.model
@@ -253,7 +256,7 @@ class IrActionsActWindowView(models.Model):
_name = 'ir.actions.act_window.view'
_table = 'ir_act_window_view'
_rec_name = 'view_id'
- _order = 'sequence'
+ _order = 'sequence,id'
sequence = fields.Integer()
view_id = fields.Many2one('ir.ui.view', string='View')
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 2915d9a0086..53a3438b21a 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -15,7 +15,6 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.osv import expression
from odoo.service.db import check_super
from odoo.tools import partition, pycompat
-from odoo.http import root
_logger = logging.getLogger(__name__)
@@ -373,8 +372,6 @@ class Users(models.Model):
if any(key.startswith('context_') or key in ('lang', 'tz') for key in values):
self.context_get.clear_cache(self)
if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
- # force deletion of all sessions for these users
- root.session_store.delete_sessions_for_uids(self.ids)
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
@@ -388,10 +385,7 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
- res = super(Users, self).unlink()
- # force deletion of all sessions for these users
- root.session_store.delete_sessions_for_uids(self.ids)
- return res
+ return super(Users, self).unlink()
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
diff --git a/odoo/addons/base/tests/test_float.py b/odoo/addons/base/tests/test_float.py
index 1c2fcc3e91f..6e42f8adf1d 100644
--- a/odoo/addons/base/tests/test_float.py
+++ b/odoo/addons/base/tests/test_float.py
@@ -92,6 +92,18 @@ class TestFloatPrecision(TransactionCase):
try_round(1.8, '2', 0, method='UP')
try_round(-1.8, '-2', 0, method='UP')
+ # Try some rounding value with rounding method DOWN instead of HALF-UP
+ # We use 2.425 because when normalizing 2.425 with precision_digits=3 it gives
+ # us 2424.9999999999995 as value, and if not handle correctly the rounding DOWN
+ # value will be incorrect (should be 2.425 and not 2.424)
+ try_round(2.425, '2.425', method='DOWN')
+ try_round(2.4249, '2.424', method='DOWN')
+ try_round(-2.425, '-2.425', method='DOWN')
+ try_round(-2.4249, '-2.424', method='DOWN')
+ try_round(-2.500, '-2.500', method='DOWN')
+ try_round(1.8, '1', 0, method='DOWN')
+ try_round(-1.8, '-1', 0, method='DOWN')
+
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
@@ -135,8 +147,8 @@ class TestFloatPrecision(TransactionCase):
try_compare(-657.4444, -657.445, 1)
# Rounding to unusual rounding units (e.g. coin values)
- def try_round(amount, expected, precision_rounding=None):
- value = float_round(amount, precision_rounding=precision_rounding)
+ def try_round(amount, expected, precision_rounding=None, method='HALF-UP'):
+ value = float_round(amount, precision_rounding=precision_rounding, rounding_method=method)
result = float_repr(value, precision_digits=2)
self.assertEqual(result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
@@ -145,6 +157,8 @@ class TestFloatPrecision(TransactionCase):
try_round(457.3, '455.00', precision_rounding=5)
try_round(457.5, '460.00', precision_rounding=5)
try_round(457.1, '456.00', precision_rounding=3)
+ try_round(2.5, '2.50', precision_rounding=0.05, method='DOWN')
+ try_round(-2.5, '-2.50', precision_rounding=0.05, method='DOWN')
def test_rounding_04(self):
""" check that proper rounding is performed for float persistence """
diff --git a/odoo/http.py b/odoo/http.py
index 63d7ece05d5..4cfe76a76a7 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -635,8 +635,9 @@ class JsonRequest(WebRequest):
body = json.dumps(response, default=ustr)
return Response(
- body, headers=[('Content-Type', mime),
- ('Content-Length', len(body))])
+ body, status=error and error.pop('http_status', 200) or 200,
+ headers=[('Content-Type', mime), ('Content-Length', len(body))]
+ )
def _handle_exception(self, exception):
"""Called within an except block to allow converting exceptions
@@ -645,13 +646,18 @@ class JsonRequest(WebRequest):
try:
return super(JsonRequest, self)._handle_exception(exception)
except Exception:
- if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException, odoo.exceptions.except_orm)):
+ if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException,
+ odoo.exceptions.except_orm, werkzeug.exceptions.NotFound)):
_logger.exception("Exception during JSON request handling.")
error = {
'code': 200,
'message': "Odoo Server Error",
'data': serialize_exception(exception)
}
+ if isinstance(exception, werkzeug.exceptions.NotFound):
+ error['http_status'] = 404
+ error['code'] = 404
+ error['message'] = "404: Not Found"
if isinstance(exception, AuthenticationError):
error['code'] = 100
error['message'] = "Odoo Session Invalid"
@@ -1029,6 +1035,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
+ self.password = password
request.uid = uid
request.disable_db = False
@@ -1043,6 +1050,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
+ security.check(self.db, self.uid, self.password)
def logout(self, keep_db=False):
for k in list(self):
@@ -1055,6 +1063,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
+ self.setdefault("password", None)
self.setdefault("context", {})
def get_context(self):
@@ -1268,17 +1277,6 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers)
return self.app(environ, start_wrapped)
-class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore):
- def delete_sessions_for_uids(self, uids):
- # pretty expensive on large session stores, especially non-local!
- uids = set(uids)
- _logger.info('Deleting all HTTP sessions for UIDs %s', uids)
- for sid in self.list():
- s = self.get(sid)
- if s.uid and s.uid in uids:
- _logger.debug('Deleting session %s', sid)
- self.delete(s)
-
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
@@ -1290,7 +1288,7 @@ class Root(object):
# Setup http sessions
path = odoo.tools.config.session_dir
_logger.debug('HTTP sessions stored in: %s', path)
- return OdooSessionStore(path, session_class=OpenERPSession)
+ return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
@lazy_property
def nodb_routing_map(self):
diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py
index 33044d21f93..041a87cabbe 100644
--- a/odoo/tools/float_utils.py
+++ b/odoo/tools/float_utils.py
@@ -61,9 +61,6 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# In order to easily support rounding to arbitrary 'steps' (e.g. coin values),
# we normalize the value before rounding it as an integer, and de-normalize
# after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5
-
- # TIE-BREAKING: HALF-UP (for normal rounding)
- # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
# Due to IEE754 float/double representation limits, the approximation of the
# real value may be slightly below the tie limit, resulting in an error of
# 1 unit in the last place (ulp) after rounding.
@@ -74,25 +71,31 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# Credit: discussion with OpenERP community members on bug 882036
normalized_value = value / rounding_factor # normalize
+ sign = math.copysign(1.0, normalized_value)
epsilon_magnitude = math.log(abs(normalized_value), 2)
epsilon = 2**(epsilon_magnitude-53)
- if rounding_method == 'HALF-UP':
- normalized_value += math.copysign(epsilon, normalized_value)
- rounded_value = round(normalized_value) # round to integer
- # TIE-BREAKING: UP (for ceiling[resp. flooring] operations)
- # When rounding the value up[resp. down], we instead subtract the epsilon value
+ # TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
+ # When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value
# as the the approximation of the real value may be slightly *above* the
# tie limit, this would result in incorrectly rounding up[resp. down] to the next number
# The math.ceil[resp. math.floor] operation is applied on the absolute value in order to
# round "away from zero" and not "towards infinity", then the sign is
# restored.
- else:
- func = math.floor if rounding_method == 'DOWN' else math.ceil
- sign = math.copysign(1.0, normalized_value)
+ if rounding_method == 'UP':
normalized_value -= sign*epsilon
- rounded_value = func(abs(normalized_value)) * sign
+ rounded_value = math.ceil(abs(normalized_value)) * sign
+
+ elif rounding_method == 'DOWN':
+ normalized_value += sign*epsilon
+ rounded_value = math.floor(abs(normalized_value)) * sign
+
+ # TIE-BREAKING: HALF-UP (for normal rounding)
+ # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
+ else:
+ normalized_value += math.copysign(epsilon, normalized_value)
+ rounded_value = round(normalized_value) # round to integer
result = rounded_value * rounding_factor # de-normalize
return result