[MERGE] forward port branch saas-15 up to e0422bfd6c
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import barcodes
|
||||
from . import barcode_events_mixin
|
||||
from . import ir_http
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user