├── README.md ├── odooclient ├── __init__.py ├── __init__.pyc ├── client.py ├── connection.py └── tools │ ├── __init__.py │ └── files_helper.py ├── test.py └── tests └── csv ├── .~lock.partners.csv# └── partners.csv /README.md: -------------------------------------------------------------------------------- 1 | #**Odoo Client Library** 2 | -- 3 | 4 | Very SIMPLE and STUPID Pythonic Odoo Client Library to make-use-of Odoo WebServices. It is designed to wrap all XML RPC Technicality into more object-orientated meaning of programming using class and dedicated Methods. 5 | 6 | ##__Features:__ 7 | - Basic-Generic **ORM API** (create, read, write, unlink ...) CRUD. 8 | - Generic **ORM Research API** (search, search_read, search_count, name_search ...). 9 | - Generic superset **Method** to avoid implmentation (symmetric code but need to be attentive). 10 | - Dedicated **WORKFLOW Methods** to drive business process smoothly. 11 | - Easy **SaaS/HTTPS or Local Instance/Database** Connection styles. 12 | 13 | 14 | -- 15 | 16 | 17 | ##Usage: 18 | 19 | - Add api directory `odooclient` to project. 20 | - Import `from odooclient import client`. 21 | 22 | ##Below are examples of `odoo-client-api` usage : 23 | 24 | - 25 | 26 | ##Saas Test 27 | ```python 28 | from odooclient import client 29 | 30 | odoo = client.OdooClient( host='demo.odoo.com', dbname='firebug', saas=True, debug=True) 31 | odoo.ServerInfo() 32 | odoo.Authenticate('admin', 'admin') 33 | ``` 34 | 35 | - 36 | 37 | ##SaaS Security Test 38 | ```python 39 | from odooclient import client 40 | 41 | odoo = client.OdooClient( host='demo.odoo.com', dbname='firebug', saas=True, debug=True) 42 | odoo.Authenticate('a@b.com', 'a') 43 | odoo.CheckSecurity('res.users', ['create']) 44 | odoo.CheckSecurity('res.partner') 45 | ``` 46 | 47 | - 48 | 49 | ##Local Test 50 | ```python 51 | from odooclient import client 52 | 53 | odoo = client.OdooClient(protocol='xmlrpc', host='localhost', dbname='test', port=8069, debug=True) 54 | odoo.ServerInfo() 55 | odoo.Authenticate('admin', 'admin') 56 | odoo.Read('res.partner', [1, 2], fields=['name']) 57 | rid = odoo.Create('res.partner', {'name': "Odoo"}) 58 | odoo.SearchCount('res.partner') 59 | odoo.Write('res.partner', [rid], {'name': 'Odoo'}) 60 | odoo.Search('res.partner', [('name', 'like', 'Odoo')], order='id desc') 61 | odoo.SearchRead('res.partner', [('name', 'like', 'Odoo')], ['name']) 62 | odoo.GetFields('res.partner') 63 | odoo.Copy('res.partner', record_id,{'name': "jigar"}) 64 | odoo.NameCreate('res.partner',"jigar") 65 | allids = [o[0] for o in odoo.NameSearch("res.partner", "jigar")] 66 | odoo.Unlink("res.partner", allids) 67 | ``` 68 | 69 | - 70 | 71 | ##Local Security Test 72 | ```python 73 | from odooclient import client 74 | 75 | odoo = client.OdooClient(protocol='xmlrpc', host='localhost', dbname='test', port=8069, debug=True) 76 | odoo.Authenticate('demo', 'demo') 77 | odoo.CheckSecurity('res.users', ['create']) 78 | odoo.CheckSecurity('res.partner' ) 79 | ``` 80 | 81 | ###Generic method `Method` 82 | 83 | - 84 | 85 | For More Control Over API , Generic `Method` is also implemented, where you can call any method from API, but you have to care full in passing Params : 86 | 87 | Method `create` with odoo-client-lib API look like : 88 | ``` 89 | odoo.Create('res.partner', {'name': "Odoo"}) 90 | ``` 91 | But this can be also called using Generic method `Method`which look like: 92 | ``` 93 | odoo.Method('res.partner', 'create', {'name': "Odoo"}) 94 | ``` 95 | This sounds more Raw and generic but doesn't make difference in this case as create being generic ORM method, but any method implementation specific to Model can be called using `Method`, this enable Extended API feature. 96 | 97 | ###Workflow methods : 98 | 99 | - 100 | 101 | Odoo has every powerful workflow engine and the lib facilitate you `workflow` engine methods like Create workflow for some record, or delete the workflow, validate workflow, trigger workflow signals. Below are example of all available methods that can be used. Suport for `worflow` is for Odoo/OpenERP v9 or earlier versionoly, from Odoov10 the workflow engone is discontinued. 102 | 103 | ``` 104 | from odooclient import client 105 | 106 | odoo = client.OdooClient(protocol='xmlrpc', host='localhost', dbname='asd', port=8069, debug=True) 107 | odoo.Authenticate('admin', 'admin') 108 | print odoo.UnlinkWorkflow('account.invoice', 58328) 109 | print odoo.CreateWorkflow('account.invoice', 58328) 110 | print odoo.StepWorkflow('account.invoice', 58328) 111 | print odoo.SignalWorkflow('account.invoice', 58328, 'invoice_open') 112 | print odoo.RedirectWorkflow('account.invoice', [(58328,58329)]) 113 | ``` 114 | 115 | 116 | Note: This is Still development copy not finalized. 117 | 118 | ##P.S. : 119 | 120 | Feature to be Added are : 121 | 122 | - Report Priting API 123 | - Tools lib. 124 | - CSV reader 125 | - Server Friendly Text Sanitizer(Unicode, UTF-8) 126 | - A Fork with ODBC Features. 127 | -------------------------------------------------------------------------------- /odooclient/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | __author__ = 'Jigar' 6 | __email__ = 'jam@odoo.com' 7 | __licence__ = 'LGPL v3' 8 | __version__ = '0.1.0' 9 | 10 | from . import connection 11 | from . import client 12 | from . import tools 13 | 14 | -------------------------------------------------------------------------------- /odooclient/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jam-odoo/odoo-client-api/5e2ff82f167f70427160effdcc5a791a4bde5229/odooclient/__init__.pyc -------------------------------------------------------------------------------- /odooclient/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | 6 | from . import connection 7 | 8 | FORMAT = "%(asctime)-15s - %(url)s - %(user)s :: " 9 | #logging.basicConfig(format=FORMAT, level=logging.DEBUG) 10 | 11 | _logger = logging.getLogger("OdooClient ") 12 | 13 | 14 | class OdooClient(object): 15 | """ 16 | >>> from odooclient import client 17 | >>> client.OdooClient(protocol='xmlrpc', host='firebug.odoo.com', port=443, dbname='firebug', saas=True) 18 | 19 | 20 | """ 21 | def __init__(self, protocol='xmlrpc', host='localhost', port=8069, version=2, dbname=None, saas=False, debug=False): 22 | if debug: 23 | logging.basicConfig(level=logging.DEBUG) 24 | if saas: 25 | protocol, port = 'xmlrpcs', 443 26 | 27 | self._served_protocols = {'xmlrpc': 'http', 'xmlrpcs': 'https'} 28 | 29 | self._protocol = self.__GetProtocol(protocol) 30 | self._host = host 31 | self._port = port 32 | self._db = dbname 33 | self._version = version 34 | 35 | self._login = False 36 | self._password = False 37 | self._uid = False 38 | 39 | self._serverinfo = {} 40 | 41 | self._url = "{protocol}://{host}:{port}".format(protocol=self._protocol, 42 | host=self._host, port=self._port,) 43 | _logger.debug('Url -> {url}'.format(url=self._url)) 44 | 45 | def __str__(self): 46 | return "".format(url=self._url) 47 | 48 | def __GetProtocol(self, protocol): 49 | if protocol not in self._served_protocols: 50 | raise NotImplementedError("The protocol '{0}' is not supported by the\ 51 | OdooClient. Please choose a protocol among these ones: {1}\ 52 | ".format(protocol, self._served_protocols)) 53 | return self._served_protocols.get(protocol) 54 | 55 | def ServerInfo(self): 56 | """ 57 | Code : 58 | common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url)) 59 | common.version() 60 | """ 61 | cn = connection.Connection(self._url, version=self._version) 62 | self._serverinfo = cn.GetServerInfo() 63 | return self._serverinfo 64 | 65 | def IsAuthenticated(self): 66 | cn = all([self._uid, self._login, self._password]) and True or False 67 | return cn 68 | 69 | def Authenticate(self, login, pwd): 70 | """ 71 | Code : 72 | common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url)) 73 | uid = common.authenticate(db, username, password, {}) 74 | """ 75 | self._login, self._password = login, pwd 76 | service = connection.Connection(self._url, version=self._version) 77 | self._uid = service.Authenticate(self._db, login, pwd, {}) 78 | return self._uid 79 | 80 | def Login(self, login, pwd): 81 | """ 82 | Code : 83 | common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url)) 84 | uid = common.authenticate(db, username, password, {}) 85 | """ 86 | self._login, self._password = login, pwd 87 | service = connection.Connection(self._url, version=self._version) 88 | self._uid = service.Login(self._db, login, pwd) 89 | return self._uid 90 | 91 | 92 | 93 | def CheckSecurity(self, model, operation_modes=['read']): 94 | """ 95 | models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url)) 96 | models.execute_kw(db, uid, password, 97 | 'res.partner', 'check_access_rights', 98 | ['read'], {'raise_exception': False}) 99 | """ 100 | service = connection.Connection(self._url, version=self._version) 101 | results = dict.fromkeys(operation_modes, False) 102 | for mode in operation_modes: 103 | response = service.Model(self._db, self._uid, self._password, model, \ 104 | 'check_access_rights', mode, raise_exception=False) 105 | results.update({mode: response}) 106 | return results 107 | 108 | def Method(self, model, method, *args, **kwrags): 109 | """ 110 | Generic Method Call if you don't find specific implementation. 111 | 112 | models.execute_kw(db, uid, password, 113 | '', '', args1, args1, ..., argsN 114 | {'kwy': ['val1', 'val2', 'valn'], 'key2': val2}) 115 | """ 116 | if not kwrags: kwrags = {} 117 | service = connection.Connection(self._url, version=self._version) 118 | response = service.Model(self._db, self._uid, self._password, model, 119 | method, *args, **kwrags) 120 | return response 121 | 122 | def Read(self, model, document_ids, fields=False, context=None): 123 | """ 124 | models.execute_kw(db, uid, password, 125 | 'res.partner', 'read', 126 | [ids], {'fields': ['name', 'country_id', 'comment']}) 127 | """ 128 | if not context: 129 | context = {} 130 | if type(document_ids) not in (int, complex, list, tuple): 131 | msg = "Invalid ids `type` {ids}. Ids should be on type `int`, \ 132 | `long`, `list` or 'tuple'.".format(ids=ids) 133 | return (False, msg) 134 | service = connection.Connection(self._url, version=self._version) 135 | response = service.Model(self._db, self._uid, self._password, model, 'read', document_ids, fields=fields, context=context) 136 | return response 137 | 138 | def Search(self, model, domain=False, context=None,**kwargs): 139 | """ 140 | search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False) 141 | 142 | models.execute_kw(db, uid, password, 143 | 'res.partner', 'search', 144 | [[['is_company', '=', True], ['customer', '=', True]]], 145 | {'offset': 10, 'limit': 5}) 146 | 147 | """ 148 | if not context: 149 | context = {} 150 | if not kwargs: 151 | kwargs ={} 152 | kwargs.update({'context': context}) 153 | service = connection.Connection(self._url, version=self._version) 154 | response = service.Model(self._db, self._uid, self._password, model, 155 | 'search', domain or [], **kwargs) 156 | return response 157 | 158 | def SearchCount(self, model, domain=False, context=None): 159 | """ 160 | search_count(self, cr, user, args, context=None): 161 | 162 | models.execute_kw(db, uid, password, 163 | 'res.partner', 'search_count', 164 | [[['is_company', '=', True], ['customer', '=', True]]]) 165 | 166 | """ 167 | if not context: 168 | context = {} 169 | service = connection.Connection(self._url, version=self._version) 170 | response = service.Model(self._db, self._uid, self._password, model, 171 | 'search_count', domain or [], context=context) 172 | return response 173 | 174 | def SearchRead(self, model, domain=False, fields=False, context=None, **kwargs): 175 | """ 176 | search_read(self, cr, uid, domain=None, fields=None, offset=0, 177 | limit=None, order=None, context=None): 178 | 179 | models.execute_kw(db, uid, password, 180 | 'res.partner', 'search_read', 181 | [[['is_company', '=', True], ['customer', '=', True]]], 182 | {'fields': ['name', 'country_id', 'comment'], 'limit': 5}) 183 | 184 | """ 185 | if not context: 186 | context = {} 187 | if not kwargs: 188 | kwargs ={} 189 | kwargs.update({'context': context}) 190 | 191 | service = connection.Connection(self._url, version=self._version) 192 | response = service.Model(self._db, self._uid, self._password, model, 193 | 'search_read', domain or [], 194 | fields=fields, **kwargs) 195 | return response 196 | 197 | def NameSearch(self, model, name, domain=False, context=None,**kwargs): 198 | """ 199 | name_search(name='', domain=None, operator='ilike', limit=100) 200 | 201 | models.execute_kw(db, uid, password, 202 | 'res.partner', 'name_search', 203 | [[['is_company', '=', True], ['customer', '=', True]]], 204 | {'offset': 10, 'limit': 5}) 205 | 206 | """ 207 | if not context: 208 | context = {} 209 | if not kwargs: 210 | kwargs ={} 211 | kwargs.update({'context': context}) 212 | service = connection.Connection(self._url, version=self._version) 213 | response = service.Model(self._db, self._uid, self._password, model, 214 | 'name_search', name, domain or [], **kwargs) 215 | return response 216 | 217 | def Create(self, model, values, context=None): 218 | """ 219 | create(self, vals): 220 | 221 | id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{ 222 | 'name': "New Partner", 223 | }]) 224 | 225 | """ 226 | if not context: context = {} 227 | service = connection.Connection(self._url, version=self._version) 228 | response = service.Model(self._db, self._uid, self._password, model, 229 | 'create', values, context=context) 230 | return response 231 | 232 | def NameCreate(self, model, name, context=None): 233 | """ 234 | name_create(name, context) 235 | 236 | models.execute_kw(db, uid, password, 237 | 'res.partner', 'name_create', 238 | [[['is_company', '=', True], ['customer', '=', True]]], 239 | {'offset': 10, 'limit': 5}) 240 | 241 | """ 242 | if not context: 243 | context = {} 244 | service = connection.Connection(self._url, version=self._version) 245 | response = service.Model(self._db, self._uid, self._password, model, 246 | 'name_create', name, context=context) 247 | return response 248 | 249 | def Write(self, model, document_ids, values, context=None): 250 | """ 251 | write(self, vals): 252 | 253 | models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], { 254 | 'name': "Newer partner" 255 | }]) 256 | """ 257 | if not context: context = {} 258 | service = connection.Connection(self._url, version=self._version) 259 | response = service.Model(self._db, self._uid, self._password, model, 260 | 'write', document_ids, values, context=context) 261 | return response 262 | 263 | def GetFields(self, model, context=None, attributes=None): 264 | """ 265 | fields_get(self, cr, user, allfields=None, context=None, 266 | write_access=True, attributes=None) 267 | models.execute_kw( 268 | db, uid, password, 'res.partner', 'fields_get', 269 | [], {'attributes': ['string', 'help', 'type']}) 270 | """ 271 | if not context: context = {} 272 | if not attributes: attributes = [] 273 | 274 | service = connection.Connection(self._url, version=self._version) 275 | response = service.Model(self._db, self._uid, self._password, model, 276 | 'fields_get', context=context, attributes=attributes) 277 | return response 278 | 279 | def Unlink(self, model, document_ids, context=None): 280 | """ 281 | 282 | models.execute_kw(db, uid, password, 283 | 'res.partner', 'unlink', [[id]]) 284 | """ 285 | if not context: context = {} 286 | service = connection.Connection(self._url, version=self._version) 287 | response = service.Model(self._db, self._uid, self._password, model, 288 | 'unlink', document_ids, context=context) 289 | return response 290 | 291 | def Copy(self, model, document_ids, default=None, context=None): 292 | """ 293 | copy(default=None) 294 | models.execute_kw(db, uid, password, 295 | 'res.partner', 'copy', [id], {'field1': "default values"}) 296 | """ 297 | if not context: context = {} 298 | if not default: default = {} 299 | service = connection.Connection(self._url, version=self._version) 300 | response = service.Model(self._db, self._uid, self._password, model, 301 | 'copy', document_ids, default=default, context=context) 302 | return response 303 | 304 | def CreateWorkflow(self, model, document_ids, context=None): 305 | """ 306 | def create_workflow(self, cr, uid, ids, context=None): 307 | Create a workflow instance for each given record IDs 308 | """ 309 | if not context: context = {} 310 | if type(document_ids) not in (int, long, list, tuple): 311 | raise Exception("Document Ids expected to be in int, long list or tuple format.") 312 | if type(document_ids) in (int, long): 313 | document_ids = [document_ids] 314 | service = connection.Connection(self._url, version=self._version) 315 | response = service.Model(self._db, self._uid, self._password, model, 316 | 'create_workflow', document_ids, context=context) 317 | return response 318 | 319 | def UnlinkWorkflow(self, model, document_ids, context=None): 320 | """ 321 | def delete_workflow(self, cr, uid, ids, context=None): 322 | Delete the workflow instances bound to the given record IDs. 323 | """ 324 | if not context: context = {} 325 | if type(document_ids) not in (int, long, list, tuple): 326 | raise Exception("Document Ids expected to be in int, long list or tuple format.") 327 | if type(document_ids) in (int, long): 328 | document_ids = [document_ids] 329 | service = connection.Connection(self._url, version=self._version) 330 | response = service.Model(self._db, self._uid, self._password, model, 331 | 'delete_workflow', document_ids, context=context) 332 | return response 333 | 334 | def StepWorkflow(self, model, document_ids, context=None): 335 | """ 336 | def step_workflow(self, cr, uid, ids, context=None): 337 | Reevaluate the workflow instances of the given record IDs. 338 | 339 | """ 340 | if not context: context = {} 341 | if type(document_ids) not in (int, long, list, tuple): 342 | raise Exception("Document Ids expected to be in int, long list or tuple format.") 343 | if type(document_ids) in (int, long): 344 | document_ids = [document_ids] 345 | service = connection.Connection(self._url, version=self._version) 346 | response = service.Model(self._db, self._uid, self._password, model, 347 | 'step_workflow', document_ids, context=context) 348 | return response 349 | 350 | def SignalWorkflow(self, model, document_ids, signal, context=None): 351 | """ 352 | def signal_workflow(self, cr, uid, ids, signal, context=None): 353 | Send given workflow signal and return a dict mapping ids to workflow results 354 | """ 355 | if not context: context = {} 356 | if type(document_ids) not in (int, long, list, tuple): 357 | raise Exception("Document Ids expected to be in int, long list or tuple format.") 358 | if type(document_ids) in (int, long): 359 | document_ids = [document_ids] 360 | service = connection.Connection(self._url, version=self._version) 361 | response = service.Model(self._db, self._uid, self._password, model, 362 | 'signal_workflow', document_ids, signal,context=context) 363 | return response 364 | 365 | def RedirectWorkflow(self, model, old_new_ids, context=None): 366 | """ 367 | def redirect_workflow(self, cr, uid, old_new_ids, context=None): 368 | Rebind the workflow instance bound to the given 'old' record IDs to 369 | the given 'new' IDs. (``old_new_ids`` is a list of pairs ``(old, new)``. 370 | """ 371 | if not context: context = {} 372 | if type(old_new_ids) not in (list, tuple): 373 | raise Exception("Document Ids expected to be in list/tuple of tuples [(1,2)] format.") 374 | service = connection.Connection(self._url, version=self._version) 375 | response = service.Model(self._db, self._uid, self._password, model, 376 | 'redirect_workflow', old_new_ids, context=context) 377 | return response 378 | 379 | def PrintReport(self, report_service, record_ids, context=None): 380 | """ 381 | invoice_ids = models.execute_kw( 382 | db, uid, password, 'account.invoice', 'search', 383 | [[('type', '=', 'out_invoice'), ('state', '=', 'open')]]) 384 | report = xmlrpclib.ServerProxy('{}/xmlrpc/2/report'.format(url)) 385 | result = report.render_report( 386 | db, uid, password, 'account.report_invoice', invoice_ids) 387 | report_data = result['result'].decode('base64') 388 | 389 | """ 390 | if not context: context = {} 391 | service = connection.Connection(self._url, version=self._version) 392 | response = service.Report(self._db, self._uid, self._password, report_service, 393 | record_ids, context=context) 394 | return response -------------------------------------------------------------------------------- /odooclient/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pprint 5 | import logging 6 | import xmlrpc.client 7 | 8 | 9 | _logger = logging.getLogger("OdooClient ") 10 | 11 | 12 | class ServiceManager(object): 13 | """ 14 | Class Will service Odoo service proxy: 15 | @param url : Database Connection URL 16 | @param service : Name of the service called 17 | @param version : Odoo WebService Version No default(2) 18 | 19 | """ 20 | 21 | def __str__(self): 22 | return "" 23 | 24 | def __init__(self, url, service, version=2): 25 | 26 | if service not in ('common', 'object', 'report', 'db'): 27 | raise NotImplementedError("Unknown Service {service}.".format(service=service)) 28 | self._service = service 29 | self._version = version 30 | self._url = url 31 | if version: 32 | self._proxy = xmlrpc.client.ServerProxy("{url}/xmlrpc/{version}/{service}".format(url=self._url, version=self._version, service=self._service), allow_none=True) 33 | else: 34 | self._proxy = xmlrpc.client.ServerProxy("{url}/xmlrpc/{service}".format(url=self._url, service=self._service), allow_none=True) 35 | 36 | def Trasmit(self, method, *args, **kwargs): 37 | try: 38 | response = getattr(self._proxy, method)(*args) 39 | _logger.debug("RPC Response of Method `%s` -> %s"%(method, response)) 40 | return response 41 | except xmlrpc.client.ProtocolError as err: 42 | _logger.debug("A protocol error occurred: \n - URL :{url}\n - Error Code : {code}\n - Error message: {msg}".format(url=err.url, code=err.errcode, msg=err.errmsg)) 43 | raise err 44 | except Exception as er: 45 | _logger.debug("Unexpected Error : \n{e}".format(e=er)) 46 | raise er 47 | 48 | class Connection(object): 49 | """ 50 | 51 | @param url : Database Connection URL 52 | @param service : Name of the service called 53 | @param version : Odoo Web Service Version No default(2) 54 | 55 | """ 56 | def __init__(self, url, service='common', version=2): 57 | 58 | if service not in ('common', 'object', 'report', 'db'): 59 | raise NotImplementedError("Unknown Service {service}.".format(service=service)) 60 | self._service = service 61 | self._version = version 62 | self._url = url 63 | self._serverinfo = {} 64 | 65 | def __str__(self): 66 | return "".format(url=self._url) 67 | 68 | def GetServerInfo(self): 69 | self._serverinfo = ServiceManager(self._url,'common', self._version).Trasmit('version') 70 | return self._serverinfo 71 | 72 | def Authenticate(self, db, user, password, session={}): 73 | try: 74 | response = ServiceManager(self._url,'common', self._version).Trasmit('authenticate', db, user, password, session) 75 | if response: 76 | _logger.debug("Successful Authentication of `{user}` Using Database `{db}`.".format(user=user, db=db)) 77 | else: 78 | _logger.debug("Unsuccessful Authentication Attempt of `{user}` Using Database `{db}`.".format(user=user, db=db)) 79 | return response 80 | except Exception as e: 81 | print ("Authenticate Exception :\n %s \n"%(e)) 82 | pass 83 | 84 | def Login(self, db, user, password): 85 | try: 86 | response = ServiceManager(self._url,'common', self._version).Trasmit('login', db, user, password) 87 | if response: 88 | _logger.debug("Successful Authentication of `{user}` Using Database `{db}`.".format(user=user, db=db)) 89 | else: 90 | _logger.debug("Unsuccessful Authentication Attempt of `{user}` Using Database `{db}`.".format(user=user, db=db)) 91 | return response 92 | except Exception as e: 93 | print ("Authenticate Exception :\n %s \n"%(e)) 94 | pass 95 | 96 | def Model(self, db, uid, password, model, method, *args, **kwrags): 97 | try: 98 | if self._version: 99 | response = ServiceManager(self._url,'object', self._version).Trasmit('execute_kw', db, uid, password, model, method, args, kwrags) 100 | else: 101 | response = ServiceManager(self._url,'object', self._version).Trasmit('execute', db, uid, password, model, method, *args, **kwrags) 102 | return response 103 | except Exception as e: 104 | print ("Unknown Exception :\n %s \n"%(e)) 105 | pass 106 | 107 | 108 | def Report(self, db, uid, password, report_service, record_ids, *args, **kwrags): 109 | try: 110 | if self._version: 111 | response = ServiceManager(self._url, 'report', self._version).Trasmit('render_report', db, uid, password, report_service, record_ids, args, kwrags) 112 | else: 113 | response = ServiceManager(self._url,'report', self._version).Trasmit('render_report', db, uid, password, report_service, record_ids, *args, **kwrags) 114 | return response 115 | except Exception as e: 116 | print ("Unknown Exception :\n %s \n"%(e)) 117 | pass 118 | -------------------------------------------------------------------------------- /odooclient/tools/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | __author__ = 'Jigar' 6 | __email__ = 'jam@odoo.com' 7 | __licence__ = 'LGPL v3' 8 | __version__ = '1.1.0' 9 | 10 | 11 | from .files_helper import * -------------------------------------------------------------------------------- /odooclient/tools/files_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import os 4 | import csv 5 | import xlrd 6 | import pprint 7 | import base64 8 | import logging 9 | from urllib.parse import urlparse 10 | import requests 11 | import itertools 12 | import traceback 13 | import importlib 14 | import io 15 | from PIL import Image 16 | 17 | FORMAT = "%(asctime)-15s - %(url)s - %(user)s :: " 18 | #logging.basicConfig(format=FORMAT, level=logging.DEBUG) 19 | _logger = logging.getLogger("OdooClient ") 20 | 21 | DEFAULT_IMAGE_TIMEOUT = 100 22 | DEFAULT_IMAGE_MAXBYTES = 25 * 1024 * 1024 23 | DEFAULT_IMAGE_CHUNK_SIZE = 32768 24 | 25 | pp = pprint.PrettyPrinter(indent=4) 26 | 27 | def chunks(l, n): 28 | for i in range(0, len(l), n): 29 | yield l[i:i + n] 30 | 31 | def read_csv_data(path): 32 | """ 33 | Reads CSV from given path and Return list of dict with Mapping 34 | """ 35 | data = csv.reader(open(path, encoding='ISO-8859-1')) 36 | # Read the column names from the first line of the file 37 | fields = next(data) 38 | data_lines = [] 39 | for row in data: 40 | items = dict(zip(fields, row)) 41 | data_lines.append(items) 42 | return data_lines 43 | 44 | def get_field_mapping(values, mapping): 45 | """ 46 | Final Field Mapper for the Preparing Data for the Import Data 47 | use for def load in orm 48 | """ 49 | fields=[] 50 | data_lst = [] 51 | for key, val in mapping.items(): 52 | if key not in fields and values: 53 | fields.append(key) 54 | value = values.get(val) 55 | if value == None: 56 | value = '' 57 | data_lst.append(value) 58 | return fields, data_lst 59 | 60 | def read_xls(fname): 61 | """ Read file content, using xlrd lib """ 62 | return xlrd.open_workbook(fname) 63 | 64 | def read_xls_sheet(sheet): 65 | for row in map(sheet.row, range(sheet.nrows)): 66 | values = [] 67 | for cell in row: 68 | if cell.ctype is xlrd.XL_CELL_NUMBER: 69 | is_float = cell.value % 1 != 0.0 70 | values.append( 71 | str(cell.value) 72 | if is_float 73 | else int(cell.value) 74 | ) 75 | elif cell.ctype is xlrd.XL_CELL_DATE: 76 | is_datetime = cell.value % 1 != 0.0 77 | # emulate xldate_as_datetime for pre-0.9.3 78 | dt = datetime.datetime(*xlrd.xldate.xldate_as_tuple(cell.value, book.datemode)) 79 | values.append( 80 | dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) 81 | if is_datetime 82 | else dt.strftime(DEFAULT_SERVER_DATE_FORMAT) 83 | ) 84 | elif cell.ctype is xlrd.XL_CELL_BOOLEAN: 85 | values.append(u'True' if cell.value else u'False') 86 | elif cell.ctype is xlrd.XL_CELL_ERROR: 87 | raise ValueError( 88 | _("Error cell found while reading XLS/XLSX file: %s") % 89 | xlrd.error_text_from_code.get( 90 | cell.value, "unknown error code %s" % cell.value) 91 | ) 92 | else: 93 | values.append(cell.value) 94 | if any(x for x in values if x.strip()): 95 | yield values 96 | 97 | def read_filemap(walk_dir): 98 | file_map = dict([ 99 | (filename, os.path.join(root, filename)) 100 | for root, subdirs, files in os.walk(walk_dir) 101 | for filename in files 102 | ]) 103 | print ('Found {:6} files in dir "{}" .'.format(len(file_map), walk_dir)) 104 | return file_map 105 | 106 | def read_image_data(file_path): 107 | img = PIL.Image.open(file_path) 108 | image_buffer = io.StringIO() 109 | img.save(image_buffer, format="JPEG") 110 | image_data = base64.b64encode(image_buffer.getvalue()) 111 | return image_data 112 | 113 | def cache_model_data(conn, model, key_field=None, value_field=None): 114 | """ 115 | This help in caching model in dict in running script so 116 | for getting some id for relations, wea are not forced to query db 117 | """ 118 | if key_field == None: 119 | key_field = 'name' 120 | if value_field == None: 121 | value_field = 'id' 122 | record_ids = {} 123 | for record in conn.SearchRead(model, [],fields=[ key_field, value_field ]): 124 | record_ids.update({record[key_field]: record[value_field]}) 125 | print ('Cached {:6} records of model `{}`.'.format(len(record_ids), model)) 126 | return record_ids 127 | 128 | def import_image_by_url(url, limit_large_image=False): 129 | """ Imports an image by URL 130 | :param str url: the original field value 131 | :return: the replacement value 132 | :rtype: bytes 133 | """ 134 | maxsize = DEFAULT_IMAGE_MAXBYTES 135 | try: 136 | response = requests.get(url, timeout=DEFAULT_IMAGE_TIMEOUT) 137 | response.raise_for_status() 138 | 139 | if response.headers.get('Content-Length') and int(response.headers['Content-Length']) > maxsize: 140 | raise ValueError("File size exceeds configured maximum (%s bytes)") % maxsize 141 | 142 | content = bytearray() 143 | for chunk in response.iter_content(DEFAULT_IMAGE_CHUNK_SIZE): 144 | content += chunk 145 | if len(content) > maxsize: 146 | print ("File size exceeds configured maximum (%s bytes)" % maxsize) 147 | 148 | # Image resolution check incase if we do not want to allow large image file. 149 | if limit_large_image: 150 | image = Image.open(io.BytesIO(content)) 151 | w, h = image.size 152 | if w * h > 42e6: # Nokia Lumia 1020 photo resolution 153 | print ("Image size excessive, imported images must be smaller than 42 million pixel") 154 | return False 155 | image_file_name = os.path.basename(urlparse(url).path) 156 | 157 | return image_file_name, base64.b64encode(content) 158 | except Exception as e: 159 | print ("Could not retrieve URL: %(url)s: %(error)s"%{'url': url, 'error': e}) 160 | return False -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pprint 4 | 5 | from odooclient import client 6 | from odooclient import tools 7 | 8 | pp = pprint.PrettyPrinter(indent=4) 9 | 10 | #Saas Test 11 | odoo = client.OdooClient( host='test.odoo.com', dbname='firebug', saas=True, debug=True) 12 | print odoo 13 | odoo.ServerInfo() 14 | odoo.Authenticate('admin', 'admin') 15 | odoo.Read('res.partner', [1, 2], fields=['name']) 16 | odoo.Search('res.partner') 17 | odoo.SearchCount('res.partner') 18 | odoo.SearchRead('res.partner', [], ['name']) 19 | odoo.GetFields('res.partner') 20 | 21 | #Security Test 22 | odoo.Authenticate('a@b.com', 'a') 23 | odoo.CheckSecurity('res.users', ['create']) 24 | odoo.CheckSecurity('res.partner') 25 | 26 | 27 | #Local Ruuning Odoo Connection 28 | odoo = client.OdooClient(protocol='xmlrpc', host='localhost', dbname='test', port=8069, debug=True) 29 | odoo.ServerInfo() 30 | odoo.Authenticate('admin', 'admin') 31 | odoo.Read('res.partner', [1, 2], fields=['name']) 32 | record_id = odoo.Create('res.partner', {'name': 'Dummy'}) 33 | odoo.SearchCount('res.partner') 34 | odoo.Write('res.partner', [record_id], {'name': 'Dummy Persion'}) 35 | odoo.Search('res.partner', [('name', 'like', 'Dummy')], order='id desc') 36 | odoo.SearchRead('res.partner', [('name', 'like', 'Dummy')], ['name']) 37 | odoo.GetFields('res.partner') 38 | odoo.Copy('res.partner', record_id,{'name': 'Dummy'}) 39 | odoo.NameCreate('res.partner','Dummy') 40 | allids = [o[0] for o in odoo.NameSearch('res.partner', 'Dummy')] 41 | odoo.Unlink('res.partner', allids) 42 | 43 | 44 | #Workflow methods works for v9 or earlier vrsion only 45 | odoo.UnlinkWorkflow('account.invoice', 1) 46 | odoo.CreateWorkflow('account.invoice', 1) 47 | odoo.StepWorkflow('account.invoice', 1) 48 | odoo.SignalWorkflow('account.invoice', 1, 'invoice_open') 49 | odoo.RedirectWorkflow('account.invoice', [(1,2)]) 50 | 51 | #Reading a CSV files: 52 | data_lines = tools.read_csv_data('./tests/csv/partners.csv') 53 | pp.pprint(data_lines) -------------------------------------------------------------------------------- /tests/csv/.~lock.partners.csv#: -------------------------------------------------------------------------------- 1 | ,shadows,shadows-oryx-pro,09.11.2018 12:19,file:///home/shadows/.config/libreoffice/4; -------------------------------------------------------------------------------- /tests/csv/partners.csv: -------------------------------------------------------------------------------- 1 | Name,Parent name,Company/Company Name,Street,Street2,City,State,Country/Country Name,Zip,Is a Company,Is a Customer,Is a Vendor,Website 2 | ASUSTeK,,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,True,False,True,http://www.asustek.com 3 | Arthur Gomez,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 4 | James Miller,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 5 | Joseph Walters,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 6 | Julia Rivero,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 7 | Peter Mitchell,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 8 | Tang Tsui,ASUSTeK,YourCompany,31 Hong Kong street,,Taipei,,Taiwan,106,False,False,True, 9 | Pieter Parter's Farm,Administrator,YourCompany,215 Vine St,,Scranton,Pennsylvania,United States,18503,False,True,True, 10 | Agrolait,,YourCompany,69 rue de Namur,,Wavre,,Belgium,1300,True,True,False,http://www.agrolait.com 11 | Edward Foster,Agrolait,YourCompany,69 rue de Namur,,Wavre,,Belgium,1300,False,False,True, 12 | Laith Jubair,Agrolait,YourCompany,69 rue de Namur,,Wavre,,Belgium,1300,False,True,False, 13 | Michel Fletcher,Agrolait,YourCompany,69 rue de Namur,,Wavre,,Belgium,1300,False,True,False, 14 | Thomas Passot,Agrolait,YourCompany,69 rue de Namur,,Wavre,,Belgium,1300,False,True,False, 15 | Camptocamp,,YourCompany,"93, Press Avenue",,Le Bourget du Lac,,France,73377,True,True,True,http://www.camptocamp.com 16 | Ayaan Agarwal,Camptocamp,YourCompany,"93, Press Avenue",,Le Bourget du Lac,,France,73377,False,True,False,http://facebook.com/odoo 17 | Benjamin Flores,Camptocamp,YourCompany,"93, Press Avenue",,Le Bourget du Lac,,France,73377,False,True,False, 18 | Phillipp Miller,Camptocamp,YourCompany,"93, Press Avenue",,Le Bourget du Lac,,France,73377,False,True,False, 19 | China Export,,YourCompany,52 Chop Suey street,,Shanghai,,China,200000,True,True,True,http://www.chinaexport.com/ 20 | Chao Wang,China Export,YourCompany,52 Chop Suey street,,Shanghai,,China,200000,False,True,False, 21 | David Simpson,China Export,YourCompany,52 Chop Suey street,,Shanghai,,China,200000,False,True,False, 22 | Jacob Taylor,China Export,YourCompany,52 Chop Suey street,,Shanghai,,China,200000,False,True,False, 23 | John M. Brown,China Export,YourCompany,52 Chop Suey street,,Shanghai,,China,200000,False,True,False, 24 | Coin gourmand,,YourCompany,"Rr. e Durrësit, Pall. M.C. Inerte","Kati.1, Laprakë, Tirana, Shqipëri",Tirana,,Albania,,False,True,False, 25 | Concrete Supply,,YourCompany,,,,,,,False,False,True, 26 | Delta PC,,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,True,False,True,http://www.distribpc.com/ 27 | Charlie Bernard,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 28 | Jessica Dupont,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 29 | Kevin Clarke,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 30 | Morgan Rose,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 31 | Richard Ellis,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,False,True, 32 | Robert Anderson,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 33 | Robin Smith,Delta PC,YourCompany,3661 Station Street,,Fremont,Cagliari,United States,94538,False,True,False, 34 | "My Company, Chicago",,YourCompany,90 Streets Avenue,,Chicago,Illinois,United States,60610,True,True,True,http://www.yourcompany.com 35 | Steven Hamilton,"My Company, Chicago",YourCompany,90 Streets Avenue,,Chicago,Illinois,United States,60610,False,True,False, 36 | Pizza Inn,,YourCompany,"#8, 1 st Floor,iscore complex","Gandhi Gramam,Gandhi Nagar",New Delhi TN,,India,607308,False,True,False, 37 | Robin Keunen,,YourCompany,,,,,,,False,True,False, 38 | The Jackson Group,,YourCompany,3203 Lamberts Branch Road,,Miami,Florida,United States,33169,True,True,False, 39 | Daniel Jackson,The Jackson Group,YourCompany,3203 Lamberts Branch Road,,Miami,Florida,United States,33169,False,True,False, 40 | William Thomas,The Jackson Group,YourCompany,3203 Lamberts Branch Road,,Miami,Florida,United States,33169,False,True,False, 41 | Think Big Systems,,YourCompany,89 Lingfield Tower,,London,,United Kingdom,,True,True,False,http://www.think-big.com 42 | Lucas Jones,Think Big Systems,YourCompany,89 Lingfield Tower,,London,,United Kingdom,,False,True,False, 43 | YourCompany,,YourCompany,1725 Slough Ave.,,Scranton,Pennsylvania,United States,18540,True,False,False,http://www.example.com 44 | Administrator,,YourCompany,215 Vine St,,Scranton,Pennsylvania,United States,18503,False,False,False, 45 | Demo Portal User,,YourCompany,"Rue Cesar de Paepe, 43",,Vivegnis,,Belgium,4683,False,True,False, 46 | Demo User,,YourCompany,"Avenue des Dessus-de-Lives, 2",,Namur (Loyers),,Belgium,5101,False,False,False, 47 | Mark Davis,YourCompany,YourCompany,1725 Slough Ave.,,Scranton,Pennsylvania,United States,18540,False,False,False, 48 | Roger Scott,YourCompany,YourCompany,1725 Slough Ave.,,Scranton,Pennsylvania,United States,18540,False,False,False, 49 | --------------------------------------------------------------------------------