[MERGE] forward port branch 9.0 up to cb1ae53

This commit is contained in:
Christophe Simonis
2016-09-02 15:19:39 +02:00
35 changed files with 444 additions and 139 deletions
@@ -101,7 +101,7 @@ class AccountBankStatement(models.Model):
journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)]) journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
if journals: if journals:
return journals[0] return journals[0]
return False return self.env['account.journal']
@api.multi @api.multi
def _get_opening_balance(self, journal_id): def _get_opening_balance(self, journal_id):
+1 -1
View File
@@ -508,7 +508,7 @@ class AccountInvoice(models.Model):
report_invoice = self.env['report']._get_report_from_name('account.report_invoice') report_invoice = self.env['report']._get_report_from_name('account.report_invoice')
except IndexError: except IndexError:
report_invoice = False report_invoice = False
if report_invoice: if report_invoice and report_invoice.attachment:
for invoice in self: for invoice in self:
with invoice.env.do_in_draft(): with invoice.env.do_in_draft():
invoice.number, invoice.state = invoice.move_name, 'open' invoice.number, invoice.state = invoice.move_name, 'open'
+3 -1
View File
@@ -316,8 +316,10 @@ class xml_decl(osv.TransientModel):
numlgn = 0 numlgn = 0
for linekey in entries: for linekey in entries:
numlgn += 1
amounts = entries[linekey] amounts = entries[linekey]
if round(amounts[0], 0) == 0:
continue
numlgn += 1
item = ET.SubElement(datas, 'Item') item = ET.SubElement(datas, 'Item')
self._set_Dim(item, 'EXSEQCODE', unicode(numlgn)) self._set_Dim(item, 'EXSEQCODE', unicode(numlgn))
self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF)) self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF))
@@ -10,7 +10,6 @@ class ResCompany(osv.Model):
Acquirer = self.pool['payment.acquirer'] Acquirer = self.pool['payment.acquirer']
company_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.id company_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.id
paypal_ids = Acquirer.search(cr, uid, [ paypal_ids = Acquirer.search(cr, uid, [
('website_published', '=', True),
('name', 'ilike', 'paypal'), ('name', 'ilike', 'paypal'),
('company_id', '=', company_id), ('company_id', '=', company_id),
], limit=1, context=context) ], limit=1, context=context)
@@ -24,7 +23,6 @@ class ResCompany(osv.Model):
company_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.id company_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.id
paypal_account = self.browse(cr, uid, id, context=context).paypal_account paypal_account = self.browse(cr, uid, id, context=context).paypal_account
paypal_ids = Acquirer.search(cr, uid, [ paypal_ids = Acquirer.search(cr, uid, [
('website_published', '=', True),
('paypal_email_account', '=', paypal_account), ('paypal_email_account', '=', paypal_account),
('company_id', '=', company_id), ('company_id', '=', company_id),
], context=context) ], context=context)
+11 -3
View File
@@ -379,7 +379,10 @@ class task(osv.osv):
""" Gives default stage_id """ """ Gives default stage_id """
if context is None: if context is None:
context = {} 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): def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
if context is None: if context is None:
@@ -417,11 +420,16 @@ class task(osv.osv):
@api.cr_uid_ids_context @api.cr_uid_ids_context
def onchange_project(self, cr, uid, id, project_id, context=None): def onchange_project(self, cr, uid, id, project_id, context=None):
values = {}
if project_id: if project_id:
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
if project and project.partner_id: if project and project.partner_id:
return {'value': {'partner_id': project.partner_id.id}} values['partner_id'] = project.partner_id.id
return {} 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): def onchange_user_id(self, cr, uid, ids, user_id, context=None):
vals = {} vals = {}
+13 -3
View File
@@ -33,7 +33,10 @@ class project_issue(osv.Model):
""" Gives default stage_id """ """ Gives default stage_id """
if context is None: if context is None:
context = {} 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): def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
if context is None: if context is None:
@@ -117,11 +120,18 @@ class project_issue(osv.Model):
return res return res
def on_change_project(self, cr, uid, ids, project_id, context=None): def on_change_project(self, cr, uid, ids, project_id, context=None):
values = {}
if project_id: if project_id:
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
if project and project.partner_id: if project and project.partner_id:
return {'value': {'partner_id': project.partner_id.id, 'email_from': project.partner_id.email}} values['partner_id'] = project.partner_id.id
return {} 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 = { _columns = {
'id': fields.integer('ID', readonly=True), 'id': fields.integer('ID', readonly=True),
+2 -1
View File
@@ -159,13 +159,14 @@ class AccountInvoice(models.Model):
#line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...] #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']]) 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_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)}) line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)})
diff_res.append({ diff_res.append({
'type': 'src', 'type': 'src',
'name': i_line.name[:64], 'name': i_line.name[:64],
'price_unit': round(price_unit - valuation_price_unit, account_prec), 'price_unit': round(price_unit - valuation_price_unit, account_prec),
'quantity': line['quantity'], '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, 'account_id': acc,
'product_id': line['product_id'], 'product_id': line['product_id'],
'uom_id': line['uom_id'], 'uom_id': line['uom_id'],
+5 -2
View File
@@ -77,12 +77,15 @@ class PurchaseOrder(models.Model):
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
return types[:1] return types[:1]
@api.depends('order_line.move_ids.picking_id') @api.depends('order_line.move_ids')
def _compute_picking(self): def _compute_picking(self):
for order in self: for order in self:
pickings = self.env['stock.picking'] pickings = self.env['stock.picking']
for line in order.order_line: 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') pickings |= moves.mapped('picking_id')
order.picking_ids = pickings order.picking_ids = pickings
order.picking_count = len(pickings) order.picking_count = len(pickings)
@@ -33,6 +33,16 @@ class procurement_order(osv.osv):
def _run(self, cr, uid, procurement, context=None): def _run(self, cr, uid, procurement, context=None):
if self._is_procurement_task(cr, uid, procurement, context=context) and not procurement.task_id: if self._is_procurement_task(cr, uid, procurement, context=context) 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 #create a task for the procurement
return self._create_service_task(cr, uid, procurement, context=context) return self._create_service_task(cr, uid, procurement, context=context)
return super(procurement_order, self)._run(cr, uid, procurement, context=context) return super(procurement_order, self)._run(cr, uid, procurement, context=context)
+27 -9
View File
@@ -412,10 +412,12 @@ class stock_quant(osv.osv):
quants_reconcile = [] quants_reconcile = []
to_move_quants = [] to_move_quants = []
self._check_location(cr, uid, location_to, context=context) self._check_location(cr, uid, location_to, context=context)
check_lot = False
for quant, qty in quants: for quant, qty in quants:
if not quant: if not quant:
#If quant is None, we will create a quant to move (and potentially a negative counterpart too) #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
quant = self._quant_create(cr, uid, 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, context=context) quant = self._quant_create(cr, uid, 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, context=context)
check_lot = True
else: else:
self._quant_split(cr, uid, quant, qty, context=context) self._quant_split(cr, uid, quant, qty, context=context)
to_move_quants.append(quant) to_move_quants.append(quant)
@@ -434,6 +436,26 @@ class stock_quant(osv.osv):
for quant in quants_reconcile: for quant in quants_reconcile:
self._quant_reconcile_negative(cr, uid, quant, move, context=context) self._quant_reconcile_negative(cr, uid, quant, move, context=context)
# 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(cr, uid, [('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
('location_id.usage', '=', 'internal')], context=context)
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
other_quants = self.browse(cr, uid, other_quants, context=context)
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.pool['stock.production.lot'].browse(cr, uid, lot_id, context=context).name
raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set."))
def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, lot_id = False, entire_pack=False, context=None): def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, lot_id = False, entire_pack=False, context=None):
context=context or {} context=context or {}
vals = {'location_id': location_dest_id.id, vals = {'location_id': location_dest_id.id,
@@ -458,7 +480,8 @@ class stock_quant(osv.osv):
if ops: if ops:
restrict_lot_id = lot_id restrict_lot_id = lot_id
location = ops.location_id location = ops.location_id
domain += [('owner_id', '=', ops.owner_id.id)] if ops.owner_id:
domain += [('owner_id', '=', ops.owner_id.id)]
if ops.package_id and not ops.product_id: if ops.package_id and not ops.product_id:
domain += [('package_id', 'child_of', ops.package_id.id)] domain += [('package_id', 'child_of', ops.package_id.id)]
elif ops.package_id and ops.product_id: elif ops.package_id and ops.product_id:
@@ -469,7 +492,8 @@ class stock_quant(osv.osv):
else: else:
restrict_lot_id = move.restrict_lot_id.id restrict_lot_id = move.restrict_lot_id.id
location = move.location_id location = move.location_id
domain += [('owner_id', '=', move.restrict_partner_id.id)] if move.restrict_partner_id:
domain += [('owner_id', '=', move.restrict_partner_id.id)]
domain += [('location_id', 'child_of', move.location_id.id)] domain += [('location_id', 'child_of', move.location_id.id)]
if context.get('force_company'): if context.get('force_company'):
domain += [('company_id', '=', context.get('force_company'))] domain += [('company_id', '=', context.get('force_company'))]
@@ -565,16 +589,10 @@ class stock_quant(osv.osv):
negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context) negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
vals.update({'propagated_from_id': negative_quant_id}) vals.update({'propagated_from_id': negative_quant_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 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 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: if qty != 1.0:
raise UserError(_('You should only receive by the piece with the same serial number')) raise UserError(_('You should only receive by the piece with the same serial number'))
other_quants = self.search(cr, uid, [('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
('qty', '>', 0.0), ('location_id.usage', '=', 'internal')], context=context)
if other_quants:
lot_name = self.pool['stock.production.lot'].browse(cr, uid, lot_id, context=context).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 #create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
quant_id = self.create(cr, SUPERUSER_ID, vals, context=context) quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
@@ -2461,7 +2479,7 @@ class stock_move(osv.osv):
rounding = ops.product_id.uom_id.rounding rounding = ops.product_id.uom_id.rounding
for pack_lot in ops.pack_lot_ids: for pack_lot in ops.pack_lot_ids:
lot_qty[pack_lot.lot_id.id] = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, pack_lot.qty, ops.product_id.uom_id.id) lot_qty[pack_lot.lot_id.id] = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, pack_lot.qty, ops.product_id.uom_id.id)
for record in ops.linked_move_operation_ids: for record in ops.linked_move_operation_ids.filtered(lambda x: x.move_id.id in main_domain):
move_qty = record.qty move_qty = record.qty
move = record.move_id move = record.move_id
domain = main_domain[move.id] domain = main_domain[move.id]
+1
View File
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import models # noqa
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import stock_dropshipping # noqa
@@ -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)
+2 -2
View File
@@ -640,8 +640,8 @@ class Database(http.Controller):
try: try:
# country code could be = "False" which is actually True in python # country code could be = "False" which is actually True in python
country_code = post.get('country_code') or False 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.proxy("db").create_database(master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code)
request.session.authenticate(name, 'admin', password) request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/') return http.local_redirect('/web/')
except Exception, e: except Exception, e:
error = "Database creation error: %s" % e error = "Database creation error: %s" % e
@@ -1565,7 +1565,7 @@ var FieldStatus = common.AbstractField.extend({
}); });
} }
} }
}, 300), }, 300, true),
}); });
var FieldMonetary = FieldFloat.extend({ var FieldMonetary = FieldFloat.extend({
@@ -679,7 +679,6 @@ var PivotView = View.extend({
if (width > 1) { if (width > 1) {
var total_cell = {width:nbr_measures, height: depth, title:""}; var total_cell = {width:nbr_measures, height: depth, title:""};
if (nbr_measures === 1) { if (nbr_measures === 1) {
total_cell.title = this.measures[this.active_measures[0]].string;
total_cell.total = true; total_cell.total = true;
} }
result[0].push(total_cell); result[0].push(total_cell);
+1 -1
View File
@@ -69,7 +69,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label for="login" class="control-label">Email</label> <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> </div>
</div> </div>
+42 -31
View File
@@ -40,7 +40,10 @@ var Tips = Class.extend({
on_view: function() { on_view: function() {
var self = this; 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 action_id = self.view.ViewManager.action ? self.view.ViewManager.action.id : null;
var model = self.view.fields_view.model; var model = self.view.fields_view.model;
var mode = self.view.fields_view.type; var mode = self.view.fields_view.type;
@@ -49,14 +52,17 @@ var Tips = Class.extend({
self.on_form_view(action_id, model); self.on_form_view(action_id, model);
} }
else if (self.view.hasOwnProperty('editor')) { else if (self.view.hasOwnProperty('editor')) {
self.view.on('view_list_rendered', self, function() { if (self._view_list_rendered) {
self.eval_tips(action_id, model, mode); 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 { else {
self.eval_tips(action_id, model, mode); self.eval_tips(action_id, model, mode);
} }
}); };
bus.on('DOM_updated', this, this._DOM_updated);
}, },
on_form_view: function(action_id, model) { 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 var type = this.view.datarecord ? this.view.datarecord.type : null
if ($('.oe_chatter').length > 0) { if ($('.oe_chatter').length > 0) {
this.view.on('chatter_messages_displayed', this, function() { if (this._chatter_messages_displayed) {
self.eval_tips(action_id, model, 'form', type); 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 { } else {
this.eval_tips(action_id, model, 'form', type); this.eval_tips(action_id, model, 'form', type);
} }
this.view.on('to_edit_mode', this, function() { if (this._to_edit_mode) {
self.remove_tips(); this.view.off('to_edit_mode', this, this._to_edit_mode);
}); }
this.view.on('to_view_mode', this, function() { this._to_edit_mode = function() { self.remove_tips(); };
self.eval_tips(action_id, model, 'form', type); 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) { eval_tips: function(action_id, model, mode, type) {
@@ -123,11 +136,12 @@ var Tips = Class.extend({
var Tip = Class.extend({ var Tip = Class.extend({
init: function(tip) { init: function(tip) {
var self = this;
this.tip = tip; this.tip = tip;
this.highlight_selector = tip.highlight_selector; this.highlight_selector = tip.highlight_selector;
this.end_selector = tip.end_selector ? tip.end_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.triggers = tip.trigger_selector ? tip.trigger_selector.split(',') : [];
this._resize = function() { self._set_breathing_position(); };
}, },
do_tip: function() { do_tip: function() {
@@ -171,17 +185,9 @@ var Tip = Class.extend({
}); });
// resize // resize
bus.on('resize', this, function() { bus.on('resize', this, this._resize);
self._set_breathing_position(); bus.on('image_loaded', this, this._resize);
}); bus.on('please_reposition_tip', this, this._resize);
bus.on('image_loaded', this, function() {
self._set_breathing_position();
});
bus.on('please_reposition_tip', this, function() {
self._set_breathing_position();
});
return true; return true;
}, },
@@ -246,6 +252,7 @@ var Tip = Class.extend({
this.$overlay.on('click', function($ev) { this.$overlay.on('click', function($ev) {
self.end_tip(); self.end_tip();
}); });
$(document).on('keyup.web_tip', function($ev) { $(document).on('keyup.web_tip', function($ev) {
if ($ev.which === 27) { // esc if ($ev.which === 27) { // esc
self.end_tip(); self.end_tip();
@@ -253,9 +260,8 @@ var Tip = Class.extend({
}); });
// resize // resize
bus.on('resize', this, function() { this._resize_position = function() { self._set_popover_position(); };
self._set_popover_position(); bus.on('resize', this, this._resize_position);
});
}, },
scroll_to_tip: function(){ scroll_to_tip: function(){
@@ -278,11 +284,16 @@ var Tip = Class.extend({
this.$overlay.remove(); this.$overlay.remove();
this.$cross.remove(); this.$cross.remove();
_.each($('.oe_tip_fix_parent'), function(el) { $('.oe_tip_fix_parent').each(function() {
$(el).removeClass('oe_tip_fix_parent'); $(this).removeClass('oe_tip_fix_parent');
}); });
$(document).off('keyup.web_tip'); $(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], {}); Tips.call('consume', [this.tip.id], {});
this.tip.is_consumed = true; this.tip.is_consumed = true;
}, },
+3
View File
@@ -666,6 +666,9 @@ class res_partner(osv.osv):
'zoom': zoom, 'zoom': zoom,
'sensor': 'false', '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) return urlplus('//maps.googleapis.com/maps/api/staticmap' , params)
def google_map_link(self, cr, uid, ids, zoom=10, context=None): def google_map_link(self, cr, uid, ids, zoom=10, context=None):
+8 -1
View File
@@ -908,7 +908,14 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
"fields": ["name", "address", "phone", "mobile", "fax", "email"]}'/> "fields": ["name", "address", "phone", "mobile", "fax", "email"]}'/>
</address> </address>
<!-- FORWARD-PORT UP TO SAAS-12 --> <!-- 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>
<template id="contactus" name="Contact us" page="True"> <template id="contactus" name="Contact us" page="True">
@@ -119,6 +119,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage):
partners = partners[pager['offset']:pager['offset'] + self._references_per_page] partners = partners[pager['offset']:pager['offset'] + self._references_per_page]
google_map_partner_ids = ','.join(map(str, [p.id for p in partners])) 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 = { values = {
'countries': countries, 'countries': countries,
@@ -130,6 +131,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage):
'pager': pager, 'pager': pager,
'searches': post, 'searches': post,
'search_path': "%s" % werkzeug.url_encode(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) return request.render("website_crm_partner_assign.index", values, status=partners and 200 or 404)
@@ -119,25 +119,27 @@
<template id="ref_country" inherit_id="website_crm_partner_assign.index" customize_show="True" name="Left World Map"> <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"> <xpath expr="//ul[@id='reseller_countries']" position="after">
<!-- modal for large map --> <t t-if="google_maps_api_key">
<div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> <!-- modal for large map -->
<div class="modal-dialog modal-lg"> <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="modal-header"> <div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <div class="modal-header">
<h4 class="modal-title">World Map</h4> <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> </div>
<iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/partners/" </div>
style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div> </div>
</div> <!-- modal end -->
</div> <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3>
<!-- modal end --> <ul class="nav">
<h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> <iframe t-attf-src="/google_map?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/partners/"
<ul class="nav"> style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe>
<iframe t-attf-src="/google_map?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/partners/" </ul>
style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> </t>
</ul>
</xpath> </xpath>
</template> </template>
@@ -85,6 +85,7 @@ class WebsiteCustomer(http.Controller):
context=request.context) context=request.context)
google_map_partner_ids = ','.join(map(str, partner_ids)) google_map_partner_ids = ','.join(map(str, partner_ids))
partners = partner_obj.browse(request.cr, openerp.SUPERUSER_ID, partner_ids, request.context) 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_obj = request.registry['res.partner.tag']
tag_ids = tag_obj.search(cr, uid, [('website_published', '=', True), ('partner_ids', 'in', partner_ids)], 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), 'search_path': "?%s" % werkzeug.url_encode(post),
'tag': tag, 'tag': tag,
'tags': tags, 'tags': tags,
'google_maps_api_key': google_maps_api_key,
} }
return request.website.render("website_customer.index", values) return request.website.render("website_customer.index", values)
@@ -68,25 +68,27 @@
<!-- Option: left column: World Map --> <!-- Option: left column: World Map -->
<template id="opt_country" inherit_id="website_customer.index" customize_show="True" name="Show 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"> <xpath expr="//div[@id='ref_left_column']" position="inside">
<!-- modal for large map --> <t t-if="google_maps_api_key">
<div class="modal fade customer_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> <!-- modal for large map -->
<div class="modal-dialog modal-lg"> <div class="modal fade customer_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="modal-header"> <div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <div class="modal-header">
<h4 class="modal-title">World Map</h4> <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> </div>
<iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/customers/" </div>
style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div> </div>
</div> <!-- modal end -->
</div> <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".customer_map_modal"><span class="fa fa-external-link" /></button></h3>
<!-- modal end --> <ul class="nav">
<h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".customer_map_modal"><span class="fa fa-external-link" /></button></h3> <iframe t-attf-src="/google_map?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/customers/"
<ul class="nav"> style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe>
<iframe t-attf-src="/google_map?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/customers/" </ul>
style="width:260px; height:240px; border:0; padding:0; margin:0;" scrolling="no"></iframe> </t>
</ul>
</xpath> </xpath>
</template> </template>
+8 -1
View File
@@ -332,7 +332,14 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<!-- FORWARD-PORT UP TO SAAS-12 --> <!-- 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='{ <div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-field-options='{
"widget": "contact", "widget": "contact",
"fields": ["address", "phone", "mobile", "fax", "email"] "fields": ["address", "phone", "mobile", "fax", "email"]
@@ -52,8 +52,10 @@ class GoogleMap(http.Controller):
else: else:
partner_url = '/partners/' partner_url = '/partners/'
google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('google_maps_api_key')
values = { values = {
'partner_url': partner_url, '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) return request.website.render("website_google_map.google_map", values)
@@ -14,7 +14,13 @@
var odoo_partner_data = <t t-raw="partner_data"/>; var odoo_partner_data = <t t-raw="partner_data"/>;
</script> </script>
<div id="odoo-google-map"></div> <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/lib/markerclusterer.js"></script>
<script type="text/javascript" src="/website_google_map/static/src/js/website_google_map.js"></script> <script type="text/javascript" src="/website_google_map/static/src/js/website_google_map.js"></script>
</body> </body>
@@ -138,6 +138,7 @@ class WebsiteMembership(http.Controller):
count_members += len(free_partner_ids) count_members += len(free_partner_ids)
google_map_partner_ids = ",".join(map(str, google_map_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)} 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, 'pager': pager,
'post': post, 'post': post,
'search': "?%s" % werkzeug.url_encode(post), 'search': "?%s" % werkzeug.url_encode(post),
'google_maps_api_key': google_maps_api_key,
} }
return request.website.render("website_membership.index", values) return request.website.render("website_membership.index", values)
@@ -104,25 +104,27 @@
<template id="opt_index_google_map" name="Left World Map" <template id="opt_index_google_map" name="Left World Map"
customize_show="True" inherit_id="website_membership.index"> customize_show="True" inherit_id="website_membership.index">
<xpath expr="//div[@id='left_column']/ul[last()]" position="after"> <xpath expr="//div[@id='left_column']/ul[last()]" position="after">
<!-- modal for large map --> <t t-if="google_maps_api_key">
<div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true"> <!-- modal for large map -->
<div class="modal-dialog modal-lg"> <div class="modal fade partner_map_modal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
<div class="modal-content"> <div class="modal-dialog modal-lg">
<div class="modal-header"> <div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <div class="modal-header">
<h4 class="modal-title">World Map</h4> <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> </div>
<iframe t-attf-src="/google_map/?width=898&amp;height=485&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/members/" </div>
style="width:898px; height:485px; border:0; padding:0; margin:0;"></iframe>
</div> </div>
</div> <!-- modal end -->
</div> <h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3>
<!-- modal end --> <ul class="nav">
<h3>World Map<button class="btn btn-link" data-toggle="modal" data-target=".partner_map_modal"><span class="fa fa-external-link" /></button></h3> <iframe t-attf-src="/google_map/?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/members/"
<ul class="nav"> style="width:260px; height:240px; border:0; padding:0; margin:0;"></iframe>
<iframe t-attf-src="/google_map/?width=260&amp;height=240&amp;partner_ids=#{ google_map_partner_ids }&amp;partner_url=/members/" </ul>
style="width:260px; height:240px; border:0; padding:0; margin:0;"></iframe> </t>
</ul>
</xpath> </xpath>
</template> </template>
+2 -1
View File
@@ -134,7 +134,8 @@ class website_sale(http.Controller):
def _get_search_order(self, post): def _get_search_order(self, post):
# OrderBy will be parsed in orm and so no direct sql injection # 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) @http.route(['/shop/change_pricelist/<model("product.pricelist"):pl_id>'], type='http', auth="public", website=True)
def pricelist_change(self, pl_id, **post): def pricelist_change(self, pl_id, **post):
+1 -1
View File
@@ -775,7 +775,7 @@
</div> </div>
</td> </td>
<td class="text-center" id="td-price" name="price"> <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='{ t-field="line.product_id.lst_price" t-field-options='{
"widget": "monetary", "widget": "monetary",
"from_currency": "line.company_id.currency_id", "from_currency": "line.company_id.currency_id",
+184 -16
View File
@@ -23,11 +23,11 @@ Although the database will follow the same process described on that form.
The required steps are: The required steps are:
* creating a request * :ref:`creating a request <upgrade-api-create-method>`
* uploading a database dump * :ref:`uploading a database dump <upgrade-api-upload-method>`
* running the upgrade process * :ref:`running the upgrade process <upgrade-api-process-method>`
* obtaining the status of the database request * :ref:`obtaining the status of the database request <upgrade-api-status-method>`
* downloading the upgraded database dump * :ref:`downloading the upgraded database dump <upgrade-api-download-method>`
The methods The methods
~~~~~~~~~~~ ~~~~~~~~~~~
@@ -60,9 +60,11 @@ The ``create`` method
:param str filename: (required) a purely informative name for you database dump file :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 :param str timezone: (optional) the timezone used by your server. Only for Odoo source version < 6.1
:return: request result :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`` ``failures``
'''''''''''' ''''''''''''
@@ -201,11 +203,17 @@ Here are 2 examples of database upgrade request creation using:
Uploading your database dump 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 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 .. py:function:: https://upgrade.odoo.com/database/v1/upload
Uploads a database dump Uploads a database dump
@@ -213,12 +221,12 @@ The ``upload`` method
:param str key: (required) your private key :param str key: (required) your private key
:param str request: (required) your request id :param str request: (required) your request id
:return: request result :return: request result
:rtype: json dictionary :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method The request id and the private key are obtained using the :ref:`create method
<upgrade-api-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. should be empty if everything went fine.
.. rst-class:: setup doc-aside .. rst-class:: setup doc-aside
@@ -230,8 +238,6 @@ should be empty if everything went fine.
import os import os
import pycurl import pycurl
from urllib import urlencode from urllib import urlencode
from io import BytesIO
import json
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload" UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload"
DUMPFILE = "openchs.70.cdump" DUMPFILE = "openchs.70.cdump"
@@ -267,6 +273,144 @@ should be empty if everything went fine.
HEADER="Content-Type: application/octet-stream" HEADER="Content-Type: application/octet-stream"
curl -H $HEADER --data-binary "@${DUMPFILE}" "${UPLOAD_URL}?${URL_PARAMS}" 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: .. _upgrade-api-process-method:
Asking to process your request Asking to process your request
@@ -284,12 +428,12 @@ The ``process`` method
:param str key: (required) your private key :param str key: (required) your private key
:param str request: (required) your request id :param str request: (required) your request id
:return: request result :return: request result
:rtype: json dictionary :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method The request id and the private key are obtained using the :ref:`create method
<upgrade-api-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. should be empty if everything went fine.
.. rst-class:: setup doc-aside .. rst-class:: setup doc-aside
@@ -350,12 +494,12 @@ The ``status`` method
:param str key: (required) your private key :param str key: (required) your private key
:param str request: (required) your request id :param str request: (required) your request id
:return: request result :return: request result
:rtype: json dictionary :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method The request id and the private key are obtained using the :ref:`create method
<upgrade-api-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. database upgrade request.
.. rst-class:: setup doc-aside .. 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
+9 -2
View File
@@ -15,6 +15,7 @@ import json
import werkzeug import werkzeug
import HTMLParser import HTMLParser
from lxml import etree from lxml import etree
from lxml.etree import LxmlError
import openerp import openerp
from openerp import tools, api from openerp import tools, api
@@ -341,7 +342,13 @@ class view(osv.osv):
if values.get('inherit_id'): if values.get('inherit_id'):
values['type'] = self.browse(cr, uid, values['inherit_id'], context).type values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
else: 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'): if not values.get('name'):
values['name'] = "%s %s" % (values.get('model'), values['type']) values['name'] = "%s %s" % (values.get('model'), values['type'])
@@ -360,7 +367,7 @@ class view(osv.osv):
# If view is modified we remove the arch_fs information thus activating the arch_db # 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 # 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 context: if ('arch' in vals or 'arch_base' in vals) and 'install_mode_data' not in context:
vals['arch_fs'] = False vals['arch_fs'] = False
# drop the corresponding view customizations (used for dashboards for example), otherwise # drop the corresponding view customizations (used for dashboards for example), otherwise
+11 -3
View File
@@ -21,11 +21,18 @@ class test_guess_mimetype(unittest.TestCase):
def test_default_mimetype_empty(self): def test_default_mimetype_empty(self):
mimetype = guess_mimetype('') 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): def test_default_mimetype(self):
mimetype = guess_mimetype('', default='test') 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): def test_mimetype_png(self):
content = base64.b64decode(PNG) content = base64.b64decode(PNG)
@@ -35,7 +42,8 @@ class test_guess_mimetype(unittest.TestCase):
def test_mimetype_bmp(self): def test_mimetype_bmp(self):
content = base64.b64decode(BMP) content = base64.b64decode(BMP)
mimetype = guess_mimetype(content, default='test') 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): def test_mimetype_jpg(self):
content = base64.b64decode(JPG) content = base64.b64decode(JPG)
+2 -2
View File
@@ -153,9 +153,9 @@ else:
# magic from pypi https://pypi.python.org/pypi/python-magic/ # magic from pypi https://pypi.python.org/pypi/python-magic/
if hasattr(magic,'from_buffer'): 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 # magic from file(1) https://packages.debian.org/squeeze/python-magic
elif hasattr(magic,'open'): elif hasattr(magic,'open'):
ms = magic.open(magic.MAGIC_MIME_TYPE) ms = magic.open(magic.MAGIC_MIME_TYPE)
ms.load() ms.load()
guess_mimetype = ms.buffer guess_mimetype = lambda bin_data, default=None: ms.buffer(bin_data)