@@ -195,9 +195,8 @@
@@ -223,14 +226,9 @@
Change My Preferences
ir.actions.act_window
res.users
+
new
form
-
form,tree
-
[('id','=',uid)]
-
-
-
- tree
-
+ form
diff --git a/openerp/addons/base/test/base_test.xml b/openerp/addons/base/test/base_test.xml
index d3c1df9bd21..27bcabfc8f9 100644
--- a/openerp/addons/base/test/base_test.xml
+++ b/openerp/addons/base/test/base_test.xml
@@ -7,7 +7,7 @@
-->
- ASUStek
+ ASUSTeK
diff --git a/openerp/addons/base/test/test_osv_expression.yml b/openerp/addons/base/test/test_osv_expression.yml
index fa7a69b74e9..03c5bad9328 100644
--- a/openerp/addons/base/test/test_osv_expression.yml
+++ b/openerp/addons/base/test/test_osv_expression.yml
@@ -9,14 +9,14 @@
Test hierarchical search in M2M with child ID1
-
!python {model: res.partner }: |
- ids = self.search(cr, uid, [('category_id', 'child_of','Components Supplier')])
+ ids = self.search(cr, uid, [('category_id', 'child_of','Customer')])
assert len(ids) >= 1, ids
-
Test hierarchical search in M2M with child ID2
-
!python {model: res.partner }: |
- ids = self.search(cr, uid, [('category_id', 'child_of','Miscellaneous Suppliers')])
- assert len(ids) >= 1, ids
+ ids = self.search(cr, uid, [('category_id', 'child_of','Manufacturer')])
+ assert len(ids) == 2, ids
-
"1.0 Setup test partner categories: parent root"
@@ -464,8 +464,8 @@
!python {model: res.partner }: |
all_ids = self.search(cr, uid, [('name', '=like', 'A_e_or')])
assert len(all_ids) == 1, "Must match one partner (Axelor), got %r"%all_ids
- all_ids = self.search(cr, uid, [('name', '=ilike', 'm_____')])
- assert len(all_ids) == 1, "Must match *only* one partner (Maxtor), got %r"%all_ids
+ all_ids = self.search(cr, uid, [('name', '=ilike', 'v%')])
+ assert len(all_ids) >= 1, "Must match one partner (Vicking Direct), got %r"%all_ids
-
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on translated field.
-
@@ -512,7 +512,7 @@
Testing for Many2Many field with category supplier and active=False
-
!python {model: res.partner }: |
- vals = {'category_id': [(6, 0, [ref("base.res_partner_category_8")])],
+ vals = {'category_id': [(6, 0, [ref("base.res_partner_category_1")])],
'name': 'OpenERP Test',
'active': False,
'child_ids': [(0, 0, {'name': 'address of OpenERP Test', 'country_id': ref("base.be")})]
diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py
index f28c397bb23..56265e353a2 100644
--- a/openerp/modules/__init__.py
+++ b/openerp/modules/__init__.py
@@ -24,11 +24,7 @@
"""
-import openerp.modules.db
-import openerp.modules.graph
-import openerp.modules.loading
-import openerp.modules.migration
-import openerp.modules.module
+from . import db, graph, loading, migration, module, registry
# TODO temporarily expose those things
from openerp.modules.module import \
diff --git a/openerp/modules/db.py b/openerp/modules/db.py
index 7490631a3b9..1bf7b5237f1 100644
--- a/openerp/modules/db.py
+++ b/openerp/modules/db.py
@@ -68,10 +68,7 @@ def initialize(cr):
category_id = create_categories(cr, categories)
if info['installable']:
- if info['auto_install'] and not info['depends']:
- state = 'to install'
- else:
- state = 'uninstalled'
+ state = 'uninstalled'
else:
state = 'uninstallable'
@@ -95,7 +92,19 @@ def initialize(cr):
for d in dependencies:
cr.execute('INSERT INTO ir_module_module_dependency \
(module_id,name) VALUES (%s, %s)', (id, d))
- cr.commit()
+
+ # Install recursively all auto-installing modules
+ while True:
+ cr.execute("""SELECT m.name FROM ir_module_module m WHERE m.auto_install AND state != 'to install'
+ AND NOT EXISTS (
+ SELECT 1 FROM ir_module_module_dependency d JOIN ir_module_module mdep ON (d.name = mdep.name)
+ WHERE d.module_id = m.id AND mdep.state != 'to install'
+ )""")
+ to_auto_install = [x[0] for x in cr.fetchall()]
+ if not to_auto_install: break
+ cr.execute("""UPDATE ir_module_module SET state='to install' WHERE name in %s""", (tuple(to_auto_install),))
+
+ cr.commit()
def create_categories(cr, categories):
""" Create the ir_module_category entries for some categories.
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index fb13707d13d..4ff23956050 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -171,11 +171,6 @@ class integer(_column):
def __init__(self, string='unknown', required=False, **args):
super(integer, self).__init__(string=string, required=required, **args)
- if required:
- _logger.debug(
- "required=True is deprecated: making an integer field"
- " `required` has no effect, as NULL values are "
- "automatically turned into 0.")
class reference(_column):
_type = 'reference'
@@ -246,11 +241,6 @@ class float(_column):
self.digits = digits
# synopsis: digits_compute(cr) -> (precision, scale)
self.digits_compute = digits_compute
- if required:
- _logger.debug(
- "required=True is deprecated: making a float field"
- " `required` has no effect, as NULL values are "
- "automatically turned into 0.0.")
def digits_change(self, cr):
if self.digits_compute:
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index d8861a20c26..563c514726c 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -2442,9 +2442,9 @@ class BaseModel(object):
context=context)
result_template = dict.fromkeys(aggregated_fields, False)
- result_template.update({groupby + '_count':0})
+ result_template[groupby + '_count'] = 0
if groupby_list and len(groupby_list) > 1:
- result_template.update(__context={'group_by': groupby_list[1:]})
+ result_template['__context'] = {'group_by': groupby_list[1:]}
# Merge the left_side (current results as dicts) with the right_side (all
# possible values as m2o pairs). Both lists are supposed to be using the
@@ -2463,10 +2463,8 @@ class BaseModel(object):
grouped_value = right_side[0]
if not grouped_value in known_values:
line = dict(result_template)
- line.update({
- groupby: right_side,
- '__domain': [(groupby,'=',grouped_value)] + domain,
- })
+ line[groupby] = right_side
+ line['__domain'] = [(groupby,'=',grouped_value)] + domain
result.append(line)
known_values[grouped_value] = line
while read_group_result or all_groups:
@@ -4197,7 +4195,7 @@ class BaseModel(object):
cr.execute("SELECT nextval('"+self._sequence+"')")
except:
raise except_orm(_('UserError'),
- _('You cannot perform this operation. New Record Creation is not allowed for this object as this object is for reporting purpose.'))
+ _('You cannot perform this operation. New Record Creation is not allowed for this object as this object is for reporting purpose.'))
id_new = cr.fetchone()[0]
for table in tocreate:
diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py
index f643b60c297..2f7f6624885 100644
--- a/openerp/tests/__init__.py
+++ b/openerp/tests/__init__.py
@@ -8,20 +8,17 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
See the :ref:`test-framework` section in the :ref:`features` list.
"""
-import test_expression
-import test_ir_sequence
-import test_orm
-import test_view_validation
-import test_uninstall
+from . import test_expression, test_ir_sequence, test_orm,\
+ test_view_validation, test_uninstall
fast_suite = [
test_ir_sequence,
- ]
+]
checks = [
test_expression,
test_orm,
test_view_validation,
- ]
+]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_exceptions/view.xml b/openerp/tests/addons/test_exceptions/view.xml
index 891316f92c4..180029e739a 100644
--- a/openerp/tests/addons/test_exceptions/view.xml
+++ b/openerp/tests/addons/test_exceptions/view.xml
@@ -50,7 +50,7 @@
new
-
+
diff --git a/openerp/tests/addons/test_impex/__init__.py b/openerp/tests/addons/test_impex/__init__.py
new file mode 100644
index 00000000000..bff786c0885
--- /dev/null
+++ b/openerp/tests/addons/test_impex/__init__.py
@@ -0,0 +1 @@
+import models
diff --git a/openerp/tests/addons/test_impex/__openerp__.py b/openerp/tests/addons/test_impex/__openerp__.py
new file mode 100644
index 00000000000..f3b5511dc30
--- /dev/null
+++ b/openerp/tests/addons/test_impex/__openerp__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-import-export',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to test import/export.""",
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base'],
+ 'data': [],
+ 'installable': True,
+ 'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py
new file mode 100644
index 00000000000..455ea6b22f4
--- /dev/null
+++ b/openerp/tests/addons/test_impex/models.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+from openerp.osv import orm, fields
+
+def selection_fn(obj, cr, uid, context=None):
+ return list(enumerate(["Corge", "Grault", "Wheee", "Moog"]))
+
+def function_fn(model, cr, uid, ids, field_name, arg, context):
+ return dict((id, 3) for id in ids)
+def function_fn_write(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context):
+ """ just so CreatorCase.export can be used
+ """
+ pass
+
+models = [
+ ('boolean', fields.boolean()),
+ ('integer', fields.integer()),
+ ('float', fields.float()),
+ ('decimal', fields.float(digits=(16, 3))),
+ ('string.bounded', fields.char('unknown', size=16)),
+ ('string', fields.char('unknown', size=None)),
+ ('date', fields.date()),
+ ('datetime', fields.datetime()),
+ ('text', fields.text()),
+ ('selection', fields.selection([(1, "Foo"), (2, "Bar"), (3, "Qux")])),
+ ('selection.function', fields.selection(selection_fn)),
+ # just relate to an integer
+ ('many2one', fields.many2one('export.integer')),
+ ('one2many', fields.one2many('export.one2many.child', 'parent_id')),
+ ('many2many', fields.many2many('export.many2many.other')),
+ ('function', fields.function(function_fn, fnct_inv=function_fn_write, type="integer")),
+ # related: specialization of fields.function, should work the same way
+ # TODO: reference
+]
+for name, field in models:
+ attrs = {
+ '_name': 'export.%s' % name,
+ '_columns': {
+ 'const': fields.integer(),
+ 'value': field
+ },
+ '_defaults': {'const': 4},
+ 'name_get': (lambda self, cr, uid, ids, context=None:
+ [(record.id, "%s:%s" % (self._name, record.value))
+ for record in self.browse(cr, uid, ids, context=context)]),
+ 'name_search': (lambda self, cr, uid, name, operator, context=None:
+ self.name_get(cr, uid,
+ self.search(cr, uid, [['value', operator, int(name.split(':')[1])]])
+ , context=context)
+ if isinstance(name, basestring) and name.split(':')[0] == self._name
+ else [])
+ }
+ NewModel = type(
+ 'Export%s' % ''.join(section.capitalize() for section in name.split('.')),
+ (orm.Model,),
+ attrs)
+
+class One2ManyChild(orm.Model):
+ _name = 'export.one2many.child'
+ # FIXME: orm.py:1161, fix to name_get on m2o field
+ _rec_name = 'value'
+
+ _columns = {
+ 'parent_id': fields.many2one('export.one2many'),
+ 'str': fields.char('unknown', size=None),
+ 'value': fields.integer()
+ }
+ def name_get(self, cr, uid, ids, context=None):
+ return [(record.id, "%s:%s" % (self._name, record.value))
+ for record in self.browse(cr, uid, ids, context=context)]
+
+class One2ManyMultiple(orm.Model):
+ _name = 'export.one2many.multiple'
+
+ _columns = {
+ 'const': fields.integer(),
+ 'child1': fields.one2many('export.one2many.child.1', 'parent_id'),
+ 'child2': fields.one2many('export.one2many.child.2', 'parent_id'),
+ }
+ _defaults = { 'const': 36 }
+
+class One2ManyChildMultiple(orm.Model):
+ _name = 'export.one2many.multiple.child'
+ # FIXME: orm.py:1161, fix to name_get on m2o field
+ _rec_name = 'value'
+
+ _columns = {
+ 'parent_id': fields.many2one('export.one2many.multiple'),
+ 'str': fields.char('unknown', size=None),
+ 'value': fields.integer()
+ }
+ def name_get(self, cr, uid, ids, context=None):
+ return [(record.id, "%s:%s" % (self._name, record.value))
+ for record in self.browse(cr, uid, ids, context=context)]
+class One2ManyChild1(orm.Model):
+ _name = 'export.one2many.child.1'
+ _inherit = 'export.one2many.multiple.child'
+class One2ManyChild2(orm.Model):
+ _name = 'export.one2many.child.2'
+ _inherit = 'export.one2many.multiple.child'
+
+class Many2ManyChild(orm.Model):
+ _name = 'export.many2many.other'
+ # FIXME: orm.py:1161, fix to name_get on m2o field
+ _rec_name = 'value'
+
+ _columns = {
+ 'str': fields.char('unknown', size=None),
+ 'value': fields.integer()
+ }
+ def name_get(self, cr, uid, ids, context=None):
+ return [(record.id, "%s:%s" % (self._name, record.value))
+ for record in self.browse(cr, uid, ids, context=context)]
+ def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
+ return (self.name_get(cr, user,
+ self.search(cr, user, [['value', operator, int(name.split(':')[1])]])
+ , context=context)
+ if isinstance(name, basestring) and name.split(':')[0] == self._name
+ else [])
diff --git a/openerp/tests/addons/test_impex/tests/__init__.py b/openerp/tests/addons/test_impex/tests/__init__.py
new file mode 100644
index 00000000000..d6af53cba1b
--- /dev/null
+++ b/openerp/tests/addons/test_impex/tests/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from . import test_export, test_import
+
+fast_suite = [
+]
+
+checks = [
+ test_export,
+ test_import,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_impex/tests/test_export.py b/openerp/tests/addons/test_impex/tests/test_export.py
new file mode 100644
index 00000000000..7e67ba581a1
--- /dev/null
+++ b/openerp/tests/addons/test_impex/tests/test_export.py
@@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+import itertools
+import openerp.modules.registry
+import openerp
+
+from openerp.tests import common
+
+
+class CreatorCase(common.TransactionCase):
+ model_name = False
+
+ def __init__(self, *args, **kwargs):
+ super(CreatorCase, self).__init__(*args, **kwargs)
+ self.model = None
+
+ def setUp(self):
+ super(CreatorCase, self).setUp()
+ self.model = self.registry(self.model_name)
+ def make(self, value):
+ id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': value})
+ return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
+ def export(self, value, fields=('value',), context=None):
+ record = self.make(value)
+ return self.model._BaseModel__export_row(
+ self.cr, openerp.SUPERUSER_ID, record,
+ [f.split('/') for f in fields],
+ context=context)
+
+class test_boolean_field(CreatorCase):
+ model_name = 'export.boolean'
+
+ def test_true(self):
+ self.assertEqual(
+ self.export(True),
+ [[u'True']])
+ def test_false(self):
+ """ ``False`` value to boolean fields is unique in being exported as a
+ (unicode) string, not a boolean
+ """
+ self.assertEqual(
+ self.export(False),
+ [[u'False']])
+
+class test_integer_field(CreatorCase):
+ model_name = 'export.integer'
+
+ def test_empty(self):
+ self.assertEqual(self.model.search(self.cr, openerp.SUPERUSER_ID, []), [],
+ "Test model should have no records")
+ def test_0(self):
+ self.assertEqual(
+ self.export(0),
+ [[False]])
+
+ def test_basic_value(self):
+ self.assertEqual(
+ self.export(42),
+ [[u'42']])
+
+ def test_negative(self):
+ self.assertEqual(
+ self.export(-32),
+ [[u'-32']])
+
+ def test_huge(self):
+ self.assertEqual(
+ self.export(2**31-1),
+ [[unicode(2**31-1)]])
+
+class test_float_field(CreatorCase):
+ model_name = 'export.float'
+
+ def test_0(self):
+ self.assertEqual(
+ self.export(0.0),
+ [[False]])
+
+ def test_epsilon(self):
+ self.assertEqual(
+ self.export(0.000000000027),
+ [[u'2.7e-11']])
+
+ def test_negative(self):
+ self.assertEqual(
+ self.export(-2.42),
+ [[u'-2.42']])
+
+ def test_positive(self):
+ self.assertEqual(
+ self.export(47.36),
+ [[u'47.36']])
+
+ def test_big(self):
+ self.assertEqual(
+ self.export(87654321.4678),
+ [[u'87654321.4678']])
+
+class test_decimal_field(CreatorCase):
+ model_name = 'export.decimal'
+
+ def test_0(self):
+ self.assertEqual(
+ self.export(0.0),
+ [[False]])
+
+ def test_epsilon(self):
+ """ epsilon gets sliced to 0 due to precision
+ """
+ self.assertEqual(
+ self.export(0.000000000027),
+ [[False]])
+
+ def test_negative(self):
+ self.assertEqual(
+ self.export(-2.42),
+ [[u'-2.42']])
+
+ def test_positive(self):
+ self.assertEqual(
+ self.export(47.36),
+ [[u'47.36']])
+
+ def test_big(self):
+ self.assertEqual(
+ self.export(87654321.4678), [[u'87654321.468']])
+
+class test_string_field(CreatorCase):
+ model_name = 'export.string.bounded'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(""),
+ [[False]])
+ def test_within_bounds(self):
+ self.assertEqual(
+ self.export("foobar"),
+ [[u"foobar"]])
+ def test_out_of_bounds(self):
+ self.assertEqual(
+ self.export("C for Sinking, "
+ "Java for Drinking, "
+ "Smalltalk for Thinking. "
+ "...and Power to the Penguin!"),
+ [[u"C for Sinking, J"]])
+
+class test_unbound_string_field(CreatorCase):
+ model_name = 'export.string'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(""),
+ [[False]])
+ def test_small(self):
+ self.assertEqual(
+ self.export("foobar"),
+ [[u"foobar"]])
+ def test_big(self):
+ self.assertEqual(
+ self.export("We flew down weekly to meet with IBM, but they "
+ "thought the way to measure software was the amount "
+ "of code we wrote, when really the better the "
+ "software, the fewer lines of code."),
+ [[u"We flew down weekly to meet with IBM, but they thought the "
+ u"way to measure software was the amount of code we wrote, "
+ u"when really the better the software, the fewer lines of "
+ u"code."]])
+
+class test_text(CreatorCase):
+ model_name = 'export.text'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(""),
+ [[False]])
+ def test_small(self):
+ self.assertEqual(
+ self.export("foobar"),
+ [[u"foobar"]])
+ def test_big(self):
+ self.assertEqual(
+ self.export("So, `bind' is `let' and monadic programming is"
+ " equivalent to programming in the A-normal form. That"
+ " is indeed all there is to monads"),
+ [[u"So, `bind' is `let' and monadic programming is equivalent to"
+ u" programming in the A-normal form. That is indeed all there"
+ u" is to monads"]])
+
+class test_date(CreatorCase):
+ model_name = 'export.date'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+ def test_basic(self):
+ self.assertEqual(
+ self.export('2011-11-07'),
+ [[u'2011-11-07']])
+
+class test_datetime(CreatorCase):
+ model_name = 'export.datetime'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+ def test_basic(self):
+ self.assertEqual(
+ self.export('2011-11-07 21:05:48'),
+ [[u'2011-11-07 21:05:48']])
+ def test_tz(self):
+ """ Export ignores the timezone and always exports to UTC
+
+ .. note:: on the other hand, export uses user lang for name_get
+ """
+ # NOTE: ignores user timezone, always exports to UTC
+ self.assertEqual(
+ self.export('2011-11-07 21:05:48', context={'tz': 'Pacific/Norfolk'}),
+ [[u'2011-11-07 21:05:48']])
+
+class test_selection(CreatorCase):
+ model_name = 'export.selection'
+ translations_fr = [
+ ("Qux", "toto"),
+ ("Bar", "titi"),
+ ("Foo", "tete"),
+ ]
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+
+ def test_value(self):
+ """ selections export the *label* for their value
+ """
+ self.assertEqual(
+ self.export(2),
+ [[u"Bar"]])
+
+ def test_localized_export(self):
+ self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': u'Français',
+ 'code': 'fr_FR',
+ 'translatable': True,
+ 'date_format': '%d.%m.%Y',
+ 'decimal_point': ',',
+ 'thousand_sep': ' ',
+ })
+ Translations = self.registry('ir.translation')
+ for source, value in self.translations_fr:
+ Translations.create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': 'export.selection,value',
+ 'lang': 'fr_FR',
+ 'type': 'selection',
+ 'src': source,
+ 'value': value
+ })
+ self.assertEqual(
+ self.export(2, context={'lang': 'fr_FR'}),
+ [[u'Bar']])
+
+class test_selection_function(CreatorCase):
+ model_name = 'export.selection.function'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+
+ def test_value(self):
+ # FIXME: selection functions export the *value* itself
+ self.assertEqual(
+ self.export(1),
+ [[u'1']])
+ self.assertEqual(
+ self.export(3),
+ [[u'3']])
+ # fucking hell
+ self.assertEqual(
+ self.export(0),
+ [[False]])
+
+class test_m2o(CreatorCase):
+ model_name = 'export.many2one'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+ def test_basic(self):
+ """ Exported value is the name_get of the related object
+ """
+ integer_id = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ name = dict(self.registry('export.integer').name_get(
+ self.cr, openerp.SUPERUSER_ID,[integer_id]))[integer_id]
+ self.assertEqual(
+ self.export(integer_id),
+ [[name]])
+ def test_path(self):
+ """ Can recursively export fields of m2o via path
+ """
+ integer_id = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ self.assertEqual(
+ self.export(integer_id, fields=['value/.id', 'value/value']),
+ [[unicode(integer_id), u'42']])
+ def test_external_id(self):
+ integer_id = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ # __export__.$class.$id
+ external_id = u'__export__.export_many2one_%d' % integer_id
+ self.assertEqual(
+ self.export(integer_id, fields=['value/id']),
+ [[external_id]])
+
+class test_o2m(CreatorCase):
+ model_name = 'export.one2many'
+ commands = [
+ (0, False, {'value': 4, 'str': 'record1'}),
+ (0, False, {'value': 42, 'str': 'record2'}),
+ (0, False, {'value': 36, 'str': 'record3'}),
+ (0, False, {'value': 4, 'str': 'record4'}),
+ (0, False, {'value': 13, 'str': 'record5'}),
+ ]
+ names = [
+ u'export.one2many.child:%d' % d['value']
+ for c, _, d in commands
+ ]
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+
+ def test_single(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})]),
+ # name_get result
+ [[u'export.one2many.child:42']])
+
+ def test_single_subfield(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})],
+ fields=['value', 'value/value']),
+ [[u'export.one2many.child:42', u'42']])
+
+ def test_integrate_one_in_parent(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})],
+ fields=['const', 'value/value']),
+ [[u'4', u'42']])
+
+ def test_multiple_records(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value/value']),
+ [
+ [u'4', u'4'],
+ [u'', u'42'],
+ [u'', u'36'],
+ [u'', u'4'],
+ [u'', u'13'],
+ ])
+
+ def test_multiple_records_name(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value']),
+ [[
+ u'4', u','.join(self.names)
+ ]])
+
+ def test_multiple_records_id(self):
+ export = self.export(self.commands, fields=['const', 'value/.id'])
+ O2M_c = self.registry('export.one2many.child')
+ ids = O2M_c.browse(self.cr, openerp.SUPERUSER_ID,
+ O2M_c.search(self.cr, openerp.SUPERUSER_ID, []))
+ self.assertEqual(
+ export,
+ [
+ ['4', str(ids[0].id)],
+ ['', str(ids[1].id)],
+ ['', str(ids[2].id)],
+ ['', str(ids[3].id)],
+ ['', str(ids[4].id)],
+ ])
+
+ def test_multiple_records_with_name_before(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value', 'value/value']),
+ [[ # exports sub-fields of very first o2m
+ u'4', u','.join(self.names), u'4'
+ ]])
+
+ def test_multiple_records_with_name_after(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value/value', 'value']),
+ [ # completely ignores name_get request
+ [u'4', u'4', ''],
+ ['', u'42', ''],
+ ['', u'36', ''],
+ ['', u'4', ''],
+ ['', u'13', ''],
+ ])
+
+ def test_multiple_subfields_neighbour(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value/str','value/value']),
+ [
+ [u'4', u'record1', u'4'],
+ ['', u'record2', u'42'],
+ ['', u'record3', u'36'],
+ ['', u'record4', u'4'],
+ ['', u'record5', u'13'],
+ ])
+
+ def test_multiple_subfields_separated(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['value/str', 'const', 'value/value']),
+ [
+ [u'record1', u'4', u'4'],
+ [u'record2', '', u'42'],
+ [u'record3', '', u'36'],
+ [u'record4', '', u'4'],
+ [u'record5', '', u'13'],
+ ])
+
+class test_o2m_multiple(CreatorCase):
+ model_name = 'export.one2many.multiple'
+
+ def make(self, value=None, **values):
+ if value is not None: values['value'] = value
+ id = self.model.create(self.cr, openerp.SUPERUSER_ID, values)
+ return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
+ def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
+ record = self.make(value, **values)
+ return self.model._BaseModel__export_row(
+ self.cr, openerp.SUPERUSER_ID, record,
+ [f.split('/') for f in fields],
+ context=context)
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(child1=False, child2=False),
+ [[False, False]])
+
+ def test_single_per_side(self):
+ self.assertEqual(
+ self.export(child1=False, child2=[(0, False, {'value': 42})]),
+ [[False, u'export.one2many.child.2:42']])
+
+ self.assertEqual(
+ self.export(child1=[(0, False, {'value': 43})], child2=False),
+ [[u'export.one2many.child.1:43', False]])
+
+ self.assertEqual(
+ self.export(child1=[(0, False, {'value': 43})],
+ child2=[(0, False, {'value': 42})]),
+ [[u'export.one2many.child.1:43', u'export.one2many.child.2:42']])
+
+ def test_single_integrate_subfield(self):
+ fields = ['const', 'child1/value', 'child2/value']
+ self.assertEqual(
+ self.export(child1=False, child2=[(0, False, {'value': 42})],
+ fields=fields),
+ [[u'36', False, u'42']])
+
+ self.assertEqual(
+ self.export(child1=[(0, False, {'value': 43})], child2=False,
+ fields=fields),
+ [[u'36', u'43', False]])
+
+ self.assertEqual(
+ self.export(child1=[(0, False, {'value': 43})],
+ child2=[(0, False, {'value': 42})],
+ fields=fields),
+ [[u'36', u'43', u'42']])
+
+ def test_multiple(self):
+ """ With two "concurrent" o2ms, exports the first line combined, then
+ exports the rows for the first o2m, then the rows for the second o2m.
+ """
+ fields = ['const', 'child1/value', 'child2/value']
+ child1 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
+ for index, v in zip(itertools.count(), [4, 42, 36, 4, 13])]
+ child2 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
+ for index, v in zip(itertools.count(10), [8, 12, 8, 55, 33, 13])]
+
+ self.assertEqual(
+ self.export(child1=child1, child2=False, fields=fields),
+ [
+ [u'36', u'4', False],
+ ['', u'42', ''],
+ ['', u'36', ''],
+ ['', u'4', ''],
+ ['', u'13', ''],
+ ])
+ self.assertEqual(
+ self.export(child1=False, child2=child2, fields=fields),
+ [
+ [u'36', False, u'8'],
+ ['', '', u'12'],
+ ['', '', u'8'],
+ ['', '', u'55'],
+ ['', '', u'33'],
+ ['', '', u'13'],
+ ])
+ self.assertEqual(
+ self.export(child1=child1, child2=child2, fields=fields),
+ [
+ [u'36', u'4', u'8'],
+ ['', u'42', ''],
+ ['', u'36', ''],
+ ['', u'4', ''],
+ ['', u'13', ''],
+ ['', '', u'12'],
+ ['', '', u'8'],
+ ['', '', u'55'],
+ ['', '', u'33'],
+ ['', '', u'13'],
+ ])
+
+class test_m2m(CreatorCase):
+ model_name = 'export.many2many'
+ commands = [
+ (0, False, {'value': 4, 'str': 'record000'}),
+ (0, False, {'value': 42, 'str': 'record001'}),
+ (0, False, {'value': 36, 'str': 'record010'}),
+ (0, False, {'value': 4, 'str': 'record011'}),
+ (0, False, {'value': 13, 'str': 'record100'}),
+ ]
+ names = [
+ u'export.many2many.other:%d' % d['value']
+ for c, _, d in commands
+ ]
+
+ def test_empty(self):
+ self.assertEqual(
+ self.export(False),
+ [[False]])
+
+ def test_single(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})]),
+ # name_get result
+ [[u'export.many2many.other:42']])
+
+ def test_single_subfield(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})],
+ fields=['value', 'value/value']),
+ [[u'export.many2many.other:42', u'42']])
+
+ def test_integrate_one_in_parent(self):
+ self.assertEqual(
+ self.export([(0, False, {'value': 42})],
+ fields=['const', 'value/value']),
+ [[u'4', u'42']])
+
+ def test_multiple_records(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value/value']),
+ [
+ [u'4', u'4'],
+ [u'', u'42'],
+ [u'', u'36'],
+ [u'', u'4'],
+ [u'', u'13'],
+ ])
+
+ def test_multiple_records_name(self):
+ self.assertEqual(
+ self.export(self.commands, fields=['const', 'value']),
+ [[ # FIXME: hardcoded comma, import uses config.csv_internal_sep
+ # resolution: remove configurable csv_internal_sep
+ u'4', u','.join(self.names)
+ ]])
+
+ # essentially same as o2m, so boring
+
+class test_function(CreatorCase):
+ model_name = 'export.function'
+
+ def test_value(self):
+ """ Exports value normally returned by accessing the function field
+ """
+ self.assertEqual(
+ self.export(42),
+ [[u'3']])
diff --git a/openerp/tests/addons/test_impex/tests/test_import.py b/openerp/tests/addons/test_impex/tests/test_import.py
new file mode 100644
index 00000000000..ab12f0375b5
--- /dev/null
+++ b/openerp/tests/addons/test_impex/tests/test_import.py
@@ -0,0 +1,906 @@
+# -*- coding: utf-8 -*-
+import openerp.modules.registry
+import openerp
+
+from openerp.tests import common
+
+def ok(n):
+ """ Successful import of ``n`` records
+
+ :param int n: number of records which should have been imported
+ """
+ return n, 0, 0, 0
+
+def error(row, message, record=None, **kwargs):
+ """ Failed import of the record ``record`` at line ``row``, with the error
+ message ``message``
+
+ :param str message:
+ :param dict record:
+ """
+ return (
+ -1, dict(record or {}, **kwargs),
+ "Line %d : %s" % (row, message),
+ '')
+
+def values(seq, field='value'):
+ return [item[field] for item in seq]
+
+class ImporterCase(common.TransactionCase):
+ model_name = False
+
+ def __init__(self, *args, **kwargs):
+ super(ImporterCase, self).__init__(*args, **kwargs)
+ self.model = None
+
+ def setUp(self):
+ super(ImporterCase, self).setUp()
+ self.model = self.registry(self.model_name)
+
+ def import_(self, fields, rows, context=None):
+ return self.model.import_data(
+ self.cr, openerp.SUPERUSER_ID, fields, rows, context=context)
+ def read(self, fields=('value',), domain=(), context=None):
+ return self.model.read(
+ self.cr, openerp.SUPERUSER_ID,
+ self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context),
+ fields=fields, context=context)
+ def browse(self, domain=(), context=None):
+ return self.model.browse(
+ self.cr, openerp.SUPERUSER_ID,
+ self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context),
+ context=context)
+
+ def xid(self, record):
+ ModelData = self.registry('ir.model.data')
+
+ ids = ModelData.search(
+ self.cr, openerp.SUPERUSER_ID,
+ [('model', '=', record._table_name), ('res_id', '=', record.id)])
+ if ids:
+ d = ModelData.read(
+ self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0]
+ if d['module']:
+ return '%s.%s' % (d['module'], d['name'])
+ return d['name']
+
+ name = dict(record.name_get())[record.id]
+ # fix dotted name_get results, otherwise xid lookups blow up
+ name = name.replace('.', '-')
+ ModelData.create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': name,
+ 'model': record._table_name,
+ 'res_id': record.id,
+ 'module': '__test__'
+ })
+ return '__test__.' + name
+
+class test_ids_stuff(ImporterCase):
+ model_name = 'export.integer'
+
+ def test_create_with_id(self):
+ self.assertRaises(
+ Exception, # dammit
+ self.import_, ['.id', 'value'], [['42', '36']])
+ def test_create_with_xid(self):
+ self.assertEqual(
+ self.import_(['id', 'value'], [['somexmlid', '42']]),
+ ok(1))
+ self.assertEqual(
+ 'somexmlid',
+ self.xid(self.browse()[0]))
+
+ def test_update_with_id(self):
+ id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': 36})
+ self.assertEqual(
+ 36,
+ self.model.browse(self.cr, openerp.SUPERUSER_ID, id).value)
+
+ self.assertEqual(
+ self.import_(['.id', 'value'], [[str(id), '42']]),
+ ok(1))
+ self.assertEqual(
+ [42], # updated value to imported
+ values(self.read()))
+
+ def test_update_with_xid(self):
+ self.import_(['id', 'value'], [['somexmlid', '36']])
+ self.assertEqual([36], values(self.read()))
+
+ self.import_(['id', 'value'], [['somexmlid', '1234567']])
+ self.assertEqual([1234567], values(self.read()))
+
+class test_boolean_field(ImporterCase):
+ model_name = 'export.boolean'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.import_(['value'], []),
+ ok(0))
+
+ def test_exported(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['False'],
+ ['True'],
+ ]),
+ ok(2))
+ records = self.read()
+ self.assertEqual([
+ False,
+ True,
+ ], values(records))
+
+ def test_falses(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ [u'0'],
+ [u'off'],
+ [u'false'],
+ [u'FALSE'],
+ [u'OFF'],
+ [u''],
+ ]),
+ ok(6))
+ self.assertEqual([
+ False,
+ False,
+ False,
+ False,
+ False,
+ False,
+ ],
+ values(self.read()))
+
+ def test_trues(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['no'],
+ ['None'],
+ ['nil'],
+ ['()'],
+ ['f'],
+ ['#f'],
+ # Problem: OpenOffice (and probably excel) output localized booleans
+ ['VRAI'],
+ ]),
+ ok(7))
+ self.assertEqual(
+ [True] * 7,
+ values(self.read()))
+
+class test_integer_field(ImporterCase):
+ model_name = 'export.integer'
+
+ def test_none(self):
+ self.assertEqual(
+ self.import_(['value'], []),
+ ok(0))
+
+ def test_empty(self):
+ self.assertEqual(
+ self.import_(['value'], [['']]),
+ ok(1))
+ self.assertEqual(
+ [False],
+ values(self.read()))
+
+ def test_zero(self):
+ self.assertEqual(
+ self.import_(['value'], [['0']]),
+ ok(1))
+ self.assertEqual(
+ self.import_(['value'], [['-0']]),
+ ok(1))
+ self.assertEqual([False, False], values(self.read()))
+
+ def test_positives(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['1'],
+ ['42'],
+ [str(2**31-1)],
+ ['12345678']
+ ]),
+ ok(4))
+ self.assertEqual([
+ 1, 42, 2**31-1, 12345678
+ ], values(self.read()))
+
+ def test_negatives(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['-1'],
+ ['-42'],
+ [str(-(2**31 - 1))],
+ [str(-(2**31))],
+ ['-12345678']
+ ]),
+ ok(5))
+ self.assertEqual([
+ -1, -42, -(2**31 - 1), -(2**31), -12345678
+ ], values(self.read()))
+
+ def test_out_of_range(self):
+ self.assertEqual(
+ self.import_(['value'], [[str(2**31)]]),
+ error(1, "integer out of range\n", value=2**31))
+ # auto-rollbacks if error is in process_liness, but not during
+ # ir.model.data write. Can differentiate because former ends lines
+ # error lines with "!"
+ self.cr.rollback()
+ self.assertEqual(
+ self.import_(['value'], [[str(-2**32)]]),
+ error(1, "integer out of range\n", value=-2**32))
+
+
+ def test_nonsense(self):
+ # FIXME: shit error reporting, exceptions half the time, messages the other half
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value'], [['zorglub']])
+
+class test_float_field(ImporterCase):
+ model_name = 'export.float'
+ def test_none(self):
+ self.assertEqual(
+ self.import_(['value'], []),
+ ok(0))
+
+ def test_empty(self):
+ self.assertEqual(
+ self.import_(['value'], [['']]),
+ ok(1))
+ self.assertEqual(
+ [False],
+ values(self.read()))
+
+ def test_zero(self):
+ self.assertEqual(
+ self.import_(['value'], [['0']]),
+ ok(1))
+ self.assertEqual(
+ self.import_(['value'], [['-0']]),
+ ok(1))
+ self.assertEqual([False, False], values(self.read()))
+
+ def test_positives(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['1'],
+ ['42'],
+ [str(2**31-1)],
+ ['12345678'],
+ [str(2**33)],
+ ['0.000001'],
+ ]),
+ ok(6))
+ self.assertEqual([
+ 1, 42, 2**31-1, 12345678, 2.0**33, .000001
+ ], values(self.read()))
+
+ def test_negatives(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['-1'],
+ ['-42'],
+ [str(-2**31 + 1)],
+ [str(-2**31)],
+ ['-12345678'],
+ [str(-2**33)],
+ ['-0.000001'],
+ ]),
+ ok(7))
+ self.assertEqual([
+ -1, -42, -(2**31 - 1), -(2**31), -12345678, -2.0**33, -.000001
+ ], values(self.read()))
+
+ def test_nonsense(self):
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value'], [['foobar']])
+
+class test_string_field(ImporterCase):
+ model_name = 'export.string.bounded'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.import_(['value'], [['']]),
+ ok(1))
+ self.assertEqual([False], values(self.read()))
+
+ def test_imported(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ [u'foobar'],
+ [u'foobarbaz'],
+ [u'Með suð í eyrum við spilum endalaust'],
+ [u"People 'get' types. They use them all the time. Telling "
+ u"someone he can't pound a nail with a banana doesn't much "
+ u"surprise him."]
+ ]),
+ ok(4))
+ self.assertEqual([
+ u"foobar",
+ u"foobarbaz",
+ u"Með suð í eyrum ",
+ u"People 'get' typ",
+ ], values(self.read()))
+
+class test_unbound_string_field(ImporterCase):
+ model_name = 'export.string'
+
+ def test_imported(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ [u'í dag viðrar vel til loftárása'],
+ # ackbar.jpg
+ [u"If they ask you about fun, you tell them – fun is a filthy"
+ u" parasite"]
+ ]),
+ ok(2))
+ self.assertEqual([
+ u"í dag viðrar vel til loftárása",
+ u"If they ask you about fun, you tell them – fun is a filthy parasite"
+ ], values(self.read()))
+
+class test_text(ImporterCase):
+ model_name = 'export.text'
+
+ def test_empty(self):
+ self.assertEqual(
+ self.import_(['value'], [['']]),
+ ok(1))
+ self.assertEqual([False], values(self.read()))
+
+ def test_imported(self):
+ s = (u"Breiðskífa er notað um útgefna hljómplötu sem inniheldur "
+ u"stúdíóupptökur frá einum flytjanda. Breiðskífur eru oftast "
+ u"milli 25-80 mínútur og er lengd þeirra oft miðuð við 33⅓ "
+ u"snúninga 12 tommu vínylplötur (sem geta verið allt að 30 mín "
+ u"hvor hlið).\n\nBreiðskífur eru stundum tvöfaldar og eru þær þá"
+ u" gefnar út á tveimur geisladiskum eða tveimur vínylplötum.")
+ self.assertEqual(
+ self.import_(['value'], [[s]]),
+ ok(1))
+ self.assertEqual([s], values(self.read()))
+
+class test_selection(ImporterCase):
+ model_name = 'export.selection'
+ translations_fr = [
+ ("Qux", "toto"),
+ ("Bar", "titi"),
+ ("Foo", "tete"),
+ ]
+
+ def test_imported(self):
+ self.assertEqual(
+ self.import_(['value'], [
+ ['Qux'],
+ ['Bar'],
+ ['Foo'],
+ ['2'],
+ ]),
+ ok(4))
+ self.assertEqual([3, 2, 1, 2], values(self.read()))
+
+ def test_imported_translated(self):
+ self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': u'Français',
+ 'code': 'fr_FR',
+ 'translatable': True,
+ 'date_format': '%d.%m.%Y',
+ 'decimal_point': ',',
+ 'thousand_sep': ' ',
+ })
+ Translations = self.registry('ir.translation')
+ for source, value in self.translations_fr:
+ Translations.create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': 'export.selection,value',
+ 'lang': 'fr_FR',
+ 'type': 'selection',
+ 'src': source,
+ 'value': value
+ })
+
+ # FIXME: can't import an exported selection field label if lang != en_US
+ # (see test_export.test_selection.test_localized_export)
+ self.assertEqual(
+ self.import_(['value'], [
+ ['toto'],
+ ['tete'],
+ ['titi'],
+ ], context={'lang': 'fr_FR'}),
+ ok(3))
+ self.assertEqual([3, 1, 2], values(self.read()))
+ self.assertEqual(
+ self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}),
+ error(1, "Key/value 'Foo' not found in selection field 'value'",
+ value=False))
+
+ def test_invalid(self):
+ self.assertEqual(
+ self.import_(['value'], [['Baz']]),
+ error(1, "Key/value 'Baz' not found in selection field 'value'",
+ # what the fuck?
+ value=False))
+ self.cr.rollback()
+ self.assertEqual(
+ self.import_(['value'], [[42]]),
+ error(1, "Key/value '42' not found in selection field 'value'",
+ value=False))
+
+class test_selection_function(ImporterCase):
+ model_name = 'export.selection.function'
+ translations_fr = [
+ ("Corge", "toto"),
+ ("Grault", "titi"),
+ ("Whee", "tete"),
+ ("Moog", "tutu"),
+ ]
+
+ def test_imported(self):
+ """ import uses fields_get, so translates import label (may or may not
+ be good news) *and* serializes the selection function to reverse it:
+ import does not actually know that the selection field uses a function
+ """
+ # NOTE: conflict between a value and a label => ?
+ self.assertEqual(
+ self.import_(['value'], [
+ ['3'],
+ ["Grault"],
+ ]),
+ ok(2))
+ self.assertEqual(
+ ['3', '1'],
+ values(self.read()))
+
+ def test_translated(self):
+ """ Expects output of selection function returns translated labels
+ """
+ self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': u'Français',
+ 'code': 'fr_FR',
+ 'translatable': True,
+ 'date_format': '%d.%m.%Y',
+ 'decimal_point': ',',
+ 'thousand_sep': ' ',
+ })
+ Translations = self.registry('ir.translation')
+ for source, value in self.translations_fr:
+ Translations.create(self.cr, openerp.SUPERUSER_ID, {
+ 'name': 'export.selection,value',
+ 'lang': 'fr_FR',
+ 'type': 'selection',
+ 'src': source,
+ 'value': value
+ })
+ self.assertEqual(
+ self.import_(['value'], [
+ ['toto'],
+ ['tete'],
+ ], context={'lang': 'fr_FR'}),
+ error(1, "Key/value 'toto' not found in selection field 'value'",
+ value=False))
+ self.assertEqual(
+ self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}),
+ ok(1))
+
+class test_m2o(ImporterCase):
+ model_name = 'export.many2one'
+
+ def test_by_name(self):
+ # create integer objects
+ integer_id1 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ integer_id2 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 36})
+ # get its name
+ name1 = dict(self.registry('export.integer').name_get(
+ self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1]
+ name2 = dict(self.registry('export.integer').name_get(
+ self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2]
+
+ self.assertEqual(
+ self.import_(['value'], [
+ # import by name_get
+ [name1],
+ [name1],
+ [name2],
+ ]),
+ ok(3))
+ # correct ids assigned to corresponding records
+ self.assertEqual([
+ (integer_id1, name1),
+ (integer_id1, name1),
+ (integer_id2, name2),],
+ values(self.read()))
+
+ def test_by_xid(self):
+ ExportInteger = self.registry('export.integer')
+ integer_id = ExportInteger.create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ xid = self.xid(ExportInteger.browse(
+ self.cr, openerp.SUPERUSER_ID, [integer_id])[0])
+
+ self.assertEqual(
+ self.import_(['value/id'], [[xid]]),
+ ok(1))
+ b = self.browse()
+ self.assertEqual(42, b[0].value.value)
+
+ def test_by_id(self):
+ integer_id = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ self.assertEqual(
+ self.import_(['value/.id'], [[integer_id]]),
+ ok(1))
+ b = self.browse()
+ self.assertEqual(42, b[0].value.value)
+
+ def test_by_names(self):
+ integer_id1 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ integer_id2 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ name1 = dict(self.registry('export.integer').name_get(
+ self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1]
+ name2 = dict(self.registry('export.integer').name_get(
+ self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2]
+ # names should be the same
+ self.assertEqual(name1, name2)
+
+ self.assertEqual(
+ self.import_(['value'], [[name2]]),
+ ok(1))
+ # FIXME: is it really normal import does not care for name_search collisions?
+ self.assertEqual([
+ (integer_id1, name1)
+ ], values(self.read()))
+
+ def test_fail_by_implicit_id(self):
+ """ Can't implicitly import records by id
+ """
+ # create integer objects
+ integer_id1 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 42})
+ integer_id2 = self.registry('export.integer').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 36})
+
+ self.assertRaises(
+ ValueError, # Because name_search all the things. Fallback schmallback
+ self.import_, ['value'], [
+ # import by id, without specifying it
+ [integer_id1],
+ [integer_id2],
+ [integer_id1],
+ ])
+
+ def test_sub_field(self):
+ """ Does not implicitly create the record, does not warn that you can't
+ import m2o subfields (at all)...
+ """
+ self.assertRaises(
+ ValueError, # No record found for 42, name_searches the bloody thing
+ self.import_, ['value/value'], [['42']])
+
+ def test_fail_noids(self):
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value'], [['nameisnoexist:3']])
+ self.cr.rollback()
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value/id'], [['noxidhere']]),
+ self.cr.rollback()
+ self.assertRaises(
+ Exception, # FIXME: Why can't you be a ValueError like everybody else?
+ self.import_, ['value/.id'], [[66]])
+
+class test_m2m(ImporterCase):
+ model_name = 'export.many2many'
+
+ # apparently, one and only thing which works is a
+ # csv_internal_sep-separated list of ids, xids, or names (depending if
+ # m2m/.id, m2m/id or m2m[/anythingelse]
+ def test_ids(self):
+ id1 = self.registry('export.many2many.other').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
+ id2 = self.registry('export.many2many.other').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
+ id3 = self.registry('export.many2many.other').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
+ id4 = self.registry('export.many2many.other').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
+ id5 = self.registry('export.many2many.other').create(
+ self.cr, openerp.SUPERUSER_ID, {'value': 99, 'str': 'record4'})
+
+ self.assertEqual(
+ self.import_(['value/.id'], [
+ ['%d,%d' % (id1, id2)],
+ ['%d,%d,%d' % (id1, id3, id4)],
+ ['%d,%d,%d' % (id1, id2, id3)],
+ ['%d' % id5]
+ ]),
+ ok(4))
+ ids = lambda records: [record.id for record in records]
+
+ b = self.browse()
+ self.assertEqual(ids(b[0].value), [id1, id2])
+ self.assertEqual(values(b[0].value), [3, 44])
+
+ self.assertEqual(ids(b[2].value), [id1, id2, id3])
+ self.assertEqual(values(b[2].value), [3, 44, 84])
+
+ def test_noids(self):
+ try:
+ self.import_(['value/.id'], [['42']])
+ self.fail("Should have raised an exception")
+ except Exception, e:
+ self.assertIs(type(e), Exception,
+ "test should be fixed on exception subclass")
+
+ def test_xids(self):
+ M2O_o = self.registry('export.many2many.other')
+ id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
+ id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
+ id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
+ id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
+ records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4])
+
+ self.assertEqual(
+ self.import_(['value/id'], [
+ ['%s,%s' % (self.xid(records[0]), self.xid(records[1]))],
+ ['%s' % self.xid(records[3])],
+ ['%s,%s' % (self.xid(records[2]), self.xid(records[1]))],
+ ]),
+ ok(3))
+
+ b = self.browse()
+ self.assertEqual(values(b[0].value), [3, 44])
+ self.assertEqual(values(b[2].value), [44, 84])
+ def test_noxids(self):
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value/id'], [['noxidforthat']])
+
+ def test_names(self):
+ M2O_o = self.registry('export.many2many.other')
+ id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
+ id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
+ id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
+ id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
+ records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4])
+
+ name = lambda record: dict(record.name_get())[record.id]
+
+ self.assertEqual(
+ self.import_(['value'], [
+ ['%s,%s' % (name(records[1]), name(records[2]))],
+ ['%s,%s,%s' % (name(records[0]), name(records[1]), name(records[2]))],
+ ['%s,%s' % (name(records[0]), name(records[3]))],
+ ]),
+ ok(3))
+
+ b = self.browse()
+ self.assertEqual(values(b[1].value), [3, 44, 84])
+ self.assertEqual(values(b[2].value), [3, 9])
+
+ def test_nonames(self):
+ self.assertRaises(
+ ValueError,
+ self.import_, ['value'], [['wherethem2mhavenonames']])
+
+ def test_import_to_existing(self):
+ M2O_o = self.registry('export.many2many.other')
+ id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
+ id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
+ id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
+ id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
+
+ xid = 'myxid'
+ self.assertEqual(
+ self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]),
+ ok(1))
+ self.assertEqual(
+ self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]),
+ ok(1))
+
+ b = self.browse()
+ self.assertEqual(len(b), 1)
+ # TODO: replacement of existing m2m values is correct?
+ self.assertEqual(values(b[0].value), [84, 9])
+
+class test_o2m(ImporterCase):
+ model_name = 'export.one2many'
+
+ def test_single(self):
+ self.assertEqual(
+ self.import_(['const', 'value/value'], [
+ ['5', '63']
+ ]),
+ ok(1))
+
+ (b,) = self.browse()
+ self.assertEqual(b.const, 5)
+ self.assertEqual(values(b.value), [63])
+
+ def test_multicore(self):
+ self.assertEqual(
+ self.import_(['const', 'value/value'], [
+ ['5', '63'],
+ ['6', '64'],
+ ]),
+ ok(2))
+
+ b1, b2 = self.browse()
+ self.assertEqual(b1.const, 5)
+ self.assertEqual(values(b1.value), [63])
+ self.assertEqual(b2.const, 6)
+ self.assertEqual(values(b2.value), [64])
+
+ def test_multisub(self):
+ self.assertEqual(
+ self.import_(['const', 'value/value'], [
+ ['5', '63'],
+ ['', '64'],
+ ['', '65'],
+ ['', '66'],
+ ]),
+ ok(4))
+
+ (b,) = self.browse()
+ self.assertEqual(values(b.value), [63, 64, 65, 66])
+
+ def test_multi_subfields(self):
+ self.assertEqual(
+ self.import_(['value/str', 'const', 'value/value'], [
+ ['this', '5', '63'],
+ ['is', '', '64'],
+ ['the', '', '65'],
+ ['rhythm', '', '66'],
+ ]),
+ ok(4))
+
+ (b,) = self.browse()
+ self.assertEqual(values(b.value), [63, 64, 65, 66])
+ self.assertEqual(
+ values(b.value, 'str'),
+ 'this is the rhythm'.split())
+
+ def test_link_inline(self):
+ id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Bf', 'value': 109
+ })
+ id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Me', 'value': 262
+ })
+
+ try:
+ self.import_(['const', 'value/.id'], [
+ ['42', '%d,%d' % (id1, id2)]
+ ])
+ except ValueError, e:
+ # should be Exception(Database ID doesn't exist: export.one2many.child : $id1,$id2)
+ self.assertIs(type(e), ValueError)
+ self.assertEqual(
+ e.args[0],
+ "invalid literal for int() with base 10: '%d,%d'" % (id1, id2))
+
+ def test_link(self):
+ id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Bf', 'value': 109
+ })
+ id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Me', 'value': 262
+ })
+
+ self.assertEqual(
+ self.import_(['const', 'value/.id'], [
+ ['42', str(id1)],
+ ['', str(id2)],
+ ]),
+ ok(2))
+
+ # No record values alongside id => o2m resolution skipped altogether,
+ # creates 2 records => remove/don't import columns sideshow columns,
+ # get completely different semantics
+ b, b1 = self.browse()
+ self.assertEqual(b.const, 42)
+ self.assertEqual(values(b.value), [])
+ self.assertEqual(b1.const, 4)
+ self.assertEqual(values(b1.value), [])
+
+ def test_link_2(self):
+ O2M_c = self.registry('export.one2many.child')
+ id1 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Bf', 'value': 109
+ })
+ id2 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, {
+ 'str': 'Me', 'value': 262
+ })
+
+ self.assertEqual(
+ self.import_(['const', 'value/.id', 'value/value'], [
+ ['42', str(id1), '1'],
+ ['', str(id2), '2'],
+ ]),
+ ok(2))
+
+ (b,) = self.browse()
+ # if an id (db or xid) is provided, expectations that objects are
+ # *already* linked and emits UPDATE (1, id, {}).
+ # Noid => CREATE (0, ?, {})
+ # TODO: xid ignored aside from getting corresponding db id?
+ self.assertEqual(b.const, 42)
+ self.assertEqual(values(b.value), [])
+
+ # FIXME: updates somebody else's records?
+ self.assertEqual(
+ O2M_c.read(self.cr, openerp.SUPERUSER_ID, id1),
+ {'id': id1, 'str': 'Bf', 'value': 1, 'parent_id': False})
+ self.assertEqual(
+ O2M_c.read(self.cr, openerp.SUPERUSER_ID, id2),
+ {'id': id2, 'str': 'Me', 'value': 2, 'parent_id': False})
+
+class test_o2m_multiple(ImporterCase):
+ model_name = 'export.one2many.multiple'
+
+ def test_multi_mixed(self):
+ self.assertEqual(
+ self.import_(['const', 'child1/value', 'child2/value'], [
+ ['5', '11', '21'],
+ ['', '12', '22'],
+ ['', '13', '23'],
+ ['', '14', ''],
+ ]),
+ ok(4))
+ # Oh yeah, that's the stuff
+ (b, b1, b2) = self.browse()
+ self.assertEqual(values(b.child1), [11])
+ self.assertEqual(values(b.child2), [21])
+
+ self.assertEqual(values(b1.child1), [12])
+ self.assertEqual(values(b1.child2), [22])
+
+ self.assertEqual(values(b2.child1), [13, 14])
+ self.assertEqual(values(b2.child2), [23])
+
+ def test_multi(self):
+ self.assertEqual(
+ self.import_(['const', 'child1/value', 'child2/value'], [
+ ['5', '11', '21'],
+ ['', '12', ''],
+ ['', '13', ''],
+ ['', '14', ''],
+ ['', '', '22'],
+ ['', '', '23'],
+ ]),
+ ok(6))
+ # What the actual fuck?
+ (b, b1) = self.browse()
+ self.assertEqual(values(b.child1), [11, 12, 13, 14])
+ self.assertEqual(values(b.child2), [21])
+ self.assertEqual(values(b1.child2), [22, 23])
+
+ def test_multi_fullsplit(self):
+ self.assertEqual(
+ self.import_(['const', 'child1/value', 'child2/value'], [
+ ['5', '11', ''],
+ ['', '12', ''],
+ ['', '13', ''],
+ ['', '14', ''],
+ ['', '', '21'],
+ ['', '', '22'],
+ ['', '', '23'],
+ ]),
+ ok(7))
+ # oh wow
+ (b, b1) = self.browse()
+ self.assertEqual(b.const, 5)
+ self.assertEqual(values(b.child1), [11, 12, 13, 14])
+ self.assertEqual(b1.const, 36)
+ self.assertEqual(values(b1.child2), [21, 22, 23])
+
+# function, related, reference: written to db as-is...
+# => function uses @type for value coercion/conversion
diff --git a/openerp/tests/common.py b/openerp/tests/common.py
index ef5b0586136..b1af4338312 100644
--- a/openerp/tests/common.py
+++ b/openerp/tests/common.py
@@ -39,13 +39,16 @@ class TransactionCase(unittest2.TestCase):
"""
def setUp(self):
- self.cr = openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+ self.cr = self.cursor()
self.uid = openerp.SUPERUSER_ID
def tearDown(self):
self.cr.rollback()
self.cr.close()
+ def cursor(self):
+ return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+
def registry(self, model):
return openerp.modules.registry.RegistryManager.get(DB)[model]
diff --git a/openerp/tools/__init__.py b/openerp/tools/__init__.py
index 6875ebd78fc..a6eeb14ba96 100644
--- a/openerp/tools/__init__.py
+++ b/openerp/tools/__init__.py
@@ -26,6 +26,7 @@ from misc import *
from convert import *
from translate import *
from graph import graph
+from image import *
from amount_to_text import *
from amount_to_text_en import *
from pdf_utils import *
diff --git a/openerp/tools/image.py b/openerp/tools/image.py
new file mode 100644
index 00000000000..1caafb23a18
--- /dev/null
+++ b/openerp/tools/image.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today 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 io
+from PIL import Image
+import StringIO
+
+# ----------------------------------------
+# Image resizing
+# ----------------------------------------
+
+def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype='PNG', avoid_if_small=False):
+ """ Function to resize an image. The image will be resized to the given
+ size, while keeping the aspect ratios, and holes in the image will be
+ filled with transparent background. The image will not be stretched if
+ smaller than the expected size.
+ Steps of the resizing:
+ - if avoid_if_small: if both image sizes are smaller than the requested
+ sizes, the original image is returned. This is used to avoid adding
+ transparent content around images that we do not want to alter but
+ just resize if too big. This is used for example when storing images
+ in the 'image' field: we keep the original image, resized to a maximal
+ size, without adding transparent content around it if smaller.
+ - create a thumbnail of the source image through using the thumbnail
+ function. Aspect ratios are preserved when using it. Note that if the
+ source image is smaller than the expected size, it will not be
+ extended, but filled to match the size.
+ - create a transparent background that will hold the final
+ image.
+ - past the thumbnail on the transparent background and center
+ it.
+
+ :param base64_source: base64-encoded version of the source
+ image
+ :param size: tuple(height, width)
+ :param encoding: the output encoding
+ :param filetype: the output filetype
+ :param avoid_if_small: do not resize if image height and width
+ are smaller than the expected size.
+ """
+ image_stream = io.BytesIO(base64_source.decode(encoding))
+ image = Image.open(image_stream)
+ # check image size: do not create a thumbnail if avoiding smaller images
+ if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
+ return base64_source
+ # create a thumbnail: will resize and keep ratios
+ image.thumbnail(size, Image.ANTIALIAS)
+ # create a transparent image for background
+ background = Image.new('RGBA', size, (255, 255, 255, 0))
+ # past the resized image on the background
+ background.paste(image, ((size[0] - image.size[0]) / 2, (size[1] - image.size[1]) / 2))
+ # return an encoded image
+ background_stream = StringIO.StringIO()
+ background.save(background_stream, filetype)
+ return background_stream.getvalue().encode(encoding)
+
+def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG'):
+ """ Wrapper on image_resize_image, to resize images larger than the standard
+ 'big' image size: 1024x1024px.
+ :param base64_source: base64 encoded source image. If False,
+ the function returns False.
+ """
+ if not base64_source:
+ return False
+ return image_resize_image(base64_source, size, encoding, filetype, True)
+
+def image_resize_image_medium(base64_source, size=(180, 180), encoding='base64', filetype='PNG'):
+ """ Wrapper on image_resize_image, to resize to the standard 'medium'
+ image size: 180x180.
+ :param base64_source: base64 encoded source image. If False,
+ the function returns False.
+ """
+ if not base64_source:
+ return False
+ return image_resize_image(base64_source, size, encoding, filetype)
+
+def image_resize_image_small(base64_source, size=(50, 50), encoding='base64', filetype='PNG'):
+ """ Wrapper on image_resize_image, to resize to the standard 'small' image
+ size: 50x50.
+ :param base64_source: base64 encoded source image. If False,
+ the function returns False.
+ """
+ if not base64_source:
+ return False
+ return image_resize_image(base64_source, size, encoding, filetype)
+
+# ----------------------------------------
+# Misc image tools
+# ---------------------------------------
+
+def image_get_resized_images(base64_source, return_big=False, return_medium=True, return_small=True,
+ big_name='image', medium_name='image_medium', small_name='image_small'):
+ """ Standard tool function that returns a dictionary containing the
+ big, medium and small versions of the source image. This function
+ is meant to be used for the methods of functional fields for
+ models using images.
+
+ Default parameters are given to be used for the getter of functional
+ image fields, for example with res.users or res.partner. It returns
+ only image_medium and image_small values, to update those fields.
+
+ :param base64_source: if set to False, other values are set to False
+ also. The purpose is to be linked to the fields that hold images in
+ OpenERP and that are binary fields.
+ :param return_big: if set, return_dict contains the 'big_name' entry
+ :param return_medium: if set, return_dict contains the 'medium_name' entry
+ :param return_small: if set, return_dict contains the 'small_name' entry
+ :param big_name: name related to the big version of the image;
+ 'image' by default.
+ :param medium_name: name related to the medium version of the
+ image; 'image_medium' by default.
+ :param small_name: name related to the small version of the
+ image; 'image_small' by default.
+ :return return_dict: dictionary with resized images, depending on
+ previous parameters.
+ """
+ return_dict = dict()
+ if return_big: return_dict[big_name] = image_resize_image_big(base64_source)
+ if return_medium: return_dict[medium_name] = image_resize_image_medium(base64_source)
+ if return_small: return_dict[small_name] = image_resize_image_small(base64_source)
+ return return_dict
\ No newline at end of file
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 3aad91fb7b8..e90fadf0492 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -279,7 +279,11 @@ email_re = re.compile(r"""
""", re.VERBOSE)
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
-reference_re = re.compile("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE)
+
+# Updated in 7.0 to match the model name as well
+# Typical form of references is
+# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
+reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE)
def html2plaintext(html, body_id=None, encoding='utf-8'):
""" From an HTML text, convert the HTML to plain text.