[MERGE] forward port branch saas-15 up to e0422bfd6c

This commit is contained in:
Christophe Simonis
2018-02-01 16:40:13 +01:00
14 changed files with 163 additions and 31 deletions
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import barcodes
from . import barcode_events_mixin
from . import ir_http
+14
View File
@@ -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
@@ -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);
@@ -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>
+3 -3
View File
@@ -125,10 +125,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(self, _t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.'));
} else {
if (value === 'granted') {
utils.send_notification(self, _t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.'));
} else {
utils.send_notification(self, _t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.'));
}
});
}
+1 -1
View File
@@ -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']
+28 -12
View File
@@ -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':
@@ -180,7 +180,17 @@ var DomainTree = DomainNode.extend({
*/
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
this._initialize(Domain.prototype.stringToArray(domain));
try {
domain = Domain.prototype.stringToArray(domain);
} catch (err) {
// TODO: domain could contain `parent` for example, which is
// currently not handled by the DomainSelector
this._isValid = false;
this.children = [];
return;
}
this._initialize(domain);
},
/**
* @see DomainNode.start
@@ -565,6 +575,18 @@ var DomainSelector = DomainTree.extend({
this._redraw();
}
},
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (!self._isValid) {
var msg = _t("This domain is not supported.");
self.$el.html(msg);
}
});
},
});
/**
@@ -401,7 +401,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.buildingBlock.clean_for_save();
_.each($ancestors, function (el) {
var $el = $(el);
$el.attr('style', ancestorsStyle.pop());
});
self._setValue(self.$content.html());
});
},
@@ -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;
}
+6 -6
View File
@@ -413,8 +413,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>
@@ -433,11 +433,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)">
+9
View File
@@ -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
+43 -2
View File
@@ -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:
+9 -3
View File
@@ -637,8 +637,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
@@ -647,13 +648,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"