diff --git a/README b/README index e7639e08fed..9449819d766 100644 --- a/README +++ b/README @@ -6,17 +6,9 @@ Customer Relationship Management software. More info at: http://www.openerp.com -Installation on Debian Ubuntu +Installation on Debian/Ubuntu ----------------------------- -Download the deb file and type: - - $ sudo dpkg -i - $ sudo apt-get install install -f - -Installation on Debian Ubuntu from nightly build ------------------------------------------------- - Add the the apt repository deb http://nightly.openerp.com/6.1/deb/ ./ @@ -26,6 +18,11 @@ in your source.list and type: $ sudo apt-get update $ sudo apt-get install openerp +Or download the deb file and type: + + $ sudo dpkg -i + $ sudo apt-get install install -f + Installation on RedHat, Fedora, CentOS -------------------------------------- @@ -42,6 +39,8 @@ Install the openerp rpm Installation on Windows ----------------------- + Check the notes in setup.py + Installation on MacOSX ----------------------- @@ -54,14 +53,7 @@ default master password is "admin". Detailed System Requirements ---------------------------- -You need the following software installed: - - postgresql-client, python-dateutil, python-feedparser, python-gdata, - python-ldap, python-libxslt1, python-lxml, python-mako, python-openid, - python-psycopg2, python-pybabel, python-pychart, python-pydot, - python-pyparsing, python-reportlab, python-simplejson, python-tz, - python-vatnumber, python-vobject, python-webdav, python-werkzeug, python-xlwt, - python-yaml, python-zsi +The dependencies are listed in setup.py For Luxembourg localization, you also need: diff --git a/openerp/addons/base/i18n/es_EC.po b/openerp/addons/base/i18n/es_EC.po index 284b4de49c8..b71c0857970 100644 --- a/openerp/addons/base/i18n/es_EC.po +++ b/openerp/addons/base/i18n/es_EC.po @@ -13,7 +13,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-11-05 04:40+0000\n" +"X-Launchpad-Export-Date: 2012-11-06 04:50+0000\n" "X-Generator: Launchpad (build 16232)\n" #. module: base diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 94a0be9e86c..105176bbd9c 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import collections import datetime import functools import operator @@ -31,9 +32,70 @@ REPLACE_WITH = lambda ids: (6, False, ids) class ConversionNotFound(ValueError): pass +class ColumnWrapper(object): + def __init__(self, column, cr, uid, pool, fromtype, context=None): + self._converter = None + self._column = column + if column._obj: + self._pool = pool + self._converter_args = { + 'cr': cr, + 'uid': uid, + 'model': pool[column._obj], + 'fromtype': fromtype, + 'context': context + } + @property + def converter(self): + if not self._converter: + self._converter = self._pool['ir.fields.converter'].for_model( + **self._converter_args) + return self._converter + + def __getattr__(self, item): + return getattr(self._column, item) + class ir_fields_converter(orm.Model): _name = 'ir.fields.converter' + def for_model(self, cr, uid, model, fromtype=str, context=None): + """ Returns a converter object for the model. A converter is a + callable taking a record-ish (a dictionary representing an openerp + record with values of typetag ``fromtype``) and returning a converted + records matching what :meth:`openerp.osv.orm.Model.write` expects. + + :param model: :class:`openerp.osv.orm.Model` for the conversion base + :returns: a converter callable + :rtype: (record: dict, logger: (field, error) -> None) -> dict + """ + columns = dict( + (k, ColumnWrapper(v.column, cr, uid, self.pool, fromtype, context)) + for k, v in model._all_columns.iteritems()) + converters = dict( + (k, self.to_field(cr, uid, model, column, fromtype, context)) + for k, column in columns.iteritems()) + + def fn(record, log): + converted = {} + for field, value in record.iteritems(): + if field in (None, 'id', '.id'): continue + if not value: + converted[field] = False + continue + try: + converted[field], ws = converters[field](value) + for w in ws: + if isinstance(w, basestring): + # wrap warning string in an ImportWarning for + # uniform handling + w = ImportWarning(w) + log(field, w) + except ValueError, e: + log(field, e) + + return converted + return fn + def to_field(self, cr, uid, model, column, fromtype=str, context=None): """ Fetches a converter for the provided column object, from the specified type. @@ -343,6 +405,10 @@ class ir_fields_converter(orm.Model): # [{subfield:ref1},{subfield:ref2},{subfield:ref3}] records = ({subfield:item} for item in record[subfield].split(',')) + def log(_, e): + if not isinstance(e, Warning): + raise e + warnings.append(e) for record in records: id = None refs = only_ref_fields(record) @@ -355,7 +421,7 @@ class ir_fields_converter(orm.Model): cr, uid, model, column, subfield, reference, context=context) warnings.extend(w2) - writable = exclude_ref_fields(record) + writable = column.converter(exclude_ref_fields(record), log) if id: commands.append(LINK_TO(id)) commands.append(UPDATE(id, writable)) diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index 8f05c30e20b..f3985045fb2 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -24,6 +24,7 @@ import logging import openerp.modules from openerp.osv import fields, osv +from tools.translate import _ _logger = logging.getLogger(__name__) @@ -336,6 +337,8 @@ class ir_translation(osv.osv): trans_model = self.pool.get(model) domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')] langs_ids = self.pool.get('res.lang').search(cr, uid, [('code', '!=', 'en_US')], context=context) + if not langs_ids: + raise osv.except_osv(_('Error'), _("Translation features are unavailable until you install an extra OpenERP translation.")) langs = [lg.code for lg in self.pool.get('res.lang').browse(cr, uid, langs_ids, context=context)] main_lang = 'en_US' translatable_fields = [] diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 9d798d8a636..df42bb81521 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -264,10 +264,15 @@ class res_partner(osv.osv, format_address): return False def _get_default_image(self, cr, uid, is_company, context=None, colorize=False): - if is_company: - image = open(openerp.modules.get_module_resource('base', 'static/src/img', 'company_image.png')).read() - else: - image = tools.image_colorize(open(openerp.modules.get_module_resource('base', 'static/src/img', 'avatar.png')).read()) + img_path = openerp.modules.get_module_resource('base', 'static/src/img', + ('company_image.png' if is_company else 'avatar.png')) + with open(img_path, 'rb') as f: + image = f.read() + + # colorize user avatars + if not is_company: + image = tools.image_colorize(image) + return tools.image_resize_image_big(image.encode('base64')) def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 57fb92a6ee0..200f144d595 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -38,7 +38,6 @@ import openerp.osv as osv import openerp.pooler as pooler import openerp.release as release import openerp.tools as tools -import openerp.tools.assertion_report as assertion_report from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID @@ -285,7 +284,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # processed_modules: for cleanup step after install # loaded_modules: to avoid double loading - report = assertion_report.assertion_report() + report = pool._assertion_report loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report) if tools.config['load_language']: diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index ae9772b661a..5aa2eca26ef 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -32,6 +32,7 @@ import openerp.cron import openerp.tools import openerp.modules.db import openerp.tools.config +from openerp.tools import assertion_report _logger = logging.getLogger(__name__) @@ -49,6 +50,7 @@ class Registry(object): self._store_function = {} self._init = True self._init_parent = {} + self._assertion_report = assertion_report.assertion_report() # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 60b6c4fc8f6..2d51ad7d6db 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1152,96 +1152,51 @@ class related(function): """ def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None): - self._field_get2(cr, uid, obj, context) - i = len(self._arg)-1 - sarg = name - while i>0: - if type(sarg) in [type([]), type( (1,) )]: - where = [(self._arg[i], 'in', sarg)] - else: - where = [(self._arg[i], '=', sarg)] - if domain: - where = map(lambda x: (self._arg[i],x[1], x[2]), domain) - domain = [] - sarg = obj.pool.get(self._relations[i]['object']).search(cr, uid, where, context=context) - i -= 1 - return [(self._arg[0], 'in', sarg)] + # assume self._arg = ('foo', 'bar', 'baz') + # domain = [(name, op, val)] => search [('foo.bar.baz', op, val)] + field = '.'.join(self._arg) + return map(lambda x: (field, x[1], x[2]), domain) def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None): - self._field_get2(cr, uid, obj, context=context) - if type(ids) != type([]): - ids=[ids] - objlst = obj.browse(cr, uid, ids) - for data in objlst: - t_id = data.id - t_data = data - for i in range(len(self.arg)): - if not t_data: break - field_detail = self._relations[i] - if not t_data[self.arg[i]]: - if self._type not in ('one2many', 'many2many'): - t_id = t_data['id'] - t_data = False - elif field_detail['type'] in ('one2many', 'many2many'): - if self._type != "many2one": - t_id = t_data.id - t_data = t_data[self.arg[i]][0] - else: - t_data = False - else: - t_id = t_data['id'] - t_data = t_data[self.arg[i]] - else: - model = obj.pool.get(self._relations[-1]['object']) - model.write(cr, uid, [t_id], {args[-1]: values}, context=context) + if isinstance(ids, (int, long)): + ids = [ids] + for record in obj.browse(cr, uid, ids, context=context): + # traverse all fields except the last one + for field in self.arg[:-1]: + record = record[field] or False + if not record: + break + elif isinstance(record, list): + # record is the result of a one2many or many2many field + record = record[0] + if record: + # write on the last field + record.write({self.arg[-1]: values}) def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None): - self._field_get2(cr, uid, obj, context) - if not ids: return {} - relation = obj._name - if self._type in ('one2many', 'many2many'): - res = dict([(i, []) for i in ids]) - else: - res = {}.fromkeys(ids, False) - - objlst = obj.browse(cr, SUPERUSER_ID, ids, context=context) - for data in objlst: - if not data: - continue - t_data = data - relation = obj._name - for i in range(len(self.arg)): - field_detail = self._relations[i] - relation = field_detail['object'] - try: - if not t_data[self.arg[i]]: - t_data = False - break - except: - t_data = False + res = {} + for record in obj.browse(cr, SUPERUSER_ID, ids, context=context): + value = record + for field in self.arg: + if isinstance(value, list): + value = value[0] + value = value[field] or False + if not value: break - if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1: - t_data = t_data[self.arg[i]][0] - elif t_data: - t_data = t_data[self.arg[i]] - if type(t_data) == type(objlst[0]): - res[data.id] = t_data.id - elif t_data: - res[data.id] = t_data - if self._type=='many2one': - ids = filter(None, res.values()) - if ids: - # name_get as root, as seeing the name of a related - # object depends on access right of source document, - # not target, so user may not have access. - ng = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, ids, context=context)) - for r in res: - if res[r]: - res[r] = (res[r], ng[res[r]]) + res[record.id] = value + + if self._type == 'many2one': + # res[id] is a browse_record or False; convert it to (id, name) or False. + # Perform name_get as root, as seeing the name of a related object depends on + # access right of source document, not target, so user may not have access. + value_ids = list(set(value.id for value in res.itervalues() if value)) + value_name = dict(obj.pool.get(self._obj).name_get(cr, SUPERUSER_ID, value_ids, context=context)) + res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems()) + elif self._type in ('one2many', 'many2many'): - for r in res: - if res[r]: - res[r] = [x.id for x in res[r]] + # res[id] is a list of browse_record or False; convert it to a list of ids + res = dict((id, value and map(int, value) or []) for id, value in res.iteritems()) + return res def __init__(self, *arg, **args): @@ -1252,22 +1207,6 @@ class related(function): # TODO: improve here to change self.store = {...} according to related objects pass - def _field_get2(self, cr, uid, obj, context=None): - if self._relations: - return - result = [] - obj_name = obj._name - for i in range(len(self._arg)): - f = obj.pool.get(obj_name).fields_get(cr, uid, [self._arg[i]], context=context)[self._arg[i]] - result.append({ - 'object': obj_name, - 'type': f['type'] - - }) - if f.get('relation',False): - obj_name = f['relation'] - result[-1]['relation'] = f['relation'] - self._relations = result class sparse(function): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 8d83e1058bc..ae0eb0f8bff 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1352,9 +1352,11 @@ class BaseModel(object): noupdate=noupdate, res_id=id, context=context)) cr.execute('RELEASE SAVEPOINT model_load_save') except psycopg2.Warning, e: + _logger.exception('Failed to import record %s', record) cr.execute('ROLLBACK TO SAVEPOINT model_load_save') messages.append(dict(info, type='warning', message=str(e))) except psycopg2.Error, e: + _logger.exception('Failed to import record %s', record) # Failed to write, log to messages, rollback savepoint (to # avoid broken transaction) and keep going cr.execute('ROLLBACK TO SAVEPOINT model_load_save') @@ -1458,15 +1460,16 @@ class BaseModel(object): field_names = dict( (f, (Translation._get_source(cr, uid, self._name + ',' + f, 'field', context.get('lang')) - or column.string or f)) + or column.string)) for f, column in columns.iteritems()) - converters = dict( - (k, Converter.to_field(cr, uid, self, column, context=context)) - for k, column in columns.iteritems()) + + convert = Converter.for_model(cr, uid, self, context=context) def _log(base, field, exception): type = 'warning' if isinstance(exception, Warning) else 'error' - record = dict(base, field=field, type=type, + # logs the logical (not human-readable) field name for automated + # processing of response, but injects human readable in message + record = dict(base, type=type, field=field, message=unicode(exception.args[0]) % base) if len(exception.args) > 1 and exception.args[1]: record.update(exception.args[1]) @@ -1476,7 +1479,6 @@ class BaseModel(object): for record, extras in stream: dbid = False xid = False - converted = {} # name_get/name_create if None in record: pass # xid @@ -1497,27 +1499,8 @@ class BaseModel(object): message=_(u"Unknown database identifier '%s'") % dbid)) dbid = False - for field, strvalue in record.iteritems(): - if field in (None, 'id', '.id'): continue - if not strvalue: - converted[field] = False - continue - - # In warnings and error messages, use translated string as - # field name - message_base = dict( - extras, record=stream.index, field=field_names[field]) - try: - converted[field], ws = converters[field](strvalue) - - for w in ws: - if isinstance(w, basestring): - # wrap warning string in an ImportWarning for - # uniform handling - w = ImportWarning(w) - _log(message_base, field, w) - except ValueError, e: - _log(message_base, field, e) + converted = convert(record, lambda field, err:\ + _log(dict(extras, record=stream.index, field=field_names[field]), field, err)) yield dbid, xid, converted, dict(extras, record=stream.index) @@ -3878,10 +3861,12 @@ class BaseModel(object): 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 and return a dict mapping ids to workflow results""" wf_service = netsvc.LocalService("workflow") + result = {} for res_id in ids: - wf_service.trg_validate(uid, self._name, res_id, signal, cr) + result[res_id] = wf_service.trg_validate(uid, self._name, res_id, signal, cr) + return result def unlink(self, cr, uid, ids, context=None): """ diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 94f179c7897..7703406ec96 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -188,7 +188,8 @@ class object_proxy(object): object = pooler.get_pool(cr.dbname).get(obj) if not object: raise except_osv('Object Error', 'Object %s doesn\'t exist' % str(obj)) - return object._workflow_signal(cr, uid, [args[0]], signal) + res_id = args[0] + return object._workflow_signal(cr, uid, [res_id], signal)[res_id] @check def exec_workflow(self, db, uid, obj, signal, *args): diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index acf4161f563..72a246d38a1 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -34,7 +34,6 @@ import openerp.netsvc import openerp.osv import openerp.tools import openerp.service.wsgi_server -import openerp.service.workers #.apidoc title: RPC Services @@ -123,6 +122,7 @@ def stop_services(): logging.shutdown() def start_services_workers(): + import openerp.service.workers openerp.multi_process = True openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run() diff --git a/openerp/service/workers.py b/openerp/service/workers.py index eae1b2839a8..7bdf45c079f 100644 --- a/openerp/service/workers.py +++ b/openerp/service/workers.py @@ -3,20 +3,17 @@ # TODO rename class: Multicorn -> Arbiter ? #----------------------------------------------------------- import errno -try: - import fcntl -except ImportError: - fcntl = None +import fcntl +import logging +import os import psutil import random import resource import select -import socket -import time -import logging -import os import signal +import socket import sys +import time import werkzeug.serving diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index aa39b777c0f..c4b77a606a9 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -9,7 +9,7 @@ See the :ref:`test-framework` section in the :ref:`features` list. """ from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\ - test_basecase, \ + test_fields, test_basecase, \ test_view_validation, test_uninstall, test_misc, test_db_cursor fast_suite = [ @@ -21,6 +21,7 @@ checks = [ test_html_sanitize, test_db_cursor, test_orm, + test_fields, test_basecase, test_view_validation, test_misc, diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py index 48bd9948a0f..95a7f90bda8 100644 --- a/openerp/tests/addons/test_impex/models.py +++ b/openerp/tests/addons/test_impex/models.py @@ -79,6 +79,7 @@ class One2ManyMultiple(orm.Model): _name = 'export.one2many.multiple' _columns = { + 'parent_id': fields.many2one('export.one2many.recursive'), 'const': fields.integer(), 'child1': fields.one2many('export.one2many.child.1', 'parent_id'), 'child2': fields.one2many('export.one2many.child.2', 'parent_id'), @@ -135,3 +136,11 @@ class SelectionWithDefault(orm.Model): 'const': 4, 'value': 2, } + +class RecO2M(orm.Model): + _name = 'export.one2many.recursive' + + _columns = { + 'value': fields.integer(), + 'child': fields.one2many('export.one2many.multiple', 'parent_id') + } diff --git a/openerp/tests/addons/test_impex/tests/contacts.json b/openerp/tests/addons/test_impex/tests/contacts.json new file mode 100644 index 00000000000..4f065037188 --- /dev/null +++ b/openerp/tests/addons/test_impex/tests/contacts.json @@ -0,0 +1 @@ +[["Wood y Wood Pecker", "", "Snow Street, 25", "Kainuu", "Finland", "Supplier", "1", "0", "1", ""], ["Roger Pecker", "Default", "Snow Street, 27", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Sharon Pecker", "Shipping", "Snow Street, 28", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Thomas Pecker", "Contact", "Snow Street, 27", "Kainuu", "Finland", "Supplier", "1", "0", "0", "Wood y Wood Pecker"], ["Norseman Roundabout", "", "Atonium Street, 45a", "Brussels", "Belgium", "Supplier", "1", "0", "1", ""], ["Yvan Holiday", "Invoice", "Atonium Street, 45b", "Brussels", "Belgium", "Supplier", "1", "0", "0", "Norseman Roundabout"], ["Jack Unsworth", "Contact", "Atonium Street, 45a", "Brussels", "Belgium", "Supplier", "1", "0", "0", "Norseman Roundabout"]] diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 8dac916b0d0..9ef2a43dec9 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json import pkgutil +import unittest2 import openerp.modules.registry import openerp @@ -246,7 +247,7 @@ class test_integer_field(ImporterCase): -1, -42, -(2**31 - 1), -(2**31), -12345678 ], values(self.read())) - @mute_logger('openerp.sql_db') + @mute_logger('openerp.sql_db', 'openerp.osv.orm') def test_out_of_range(self): result = self.import_(['value'], [[str(2**31)]]) self.assertIs(result['ids'], False) @@ -388,14 +389,14 @@ class test_unbound_string_field(ImporterCase): class test_required_string_field(ImporterCase): model_name = 'export.string.required' - @mute_logger('openerp.sql_db') + @mute_logger('openerp.sql_db', 'openerp.osv.orm') def test_empty(self): result = self.import_(['value'], [[]]) self.assertEqual(result['messages'], [message( u"Missing required value for the field 'unknown'")]) self.assertIs(result['ids'], False) - @mute_logger('openerp.sql_db') + @mute_logger('openerp.sql_db', 'openerp.osv.orm') def test_not_provided(self): result = self.import_(['const'], [['12']]) self.assertEqual(result['messages'], [message( @@ -1007,6 +1008,46 @@ class test_realworld(common.TransactionCase): self.assertFalse(result['messages']) self.assertEqual(len(result['ids']), len(data)) + def test_backlink(self): + data = json.loads(pkgutil.get_data(self.__module__, 'contacts.json')) + result = self.registry('res.partner').load( + self.cr, openerp.SUPERUSER_ID, + ["name", "type", "street", "city", "country_id", "category_id", + "supplier", "customer", "is_company", "parent_id"], + data) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), len(data)) + + def test_recursive_o2m(self): + """ The content of the o2m field's dict needs to go through conversion + as it may be composed of convertables or other relational fields + """ + self.registry('ir.model.data').clear_caches() + Model = self.registry('export.one2many.recursive') + result = Model.load(self.cr, openerp.SUPERUSER_ID, + ['value', 'child/const', 'child/child1/str', 'child/child2/value'], + [ + ['4', '42', 'foo', '55'], + ['', '43', 'bar', '56'], + ['', '', 'baz', ''], + ['', '55', 'qux', '57'], + ['5', '99', 'wheee', ''], + ['', '98', '', '12'], + ], + context=None) + + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 2) + + b = Model.browse(self.cr, openerp.SUPERUSER_ID, result['ids'], context=None) + self.assertEqual((b[0].value, b[1].value), (4, 5)) + + self.assertEqual([child.str for child in b[0].child[1].child1], + ['bar', 'baz']) + self.assertFalse(len(b[1].child[1].child1)) + self.assertEqual([child.value for child in b[1].child[1].child2], + [12]) + class test_date(ImporterCase): model_name = 'export.date' diff --git a/openerp/tests/test_fields.py b/openerp/tests/test_fields.py new file mode 100644 index 00000000000..ae401e77032 --- /dev/null +++ b/openerp/tests/test_fields.py @@ -0,0 +1,112 @@ +# +# test cases for fields access, etc. +# + +import unittest2 +import common + +import openerp +from openerp.osv import fields + +class TestRelatedField(common.TransactionCase): + + def setUp(self): + super(TestRelatedField, self).setUp() + self.partner = self.registry('res.partner') + self.company = self.registry('res.company') + + def test_0_related(self): + """ test an usual related field """ + # add a related field test_related_company_id on res.partner + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'related_company_partner_id': fields.related('company_id', 'partner_id', type='many2one', obj='res.partner'), + }) + + # find a company with a non-null partner_id + ids = self.company.search(self.cr, self.uid, [('partner_id', '!=', False)], limit=1) + id = ids[0] + + # find partners that satisfy [('partner_id.company_id', '=', id)] + company_ids = self.company.search(self.cr, self.uid, [('partner_id', '=', id)]) + partner_ids1 = self.partner.search(self.cr, self.uid, [('company_id', 'in', company_ids)]) + partner_ids2 = self.partner.search(self.cr, self.uid, [('related_company_partner_id', '=', id)]) + self.assertEqual(partner_ids1, partner_ids2) + + # restore res.partner fields + self.partner._columns = old_columns + + def do_test_company_field(self, field): + # get a partner with a non-null company_id + ids = self.partner.search(self.cr, self.uid, [('company_id', '!=', False)], limit=1) + partner = self.partner.browse(self.cr, self.uid, ids[0]) + + # check reading related field + self.assertEqual(partner[field], partner.company_id) + + # check that search on related field is equivalent to original field + ids1 = self.partner.search(self.cr, self.uid, [('company_id', '=', partner.company_id.id)]) + ids2 = self.partner.search(self.cr, self.uid, [(field, '=', partner.company_id.id)]) + self.assertEqual(ids1, ids2) + + def test_1_single_related(self): + """ test a related field with a single indirection like fields.related('foo') """ + # add a related field test_related_company_id on res.partner + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'single_related_company_id': fields.related('company_id', type='many2one', obj='res.company'), + }) + + self.do_test_company_field('single_related_company_id') + + # restore res.partner fields + self.partner._columns = old_columns + + def test_2_related_related(self): + """ test a related field referring to a related field """ + # add a related field on a related field on res.partner + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'single_related_company_id': fields.related('company_id', type='many2one', obj='res.company'), + 'related_related_company_id': fields.related('single_related_company_id', type='many2one', obj='res.company'), + }) + + self.do_test_company_field('related_related_company_id') + + # restore res.partner fields + self.partner._columns = old_columns + + def test_3_read_write(self): + """ write on a related field """ + # add a related field test_related_company_id on res.partner + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'related_company_partner_id': fields.related('company_id', 'partner_id', type='many2one', obj='res.partner'), + }) + + # find a company with a non-null partner_id + company_ids = self.company.search(self.cr, self.uid, [('partner_id', '!=', False)], limit=1) + company = self.company.browse(self.cr, self.uid, company_ids[0]) + + # find partners that satisfy [('partner_id.company_id', '=', company.id)] + partner_ids = self.partner.search(self.cr, self.uid, [('related_company_partner_id', '=', company.id)]) + partner = self.partner.browse(self.cr, self.uid, partner_ids[0]) + + # create a new partner, and assign it to company + new_partner_id = self.partner.create(self.cr, self.uid, {'name': 'Foo'}) + partner.write({'related_company_partner_id': new_partner_id}) + + company = self.company.browse(self.cr, self.uid, company_ids[0]) + self.assertEqual(company.partner_id.id, new_partner_id) + + partner = self.partner.browse(self.cr, self.uid, partner_ids[0]) + self.assertEqual(partner.related_company_partner_id.id, new_partner_id) + + # restore res.partner fields + self.partner._columns = old_columns + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/setup.py b/setup.py index 6550392e106..45f19ea7a7d 100755 --- a/setup.py +++ b/setup.py @@ -74,6 +74,19 @@ def py2exe_options(): execfile(join(os.path.dirname(__file__), 'openerp', 'release.py')) +# Notes for OpenERP developer on windows: +# +# To setup a windows developer evironement install python2.7 then pip and use +# "pip install " for every dependency listed below. +# +# Dependecies that requires DLLs are not installable with pip install, for +# them we added comments with links where you can find the installers. +# +# OpenERP on windows also require the pywin32, the binary can be found at +# http://pywin32.sf.net +# +# Both python2.7 32bits and 64bits are known to work. + setuptools.setup( name = 'openerp', version = version, @@ -90,29 +103,30 @@ setuptools.setup( dependency_links = ['http://download.gna.org/pychart/'], #include_package_data = True, install_requires = [ - 'pychart', + 'pychart', # not on pypi, use: pip install http://download.gna.org/pychart/PyChart-1.39.tar.gz 'babel', 'docutils', 'feedparser', 'gdata', - 'lxml < 3', + 'lxml < 3', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/ 'mako', - 'psutil', + 'PIL', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/ + 'psutil', # windows binary code.google.com/p/psutil/downloads/list 'psycopg2', 'pydot', 'python-dateutil < 2', - 'python-ldap', + 'python-ldap', # optional 'python-openid', 'pytz', 'pywebdav', 'pyyaml', - 'reportlab', + 'reportlab', # windows binary pypi.python.org/pypi/reportlab 'simplejson', 'vatnumber', 'vobject', 'werkzeug', 'xlwt', - 'zsi', + 'zsi', # optional ], extras_require = { 'SSL' : ['pyopenssl'],