diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index a5265ba694c..d390d235f92 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -44,6 +44,7 @@ The kernel of OpenERP, needed for all installation. 'data/res.country.state.csv', 'ir/wizard/wizard_menu_view.xml', 'ir/ir.xml', + 'ir/ir_translation_view.xml', 'ir/ir_filters.xml', 'ir/ir_config_parameter_view.xml', 'ir/workflow/workflow_view.xml', diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 3189870aa68..ea549c39993 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1147,75 +1147,6 @@ - - - - Translations - ir.translation - - - - - - - - - - - - - Translations - ir.translation - -
-
- -
- - - - - - - - - - - - - - - - - - -
-
-
- - Translations - ir.translation - - - - - - - - - - - - - Translated Terms - ir.translation - form - - - - + + Translations + ir.translation + + + + + + + + + + + + + + Translations + ir.translation + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Translations + ir.translation + + + + + + + + + + + + + Translated Terms + ir.translation + form + + + + + + + \ No newline at end of file diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 4bbff5479c3..213cff18cc6 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -265,7 +265,7 @@ class ir_ui_menu(osv.osv): } if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model: obj = self.pool.get(menu.action.res_model) - if obj._needaction: + if obj and obj._needaction: if menu.action.type=='ir.actions.act_window': dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or [] else: diff --git a/openerp/addons/base/ir/wizard/__init__.py b/openerp/addons/base/ir/wizard/__init__.py index 3f5495524f5..23cc77e8c15 100644 --- a/openerp/addons/base/ir/wizard/__init__.py +++ b/openerp/addons/base/ir/wizard/__init__.py @@ -19,6 +19,5 @@ # ############################################################################## import wizard_menu -import wizard_screen # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/wizard/wizard_menu.py b/openerp/addons/base/ir/wizard/wizard_menu.py index 3b4dc797c99..17f0fc13f1d 100644 --- a/openerp/addons/base/ir/wizard/wizard_menu.py +++ b/openerp/addons/base/ir/wizard/wizard_menu.py @@ -47,25 +47,6 @@ class wizard_model_menu(osv.osv_memory): 'icon': 'STOCK_INDENT' }, context) return {'type':'ir.actions.act_window_close'} -wizard_model_menu() - -class wizard_model_menu_line(osv.osv_memory): - _name = 'wizard.ir.model.menu.create.line' - _columns = { - 'wizard_id': fields.many2one('wizard.ir.model.menu.create','Wizard'), - 'sequence': fields.integer('Sequence'), - 'view_type': fields.selection([ - ('tree','Tree'), - ('form','Form'), - ('graph','Graph'), - ('calendar','Calendar'), - ('gantt','Gantt')],'View Type',required=True), - 'view_id': fields.many2one('ir.ui.view', 'View'), - } - _defaults = { - 'view_type': lambda self,cr,uid,ctx: 'tree' - } -wizard_model_menu_line() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/wizard/wizard_screen.py b/openerp/addons/base/ir/wizard/wizard_screen.py deleted file mode 100644 index d413ce0474e..00000000000 --- a/openerp/addons/base/ir/wizard/wizard_screen.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Copyright (C) 2010 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 -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import base64 -import os -import random - -import tools -from osv import fields,osv - -# Simple base class for wizards that wish to use random images on the left -# side of the form. -class wizard_screen(osv.osv_memory): - _name = 'ir.wizard.screen' - - def _get_image(self, cr, uid, context=None): - path = os.path.join('base','res','config_pixmaps','%d.png'%random.randrange(1,4)) - image_file = file_data = tools.file_open(path,'rb') - try: - file_data = image_file.read() - return base64.encodestring(file_data) - finally: - image_file.close() - - def _get_image_fn(self, cr, uid, ids, name, args, context=None): - image = self._get_image(cr, uid, context) - return dict.fromkeys(ids, image) # ok to use .fromkeys() as the image is same for all - - _columns = { - 'config_logo': fields.function(_get_image_fn, string='Image', type='binary'), - } - - _defaults = { - 'config_logo': _get_image - } -wizard_screen() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 7621d5907ba..e86bcc15d74 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -627,45 +627,14 @@ class module(osv.osv): self.write(cr, uid, [mod_browse.id], {'category_id': p_id}) def update_translations(self, cr, uid, ids, filter_lang=None, context=None): - if context is None: - context = {} if not filter_lang: - pool = pooler.get_pool(cr.dbname) - lang_obj = pool.get('res.lang') - lang_ids = lang_obj.search(cr, uid, [('translatable', '=', True)]) - filter_lang = [lang.code for lang in lang_obj.browse(cr, uid, lang_ids)] + res_lang = self.pool.get('res.lang') + lang_ids = res_lang.search(cr, uid, [('translatable', '=', True)]) + filter_lang = [lang.code for lang in res_lang.browse(cr, uid, lang_ids)] elif not isinstance(filter_lang, (list, tuple)): filter_lang = [filter_lang] - - for mod in self.browse(cr, uid, ids): - if mod.state != 'installed': - continue - modpath = modules.get_module_path(mod.name) - if not modpath: - # unable to find the module. we skip - continue - for lang in filter_lang: - iso_lang = tools.get_iso_codes(lang) - f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po') - context2 = context and context.copy() or {} - if f and '_' in iso_lang: - iso_lang2 = iso_lang.split('_')[0] - f2 = modules.get_module_resource(mod.name, 'i18n', iso_lang2 + '.po') - if f2: - _logger.info('module %s: loading base translation file %s for language %s', mod.name, iso_lang2, lang) - tools.trans_load(cr, f2, lang, verbose=False, context=context) - context2['overwrite'] = True - # Implementation notice: we must first search for the full name of - # the language derivative, like "en_UK", and then the generic, - # like "en". - if (not f) and '_' in iso_lang: - iso_lang = iso_lang.split('_')[0] - f = modules.get_module_resource(mod.name, 'i18n', iso_lang + '.po') - if f: - _logger.info('module %s: loading translation file (%s) for language %s', mod.name, iso_lang, lang) - tools.trans_load(cr, f, lang, verbose=False, context=context2) - elif iso_lang != 'en': - _logger.warning('module %s: no translation for language %s', mod.name, iso_lang) + modules = [m.name for m in self.browse(cr, uid, ids) if m.state == 'installed'] + self.pool.get('ir.translation').load(cr, modules, filter_lang, context=context) def check(self, cr, uid, ids, context=None): for mod in self.browse(cr, uid, ids, context=context): diff --git a/openerp/addons/base/module/wizard/base_export_language.py b/openerp/addons/base/module/wizard/base_export_language.py index 5ff99501943..95f1d85bf6c 100644 --- a/openerp/addons/base/module/wizard/base_export_language.py +++ b/openerp/addons/base/module/wizard/base_export_language.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# OpenERP, Open Source Business Applications +# Copyright (c) 2004-2012 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 @@ -22,68 +22,58 @@ import tools import base64 import cStringIO -import pooler from osv import fields,osv from tools.translate import _ from tools.misc import get_iso_codes +NEW_LANG_KEY = '__new__' + class base_language_export(osv.osv_memory): + _name = "base.language.export" def _get_languages(self, cr, uid, context): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) - langs=lang_obj.browse(cr, uid, ids) - return [(lang.code, lang.name) for lang in langs] - - def act_cancel(self, cr, uid, ids, context=None): - #self.unlink(cr, uid, ids, context) - return {'type':'ir.actions.act_window_close' } - - def act_destroy(self, *args): - return {'type':'ir.actions.act_window_close' } + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, [('translatable', '=', True)]) + langs = lang_obj.browse(cr, uid, ids) + return [(NEW_LANG_KEY, _('New Language (Empty translation template)'))] + [(lang.code, lang.name) for lang in langs] + + _columns = { + 'name': fields.char('File Name', readonly=True), + 'lang': fields.selection(_get_languages, 'Language', required=True), + 'format': fields.selection([('csv','CSV File'), + ('po','PO File'), + ('tgz', 'TGZ Archive')], 'File Format', required=True), + 'modules': fields.many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id', 'Modules To Export', domain=[('state','=','installed')]), + 'data': fields.binary('File', readonly=True), + 'state': fields.selection([('choose', 'choose'), # choose language + ('get', 'get')]) # get the file + } + _defaults = { + 'state': 'choose', + 'name': 'lang.tar.gz', + 'lang': NEW_LANG_KEY, + 'format': 'csv', + } def act_getfile(self, cr, uid, ids, context=None): this = self.browse(cr, uid, ids)[0] + lang = this.lang if this.lang != NEW_LANG_KEY else False mods = map(lambda m: m.name, this.modules) or ['all'] mods.sort() - buf=cStringIO.StringIO() - tools.trans_export(this.lang, mods, buf, this.format, cr) - if this.format == 'csv': - this.advice = _("Save this document to a .CSV file and open it with your favourite spreadsheet software. The file encoding is UTF-8. You have to translate the latest column before reimporting it.") - elif this.format == 'po': - if not this.lang: - this.format = 'pot' - this.advice = _("Save this document to a %s file and edit it with a specific software or a text editor. The file encoding is UTF-8.") % ('.'+this.format,) - elif this.format == 'tgz': - ext = this.lang and '.po' or '.pot' - this.advice = _('Save this document to a .tgz file. This archive containt UTF-8 %s files and may be uploaded to launchpad.') % (ext,) - filename = _('new') - if not this.lang and len(mods) == 1: + buf = cStringIO.StringIO() + tools.trans_export(lang, mods, buf, this.format, cr) + filename = 'new' + if lang: + filename = get_iso_codes(lang) + elif len(mods) == 1: filename = mods[0] - if this.lang: - filename = get_iso_codes(this.lang) this.name = "%s.%s" % (filename, this.format) - out=base64.encodestring(buf.getvalue()) + out = base64.encodestring(buf.getvalue()) buf.close() - return self.write(cr, uid, ids, {'state':'get', 'data':out, 'advice':this.advice, 'name':this.name}, context=context) + self.write(cr, uid, ids, {'state': 'get', + 'data': out, + 'name':this.name}, context=context) + return True - _name = "base.language.export" - _inherit = "ir.wizard.screen" - _columns = { - 'name': fields.char('File Name', 16, readonly=True), - 'lang': fields.selection(_get_languages, 'Language', help='To export a new language, do not select a language.'), # not required: unset = new language - 'format': fields.selection( ( ('csv','CSV File'), ('po','PO File'), ('tgz', 'TGZ Archive')), 'File Format', required=True), - 'modules': fields.many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id', 'Modules', domain=[('state','=','installed')]), - 'data': fields.binary('File', readonly=True), - 'advice': fields.text('Advice', readonly=True), - 'state': fields.selection( ( ('choose','choose'), # choose language - ('get','get'), # get the file - ) ), - } - _defaults = { - 'state': lambda *a: 'choose', - 'name': lambda *a: 'lang.tar.gz' - } -base_language_export() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_export_language_view.xml b/openerp/addons/base/module/wizard/base_export_language_view.xml index 88e411845c3..cbf7104f957 100644 --- a/openerp/addons/base/module/wizard/base_export_language_view.xml +++ b/openerp/addons/base/module/wizard/base_export_language_view.xml @@ -1,31 +1,43 @@ - Export Translations base.language.export
- - + + + - - - - - - - - - + + +
+

