diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
index a1aa1eed26c..c81ab6016de 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
@@ -203,7 +203,7 @@ class account_analytic_line(osv.osv):
unit_price = total_price*-1.0 / total_qty
factor = self.pool['hr_timesheet_invoice.factor'].browse(cr, uid, factor_id, context=uom_context)
- factor_name = factor.customer_name
+ factor_name = factor.customer_name or ''
curr_invoice_line = {
'price_unit': unit_price,
'quantity': total_qty,
diff --git a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py
index 6d41cc6443d..6725f67abe9 100644
--- a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py
+++ b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py
@@ -73,7 +73,7 @@ class hr_timesheet_report(osv.osv):
htss.state"""
def _from(self):
- return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (hat.sheet_id=htss.id) join totals as t on (t.sheet_id = hat.sheet_id and t.date = aal.date)"
+ return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (hat.sheet_id=htss.id) left join totals as t on (t.sheet_id = hat.sheet_id and t.date = aal.date)"
def _group_by(self):
return super(hr_timesheet_report, self)._group_by() + """,
diff --git a/addons/procurement_jit/procurement_jit.py b/addons/procurement_jit/procurement_jit.py
index 67a87ff4b5c..05586baf9a7 100644
--- a/addons/procurement_jit/procurement_jit.py
+++ b/addons/procurement_jit/procurement_jit.py
@@ -33,14 +33,14 @@ class procurement_order(osv.osv):
self.check(cr, uid, [procurement_id], context=context)
return procurement_id
- def run(self, cr, uid, ids, context=None):
+ def run(self, cr, uid, ids, autocommit=False, context=None):
context = dict(context or {}, procurement_autorun_defer=True)
- res = super(procurement_order, self).run(cr, uid, ids, context=context)
+ res = super(procurement_order, self).run(cr, uid, ids, autocommit=autocommit, context=context)
procurement_ids = self.search(cr, uid, [('move_dest_id.procurement_id', 'in', ids)], order='id', context=context)
if procurement_ids:
- return self.run(cr, uid, procurement_ids, context=context)
+ return self.run(cr, uid, procurement_ids, autocommit=autocommit, context=context)
return res
class stock_move(osv.osv):
diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py
index e1ae51c79eb..dc5e9ed8605 100644
--- a/addons/stock_account/wizard/stock_valuation_history.py
+++ b/addons/stock_account/wizard/stock_valuation_history.py
@@ -56,8 +56,10 @@ class stock_history(osv.osv):
for product_id in ids:
line_ids.add(product_id)
line_ids = list(line_ids)
- cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),))
- lines_rec = cr.dictfetchall()
+ lines_rec = {}
+ if line_ids:
+ cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),))
+ lines_rec = cr.dictfetchall()
lines_dict = dict((line['id'], line) for line in lines_rec)
product_ids = list(set(line_rec['product_id'] for line_rec in lines_rec))
products_rec = self.pool['product.product'].read(cr, uid, product_ids, ['cost_method', 'product_tmpl_id'], context=context)
diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js
index acbfb9c4dc2..540e1fe8b75 100644
--- a/addons/web_calendar/static/src/js/web_calendar.js
+++ b/addons/web_calendar/static/src/js/web_calendar.js
@@ -565,7 +565,7 @@ openerp.web_calendar = function(instance) {
attendee_showed += 1;
if (attendee_showed<= MAX_ATTENDEES) {
if (self.avatar_model !== null) {
- the_title_avatar += '
';
}
else {
@@ -573,12 +573,12 @@ openerp.web_calendar = function(instance) {
tempColor = (self.all_filters[the_attendee_people] !== undefined)
? self.all_filters[the_attendee_people].color
: (self.all_filters[-1] ? self.all_filters[-1].color : 1);
- the_title_avatar += '';
+ the_title_avatar += '';
}//else don't add myself
}
}
else {
- attendee_other += self.all_attendees[the_attendee_people] +", ";
+ attendee_other += _.escape(self.all_attendees[the_attendee_people]) +", ";
}
}
);
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index 15f34edcb1d..dd97c19e817 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -183,7 +183,7 @@
if (!this.$target.has('.media_iframe_video_size')) {
var editor = '
';
var size = '
';
- this.$target.html(editor+size+'');
+ this.$target.html(editor+size+'');
}
},
});
diff --git a/addons/website_google_map/controllers/main.py b/addons/website_google_map/controllers/main.py
index efddba1b10f..870cda8a435 100644
--- a/addons/website_google_map/controllers/main.py
+++ b/addons/website_google_map/controllers/main.py
@@ -4,6 +4,7 @@ import json
from openerp import SUPERUSER_ID
from openerp.addons.web import http
from openerp.addons.web.http import request
+from openerp.tools import html_escape as escape
class google_map(http.Controller):
@@ -48,12 +49,13 @@ class google_map(http.Controller):
}
request.context.update({'show_address': True})
for partner in partner_obj.browse(cr, SUPERUSER_ID, partners_ids, context=context):
+ # TODO in master, do not use `escape` but `t-esc` in the qweb template.
partner_data["partners"].append({
'id': partner.id,
- 'name': partner.name,
- 'address': '\n'.join(partner.name_get()[0][1].split('\n')[1:]),
- 'latitude': partner.partner_latitude,
- 'longitude': partner.partner_longitude,
+ 'name': escape(partner.name),
+ 'address': escape('\n'.join(partner.name_get()[0][1].split('\n')[1:])),
+ 'latitude': escape(str(partner.partner_latitude)),
+ 'longitude': escape(str(partner.partner_longitude)),
})
# generate the map
diff --git a/doc/cla/corporate/mentis.md b/doc/cla/corporate/mentis.md
new file mode 100644
index 00000000000..d8d9562f74b
--- /dev/null
+++ b/doc/cla/corporate/mentis.md
@@ -0,0 +1,17 @@
+Slovenia, 2015-06-29
+
+Mentis d.o.o. 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,
+
+Dušan Laznik dusan.laznik@mentis.si https://github.com/laznikd
+
+List of contributors:
+
+Dušan Laznik dusan.laznik@mentis.si https://github.com/laznikd
+Aleksander Dirntiš sandi.dirntis@mentis.si https://github.com/adirntis
+Matjaž Kalič matjaz.kalic@mentis.si https://github.com/kalicm
diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py
index 77c0c830cba..69dbe144f87 100644
--- a/openerp/addons/base/ir/ir_fields.py
+++ b/openerp/addons/base/ir/ir_fields.py
@@ -37,6 +37,19 @@ class ConversionNotFound(ValueError): pass
class ir_fields_converter(models.Model):
_name = 'ir.fields.converter'
+ @api.model
+ def _format_import_error(self, error_type, error_msg, error_params=(), error_args=None):
+ # sanitize error params for later formatting by the import system
+ sanitize = lambda p: p.replace('%', '%%') if isinstance(p, basestring) else p
+ if error_params:
+ if isinstance(error_params, basestring):
+ error_params = sanitize(error_params)
+ elif isinstance(error_params, dict):
+ error_params = dict((k, sanitize(v)) for k, v in error_params.iteritems())
+ elif isinstance(error_params, tuple):
+ error_params = tuple(map(sanitize, error_params))
+ return error_type(error_msg % error_params, error_args)
+
@api.model
def for_model(self, model, fromtype=str):
""" Returns a converter object for the model. A converter is a
@@ -148,29 +161,34 @@ class ir_fields_converter(models.Model):
if value.lower() in falses:
return False, []
- return True, [ImportWarning(
- _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'")
- % (value, yes), {
- 'moreinfo': _(u"Use '1' for yes and '0' for no")
- })]
+ return True, [self._format_import_error(
+ ImportWarning,
+ _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'"),
+ (value, yes),
+ {'moreinfo': _(u"Use '1' for yes and '0' for no")}
+ )]
@api.model
def _str_to_integer(self, model, field, value):
try:
return int(value), []
except ValueError:
- raise ValueError(
- _(u"'%s' does not seem to be an integer for field '%%(field)s'")
- % value)
+ raise self._format_import_error(
+ ValueError,
+ _(u"'%s' does not seem to be an integer for field '%%(field)s'"),
+ value
+ )
@api.model
def _str_to_float(self, model, field, value):
try:
return float(value), []
except ValueError:
- raise ValueError(
- _(u"'%s' does not seem to be a number for field '%%(field)s'")
- % value)
+ raise self._format_import_error(
+ ValueError,
+ _(u"'%s' does not seem to be a number for field '%%(field)s'"),
+ value
+ )
@api.model
def _str_id(self, model, field, value):
@@ -184,10 +202,12 @@ class ir_fields_converter(models.Model):
time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
return value, []
except ValueError:
- raise ValueError(
- _(u"'%s' does not seem to be a valid date for field '%%(field)s'") % value, {
- 'moreinfo': _(u"Use the format '%s'") % u"2012-12-31"
- })
+ raise self._format_import_error(
+ ValueError,
+ _(u"'%s' does not seem to be a valid date for field '%%(field)s'"),
+ value,
+ {'moreinfo': _(u"Use the format '%s'") % u"2012-12-31"}
+ )
@api.model
def _input_tz(self):
@@ -215,10 +235,12 @@ class ir_fields_converter(models.Model):
parsed_value = datetime.datetime.strptime(
value, DEFAULT_SERVER_DATETIME_FORMAT)
except ValueError:
- raise ValueError(
- _(u"'%s' does not seem to be a valid datetime for field '%%(field)s'") % value, {
- 'moreinfo': _(u"Use the format '%s'") % u"2012-12-31 23:59:59"
- })
+ raise self._format_import_error(
+ ValueError,
+ _(u"'%s' does not seem to be a valid datetime for field '%%(field)s'"),
+ value,
+ {'moreinfo': _(u"Use the format '%s'") % u"2012-12-31 23:59:59"}
+ )
input_tz = self._input_tz()# Apply input tz to the parsed naive datetime
dt = input_tz.localize(parsed_value, is_dst=False)
@@ -251,12 +273,12 @@ class ir_fields_converter(models.Model):
if value == unicode(item) or value in labels:
return item, []
- raise ValueError(
- _(u"Value '%s' not found in selection field '%%(field)s'") % (
- value), {
- 'moreinfo': [_label or unicode(item) for item, _label in selection
- if _label or item]
- })
+ raise self._format_import_error(
+ ValueError,
+ _(u"Value '%s' not found in selection field '%%(field)s'"),
+ value,
+ {'moreinfo': [_label or unicode(item) for item, _label in selection if _label or item]}
+ )
@api.model
def db_id_for(self, model, field, subfield, value):
@@ -298,8 +320,10 @@ class ir_fields_converter(models.Model):
id = tentative_id
except psycopg2.DataError:
# type error
- raise ValueError(
- _(u"Invalid database id '%s' for the field '%%(field)s'") % value,
+ raise self._format_import_error(
+ ValueError,
+ _(u"Invalid database id '%s' for the field '%%(field)s'"),
+ value,
{'moreinfo': action})
elif subfield == 'id':
field_type = _(u"external id")
@@ -321,12 +345,17 @@ class ir_fields_converter(models.Model):
% (len(ids))))
id, _name = ids[0]
else:
- raise Exception(_(u"Unknown sub-field '%s'") % subfield)
+ raise self._format_import_error(
+ Exception,
+ _(u"Unknown sub-field '%s'"),
+ subfield
+ )
if id is None:
- raise ValueError(
- _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'")
- % {'field_type': field_type, 'value': value},
+ raise self._format_import_error(
+ ValueError,
+ _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'"),
+ {'field_type': field_type, 'value': value},
{'moreinfo': action})
return id, field_type, warnings
diff --git a/openerp/addons/test_impex/tests/test_import.py b/openerp/addons/test_impex/tests/test_import.py
index 11bacf6cce1..eab986fb29a 100644
--- a/openerp/addons/test_impex/tests/test_import.py
+++ b/openerp/addons/test_impex/tests/test_import.py
@@ -111,6 +111,12 @@ class test_ids_stuff(ImporterCase):
self.import_(['id', 'value'], [['somexmlid', '1234567']])
self.assertEqual([1234567], values(self.read()))
+ def test_wrong_format(self):
+ self.assertEqual(
+ self.import_(['value'], [['50%']]),
+ error(1, u"'50%' does not seem to be an integer for field 'unknown'"))
+
+
class test_boolean_field(ImporterCase):
model_name = 'export.boolean'
@@ -163,10 +169,13 @@ class test_boolean_field(ImporterCase):
# Problem: OpenOffice (and probably excel) output localized booleans
['VRAI'],
[u'OFF'],
+ [u'是的'],
+ ['!&%#${}'],
+ ['%(field)s'],
]),
- ok(8))
+ ok(11))
self.assertEqual(
- [True] * 8,
+ [True] * 11,
values(self.read()))
class test_integer_field(ImporterCase):
diff --git a/openerp/fields.py b/openerp/fields.py
index 47eb5771d6f..7b11b4e2fe4 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -30,7 +30,7 @@ import logging
import pytz
import xmlrpclib
-from openerp.tools import float_round, frozendict, html_sanitize, ustr
+from openerp.tools import float_round, frozendict, html_sanitize, ustr, OrderedSet
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
@@ -1606,34 +1606,33 @@ class _RelationalMulti(_Relational):
return value.with_env(record.env)
elif isinstance(value, list):
# value is a list of record ids or commands
- if not record.id:
- record = record.browse() # new record has no value
- result = record[self.name]
- # modify result with the commands;
- # beware to not introduce duplicates in result
+ comodel = record.env[self.comodel_name]
+ ids = OrderedSet(record[self.name].ids)
+ # modify ids with the commands
for command in value:
if isinstance(command, (tuple, list)):
if command[0] == 0:
- result += result.new(command[2])
+ ids.add(comodel.new(command[2]).id)
elif command[0] == 1:
- result.browse(command[1]).update(command[2])
- result += result.browse(command[1]) - result
+ comodel.browse(command[1]).update(command[2])
+ ids.add(command[1])
elif command[0] == 2:
# note: the record will be deleted by write()
- result -= result.browse(command[1])
+ ids.discard(command[1])
elif command[0] == 3:
- result -= result.browse(command[1])
+ ids.discard(command[1])
elif command[0] == 4:
- result += result.browse(command[1]) - result
+ ids.add(command[1])
elif command[0] == 5:
- result = result.browse()
+ ids.clear()
elif command[0] == 6:
- result = result.browse(command[2])
+ ids = OrderedSet(command[2])
elif isinstance(command, dict):
- result += result.new(command)
+ ids.add(comodel.new(command).id)
else:
- result += result.browse(command) - result
- return result
+ ids.add(command)
+ # return result as a recordset
+ return comodel.browse(list(ids))
elif not value:
return self.null(record.env)
raise ValueError("Wrong value for %s: %s" % (self, value))
diff --git a/openerp/models.py b/openerp/models.py
index aaf692b6e5e..104fb404db8 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -1194,11 +1194,8 @@ class BaseModel(object):
type = 'warning' if isinstance(exception, Warning) else 'error'
# logs the logical (not human-readable) field name for automated
# processing of response, but injects human readable in message
- # Before applying string substitution, we need to escape all '%'
- # characters that are not part of substitutable reference.
- regex = '%%(?!\((%s)\)s)' % '|'.join(base.keys())
- message = unicode(re.sub(regex, '%%', exception.args[0])) % base
- record = dict(base, type=type, field=field, message=message)
+ record = dict(base, type=type, field=field,
+ message=unicode(exception.args[0]) % base)
if len(exception.args) > 1 and exception.args[1]:
record.update(exception.args[1])
log(record)
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 48b9e633c78..92545925bad 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -37,7 +37,7 @@ import threading
import time
import werkzeug.utils
import zipfile
-from collections import defaultdict, Mapping
+from collections import defaultdict, Mapping, OrderedDict
from datetime import datetime
from itertools import islice, izip, groupby
from lxml import etree
@@ -1136,6 +1136,17 @@ class frozendict(dict):
def update(self, *args, **kwargs):
raise NotImplementedError("'update' not supported on frozendict")
+class OrderedSet(OrderedDict):
+ """ A simple collection that remembers the elements insertion order. """
+ def __init__(self, seq=()):
+ super(OrderedSet, self).__init__([(x, None) for x in seq])
+
+ def add(self, elem):
+ self[elem] = None
+
+ def discard(self, elem):
+ self.pop(elem, None)
+
@contextmanager
def ignore(*exc):
try:
diff --git a/requirements.txt b/requirements.txt
index 3f19e42c037..df44bfdc5c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ Jinja2==2.7.3
Mako==1.0.1
MarkupSafe==0.23
Pillow==2.7.0
-http://download.gna.org/pychart/PyChart-1.39.tar.gz#egg=PyChart
+Python-Chart==1.39
PyYAML==3.11
Werkzeug==0.9.6
argparse==1.2.1
diff --git a/setup.py b/setup.py
index 8586517de7a..31d21931830 100644
--- a/setup.py
+++ b/setup.py
@@ -87,7 +87,7 @@ def py2exe_options():
'PIL',
'poplib',
'psutil',
- 'pychart',
+ 'python-chart',
'pydot',
'pyparsing',
'pyPdf',
@@ -128,7 +128,6 @@ setup(
packages=find_packages(),
package_dir={'%s' % lib_name: 'openerp'},
include_package_data=True,
- dependency_links=['http://download.gna.org/pychart/'],
install_requires=[
'babel >= 1.0',
'decorator',
@@ -144,7 +143,7 @@ setup(
'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycogreen',
'psycopg2 >= 2.2',
- 'pychart', # not on pypi, use: pip install http://download.gna.org/pychart/PyChart-1.39.tar.gz
+ 'python-chart',
'pydot',
'pyparsing < 2',
'pypdf',