diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7ab0343ed2c..d87e3eeeea5 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -998,7 +998,7 @@ class AccountMoveLine(models.Model): second_line_dict['amount_currency'] = -second_line_dict['amount_currency'] # Create the move - writeoff_move = self.env['account.move'].create({ + writeoff_move = self.env['account.move'].with_context(apply_taxes=True).create({ 'journal_id': vals['journal_id'], 'date': vals['date'], 'state': 'draft', 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/res_partner.py b/addons/crm/models/res_partner.py index d3fc76d8cc5..dbbd31965f0 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') activities_count = fields.Integer("Activities", compute='_compute_activities_count') diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index 07fa2d12cf5..cd046ec95a7 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 fb2e2e4f611..81a1c9f9386 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/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 b5389153168..78de88c74da 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 0f85ebc7d34..3ca93dd3b90 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 f7b799b3bfd..85bd770bacc 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -665,7 +665,7 @@ class Database(http.Controller): request.session.authenticate(name, post['login'], password) return http.local_redirect('/web/') except Exception, 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) @@ -676,7 +676,7 @@ class Database(http.Controller): dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name]) return http.local_redirect('/web/database/manager') except Exception, 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) @@ -686,7 +686,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, 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) @@ -704,7 +704,7 @@ class Database(http.Controller): return response except Exception, 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) @@ -714,7 +714,7 @@ class Database(http.Controller): dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)]) return http.local_redirect('/web/database/manager') except Exception, 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) @@ -723,7 +723,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, 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 567fc079836..339c7437afe 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -228,6 +228,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'); @@ -342,7 +345,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_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 79d5d424313..168d1a903da 100644 --- a/addons/website_portal_sale/controllers/main.py +++ b/addons/website_portal_sale/controllers/main.py @@ -152,11 +152,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 1208625bc9f..3372752fdd4 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 be607bb72e2..882805f2836 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -569,7 +569,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 8ad94fd4f56..fb81676db0c 100644 --- a/odoo/service/model.py +++ b/odoo/service/model.py @@ -57,10 +57,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], (long, int)): @@ -74,29 +79,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], (long, int, 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,