Export Complete

+

Here is the exported translation file:

+

This file was generated using the universal Unicode/UTF-8 file encoding, please be sure to view and edit + using the same encoding.

+

The next step depends on the file format: +

    +
  • CSV format: you may edit it directly with your favorite spreadsheet software, + the rightmost column (value) contains the translations
  • +
  • PO(T) format: you should edit it with a PO editor such as + POEdit, or your preferred text editor
  • +
  • TGZ format: this is a compressed archive containing a PO file, directly suitable + for uploading to OpenERP's translation platform, + Launchpad
  • +
+

+

For more details about translating OpenERP in your language, please refer to the + documentation.

+
-
-
diff --git a/openerp/addons/base/module/wizard/base_import_language.py b/openerp/addons/base/module/wizard/base_import_language.py index 94b9ae4db59..9c36c9d7c54 100644 --- a/openerp/addons/base/module/wizard/base_import_language.py +++ b/openerp/addons/base/module/wizard/base_import_language.py @@ -29,11 +29,9 @@ class base_language_import(osv.osv_memory): _name = "base.language.import" _description = "Language Import" - _inherit = "ir.wizard.screen" - _columns = { - 'name': fields.char('Language Name',size=64 , required=True), - 'code': fields.char('Code (eg:en__US)',size=5 , required=True), + 'name': fields.char('Language Name', size=64 , required=True), + 'code': fields.char('ISO Code', size=5, help="ISO Language and Country code, e.g. en_US", required=True), 'data': fields.binary('File', required=True), 'overwrite': fields.boolean('Overwrite Existing Terms', help="If you enable this option, existing translations (including custom ones) " @@ -41,31 +39,25 @@ class base_language_import(osv.osv_memory): } def import_lang(self, cr, uid, ids, context=None): - """ - Import Language - @param cr: the current row, from the database cursor. - @param uid: the current user’s ID for security checks. - @param ids: the ID or list of IDs - @param context: A standard dictionary - """ if context is None: context = {} - import_data = self.browse(cr, uid, ids)[0] - if import_data.overwrite: + this = self.browse(cr, uid, ids[0]) + if this.overwrite: context.update(overwrite=True) fileobj = TemporaryFile('w+') - fileobj.write(base64.decodestring(import_data.data)) + try: + fileobj.write(base64.decodestring(this.data)) + + # now we determine the file format + fileobj.seek(0) + first_line = fileobj.readline().strip().replace('"', '').replace(' ', '') + fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po' + fileobj.seek(0) + + tools.trans_load_data(cr, fileobj, fileformat, this.code, lang_name=this.name, context=context) + finally: + fileobj.close() + return True - # now we determine the file format - fileobj.seek(0) - first_line = fileobj.readline().strip().replace('"', '').replace(' ', '') - fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po' - fileobj.seek(0) - - tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name, context=context) - fileobj.close() - return {} - -base_language_import() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_import_language_view.xml b/openerp/addons/base/module/wizard/base_import_language_view.xml index 727de4e381b..5e2aada7766 100644 --- a/openerp/addons/base/module/wizard/base_import_language_view.xml +++ b/openerp/addons/base/module/wizard/base_import_language_view.xml @@ -8,7 +8,7 @@
- + diff --git a/openerp/addons/base/module/wizard/base_language_install.py b/openerp/addons/base/module/wizard/base_language_install.py index 1e40f84cc60..ab65159f0d4 100644 --- a/openerp/addons/base/module/wizard/base_language_install.py +++ b/openerp/addons/base/module/wizard/base_language_install.py @@ -27,9 +27,7 @@ class base_language_install(osv.osv_memory): """ Install Language""" _name = "base.language.install" - _inherit = "ir.wizard.screen" _description = "Install Language" - _columns = { 'lang': fields.selection(tools.scan_languages(),'Language', required=True), 'overwrite': fields.boolean('Overwrite Existing Terms', help="If you check this box, your customized translations will be overwritten and replaced by the official ones."), @@ -63,6 +61,5 @@ class base_language_install(osv.osv_memory): 'target': 'new', 'res_id': ids and ids[0] or False, } -base_language_install() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_module_import.py b/openerp/addons/base/module/wizard/base_module_import.py index 3ef9132af0e..34f22bf82f0 100644 --- a/openerp/addons/base/module/wizard/base_module_import.py +++ b/openerp/addons/base/module/wizard/base_module_import.py @@ -34,9 +34,7 @@ class base_module_import(osv.osv_memory): """ Import Module """ _name = "base.module.import" - _inherit = "ir.wizard.screen" _description = "Import Module" - _columns = { 'module_file': fields.binary('Module .ZIP file', required=True), 'state':fields.selection([('init','init'),('done','done')], @@ -88,7 +86,5 @@ class base_module_import(osv.osv_memory): 'type': 'ir.actions.act_window', } -base_module_import() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_module_update.py b/openerp/addons/base/module/wizard/base_module_update.py index a0d21840d90..0893761cd28 100644 --- a/openerp/addons/base/module/wizard/base_module_update.py +++ b/openerp/addons/base/module/wizard/base_module_update.py @@ -25,7 +25,6 @@ class base_module_update(osv.osv_memory): _name = "base.module.update" _description = "Update Module" - _inherit = "ir.wizard.screen" _columns = { 'update': fields.integer('Number of modules updated', readonly=True), @@ -55,7 +54,4 @@ class base_module_update(osv.osv_memory): } return res - -base_module_update() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/openerp/addons/base/module/wizard/base_update_translations.py b/openerp/addons/base/module/wizard/base_update_translations.py index d24851b1cd5..abfbc445316 100644 --- a/openerp/addons/base/module/wizard/base_update_translations.py +++ b/openerp/addons/base/module/wizard/base_update_translations.py @@ -21,31 +21,28 @@ from osv import osv, fields import tools -import pooler import cStringIO from tools.translate import _ class base_update_translations(osv.osv_memory): def _get_languages(self, cr, uid, context): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) - langs=lang_obj.browse(cr, uid, ids) + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),]) + langs = lang_obj.browse(cr, uid, ids) return [(lang.code, lang.name) for lang in langs] def _get_lang_name(self, cr, uid, lang_code): - lang_obj=pooler.get_pool(cr.dbname).get('res.lang') - ids=lang_obj.search(cr, uid, [('code', '=', lang_code)]) + lang_obj = self.pool.get('res.lang') + ids = lang_obj.search(cr, uid, [('code', '=', lang_code)]) if not ids: raise osv.except_osv(_('Error!'), _('No language with code "%s" exists') % lang_code) lang = lang_obj.browse(cr, uid, ids[0]) return lang.name - def act_cancel(self, cr, uid, ids, context=None): - return {'type': 'ir.actions.act_window_close'} def act_update(self, cr, uid, ids, context=None): this = self.browse(cr, uid, ids)[0] lang_name = self._get_lang_name(cr, uid, this.lang) - buf=cStringIO.StringIO() + buf = cStringIO.StringIO() tools.trans_export(this.lang, ['all'], buf, 'csv', cr) tools.trans_load_data(cr, buf, 'csv', this.lang, lang_name=lang_name) buf.close() @@ -66,11 +63,8 @@ class base_update_translations(osv.osv_memory): return res _name = 'base.update.translations' - _inherit = "ir.wizard.screen" _columns = { 'lang': fields.selection(_get_languages, 'Language', required=True), } -base_update_translations() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/module/wizard/base_update_translations_view.xml b/openerp/addons/base/module/wizard/base_update_translations_view.xml index 4a20ef30bee..18fd30532bd 100644 --- a/openerp/addons/base/module/wizard/base_update_translations_view.xml +++ b/openerp/addons/base/module/wizard/base_update_translations_view.xml @@ -12,7 +12,7 @@ diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index d6b42e970bf..c24fc11a377 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -37,7 +37,6 @@ class res_config_configurable(osv.osv_memory): their view inherit from the related res_config_view_base view. ''' _name = 'res.config' - _inherit = 'ir.wizard.screen' def _next_action(self, cr, uid, context=None): Todos = self.pool['ir.actions.todo'] diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index bb698c2d212..c5c95276b16 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -301,7 +301,7 @@ class res_partner(osv.osv, format_address): def onchange_type(self, cr, uid, ids, is_company, context=None): # get value as for an onchange on the image - value = tools.image_get_resized_images(self._get_default_image(cr, uid, is_company, context), return_big=True) + value = tools.image_get_resized_images(self._get_default_image(cr, uid, is_company, context), return_big=True, return_medium=False, return_small=False) value['title'] = False if is_company: value['parent_id'] = False diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index f324ed67522..ec995c6a8cc 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -51,6 +51,7 @@ import pickle import re import simplejson import time +import traceback import types from lxml import etree @@ -371,8 +372,10 @@ class browse_record(object): else: return attr else: - error_msg = "Field '%s' does not exist in object '%s'" % (name, self) + error_msg = "Field '%s' does not exist in object '%s'" % (name, self) self.__logger.warning(error_msg) + if self.__logger.isEnabledFor(logging.DEBUG): + self.__logger.debug(''.join(traceback.format_stack())) raise KeyError(error_msg) # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields @@ -709,7 +712,7 @@ class BaseModel(object): def log(self, cr, uid, id, message, secondary=False, context=None): return _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.") - + def view_init(self, cr, uid, fields_list, context=None): """Override this method to do specific things when a view on the object is opened.""" pass @@ -899,7 +902,7 @@ class BaseModel(object): # If new class defines a constraint with # same function name, we let it override # the old one. - + new[c2] = c exist = True break @@ -1593,7 +1596,7 @@ class BaseModel(object): def user_has_groups(self, cr, uid, groups, context=None): """Return true if the user is at least member of one of the groups in groups_str. Typically used to resolve ``groups`` attribute - in view and model definitions. + in view and model definitions. :param str groups: comma-separated list of fully-qualified group external IDs, e.g.: ``base.group_user,base.group_system`` @@ -1636,7 +1639,7 @@ class BaseModel(object): the field should be completely removed from the view, as it is completely unavailable for non-members - :return: True if field should be included in the result of fields_view_get + :return: True if field should be included in the result of fields_view_get """ if node.tag == 'field' and node.get('name') in self._all_columns: column = self._all_columns[node.get('name')].column @@ -1911,7 +1914,7 @@ class BaseModel(object): def _get_default_calendar_view(self, cr, user, context=None): """ Generates a default calendar view by trying to infer calendar fields from a number of pre-set attribute names - + :param cr: database cursor :param int user: user id :param dict context: connection context @@ -1990,7 +1993,7 @@ class BaseModel(object): def raise_view_error(error_msg, child_view_id): view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context) - error_msg = error_msg % {'parent_xml_id': view.xml_id} + error_msg = error_msg % {'parent_xml_id': view.xml_id} raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" % (child_view.xml_id, self._name, error_msg)) @@ -2026,7 +2029,7 @@ class BaseModel(object): if all(node.get(attr) == spec.get(attr) \ for attr in spec.attrib if attr not in ('position','version')): - # Version spec should match parent's root element's version + # Version spec should match parent's root element's version if spec.get('version') and spec.get('version') != source.get('version'): return None return node @@ -2100,7 +2103,7 @@ class BaseModel(object): raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ (tag, spec.get('version'), source.get('version')), inherit_id) raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id) - + return source def apply_view_inheritance(cr, user, source, inherit_id): @@ -2315,7 +2318,7 @@ class BaseModel(object): or ``'='``. :param int limit: optional max number of records to return :rtype: list - :return: list of pairs ``(id,text_repr)`` for all matching records. + :return: list of pairs ``(id,text_repr)`` for all matching records. """ return self._name_search(cr, user, name, args, operator, context, limit) @@ -2445,7 +2448,7 @@ class BaseModel(object): # This is useful to implement kanban views for instance, where all columns # should be displayed even if they don't contain any record. - # Grab the list of all groups that should be displayed, including all present groups + # Grab the list of all groups that should be displayed, including all present groups present_group_ids = [x[groupby][0] for x in read_group_result if x[groupby]] all_groups,folded = self._group_by_full[groupby](self, cr, uid, present_group_ids, domain, read_group_order=read_group_order, @@ -2821,28 +2824,28 @@ class BaseModel(object): def _m2o_fix_foreign_key(self, cr, source_table, source_field, dest_model, ondelete): # Find FK constraint(s) currently established for the m2o field, - # and see whether they are stale or not + # and see whether they are stale or not cr.execute("""SELECT confdeltype as ondelete_rule, conname as constraint_name, cl2.relname as foreign_table FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2 - WHERE con.conrelid = cl1.oid - AND cl1.relname = %s - AND con.confrelid = cl2.oid - AND array_lower(con.conkey, 1) = 1 - AND con.conkey[1] = att1.attnum - AND att1.attrelid = cl1.oid - AND att1.attname = %s - AND array_lower(con.confkey, 1) = 1 - AND con.confkey[1] = att2.attnum - AND att2.attrelid = cl2.oid - AND att2.attname = %s + WHERE con.conrelid = cl1.oid + AND cl1.relname = %s + AND con.confrelid = cl2.oid + AND array_lower(con.conkey, 1) = 1 + AND con.conkey[1] = att1.attnum + AND att1.attrelid = cl1.oid + AND att1.attname = %s + AND array_lower(con.confkey, 1) = 1 + AND con.confkey[1] = att2.attnum + AND att2.attrelid = cl2.oid + AND att2.attname = %s AND con.contype = 'f'""", (source_table, source_field, 'id')) constraints = cr.dictfetchall() if constraints: if len(constraints) == 1: # Is it the right constraint? - cons, = constraints + cons, = constraints if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\ or cons['foreign_table'] != dest_model._table: _schema.debug("Table '%s': dropping obsolete FK constraint: '%s'", @@ -2964,14 +2967,14 @@ class BaseModel(object): ('numeric', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ] - if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size < f.size: + if f_pg_type == 'varchar' and f._type == 'char' and ((f.size is None and f_pg_size) or f_pg_size < f.size): cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k)) cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size))) cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size))) cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,)) cr.commit() _schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s", - self._table, k, f_pg_size, f.size) + self._table, k, f_pg_size or 'unlimited', f.size or 'unlimited') for c in casts: if (f_pg_type==c[0]) and (f._type==c[1]): if f_pg_type != f_obj_type: @@ -3204,8 +3207,7 @@ class BaseModel(object): # attlen is the number of bytes necessary to represent the type when # the type has a fixed size. If the type has a varying size attlen is # -1 and atttypmod is the size limit + 4, or -1 if there is no limit. - # Thus the query can return a negative size for a unlimited varchar. - cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN a.atttypmod-4 ELSE a.attlen END as size " \ + cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN (CASE WHEN a.atttypmod=-1 THEN 0 ELSE a.atttypmod-4 END) ELSE a.attlen END as size " \ "FROM pg_class c,pg_attribute a,pg_type t " \ "WHERE c.relname=%s " \ "AND c.oid=a.attrelid " \ @@ -3215,7 +3217,7 @@ class BaseModel(object): def _o2m_raise_on_missing_reference(self, cr, f): # TODO this check should be a method on fields.one2many. - + other = self.pool.get(f._obj) if other: # TODO the condition could use fields_get_keys(). @@ -3732,7 +3734,7 @@ class BaseModel(object): the length of `ids`, and raise an appropriate exception if it does not. """ if cr.rowcount != len(ids): - # Attempt to distinguish record rule restriction vs deleted records, + # Attempt to distinguish record rule restriction vs deleted records, # to provide a more specific error message cr.execute('SELECT id FROM ' + self._table + ' WHERE id IN %s', (tuple(ids),)) if cr.rowcount != len(ids): @@ -3787,13 +3789,13 @@ class BaseModel(object): self._check_record_rules_result_count(cr, uid, sub_ids, operation, context=context) def _workflow_trigger(self, cr, uid, ids, trigger, context=None): - """Call given workflow trigger as a result of a CRUD operation""" + """Call given workflow trigger as a result of a CRUD operation""" wf_service = netsvc.LocalService("workflow") for res_id in ids: getattr(wf_service, trigger)(uid, self._name, res_id, cr) def _workflow_signal(self, cr, uid, ids, signal, context=None): - """Send given workflow signal""" + """Send given workflow signal""" wf_service = netsvc.LocalService("workflow") for res_id in ids: wf_service.trg_validate(uid, self._name, res_id, signal, cr) @@ -3824,7 +3826,7 @@ class BaseModel(object): self.check_access_rights(cr, uid, 'unlink') ir_property = self.pool.get('ir.property') - + # Check if the records are used as default properties. domain = [('res_id', '=', False), ('value_reference', 'in', ['%s,%s' % (self._name, i) for i in ids]), @@ -4952,7 +4954,7 @@ class BaseModel(object): :return: map of ids to their fully qualified XML ID, defaulting to an empty string when there's none - (to be usable as a function field), + (to be usable as a function field), e.g.:: { 'id': 'module.ext_id', diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 5239b37bd2d..e7476827093 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -33,6 +33,7 @@ import logging import tarfile import tempfile import threading +from babel.messages import extract from os.path import join from datetime import datetime @@ -47,6 +48,9 @@ from openerp import SUPERUSER_ID _logger = logging.getLogger(__name__) +# used to notify web client that these translations should be loaded in the UI +WEB_TRANSLATION_COMMENT = "openerp-web" + _LOCALE2WIN32 = { 'af_ZA': 'Afrikaans_South Africa', 'sq_AL': 'Albanian_Albania', @@ -262,7 +266,7 @@ class TinyPoFile(object): self.lines_count = len(self.lines); self.first = True - self.tnrs= [] + self.extra_lines= [] return self def _get_lines(self): @@ -278,14 +282,14 @@ class TinyPoFile(object): return (self.lines_count - len(self.lines)) def next(self): - type = name = res_id = source = trad = None - - if self.tnrs: - type, name, res_id, source, trad = self.tnrs.pop(0) + trans_type = name = res_id = source = trad = None + if self.extra_lines: + trans_type, name, res_id, source, trad, comments = self.extra_lines.pop(0) if not res_id: res_id = '0' else: - tmp_tnrs = [] + comments = [] + targets = [] line = None fuzzy = False while (not line): @@ -295,15 +299,20 @@ class TinyPoFile(object): while line.startswith('#'): if line.startswith('#~ '): break - if line.startswith('#:'): + if line.startswith('#.'): + line = line[2:].strip() + if not line.startswith('module:'): + comments.append(line) + elif line.startswith('#:'): for lpart in line[2:].strip().split(' '): trans_info = lpart.strip().split(':',2) if trans_info and len(trans_info) == 2: - # looks like the translation type is missing, which is not + # looks like the translation trans_type is missing, which is not # unexpected because it is not a GetText standard. Default: 'code' trans_info[:0] = ['code'] if trans_info and len(trans_info) == 3: - tmp_tnrs.append(trans_info) + # this is a ref line holding the destination info (model, field, record) + targets.append(trans_info) elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'): fuzzy = True line = self.lines.pop(0).strip() @@ -326,7 +335,7 @@ class TinyPoFile(object): # if the source is "" and it's the first msgid, it's the special # msgstr with the informations about the traduction and the # traductor; we skip it - self.tnrs = [] + self.extra_lines = [] while line: line = self.lines.pop(0).strip() return self.next() @@ -343,10 +352,11 @@ class TinyPoFile(object): trad += unquote(line) line = self.lines.pop(0).strip() - if tmp_tnrs and not fuzzy: - type, name, res_id = tmp_tnrs.pop(0) - for t, n, r in tmp_tnrs: - self.tnrs.append((t, n, r, source, trad)) + if targets and not fuzzy: + trans_type, name, res_id = targets.pop(0) + for t, n, r in targets: + if t == trans_type == 'code': continue + self.extra_lines.append((t, n, r, source, trad, comments)) self.first = False @@ -355,7 +365,7 @@ class TinyPoFile(object): self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', self.cur_line(), source[:30]) return self.next() - return type, name, res_id, source, trad + return trans_type, name, res_id, source, trad, '\n'.join(comments) def write_infos(self, modules): import openerp.release as release @@ -384,11 +394,13 @@ class TinyPoFile(object): } ) - def write(self, modules, tnrs, source, trad): + def write(self, modules, tnrs, source, trad, comments=None): plurial = len(modules) > 1 and 's' or '' self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules))) + if comments: + self.buffer.write(''.join(('#. %s\n' % c for c in comments))) code = False for typy, name, res_id in tnrs: @@ -415,44 +427,43 @@ class TinyPoFile(object): def trans_export(lang, modules, buffer, format, cr): - def _process(format, modules, rows, buffer, lang, newlang): + def _process(format, modules, rows, buffer, lang): if format == 'csv': - writer=csv.writer(buffer, 'UNIX') - for row in rows: - writer.writerow(row) + writer = csv.writer(buffer, 'UNIX') + # write header first + writer.writerow(("module","type","name","res_id","src","value")) + for module, type, name, res_id, src, trad, comments in rows: + # Comments are ignored by the CSV writer + writer.writerow((module, type, name, res_id, src, trad)) elif format == 'po': - rows.pop(0) writer = TinyPoFile(buffer) writer.write_infos(modules) # we now group the translations by source. That means one translation per source. grouped_rows = {} - for module, type, name, res_id, src, trad in rows: + for module, type, name, res_id, src, trad, comments in rows: row = grouped_rows.setdefault(src, {}) row.setdefault('modules', set()).add(module) if ('translation' not in row) or (not row['translation']): row['translation'] = trad row.setdefault('tnrs', []).append((type, name, res_id)) + row.setdefault('comments', set()).update(comments) for src, row in grouped_rows.items(): - writer.write(row['modules'], row['tnrs'], src, row['translation']) + writer.write(row['modules'], row['tnrs'], src, row['translation'], row['comments']) elif format == 'tgz': - rows.pop(0) rows_by_module = {} for row in rows: module = row[0] - # first row is the "header", as in csv, it will be popped - rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],]) - rows_by_module[module].append(row) - + rows_by_module.setdefault(module, []).append(row) tmpdir = tempfile.mkdtemp() for mod, modrows in rows_by_module.items(): tmpmoddir = join(tmpdir, mod, 'i18n') os.makedirs(tmpmoddir) - pofilename = (newlang and mod or lang) + ".po" + (newlang and 't' or '') + pofilename = (lang if lang else mod) + ".po" + ('t' if not lang else '') buf = file(join(tmpmoddir, pofilename), 'w') - _process('po', [mod], modrows, buf, lang, newlang) + _process('po', [mod], modrows, buf, lang) buf.close() tar = tarfile.open(fileobj=buffer, mode='w|gz') @@ -463,16 +474,15 @@ def trans_export(lang, modules, buffer, format, cr): raise Exception(_('Unrecognized extension: must be one of ' '.csv, .po, or .tgz (received .%s).' % format)) - newlang = not bool(lang) - if newlang: - lang = 'en_US' - trans = trans_generate(lang, modules, cr) - if newlang and format!='csv': - for trx in trans: - trx[-1] = '' - modules = set([t[0] for t in trans[1:]]) - _process(format, modules, trans, buffer, lang, newlang) - del trans + trans_lang = lang + if not trans_lang and format == 'csv': + # CSV files are meant for translators and they need a starting point, + # so we at least put the original term in the translation column + trans_lang = 'en_US' + translations = trans_generate(lang, modules, cr) + modules = set([t[0] for t in translations[1:]]) + _process(format, modules, translations, buffer, lang) + del translations def trans_parse_xsl(de): res = [] @@ -535,6 +545,46 @@ def in_modules(object_name, modules): module = module_dict.get(module, module) return module in modules + +def babel_extract_qweb(fileobj, keywords, comment_tags, options): + """Babel message extractor for qweb template files. + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should + be recognized as translation functions + :param comment_tags: a list of translator tags to search for and + include in the results + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` + tuples + :rtype: ``iterator`` + """ + result = [] + def handle_text(text, lineno): + text = (text or "").strip() + if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc. + result.append((lineno, None, text, [])) + + # not using elementTree.iterparse because we need to skip sub-trees in case + # the ancestor element had a reason to be skipped + def iter_elements(current_element): + for el in current_element: + if isinstance(el, SKIPPED_ELEMENT_TYPES): continue + if "t-js" not in el.attrib and \ + not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \ + not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"): + handle_text(el.text, el.sourceline) + for att in ('title', 'alt', 'label', 'placeholder'): + if att in el.attrib: + handle_text(el.attrib[att], el.sourceline) + iter_elements(el) + handle_text(el.tail, el.sourceline) + + tree = etree.parse(fileobj) + iter_elements(tree.getroot()) + + return result + + def trans_generate(lang, modules, cr): dbname = cr.dbname @@ -566,8 +616,8 @@ def trans_generate(lang, modules, cr): cr.execute(query, query_param) _to_translate = [] - def push_translation(module, type, name, id, source): - tuple = (module, source, name, id, type) + def push_translation(module, type, name, id, source, comments=None): + tuple = (module, source, name, id, type, comments or []) if source and tuple not in _to_translate: _to_translate.append(tuple) @@ -717,7 +767,7 @@ def trans_generate(lang, modules, cr): if not hasattr(msg, '__call__'): push_translation(module, term_type, model, 0, encode(msg)) - for (model_id, model, module) in cr.fetchall(): + for (_, model, module) in cr.fetchall(): module = encode(module) model = encode(model) @@ -733,7 +783,6 @@ def trans_generate(lang, modules, cr): for constraint in getattr(model_obj, '_sql_constraints', []): push_constraint_msg(module, 'sql_constraint', model, constraint[2]) - # parse source code for _() calls def get_module_from_path(path, mod_paths=None): if not mod_paths: # First, construct a list of possible paths @@ -771,92 +820,71 @@ def trans_generate(lang, modules, cr): _logger.debug("Scanning modules at paths: ", path_list) mod_paths = [] - join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL) - join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL) - re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL) - re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL) - def export_code_terms_from_file(fname, path, root, terms_type): + def verified_module_filepaths(fname, path, root): fabsolutepath = join(root, fname) frelativepath = fabsolutepath[len(path):] + display_path = "addons%s" % frelativepath module = get_module_from_path(fabsolutepath, mod_paths=mod_paths) - is_mod_installed = module in installed_modules - if (('all' in modules) or (module in modules)) and is_mod_installed: - _logger.debug("Scanning code of %s at module: %s", frelativepath, module) - src_file = misc.file_open(fabsolutepath, subdir='') + if (('all' in modules) or (module in modules)) and module in installed_modules: + return module, fabsolutepath, frelativepath, display_path + return None, None, None, None + + def babel_extract_terms(fname, path, root, extract_method="python", trans_type='code', + extra_comments=None, extract_keywords={'_': None}): + module, fabsolutepath, _, display_path = verified_module_filepaths(fname, path, root) + extra_comments = extra_comments or [] + if module: + src_file = open(fabsolutepath, 'r') try: - code_string = src_file.read() + for lineno, message, comments in extract.extract(extract_method, src_file, + keywords=extract_keywords): + push_translation(module, trans_type, display_path, lineno, + encode(message), comments + extra_comments) finally: src_file.close() - if module in installed_modules: - frelativepath = str("addons" + frelativepath) - ite = re_dquotes.finditer(code_string) - code_offset = 0 - code_line = 1 - for i in ite: - src = i.group(1) - if src.startswith('""'): - assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30]) - src = src[2:-2] - else: - src = join_dquotes.sub(r'\1', src) - # try to count the lines from the last pos to our place: - code_line += code_string[code_offset:i.start(1)].count('\n') - # now, since we did a binary read of a python source file, we - # have to expand pythonic escapes like the interpreter does. - src = src.decode('string_escape') - push_translation(module, terms_type, frelativepath, code_line, encode(src)) - code_line += i.group(1).count('\n') - code_offset = i.end() # we have counted newlines up to the match end - - ite = re_quotes.finditer(code_string) - code_offset = 0 #reset counters - code_line = 1 - for i in ite: - src = i.group(1) - if src.startswith("''"): - assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30]) - src = src[2:-2] - else: - src = join_quotes.sub(r'\1', src) - code_line += code_string[code_offset:i.start(1)].count('\n') - src = src.decode('string_escape') - push_translation(module, terms_type, frelativepath, code_line, encode(src)) - code_line += i.group(1).count('\n') - code_offset = i.end() # we have counted newlines up to the match end for path in path_list: _logger.debug("Scanning files of modules at %s", path) for root, dummy, files in osutil.walksymlinks(path): - for fname in itertools.chain(fnmatch.filter(files, '*.py')): - export_code_terms_from_file(fname, path, root, 'code') - for fname in itertools.chain(fnmatch.filter(files, '*.mako')): - export_code_terms_from_file(fname, path, root, 'report') + for fname in fnmatch.filter(files, '*.py'): + babel_extract_terms(fname, path, root) + for fname in fnmatch.filter(files, '*.mako'): + babel_extract_terms(fname, path, root, trans_type='report') + # Javascript source files in the static/src/js directory, rest is ignored (libs) + if fnmatch.fnmatch(root, '*/static/src/js*'): + for fname in fnmatch.filter(files, '*.js'): + babel_extract_terms(fname, path, root, 'javascript', + extra_comments=[WEB_TRANSLATION_COMMENT], + extract_keywords={'_t': None}) + # QWeb template files + if fnmatch.fnmatch(root, '*/static/src/xml*'): + for fname in fnmatch.filter(files, '*.xml'): + babel_extract_terms(fname, path, root, 'openerp.tools.translate:babel_extract_qweb', + extra_comments=[WEB_TRANSLATION_COMMENT]) - - out = [["module","type","name","res_id","src","value"]] # header + out = [] _to_translate.sort() # translate strings marked as to be translated - for module, source, name, id, type in _to_translate: - trans = trans_obj._get_source(cr, uid, name, type, lang, source) - out.append([module, type, name, id, source, encode(trans) or '']) - + for module, source, name, id, type, comments in _to_translate: + trans = '' if not lang else trans_obj._get_source(cr, uid, name, type, lang, source) + out.append([module, type, name, id, source, encode(trans) or '', comments]) return out -def trans_load(cr, filename, lang, verbose=True, context=None): +def trans_load(cr, filename, lang, verbose=True, module_name=None, context=None): try: fileobj = misc.file_open(filename) _logger.info("loading %s", filename) fileformat = os.path.splitext(filename)[-1][1:].lower() - r = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, context=context) + result = trans_load_data(cr, fileobj, fileformat, lang, verbose=verbose, module_name=module_name, context=context) fileobj.close() - return r + return result except IOError: if verbose: _logger.error("couldn't read translation file %s", filename) return None -def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None): +def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, module_name=None, context=None): """Populates the ir_translation table.""" if verbose: _logger.info('loading translation file for language %s', lang) @@ -868,8 +896,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, trans_obj = pool.get('ir.translation') iso_lang = misc.get_iso_codes(lang) try: - uid = 1 - ids = lang_obj.search(cr, uid, [('code','=', lang)]) + ids = lang_obj.search(cr, SUPERUSER_ID, [('code','=', lang)]) if not ids: # lets create the language with locale information @@ -886,14 +913,14 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, break elif fileformat == 'po': reader = TinyPoFile(fileobj) - f = ['type', 'name', 'res_id', 'src', 'value'] + f = ['type', 'name', 'res_id', 'src', 'value', 'comments'] else: _logger.error('Bad file format: %s', fileformat) raise Exception(_('Bad file format')) # read the rest of the file line = 1 - irt_cursor = trans_obj._get_import_cursor(cr, uid, context=context) + irt_cursor = trans_obj._get_import_cursor(cr, SUPERUSER_ID, context=context) for row in reader: line += 1 @@ -903,39 +930,32 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # dictionary which holds values for this line of the csv file # {'lang': ..., 'type': ..., 'name': ..., 'res_id': ..., - # 'src': ..., 'value': ...} - dic = {'lang': lang} - dic_module = False - for i in range(len(f)): - if f[i] in ('module',): - continue - dic[f[i]] = row[i] + # 'src': ..., 'value': ..., 'module':...} + dic = dict.fromkeys(('name', 'res_id', 'src', 'type', 'imd_model', 'imd_name', 'module', 'value', 'comments')) + dic['lang'] = lang + for i, field in enumerate(f): + dic[field] = row[i] # This would skip terms that fail to specify a res_id - if not dic.get('res_id', False): + if not dic.get('res_id'): continue res_id = dic.pop('res_id') if res_id and isinstance(res_id, (int, long)) \ or (isinstance(res_id, basestring) and res_id.isdigit()): dic['res_id'] = int(res_id) + dic['module'] = module_name else: - try: - tmodel = dic['name'].split(',')[0] - if '.' in res_id: - tmodule, tname = res_id.split('.', 1) - else: - tmodule = dic_module - tname = res_id - dic['imd_model'] = tmodel - dic['imd_module'] = tmodule - dic['imd_name'] = tname - - dic['res_id'] = None - except Exception: - _logger.warning("Could not decode resource for %s, please fix the po file.", - dic['res_id'], exc_info=True) - dic['res_id'] = None + tmodel = dic['name'].split(',')[0] + if '.' in res_id: + tmodule, tname = res_id.split('.', 1) + else: + tmodule = False + tname = res_id + dic['imd_model'] = tmodel + dic['imd_name'] = tname + dic['module'] = tmodule + dic['res_id'] = None irt_cursor.push(dic) diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index aec17d3c31f..7e1c7152b0f 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -313,13 +313,13 @@ class YamlInterpreter(object): #context = self.get_context(record, self.eval_context) #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module context = record.context + view_info = False if view_id: varg = view_id if view_id is True: varg = False - view = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context) - view_id = etree.fromstring(view['arch'].encode('utf-8')) + view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context) - record_dict = self._create_record(model, fields, view_id, default=default) + record_dict = self._create_record(model, fields, view_info, default=default) _logger.debug("RECORD_DICT %s" % record_dict) id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \ self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context) @@ -327,16 +327,18 @@ class YamlInterpreter(object): if config.get('import_partial'): self.cr.commit() - def _create_record(self, model, fields, view=False, parent={}, default=True): - if view is not False: - defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {} - fg = model.fields_get(self.cr, SUPERUSER_ID, context=self.context) - else: - defaults = {} - fg = {} - record_dict = {} - fields = fields or {} - + def _create_record(self, model, fields, view_info=False, parent={}, default=True): + """This function processes the !record tag in yalm 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. + :param model: model instance + :param fields: dictonary mapping the field names and their values + :param view_info: result of fields_view_get() called on the object + :param parent: dictionary containing the values already computed for the parent, in case of one2many fields + :param default: if True, the default values must be processed too or not + :return: dictionary mapping the field names and their values, ready to use when calling the create() function + :rtype: dict + """ def process_val(key, val): if fg[key]['type']=='many2one': if type(val) in (tuple,list): @@ -348,63 +350,75 @@ class YamlInterpreter(object): val = map(lambda x: (0,0,x), val) return val - # Process all on_change calls - nodes = (view is not False) and [view] or [] - while nodes: - el = nodes.pop(0) - if el.tag=='field': - field_name = el.attrib['name'] - assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name) - if field_name in fields: - view2 = None - # if the form view is not inline, we call fields_view_get - if (view is not False) and (fg[field_name]['type']=='one2many'): - view2 = view.find("field[@name='%s']/form"%(field_name,)) - if not view2: - view2 = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, 'form', self.context) - view2 = etree.fromstring(view2['arch'].encode('utf-8')) + view = view_info and etree.fromstring(view_info['arch'].encode('utf-8')) or False + fields = fields or {} + if view is not False: + fg = view_info['fields'] + # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may + # have references like `base.main_company´ in the yaml file and it's not compatible with the function) + defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {} - field_value = self._eval_field(model, field_name, fields[field_name], view2, parent=record_dict, default=default) - record_dict[field_name] = field_value - #if (field_name in defaults) and defaults[field_name] == field_value: - # print '*** You can remove these lines:', field_name, field_value - elif (field_name in defaults): - if (field_name not in record_dict): - record_dict[field_name] = process_val(field_name, defaults[field_name]) - else: - continue + # copy the default values in record_dict, only if they are in the view (because that's what the client does) + # the other default values will be added later on by the create(). + record_dict = dict([(key, val) for key, val in defaults.items() if key in fg]) - if not el.attrib.get('on_change', False): - continue - match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change']) - assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], ) + # Process all on_change calls + nodes = [view] + while nodes: + el = nodes.pop(0) + if el.tag=='field': + field_name = el.attrib['name'] + assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name) + if field_name in fields: + one2many_form_view = None + if (view is not False) and (fg[field_name]['type']=='one2many'): + # for one2many fields, we want to eval them using the inline form view defined on the parent + one2many_form_view = view_info['fields'][field_name]['views'].get('form') + # if the form view is not defined inline, we call fields_view_get() + if not one2many_form_view: + one2many_form_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, 'form', self.context) - # creating the context - class parent2(object): - def __init__(self, d): - self.d = d - def __getattr__(self, name): - return self.d.get(name, False) + field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default) + record_dict[field_name] = field_value + #if (field_name in defaults) and defaults[field_name] == field_value: + # print '*** You can remove these lines:', field_name, field_value - ctx = record_dict.copy() - ctx['context'] = self.context - ctx['uid'] = 1 - ctx['parent'] = parent2(parent) - for a in fg: - if a not in ctx: - ctx[a]=process_val(a, defaults.get(a, False)) + #if field_name has a default value or a value is given in the yaml file, we must call its on_change() + elif field_name not in defaults: + continue - # Evaluation args - args = map(lambda x: eval(x, ctx), match.group(2).split(',')) - result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args) - for key, val in (result or {}).get('value', {}).items(): - if key not in fields: - assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist on the object '%s'" % (key, match.group(1), model._name) + if not el.attrib.get('on_change', False): + continue + match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change']) + assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], ) + + # creating the context + class parent2(object): + def __init__(self, d): + self.d = d + def __getattr__(self, name): + return self.d.get(name, False) + + ctx = record_dict.copy() + ctx['context'] = self.context + ctx['uid'] = SUPERUSER_ID + ctx['parent'] = parent2(parent) + for a in fg: + if a not in ctx: + ctx[a]=process_val(a, defaults.get(a, False)) + + # Evaluation args + args = map(lambda x: eval(x, ctx), match.group(2).split(',')) + result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args) + for key, val in (result or {}).get('value', {}).items(): + assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name']) record_dict[key] = process_val(key, val) #if (key in fields) and record_dict[key] == process_val(key, val): # print '*** You can remove these lines:', key, val - else: - nodes = list(el) + nodes + else: + nodes = list(el) + nodes + else: + record_dict = {} for field_name, expression in fields.items(): if field_name in record_dict: @@ -440,7 +454,7 @@ class YamlInterpreter(object): def process_eval(self, node): return eval(node.expression, self.eval_context) - def _eval_field(self, model, field_name, expression, view=False, parent={}, default=True): + def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True): # TODO this should be refactored as something like model.get_field() in bin/osv if field_name in model._columns: column = model._columns[field_name] @@ -461,7 +475,7 @@ class YamlInterpreter(object): value = self.get_id(expression) elif column._type == "one2many": other_model = self.get_model(column._obj) - value = [(0, 0, self._create_record(other_model, fields, view, parent, default=default)) for fields in expression] + value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression] elif column._type == "many2many": ids = [self.get_id(xml_id) for xml_id in expression] value = [(6, 0, ids)]