diff --git a/doc/changelog.rst b/doc/changelog.rst index ce639d2d3c8..eadfe81b5e8 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -31,5 +31,15 @@ Changelog ``openerp.exceptions.RedirectWarning``. - Give a pair of new methods to ``res.config.settings`` and a helper to make them easier to use: ``get_config_warning()``. -- Path to webkit report files (field ``report_file``) must be writen with the - Unix way (with ``/`` and not ``\``) \ No newline at end of file +- Path to webkit report files (field ``report_file``) must be written the + Unix way (with ``/`` and not ``\``) + + +`7.0` +----- + +- Modules may now include an ``i18n_extra`` directory that will be treated like the + default ``i18n`` directory. This is typically useful for manual translation files + that are not managed by Launchpad's translation system. An example is l10n modules + that depend on ``l10n_multilang``. + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 7eb4f5ecd94..3b709934a27 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -195,7 +195,8 @@ class ir_model(osv.osv): ctx = dict(context, field_name=vals['name'], field_state='manual', - select=vals.get('select_level', '0')) + select=vals.get('select_level', '0'), + update_custom_fields=True) self.pool[vals['model']]._auto_init(cr, ctx) openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) return res diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index acb6daa7785..0f9b6ea3886 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -445,6 +445,13 @@ class ir_translation(osv.osv): tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context) context['overwrite'] = True # make sure the requested translation will override the base terms later + # i18n_extra folder is for additional translations handle manually (eg: for l10n_be) + base_trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po') + if base_trans_extra_file: + _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang) + tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context) + context['overwrite'] = True # make sure the requested translation will override the base terms later + # Step 2: then load the main translation file, possibly overriding the terms coming from the base language trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po') if trans_file: @@ -452,6 +459,11 @@ class ir_translation(osv.osv): tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context) elif lang_code != 'en_US': _logger.warning('module %s: no translation for language %s', module_name, lang_code) + + trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po') + if trans_extra_file: + _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang) + tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context) return True diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 73530a122ad..9699545ddf5 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -187,7 +187,7 @@ class view(osv.osv): if self.pool._init: # Module init currently in progress, only consider views from modules whose code was already loaded query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) - WHERE v.inherit_id=%s AND v.model=%s AND md.module in %s + WHERE v.inherit_id=%s AND v.model=%s AND (md.module IS NULL or md.module in %s) ORDER BY priority""" query_params = (view_id, model, tuple(self.pool._init_modules)) else: diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index efe33f32fd9..a496a1eccdf 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -30,11 +30,13 @@ from openerp.tools.translate import _ CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') class res_currency(osv.osv): - def _current_rate(self, cr, uid, ids, name, arg, context=None): - return self._get_current_rate(cr, uid, ids, name, arg, context=context) + return self._get_current_rate(cr, uid, ids, context=context) - def _get_current_rate(self, cr, uid, ids, name, arg, context=None): + def _current_rate_silent(self, cr, uid, ids, name, arg, context=None): + return self._get_current_rate(cr, uid, ids, raise_on_no_rate=False, context=context) + + def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None): if context is None: context = {} res = {} @@ -52,9 +54,12 @@ class res_currency(osv.osv): if cr.rowcount: id, rate = cr.fetchall()[0] res[id] = rate + elif not raise_on_no_rate: + res[id] = 0 else: raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id))) return res + _name = "res.currency" _description = "Currency" _columns = { @@ -63,6 +68,10 @@ class res_currency(osv.osv): 'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."), 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6), help='The rate of the currency to the currency of rate 1.'), + + # Do not use for computation ! Same as rate field with silent failing + 'rate_silent': fields.function(_current_rate_silent, string='Current Rate', digits=(12,6), + help='The rate of the currency to the currency of rate 1 (0 if no rate defined).'), 'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'), 'accuracy': fields.integer('Computational Accuracy'), 'rounding': fields.float('Rounding Factor', digits=(12,6)), diff --git a/openerp/addons/base/res/res_currency_view.xml b/openerp/addons/base/res/res_currency_view.xml index 907eec866fb..c74b677ea72 100644 --- a/openerp/addons/base/res/res_currency_view.xml +++ b/openerp/addons/base/res/res_currency_view.xml @@ -22,7 +22,7 @@ - + @@ -37,7 +37,7 @@
- + diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 566ee6066dd..97b0ac454ae 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -208,6 +208,8 @@ class res_partner(osv.osv, format_address): return result def _display_name_compute(self, cr, uid, ids, name, args, context=None): + context = dict(context or {}) + context.pop('show_address', None) return dict(self.name_get(cr, uid, ids, context=context)) # indirections to avoid passing a copy of the overridable method when declaring the function field diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index ef02882422c..18a1e9508f5 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -398,7 +398,9 @@ def is_leaf(element, internal=False): INTERNAL_OPS += ('inselect',) return (isinstance(element, tuple) or isinstance(element, list)) \ and len(element) == 3 \ - and element[1] in INTERNAL_OPS + and element[1] in INTERNAL_OPS \ + and ((isinstance(element[0], basestring) and element[0]) + or element in (TRUE_LEAF, FALSE_LEAF)) # -------------------------------------------------- diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 163abcb006d..f96d00815c9 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -383,16 +383,18 @@ class browse_record(object): raise KeyError(error_msg) # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields - if col._prefetch: + if col._prefetch and not col.groups: # gen the list of "local" (ie not inherited) fields which are classic or many2one - fields_to_fetch = filter(lambda x: x[1]._classic_write and x[1]._prefetch, self._table._columns.items()) + field_filter = lambda x: x[1]._classic_write and x[1]._prefetch and not x[1].groups + fields_to_fetch = filter(field_filter, self._table._columns.items()) # gen the list of inherited fields inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items()) # complete the field list with the inherited fields which are classic or many2one - fields_to_fetch += filter(lambda x: x[1]._classic_write and x[1]._prefetch, inherits) + fields_to_fetch += filter(field_filter, inherits) # otherwise we fetch only that field else: fields_to_fetch = [(name, col)] + ids = filter(lambda id: name not in self._data[id], self._data.keys()) # read the results field_names = map(lambda x: x[0], fields_to_fetch) diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py index fceb318401b..548a3f833f5 100644 --- a/openerp/report/report_sxw.py +++ b/openerp/report/report_sxw.py @@ -109,7 +109,7 @@ class _date_format(str, _format): if self.val: if getattr(self,'name', None): date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT) - return date.strftime(str(self.lang_obj.date_format)) + return date.strftime(self.lang_obj.date_format.encode('utf-8')) return self.val class _dttime_format(str, _format): @@ -120,8 +120,8 @@ class _dttime_format(str, _format): def __str__(self): if self.val and getattr(self,'name', None): return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\ - .strftime("%s %s"%(str(self.lang_obj.date_format), - str(self.lang_obj.time_format))) + .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'), + (self.lang_obj.time_format).encode('utf-8'))) return self.val @@ -313,7 +313,7 @@ class rml_parse(object): date = datetime_field.context_timestamp(self.cr, self.uid, timestamp=date, context=self.localcontext) - return date.strftime(date_format) + return date.strftime(date_format.encode('utf-8')) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) if currency_obj: diff --git a/openerp/tests/test_acl.py b/openerp/tests/test_acl.py index 1d8d6bfb22a..e80390cd5a2 100644 --- a/openerp/tests/test_acl.py +++ b/openerp/tests/test_acl.py @@ -9,6 +9,7 @@ import common # test group that demo user should not have GROUP_TECHNICAL_FEATURES = 'base.group_no_one' + class TestACL(common.TransactionCase): def setUp(self): @@ -22,25 +23,25 @@ class TestACL(common.TransactionCase): def test_field_visibility_restriction(self): """Check that model-level ``groups`` parameter effectively restricts access to that - field for users who do not belong to one of the explicitly allowed groups""" + field for users who do not belong to one of the explicitly allowed groups""" # Verify the test environment first original_fields = self.res_currency.fields_get(self.cr, self.demo_uid, []) form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form') view_arch = etree.fromstring(form_view.get('arch')) has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group before the test") - self.assertTrue('rate' in original_fields, "'rate' field must be properly visible before the test") - self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [], - "Field 'rate' must be found in view definition before the test") + self.assertTrue('accuracy' in original_fields, "'accuracy' field must be properly visible before the test") + self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [], + "Field 'accuracy' must be found in view definition before the test") # Restrict access to the field and check it's gone - self.res_currency._columns['rate'].groups = GROUP_TECHNICAL_FEATURES + self.res_currency._columns['accuracy'].groups = GROUP_TECHNICAL_FEATURES fields = self.res_currency.fields_get(self.cr, self.demo_uid, []) form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form') view_arch = etree.fromstring(form_view.get('arch')) - self.assertFalse('rate' in fields, "'rate' field should be gone") - self.assertEquals(view_arch.xpath("//field[@name='rate']"), [], - "Field 'rate' must not be found in view definition") + self.assertFalse('accuracy' in fields, "'accuracy' field should be gone") + self.assertEquals(view_arch.xpath("//field[@name='accuracy']"), [], + "Field 'accuracy' must not be found in view definition") # Make demo user a member of the restricted group and check that the field is back self.tech_group.write({'users': [(4, self.demo_uid)]}) @@ -50,13 +51,13 @@ class TestACL(common.TransactionCase): view_arch = etree.fromstring(form_view.get('arch')) #import pprint; pprint.pprint(fields); pprint.pprint(form_view) self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group") - self.assertTrue('rate' in fields, "'rate' field must be properly visible again") - self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [], - "Field 'rate' must be found in view definition again") + self.assertTrue('accuracy' in fields, "'accuracy' field must be properly visible again") + self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [], + "Field 'accuracy' must be found in view definition again") #cleanup self.tech_group.write({'users': [(3, self.demo_uid)]}) - self.res_currency._columns['rate'].groups = False + self.res_currency._columns['accuracy'].groups = False @mute_logger('openerp.osv.orm') def test_field_crud_restriction(self): @@ -65,7 +66,7 @@ class TestACL(common.TransactionCase): has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group") self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids'])) - self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) + self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) # Now restrict access to the field and check it's forbidden self.res_partner._columns['bank_ids'].groups = GROUP_TECHNICAL_FEATURES @@ -79,12 +80,30 @@ class TestACL(common.TransactionCase): has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group") self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids'])) - self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) - + self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) + #cleanup self.tech_group.write({'users': [(3, self.demo_uid)]}) self.res_partner._columns['bank_ids'].groups = False + def test_fields_browse_restriction(self): + """Test access to records having restricted fields""" + self.res_partner._columns['email'].groups = GROUP_TECHNICAL_FEATURES + try: + P = self.res_partner + pid = P.search(self.cr, self.demo_uid, [], limit=1)[0] + part = P.browse(self.cr, self.demo_uid, pid) + # accessing fields must no raise exceptions... + part.name + # ... except they are restricted + with self.assertRaises(openerp.osv.orm.except_orm) as cm: + part.email + + self.assertEqual(cm.exception.args[0], 'Access Denied') + finally: + self.res_partner._columns['email'].groups = False + + if __name__ == '__main__': unittest2.main() diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 6ab858d1d67..4581d69c6ef 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Business Applications -# Copyright (C) 2012 OpenERP S.A. (). +# Copyright (C) 2012-2013 OpenERP S.A. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -44,29 +44,53 @@ tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "if tags_to_remove = ['html', 'body', 'font'] -def html_sanitize(src): +def html_sanitize(src, silent=True): if not src: return src src = ustr(src, errors='replace') + logger = _logger.getChild('html_sanitize') + # html encode email tags part = re.compile(r"(<(([^a<>]|a[^<>\s])[^<>]*)@[^<>]+>)", re.IGNORECASE | re.DOTALL) src = part.sub(lambda m: cgi.escape(m.group(1)), src) - # some corner cases make the parser crash (such as in test_mail) + kwargs = { + 'page_structure': True, + 'style': False, # do not remove style attributes + 'forms': True, # remove form tags + } + if etree.LXML_VERSION >= (2, 3, 1): + # kill_tags attribute has been added in version 2.3.1 + kwargs.update({ + 'kill_tags': tags_to_kill, + 'remove_tags': tags_to_remove, + }) + else: + kwargs['remove_tags'] = tags_to_kill + tags_to_remove + + if etree.LXML_VERSION >= (3, 1, 0): + kwargs.update({ + 'safe_attrs_only': True, + 'safe_attrs': clean.defs.safe_attrs | set(['style']), + }) + else: + # lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style" + kwargs['safe_attrs_only'] = False + try: - cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, kill_tags=tags_to_kill, remove_tags=tags_to_remove) + # some corner cases make the parser crash (such as in test_mail) + cleaner = clean.Cleaner(**kwargs) cleaned = cleaner.clean_html(src) - except TypeError, e: - # lxml.clean version < 2.3.1 does not have a kill_tags attribute - # to remove in 2014 - cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, remove_tags=tags_to_kill + tags_to_remove) - cleaned = cleaner.clean_html(src) - except etree.ParserError, e: - _logger.warning('html_sanitize: ParserError "%s" obtained when sanitizing "%s"' % (e, src)) + except etree.ParserError: + if not silent: + raise + logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True) cleaned = '

ParserError when sanitizing

' - except Exception, e: - _logger.warning('html_sanitize: unknown error "%s" obtained when sanitizing "%s"' % (e, src)) + except Exception: + if not silent: + raise + logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True) cleaned = '

Unknown error when sanitizing

' return cleaned diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 4f2d8177c0f..65e7c38e536 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -421,6 +421,7 @@ def get_iso_codes(lang): ALL_LANGUAGES = { 'ab_RU': u'Abkhazian / аҧсуа', + 'am_ET': u'Amharic / አምሃርኛ', 'ar_SY': u'Arabic / الْعَرَبيّة', 'bg_BG': u'Bulgarian / български език', 'bs_BS': u'Bosnian / bosanski jezik',