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',