diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-02-01 14:16:50 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-02-01 14:16:50 +0100 |
| commit | 7d465aa56cf421d009b25a41fc226f82de7c2192 (patch) | |
| tree | 6838248a00d58299b31fee243109ceada8ec0b77 | |
| parent | 5b230c34cb960f3f729d6612735dd9c3c896b785 (diff) | |
| parent | 4c9bf7faae4e5e53955fde1e69ef82e72dd51ccb (diff) | |
[MERGE] forward port branch 10.0 up to 4c9bf7faae4
| -rw-r--r-- | addons/barcodes/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/barcodes/models/ir_http.py | 14 | ||||
| -rw-r--r-- | addons/barcodes/static/src/js/barcode_events.js | 3 | ||||
| -rw-r--r-- | addons/l10n_in_hr_payroll/views/report_payslip_details_template.xml | 6 | ||||
| -rw-r--r-- | addons/mail/static/src/js/client_action.js | 6 | ||||
| -rw-r--r-- | addons/mrp/models/stock_move.py | 2 | ||||
| -rw-r--r-- | addons/stock_account/report/stock_history.py | 40 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/backend.js | 12 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/summernote.js | 8 | ||||
| -rw-r--r-- | addons/website_sale/views/templates.xml | 12 | ||||
| -rw-r--r-- | doc/cla/individual/moutio.md | 9 | ||||
| -rw-r--r-- | doc/reference/module.rst | 45 | ||||
| -rw-r--r-- | odoo/http.py | 12 |
13 files changed, 140 insertions, 30 deletions
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 @@ <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/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 @@ <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> @@ -431,11 +431,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)"> 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" |
