diff --git a/addons/barcodes/models/__init__.py b/addons/barcodes/models/__init__.py
index ce467f9134f..7d377d5a2b1 100644
--- a/addons/barcodes/models/__init__.py
+++ b/addons/barcodes/models/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import barcodes
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 0adff5f8e1b..bff05aa4636 100644
--- a/addons/barcodes/static/src/js/barcode_events.js
+++ b/addons/barcodes/static/src/js/barcode_events.js
@@ -2,6 +2,7 @@ odoo.define('barcodes.BarcodeEvents', function(require) {
"use strict";
var core = require('web.core');
+var session = require('web.session');
var mixins = core.mixins;
@@ -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/l10n_in_hr_payroll/views/report_payslip_details_template.xml b/addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml
index eacbaa1533b..a8165b4420a 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 @@
-
+
|
@@ -75,7 +76,8 @@
-
+
|
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index 3b166cb664c..bc608a87e32 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -128,10 +128,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var def = window.Notification && window.Notification.requestPermission();
if (def) {
def.then(function (value) {
- if (value === 'denied') {
- utils.send_notification(_t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.'));
- } else {
+ if (value === 'granted') {
utils.send_notification(_t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.'));
+ } else {
+ utils.send_notification(_t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.'));
}
});
}
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 89d0dfbc009..ceef7f37cfa 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -359,7 +359,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/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py
index 94f2c5f36a3..ee5a5554729 100644
--- a/addons/stock_account/report/stock_history.py
+++ b/addons/stock_account/report/stock_history.py
@@ -25,22 +25,35 @@ class StockHistory(models.Model):
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ # Step 1: retrieve the standard read_group output. In case of inventory valuation, this
+ # will be mostly used as a 'skeleton' since the inventory value needs to be computed based
+ # on the individual lines.
res = super(StockHistory, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
if 'inventory_value' in fields:
+ groupby_list = groupby if not lazy else groupby[:-1]
date = self._context.get('history_date', fieldsDatetime.now())
- stock_history = self.env['stock.history']
- group_lines = {}
- for line in res:
- domain = line.get('__domain', domain)
- group_lines.setdefault(str(domain), self.search(domain))
- stock_history |= group_lines[str(domain)]
- # get data of stock_history in one shot to speed things up (the view can be very slow)
+ # Step 2: retrieve the stock history lines. The result contains the 'expanded'
+ # version of the read_group. We build the query manually for performance reason
+ # (and avoid a costly 'WHERE id IN ...').
+ fields_2 = set(
+ ['id', 'product_id', 'price_unit_on_quant', 'company_id', 'quantity'] + groupby_list
+ )
+ tables, where_clause, where_clause_params = self._where_calc(domain).get_sql()
+ select = "SELECT %s FROM %s WHERE %s "
+ query = select % (','.join(fields_2), tables, where_clause)
+ self._cr.execute(query, where_clause_params)
+
+ # Step 3: match the lines retrieved at step 2 with the aggregated results of step 1.
+ # In other words, we link each item of the read_group result with the corresponding
+ # lines.
stock_history_data = {}
- if stock_history:
- self._cr.execute("""SELECT id, product_id, price_unit_on_quant, company_id, quantity
- FROM stock_history WHERE id in %s""", (tuple(stock_history.ids),))
- stock_history_data = {line['id']: line for line in self._cr.dictfetchall()}
+ stock_histories_by_group = {}
+ for line in self._cr.dictfetchall():
+ stock_history_data[line['id']] = line
+ key = tuple(line[g] if g in line else False for g in groupby_list)
+ stock_histories_by_group.setdefault(key, [])
+ stock_histories_by_group[key] += [line['id']]
histories_dict = {}
not_real_cost_method_products = self.env['product.product'].browse(
@@ -56,7 +69,10 @@ class StockHistory(models.Model):
for line in res:
inv_value = 0.0
- for stock_history in group_lines.get(str(line.get('__domain', domain))):
+ # Build the same keys than above, but need to take into account Many2one are tuples
+ key = tuple(line[g] if g in line else False for g in groupby_list)
+ key = tuple(k[0] if isinstance(k, tuple) else k for k in key)
+ for stock_history in self.env['stock.history'].browse(stock_histories_by_group[key]):
history_data = stock_history_data[stock_history.id]
product_id = history_data['product_id']
if self.env['product.product'].browse(product_id).cost_method == 'real':
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index f4f8d7144b6..23505c082fa 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -405,7 +405,19 @@ var FieldTextHtml = widget.extend({
var layoutInfo = this.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
+ var $ancestors = this.$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});
+ });
this.editor.buildingBlock.clean_for_save();
+ _.each($ancestors, function (el) {
+ var $el = $(el);
+ $el.attr('style', ancestorsStyle.pop());
+ });
this.internal_set_value( this.$content.html() );
}
},
diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js
index a3e3db42a71..8166d77201e 100644
--- a/addons/web_editor/static/src/js/summernote.js
+++ b/addons/web_editor/static/src/js/summernote.js
@@ -2018,6 +2018,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/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 0e14f1a341e..774b8cbe5e3 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -411,8 +411,8 @@
-
-
+
+
@@ -431,11 +431,11 @@
- -
-
+ -
+
- -
-
+ -
+
-
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/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/odoo/http.py b/odoo/http.py
index 56b1f8e6930..780521db857 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -629,8 +629,9 @@ class JsonRequest(WebRequest):
body = json.dumps(response)
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
@@ -639,13 +640,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"