summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2016-09-02 16:28:28 +0200
committerChristophe Simonis <chs@odoo.com>2016-09-02 16:28:28 +0200
commit54e5bbdf3531ecfee84fb6aae627d346c2425c30 (patch)
tree2920139e3f9b019371cb2f332a53e0ab5ff8d142
parent198c080b3512e8b7fbcd9ab47d15746776d5d1a6 (diff)
parent897b9b8fc0548c5165d30b7f20b47aa00e918f1d (diff)
[MERGE] forward port branch saas-10 up to 897b9b8
-rw-r--r--addons/account/models/account_bank_statement.py2
-rw-r--r--addons/account/models/account_invoice.py2
-rw-r--r--addons/l10n_be_intrastat/wizard/xml_decl.py4
-rw-r--r--addons/project/project.py14
-rw-r--r--addons/project_issue/project_issue.py16
-rw-r--r--addons/purchase/invoice.py3
-rw-r--r--addons/purchase/purchase.py7
-rw-r--r--addons/sale_timesheet/models/sale_service.py10
-rw-r--r--addons/stock/models/stock_quant.py34
-rw-r--r--addons/stock_dropshipping/__init__.py1
-rw-r--r--addons/stock_dropshipping/models/__init__.py4
-rw-r--r--addons/stock_dropshipping/models/stock_dropshipping.py19
-rw-r--r--addons/web/controllers/main.py4
-rw-r--r--addons/web/static/src/js/views/form_widgets.js2
-rw-r--r--addons/web/static/src/js/views/pivot_view.js1
-rw-r--r--addons/web/views/database_manager.html2
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.components.less3
-rw-r--r--addons/web_tip/static/src/js/tip.js73
-rw-r--r--addons/website/models/website.py3
-rw-r--r--addons/website/static/src/less/website.ui.components.less2
-rw-r--r--addons/website/views/website_templates.xml9
-rw-r--r--addons/website_crm_partner_assign/controllers/main.py2
-rw-r--r--addons/website_crm_partner_assign/views/website_crm_partner_assign.xml36
-rw-r--r--addons/website_customer/controllers/main.py2
-rw-r--r--addons/website_customer/views/website_customer.xml36
-rw-r--r--addons/website_event/views/website_event_templates.xml9
-rw-r--r--addons/website_google_map/controllers/main.py4
-rw-r--r--addons/website_google_map/views/google_map_templates.xml8
-rw-r--r--addons/website_membership/controllers/main.py2
-rw-r--r--addons/website_membership/views/website_membership.xml36
-rw-r--r--addons/website_sale/controllers/main.py3
-rw-r--r--addons/website_sale/views/templates.xml2
-rw-r--r--doc/reference/upgrade_api.rst200
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py11
-rw-r--r--openerp/addons/base/tests/test_mimetypes.py14
-rw-r--r--openerp/tools/mimetypes.py4
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&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/partners/"
+ style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div>
- <iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/customers/"
+ style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div>
- <iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/members/"
+ style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div>
- <iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;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 &gt; 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 &gt; 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)