[MERGE] forward port of branch saas-6 up to e9f1ee4
This commit is contained in:
@@ -60,8 +60,8 @@
|
||||
<th>Description</th>
|
||||
<th class="text-right">Quantity</th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
<th t-if="display_discount" groups="sale.group_discount_per_so_line">Disc.(%)</th>
|
||||
<th>Taxes</th>
|
||||
<th t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">Disc.(%)</th>
|
||||
<th class="text-right">Taxes</th>
|
||||
<th class="text-right">Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import datetime as DT
|
||||
import dateutil
|
||||
import time
|
||||
import logging
|
||||
|
||||
@@ -121,7 +123,10 @@ class base_action_rule(osv.osv):
|
||||
""" Prepare the context used when evaluating python code
|
||||
:returns: dict -- evaluation context given to (safe_)eval """
|
||||
return {
|
||||
'datetime': DT,
|
||||
'dateutil': dateutil,
|
||||
'time': time,
|
||||
'uid': uid,
|
||||
'user': self.pool['res.users'].browse(cr, uid, uid, context=context),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from os.path import join as opj
|
||||
import openerp
|
||||
from openerp import fields
|
||||
from openerp.osv import osv
|
||||
from openerp.tools import convert_file
|
||||
from openerp.tools import convert_file, exception_to_unicode
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
|
||||
@@ -105,7 +105,7 @@ class ir_module_module(osv.osv):
|
||||
self.import_module(cr, uid, mod_name, path, force=force, context=context)
|
||||
success.append(mod_name)
|
||||
except Exception, e:
|
||||
errors[mod_name] = str(e)
|
||||
errors[mod_name] = exception_to_unicode(e)
|
||||
r = ["Successfully imported module '%s'" % mod for mod in success]
|
||||
for mod, error in errors.items():
|
||||
r.append("Error while importing module '%s': %r" % (mod, error))
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import test_new_lead_notification
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from openerp.tests.common import TransactionCase
|
||||
|
||||
class NewLeadNotificationTest(TransactionCase):
|
||||
@unittest.skip("Broken in 9.0. auto_subscribe does not handle channels")
|
||||
def test_new_lead_notification(self):
|
||||
# Create a new user
|
||||
groups = [
|
||||
self.env.ref('base.group_user').id,
|
||||
self.env.ref('base.group_sale_salesman').id,
|
||||
]
|
||||
user = self.env["res.users"].create({
|
||||
"name": __file__,
|
||||
"login": __file__,
|
||||
'groups_id': [(6, 0, groups)],
|
||||
})
|
||||
|
||||
chan = self.env['mail.channel'].create({
|
||||
'name': 'Follow leads',
|
||||
'group_ids': [(6, 0, groups)],
|
||||
})
|
||||
|
||||
# Subscribe him to sales department
|
||||
team = self.env.ref("sales_team.team_sales_department")
|
||||
subtype = self.env.ref("crm.mt_salesteam_lead")
|
||||
team.message_subscribe(channel_ids=[chan.id], subtype_ids=[subtype.id])
|
||||
|
||||
# Imitate what happens in the controller when somebody creates a new
|
||||
# lead from the website form
|
||||
lead = (self.env["crm.lead"]
|
||||
.with_context(mail_create_nosubscribe=True)
|
||||
.sudo()
|
||||
.create({"contact_name": "Somebody",
|
||||
"description": "Some question",
|
||||
"email_from": "somemail@example.com",
|
||||
"name": "Some subject",
|
||||
"partner_name": "Some company",
|
||||
"team_id": team.id,
|
||||
"phone": "+0000000000"}))
|
||||
|
||||
# The user should have a new unread message
|
||||
self.assertTrue(lead.sudo(user).message_unread)
|
||||
@@ -12,7 +12,7 @@ from urlparse import urlparse
|
||||
|
||||
from openerp import models, fields, api, _
|
||||
|
||||
URL_REGEX = r'(\bhref=[\'"]([^\'"]+)[\'"])'
|
||||
URL_REGEX = r'(\bhref=[\'"](?!mailto:)([^\'"]+)[\'"])'
|
||||
|
||||
def VALIDATE_URL(url):
|
||||
if urlparse(url).scheme not in ('http', 'https', 'ftp', 'ftps'):
|
||||
|
||||
@@ -5,6 +5,8 @@ import logging
|
||||
from email.utils import formataddr
|
||||
from urlparse import urljoin
|
||||
|
||||
import psycopg2
|
||||
|
||||
from openerp import _, api, fields, models
|
||||
from openerp import tools
|
||||
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
|
||||
@@ -285,6 +287,12 @@ class MailMail(models.Model):
|
||||
'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
|
||||
mail.id, mail.message_id)
|
||||
raise
|
||||
except psycopg2.Error:
|
||||
# If an error with the database occurs, chances are that the cursor is unusable.
|
||||
# This will lead to an `psycopg2.InternalError` being raised when trying to write
|
||||
# `state`, shadowing the original exception and forbid a retry on concurrent
|
||||
# update. Let's bubble it.
|
||||
raise
|
||||
except Exception as e:
|
||||
failure_reason = tools.ustr(e)
|
||||
_logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
|
||||
|
||||
@@ -201,6 +201,13 @@ class MailThread(models.AbstractModel):
|
||||
doc_name = self.env['ir.model'].search([('model', '=', self._name)]).read(['name'])[0]['name']
|
||||
thread.message_post(body=_('%s created') % doc_name)
|
||||
|
||||
# auto_subscribe: take values and defaults into account
|
||||
create_values = dict(values)
|
||||
for key, val in self._context.iteritems():
|
||||
if key.startswith('default_') and key[8:] not in create_values:
|
||||
create_values[key[8:]] = val
|
||||
thread.message_auto_subscribe(create_values.keys(), values=create_values)
|
||||
|
||||
# track values
|
||||
if not self._context.get('mail_notrack'):
|
||||
if 'lang' not in self._context:
|
||||
@@ -212,13 +219,6 @@ class MailThread(models.AbstractModel):
|
||||
initial_values = {thread.id: dict.fromkeys(tracked_fields, False)}
|
||||
track_thread.message_track(tracked_fields, initial_values)
|
||||
|
||||
# auto_subscribe: take values and defaults into account
|
||||
create_values = dict(values)
|
||||
for key, val in self._context.iteritems():
|
||||
if key.startswith('default_') and key[8:] not in create_values:
|
||||
create_values[key[8:]] = val
|
||||
thread.message_auto_subscribe(create_values.keys(), values=create_values)
|
||||
|
||||
return thread
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
import urlparse
|
||||
import re
|
||||
import werkzeug.urls
|
||||
@@ -79,6 +80,10 @@ class MailMail(osv.Model):
|
||||
domain = self.pool.get("ir.config_parameter").get_param(cr, uid, "web.base.url", context=context)
|
||||
base = "<base href='%s'>" % domain
|
||||
body = tools.append_content_to_html(base, body, plaintext=False, container_tag='div')
|
||||
# resolve relative image url to absolute for outlook.com
|
||||
def _sub_relative2absolute(match):
|
||||
return match.group(1) + urlparse.urljoin(domain, match.group(2)) + '"'
|
||||
body = re.sub('(<img(?=\s)[^>]*\ssrc=")(/[^/][^"]+)"', _sub_relative2absolute, body)
|
||||
|
||||
# generate tracking URL
|
||||
if mail.statistics_ids:
|
||||
|
||||
@@ -46,18 +46,21 @@ class product_uom(osv.osv):
|
||||
def name_create(self, cr, uid, name, context=None):
|
||||
""" The UoM category and factor are required, so we'll have to add temporary values
|
||||
for imported UoMs """
|
||||
if not context:
|
||||
context = {}
|
||||
uom_categ = self.pool.get('product.uom.categ')
|
||||
values = {self._rec_name: name, 'factor': 1}
|
||||
# look for the category based on the english name, i.e. no context on purpose!
|
||||
# TODO: should find a way to have it translated but not created until actually used
|
||||
categ_misc = 'Unsorted/Imported Units'
|
||||
categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
|
||||
if categ_id:
|
||||
categ_id = categ_id[0]
|
||||
else:
|
||||
categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
|
||||
uom_id = self.create(cr, uid, {self._rec_name: name,
|
||||
'category_id': categ_id,
|
||||
'factor': 1})
|
||||
if not context.get('default_category_id'):
|
||||
categ_misc = 'Unsorted/Imported Units'
|
||||
categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
|
||||
if categ_id:
|
||||
values['category_id'] = categ_id[0]
|
||||
else:
|
||||
values['category_id'] = uom_categ.name_create(
|
||||
cr, uid, categ_misc, context=context)[0]
|
||||
uom_id = self.create(cr, uid, values, context=context)
|
||||
return self.name_get(cr, uid, [uom_id], context=context)[0]
|
||||
|
||||
def create(self, cr, uid, data, context=None):
|
||||
|
||||
@@ -127,5 +127,12 @@
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="report_project_task_user_report_comp_rule">
|
||||
<field name="name">Task Analysis multi-company</field>
|
||||
<field name="model_id" ref="model_report_project_task_user"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -71,7 +71,7 @@ class PurchaseOrder(models.Model):
|
||||
@api.model
|
||||
def _default_picking_type(self):
|
||||
type_obj = self.env['stock.picking.type']
|
||||
company_id = self.env.user.company_id.id
|
||||
company_id = self.env.context.get('company_id') or self.env.user.company_id.id
|
||||
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
|
||||
if not types:
|
||||
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
|
||||
|
||||
@@ -18,6 +18,7 @@ from report_helper import WebKitHelper
|
||||
import openerp
|
||||
from openerp.modules.module import get_module_resource
|
||||
from openerp.report.report_sxw import *
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import tools
|
||||
from openerp.tools.translate import _
|
||||
from urllib import urlencode, quote as quote
|
||||
@@ -102,7 +103,7 @@ class WebKitParser(report_sxw):
|
||||
def get_lib(self, cursor, uid):
|
||||
"""Return the lib wkhtml path"""
|
||||
proxy = self.pool['ir.config_parameter']
|
||||
webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
|
||||
webkit_path = proxy.get_param(cursor, SUPERUSER_ID, 'webkit_path')
|
||||
|
||||
if not webkit_path:
|
||||
try:
|
||||
|
||||
@@ -29,7 +29,7 @@ var GraphView = View.extend({
|
||||
this.widget = undefined;
|
||||
},
|
||||
start: function () {
|
||||
var load_fields = this.model.call('fields_get', [])
|
||||
var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()})
|
||||
.then(this.prepare_fields.bind(this));
|
||||
|
||||
return $.when(this._super(), load_fields);
|
||||
|
||||
@@ -73,7 +73,7 @@ var PivotView = View.extend({
|
||||
start: function () {
|
||||
this.$table_container = this.$('.o-pivot-table');
|
||||
|
||||
var load_fields = this.model.call('fields_get', [])
|
||||
var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()})
|
||||
.then(this.prepare_fields.bind(this));
|
||||
|
||||
return $.when(this._super(), load_fields).then(this.render_field_selection.bind(this));
|
||||
|
||||
@@ -163,7 +163,7 @@ class ir_http(orm.AbstractModel):
|
||||
if path[1] == request.website.default_lang_code:
|
||||
request.context['edit_translations'] = False
|
||||
if not request.context.get('tz'):
|
||||
request.context['tz'] = request.session['geoip'].get('time_zone')
|
||||
request.context['tz'] = request.session.get('geoip', {}).get('time_zone')
|
||||
# bind modified context
|
||||
request.website = request.website.with_context(request.context)
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ hr {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.jumbotron.para_large p{
|
||||
.jumbotron.para_large p {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ body > nav.navbar {
|
||||
}
|
||||
}
|
||||
/* ----- BOOTSTRAP FIX ----- */
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.table .img-responsive {
|
||||
width: 100%;
|
||||
@@ -753,6 +752,10 @@ div.carousel .item.text_only .container > .carousel-image {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* ---- footer style ---- {{{ */
|
||||
#footer div.row {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.container nav#oe_main_menu_navbar {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ hr
|
||||
.para_large
|
||||
font-size: 120%
|
||||
|
||||
.jumbotron.para_large p{
|
||||
font-size: 150%;
|
||||
}
|
||||
.jumbotron.para_large p
|
||||
font-size: 150%
|
||||
|
||||
.readable
|
||||
font-size: 120%
|
||||
@@ -616,6 +615,10 @@ div.carousel
|
||||
.navbar-nav.navbar-right:last-child
|
||||
margin-right: 0 !important
|
||||
|
||||
/* ---- footer style ---- {{{ */
|
||||
#footer div.row
|
||||
word-wrap: break-word
|
||||
|
||||
.container
|
||||
nav#oe_main_menu_navbar
|
||||
position: inherit
|
||||
|
||||
@@ -382,13 +382,13 @@
|
||||
<xpath expr="//div[@id='footer']" position="replace">
|
||||
<div id="footer" class="container hidden-print">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<h4>Our products & Services</h4>
|
||||
<ul class="list-unstyled" id="products">
|
||||
<li><a href="/">Home</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3" id="info">
|
||||
<div class="col-md-4 col-lg-3" id="info">
|
||||
<h4>Connect with us</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/page/website.contactus">Contact us</a></li>
|
||||
@@ -406,7 +406,7 @@
|
||||
<a t-att-href="website.social_github" t-if="website.social_github"><i class="fa fa-github"/></a>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-offset-1">
|
||||
<div class="col-md-4 col-lg-5 col-lg-offset-1">
|
||||
<h4>
|
||||
<span t-field="res_company.name" />
|
||||
<small> - <a href="/page/website.aboutus">About us</a></small>
|
||||
|
||||
@@ -217,7 +217,9 @@ class sale_order(osv.osv):
|
||||
options = []
|
||||
for option in quote_template.options:
|
||||
if pricelist_id:
|
||||
price = pricelist_obj.price_get(cr, uid, [pricelist_id], option.product_id.id, 1, context=context)[pricelist_id]
|
||||
uom_context = context.copy()
|
||||
uom_context['uom'] = option.uom_id.id
|
||||
price = pricelist_obj.price_get(cr, uid, [pricelist_id], option.product_id.id, 1, context=uom_context)[pricelist_id]
|
||||
else:
|
||||
price = option.price_unit
|
||||
options.append((0, 0, {
|
||||
@@ -405,6 +407,10 @@ class sale_order_option(osv.osv):
|
||||
if product.description_sale:
|
||||
self.name += '\n' + product.description_sale
|
||||
self.uom_id = product.product_tmpl_id.uom_id
|
||||
if product and self.order_id.pricelist_id:
|
||||
partner_id = self.order_id.partner_id.id
|
||||
pricelist = self.order_id.pricelist_id.id
|
||||
self.price_unit = self.order_id.pricelist_id.price_get(product.id, self.quantity, partner_id)[pricelist]
|
||||
|
||||
|
||||
class product_template(osv.Model):
|
||||
|
||||
@@ -803,13 +803,16 @@ class website_sale(http.Controller):
|
||||
# find an already existing transaction
|
||||
tx = request.website.sale_get_transaction()
|
||||
if tx:
|
||||
if tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
|
||||
tx_id = tx.id
|
||||
if tx.reference != order.name:
|
||||
tx = False
|
||||
tx_id = False
|
||||
elif tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
|
||||
tx.write({
|
||||
'acquirer_id': acquirer_id,
|
||||
'amount': order.amount_total,
|
||||
})
|
||||
tx_id = tx.id
|
||||
else:
|
||||
if not tx:
|
||||
tx_id = transaction_obj.create(cr, SUPERUSER_ID, {
|
||||
'acquirer_id': acquirer_id,
|
||||
'type': 'form',
|
||||
|
||||
@@ -93,9 +93,9 @@ $('.oe_website_sale').each(function () {
|
||||
|
||||
$(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) {
|
||||
var product_ids = [];
|
||||
var product_dom = $("ul.js_add_cart_variants[data-attribute_value_ids]").first();
|
||||
var product_dom = $(".js_add_cart_variants[data-attribute_value_ids]").first();
|
||||
product_dom.data("attribute_value_ids").forEach(function(entry) {
|
||||
product_ids.push(entry[0]);});
|
||||
product_ids.push(entry);});
|
||||
var qty = $(event.target).closest('form').find('input[name="add_qty"]').val();
|
||||
|
||||
if ($("#product_detail").length) {
|
||||
@@ -241,7 +241,7 @@ $('.oe_website_sale').each(function () {
|
||||
});
|
||||
|
||||
$(oe_website_sale).on('change', 'input.js_variant_change, select.js_variant_change, ul[data-attribute_value_ids]', function (ev) {
|
||||
var $ul = $(ev.target).closest('ul.js_add_cart_variants');
|
||||
var $ul = $(ev.target).closest('.js_add_cart_variants');
|
||||
var $parent = $ul.closest('.js_product');
|
||||
var $product_id = $parent.find('input.product_id').first();
|
||||
var $price = $parent.find(".oe_price:first .oe_currency_value");
|
||||
@@ -312,7 +312,7 @@ $('.oe_website_sale').each(function () {
|
||||
$('input.js_product_change', this).first().trigger('change');
|
||||
});
|
||||
|
||||
$('ul.js_add_cart_variants', oe_website_sale).each(function () {
|
||||
$('.js_add_cart_variants', oe_website_sale).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
<h1 itemprop="name" t-field="product.name">Product Name</h1>
|
||||
<span itemprop="url" style="display:none;" t-esc="'/shop/product/%s' % slug(product)"/>
|
||||
|
||||
<form t-att-action="keep('/shop/cart/update')" class="js_add_cart_variants" method="POST">
|
||||
<form t-att-action="keep('/shop/cart/update')" class="js_add_cart_variants" t-att-data-attribute_value_ids="product.product_variant_ids.ids" method="POST">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="js_product">
|
||||
<t t-placeholder="select">
|
||||
|
||||
@@ -6,8 +6,8 @@ var website = require('website.website');
|
||||
var base = require('web_editor.base');
|
||||
require('website_sale.website_sale');
|
||||
|
||||
if(!$('ul.js_add_cart_variants[data-attribute_value_ids]').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain 'ul.js_add_cart_variants[data-attribute_value_ids]'");
|
||||
if(!$('.js_add_cart_variants[data-attribute_value_ids]').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.js_add_cart_variants[data-attribute_value_ids]'");
|
||||
}
|
||||
|
||||
$('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
@@ -71,7 +71,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
});
|
||||
|
||||
$modal.find('input[name="add_qty"]').val(quantity).change();
|
||||
$('ul.js_add_cart_variants').each(function () {
|
||||
$('.js_add_cart_variants').each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
var qty = $dom.find('input[name="add_qty"]').val();
|
||||
var product_ids = [product_id];
|
||||
var $products_dom = [];
|
||||
$modal.find("ul.js_add_cart_variants[data-attribute_value_ids]").each(function(){
|
||||
$modal.find(".js_add_cart_variants[data-attribute_value_ids]").each(function(){
|
||||
var $el = $(this);
|
||||
$products_dom.push($el);
|
||||
_.each($el.data("attribute_value_ids"), function (values) {
|
||||
|
||||
@@ -152,7 +152,7 @@ var PDFSlidesViewer = (function(){
|
||||
}
|
||||
|
||||
PDFSlidesViewer.prototype.toggleFullScreen = function(){
|
||||
var el = this.canvas;
|
||||
var el = this.canvas.parentNode;
|
||||
|
||||
var isFullscreenAvailable = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled || false;
|
||||
if(isFullscreenAvailable){ // Full screen supported
|
||||
|
||||
@@ -139,10 +139,10 @@ $(document).ready(function () {
|
||||
|
||||
// switching slide with keyboard
|
||||
$(document).keydown(function (ev) {
|
||||
if (ev.keyCode === 37) {
|
||||
if (ev.keyCode === 37 || ev.keyCode === 38) {
|
||||
embedded_viewer.previous();
|
||||
}
|
||||
if (ev.keyCode === 39) {
|
||||
if (ev.keyCode === 39 || ev.keyCode === 40) {
|
||||
embedded_viewer.next();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
Spain, 2015-05-07
|
||||
|
||||
Grupo ESOC agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Nuria Pastor <n.pastor@grupoesoc.es> https://github.com/grupoesoc/
|
||||
|
||||
List of contributors:
|
||||
|
||||
Jairo Llopis <j.llopis@grupoesoc.es> https://github.com/Yajo
|
||||
@@ -0,0 +1,11 @@
|
||||
Spain, september 24th 2015
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Jairo Llopis yajo.sk8@gmail.com https://github.com/Yajo
|
||||
@@ -0,0 +1,11 @@
|
||||
Belgium, 2015-09-09
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Cédric Krier cedk@users.noreply.github.com https://github.com/cedk
|
||||
+7
-7
@@ -25,7 +25,7 @@ declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
<name> <email> https://github.com/<login>
|
||||
<name> <git_email> https://github.com/<login>
|
||||
```
|
||||
|
||||
Replacing the following placeholders:
|
||||
@@ -33,7 +33,7 @@ Signed,
|
||||
* `<country>`: your country
|
||||
* `<date>`: current date in the form `YYYY-MM-DD`
|
||||
* `<name>`: your name
|
||||
* `<email>`: your email
|
||||
* `<git_email>`: your git committer email **(use `git config user.email` to see it)**
|
||||
* `<login>`: your GitHub login
|
||||
|
||||
3. An Odoo R&D Team member will verify and accept your Pull Request. You can
|
||||
@@ -61,11 +61,11 @@ declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
<name> <email> https://github.com/<login>
|
||||
<name> <git_email> https://github.com/<login>
|
||||
|
||||
List of contributors:
|
||||
|
||||
<name> <email> https://github.com/<login>
|
||||
<name> <git_email> https://github.com/<login>
|
||||
```
|
||||
|
||||
The List of contributors should list the individual contributors working
|
||||
@@ -76,14 +76,14 @@ List of contributors:
|
||||
* `<country>`: your country
|
||||
* `<date>`: current date in the form `YYYY-MM-DD`
|
||||
* `<name>`: your name
|
||||
* `<email>`: your email
|
||||
* `<git_email>`: git committer email **(use `git config user.email` to see it)**
|
||||
* `<login>`: your GitHub login
|
||||
|
||||
3. An Odoo R&D Team member will verify and accept your Pull Request. You can
|
||||
make other pull requests, but we won't be able to merge them until your CLA
|
||||
signature is merged.
|
||||
|
||||
## If you dont have a github account
|
||||
## If you don't have a github account
|
||||
|
||||
If you cannot submit your signature using a pull request, you may alternatively
|
||||
print the CLA, complete it, sign it, scan it and send it by email to
|
||||
@@ -95,7 +95,7 @@ make the pull request on your behalf.
|
||||
|
||||
# FAQ
|
||||
|
||||
## Why do i need to accept a CLA ?
|
||||
## Why do I need to accept a CLA ?
|
||||
|
||||
The goal of having a Contributor License Agreement for Odoo is to:
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ Common ORM methods
|
||||
<reference/orm/oldapi>`::
|
||||
|
||||
>>> self.browse([7, 18, 12])
|
||||
res.partner(7, 18, 12])
|
||||
res.partner(7, 18, 12)
|
||||
|
||||
:meth:`~openerp.models.Model.exists`
|
||||
Returns a new recordset containing only the records which exist in the
|
||||
|
||||
@@ -139,21 +139,14 @@ class view(osv.osv):
|
||||
result.update(map(itemgetter('res_id', 'id'), data_ids))
|
||||
return result
|
||||
|
||||
def _convert(self, cr, uid, s, view):
|
||||
matches = re.finditer('[^%]%\((.*?)\)[ds]', s)
|
||||
done = []
|
||||
for m in matches:
|
||||
found = m.group()[1:]
|
||||
if found in done:
|
||||
continue
|
||||
done.append(found)
|
||||
xmlid = m.groups()[0]
|
||||
def _resolve_external_ids(self, cr, uid, view, arch_fs):
|
||||
def replacer(m):
|
||||
xmlid = m.group('xmlid')
|
||||
if '.' not in xmlid:
|
||||
mod = view.get_external_id(cr, uid).get(view.id).split('.')[0]
|
||||
xmlid = '%s.%s' % (mod, xmlid)
|
||||
res = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xmlid)
|
||||
s = s.replace(found, str(res))
|
||||
return s
|
||||
return m.group('prefix') + str(self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xmlid))
|
||||
return re.sub('(?P<prefix>[^%])%\((?P<xmlid>.*?)\)[ds]', replacer, arch_fs)
|
||||
|
||||
def _arch_get(self, cr, uid, ids, name, arg, context=None):
|
||||
result = {}
|
||||
@@ -164,7 +157,7 @@ class view(osv.osv):
|
||||
fullpath = get_resource_path(*view.arch_fs.split('/'))
|
||||
arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
|
||||
# replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
|
||||
arch_fs = self._convert(cr, uid, arch_fs, view)
|
||||
arch_fs = arch_fs and self._resolve_external_ids(cr, uid, view, arch_fs)
|
||||
result[view.id] = arch_fs or view.arch_db
|
||||
return result
|
||||
|
||||
|
||||
@@ -11,4 +11,8 @@
|
||||
'data': ['ir.model.access.csv'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'test': [
|
||||
'tests/test_import_reference.yml',
|
||||
'tests/test_import_menuitem.yml',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
- |
|
||||
YAML Import menuitem scenario:
|
||||
Check that !menuitem import works with YAML.
|
||||
|
||||
- |
|
||||
Given a standard three level menuitem structure including:
|
||||
|
||||
A top level menu item (no parent no action)
|
||||
-
|
||||
!menuitem {
|
||||
id: test_menu_top,
|
||||
name: "Test Menu Top",
|
||||
}
|
||||
- >
|
||||
An intermediary menu item (parent, but no action)
|
||||
-
|
||||
!menuitem {
|
||||
id: test_menu_sub,
|
||||
parent: test_menu_top,
|
||||
name: "Test Menu Sub",
|
||||
}
|
||||
|
||||
- >
|
||||
A leaf menu item (parent and action)
|
||||
-
|
||||
!menuitem {
|
||||
id: test_menu_action,
|
||||
name: "Test Menu Action",
|
||||
parent: test_menu_sub,
|
||||
action: base.open_module_tree,
|
||||
}
|
||||
|
||||
- >
|
||||
Another leaf menu item (parent and action) in another menu structure with
|
||||
dotted parent id
|
||||
-
|
||||
!menuitem {
|
||||
id: test_menu_action_2,
|
||||
name: "Test Menu Action 2",
|
||||
parent: base.menu_management,
|
||||
action: base.open_module_tree,
|
||||
sequence: 93,
|
||||
}
|
||||
|
||||
- >
|
||||
Then these menu items should be present and properly configured.
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_top, string: menu item "top" is properly configured }:
|
||||
- name == 'Test Menu Top'
|
||||
- parent_id.name == False
|
||||
- action == False
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_sub, string: menu item "sub" is properly configured }:
|
||||
- name == 'Test Menu Sub'
|
||||
- parent_id.name == 'Test Menu Top'
|
||||
- action == False
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_action, string: menu item "action" is properly configured }:
|
||||
- name == 'Test Menu Action'
|
||||
- parent_id.name == 'Test Menu Sub'
|
||||
- action.name == 'Apps'
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_action_2, string: menu item "action_2" is properly configured }:
|
||||
- name == 'Test Menu Action 2'
|
||||
- sequence == 93
|
||||
- parent_id.name == 'Apps'
|
||||
- action.name == 'Apps'
|
||||
@@ -0,0 +1,44 @@
|
||||
- |
|
||||
YAML Import reference scenario:
|
||||
Check that importing into a "reference" type field works with YAML using
|
||||
both implicit references and !ref.
|
||||
|
||||
- |
|
||||
Given records imitating menu entries similar to the following entry from the base module:
|
||||
<menuitem id="menu_module_tree" parent="menu_management" name="Apps"
|
||||
sequence="5" action="open_module_tree" />
|
||||
|
||||
taken from openerp/addons/base/module/module_view.xml
|
||||
|
||||
- >
|
||||
A Menu item using !record and implicit xml_id lookup and sequence cast:
|
||||
-
|
||||
!record { model: ir.ui.menu, id: test_menu_0 }:
|
||||
name: "Local Modules 0"
|
||||
parent_id: base.menu_management
|
||||
sequence: 90
|
||||
action: base.open_module_tree
|
||||
|
||||
- >
|
||||
A Menu item using !record and explicit !ref and sequence !eval
|
||||
-
|
||||
!record { model: ir.ui.menu, id: test_menu_1 }:
|
||||
name: "Local Modules 1"
|
||||
parent_id: !refid base.menu_management
|
||||
sequence: !eval 91
|
||||
action: !refid base.open_module_tree
|
||||
- >
|
||||
Then these menu items should be present, properly configured and
|
||||
pointing to the same action.
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_0, string: menu item 0 is properly configured }:
|
||||
- name == 'Local Modules 0'
|
||||
- sequence == 90
|
||||
- parent_id.name == 'Apps'
|
||||
- action.name == 'Apps'
|
||||
-
|
||||
!assert { model: ir.ui.menu, id: test_menu_1, string: menu item 1 is properly configured }:
|
||||
- name == 'Local Modules 1'
|
||||
- sequence == 91
|
||||
- parent_id.name == 'Apps'
|
||||
- action.name == 'Apps'
|
||||
+26
-3
@@ -5847,7 +5847,19 @@ class BaseModel(object):
|
||||
if 'domain' in method_res:
|
||||
result.setdefault('domain', {}).update(method_res['domain'])
|
||||
if 'warning' in method_res:
|
||||
result['warning'] = method_res['warning']
|
||||
if result.get('warning'):
|
||||
if method_res['warning']:
|
||||
# Concatenate multiple warnings
|
||||
warning = result['warning']
|
||||
warning['message'] = '\n\n'.join(filter(None, [
|
||||
warning.get('title'),
|
||||
warning.get('message'),
|
||||
method_res['warning'].get('title'),
|
||||
method_res['warning'].get('message')
|
||||
]))
|
||||
warning['title'] = _('Warnings')
|
||||
else:
|
||||
result['warning'] = method_res['warning']
|
||||
return
|
||||
|
||||
# onchange V7
|
||||
@@ -5885,8 +5897,19 @@ class BaseModel(object):
|
||||
if 'domain' in method_res:
|
||||
result.setdefault('domain', {}).update(method_res['domain'])
|
||||
if 'warning' in method_res:
|
||||
result['warning'] = method_res['warning']
|
||||
|
||||
if result.get('warning'):
|
||||
if method_res['warning']:
|
||||
# Concatenate multiple warnings
|
||||
warning = result['warning']
|
||||
warning['message'] = '\n\n'.join(filter(None, [
|
||||
warning.get('title'),
|
||||
warning.get('message'),
|
||||
method_res['warning'].get('title'),
|
||||
method_res['warning'].get('message')
|
||||
]))
|
||||
warning['title'] = _('Warnings')
|
||||
else:
|
||||
result['warning'] = method_res['warning']
|
||||
@api.multi
|
||||
def onchange(self, values, field_name, field_onchange):
|
||||
""" Perform an onchange on the given field.
|
||||
|
||||
@@ -136,7 +136,7 @@ class YamlInterpreter(object):
|
||||
id = xml_id
|
||||
if '.' in xml_id:
|
||||
module, id = xml_id.split('.', 1)
|
||||
assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
|
||||
assert '.' not in id, "The ID reference '%s' must contain at most one dot.\n" \
|
||||
"It is used to refer to other modules ID, in the form: module.record_id" \
|
||||
% (xml_id,)
|
||||
if module != self.module:
|
||||
@@ -165,9 +165,9 @@ class YamlInterpreter(object):
|
||||
_, id = self.pool['ir.model.data'].get_object_reference(self.cr, self.uid, module, checked_xml_id)
|
||||
self.id_map[xml_id] = id
|
||||
except ValueError:
|
||||
raise ValueError("""%s not found when processing %s.
|
||||
raise ValueError("""%r not found when processing %s.
|
||||
This Yaml file appears to depend on missing data. This often happens for
|
||||
tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
|
||||
tests that belong to a module's test suite and depend on each other.""" % (xml_id, self.filename))
|
||||
|
||||
return id
|
||||
|
||||
@@ -337,7 +337,7 @@ class YamlInterpreter(object):
|
||||
self.cr.commit()
|
||||
|
||||
def _create_record(self, model, fields, view_info=None, parent={}, default=True, context=None):
|
||||
"""This function processes the !record tag in yalm files. It simulates the record creation through an xml
|
||||
"""This function processes the !record tag in yaml files. It simulates the record creation through an xml
|
||||
view (either specified on the !record tag or the default one for this object), including the calls to
|
||||
on_change() functions, and sending only values for fields that aren't set as readonly.
|
||||
:param model: model instance
|
||||
@@ -526,7 +526,11 @@ class YamlInterpreter(object):
|
||||
else:
|
||||
value = ids
|
||||
elif node.id:
|
||||
value = self.get_id(node.id)
|
||||
if field and field.type == 'reference':
|
||||
record = self.get_record(node.id)
|
||||
value = "%s,%s" % (record._name, record.id)
|
||||
else:
|
||||
value = self.get_id(node.id)
|
||||
else:
|
||||
value = None
|
||||
return value
|
||||
@@ -544,7 +548,7 @@ class YamlInterpreter(object):
|
||||
elements = self.process_ref(expression, field)
|
||||
if field.type in ("many2many", "one2many"):
|
||||
value = [(6, 0, elements)]
|
||||
else: # many2one
|
||||
else: # many2one or reference
|
||||
if isinstance(elements, (list,tuple)):
|
||||
value = self._get_first_result(elements)
|
||||
else:
|
||||
@@ -565,6 +569,9 @@ class YamlInterpreter(object):
|
||||
# enforce ISO format for string datetime values, to be locale-agnostic during tests
|
||||
time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value = expression
|
||||
elif field.type == "reference":
|
||||
record = self.get_record(expression)
|
||||
value = "%s,%s" % (record._name, record.id)
|
||||
else: # scalar field
|
||||
if is_eval(expression):
|
||||
value = self.process_eval(expression)
|
||||
|
||||
Reference in New Issue
Block a user