diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py
index 2cc4ac92ab8..778a844ad83 100644
--- a/addons/base_geolocalize/models/res_partner.py
+++ b/addons/base_geolocalize/models/res_partner.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
-import urllib
+import urllib2
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
@@ -9,10 +9,10 @@ from odoo.exceptions import UserError
def geo_find(addr):
url = 'https://maps.googleapis.com/maps/api/geocode/json?sensor=false&address='
- url += urllib.quote(addr.encode('utf8'))
+ url += urllib2.quote(addr.encode('utf8'))
try:
- result = json.load(urllib.urlopen(url))
+ result = json.load(urllib2.urlopen(url))
except Exception as e:
raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e)
diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py
index 4eed2ef8472..17dcef988be 100644
--- a/addons/base_vat/models/res_partner.py
+++ b/addons/base_vat/models/res_partner.py
@@ -114,7 +114,7 @@ class ResPartner(models.Model):
country_code = partner.commercial_partner_id.country_id.code
if country_code:
if not check_func(country_code.lower(), partner.vat):
- msg = self._construct_constraint_msg(country_code.lower())
+ msg = partner._construct_constraint_msg(country_code.lower())
raise ValidationError(msg)
def _construct_constraint_msg(self, country_code):
diff --git a/addons/calendar/controllers/bus.py b/addons/calendar/controllers/bus.py
index 90203db4463..4ed07ebba9e 100644
--- a/addons/calendar/controllers/bus.py
+++ b/addons/calendar/controllers/bus.py
@@ -10,5 +10,6 @@ class CalendarBusController(BusController):
# --------------------------
def _poll(self, dbname, channels, last, options):
if request.session.uid:
+ channels = list(channels)
channels.append((request.db, 'calendar.alarm', request.env.user.partner_id.id))
return super(CalendarBusController, self)._poll(dbname, channels, last, options)
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 20ae5ab52f9..fb4472f43e8 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -104,7 +104,7 @@ class Lead(models.Model):
date_conversion = fields.Datetime('Conversion Date', readonly=True)
# Messaging and marketing
- message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact")
+ message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
# Only used for type opportunity
probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability())
diff --git a/addons/crm/models/res_partner.py b/addons/crm/models/res_partner.py
index f4f3967cd1d..bc31c6b9c1a 100644
--- a/addons/crm/models/res_partner.py
+++ b/addons/crm/models/res_partner.py
@@ -10,7 +10,7 @@ class Partner(models.Model):
team_id = fields.Many2one('crm.team', string='Sales Channel', oldname='section_id')
opportunity_ids = fields.One2many('crm.lead', 'partner_id', string='Opportunities', domain=[('type', '=', 'opportunity')])
- meeting_ids = fields.Many2many('calendar.event', 'calendar_event_res_partner_rel', 'res_partner_id', 'calendar_event_id', string='Meetings')
+ meeting_ids = fields.Many2many('calendar.event', 'calendar_event_res_partner_rel', 'res_partner_id', 'calendar_event_id', string='Meetings', copy=False)
opportunity_count = fields.Integer("Opportunity", compute='_compute_opportunity_count')
meeting_count = fields.Integer("# Meetings", compute='_compute_meeting_count')
diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py
index 9cd47c39179..7c18edd80a2 100644
--- a/addons/google_account/models/google_service.py
+++ b/addons/google_account/models/google_service.py
@@ -139,7 +139,11 @@ class GoogleService(models.TransientModel):
if error.code == 400: # invalid grant
with registry(request.session.db).cursor() as cur:
self.env(cur)['res.users'].browse(self.env.uid).write({'google_%s_rtoken' % service: False})
- error_key = json.loads(error.read()).get("error", "nc")
+ try:
+ error_file = error.read()
+ error_key = json.loads(error_file).get("error", "nc")
+ except:
+ error_key = error
_logger.exception("Bad google request : %s !", error_key)
error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]") % error_key
raise self.env['res.config.settings'].get_config_warning(error_msg)
diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py
index 169368b9bd5..91eb12166a3 100644
--- a/addons/google_calendar/models/google_calendar.py
+++ b/addons/google_calendar/models/google_calendar.py
@@ -513,7 +513,7 @@ class GoogleCalendar(models.AbstractModel):
if type == "write":
res = CalendarEvent.browse(event['id']).write(result)
elif type == "copy":
- result['recurrence'] = True
+ result['recurrency'] = True
res = CalendarEvent.browse([event['id']]).write(result)
elif type == "create":
res = CalendarEvent.create(result).id
diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py
index f43e585fcba..16a14449bee 100644
--- a/addons/hw_posbox_homepage/controllers/main.py
+++ b/addons/hw_posbox_homepage/controllers/main.py
@@ -55,9 +55,7 @@ index_template = """
If you need to grant remote debugging access to a developer, you can do it here.
-
- If you need to display the current customer basket on another device, you can do it here.
-
+ %s
The PosBox software installed on this posbox is version 16,
the posbox version number is independent from Odoo. You can upgrade
@@ -72,10 +70,21 @@ index_template = """
class PosboxHomepage(odoo.addons.web.controllers.main.Home):
+
+ def get_hw_screen_message(self):
+ return """
+
+ The activate the customer display feature, you will need to reinstall the PosBox software.
+ You can find the latest images on the Odoo Nightly builds website.
+ Make sure to download at least the version 16.
+ Odoo version 11, or above, is required to use the customer display feature.
+
+"""
+
@http.route('/', type='http', auth='none', website=True)
def index(self):
#return request.render('hw_posbox_homepage.index',mimetype='text/html')
- return index_template
+ return index_template % self.get_hw_screen_message()
@http.route('/wifi', type='http', auth='none', website=True)
def wifi(self):
diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py
index a5a3136cec8..fc2724dd31d 100644
--- a/addons/hw_scanner/controllers/main.py
+++ b/addons/hw_scanner/controllers/main.py
@@ -119,7 +119,10 @@ class Scanner(Thread):
def get_devices(self):
try:
if not evdev:
- return None
+ return []
+
+ if not os.path.isdir(self.input_dir):
+ return []
new_devices = [device for device in listdir(self.input_dir)
if join(self.input_dir, device) not in [dev.evdev.fn for dev in self.open_devices]]
diff --git a/addons/hw_screen/controllers/main.py b/addons/hw_screen/controllers/main.py
index f6101e7c8ca..565bdbe02a3 100644
--- a/addons/hw_screen/controllers/main.py
+++ b/addons/hw_screen/controllers/main.py
@@ -4,6 +4,7 @@
from odoo import http
from odoo.tools import config
from odoo.addons.web.controllers import main as web
+from openerp.addons.hw_posbox_homepage.controllers import main as homepage
import logging
import netifaces as ni
@@ -16,6 +17,14 @@ self_port = str(config['xmlrpc_port'] or 8069)
_logger = logging.getLogger(__name__)
+class Homepage(homepage.PosboxHomepage):
+
+ def get_hw_screen_message(self):
+ return """
+
+If you need to display the current customer basket on another device, you can do it here.
+
+"""
class HardwareScreen(web.Home):
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index 4a2f236f272..abebd022fea 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -20,7 +20,7 @@ class Partner(models.Model):
_mail_flat_thread = False
_mail_mass_mailing = _('Customers')
- message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact")
+ message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
opt_out = fields.Boolean(
'Opt-Out', help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
"Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing.")
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 8daa5ce392b..dbf363dc058 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -77,7 +77,7 @@ class MassMailingContact(models.Model):
'contact_id', 'list_id', string='Mailing Lists')
opt_out = fields.Boolean(string='Opt Out', help='The contact has chosen not to receive mails anymore from this list')
unsubscription_date = fields.Datetime(string='Unsubscription Date')
- message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.')
+ message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.', default=0)
country_id = fields.Many2one('res.country', string='Country')
tag_ids = fields.Many2many('res.partner.category', string='Tags')
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index ac799bd0538..a797892a4c2 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -327,8 +327,12 @@ class MrpProduction(models.Model):
return self.env['stock.move']
if bom_line.product_id.type not in ['product', 'consu']:
return self.env['stock.move']
- if self.bom_id.routing_id and self.bom_id.routing_id.location_id:
- source_location = self.bom_id.routing_id.location_id
+ if self.routing_id:
+ routing = self.routing_id
+ else:
+ routing = self.bom_id.routing_id
+ if routing and routing.location_id:
+ source_location = routing.location_id
else:
source_location = self.location_src_id
original_quantity = self.product_qty - self.qty_produced
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 5076972a3e5..9e75caebe0c 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -631,7 +631,7 @@ td {
margin-right: 0;
text-align: center;
vertical-align: top;
- width: 188px;
+ width: 186px;
border: none;
border-radius: 0;
border-top: 1px solid;
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index c4dc828b540..abe09f05fd6 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -335,7 +335,7 @@ class ResCountryGroup(models.Model):
class PricelistItem(models.Model):
_name = "product.pricelist.item"
_description = "Pricelist item"
- _order = "applied_on, min_quantity desc, categ_id desc"
+ _order = "applied_on, min_quantity desc, categ_id desc, id"
product_tmpl_id = fields.Many2one(
'product.template', 'Product Template', ondelete='cascade',
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index 577d8e3841e..41ab8d76787 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -112,7 +112,7 @@
-
+
diff --git a/addons/product_extended/models/product.py b/addons/product_extended/models/product.py
index 11a7bdc07ef..f0f886b66d4 100644
--- a/addons/product_extended/models/product.py
+++ b/addons/product_extended/models/product.py
@@ -35,7 +35,6 @@ class ProductProduct(models.Model):
def _calc_price(self, bom):
price = 0.0
- workcenter_cost = 0.0
result, result2 = bom.explode(self, 1)
for sbom, sbom_data in result2:
if not sbom.attribute_value_ids:
@@ -44,9 +43,8 @@ class ProductProduct(models.Model):
if bom.routing_id:
total_cost = 0.0
for order in bom.routing_id.operation_ids:
- total_cost += (order.time_cycle_manual/60) * order.workcenter_id.costs_hour
- workcenter_cost = total_cost / len(bom.routing_id.operation_ids)
- price += bom.product_uom_id._compute_price(workcenter_cost, bom.product_id.uom_id)
+ total_cost += (order.time_cycle/60) * order.workcenter_id.costs_hour
+ price += bom.product_uom_id._compute_price(total_cost, bom.product_id.uom_id)
# Convert on product UoM quantities
if price > 0:
price = bom.product_uom_id._compute_price(price / bom.product_qty, self.uom_id)
diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py
index fa65e79b38e..8911e3bc716 100644
--- a/addons/product_margin/models/product_product.py
+++ b/addons/product_margin/models/product_product.py
@@ -104,7 +104,7 @@ class ProductProduct(models.Model):
#Cost price is calculated afterwards as it is a property
sqlstr = """
select
- sum(l.price_unit * l.quantity)/sum(nullif(l.quantity,0)) as avg_unit_price,
+ sum(l.price_unit * l.quantity)/nullif(sum(l.quantity),0) as avg_unit_price,
sum(l.quantity) as num_qty,
sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
sum(l.quantity * pt.list_price) as sale_expected
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 16bf6ddf57a..a346fa1a318 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -253,7 +253,10 @@ class SaleOrder(models.Model):
@api.model
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
- vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or _('New')
+ if 'company_id' in vals:
+ vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('sale.order') or _('New')
+ else:
+ vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or _('New')
# Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined
if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
diff --git a/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py
index f06065512d5..6a54492b320 100644
--- a/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py
+++ b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
+import unittest
from odoo.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon
class TestLandedCosts(TestStockLandedCostsCommon):
@@ -45,6 +45,10 @@ class TestLandedCosts(TestStockLandedCostsCommon):
'location_dest_id': self.customer_location_id})
def test_00_landed_costs_on_incoming_shipment(self):
+ chart_of_accounts = self.env.user.company_id.chart_template_id
+ generic_coa = self.env.ref('l10n_generic_coa.configurable_chart_template')
+ if chart_of_accounts != generic_coa:
+ raise unittest.SkipTest('Skip this test as it works only with %s (%s loaded)' % (generic_coa.name, chart_of_accounts.name))
""" Test landed cost on incoming shipment """
#
# (A) Purchase product
@@ -94,6 +98,11 @@ class TestLandedCosts(TestStockLandedCostsCommon):
self.assertEqual(account_entry['debit'], 430.0, 'Wrong Account Entry')
def test_01_negative_landed_costs_on_incoming_shipment(self):
+ chart_of_accounts = self.env.user.company_id.chart_template_id
+ generic_coa = self.env.ref('l10n_generic_coa.configurable_chart_template')
+ if chart_of_accounts != generic_coa:
+ raise unittest.SkipTest('Skip this test as it works only with %s (%s loaded)' % (generic_coa.name, chart_of_accounts.name))
+
""" Test negative landed cost on incoming shipment """
#
# (A) Purchase Product
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 30a4f24c2a6..2cd2c2ac7da 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -680,7 +680,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception as e:
- error = "Database creation error: %s" % e
+ error = "Database creation error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -691,7 +691,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database duplication error: %s" % e
+ error = "Database duplication error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -701,7 +701,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database deletion error: %s" % e
+ error = "Database deletion error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -719,7 +719,7 @@ class Database(http.Controller):
return response
except Exception as e:
_logger.exception('Database.backup')
- error = "Database backup error: %s" % e
+ error = "Database backup error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -729,7 +729,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database restore error: %s" % e
+ error = "Database restore error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -738,7 +738,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Master password update error: %s" % e
+ error = "Master password update error: %s" % str(e) or repr(e)
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js
index 42f2103f95f..e5d184117f4 100644
--- a/addons/web_editor/static/src/js/snippets.editor.js
+++ b/addons/web_editor/static/src/js/snippets.editor.js
@@ -1118,7 +1118,7 @@ data.Editor = Class.extend({
}).bind(this));
// Activate the overlay
- $style_button.toggleClass("hidden", $ul.children(":not(.divider):not(.hidden)").length === 0);
+ $style_button.toggleClass("hidden", $ul.children(":not(.o_main_header):not(.divider):not(.hidden)").length === 0);
this.$overlay.toggleClass("oe_active", !!focus);
function _do_action_focus(style, $dest) {
diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js
index 4d533cadd00..1444f404ff6 100644
--- a/addons/web_editor/static/src/js/snippets.options.js
+++ b/addons/web_editor/static/src/js/snippets.options.js
@@ -576,15 +576,18 @@ odoo.define('web_editor.snippets.options', function (require) {
registry.background = SnippetOption.extend({
start: function () {
var res = this._super.apply(this, arguments);
- this.$target.off(".background-option")
- .on("background-color-event.background-option", (function (e, type) {
- e.stopPropagation();
- if (e.currentTarget !== e.target) return;
- this.$el.find("li:first > a").trigger(type);
- }).bind(this));
+ this.bind_bg_events();
return res;
},
+ bind_bg_events: function () {
+ this.$target.off(".background-option")
+ .on("background-color-event.background-option", (function (e, type) {
+ e.stopPropagation();
+ if (e.currentTarget !== e.target) return;
+ this.$el.find("li:first > a").trigger(type);
+ }).bind(this));
+ },
background: function (type, value, $li) {
if (value && value.length) {
this.$target.css("background-image", "url(" + value + ")");
diff --git a/addons/web_tour/static/src/less/tip.less b/addons/web_tour/static/src/less/tip.less
index 8f994e64ee7..b2679e886c8 100644
--- a/addons/web_tour/static/src/less/tip.less
+++ b/addons/web_tour/static/src/less/tip.less
@@ -238,3 +238,9 @@
}
}
}
+
+@media print {
+ .o_tooltip {
+ display: none;
+ }
+}
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index ed4b810179c..0a1cd2e2923 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -328,7 +328,9 @@ animation.registry.parallax = animation.Class.extend({
if (!this.$bg || !this.$bg.length) {
this.$bg = this.$("> .s_parallax_bg");
if (!this.$bg.length) {
- this.$bg = $("", {"class": "s_parallax_bg"}).prependTo(this.$target);
+ this.$bg = $('', {
+ class: 's_parallax_bg' + (this.$target.hasClass('oe_custom_bg') ? ' oe_custom_bg' : ''),
+ }).prependTo(this.$target);
}
}
var urlTarget = this.$target.css("background-image");
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 083ed195905..0ec8ba7caa7 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -233,6 +233,9 @@ options.registry.carousel = options.registry.slider.extend({
s_option.$target = self.$target.find(".item.active");
s_option.set_active();
s_option.$target.trigger("snippet-option-change", [s_option]);
+ if (opt_name === 'background') {
+ s_option.bind_bg_events();
+ }
});
});
this.$target.trigger('slid.bs.carousel');
@@ -347,7 +350,6 @@ options.registry.parallax = options.Class.extend({
_refresh: function () {
_.defer((function () {
this.$target.data("snippet-view")._rebuild();
- this._update_target_to_bg();
}).bind(this));
},
_toggle_refresh_callback: function (on) {
diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less
index cde2501e605..0544bb847dd 100644
--- a/addons/website/static/src/less/website.snippets.less
+++ b/addons/website/static/src/less/website.snippets.less
@@ -407,7 +407,6 @@ div.carousel {
background-size: cover;
background-attachment: scroll;
pointer-events: none;
- z-index: -1;
}
&.s_parallax_is_fixed > .s_parallax_bg {
background-attachment: fixed;
diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml
index cff4d7ed4a8..40562d95769 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -528,7 +528,7 @@
-
diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py
index 7da7dc9395c..5137fb0671f 100644
--- a/addons/website_event_track/models/event_track.py
+++ b/addons/website_event_track/models/event_track.py
@@ -126,7 +126,7 @@ class Track(models.Model):
vals['kanban_state'] = 'normal'
res = super(Track, self).write(vals)
if vals.get('partner_id'):
- self.message_subscribe(vals['partner_id'])
+ self.message_subscribe([vals['partner_id']])
return res
@api.model
diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py
index ca536bc28bd..fb21827b0fe 100644
--- a/addons/website_forum/controllers/main.py
+++ b/addons/website_forum/controllers/main.py
@@ -152,7 +152,7 @@ class WebsiteForum(http.Controller):
question_ids = Post.search(domain, limit=self._post_per_page, offset=pager['offset'], order=sorting)
- values = self._prepare_forum_values(forum=forum, searches=post)
+ values = self._prepare_forum_values(forum=forum, searches=post, header={'ask_hide': not forum.active})
values.update({
'main_object': tag or forum,
'question_ids': question_ids,
diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py
index 8638b650dd4..f817c0455ef 100644
--- a/addons/website_portal_sale/controllers/main.py
+++ b/addons/website_portal_sale/controllers/main.py
@@ -150,11 +150,14 @@ class website_account(website_account):
order.check_access_rule('read')
except AccessError:
return request.render("website.403")
- order_invoice_lines = {il.product_id.id: il.invoice_id for il in order.invoice_ids.mapped('invoice_line_ids')}
+
+ order_sudo = order.sudo()
+ order_invoice_lines = {il.product_id.id: il.invoice_id for il in order_sudo.invoice_ids.mapped('invoice_line_ids')}
+
history = request.session.get('my_orders_history', [])
values = {
- 'order': order.sudo(),
+ 'order': order_sudo,
'order_invoice_lines': order_invoice_lines,
}
values.update(get_records_pager(history, order))
diff --git a/addons/website_portal_sale/models/payment.py b/addons/website_portal_sale/models/payment.py
index 9842567abb1..8bfd8167229 100644
--- a/addons/website_portal_sale/models/payment.py
+++ b/addons/website_portal_sale/models/payment.py
@@ -61,28 +61,24 @@ class PaymentTransaction(models.Model):
def _confirm_so(self, acquirer_name=False):
for tx in self:
- try:
- # check tx state, confirm the potential SO
- if tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']:
- # verify SO/TX match, excluding tx.fees which are currently not included in SO
- amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0
- if amount_matches:
- if not acquirer_name:
- acquirer_name = tx.sale_order_id.payment_acquirer_id.provider or 'unknown'
- if tx.state == 'authorized' and tx.acquirer_id.auto_confirm == 'authorize':
- _logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
- tx.sale_order_id.with_context(send_email=True).action_confirm()
- if tx.state == 'done' and tx.acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']:
- _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
- tx.sale_order_id.with_context(send_email=True).action_confirm()
+ # check tx state, confirm the potential SO
+ if tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']:
+ # verify SO/TX match, excluding tx.fees which are currently not included in SO
+ amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0
+ if amount_matches:
+ if not acquirer_name:
+ acquirer_name = tx.sale_order_id.payment_acquirer_id.provider or 'unknown'
+ if tx.state == 'authorized' and tx.acquirer_id.auto_confirm == 'authorize':
+ _logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
+ tx.sale_order_id.with_context(send_email=True).action_confirm()
+ if tx.state == 'done' and tx.acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']:
+ _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
+ tx.sale_order_id.with_context(send_email=True).action_confirm()
- if tx.acquirer_id.auto_confirm == 'generate_and_pay_invoice':
- self._generate_and_pay_invoice(tx, acquirer_name)
- elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft':
- _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
- tx.sale_order_id.force_quotation_send()
- else:
- _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
-
- except Exception:
- _logger.exception('Fail to confirm the order or send the confirmation email%s', tx and ' for the transaction %s' % tx.reference or '')
+ if tx.acquirer_id.auto_confirm == 'generate_and_pay_invoice':
+ self._generate_and_pay_invoice(tx, acquirer_name)
+ elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft':
+ _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
+ tx.sale_order_id.force_quotation_send()
+ else:
+ _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index e60362d8472..8d97078b429 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -245,7 +245,9 @@ class WebsiteSale(http.Controller):
ProductAttribute = request.env['product.attribute']
if products:
- attributes = ProductAttribute.search([('attribute_line_ids.product_tmpl_id', 'in', products.ids)])
+ # get all products without limit
+ selected_products = Product.search(domain, limit=False)
+ attributes = ProductAttribute.search([('attribute_line_ids.product_tmpl_id', 'in', selected_products.ids)])
else:
attributes = ProductAttribute.browse(attributes_ids)
diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md
index 31be8b5f9b8..784518b093e 100644
--- a/doc/cla/corporate/savoirfairelinux.md
+++ b/doc/cla/corporate/savoirfairelinux.md
@@ -1,4 +1,4 @@
-Canada, France, 8-2-2015
+Canada, France, 2017-05-12
Savoir-faire Linux agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
@@ -8,22 +8,25 @@ declaration.
Signed,
-Maxime Chambreuil partners@savoirfairelinux.com https://github.com/savoirfairelinux
+Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard
List of contributors:
+Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga
Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau
-David Cormier david.cormier@savoirfairelinux.com https://github.com/cormier
David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid
-Davin Baragiotta davin.baragiotta@savoirfairelinux.com https://github.com/giotta
-El Hadji Dem elhadji.dem@savoirfairelinux.com https://github.com/ehdem
-Guillaume Auger guillaume.auger@savoirfairelinux.com https://github.com/jehog
+Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard
Joao Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf
-Jordi Riera jordi.riera@savoirfairelinux.com https://github.com/foutoucour
-Julien Roux julien.roux@savoirfairelinux.com https://github.com/jrouxsfl
-Loïc Faure-Lacroix loic.lacroix@savoirfairelinux.com https://github.com/llacroix
-Maxime Chambreuil maxime.chambreuil@savoirfairelinux.com https://github.com/max3903
-Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp
+Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL
Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche
-Sandy Carter sandy.carter@savoirfairelinux.com https://github.com/bwrsandman
-Vincent Vinet vincent.vinet@savoirfairelinux.com https://github.com/veloutin
+David Cormier david.cormier@savoirfairelinux.com https://github.com/cormier (up to 2017-05-12)
+Davin Baragiotta davin.baragiotta@savoirfairelinux.com https://github.com/giotta (up to 2017-05-12)
+El Hadji Dem elhadji.dem@savoirfairelinux.com https://github.com/ehdem (up to 2017-05-12)
+Guillaume Auger guillaume.auger@savoirfairelinux.com https://github.com/jehog (up to 2017-05-12)
+Jordi Riera jordi.riera@savoirfairelinux.com https://github.com/foutoucour (up to 2017-05-12)
+Julien Roux julien.roux@savoirfairelinux.com https://github.com/jrouxsfl (up to 2017-05-12)
+Loïc Faure-Lacroix loic.lacroix@savoirfairelinux.com https://github.com/llacroix (up to 2017-05-12)
+Maxime Chambreuil maxime.chambreuil@savoirfairelinux.com https://github.com/max3903 (up to 2017-05-12)
+Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp (up to 2017-05-12)
+Sandy Carter sandy.carter@savoirfairelinux.com https://github.com/bwrsandman (up to 2017-05-12)
+Vincent Vinet vincent.vinet@savoirfairelinux.com https://github.com/veloutin (up to 2017-05-12)
diff --git a/doc/cla/individual/JesusVMayor.md b/doc/cla/individual/JesusVMayor.md
index c009f323be9..456778e94f5 100644
--- a/doc/cla/individual/JesusVMayor.md
+++ b/doc/cla/individual/JesusVMayor.md
@@ -9,4 +9,4 @@ declaration.
Signed,
Jesús Ventosinos Mayor jesus@pexego.es https://github.com/jesusVMayor
-
+Jesús Ventosinos Mayor jesus@comunitea.com https://github.com/jesusVMayor
diff --git a/odoo/models.py b/odoo/models.py
index da99e73f2fe..518e0f7b859 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -559,7 +559,7 @@ class BaseModel(object):
field = cls._fields.get(name)
if not field:
_logger.warning("method %s.%s: @constrains parameter %r is not a field name", cls._name, attr, name)
- elif not (field.store or field.inverse):
+ elif not (field.store or field.inverse or field.inherited):
_logger.warning("method %s.%s: @constrains parameter %r is not writeable", cls._name, attr, name)
methods.append(func)
diff --git a/odoo/service/model.py b/odoo/service/model.py
index ba243bf432c..9bb3770e5c3 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -58,10 +58,15 @@ def check(f):
elif isinstance(kwargs, dict):
if 'context' in kwargs:
ctx = kwargs['context']
- elif 'kwargs' in kwargs:
+ elif 'kwargs' in kwargs and kwargs['kwargs'].get('context'):
# http entry points such as call_kw()
ctx = kwargs['kwargs'].get('context')
-
+ else:
+ try:
+ from odoo.http import request
+ ctx = request.env.context
+ except Exception:
+ pass
uid = 1
if args and isinstance(args[0], pycompat.integer_types):
@@ -75,29 +80,6 @@ def check(f):
# queries (as in IntegrityError) will invalidate the current one.
cr = False
- if hasattr(src, '__call__'):
- # callable. We need to find the right parameters to call
- # the orm._sql_message(self, cr, uid, ids, context) function,
- # or we skip..
- # our signature is f(registry, dbname [,uid, obj, method, args])
- try:
- if args and len(args) > 1:
- # TODO self doesn't exist, but was already wrong before (it was not a registry but just the object_service.
- obj = self.get(args[1])
- if len(args) > 3 and isinstance(args[3], (pycompat.integer_types, list)):
- ids = args[3]
- else:
- ids = []
- cr = odoo.sql_db.db_connect(dbname).cursor()
- return src(obj, cr, uid, ids, context=(ctx or {}))
- except Exception:
- pass
- finally:
- if cr: cr.close()
-
- return False # so that the original SQL error will
- # be returned, it is the best we have.
-
try:
cr = odoo.sql_db.db_connect(dbname).cursor()
res = translate(cr, name=False, source_type=ttype,
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 4c570e7cc87..45ff5e28bbd 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -209,7 +209,16 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
def _fileopen(path, mode, basedir, pathinfo, basename=None):
- name = os.path.normpath(os.path.join(basedir, path))
+ name = os.path.normpath(os.path.normcase(os.path.join(basedir, path)))
+
+ import odoo.modules as addons
+ paths = addons.module.ad_paths + [config['root_path']]
+ for addons_path in paths:
+ addons_path = os.path.normpath(os.path.normcase(addons_path)) + os.sep
+ if name.startswith(addons_path):
+ break
+ else:
+ raise ValueError("Unknown path: %s" % name)
if basename is None:
basename = name