diff options
36 files changed, 446 insertions, 138 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 09ec5256593..32cbefa051a 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -101,7 +101,7 @@ class AccountBankStatement(models.Model): journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)]) if journals: return journals[0] - return False + return self.env['account.journal'] @api.multi def _get_opening_balance(self, journal_id): diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index f7b45c2d037..6f7fee90d01 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -525,7 +525,7 @@ class AccountInvoice(models.Model): report_invoice = self.env['report']._get_report_from_name('account.report_invoice') except IndexError: report_invoice = False - if report_invoice: + if report_invoice and report_invoice.attachment: for invoice in self: with invoice.env.do_in_draft(): invoice.number, invoice.state = invoice.move_name, 'open' diff --git a/addons/l10n_be_intrastat/wizard/xml_decl.py b/addons/l10n_be_intrastat/wizard/xml_decl.py index 2771e8b63cd..35151da426d 100644 --- a/addons/l10n_be_intrastat/wizard/xml_decl.py +++ b/addons/l10n_be_intrastat/wizard/xml_decl.py @@ -316,8 +316,10 @@ class xml_decl(osv.TransientModel): numlgn = 0 for linekey in entries: - numlgn += 1 amounts = entries[linekey] + if round(amounts[0], 0) == 0: + continue + numlgn += 1 item = ET.SubElement(datas, 'Item') self._set_Dim(item, 'EXSEQCODE', unicode(numlgn)) self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF)) diff --git a/addons/project/project.py b/addons/project/project.py index 7578046c147..35c0a1b6340 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -379,7 +379,10 @@ class task(osv.osv): """ Gives default stage_id """ if context is None: context = {} - return self.stage_find(cr, uid, [], context.get('default_project_id'), [('fold', '=', False)], context=context) + default_project_id = context.get('default_project_id') + if not default_project_id: + return False + return self.stage_find(cr, uid, [], default_project_id, [('fold', '=', False)], context=context) def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None): if context is None: @@ -409,11 +412,16 @@ class task(osv.osv): @api.cr_uid_ids_context def onchange_project(self, cr, uid, id, project_id, context=None): + values = {} if project_id: project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) if project and project.partner_id: - return {'value': {'partner_id': project.partner_id.id}} - return {} + values['partner_id'] = project.partner_id.id + values['stage_id'] = self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context) + else: + values['stage_id'] = False + values['partner_id'] = False + return {'value': values} def onchange_user_id(self, cr, uid, ids, user_id, context=None): vals = {} diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index b0a9dbf0b25..1b323917860 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -33,7 +33,10 @@ class project_issue(osv.Model): """ Gives default stage_id """ if context is None: context = {} - return self.stage_find(cr, uid, [], context.get('default_project_id'), [('fold', '=', False)], context=context) + default_project_id = context.get('default_project_id') + if not default_project_id: + return False + return self.stage_find(cr, uid, [], default_project_id, [('fold', '=', False)], context=context) def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None): if context is None: @@ -117,11 +120,18 @@ class project_issue(osv.Model): return res def on_change_project(self, cr, uid, ids, project_id, context=None): + values = {} if project_id: project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) if project and project.partner_id: - return {'value': {'partner_id': project.partner_id.id, 'email_from': project.partner_id.email}} - return {} + values['partner_id'] = project.partner_id.id + values['email_from'] = project.partner_id.email + values['stage_id'] = self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context) + else: + values['partner_id'] = False + values['email_from'] = False + values['stage_id'] = False + return {'value': values} _columns = { 'id': fields.integer('ID', readonly=True), diff --git a/addons/purchase/invoice.py b/addons/purchase/invoice.py index 7e78dc683e6..2f634ef2c6a 100644 --- a/addons/purchase/invoice.py +++ b/addons/purchase/invoice.py @@ -160,13 +160,14 @@ class AccountInvoice(models.Model): #line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...] taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']]) price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded'] + price_before = line.get('price', 0.0) line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)}) diff_res.append({ 'type': 'src', 'name': i_line.name[:64], 'price_unit': round(price_unit - valuation_price_unit, account_prec), 'quantity': line['quantity'], - 'price': round((price_unit - valuation_price_unit) * line['quantity'], account_prec), + 'price': round(price_before - line.get('price', 0.0), account_prec), 'account_id': acc, 'product_id': line['product_id'], 'uom_id': line['uom_id'], diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 8d558931ce5..e9ee4877209 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -73,12 +73,15 @@ class PurchaseOrder(models.Model): types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) return types[:1] - @api.depends('order_line.move_ids.picking_id') + @api.depends('order_line.move_ids') def _compute_picking(self): for order in self: pickings = self.env['stock.picking'] for line in order.order_line: - moves = line.move_ids.filtered(lambda r: r.state != 'cancel') + # We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and + # do some recursive search, but that could be prohibitive if not done correctly. + moves = line.move_ids | line.move_ids.mapped('returned_move_ids') + moves = moves.filtered(lambda r: r.state != 'cancel') pickings |= moves.mapped('picking_id') order.picking_ids = pickings order.picking_count = len(pickings) diff --git a/addons/sale_timesheet/models/sale_service.py b/addons/sale_timesheet/models/sale_service.py index a60ed3a8167..d34b55f4708 100644 --- a/addons/sale_timesheet/models/sale_service.py +++ b/addons/sale_timesheet/models/sale_service.py @@ -29,6 +29,16 @@ class procurement_order(osv.osv): def _run(self, cr, uid, ids, context=None): procurement = self.browse(cr, uid, ids[0], context=context) if procurement._is_procurement_task() and not procurement.task_id: + # If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a new + # task. + if procurement.sale_line_id: + existing_task = self.pool['project.task'].search( + cr, uid, [('sale_line_id', '=', procurement.sale_line_id.id)], + context=context + ) + if existing_task: + return existing_task + #create a task for the procurement return self._create_service_task(cr, uid, procurement, context=context) return super(procurement_order, self)._run(cr, uid, ids, context=context) diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index b0d1c2d92c1..dac57f6b321 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -177,11 +177,13 @@ class Quant(models.Model): quants_reconcile_sudo = self.env['stock.quant'].sudo() quants_move_sudo = self.env['stock.quant'].sudo() + check_lot = False for quant, qty in quants: if not quant: #If quant is None, we will create a quant to move (and potentially a negative counterpart too) quant = self._quant_create_from_move( qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to) + check_lot = True else: quant._quant_split(qty) quants_move_sudo |= quant @@ -201,6 +203,24 @@ class Quant(models.Model): if self._cr.fetchone(): quants_reconcile_sudo._quant_reconcile_negative(move) + # In case of serial tracking, check if the product does not exist somewhere internally already + # Checking that a positive quant already exists in an internal location is too restrictive. + # Indeed, if a warehouse is configured with several steps (e.g. "Pick + Pack + Ship") and + # one step is forced (creates a quant of qty = -1.0), it is not possible afterwards to + # correct the inventory unless the product leaves the stock. + picking_type = move.picking_id and move.picking_id.picking_type_id or False + if check_lot and lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)): + other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id), + ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')]) + if other_quants: + # We raise an error if: + # - the total quantity is strictly larger than 1.0 + # - there are more than one negative quant, to avoid situations where the user would + # force the quantity at several steps of the process + if sum(other_quants.mapped('qty')) > 1.0 or len([q for q in other_quants.mapped('qty') if q < 0]) > 1: + lot_name = self.env['stock.production.lot'].browse(lot_id).name + raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set.")) + @api.model def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, @@ -234,16 +254,10 @@ class Quant(models.Model): negative_quant_id = self.sudo().create(negative_vals) vals.update({'propagated_from_id': negative_quant_id.id}) - # In case of serial tracking, check if the product does not exist somewhere internally already picking_type = move.picking_id and move.picking_id.picking_type_id or False if lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)): if qty != 1.0: raise UserError(_('You should only receive by the piece with the same serial number')) - other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id), - ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')]) - if other_quants: - lot_name = self.env['stock.production.lot'].browse(lot_id).name - raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set.")) # create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants return self.sudo().create(vals) @@ -423,7 +437,9 @@ class Quant(models.Model): if pack_operation_id: pack_operation = self.env['stock.pack.operation'].browse(pack_operation_id) - domain += [('owner_id', '=', pack_operation.owner_id.id), ('location_id', '=', pack_operation.location_id.id)] + domain += [('location_id', '=', pack_operation.location_id.id)] + if pack_operation_id.owner_id: + domain += [('owner_id', '=', pack_operation.owner_id.id)] if pack_operation.package_id and not pack_operation.product_id: domain += [('package_id', 'child_of', pack_operation.package_id.id)] elif pack_operation.package_id and pack_operation.product_id: @@ -431,7 +447,9 @@ class Quant(models.Model): else: domain += [('package_id', '=', False)] else: - domain += [('owner_id', '=', move.restrict_partner_id.id), ('location_id', 'child_of', move.location_id.id)] + domain += [('location_id', 'child_of', move.location_id.id)] + if move.restrict_partner_id: + domain += [('owner_id', '=', move.restrict_partner_id.id)] if company_id: domain += [('company_id', '=', company_id)] diff --git a/addons/stock_dropshipping/__init__.py b/addons/stock_dropshipping/__init__.py index 67dee8c60db..d7156f09070 100644 --- a/addons/stock_dropshipping/__init__.py +++ b/addons/stock_dropshipping/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import models # noqa diff --git a/addons/stock_dropshipping/models/__init__.py b/addons/stock_dropshipping/models/__init__.py new file mode 100644 index 00000000000..16c708eba5a --- /dev/null +++ b/addons/stock_dropshipping/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import stock_dropshipping # noqa diff --git a/addons/stock_dropshipping/models/stock_dropshipping.py b/addons/stock_dropshipping/models/stock_dropshipping.py new file mode 100644 index 00000000000..0c2e2424fa0 --- /dev/null +++ b/addons/stock_dropshipping/models/stock_dropshipping.py @@ -0,0 +1,19 @@ +# coding: utf-8 +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import models, api + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + @api.model + def _anglo_saxon_sale_move_lines(self, i_line): + for sale_line in i_line.sale_line_ids: + for proc in sale_line.procurement_ids: + if proc.purchase_line_id: + # if the invoice line is related to sale order lines having one of its + # procurement_ids with a purchase_line_id set, it means that it is a + # confirmed dropship and in that case we mustn't create the cost of + # sale line (because the product won't enter the stock) + return [] + return super(AccountInvoice, self)._anglo_saxon_sale_move_lines(i_line) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 902e7500650..057bdd45aef 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -638,8 +638,8 @@ class Database(http.Controller): try: # country code could be = "False" which is actually True in python country_code = post.get('country_code') or False - request.session.proxy("db").create_database(master_pwd, name, bool(post.get('demo')), lang, password, post.get('login'), country_code) - request.session.authenticate(name, 'admin', password) + request.session.proxy("db").create_database(master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code) + request.session.authenticate(name, post['login'], password) return http.local_redirect('/web/') except Exception, e: error = "Database creation error: %s" % e diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index 3bc97996962..3b4628322f7 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -1440,7 +1440,7 @@ var FieldStatus = common.AbstractField.extend({ }); } } - }, 300), + }, 300, true), }); var FieldMonetary = FieldFloat.extend({ diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 83e75faf9a6..264fa2af985 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -692,7 +692,6 @@ var PivotView = View.extend({ if (width > 1) { var total_cell = {width:nbr_measures, height: depth, title:""}; if (nbr_measures === 1) { - total_cell.title = this.measures[this.active_measures[0]].string; total_cell.total = true; } result[0].push(total_cell); diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html index dfcb95d4d05..7cef4678e8d 100644 --- a/addons/web/views/database_manager.html +++ b/addons/web/views/database_manager.html @@ -69,7 +69,7 @@ <div class="row"> <div class="col-md-12"> <label for="login" class="control-label">Email</label> - <input id="login" type="text" name="login" class="form-control" autocomplete="off"/> + <input id="login" type="text" name="login" class="form-control" required="required" autocomplete="off"/> </div> </div> </div> diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less index 6cf994e2f03..a21d63fe642 100644 --- a/addons/web_editor/static/src/less/web_editor.ui.components.less +++ b/addons/web_editor/static/src/less/web_editor.ui.components.less @@ -243,7 +243,8 @@ body .modal { font-weight: bold; } .o_bg_img_opt_object { - width: 100%; + display: inline-block; + max-width: 100%; background: white; min-height: 10px; position: relative; diff --git a/addons/web_tip/static/src/js/tip.js b/addons/web_tip/static/src/js/tip.js index 85774c1937b..47d20a2f7ee 100644 --- a/addons/web_tip/static/src/js/tip.js +++ b/addons/web_tip/static/src/js/tip.js @@ -40,7 +40,10 @@ var Tips = Class.extend({ on_view: function() { var self = this; - bus.on('DOM_updated', this, function() { + if (this._DOM_updated) { + bus.off('DOM_updated', this, this._DOM_updated); + } + this._DOM_updated = function() { var action_id = self.view.ViewManager.action ? self.view.ViewManager.action.id : null; var model = self.view.fields_view.model; var mode = self.view.fields_view.type; @@ -49,14 +52,17 @@ var Tips = Class.extend({ self.on_form_view(action_id, model); } else if (self.view.hasOwnProperty('editor')) { - self.view.on('view_list_rendered', self, function() { - self.eval_tips(action_id, model, mode); - }); + if (self._view_list_rendered) { + self.view.off('view_list_rendered', self, self._view_list_rendered); + } + self._view_list_rendered = function() { self.eval_tips(action_id, model, mode); }; + self.view.on('view_list_rendered', self, self._view_list_rendered); } else { self.eval_tips(action_id, model, mode); } - }); + }; + bus.on('DOM_updated', this, this._DOM_updated); }, on_form_view: function(action_id, model) { @@ -64,19 +70,26 @@ var Tips = Class.extend({ var type = this.view.datarecord ? this.view.datarecord.type : null if ($('.oe_chatter').length > 0) { - this.view.on('chatter_messages_displayed', this, function() { - self.eval_tips(action_id, model, 'form', type); - }); + if (this._chatter_messages_displayed) { + this.view.off('chatter_messages_displayed', this, this._chatter_messages_displayed); + } + this._chatter_messages_displayed = function() { self.eval_tips(action_id, model, 'form', type); }; + this.view.on('chatter_messages_displayed', this, this._chatter_messages_displayed); } else { this.eval_tips(action_id, model, 'form', type); } - this.view.on('to_edit_mode', this, function() { - self.remove_tips(); - }); - this.view.on('to_view_mode', this, function() { - self.eval_tips(action_id, model, 'form', type); - }); + if (this._to_edit_mode) { + this.view.off('to_edit_mode', this, this._to_edit_mode); + } + this._to_edit_mode = function() { self.remove_tips(); }; + this.view.on('to_edit_mode', this, this._to_edit_mode); + + if (this._to_view_mode) { + this.view.off('to_view_mode', this, this._to_view_mode); + } + this._to_view_mode = function() { self.eval_tips(action_id, model, 'form', type); }; + this.view.on('to_view_mode', this, this._to_view_mode); }, eval_tips: function(action_id, model, mode, type) { @@ -123,11 +136,12 @@ var Tips = Class.extend({ var Tip = Class.extend({ init: function(tip) { + var self = this; this.tip = tip; - this.highlight_selector = tip.highlight_selector; this.end_selector = tip.end_selector ? tip.end_selector : tip.highlight_selector; this.triggers = tip.trigger_selector ? tip.trigger_selector.split(',') : []; + this._resize = function() { self._set_breathing_position(); }; }, do_tip: function() { @@ -171,17 +185,9 @@ var Tip = Class.extend({ }); // resize - bus.on('resize', this, function() { - self._set_breathing_position(); - }); - - bus.on('image_loaded', this, function() { - self._set_breathing_position(); - }); - - bus.on('please_reposition_tip', this, function() { - self._set_breathing_position(); - }); + bus.on('resize', this, this._resize); + bus.on('image_loaded', this, this._resize); + bus.on('please_reposition_tip', this, this._resize); return true; }, @@ -246,6 +252,7 @@ var Tip = Class.extend({ this.$overlay.on('click', function($ev) { self.end_tip(); }); + $(document).on('keyup.web_tip', function($ev) { if ($ev.which === 27) { // esc self.end_tip(); @@ -253,9 +260,8 @@ var Tip = Class.extend({ }); // resize - bus.on('resize', this, function() { - self._set_popover_position(); - }); + this._resize_position = function() { self._set_popover_position(); }; + bus.on('resize', this, this._resize_position); }, scroll_to_tip: function(){ @@ -278,11 +284,16 @@ var Tip = Class.extend({ this.$overlay.remove(); this.$cross.remove(); - _.each($('.oe_tip_fix_parent'), function(el) { - $(el).removeClass('oe_tip_fix_parent'); + $('.oe_tip_fix_parent').each(function() { + $(this).removeClass('oe_tip_fix_parent'); }); $(document).off('keyup.web_tip'); + bus.off('resize', this, this._resize_position); + bus.off('resize', this, this._resize); + bus.off('image_loaded', this, this._resize); + bus.off('please_reposition_tip', this, this._resize); + Tips.call('consume', [this.tip.id], {}); this.tip.is_consumed = true; }, diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 2a22bc673c4..fd3904406ce 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -667,6 +667,9 @@ class res_partner(osv.osv): 'zoom': zoom, 'sensor': 'false', } + google_maps_api_key = self.pool['ir.config_parameter'].get_param(cr, openerp.SUPERUSER_ID, 'google_maps_api_key', context=context) + if google_maps_api_key: + params['key'] = google_maps_api_key return urlplus('//maps.googleapis.com/maps/api/staticmap' , params) def google_map_link(self, cr, uid, ids, zoom=10, context=None): diff --git a/addons/website/static/src/less/website.ui.components.less b/addons/website/static/src/less/website.ui.components.less index 09f536e6453..9fdc7b6a477 100644 --- a/addons/website/static/src/less/website.ui.components.less +++ b/addons/website/static/src/less/website.ui.components.less @@ -244,7 +244,7 @@ body .modal { } // LOGIN FORM -.oe_login_form { +.oe_login_form, .oe_signup_form { max-width: 300px; position: relative; margin: 50px auto; diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 73008844b7c..343fcf1e648 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -911,7 +911,14 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml "fields": ["name", "address", "phone", "mobile", "fax", "email"]}'/> </address> <!-- FORWARD-PORT UP TO SAAS-12 --> - <a t-att-href="res_company.google_map_link()" target="_BLANK">Visualize on Google Maps</a> + <t t-if="'key=' in res_company.google_map_img()"> + <a t-att-href="res_company.google_map_link()" target="_BLANK"> + <img class="thumbnail img-responsive" t-att-src="res_company.google_map_img()" /> + </a> + </t> + <t t-if="'key=' not in res_company.google_map_img()"> + <a t-att-href="res_company.google_map_link()" target="_BLANK">Visualize on Google Maps</a> + </t> </template> <template id="contactus" name="Contact us" page="True"> diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 9345de36864..222d4f5de95 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -119,6 +119,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage): partners = partners[pager['offset']:pager['offset'] + self._references_per_page] google_map_partner_ids = ','.join(map(str, [p.id for p in partners])) + google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('google_maps_api_key') values = { 'countries': countries, @@ -130,6 +131,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage): 'pager': pager, 'searches': post, 'search_path': "%s" % werkzeug.url_encode(post), + 'google_maps_api_key': google_maps_api_key, } return request.render("website_crm_partner_assign.index", values, status=partners and 200 or 404) diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml index 8263a788eb2..89a84922d71 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml @@ -119,25 +119,27 @@ <template id="ref_country" inherit_id="website_crm_partner_assign.index" customize_show="True" name="Left World Map"> <xpath expr="//ul[@id='reseller_countries']" position="after"> - <!-- modal for large map --> - <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> - <div class="modal-dialog modal-lg"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">World Map</h4> + <t t-if="google_maps_api_key"> + <!-- modal for large map --> + <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">World Map</h4> + </div> + <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/partners/" + style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> </div> - <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/partners/" - style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> + </div> </div> - </div> - </div> - <!-- modal end --> - <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> - <ul class="nav"> - <iframe t-attf-src="/google_map?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/partners/" - style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> - </ul> + <!-- modal end --> + <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> + <ul class="nav"> + <iframe t-attf-src="/google_map?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/partners/" + style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> + </ul> + </t> </xpath> </template> diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index e7a68808a57..d5a0dc1052c 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -85,6 +85,7 @@ class WebsiteCustomer(http.Controller): context=request.context) google_map_partner_ids = ','.join(map(str, partner_ids)) partners = partner_obj.browse(request.cr, openerp.SUPERUSER_ID, partner_ids, request.context) + google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('google_maps_api_key') tag_obj = request.registry['res.partner.tag'] tag_ids = tag_obj.search(cr, uid, [('website_published', '=', True), ('partner_ids', 'in', partner_ids)], @@ -103,6 +104,7 @@ class WebsiteCustomer(http.Controller): 'search_path': "?%s" % werkzeug.url_encode(post), 'tag': tag, 'tags': tags, + 'google_maps_api_key': google_maps_api_key, } return request.website.render("website_customer.index", values) diff --git a/addons/website_customer/views/website_customer.xml b/addons/website_customer/views/website_customer.xml index 680b82c4a18..f13d3ec6d6d 100644 --- a/addons/website_customer/views/website_customer.xml +++ b/addons/website_customer/views/website_customer.xml @@ -68,25 +68,27 @@ <!-- Option: left column: World Map --> <template id="opt_country" inherit_id="website_customer.index" customize_show="True" name="Show Map"> <xpath expr="//div[@id='ref_left_column']" position="inside"> - <!-- modal for large map --> - <div class="modal fade customer_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> - <div class="modal-dialog modal-lg"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">World Map</h4> + <t t-if="google_maps_api_key"> + <!-- modal for large map --> + <div class="modal fade customer_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">World Map</h4> + </div> + <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/customers/" + style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> </div> - <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/customers/" - style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> + </div> </div> - </div> - </div> - <!-- modal end --> - <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".customer_map_modal"><span class="fa fa-external-link" /></button></h3> - <ul class="nav"> - <iframe t-attf-src="/google_map?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/customers/" - style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> - </ul> + <!-- modal end --> + <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".customer_map_modal"><span class="fa fa-external-link" /></button></h3> + <ul class="nav"> + <iframe t-attf-src="/google_map?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/customers/" + style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> + </ul> + </t> </xpath> </template> diff --git a/addons/website_event/views/website_event_templates.xml b/addons/website_event/views/website_event_templates.xml index c5f28beb6e5..5a00e44953a 100644 --- a/addons/website_event/views/website_event_templates.xml +++ b/addons/website_event/views/website_event_templates.xml @@ -335,7 +335,14 @@ </div> <div class="panel-body"> <!-- FORWARD-PORT UP TO SAAS-12 --> - <a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a> + <t t-if="'key=' in event.google_map_img()"> + <a t-att-href="event.google_map_link()" target="_BLANK"> + <img class="thumbnail img-responsive" t-att-src="event.google_map_img()" /> + </a> + </t> + <t t-if="'key=' not in event.google_map_img()"> + <a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a> + </t> <div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-field-options='{ "widget": "contact", "fields": ["address", "phone", "mobile", "fax", "email"] diff --git a/addons/website_google_map/controllers/main.py b/addons/website_google_map/controllers/main.py index 01a547920bf..ce508cac293 100644 --- a/addons/website_google_map/controllers/main.py +++ b/addons/website_google_map/controllers/main.py @@ -52,8 +52,10 @@ class GoogleMap(http.Controller): else: partner_url = '/partners/' + google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('google_maps_api_key') values = { 'partner_url': partner_url, - 'partner_data': json.dumps(partner_data) + 'partner_data': json.dumps(partner_data), + 'google_maps_api_key': google_maps_api_key, } return request.website.render("website_google_map.google_map", values) diff --git a/addons/website_google_map/views/google_map_templates.xml b/addons/website_google_map/views/google_map_templates.xml index 6869b5a674e..502a3b902fa 100644 --- a/addons/website_google_map/views/google_map_templates.xml +++ b/addons/website_google_map/views/google_map_templates.xml @@ -14,7 +14,13 @@ var odoo_partner_data = <t t-raw="partner_data"/>; </script> <div id="odoo-google-map"></div> - <script src="//maps.google.com/maps/api/js?sensor=false"></script> + <t t-if="google_maps_api_key"> + <script t-attf-src="//maps.google.com/maps/api/js?key=#{google_maps_api_key}"></script> + </t> + <t t-if="not google_maps_api_key"> + <script src="//maps.google.com/maps/api/js"></script> + </t> + <script type="text/javascript" src="/website_google_map/static/src/lib/markerclusterer.js"></script> <script type="text/javascript" src="/website_google_map/static/src/js/website_google_map.js"></script> </body> diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index 7fc325bdbbb..53d3fb7c122 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -138,6 +138,7 @@ class WebsiteMembership(http.Controller): count_members += len(free_partner_ids) google_map_partner_ids = ",".join(map(str, google_map_partner_ids)) + google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('google_maps_api_key') partners = { p.id: p for p in partner_obj.browse(request.cr, SUPERUSER_ID, list(page_partner_ids), request.context)} @@ -162,6 +163,7 @@ class WebsiteMembership(http.Controller): 'pager': pager, 'post': post, 'search': "?%s" % werkzeug.url_encode(post), + 'google_maps_api_key': google_maps_api_key, } return request.website.render("website_membership.index", values) diff --git a/addons/website_membership/views/website_membership.xml b/addons/website_membership/views/website_membership.xml index 8ea67e2ad91..8ff6b4a7658 100644 --- a/addons/website_membership/views/website_membership.xml +++ b/addons/website_membership/views/website_membership.xml @@ -104,25 +104,27 @@ <template id="opt_index_google_map" name="Left World Map" customize_show="True" inherit_id="website_membership.index"> <xpath expr="//div[@id='left_column']/ul[last()]" position="after"> - <!-- modal for large map --> - <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> - <div class="modal-dialog modal-lg"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">World Map</h4> + <t t-if="google_maps_api_key"> + <!-- modal for large map --> + <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">World Map</h4> + </div> + <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/members/" + style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> </div> - <iframe t-attf-src="/google_map/?width=898&height=485&partner_ids=#{ google_map_partner_ids }&partner_url=/members/" - style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe> + </div> </div> - </div> - </div> - <!-- modal end --> - <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> - <ul class="nav"> - <iframe t-attf-src="/google_map/?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/members/" - style="width:260px; height:240px; border:0; padding:0; margin:0;"></iframe> - </ul> + <!-- modal end --> + <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> + <ul class="nav"> + <iframe t-attf-src="/google_map/?width=260&height=240&partner_ids=#{ google_map_partner_ids }&partner_url=/members/" + style="width:260px; height:240px; border:0; padding:0; margin:0;"></iframe> + </ul> + </t> </xpath> </template> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 88a69e08acd..bc367eb917b 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -124,7 +124,8 @@ class WebsiteSale(http.Controller): def _get_search_order(self, post): # OrderBy will be parsed in orm and so no direct sql injection - return 'website_published desc,%s' % post.get('order', 'website_sequence desc') + # id is added to be sure that order is a unique sort key + return 'website_published desc,%s , id desc' % post.get('order', 'website_sequence desc') @http.route(['/shop/change_pricelist/<model("product.pricelist"):pl_id>'], type='http', auth="public", website=True) def pricelist_change(self, pl_id, **post): diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 2bd26da5fe8..efad9a19072 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -780,7 +780,7 @@ </div> </td> <td class="text-center" id="td-price" name="price"> - <del t-attf-class="text-danger #{'' if line.product_id.lst_price > line.discounted_price else 'hidden'}" style="text-decoration: line-through; white-space: nowrap;" + <del t-attf-class="text-danger #{'' if line.product_id.lst_price - line.discounted_price > 0.1 else 'hidden'}" style="text-decoration: line-through; white-space: nowrap;" t-field="line.product_id.lst_price" t-field-options='{ "widget": "monetary", "from_currency": "line.company_id.currency_id", diff --git a/doc/reference/upgrade_api.rst b/doc/reference/upgrade_api.rst index dd03f4f3f5d..567d8aa29fc 100644 --- a/doc/reference/upgrade_api.rst +++ b/doc/reference/upgrade_api.rst @@ -23,11 +23,11 @@ Although the database will follow the same process described on that form. The required steps are: -* creating a request -* uploading a database dump -* running the upgrade process -* obtaining the status of the database request -* downloading the upgraded database dump +* :ref:`creating a request <upgrade-api-create-method>` +* :ref:`uploading a database dump <upgrade-api-upload-method>` +* :ref:`running the upgrade process <upgrade-api-process-method>` +* :ref:`obtaining the status of the database request <upgrade-api-status-method>` +* :ref:`downloading the upgraded database dump <upgrade-api-download-method>` The methods ~~~~~~~~~~~ @@ -60,9 +60,11 @@ The ``create`` method :param str filename: (required) a purely informative name for you database dump file :param str timezone: (optional) the timezone used by your server. Only for Odoo source version < 6.1 :return: request result - :rtype: json dictionary + :rtype: JSON dictionary -The *create* method returns a json dictionary containing the following keys: +The *create* method returns a JSON dictionary containing the following keys: + +.. _upgrade-api-json-failure: ``failures`` '''''''''''' @@ -201,11 +203,17 @@ Here are 2 examples of database upgrade request creation using: Uploading your database dump ============================ -This action upload your database dump. +There are 2 methods to upload your database dump: + +* the ``upload`` method using the HTTPS protocol +* the ``request_sftp_access`` method using the SFTP protocol The ``upload`` method --------------------- +It's the most simple and most straightforward way of uploading your database dump. +It uses the HTTPS protocol. + .. py:function:: https://upgrade.odoo.com/database/v1/upload Uploads a database dump @@ -213,12 +221,12 @@ The ``upload`` method :param str key: (required) your private key :param str request: (required) your request id :return: request result - :rtype: json dictionary + :rtype: JSON dictionary The request id and the private key are obtained using the :ref:`create method <upgrade-api-create-method>` -The result is a json dictionary containing the list of ``failures``, which +The result is a JSON dictionary containing the list of ``failures``, which should be empty if everything went fine. .. rst-class:: setup doc-aside @@ -230,8 +238,6 @@ should be empty if everything went fine. import os import pycurl from urllib import urlencode - from io import BytesIO - import json UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload" DUMPFILE = "openchs.70.cdump" @@ -267,6 +273,144 @@ should be empty if everything went fine. HEADER="Content-Type: application/octet-stream" curl -H $HEADER --data-binary "@${DUMPFILE}" "${UPLOAD_URL}?${URL_PARAMS}" +.. _upgrade-api-request-sftp-access-method: + +The ``request_sftp_access`` method +---------------------------------- + +This method is recommanded for big database dumps. +It uses the SFTP protocol and supports resuming. + +It will create a temporary SFTP server where you can connect to and allow you +to upload your database dump using an SFTP client. + +.. py:function:: https://upgrade.odoo.com/database/v1/request_sftp_access + + Creates an SFTP server + + :param str key: (required) your private key + :param str request: (required) your request id + :param str ssh_keys: (required) the path to a file listing the ssh public keys you'd like to use + :return: request result + :rtype: JSON dictionary + +The request id and the private key are obtained using the :ref:`create method +<upgrade-api-create-method>` + +The file listing your ssh public keys should be roughly similar to a standard ``authorized_keys`` file. +This file should only contains public keys, blank lines or comments (lines starting with the ``#`` character) + +Your database upgrade request should be in the ``draft`` state. + +The ``request_sftp_access`` method returns a JSON dictionary containing the following keys: + + +.. rst-class:: setup doc-aside + +.. switcher:: + + .. code-block:: python + + import os + import pycurl + from urllib import urlencode + + UPLOAD_URL = "https://upgrade.odoo.com/database/v1/request_sftp_access" + SSH_KEYS="/path/to/your/authorized_keys" + + fields = dict([ + ('request', '10534'), + ('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='), + ]) + postfields = urlencode(fields) + + c = pycurl.Curl() + c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields) + c.setopt(pycurl.POST, 1) + c.setopt(c.HTTPPOST,[("ssh_keys", + (c.FORM_FILE, SSH_KEYS, + c.FORM_CONTENTTYPE, "text/plain")) + ]) + + c.perform() + c.close() + + .. code-block:: bash + + REQUEST_SFTP_ACCESS_URL="https://upgrade.odoo.com/database/v1/request_sftp_access" + SSH_KEYS=/path/to/your/authorized_keys + KEY="Aw7pItGVKFuZ_FOR3U8VFQ==" + REQUEST_ID="10534" + URL_PARAMS="key=${KEY}&request=${REQUEST}" + + curl -sS "${REQUEST_SFTP_ACCESS_URL}?${URL_PARAMS}" -F ssh_keys=@${SSH_KEYS} > request_sftp_result.json + + # check for failures + failures=$(cat request_sftp_result.json | jq -r '.failures[]') + if [ "$failures" != "" ]; then + echo $failures | jq -r '.' + exit 1 + fi + + +``failures`` +'''''''''''' + +The list of errors. See :ref:`failures <upgrade-api-json-failure>` for an +explanation about the JSON dictionary returned in case of failure. + +``request`` +''''''''''' + +If the call is successful, the value associated to the *request* key +will be a dictionary containing your SFTP connexion parameters: + +* ``hostname``: the host address to connect to +* ``sftp_port``: the port to connect to +* ``sftp_user``: the SFTP user to use for connecting +* ``shared_file``: the filename you need to use (identical to the ``filename`` value you have used when creating the request in the :ref:`create method <upgrade-api-create-method>`.) +* ``request_id``: the related upgrade request id (only informative, ,not required for the connection) +* ``sample_command``: a sample command using the 'sftp' client + +You should normally be able to connect using the sample command as is. + +You will only have access to the ``shared_file``. No other files will be +accessible and you will not be able to create new files in your shared +environment on the SFTP server. + +Using the 'sftp' client ++++++++++++++++++++++++ + +Once you have successfully connected using your SFTP client, you can upload +your database dump. Here is a sample session using the 'sftp' client: + +:: + + $ sftp -P 2200 user_10534@upgrade.odoo.com + Connected to upgrade.odoo.com. + sftp> put /path/to/openchs.70.cdump openchs.70.cdump + Uploading /path/to/openchs.70.cdump to /openchs.70.cdump + sftp> ls -l openchs.70.cdump + -rw-rw-rw- 0 0 0 849920 Aug 30 15:58 openchs.70.cdump + +If your connection is interrupted, you can continue your file transfer using +the ``-a`` command line switch: + +.. code-block:: text + + sftp> put -a /path/to/openchs.70.cdump openchs.70.cdump + Resuming upload of /path/to/openchs.70.cdump to /openchs.70.cdump + +If you don't want to manually type the command and need to automate your +database upgrade using a script, you can use a batch file or pipe your commands to 'sftp': + +:: + + echo "put /path/to/openchs.70.cdump openchs.70.cdump" | sftp -b - -P 2200 user_10534@upgrade.odoo.com + +The ``-b`` parameter takes a filename. If the filename is ``-``, it reads the commands from standard input. + + .. _upgrade-api-process-method: Asking to process your request @@ -284,12 +428,12 @@ The ``process`` method :param str key: (required) your private key :param str request: (required) your request id :return: request result - :rtype: json dictionary + :rtype: JSON dictionary The request id and the private key are obtained using the :ref:`create method <upgrade-api-create-method>` -The result is a json dictionary containing the list of ``failures``, which +The result is a JSON dictionary containing the list of ``failures``, which should be empty if everything went fine. .. rst-class:: setup doc-aside @@ -350,12 +494,12 @@ The ``status`` method :param str key: (required) your private key :param str request: (required) your request id :return: request result - :rtype: json dictionary + :rtype: JSON dictionary The request id and the private key are obtained using the :ref:`create method <upgrade-api-create-method>` -The result is a json dictionary containing various information about your +The result is a JSON dictionary containing various information about your database upgrade request. .. rst-class:: setup doc-aside @@ -502,4 +646,28 @@ The ``request`` key contains various useful information about your request: } } +.. _upgrade-api-download-method: + +Downloading your database dump +============================== + +Beside downloading your migrated database using the URL provided by the +:ref:`status method <upgrade-api-status-method>`, you can also use the SFTP +protocol as described in the :ref:`request_sftp_access method +<upgrade-api-request-sftp-access-method>` + +The diffence is that you'll only be able to download the migrated database. No +uploading will be possible. + +Your database upgrade request should be in the ``done`` state. + +Once you have successfully connected using your SFTP client, you can download +your database dump. Here is a sample session using the 'sftp' client: + +:: + + $ sftp -P 2200 user_10534@upgrade.odoo.com + Connected to upgrade.odoo.com. + sftp> get upgraded_openchs.70.cdump /path/to/upgraded_openchs.70.cdump + Downloading /upgraded_openchs.70.cdump to /path/to/upgraded_openchs.70.cdump diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 843c5ad3400..52beff348da 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -14,6 +14,7 @@ from operator import itemgetter import json import werkzeug from lxml import etree +from lxml.etree import LxmlError from lxml.builder import E from odoo import api, fields, models, tools, SUPERUSER_ID, _ @@ -324,7 +325,13 @@ actual arch. if values.get('inherit_id'): values['type'] = self.browse(values['inherit_id']).type else: - values['type'] = etree.fromstring(values['arch']).tag + + try: + values['type'] = etree.fromstring(values.get('arch') or values.get('arch_base')).tag + except LxmlError: + # don't raise here, the constraint that runs `self._check_xml` will + # do the job properly. + pass if not values.get('name'): values['name'] = "%s %s" % (values.get('model'), values['type']) @@ -336,7 +343,7 @@ actual arch. def write(self, vals): # If view is modified we remove the arch_fs information thus activating the arch_db # version. An `init` of the view will restore the arch_fs for the --dev mode - if 'arch' in vals and 'install_mode_data' not in self._context: + if ('arch' in vals or 'arch_base' in vals) and 'install_mode_data' not in self._context: vals['arch_fs'] = False # drop the corresponding view customizations (used for dashboards for example), otherwise diff --git a/openerp/addons/base/tests/test_mimetypes.py b/openerp/addons/base/tests/test_mimetypes.py index 0d3ce6b71fc..a76b522a166 100644 --- a/openerp/addons/base/tests/test_mimetypes.py +++ b/openerp/addons/base/tests/test_mimetypes.py @@ -21,11 +21,18 @@ class test_guess_mimetype(unittest.TestCase): def test_default_mimetype_empty(self): mimetype = guess_mimetype('') - self.assertEqual(mimetype, 'application/octet-stream') + # odoo implementation returns application/octet-stream by default + # if available, python-magic returns application/x-empty + self.assertIn(mimetype, ('application/octet-stream', 'application/x-empty')) def test_default_mimetype(self): mimetype = guess_mimetype('', default='test') - self.assertEqual(mimetype, 'test') + # if available, python-magic returns application/x-empty + self.assertIn(mimetype, ('test', 'application/x-empty')) + + def test_mimetype_octet_stream(self): + mimetype = guess_mimetype('\0') + self.assertEqual(mimetype, 'application/octet-stream') def test_mimetype_png(self): content = base64.b64decode(PNG) @@ -35,7 +42,8 @@ class test_guess_mimetype(unittest.TestCase): def test_mimetype_bmp(self): content = base64.b64decode(BMP) mimetype = guess_mimetype(content, default='test') - self.assertEqual(mimetype, 'image/bmp') + # mimetype should match image/bmp, image/x-ms-bmp, ... + self.assertRegexpMatches(mimetype, r'image/.*\bbmp') def test_mimetype_jpg(self): content = base64.b64decode(JPG) diff --git a/openerp/tools/mimetypes.py b/openerp/tools/mimetypes.py index 581dcb27bd6..370c4b2e85b 100644 --- a/openerp/tools/mimetypes.py +++ b/openerp/tools/mimetypes.py @@ -153,9 +153,9 @@ else: # magic from pypi https://pypi.python.org/pypi/python-magic/ if hasattr(magic,'from_buffer'): - guess_mimetype = lambda bin_data: magic.from_buffer(bin_data, mime=True) + guess_mimetype = lambda bin_data, default=None: magic.from_buffer(bin_data, mime=True) # magic from file(1) https://packages.debian.org/squeeze/python-magic elif hasattr(magic,'open'): ms = magic.open(magic.MAGIC_MIME_TYPE) ms.load() - guess_mimetype = ms.buffer + guess_mimetype = lambda bin_data, default=None: ms.buffer(bin_data) |
