diff --git a/AUTHORS b/AUTHORS index c8b2a90..c0c1d66 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Pedro A. Gracia Fajardo Gamaliel Toro +Raimon Esteve Cusiné diff --git a/README.md b/README.md index c8d4248..9ede61e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - +This is an experimental fork of OOOP. Use at your own risk. **Warning: this is a very initial release.** diff --git a/ooop.py b/ooop.py old mode 100644 new mode 100755 index 0d23fad..37247b6 --- a/ooop.py +++ b/ooop.py @@ -20,21 +20,17 @@ # ######################################################################## +import sys import xmlrpclib import time import base64 import types from datetime import datetime, date -# check if pydot is installed -try: - import pydot -except: - pydot = False __author__ = "Pedro Gracia " __license__ = "GPLv3+" -__version__ = "0.2.3" +__version__ = "0.3.0" OOOPMODELS = 'ir.model' @@ -108,11 +104,17 @@ def execute(self, *args, **kargs): return getattr(_model, action)(self.cr, uid) # is callable +class LoginFailed(Exception): + pass + + class OOOP: """ Main class to manage xml-rpc communication with openerp-server """ def __init__(self, user='admin', pwd='admin', dbname='openerp', uri='http://localhost', port=8069, debug=False, - exe=False, active=True, **kwargs): + exe=False, active=True, context=None, lang=None, + load_models=True, verify=True, + **kwargs): self.user = user # default: 'admin' self.pwd = pwd # default: 'admin' self.dbname = dbname # default: 'openerp' @@ -127,6 +129,24 @@ def __init__(self, user='admin', pwd='admin', dbname='openerp', self.uid = None self.models = {} self.fields = {} + self.verify = verify + + self.context = context if context else {} + + if lang: + self.context['lang'] = lang + + self.http_kws = {} + if self.uri.startswith('https://'): + if sys.version_info < (2, 7, 9): + if self.verify: + raise ValueError( + "SSL connections are not verified for " + "valid certificate in python version < 2.7.9") + else: + if not self.verify: + import ssl + self.http_kws["context"] = ssl._create_unverified_context() #has to be uid, cr, parent (the openerp model to get the pool) if len(kwargs) == 3: @@ -134,17 +154,21 @@ def __init__(self, user='admin', pwd='admin', dbname='openerp', self.objectsock = objectsock_mock(kwargs['parent'], kwargs['cr']) else: self.connect() - - self.load_models() + + if load_models: + self.load_models() + def connect(self): """login and sockets to xmlrpc services: common, object and report""" self.uid = self.login(self.dbname, self.user, self.pwd) - self.objectsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/object' % (self.uri, self.port)) - self.reportsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/report' % (self.uri, self.port)) - + if self.uid is False: + raise LoginFailed() + self.objectsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/object' % (self.uri, self.port), allow_none=True, **self.http_kws) + self.reportsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/report' % (self.uri, self.port), **self.http_kws) + def login(self, dbname, user, pwd): - self.commonsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/common' % (self.uri, self.port)) + self.commonsock = xmlrpclib.ServerProxy('%s:%i/xmlrpc/common' % (self.uri, self.port), **self.http_kws) return self.commonsock.login(dbname, user, pwd) def execute(self, model, *args): @@ -156,43 +180,45 @@ def create(self, model, data): """ create a new register """ if self.debug: print "DEBUG [create]:", model, data - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'create', data) + if 'id' in data: + del data['id'] + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'create', data, self.context) def unlink(self, model, ids): """ remove register """ if self.debug: print "DEBUG [unlink]:", model, ids - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'unlink', ids) + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'unlink', ids, self.context) def write(self, model, ids, value): """ update register """ if self.debug: print "DEBUG [write]:", model, ids, value - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'write', ids, value) + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'write', ids, value, self.context) def read(self, model, ids, fields=[]): """ update register """ if self.debug: print "DEBUG [read]:", model, ids, fields - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'read', ids, fields) + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'read', ids, fields, self.context) def read_all(self, model, fields=[]): """ update register """ if self.debug: print "DEBUG [read_all]:", model, fields - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'read', self.all(model), fields) + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'read', self.all(model), fields, self.context) - def search(self, model, query): + def search(self, model, query, offset=0, limit=None, order=None, count=False): """ return ids that match with 'query' """ if self.debug: - print "DEBUG [search]:", model, query - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'search', query) - + print "DEBUG [search]:", model, query, offset, limit, order + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, 'search', query, offset, limit, order, count, self.context) + # TODO: verify if remove this def custom_execute(self, model, ids, remote_method, data): if self.debug: print "DEBUG [custom_execute]:", self.dbname, self.uid, self.pwd, model, args - return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, ids, remote_method, data) + return self.objectsock.execute(self.dbname, self.uid, self.pwd, model, ids, remote_method, data, self.context) def all(self, model, query=[]): """ return all ids """ @@ -237,7 +263,15 @@ def set_model(self, model, r={}, deep=None): def export(self, filename, filetype, showfields=True, model=None, deep=-1): """Export the model to dot file""" #o2m 0..* m2m *..* m2o *..0 - + + ## import pydot only there because its the sole function using it, + ## and it's quite long to import. + # check if pydot is installed + try: + import pydot + except: + pydot = False + if not pydot: raise ImportError('no pydot package found') @@ -381,7 +415,7 @@ def delete(self): if self.parent: objects = self.parent.objects self.parent.objects = objects[:self.low] + objects[self.high:] - return self.manager._ooop.unlink(self.model, self.objects) + return self.manager._ooop.unlink(self.model, [o._ref for o in self.objects]) def append(self, value): if self.data: @@ -451,15 +485,27 @@ def all(self, fields=[], offset=0, limit=999999, as_list=False): def filter(self, fields=[], as_list=False, **kargs): q = [] # query dict + offset = 0 + limit = None + order = '' for key, value in kargs.items(): - if not '__' in key: - op = '=' - else: + if key == 'offset': + if int(value): + offset = value + elif key == 'limit': + if int(value): + limit = value + elif key == 'order': + order = value + elif '__' in key: i = key.find('__') op = OPERATORS[key[i+2:]] key = key[:i] - q.append(('%s' % key, op, value)) - ids = self._ooop.search(self._model, q) + q.append(('%s' % key, op, value)) + else: + op = '=' + q.append(('%s' % key, op, value)) + ids = self._ooop.search(self._model, q, offset, limit, order) if as_list: return self.read(ids, fields) return List(self, ids) @@ -519,11 +565,16 @@ def __init__(self, manager, ref=None, model=None, copy=False, data=None, fields= default_values = self._manager.default_get(field_names) # convert DateTime instance to datetime.datetime object for i in default_values: + if i not in self.fields: + continue if self.fields[i]['ttype'] == 'datetime': - t = default_values[i].timetuple() + if isinstance(default_values[i], str): + t = datetime.strptime(default_values[i], "%Y-%m-%d %H:%M:%S").timetuple() + else: + t = default_values[i].timetuple() default_values[i] = datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) # active by default ? - if self._ooop.active: + if 'active' in self.fields and self._ooop.active: default_values['active'] = True default_values.update(**kargs) # initial values from caller self.init_values(**default_values) @@ -541,7 +592,7 @@ def init_values(self, *args, **kargs): else: self.__dict__[name] = List(Manager(relation, self._ooop), data=self, model=relation) elif ttype == 'many2one': - if name in keys and kargs[name]: + if name in keys and kargs[name] is not False: # manager, ref=None, model=None, copy=False instance = Data(Manager(relation, self._ooop), kargs[name], relation) self.INSTANCES['%s:%s' % (relation, kargs[name])] = instance @@ -604,19 +655,22 @@ def __getattr__(self, field): try: name = self.fields[field]['name'] except: - raise NameError('field \'%s\' is not defined' % field) + raise AttributeError('field \'%s\' is not defined' % field) ttype = self.fields[field]['ttype'] relation = self.fields[field]['relation'] + assert(isinstance(data, list) and len(data) == 1) + data = data[0] if ttype == 'many2one': if data[name]: # TODO: review this self.__dict__['__%s' % name] = data[name] - key = '%s:%i' % (relation, data[name][0]) + assert(isinstance(data[name], int)) + key = '%s:%i' % (relation, data[name]) if key in self.INSTANCES.keys(): self.__dict__[name] = self.INSTANCES[key] else: # TODO: use a Manager instance, not Data instance = Data(Manager(relation, self._ooop), - data[name][0], relation, data=self) + data[name], relation, data=self) self.__dict__[name] = instance self.INSTANCES[key] = instance else: @@ -641,11 +695,10 @@ def __getattr__(self, field): self.__dict__[name] = List(Manager(relation, self._ooop), data=self, model=relation) elif ttype == "datetime" and data[name]: - p1, p2 = data[name].split(".", 1) - d1 = datetime.strptime(p1, "%Y-%m-%d %H:%M:%S") - ms = int(p2.ljust(6,'0')[:6]) - d1.replace(microsecond=ms) - self.__dict__[name] = d1 + if len(data[name]) > 19: + self.__dict__[name] = datetime.strptime(data[name], "%Y-%m-%d %H:%M:%S.%f") + else: + self.__dict__[name] = datetime.strptime(data[name], "%Y-%m-%d %H:%M:%S") elif ttype == "date" and data[name]: self.__dict__[name] = date.fromordinal(datetime.strptime(data[name], "%Y-%m-%d").toordinal()) else: @@ -683,7 +736,10 @@ def save(self): if self.__dict__[name]: data[name] = self.__dict__[name]._ref # update __name and INSTANCES (cache) - self.__dict__['__%s' % name] = [self.__dict__[name]._ref, self.__dict__[name].name] + ## TODO: remove the need of having a ".name" in object ! + ## XXXvlab: what's the use of the '__%s' attributes ? + if 'name' in dir(self.__dict__[name]): + self.__dict__['__%s' % name] = [self.__dict__[name]._ref, self.__dict__[name].name] self.INSTANCES['%s:%s' % (relation, self.__dict__[name]._ref)] = self.__dict__[name] if self._ooop.debug: @@ -692,7 +748,7 @@ def save(self): # create or write the object if self._ref > 0 and not self._copy: # same object - self._ooop.write(self._model, self._ref, data) + self._ooop.write(self._model, [self._ref], data) else: self._ref = self._ooop.create(self._model, data) @@ -703,7 +759,7 @@ def save(self): def delete(self): if self._ref > 0: - self._ooop.unlink(self._model, self._ref) + self._ooop.unlink(self._model, [self._ref]) #else: # pass # TODO remove(self)