diff --git a/addons/stock/views/stock_production_lot_views.xml b/addons/stock/views/stock_production_lot_views.xml
index 31a2c532a13..cfa5d3929ce 100644
--- a/addons/stock/views/stock_production_lot_views.xml
+++ b/addons/stock/views/stock_production_lot_views.xml
@@ -15,7 +15,6 @@
-
diff --git a/addons/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py
index 3e67dd1db8d..ccf150954dc 100644
--- a/addons/stock_account/report/stock_history.py
+++ b/addons/stock_account/report/stock_history.py
@@ -79,7 +79,7 @@ class StockHistory(models.Model):
product_template_id,
SUM(quantity) as quantity,
date,
- SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
+ COALESCE(SUM(price_unit_on_quant * quantity) / NULLIF(SUM(quantity), 0), 0) as price_unit_on_quant,
source,
string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number
FROM
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index 99ce35e02a3..3d367b0364b 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -638,7 +638,7 @@ class SurveyQuestion(models.Model):
if self.comments_allowed:
comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
# Preventing answers with blank value
- if all([True if not answer.strip() else False for answer in answer_candidates.values()]):
+ if all([True if not answer.strip() else False for answer in answer_candidates.values()]) and answer_candidates:
errors.update({answer_tag: self.constr_error_msg})
# There is no answer neither comments (if comments count as answer)
if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js
index 6af97721926..516436af93c 100644
--- a/addons/web/static/src/js/framework/session.js
+++ b/addons/web/static/src/js/framework/session.js
@@ -86,8 +86,11 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
* Init a session, reloads from cookie, if it exists
*/
session_init: function () {
+ var def = this.session_reload();
+ if (this.is_frontend) return def;
+
var self = this;
- return this.session_reload().then(function() {
+ return def.then(function() {
var modules = self.module_list.join(',');
var deferred = self.load_qweb(modules);
if(self.session_is_valid()) {
diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js
index 46136e21565..06bca6cd382 100644
--- a/addons/web/static/src/js/views/form_relational_widgets.js
+++ b/addons/web/static/src/js/views/form_relational_widgets.js
@@ -26,7 +26,7 @@ var M2ODialog = Dialog.extend({
title: _.str.sprintf(_t("Create a %s"), parent.string),
size: 'medium',
buttons: [
- {text: _t('Create'), classes: 'btn-primary', click: function() {
+ {text: _t('Create'), classes: 'btn-primary', click: function(e) {
if (this.$("input").val() !== ''){
this.getParent()._quick_create(this.$("input").val());
this.close();
@@ -997,7 +997,9 @@ var X2ManyListView = ListView.extend({
*/
var X2ManyList = ListView.List.extend({
pad_table_to: function (count) {
- if (!this.view.is_action_enabled('create') || this.view.x2m.get('effective_readonly')) {
+ var ftype = this.view.x2m.field.type;
+ var is_readonly = this.view.x2m.get('effective_readonly');
+ if (is_readonly || (ftype === 'one2many' && !this.view.is_action_enabled('create'))) {
this._super(count);
return;
}
@@ -1304,7 +1306,7 @@ var Many2ManyListView = X2ManyListView.extend({
context: this.x2m.build_context(),
title: _t("Add: ") + this.x2m.string,
alternative_form_view: this.x2m.field.views ? this.x2m.field.views.form : undefined,
- no_create: this.x2m.options.no_create,
+ no_create: this.x2m.options.no_create || !this.is_action_enabled('create'),
on_selected: function(element_ids) {
return self.x2m.data_link_multi(element_ids).then(function() {
self.x2m.reload_current_view();
diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js
index 572fcbff95f..b02c91ed147 100644
--- a/addons/web/static/src/js/views/list_view.js
+++ b/addons/web/static/src/js/views/list_view.js
@@ -1508,7 +1508,7 @@ ListView.Groups = Class.extend({
// drag and drop enabled if list is not sorted (unless it is sorted by
// its sequence field (ASC)), and there is a visible column with
// @widget=handle or "sequence" column in the view.
- if ((dataset.sort && [seqname, seqname + 'ASC', ''].indexOf(dataset.sort()) === -1)
+ if ((dataset.sort && [seqname, seqname + ' ASC', ''].indexOf(dataset.sort()) === -1)
|| !_(this.columns).findWhere({'name': seqname})) {
return;
}
diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js
index 57c26311acc..95e8d13e5ff 100644
--- a/addons/web/static/src/js/views/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot_view.js
@@ -57,6 +57,7 @@ var PivotView = View.extend({
this.headers = {};
this.cells = {};
this.has_data = false;
+ this.$buttons = $();
this.last_header_selected = null;
this.sorted_column = {};
diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py
index 312dcca81f2..089f2b97f35 100644
--- a/addons/website_sale_digital/controllers/main.py
+++ b/addons/website_sale_digital/controllers/main.py
@@ -34,20 +34,16 @@ class WebsiteSaleDigital(website_account):
return response
order = response.qcontext['order']
invoiced_lines = request.env['account.invoice.line'].sudo().search([('invoice_id', 'in', order.invoice_ids.ids), ('invoice_id.state', '=', 'paid')])
+ products = invoiced_lines.mapped('product_id') | order.order_line.filtered(lambda r: not r.price_subtotal).mapped('product_id')
purchased_products_attachments = {}
- for il in invoiced_lines:
- product = il.product_id
- # Ignore products that do not have digital content
- if not product.product_tmpl_id.type == 'digital':
- continue
-
+ for product in products:
# Search for product attachments
Attachment = request.env['ir.attachment']
product_id = product.id
template = product.product_tmpl_id
att = Attachment.search_read(
- domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id)],
+ domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)],
fields=['name', 'write_date'],
order='write_date desc',
)
diff --git a/addons/website_sale_digital/models/account_invoice.py b/addons/website_sale_digital/models/account_invoice.py
index b6b28026dde..798faca55a0 100644
--- a/addons/website_sale_digital/models/account_invoice.py
+++ b/addons/website_sale_digital/models/account_invoice.py
@@ -13,7 +13,13 @@ class AccountInvoiceLine(models.Model):
# Get paid invoices
purchases = self.sudo().search_read(
- domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id), ('product_id.product_tmpl_id.type', '=', 'digital')],
+ domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id)],
+ fields=['product_id'],
+ )
+
+ # Get free products
+ purchases += self.env['sale.order.line'].sudo().search_read(
+ domain=[('price_subtotal', '=', 0.0), ('order_id.partner_id', '=', partner.id)],
fields=['product_id'],
)
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index 977a39316b5..91211ea79f4 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -494,11 +494,12 @@ class Slide(models.Model):
# --------------------------------------------------
@api.model
- def _fetch_data(self, base_url, data, content_type=False):
+ def _fetch_data(self, base_url, data, content_type=False, extra_params=False):
result = {'values': dict()}
try:
if data:
- base_url = base_url + '?%s' % urlencode(data)
+ sep = '?' if not extra_params else '&'
+ base_url = base_url + '%s%s' % (sep, urlencode(data))
req = urllib2.Request(base_url)
content = urllib2.urlopen(req).read()
if content_type == 'json':
@@ -566,12 +567,13 @@ class Slide(models.Model):
def _parse_google_document(self, document_id, only_preview_fields):
def get_slide_type(vals):
# TDE FIXME: WTF ??
- image = Image.open(io.BytesIO(vals['image'].decode('base64')))
- width, height = image.size
- if height > width:
- return 'document'
- else:
- return 'presentation'
+ slide_type = 'presentation'
+ if vals.get('image'):
+ image = Image.open(io.BytesIO(vals['image'].decode('base64')))
+ width, height = image.size
+ if height > width:
+ return 'document'
+ return slide_type
# Google drive doesn't use a simple API key to access the data, but requires an access
# token. However, this token is generated in module google_drive, which is not in the
@@ -609,12 +611,14 @@ class Slide(models.Model):
values['datas'] = values['image']
values['slide_type'] = 'infographic'
elif google_values['mimeType'].startswith('application/vnd.google-apps'):
- values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], {}, 'pdf')['values']
values['slide_type'] = get_slide_type(values)
- if google_values['exportLinks'].get('text/plain'):
- values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], {})['values']
- if google_values['exportLinks'].get('text/csv'):
- values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], {})['values']
+ if 'exportLinks' in google_values:
+ values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], params, 'pdf', extra_params=True)['values']
+ # Content indexing
+ if google_values['exportLinks'].get('text/plain'):
+ values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], params, extra_params=True)['values']
+ elif google_values['exportLinks'].get('text/csv'):
+ values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], params, extra_params=True)['values']
elif google_values['mimeType'] == 'application/pdf':
# TODO: Google Drive PDF document doesn't provide plain text transcript
values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values']
diff --git a/doc/cla/corporate/somko.md b/doc/cla/corporate/somko.md
new file mode 100644
index 00000000000..0c864ab8d24
--- /dev/null
+++ b/doc/cla/corporate/somko.md
@@ -0,0 +1,17 @@
+Belgium, 30/01/2017
+
+Somko agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Frederik Santens info@somko.be https://github.com/somko
+
+List of contributors:
+
+Frederik Santens info@somko.be https://github.com/somko
+Thomas Vanmellaerts thomas.vanmellaerts@somko.be https://github.com/thomassomko
+Kasper Declercq kasper.declercq@somko.be https://github.com/kasperdeclercq
\ No newline at end of file
diff --git a/doc/cla/individual/yueqin.md b/doc/cla/individual/yueqin.md
new file mode 100644
index 00000000000..972acd4fa8f
--- /dev/null
+++ b/doc/cla/individual/yueqin.md
@@ -0,0 +1,11 @@
+
+
+China, 2017-02-06
+
+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,
+
+Yue Qin 602559817@qq.com https://github.com/yueqin
diff --git a/doc/setup/install.rst b/doc/setup/install.rst
index 9d90fef01d7..3a8d45afa89 100644
--- a/doc/setup/install.rst
+++ b/doc/setup/install.rst
@@ -207,9 +207,9 @@ RPM
.. warning::
- with RHEL-based distributions (RHEL, CenOS, Scientific Linux), EPEL_ must
+ with RHEL-based distributions (RHEL, CentOS, Scientific Linux), EPEL_ must
be added to the distribution's repositories for all of Odoo's
- dependencies to be available. For CenOS:
+ dependencies to be available. For CentOS:
.. code-block:: console
diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py
index 44df2bc4770..1b2cbad555d 100644
--- a/odoo/addons/base/ir/ir_actions.py
+++ b/odoo/addons/base/ir/ir_actions.py
@@ -611,10 +611,11 @@ class IrActionsServer(models.Model):
:type action: browse record
:returns: dict -- evaluation context given to (safe_)safe_eval """
def log(message, level="info"):
- self._cr.execute("""
- INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
- VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
- """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name))
+ with self.pool.cursor() as cr:
+ cr.execute("""
+ INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
+ VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name))
eval_context = super(IrActionsServer, self)._get_eval_context(action=action)
model = self.env[action.model_id.model]
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index 4785359063b..5189d7360f7 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -1340,7 +1340,7 @@ class IrModelData(models.Model):
class WizardModelMenu(models.TransientModel):
_name = 'wizard.ir.model.menu.create'
- menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True)
+ menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True, ondelete='cascade')
name = fields.Char(string='Menu Name', required=True)
@api.multi
diff --git a/odoo/addons/base/ir/ir_ui_menu.py b/odoo/addons/base/ir/ir_ui_menu.py
index b319f77f909..6a7aaa8a482 100644
--- a/odoo/addons/base/ir/ir_ui_menu.py
+++ b/odoo/addons/base/ir/ir_ui_menu.py
@@ -55,7 +55,7 @@ class IrUiMenu(models.Model):
if level <= 0:
return '...'
if self.parent_id:
- return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name
+ return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "")
else:
return self.name
diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py
index c8bcaa042ad..1601819ae94 100644
--- a/odoo/addons/base/res/res_partner.py
+++ b/odoo/addons/base/res/res_partner.py
@@ -430,6 +430,7 @@ class Partner(models.Model):
sync_children = self.child_ids.filtered(lambda c: not c.is_company)
for child in sync_children:
child._commercial_sync_to_children()
+ sync_children._compute_commercial_partner()
return sync_children.write(sync_vals)
@api.multi
@@ -453,6 +454,10 @@ class Partner(models.Model):
commercial_fields = self._commercial_fields()
if any(field in values for field in commercial_fields):
self._commercial_sync_to_children()
+ for child in self.child_ids.filtered(lambda c: not c.is_company):
+ if child.commercial_partner_id != self.commercial_partner_id :
+ self._commercial_sync_to_children()
+ break
# 2b. Address fields: sync if address changed
address_fields = self._address_fields()
if any(field in values for field in address_fields):
diff --git a/odoo/models.py b/odoo/models.py
index c5a6599f5b5..f511b7cce33 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -2001,7 +2001,7 @@ class BaseModel(object):
prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else ''
query = """
- SELECT min(%(table)s.id) AS id, count(%(table)s.id) AS %(count_field)s %(extra_fields)s
+ SELECT min("%(table)s".id) AS id, count("%(table)s".id) AS "%(count_field)s" %(extra_fields)s
FROM %(from)s
%(where)s
%(groupby)s