odoorpc-0.7.0/0000755000232200023220000000000013376743447013553 5ustar debalancedebalanceodoorpc-0.7.0/doc/0000755000232200023220000000000013376743447014320 5ustar debalancedebalanceodoorpc-0.7.0/doc/make.bat0000644000232200023220000001507013376743447015730 0ustar debalancedebalance@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OdooRPC.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OdooRPC.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end odoorpc-0.7.0/doc/source/0000755000232200023220000000000013376743447015620 5ustar debalancedebalanceodoorpc-0.7.0/doc/source/tuto_browse.rst0000644000232200023220000000337213376743447020733 0ustar debalancedebalance.. _tuto-browse-records: Browse records ************** A great functionality of `OdooRPC` is its ability to generate objects that are similar to records used on the server side. Get records =========== To get one or more records (a recordset), you will use the :func:`browse ` method from a model proxy:: >>> Partner = odoo.env['res.partner'] >>> partner = Partner.browse(1) # fetch one record, partner ID = 1 >>> partner Recordset('res.partner', [1]) >>> partner.name 'YourCompany' >>> for partner in Partner.browse([1, 3]): # fetch several records >>> print(partner.name) ... YourCompany Mitchell Admin From such objects, it is possible to easily explore relationships. The related records are generated on the fly:: >>> partner = Partner.browse(1) >>> for child in partner.child_ids: ... print("%s (%s)" % (child.name, child.parent_id.name)) ... Chester Reed (YourCompany) Dwayne Newman (YourCompany) Outside relation fields, `Python` data types are used, like ``datetime.date`` and ``datetime.datetime``:: >>> Purchase = odoo.env['purchase.order'] >>> order = Purchase.browse(1) >>> order.date_order datetime.datetime(2018, 10, 18, 8, 18, 56) A list of data types used by records fields are available :ref:`here `. Get records corresponding to an External ID =========================================== To get a record through its external ID, use the :func:`ref ` method from the environment:: >>> lang_en = odoo.env.ref('base.lang_en') >>> lang_en Recordset('res.lang', [1]) >>> lang_en.code 'en_US' :ref:`Next step: Call methods from a Model or from records ` odoorpc-0.7.0/doc/source/download_install.rst0000644000232200023220000000273013376743447021711 0ustar debalancedebalance.. _download-install: Download and install instructions ================================= Python Package Index (PyPI) --------------------------- You can install `OdooRPC` with `pip`:: $ pip install odoorpc No dependency is required. Source code ----------- The project is hosted on `GitHub `_. To get the last stable release (``master`` branch), just type:: $ git clone https://github.com/OCA/odoorpc.git Also, the project uses the `Git Flow extension `_ to manage its branches and releases. If you want to contribute, make sure to make your Pull Request against the `develop` branch. Run tests --------- Unit tests depend on the standard module `unittest` (Python 2.7 and 3.x) and on a running Odoo instance. To run all unit tests from the project directory, run the following command:: $ python -m unittest discover -v To run a specific test:: $ python -m unittest -v odoorpc.tests.test_init To configure the connection to the server, some environment variables are available:: $ export ORPC_TEST_PROTOCOL=jsonrpc $ export ORPC_TEST_HOST=localhost $ export ORPC_TEST_PORT=8069 $ export ORPC_TEST_DB=odoorpc_test $ export ORPC_TEST_USER=admin $ export ORPC_TEST_PWD=admin $ export ORPC_TEST_VERSION=10.0 $ export ORPC_TEST_SUPER_PWD=admin $ python -m unittest discover -v The database ``odoorpc_test`` will be created if it does not exist. odoorpc-0.7.0/doc/source/ref_rpc.rst0000644000232200023220000000010313376743447017764 0ustar debalancedebalanceodoorpc.rpc =========== .. automodule:: odoorpc.rpc :members: odoorpc-0.7.0/doc/source/faq.rst0000644000232200023220000001336013376743447017124 0ustar debalancedebalance.. _faq: Frequently Asked Questions (FAQ) ================================ Why OdooRPC? And why migrate from OERPLib to OdooRPC? ----------------------------------------------------- It was a tough decision, but several reasons motivated the `OdooRPC` project: **RPC Protocol** The first point is about the supported protocol, `XML-RPC` is kept in `Odoo` for compatibility reasons (and will not evolve anymore, maybe removed one day), replaced by the `JSON-RPC` one. Although these protocols are almost similar in the way we build RPC requests, some points make `JSON-RPC` a better and reliable choice like the way to handle errors raised by the `Odoo` server (access to the type of exception raised, the complete server traceback...). To keep a clean and maintainable base code, it would have been difficult to support both protocols in `OERPLib`, and it is why `OdooRPC` only support `JSON-RPC`. Another good point with `JSON-RPC` is the ability to request all server web controllers to reproduce requests (`type='json'` ones) made by the official `Javascript` web client. As the code to make such requests is based on standard `HTTP` related Python modules, `OdooRPC` is also able to request `HTTP` web controllers (`type='http'` ones). In fact, you could see `OdooRPC` as a high level API for `Odoo` with which you could replicate the behaviour of the official `Javascript` web client, but in `Python`. **New server API** One goal of `OERPLib` was to give an API not too different from the server side API to reduce the learning gap between server-side development and client-side with an `RPC` library. With the new API which appears in `Odoo` 8.0 this is another brake (the old API has even been removed since Odoo 10.0), so the current API of `OERPLib` is not anymore consistent. As such, `OdooRPC` mimics A LOT the new API of Odoo, for more consistency (see the :ref:`tutorials `). **New brand Odoo** `OpenERP` became `Odoo`, so what does `OERPLib` mean? `OEWhat`? This is obvious for old developers which start the `OpenERP` adventure since the early days, but the `OpenERP` brand is led to disappear, and it can be confusing for newcomers in the `Odoo` world. So, `OdooRPC` speaks for itself. **Maintenance cost, code cleanup** `OpenERP` has evolved a lot since the version 5.0 (2009), making `OERPLib` hard to maintain (write tests for all versions before each `OERPLib` and `OpenERP` release is very time consuming). All the compatibility code for `OpenERP` 5.0 to 7.0 was dropped for `OdooRPC`, making the project more maintainable. `Odoo` is now a more mature product, and `OdooRPC` should suffer less about compatibility issues from one release to another. As `OdooRPC` has not the same constraints concerning `Python` environments where it could be running on, it is able to work on `Python` 2.7 to 3.X. `OdooRPC` is turned towards the future, so you are encouraged to use or migrate on it for projects based on `Odoo` >= 8.0. It is more reliable, better covered by unit tests, and almost identical to the server side new API. Connect to an Odoo Online (SaaS) instance ----------------------------------------- First, you have to connect on your `Odoo` instance, and set a password for your user account in order to active the `RPC` interface. Then, just use the ``jsonrpc+ssl`` protocol with the port 443:: >>> import odoorpc >>> odoo = odoorpc.ODOO('foobar.my.odoo.com', protocol='jsonrpc+ssl', port=443) >>> odoo.version '8.saas~5' Update a record with an `on_change` method ------------------------------------------ .. note: It is about the the old API (`on_change` statement declared in a XML view with its associated Python method). `OdooRPC` does not provide helpers for such methods currently. A call to an ``on_change`` method intend to be executed from a view and there is no support for that (not yet?) such as fill a form, validate it, etc... But you can emulate an ``on_change`` by writing your own function, for instance:: def on_change(record, method, args=None, kwargs=None): """Update `record` with the result of the on_change `method`""" res = record._odoo.execute_kw(record._name, method, args, kwargs) for k, v in res['value'].iteritems(): setattr(record, k, v) And call it on a record with the desired method and its parameters:: >>> order = odoo.get('sale.order').browse(42) >>> on_change(order, 'product_id_change', args=[ARGS], kwargs={KWARGS}) Some model methods does not accept the `context` parameter ---------------------------------------------------------- The ``context`` parameter is sent automatically for each call to a `Model` method. But on the side of the `Odoo` server, some methods have no ``context`` parameter, and `OdooRPC` has no way to guess it, which results in an nasty exception. So you have to disable temporarily this behaviour by yourself by setting the ``auto_context`` option to ``False``:: >>> odoo.config['auto_context'] = False # 'get()' method of 'ir.sequence' does not support the context parameter >>> next_seq = odoo.get('ir.sequence').get('stock.lot.serial') >>> odoo.config['auto_context'] = True # Restore the configuration Change the behaviour of a script according to the version of Odoo ----------------------------------------------------------------- You can compare versions of `Odoo` servers with the :func:`v ` function applied on the :attr:`ODOO.version ` property, for instance:: import odoorpc from odoorpc.tools import v for session in odoorpc.ODOO.list(): odoo = odoorpc.ODOO.load(session) if v(odoo.version) < v('10.0'): pass # do some stuff else: pass # do something else odoorpc-0.7.0/doc/source/tuto_logging.rst0000644000232200023220000000337713376743447021065 0ustar debalancedebalance.. _tuto-logging: Configure logging with OdooRPC ------------------------------ `OdooRPC` generates debug logs for all HTTP queries performed against the `Odoo` server. It is up to you to configure a logger (with `logging`) on the ``odoorpc`` package, the sole requirement is to set its level to ``DEBUG``:: >>> import logging >>> logging.basicConfig() >>> logger = logging.getLogger('odoorpc') >>> logger.setLevel(logging.DEBUG) Then all queries generated by OdooRPC will be logged:: >>> import odoorpc >>> odoo = odoorpc.ODOO() >>> odoo.login('dbname', 'admin', 'admin') DEBUG:odoorpc.rpc.jsonrpclib:(JSON,send) http://localhost:8069/web/session/authenticate {'jsonrpc': '2.0', 'id': 499807971, 'method': 'call', 'params': {'db': 'dbname', 'login': 'admin', 'password': '**********'}} DEBUG:odoorpc.rpc.jsonrpclib:(JSON,recv) http://localhost:8069/web/session/authenticate {'jsonrpc': '2.0', 'id': 499807971, 'method': 'call', 'params': {'db': 'dbname', 'login': 'admin', 'password': '**********'}} => {'result': {'is_admin': True, 'server_version': '12.0-20181008', 'currencies': {'2': {'digits': [69, 2], 'position': 'before', 'symbol': '$'}, '1': {'digits': [69, 2], 'position': 'after', 'symbol': '€'}}, 'partner_display_name': 'YourCompany, Mitchell Admin', 'company_id': 1, 'username': 'admin', 'web_tours': [], 'user_companies': False, 'session_id': '61cb37d21771531f789bea631a03236aa21f06d4', 'is_system': True, 'server_version_info': [12, 0, 0, 'final', 0, ''], 'db': 'odoorpc_v12', 'name': 'Mitchell Admin', 'web.base.url': 'http://localhost:8069', 'user_context': {'lang': 'fr_FR', 'tz': 'Europe/Brussels', 'uid': 2}, 'odoobot_initialized': True, 'show_effect': 'True', 'partner_id': 3, 'uid': 2}, 'id': 499807971, 'jsonrpc': '2.0'} odoorpc-0.7.0/doc/source/tuto_browse_update.rst0000644000232200023220000001476513376743447022305 0ustar debalancedebalance.. _tuto-update-browse-records: Update data through records *************************** By default when updating values of a record, the change is automatically sent to the server. Let's update the name of a partner:: >>> Partner = odoo.env['res.partner'] >>> partner_id = Partner.create({'name': "Contact Test"}) >>> partner = Partner.browse(partner_id) >>> partner.name = "MyContact" This is equivalent to:: >>> Partner.write([partner.id], {'name': "MyContact"}) As one update is equivalent to one RPC query, if you need to update several fields for one record it is encouraged to use the `write` method as above :: >>> partner.write({'name': "MyContact", 'website': 'http://example.net'}) # one RPC query Or, deactivate the ``auto_commit`` option and commit the changes manually:: >>> odoo.config['auto_commit'] = False >>> partner.name = "MyContact" >>> partner.website = 'http://example.net' >>> partner.env.commit() # one RPC by record modified Char, Float, Integer, Boolean, Text and Binary '''''''''''''''''''''''''''''''''''''''''''''' As see above, it's as simple as that:: >>> partner.name = "New Name" Selection ''''''''' Same as above, except there is a check about the value assigned. For instance, the field ``type`` of the ``res.partner`` model accept values contains in ``['default', 'invoice', 'delivery', 'contact', 'other']``:: >>> partner.type = 'delivery' # Ok >>> partner.type = 'foobar' # Error! Traceback (most recent call last): File "", line 1, in File "odoorpc/service/model/fields.py", line 148, in __set__ value = self.check_value(value) File "odoorpc/service/model/fields.py", line 160, in check_value field_name=self.name, ValueError: The value 'foobar' supplied doesn't match with the possible values '['contact', 'invoice', 'delivery', 'other']' for the 'type' field Many2one '''''''' You can also update a ``many2one`` field, with either an ID or a record:: >>> partner.parent_id = 1 # with an ID >>> partner.parent_id = Partner.browse(1) # with a record object You can't put any ID or record, a check is made on the relationship to ensure data integrity:: >>> User = odoo.env['res.users'] >>> user = User.browse(1) >>> partner = Partner.browse(2) >>> partner.parent_id = user Traceback (most recent call last): File "", line 1, in File "odoorpc/service/model/fields.py", line 263, in __set__ o_rel = self.check_value(o_rel) File "odoorpc/service/model/fields.py", line 275, in check_value field_name=self.name)) ValueError: Instance of 'res.users' supplied doesn't match with the relation 'res.partner' of the 'parent_id' field. One2many and Many2many '''''''''''''''''''''' ``one2many`` and ``many2many`` fields can be updated by providing a list of tuple as specified in the `Odoo` documentation (`link `_), a list of records, a list of record IDs, an empty list or ``False``: With a tuple (as documented), no magic here:: >>> user = odoo.env['res.users'].browse(1) >>> user.groups_id = [(6, 0, [8, 5, 6, 4])] With a recordset:: >>> groups = odoo.env['res.groups'].browse([8, 5, 6, 4]) >>> user.groups_id = groups With a list of record IDs:: >>> user.groups_id = [8, 5, 6, 4] The last two examples are equivalent to the first (they generate a ``(6, 0, IDS)`` tuple). However, if you set an empty list or ``False``, the relation between records will be removed:: >>> user.groups_id = [] >>> user.groups_id Recordset('res.group', []) >>> user.groups_id = False >>> user.groups_id Recordset('res.group', []) Another facility provided by `OdooRPC` is adding and removing objects using `Python` operators ``+=`` and ``-=``. As usual, you can add an ID, a record, or a list of them: With a list of records:: >>> groups = odoo.env['res.groups'].browse([4, 5]) Recordset('res.group', [1, 2, 3]) >>> user.groups_id += groups >>> user.groups_id Recordset('res.group', [1, 2, 3, 4, 5]) With a list of record IDs:: >>> user.groups_id += [4, 5] >>> user.groups_id Recordset('res.group', [1, 2, 3, 4, 5]) With an ID only:: >>> user.groups_id -= 4 >>> user.groups_id Recordset('res.group', [1, 2, 3, 5]) With a record only:: >>> group = odoo.env['res.groups'].browse(5) >>> user.groups_id -= group >>> user.groups_id Recordset('res.group', [1, 2, 3]) Reference ''''''''' To update a ``reference`` field, you have to use either a string or a record object as below:: >>> IrActionServer = odoo.env['ir.actions.server'] >>> action_server = IrActionServer.browse(8) >>> action_server.ref_object = 'res.partner,1' # with a string with the format '{relation},{id}' >>> action_server.ref_object = Partner.browse(1) # with a record object A check is made on the relation name:: >>> action_server.ref_object = 'foo.bar,42' Traceback (most recent call last): File "", line 1, in File "odoorpc/service/model/fields.py", line 370, in __set__ value = self.check_value(value) File "odoorpc/service/model/fields.py", line 400, in check_value self._check_relation(relation) File "odoorpc/service/model/fields.py", line 381, in _check_relation field_name=self.name, ValueError: The value 'foo.bar' supplied doesn't match with the possible values '[...]' for the 'ref_object' field Date and Datetime ''''''''''''''''' ``date`` and ``datetime`` fields accept either string values or ``datetime.date/datetime.datetime`` objects. With ``datetime.date`` and ``datetime.datetime`` objects:: >>> import datetime >>> Purchase = odoo.env['purchase.order'] >>> order = Purchase.browse(1) >>> order.date_order = datetime.datetime(2018, 10, 18, 8, 18, 56) With formated strings:: >>> order.date_order = "2018-11-07" # %Y-%m-%d >>> order.date_order = "2018-11-07 12:31:24" # %Y-%m-%d %H:%M:%S As always, a wrong type will raise an exception:: >>> order.date_order = "foobar" Traceback (most recent call last): File "", line 1, in File "odoorpc/fields.py", line 187, in setter value = self.check_value(value) File "odoorpc/fields.py", line 203, in check_value self.pattern)) ValueError: Value not well formatted, expecting '%Y-%m-%d %H:%M:%S' format :ref:`Next step: Change the user's context ` odoorpc-0.7.0/doc/source/_static/0000755000232200023220000000000013376743447017246 5ustar debalancedebalanceodoorpc-0.7.0/doc/source/_static/logo.png0000644000232200023220000000514713376743447020723 0ustar debalancedebalancePNG  IHDRddpTbKGD pHYs B(xtIME,' IDATxmtw71*ZhUOQB@nAm BemlD0`9$ [ ZXR{*J򲻷{gvg?Csow!P@ (P@ (P?lX:8Jn!B!)OMߥeqW@ܒF#8l FZ[(h4Uz+c<2.~d(~bۀVs볗O,|'ֵݫ bU<YstߩwV%FY^ - s! cl-1imb Hg&aXYU%fC0 kҧ0`mX747ygf4IkLlI1|KꘉmX[o1@$P* Hz @R#e%m^qTJNR]oO^?bDe{\)v`t3 bX}3eن덧zP%ʶam.r^ ͍4QCa-,?*"9&c>1/vfH7pClBҩERjMWnJCUwG@0x…4A&̜ѱ kƇcޒ)K&L[:0k*<(Z]0usNa.?t̒ R(2aVoj6$է/_Tq%LrQl#FHp ^SփmXϣwʑYe:6ˆ':jndS7;ڑ9];BOCFAct;W2K\ڶ|Rw9V̷K,*"JꮉJhwSN5} D ^7C9 3G,tQ|_c?ZӸ0{vviuN BWEvqi.wb T2f@B w_/i})Ke>]ܼaZ*x7^NƇzDtwNDs O@+p"Sumޮsu;6π=ρ{:W'g`KtFmCū 6~(plj r\>h@R օz#[@^YMbZ3c+2Z%n;e CDF2}^zgHMkD&L!vĚGJ82,yH x<xb ~];2Qb݁ocշEvZ$S}OZJ(ܮiju 3+.$$#vރWK5RQ 7`n«k2]~ \?BSvCoV(iftYzs>Qo'E؆ k 0~8c B17op#^8Ū53i)gF$^ L AsH۰=EmFs# k.B5cUݵ"dj _ý^ʄix)++a%pG:vIvn!&xAmXCW.u KlJ #n֩/w!Nw{͓3R%}U3͢j1c { ^f XԽU, 9ceA}j*ppK u!)wS` property. The :func:`list ` method allows you to list all reports available on your `Odoo` server (classified by models), while the :func:`download ` method will retrieve a report as a file (in PDF, HTML... depending of the report). To list available reports grouped by models:: >>> from pprint import pprint >>> pprint(odoo.report.list()) {'account.common.journal.report': [{'name': 'Journals Audit', 'report_name': 'account.report_journal', 'report_type': 'qweb-pdf'}], 'account.invoice': [{'name': 'Invoices', 'report_name': 'account.report_invoice_with_payments', 'report_type': 'qweb-pdf'}, {'name': 'Invoices without Payment', 'report_name': 'account.report_invoice', 'report_type': 'qweb-pdf'}], 'account.payment': [{'name': 'Payment Receipt', 'report_name': 'account.report_payment_receipt', 'report_type': 'qweb-pdf'}], 'ir.model': [{'name': 'Model Overview', 'report_name': 'base.report_irmodeloverview', 'report_type': 'qweb-pdf'}], 'ir.module.module': [{'name': 'Technical guide', 'report_name': 'base.report_irmodulereference', 'report_type': 'qweb-pdf'}], 'product.packaging': [{'name': 'Product Packaging (PDF)', 'report_name': 'product.report_packagingbarcode', 'report_type': 'qweb-pdf'}], 'product.product': [{'name': 'Pricelist', 'report_name': 'product.report_pricelist', 'report_type': 'qweb-pdf'}, {'name': 'Product Barcode (PDF)', 'report_name': 'product.report_productbarcode', 'report_type': 'qweb-pdf'}, {'name': 'Product Label (PDF)', 'report_name': 'product.report_productlabel', 'report_type': 'qweb-pdf'}], 'product.template': [{'name': 'Product Barcode (PDF)', 'report_name': 'product.report_producttemplatebarcode', 'report_type': 'qweb-pdf'}, {'name': 'Product Label (PDF)', 'report_name': 'product.report_producttemplatelabel', 'report_type': 'qweb-pdf'}], 'purchase.order': [{'name': 'Purchase Order', 'report_name': 'purchase.report_purchaseorder', 'report_type': 'qweb-pdf'}, {'name': 'Request for Quotation', 'report_name': 'purchase.report_purchasequotation', 'report_type': 'qweb-pdf'}], 'res.company': [{'name': 'Preview External Report', 'report_name': 'web.preview_externalreport', 'report_type': 'qweb-pdf'}, {'name': 'Preview Internal Report', 'report_name': 'web.preview_internalreport', 'report_type': 'qweb-pdf'}], 'res.partner': [{'name': 'Aged Partner Balance', 'report_name': 'account.report_agedpartnerbalance', 'report_type': 'qweb-pdf'}], 'sale.order': [{'name': 'PRO-FORMA Invoice', 'report_name': 'sale.report_saleorder_pro_forma', 'report_type': 'qweb-pdf'}, {'name': 'Quotation / Order', 'report_name': 'sale.report_saleorder', 'report_type': 'qweb-pdf'}]} To download a report:: >>> report = odoo.report.download('account.report_invoice', [1]) The method will return a file-like object, you will have to read its content in order to save it on your file-system:: >>> with open('invoice.pdf', 'w') as report_file: ... report_file.write(report.read()) ... :ref:`Next step: Save your credentials (session) ` odoorpc-0.7.0/doc/source/ref_models.rst0000644000232200023220000000026513376743447020474 0ustar debalancedebalanceodoorpc.models ============== .. automodule:: odoorpc.models .. autoclass:: odoorpc.models.Model :members: :special-members: :exclude-members: __hash__, __metaclass__ odoorpc-0.7.0/doc/source/tuto_session.rst0000644000232200023220000000227713376743447021120 0ustar debalancedebalance.. _tuto-manage-sessions: Save your credentials (session) ------------------------------- Once you are authenticated with your :class:`ODOO ` instance, you can :func:`save ` your credentials under a code name and use this one to quickly instantiate a new :class:`ODOO ` class:: >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost') >>> user = odoo.login('tutorial', 'admin', 'admin') >>> odoo.save('tutorial') By default, these informations are stored in the ``~/.odoorpcrc`` file. You can however use another file:: >>> odoo.save('tutorial', '~/my_own_odoorpcrc') Then, use the :func:`odoorpc.ODOO.load` class method:: >>> import odoorpc >>> odoo = odoorpc.ODOO.load('tutorial') Or, if you have saved your configuration in another file:: >>> odoo = odoorpc.ODOO.load('tutorial', '~/my_own_odoorpcrc') You can check available sessions with :func:`odoorpc.ODOO.list`, and remove them with :func:`odoorpc.ODOO.remove`:: >>> odoorpc.ODOO.list() ['tutorial'] >>> odoorpc.ODOO.remove('tutorial') >>> 'tutorial' not in odoorpc.ODOO.list() True :ref:`Next step: Configure logging with OdooRPC ` odoorpc-0.7.0/doc/source/ref_error.rst0000644000232200023220000000011213376743447020331 0ustar debalancedebalanceodoorpc.error ============= .. automodule:: odoorpc.error :members: odoorpc-0.7.0/doc/source/conf.py0000644000232200023220000002241413376743447017122 0ustar debalancedebalance# -*- coding: utf-8 -*- # # OdooRPC documentation build configuration file, created by # sphinx-quickstart on Sun Jul 6 16:32:50 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os from datetime import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('./../..')) # To find 'sphinx_ext' package # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx_ext.doctest_custom', ] doctest_global_setup = """ import os PROTOCOL = os.environ.get('ORPC_TEST_PROTOCOL', 'jsonrpc') HOST = os.environ.get('ORPC_TEST_HOST', 'localhost') PORT = os.environ.get('ORPC_TEST_PORT', 8069) DB = os.environ.get('ORPC_TEST_DB', 'odoorpc_doctest') USER = os.environ.get('ORPC_TEST_USER', 'admin') PWD = os.environ.get('ORPC_TEST_PWD', 'admin') VERSION = os.environ.get('ORPC_TEST_VERSION', '10.0') SUPER_PWD = os.environ.get('ORPC_TEST_SUPER_PWD', 'admin') import odoorpc odoo = odoorpc.ODOO(HOST, protocol=PROTOCOL, port=PORT, version=VERSION) # == create a database if DB not in odoo.db.list(): odoo.db.create(SUPER_PWD, DB, True) odoo.login(DB, USER, PWD) # == install fr_FR language Wizard = odoo.env['base.language.install'] wiz_id = Wizard.create({'lang': 'fr_FR'}) Wizard.lang_install([wiz_id]) # == install some modules odoo.config['timeout'] = 600 Module = odoo.env['ir.module.module'] module_ids = Module.search([('name', 'in', ['sale', 'crm']), ('state', '=', 'uninstalled')]) if module_ids: Module.button_immediate_install(module_ids) odoo.config['timeout'] = 120 """ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'OdooRPC' copyright = u'2014-{0}, Sébastien Alix'.format(datetime.now().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.7.0' # The full version, including alpha/beta/rc tags. release = '0.7.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'nature' html_style = 'odoorpc.css' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/logo.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'OdooRPCdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'OdooRPC.tex', u'OdooRPC Documentation', u'Sébastien Alix', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'odoorpc', u'OdooRPC Documentation', [u'Sébastien Alix'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'OdooRPC', u'OdooRPC Documentation', u'Sébastien Alix', 'OdooRPC', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False odoorpc-0.7.0/doc/source/tuto_rpc_queries.rst0000644000232200023220000000363013376743447021750 0ustar debalancedebalance .. _tuto-execute-queries: Execute RPC queries ******************* The basic methods to execute RPC queries related to data models are :func:`execute ` and :func:`execute_kw `. They take at least two parameters (the model and the name of the method to call) following by additional variable parameters according to the method called:: >>> order_data = odoo.execute('sale.order', 'read', [1], ['name']) This instruction will call the ``read`` method of the ``sale.order`` model for the order ID=1, and will only returns the value of the field ``name``. However there is a more efficient way to perform methods of a model by getting a proxy of it with the :func:`model registry `, which provides an API almost syntactically identical to the `Odoo` server side API (see :class:`odoorpc.models.Model`), and which is able to set the user context automatically (both for read and write operations):: >>> User = odoo.env['res.users'] >>> User.write([6], {'name': "Dupont D."}) True >>> odoo.env.context {'lang': 'fr_FR', 'tz': False} >>> Product = odoo.env['product.template'] >>> Product.name_get([25]) [[25, '[FURN_8220] Bureau Quatre Personnes']] To stop sending the user context, use the :attr:`odoorpc.ODOO.config` property:: >>> odoo.config['auto_context'] = False >>> Product.name_get([25]) # Without context, lang 'en_US' by default [[25, '[FURN_8220] Four Person Desk']] .. note:: The ``auto_context`` option only affect methods called from model proxies. Here is another example of how to install a module (you have to be logged as an administrator to perform this task):: >>> Module = odoo.env['ir.module.module'] >>> module_ids = Module.search([('name', '=', 'purchase')]) >>> Module.button_immediate_install(module_ids) :ref:`Next step: Browse records ` odoorpc-0.7.0/doc/source/ref_fields.rst0000644000232200023220000000174513376743447020463 0ustar debalancedebalance.. _fields: Browse object fields ==================== The table below presents the Python types returned by `OdooRPC` for each `Odoo` fields used by :class:`Recordset ` objects (see the :func:`browse ` method): ================ ============================== `Odoo` fields Python types used in `OdooRPC` ================ ============================== fields.Binary unicode or str fields.Boolean bool fields.Char unicode or str fields.Date `datetime `_.date fields.Datetime `datetime `_.datetime fields.Float float fields.Integer integer fields.Selection unicode or str fields.Text unicode or str fields.Html unicode or str fields.Many2one ``Recordset`` fields.One2many ``Recordset`` fields.Many2many ``Recordset`` fields.Reference ``Recordset`` ================ ============================== odoorpc-0.7.0/doc/source/tuto_login.rst0000644000232200023220000000177313376743447020545 0ustar debalancedebalance.. _tuto-login: Login to your new database ************************** Use the :func:`login ` method on a database with the account of your choice:: >>> odoo.login('tutorial', 'admin', 'password') .. note:: Under the hood the login method creates a cookie, and all requests thereafter which need a user authentication are cookie-based. Once logged in, you can check some information through the :class:`environment `:: >>> odoo.env.db 'tutorial' >>> odoo.env.context {'lang': 'fr_FR', 'tz': 'Europe/Brussels', 'uid': 1} >>> odoo.env.uid 1 >>> odoo.env.lang 'fr_FR' >>> odoo.env.user.name # name of the user 'Mitchell Admin' >>> odoo.env.user.company_id.name # the name of its company 'YourCompany' From now, you can easily execute any kind of queries on your `Odoo` server (execute model methods, trigger workflow, download reports...). :ref:`Next step: Execute RPC queries ` odoorpc-0.7.0/doc/source/ref_db.rst0000644000232200023220000000010013376743447017562 0ustar debalancedebalanceodoorpc.db ========== .. automodule:: odoorpc.db :members: odoorpc-0.7.0/doc/source/ref_odoo.rst0000644000232200023220000000010613376743447020143 0ustar debalancedebalanceodoorpc.ODOO ============ .. autoclass:: odoorpc.ODOO :members: odoorpc-0.7.0/doc/source/tuto_context.rst0000644000232200023220000000301413376743447021107 0ustar debalancedebalance.. _tuto-context: Change the user's context ************************* `Odoo` uses the user's context to adapt the results of some queries like: - reading/updating the translatable fields (english, french...), - reading/updating the date fields following the user's timezone - change the behavior of a method/query according to the context keys Global context '''''''''''''' Changing this context can be done globally by updating the :func:`context `: .. doctest:: >>> odoo.env.context['lang'] = 'en_US' >>> odoo.env.context['tz'] = 'Europe/Paris' From now all queries will be performed with the above updated context. Model/recordset context ''''''''''''''''''''''' The context can also be updated punctually with the :func:`with_context ` method on a model or a recordset (without impacting the global context). For instance to update translations of a recordset: .. doctest:: >>> Product = odoo.env['product.product'] >>> product_en = Product.browse(1) >>> product_en.env.lang 'en_US' >>> product_en.name = "My product" # Update the english translation >>> product_fr = product_en.with_context(lang='fr_FR') >>> product_fr.env.lang 'fr_FR' >>> product_fr.name = "Mon produit" # Update the french translation Or to retrieve all records (visible records and archived ones): .. doctest:: >>> all_product_ids = Product.with_context(active_test=False).search([]) :ref:`Next step: Download reports ` odoorpc-0.7.0/doc/source/ref_report.rst0000644000232200023220000000011413376743447020515 0ustar debalancedebalanceodoorpc.report ============== .. automodule:: odoorpc.report :members: odoorpc-0.7.0/doc/source/reference.rst0000644000232200023220000000033613376743447020312 0ustar debalancedebalance.. _reference: Reference ========= .. toctree:: :maxdepth: 2 ref_fields ref_odoorpc ref_odoo ref_db ref_report ref_models ref_env ref_rpc ref_session ref_tools ref_error odoorpc-0.7.0/doc/source/index.rst0000644000232200023220000000632313376743447017465 0ustar debalancedebalance.. OdooRPC documentation master file, created by sphinx-quickstart on Sun Jul 6 16:32:50 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to OdooRPC's documentation! =================================== Introduction ------------ **OdooRPC** is a Python package providing an easy way to pilot your `Odoo `_ servers through `RPC`. Features supported: - access to all data model methods (even ``browse``) with an API similar to the server-side API, - use named parameters with model methods, - user context automatically sent providing support for internationalization, - browse records, - execute workflows, - manage databases, - reports downloading, - JSON-RPC protocol (SSL supported), Quick start ----------- How does it work? See below:: import odoorpc # Prepare the connection to the server odoo = odoorpc.ODOO('localhost', port=8069) # Check available databases print(odoo.db.list()) # Login odoo.login('db_name', 'user', 'passwd') # Current user user = odoo.env.user print(user.name) # name of the user connected print(user.company_id.name) # the name of its company # Simple 'raw' query user_data = odoo.execute('res.users', 'read', [user.id]) print(user_data) # Use all methods of a model if 'sale.order' in odoo.env: Order = odoo.env['sale.order'] order_ids = Order.search([]) for order in Order.browse(order_ids): print(order.name) products = [line.product_id.name for line in order.order_line] print(products) # Update data through a record user.name = "Brian Jones" For more details and features, see the :ref:`tutorials `, the :ref:`Frequently Asked Questions (FAQ) ` and the :ref:`API reference ` sections. Contents -------- .. toctree:: :maxdepth: 3 download_install tutorials faq reference Supported Odoo server versions ------------------------------ `OdooRPC` is tested on all major releases of `Odoo` (starting from 8.0). Supported Python versions ------------------------- `OdooRPC` support Python 2.7, 3.4, 3.5 and 3.6. License ------- This software is made available under the `LGPL v3` license. Bug Tracker ----------- Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smash it by providing detailed and welcomed feedback. Credits ------- Contributors ____________ * Sébastien Alix Do not contact contributors directly about support or help with technical issues. Maintainer __________ .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org This package is maintained by the OCA. OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` odoorpc-0.7.0/doc/source/ref_env.rst0000644000232200023220000000024313376743447017775 0ustar debalancedebalanceodoorpc.env =========== .. automodule:: odoorpc.env :members: :special-members: __getitem__, __contains__ :exclude-members: commit, dirty, invalidate odoorpc-0.7.0/doc/source/tuto_browse_methods.rst0000644000232200023220000000262313376743447022454 0ustar debalancedebalance.. _tuto-browse-methods: Call methods from a Model or from records ***************************************** Unlike the `Odoo` API, there is a difference between class methods (e.g.: `create`, `search`, ...) and instance methods that apply directly on existing records (`write`, `read`, ...):: >>> User = odoo.env['res.users'] >>> User.write([6], {'name': "Dupont D."}) # Using the class method True >>> user = User.browse(6) >>> user.write({'name': "Dupont D."}) # Using the instance method >>> user.mapped('company_id.partner_id.child_ids.name') # Another use of instance method ['Chester Reed', 'Dwayne Newman'] When a method is called directly on records, their `ids` (here `user.ids`) is simply passed as the first parameter. This also means that you are not able to call class methods such as `create` or `search` from a set of records:: >>> User = odoo.env['res.users'] >>> User.create({...}) # Works >>> user = User.browse(1) >>> user.ids [1] >>> user.create({...}) # Error, `create()` does not accept `ids` in first parameter >>> user.__class__.create({...}) # Works This is a behaviour `by design`: `OdooRPC` has no way to make the difference between a `class` or an `instance` method through RPC, this is why it differs from the `Odoo` API. :ref:`Next step: Update data through records ` odoorpc-0.7.0/doc/source/tutorials.rst0000644000232200023220000000060413376743447020400 0ustar debalancedebalance.. _tutorials: Tutorials ========= .. note:: The tutorial is based on `Odoo 12.0`, the examples must be adapted following the version of `Odoo` you are using. .. toctree:: :maxdepth: 2 tuto_create_db tuto_login tuto_rpc_queries tuto_browse tuto_browse_methods tuto_browse_update tuto_context tuto_report tuto_session tuto_logging odoorpc-0.7.0/doc/source/tuto_create_db.rst0000644000232200023220000000227413376743447021342 0ustar debalancedebalanceCreate a new database ********************* To dialog with your `Odoo` server, you need an instance of the :class:`odoorpc.ODOO` class. Let's instanciate it:: >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', 'jsonrpc', 8069) Two protocols are available: ``jsonrpc`` and ``jsonrpc+ssl``. Then, create your database for the purposes of this tutorial (you need to know the `super` admin password to do this):: >>> odoo.db.create('super_password', 'tutorial', demo=True, lang='fr_FR', admin_password='password') The creation process may take some time on the server. If you get a timeout error, set a higher timeout before repeating the process:: >>> odoo.config['timeout'] = 300 # Set the timeout to 300 seconds >>> odoo.db.create('super_password', 'tutorial', demo=True, lang='fr_FR', admin_password='password') To check available databases, use the :attr:`odoo.db ` property with the :func:`list ` method:: >>> odoo.db.list() ['tutorial'] You are now ready to login to your database! Documentation about databases management is available :class:`here `. :ref:`Next step: Login to your new database ` odoorpc-0.7.0/doc/source/ref_tools.rst0000644000232200023220000000011113376743447020337 0ustar debalancedebalanceodoorpc.tools ============= .. automodule:: odoorpc.tools :members: odoorpc-0.7.0/doc/Makefile0000644000232200023220000001516713376743447015772 0ustar debalancedebalance# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OdooRPC.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OdooRPC.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/OdooRPC" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OdooRPC" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." odoorpc-0.7.0/setup.cfg0000644000232200023220000000003413376743447015371 0ustar debalancedebalance[bdist_wheel] universal = 1 odoorpc-0.7.0/LICENSE0000644000232200023220000001674313376743447014573 0ustar debalancedebalance GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. odoorpc-0.7.0/MANIFEST.in0000644000232200023220000000024513376743447015312 0ustar debalancedebalanceinclude AUTHORS include CHANGELOG include LICENSE include README.rst include doc/Makefile doc/make.bat recursive-include doc/source * recursive-include sphinx_ext * odoorpc-0.7.0/sphinx_ext/0000755000232200023220000000000013376743447015744 5ustar debalancedebalanceodoorpc-0.7.0/sphinx_ext/__init__.py0000644000232200023220000000000013376743447020043 0ustar debalancedebalanceodoorpc-0.7.0/sphinx_ext/doctest_custom.py0000644000232200023220000000142413376743447021356 0ustar debalancedebalance# -*- coding: utf-8 -*- import sys import re from doctest import OutputChecker from sphinx.ext.doctest import * class Py23OutputChecker(OutputChecker): """OutputChecker to ignore unicode literals when checking outputs.""" def check_output(self, want, got, optionflags): if got: got = re.sub("u'(.*?)'", "'\\1'", got) got = re.sub('u"(.*?)"', '"\\1"', got) return OutputChecker.check_output(self, want, got, optionflags) original_init = SphinxDocTestRunner.__init__ def custom_init(self, checker=None, verbose=None, optionflags=0): checker = Py23OutputChecker() original_init(self, checker, verbose, optionflags) SphinxDocTestRunner.__init__ = custom_init # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/setup.py0000644000232200023220000000511513376743447015267 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: UTF-8 -*- import os import setuptools name = 'OdooRPC' version = '0.7.0' description = ("OdooRPC is a Python package providing an easy way to " "pilot your Odoo servers through RPC.") with open("README.rst", "r") as readme: long_description = readme.read() keywords = ("openerp odoo server rpc client xml-rpc xmlrpc jsonrpc json-rpc " "odoorpc oerplib communication lib library python " "service web webservice") author = "Sebastien Alix" author_email = 'seb@usr-src.org' url = 'https://github.com/OCA/odoorpc' license = 'LGPL v3' doc_build_dir = 'doc/build' doc_source_dir = 'doc/source' cmdclass = {} command_options = {} # 'build_doc' option try: from sphinx.setup_command import BuildDoc if not os.path.exists(doc_build_dir): os.mkdir(doc_build_dir) cmdclass.update({'build_doc': BuildDoc}) command_options.update({ 'build_doc': { #'project': ('setup.py', name), 'version': ('setup.py', version), 'release': ('setup.py', version), 'source_dir': ('setup.py', doc_source_dir), 'build_dir': ('setup.py', doc_build_dir), 'builder': ('setup.py', 'html'), }}) except Exception: print("No Sphinx module found. You have to install Sphinx " "to be able to generate the documentation.") setuptools.setup(name=name, version=version, description=description, long_description=long_description, long_description_content_type="text/x-rst", keywords=keywords, author=author, author_email=author_email, url=url, packages=['odoorpc', 'odoorpc.rpc'], license=license, cmdclass=cmdclass, command_options=command_options, classifiers=[ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Odoo", ], ) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/examples/0000755000232200023220000000000013376743447015371 5ustar debalancedebalanceodoorpc-0.7.0/examples/example.py0000755000232200023220000000547413376743447017413 0ustar debalancedebalance#!/usr/bin/env python """A sample script to demonstrate some of functionalities of OERPLib.""" import oerplib # XMLRPC server configuration SERVER = 'localhost' PROTOCOL = 'xmlrpc' PORT = 8069 # Name of the OpenERP database to use DATABASE = 'db_name' USER = 'admin' PASSWORD = 'password' try: # Login oerp = oerplib.OERP( server=SERVER, database=DATABASE, protocol=PROTOCOL, port=PORT) oerp.login(USER, PASSWORD) # ----------------------- # # -- Low level methods -- # # ----------------------- # # Execute - search user_ids = oerp.execute('res.users', 'search', [('id', '=', oerp.user.id)]) # Execute - read user_data = oerp.execute('res.users', 'read', user_ids[0]) # Execute - write oerp.execute('res.users', 'write', user_ids[0], {'name': "Administrator"}) # Execute - create new_user_id = oerp.execute('res.users', 'create', {'login': "New user"}) # --------------------- # # -- Dynamic methods -- # # --------------------- # # Get the model user_obj = oerp.get('res.users') # Search IDs of a model that match criteria user_obj.search([('name', 'ilike', "Administrator")]) # Create a record new_user_id = user_obj.create({'login': "new_user"}) # Read data of a record (just the name field) user_data = user_obj.read([new_user_id], ['name']) # Write a record user_obj.write([new_user_id], {'name': "New user"}) # Delete a record user_obj.unlink([new_user_id]) # -------------------- # # -- Browse objects -- # # -------------------- # # Browse an object user = user_obj.browse(oerp.user.id) print(user.name) print(user.company_id.name) # .. or many objects order_obj = oerp.get('sale.order') for order in order_obj.browse([68, 69]): print(order.name) print(order.partner_id.name) for line in order.order_line: print('\t{0}'.format(line.name)) # ----------------------- # # -- Download a report -- # # ----------------------- # so_pdf_path = oerp.report('sale.order', 'sale.order', 1) inv_pdf_path = oerp.report('webkitaccount.invoice', 'account.invoice', 1) # ------------------------- # # -- Databases management-- # # ------------------------- # # List databases print(oerp.db.list()) # Create a database in background oerp.db.create( 'super_admin_passwd', 'my_db', demo_data=True, lang='fr_FR', admin_passwd='admin_passwd') # ... after a while, dump it my_dump = oerp.db.dump('super_admin_password', 'my_db') # Create a new database from the dump oerp.db.restore('super_admin_password', 'my_new_db', my_dump) # Delete the old one oerp.db.drop('super_admin_password', 'my_db') except oerplib.error.Error as e: print(e) except Exception as e: print(e) odoorpc-0.7.0/CHANGELOG0000644000232200023220000000427513376743447014775 0ustar debalancedebalance0.7.0 ===== - IMP: Support added for Odoo 12.0 - IMP: Convenient script 'run_tests_docker.sh' to run tests locally against the official Odoo Docker image - IMP: Implement 'with_context(...)' and 'with_env(...)' methods on the Model class (they were only available on recordset until now) - IMP: Logger added (all requests, params + response) + Documentation 0.6.2 ===== - FIX: Perform HTTP requests with or without a leading slash in the URL - FIX: Handle RPCError exceptions with either bytes or unicode message - FIX: Sphinx doc: could not import extension sphinx_ext.doctest_custom 0.6.1 ===== - IMP: OCA rebranding - IMP: Drop support for Python 3.2 and 3.3 - IMP: Support added for Odoo 11.0 0.6.0 ===== - IMP: Adds support for passing a custom URL opener (e.g. to handle HTTP basic authentication) - IMP: Support added for Python 3.6 0.5.1 ===== - FIX: Session file loading, read the `timeout` value as a float 0.5.0 ===== - IMP: Support added for Odoo 10.0 - IMP: Documentation updated to be in line with Odoo 10.0 0.4.3 ===== - IMP: Documentation (minor fixes) 0.4.2 ===== - IMP: Unit tests: - autodetect server version - tests added for binary fields - IMP: The timeout can be set to 'None' (infinite timeout) - FIX: Underscore prefixed methods are not forwarded to the server 0.4.1 ===== - IMP: New feature, check if a model exists in the Odoo database (see the README or Quick Start section in the documentation) - IMP: Support added for Jython 270 - FIX: Some methods in Odoo 9 return no result (issue #12) 0.4.0 ===== - IMP: Support added for Odoo 9.0 - IMP: Support added for Python 3.5 - IMP: The 'data' parameter of the 'ODOO.http()' method is now optional 0.3.0 ===== - FIX: 'ODOO.exec_workflow()' method now works correctly (issue #7) - FIX: .travis.yml - URL of wkhtmltox has changed (issue #9) - FIX: README.rst - Fixed shields (pypip.in replaced by shields.io) 0.2.0 ===== - IMP: Sphinx Doctest integration (with Travis CI) - IMP: Internal Python modules reorganized - FIX: The recordset environment/context was not taken into account when calling a RPC method from it ('ODOO.env' was used instead) - FIX: Missing the MANIFEST.in file (issue #6) 0.1.0 ===== - Initial release odoorpc-0.7.0/odoorpc/0000755000232200023220000000000013376743447015220 5ustar debalancedebalanceodoorpc-0.7.0/odoorpc/fields.py0000644000232200023220000006520613376743447017051 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module contains classes representing the fields supported by Odoo. A field is a Python descriptor which defines getter/setter methods for its related attribute. """ import sys import datetime #from odoorpc import error from odoorpc.models import Model, IncrementalRecords def is_int(value): """Return `True` if ``value`` is an integer.""" if isinstance(value, bool): return False try: int(value) return True except ValueError: return False # Python 2 if sys.version_info[0] < 3: def is_string(value): """Return `True` if ``value`` is a string.""" return isinstance(value, basestring) # Python >= 3 else: def is_string(value): """Return `True` if ``value`` is a string.""" return isinstance(value, str) def odoo_tuple_in(iterable): """Return `True` if `iterable` contains an expected tuple like ``(6, 0, IDS)`` (and so on). >>> odoo_tuple_in([0, 1, 2]) # Simple list False >>> odoo_tuple_in([(6, 0, [42])]) # List of tuples True >>> odoo_tuple_in([[1, 42]]) # List of lists True """ if not iterable: return False def is_odoo_tuple(elt): """Return `True` if `elt` is a Odoo special tuple.""" try: return elt[:1][0] in [1, 2, 3, 4, 5] \ or elt[:2] in [(6, 0), [6, 0], (0, 0), [0, 0]] except (TypeError, IndexError): return False return any(is_odoo_tuple(elt) for elt in iterable) def tuples2ids(tuples, ids): """Update `ids` according to `tuples`, e.g. (3, 0, X), (4, 0, X)...""" for value in tuples: if value[0] == 6 and value[2]: ids = value[2] elif value[0] == 5: ids[:] = [] elif value[0] == 4 and value[1] and value[1] not in ids: ids.append(value[1]) elif value[0] == 3 and value[1] and value[1] in ids: ids.remove(value[1]) return ids def records2ids(iterable): """Replace records contained in `iterable` with their corresponding IDs: >>> groups = list(odoo.env.user.groups_id) >>> records2ids(groups) [1, 2, 3, 14, 17, 18, 19, 7, 8, 9, 5, 20, 21, 22, 23] """ def record2id(elt): """If `elt` is a record, return its ID.""" if isinstance(elt, Model): return elt.id return elt return [record2id(elt) for elt in iterable] class BaseField(object): """Field which all other fields inherit. Manage common metadata. """ def __init__(self, name, data): self.name = name self.type = 'type' in data and data['type'] or False self.string = 'string' in data and data['string'] or False self.size = 'size' in data and data['size'] or False self.required = 'required' in data and data['required'] or False self.readonly = 'readonly' in data and data['readonly'] or False self.help = 'help' in data and data['help'] or False self.states = 'states' in data and data['states'] or False def __get__(self, instance, owner): pass def __set__(self, instance, value): """Each time a record is modified, it is marked as dirty in the environment. """ instance.env.dirty.add(instance) if instance._odoo.config.get('auto_commit'): instance.env.commit() def __str__(self): """Return a human readable string representation of the field.""" attrs = ['string', 'relation', 'required', 'readonly', 'size', 'domain'] attrs_rep = [] for attr in attrs: if hasattr(self, attr): value = getattr(self, attr) if value: if is_string(value): attrs_rep.append("{0}='{1}'".format(attr, value)) else: attrs_rep.append("{0}={1}".format(attr, value)) attrs_rep = ", ".join(attrs_rep) return "{0}({1})".format(self.type, attrs_rep) def check_value(self, value): """Check the validity of a value for the field.""" #if self.readonly: # raise error.Error( # "'{field_name}' field is readonly".format( # field_name=self.name)) if value and self.size: if not is_string(value): raise ValueError("Value supplied has to be a string") if len(value) > self.size: raise ValueError( "Lenght of the '{0}' is limited to {1}".format( self.name, self.size)) if not value and self.required: raise ValueError("'{0}' field is required".format(self.name)) return value def store(self, record, value): """Store the value in the record.""" record._values[self.name][record.id] = value class Binary(BaseField): """Equivalent of the `fields.Binary` class.""" def __init__(self, name, data): super(Binary, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name][instance.id] if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Binary, self).__set__(instance, value) class Boolean(BaseField): """Equivalent of the `fields.Boolean` class.""" def __init__(self, name, data): super(Boolean, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name][instance.id] if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): value = bool(value) value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Boolean, self).__set__(instance, value) class Char(BaseField): """Equivalent of the `fields.Char` class.""" def __init__(self, name, data): super(Char, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Char, self).__set__(instance, value) class Date(BaseField): """Represent the OpenObject 'fields.data'""" pattern = "%Y-%m-%d" def __init__(self, name, data): super(Date, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) or False if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] try: res = datetime.datetime.strptime(value, self.pattern).date() except (ValueError, TypeError): res = value return res def __set__(self, instance, value): value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Date, self).__set__(instance, value) def check_value(self, value): super(Date, self).check_value(value) if isinstance(value, datetime.date): value = value.strftime("%Y-%m-%d") elif is_string(value): try: datetime.datetime.strptime(value, self.pattern) except: raise ValueError( "String not well formatted, expecting '{0}' format".format( self.pattern)) elif isinstance(value, bool) or value is None: return value else: raise ValueError( "Expecting a datetime.date object or string") return value class Datetime(BaseField): """Represent the OpenObject 'fields.datetime'""" pattern = "%Y-%m-%d %H:%M:%S" def __init__(self, name, data): super(Datetime, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] try: res = datetime.datetime.strptime(value, self.pattern) except (ValueError, TypeError): res = value return res def __set__(self, instance, value): value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Datetime, self).__set__(instance, value) def check_value(self, value): super(Datetime, self).check_value(value) if isinstance(value, datetime.datetime): value = value.strftime("%Y-%m-%d %H:%M:%S") elif is_string(value): try: datetime.datetime.strptime(value, self.pattern) except: raise ValueError( "Value not well formatted, expecting '{0}' format".format( self.pattern)) elif isinstance(value, bool): return value else: raise ValueError( "Expecting a datetime.datetime object or string") return value class Float(BaseField): """Equivalent of the `fields.Float` class.""" def __init__(self, name, data): super(Float, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] if value in [None, False]: value = 0.0 return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Float, self).__set__(instance, value) class Integer(BaseField): """Equivalent of the `fields.Integer` class.""" def __init__(self, name, data): super(Integer, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] if value in [None, False]: value = 0 return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Integer, self).__set__(instance, value) class Selection(BaseField): """Represent the OpenObject 'fields.selection'""" def __init__(self, name, data): super(Selection, self).__init__(name, data) self.selection = 'selection' in data and data['selection'] or False def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id, False) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Selection, self).__set__(instance, value) def check_value(self, value): super(Selection, self).check_value(value) selection = [val[0] for val in self.selection] if value and value not in selection: raise ValueError( "The value '{0}' supplied doesn't match with the possible " "values '{1}' for the '{2}' field".format( value, selection, self.name, )) return value class Many2many(BaseField): """Represent the OpenObject 'fields.many2many'""" def __init__(self, name, data): super(Many2many, self).__init__(name, data) self.relation = 'relation' in data and data['relation'] or False self.context = 'context' in data and data['context'] or {} self.domain = 'domain' in data and data['domain'] or False def __get__(self, instance, owner): """Return a recordset.""" ids = None if instance._values[self.name].get(instance.id): ids = instance._values[self.name][instance.id][:] # None value => get the value on the fly if ids is None: args = [[instance.id], [self.name]] kwargs = {'context': self.context, 'load': '_classic_write'} orig_ids = instance._odoo.execute_kw( instance._name, 'read', args, kwargs)[0][self.name] instance._values[self.name][instance.id] = orig_ids ids = orig_ids and orig_ids[:] or [] # Take updated values into account if instance.id in instance._values_to_write[self.name]: values = instance._values_to_write[self.name][instance.id] # Handle ODOO tuples to update 'ids' ids = tuples2ids(values, ids or []) # Handle the field context Relation = instance.env[self.relation] env = instance.env if self.context: context = instance.env.context.copy() context.update(self.context) env = instance.env(context=context) return Relation._browse( env, ids, from_record=(instance, self)) def __set__(self, instance, value): value = self.check_value(value) if isinstance(value, IncrementalRecords): value = value.tuples else: if value and not odoo_tuple_in(value): value = [(6, 0, records2ids(value))] elif not value: value = [(5, )] instance._values_to_write[self.name][instance.id] = value super(Many2many, self).__set__(instance, value) def check_value(self, value): if value: if not isinstance(value, list) \ and not isinstance(value, Model) \ and not isinstance(value, IncrementalRecords): raise ValueError( "The value supplied has to be a list, a recordset " "or 'False'") return super(Many2many, self).check_value(value) def store(self, record, value): """Store the value in the record.""" if record._values[self.name].get(record.id): tuples2ids(value, record._values[self.name][record.id]) else: record._values[self.name][record.id] = tuples2ids(value, []) class Many2one(BaseField): """Represent the OpenObject 'fields.many2one'""" def __init__(self, name, data): super(Many2one, self).__init__(name, data) self.relation = 'relation' in data and data['relation'] or False self.context = 'context' in data and data['context'] or {} self.domain = 'domain' in data and data['domain'] or False def __get__(self, instance, owner): id_ = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: id_ = instance._values_to_write[self.name][instance.id] # None value => get the value on the fly if id_ is None: args = [[instance.id], [self.name]] kwargs = {'context': self.context, 'load': '_classic_write'} id_ = instance._odoo.execute_kw( instance._name, 'read', args, kwargs)[0][self.name] instance._values[self.name][instance.id] = id_ Relation = instance.env[self.relation] if id_: env = instance.env if self.context: context = instance.env.context.copy() context.update(self.context) env = instance.env(context=context) return Relation._browse( env, id_, from_record=(instance, self)) return Relation.browse(False) def __set__(self, instance, value): if isinstance(value, Model): o_rel = value elif is_int(value): rel_obj = instance.env[self.relation] o_rel = rel_obj.browse(value) elif value in [None, False]: o_rel = False else: raise ValueError("Value supplied has to be an integer, " "a record object or 'None/False'.") o_rel = self.check_value(o_rel) #instance.__data__['updated_values'][self.name] = \ # o_rel and [o_rel.id, False] instance._values_to_write[self.name][instance.id] = \ o_rel and o_rel.id or False super(Many2one, self).__set__(instance, value) def check_value(self, value): super(Many2one, self).check_value(value) if value and value._name != self.relation: raise ValueError( ("Instance of '{model}' supplied doesn't match with the " + "relation '{relation}' of the '{field_name}' field.").format( model=value._name, relation=self.relation, field_name=self.name)) return value class One2many(BaseField): """Represent the OpenObject 'fields.one2many'""" def __init__(self, name, data): super(One2many, self).__init__(name, data) self.relation = 'relation' in data and data['relation'] or False self.context = 'context' in data and data['context'] or {} self.domain = 'domain' in data and data['domain'] or False def __get__(self, instance, owner): """Return a recordset.""" ids = None if instance._values[self.name].get(instance.id): ids = instance._values[self.name][instance.id][:] # None value => get the value on the fly if ids is None: args = [[instance.id], [self.name]] kwargs = {'context': self.context, 'load': '_classic_write'} orig_ids = instance._odoo.execute_kw( instance._name, 'read', args, kwargs)[0][self.name] instance._values[self.name][instance.id] = orig_ids ids = orig_ids and orig_ids[:] or [] # Take updated values into account if instance.id in instance._values_to_write[self.name]: values = instance._values_to_write[self.name][instance.id] # Handle ODOO tuples to update 'ids' ids = tuples2ids(values, ids or []) Relation = instance.env[self.relation] env = instance.env if self.context: context = instance.env.context.copy() context.update(self.context) env = instance.env(context=context) return Relation._browse( env, ids, from_record=(instance, self)) def __set__(self, instance, value): value = self.check_value(value) if isinstance(value, IncrementalRecords): value = value.tuples else: if value and not odoo_tuple_in(value): value = [(6, 0, records2ids(value))] elif not value: value = [(5, )] instance._values_to_write[self.name][instance.id] = value super(One2many, self).__set__(instance, value) def check_value(self, value): if value: if not isinstance(value, list) \ and not isinstance(value, Model) \ and not isinstance(value, IncrementalRecords): raise ValueError( "The value supplied has to be a list, a recordset " "or 'False'") return super(One2many, self).check_value(value) def store(self, record, value): """Store the value in the record.""" if record._values[self.name].get(record.id): tuples2ids(value, record._values[self.name][record.id]) else: record._values[self.name][record.id] = tuples2ids(value, []) class Reference(BaseField): """Represent the OpenObject 'fields.reference'.""" def __init__(self, name, data): super(Reference, self).__init__(name, data) self.context = 'context' in data and data['context'] or {} self.domain = 'domain' in data and data['domain'] or False self.selection = 'selection' in data and data['selection'] or False def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) or False if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] # None value => get the value on the fly if value is None: args = [[instance.id], [self.name]] kwargs = {'context': self.context, 'load': '_classic_write'} value = instance._odoo.execute_kw( instance._name, 'read', args, kwargs)[0][self.name] instance._values_to_write[self.name][instance.id] = value if value: parts = value.rpartition(',') relation, o_id = parts[0], parts[2] relation = relation.strip() o_id = int(o_id.strip()) if relation and o_id: Relation = instance.env[relation] env = instance.env if self.context: context = instance.env.context.copy() context.update(self.context) env = instance.env(context=context) return Relation._browse( env, o_id, from_record=(instance, self)) return False def __set__(self, instance, value): value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Reference, self).__set__(instance, value) def _check_relation(self, relation): """Raise a `ValueError` if `relation` is not allowed among the possible values. """ selection = [val[0] for val in self.selection] if relation not in selection: raise ValueError( ("The value '{value}' supplied doesn't match with the possible" " values '{selection}' for the '{field_name}' field").format( value=relation, selection=selection, field_name=self.name, )) return relation def check_value(self, value): if isinstance(value, Model): relation = value.__class__.__osv__['name'] self._check_relation(relation) value = "%s,%s" % (relation, value.id) super(Reference, self).check_value(value) elif is_string(value): super(Reference, self).check_value(value) parts = value.rpartition(',') relation, o_id = parts[0], parts[2] relation = relation.strip() o_id = o_id.strip() #o_rel = instance.__class__.__odoo__.browse(relation, o_id) if not relation or not is_int(o_id): raise ValueError("String not well formatted, expecting " "'{relation},{id}' format") self._check_relation(relation) else: raise ValueError("Value supplied has to be a string or" " a browse_record object.") return value class Text(BaseField): """Equivalent of the `fields.Text` class.""" def __init__(self, name, data): super(Text, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name].get(instance.id) if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): if value is None: value = False value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Text, self).__set__(instance, value) class Html(Text): """Equivalent of the `fields.Html` class.""" def __init__(self, name, data): super(Html, self).__init__(name, data) class Unknown(BaseField): """Represent an unknown field. This should not happen but this kind of field only exists to avoid a blocking situation from a RPC point of view. """ def __init__(self, name, data): super(Unknown, self).__init__(name, data) def __get__(self, instance, owner): value = instance._values[self.name][instance.id] if instance.id in instance._values_to_write[self.name]: value = instance._values_to_write[self.name][instance.id] return value def __set__(self, instance, value): value = self.check_value(value) instance._values_to_write[self.name][instance.id] = value super(Unknown, self).__set__(instance, value) TYPES_TO_FIELDS = { 'binary': Binary, 'boolean': Boolean, 'char': Char, 'date': Date, 'datetime': Datetime, 'float': Float, 'html': Html, 'integer': Integer, 'many2many': Many2many, 'many2one': Many2one, 'one2many': One2many, 'reference': Reference, 'selection': Selection, 'text': Text, } def generate_field(name, data): """Generate a well-typed field according to the data dictionary supplied (obtained via the `fields_get' method of any models). """ assert 'type' in data field = TYPES_TO_FIELDS.get(data['type'], Unknown)(name, data) return field # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/report.py0000644000232200023220000001605113376743447017110 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module provide the :class:`Report` class to list available reports and to generate/download them. """ import base64 import io from odoorpc.tools import v, get_encodings def encode2bytes(data): for encoding in get_encodings(): try: return data.decode(encoding) except Exception: pass return data class Report(object): """The `Report` class represents the report management service. It provides methods to list and download available reports from the server. .. note:: This service have to be used through the :attr:`odoorpc.ODOO.report` property. .. doctest:: :options: +SKIP >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', port=8069) >>> odoo.login('odoorpc_test', 'admin', 'password') >>> odoo.report .. doctest:: :hide: >>> import odoorpc >>> odoo = odoorpc.ODOO(HOST, protocol=PROTOCOL, port=PORT) >>> odoo.login(DB, USER, PWD) >>> odoo.report """ def __init__(self, odoo): self._odoo = odoo def download(self, name, ids, datas=None, context=None): """Download a report from the server and return it as a remote file. For instance, to download the "Quotation / Order" report of sale orders identified by the IDs ``[2, 3]``: .. doctest:: :options: +SKIP >>> report = odoo.report.download('sale.report_saleorder', [2, 3]) .. doctest:: :hide: >>> report = odoo.report.download('sale.report_saleorder', [2]) Write it on the file system: .. doctest:: :options: +SKIP >>> with open('sale_orders.pdf', 'wb') as report_file: ... report_file.write(report.read()) ... .. doctest:: :hide: >>> with open('sale_orders.pdf', 'wb') as report_file: ... fileno = report_file.write(report.read()) # Python 3 ... *Python 2:* :return: `io.BytesIO` :raise: :class:`odoorpc.error.RPCError` (wrong parameters) :raise: `ValueError` (received invalid data) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `io.BytesIO` :raise: :class:`odoorpc.error.RPCError` (wrong parameters) :raise: `ValueError` (received invalid data) :raise: `urllib.error.URLError` (connection error) """ if context is None: context = self._odoo.env.context def check_report(name): report_model = 'ir.actions.report' if v(self._odoo.version)[0] < 11: report_model = 'ir.actions.report.xml' IrReport = self._odoo.env[report_model] report_ids = IrReport.search([('report_name', '=', name)]) report_id = report_ids and report_ids[0] or False if not report_id: raise ValueError("The report '%s' does not exist." % name) return report_id report_id = check_report(name) # Odoo >= 11.0 if v(self._odoo.version)[0] >= 11: IrReport = self._odoo.env['ir.actions.report'] report = IrReport.browse(report_id) response = report.with_context(context).render(ids, data=datas) content = response[0] # On the server the result is a bytes string, # but the RPC layer of Odoo returns it as a unicode string, # so we encode it again as bytes result = content.encode('latin1') return io.BytesIO(result) # Odoo < 11.0 else: args_to_send = [self._odoo.env.db, self._odoo.env.uid, self._odoo._password, name, ids, datas, context] data = self._odoo.json( '/jsonrpc', {'service': 'report', 'method': 'render_report', 'args': args_to_send}) if 'result' not in data and not data['result'].get('result'): raise ValueError("Received invalid data.") # Encode to bytes forced to be compatible with Python 3.2 # (its 'base64.standard_b64decode()' function only accepts bytes) result = encode2bytes(data['result']['result']) content = base64.standard_b64decode(result) return io.BytesIO(content) def list(self): """List available reports from the server by returning a dictionary with reports classified by data model: .. doctest:: :options: +SKIP >>> odoo.report.list()['account.invoice'] [{'name': u'Duplicates', 'report_name': u'account.account_invoice_report_duplicate_main', 'report_type': u'qweb-pdf'}, {'name': 'Invoices', 'report_type': 'qweb-pdf', 'report_name': 'account.report_invoice'}] .. doctest:: :hide: >>> from pprint import pprint as pp >>> any(data['report_name'] == 'account.report_invoice' ... for data in odoo.report.list()['account.invoice']) True *Python 2:* :return: `list` of dictionaries :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `list` of dictionaries :raise: `urllib.error.URLError` (connection error) """ report_model = 'ir.actions.report' if v(self._odoo.version)[0] < 11: report_model = 'ir.actions.report.xml' IrReport = self._odoo.env[report_model] report_ids = IrReport.search([]) reports = IrReport.read( report_ids, ['name', 'model', 'report_name', 'report_type']) result = {} for report in reports: model = report.pop('model') report.pop('id') if model not in result: result[model] = [] result[model].append(report) return result # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/__init__.py0000644000232200023220000000645013376743447017336 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """The `odoorpc` module defines the :class:`ODOO` class. The :class:`ODOO` class is the entry point to manage `Odoo` servers. You can use this one to write `Python` programs that performs a variety of automated jobs that communicate with a `Odoo` server. Here's a sample session using this module:: >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', port=8069) # connect to localhost, default port >>> odoo.login('dbname', 'admin', 'admin') To catch debug logs of OdooRPC from your own code, you have to configure a logger the way you want with a log level set to `DEBUG`:: >>> import logging >>> logging.basicConfig() >>> logger = logging.getLogger('odoorpc') >>> logger.setLevel(logging.DEBUG) Then all queries generated by OdooRPC will be logged:: >>> import odoorpc >>> odoo = odoorpc.ODOO() >>> odoo.login('dbname', 'admin', 'admin') DEBUG:odoorpc.rpc.jsonrpclib:(JSON,send) http://localhost:8069/web/session/authenticate {'jsonrpc': '2.0', 'id': 499807971, 'method': 'call', 'params': {'db': 'dbname', 'login': 'admin', 'password': '**********'}} DEBUG:odoorpc.rpc.jsonrpclib:(JSON,recv) http://localhost:8069/web/session/authenticate {'jsonrpc': '2.0', 'id': 499807971, 'method': 'call', 'params': {'db': 'dbname', 'login': 'admin', 'password': '**********'}} => {'result': {'is_admin': True, 'server_version': '12.0-20181008', 'currencies': {'2': {'digits': [69, 2], 'position': 'before', 'symbol': '$'}, '1': {'digits': [69, 2], 'position': 'after', 'symbol': '€'}}, 'partner_display_name': 'YourCompany, Mitchell Admin', 'company_id': 1, 'username': 'admin', 'web_tours': [], 'user_companies': False, 'session_id': '61cb37d21771531f789bea631a03236aa21f06d4', 'is_system': True, 'server_version_info': [12, 0, 0, 'final', 0, ''], 'db': 'odoorpc_v12', 'name': 'Mitchell Admin', 'web.base.url': 'http://localhost:8069', 'user_context': {'lang': 'fr_FR', 'tz': 'Europe/Brussels', 'uid': 2}, 'odoobot_initialized': True, 'show_effect': 'True', 'partner_id': 3, 'uid': 2}, 'id': 499807971, 'jsonrpc': '2.0'} """ __author__ = 'ABF Osiell - Sebastien Alix' __email__ = 'sebastien.alix@osiell.com' __licence__ = 'LGPL v3' __version__ = '0.7.0' __all__ = ['ODOO', 'error'] import logging from odoorpc.odoo import ODOO from odoorpc import error logging.getLogger(__name__).addHandler(logging.NullHandler()) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/env.py0000644000232200023220000002556413376743447016376 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """Supply the :class:`Environment` class to manage records more efficiently.""" import sys import weakref from odoorpc.models import Model from odoorpc import fields FIELDS_RESERVED = ['id', 'ids', '__odoo__', '__osv__', '__data__', 'env'] class Environment(object): """An environment wraps data like the user ID, context or current database name, and provides an access to data model proxies. .. doctest:: :options: +SKIP >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost') >>> odoo.login('db_name', 'admin', 'password') >>> odoo.env Environment(db='db_name', uid=1, context={'lang': 'fr_FR', 'tz': 'Europe/Brussels', 'uid': 1}) .. doctest:: :hide: >>> odoo.env Environment(db=..., uid=..., context=...) """ def __init__(self, odoo, db, uid, context): self._odoo = odoo self._db = db self._uid = uid self._context = context self._registry = {} self._dirty = weakref.WeakSet() # set of records updated locally def __repr__(self): return "Environment(db=%s, uid=%s, context=%s)" % ( repr(self._db), self._uid, self._context) @property def dirty(self): """ .. warning:: This property is used internally and should not be used directly. As such, it should not be referenced in the user documentation. List records having local changes. These changes can be committed to the server with the :func:`commit` method, or invalidated with :func:`invalidate`. """ return self._dirty @property def context(self): """The context of the user connected. .. doctest:: :options: +SKIP >>> odoo.env.context {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2} .. doctest:: :hide: >>> from pprint import pprint as pp >>> pp(odoo.env.context) {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': ...} """ return self._context @property def db(self): """The database currently used. .. doctest:: :options: +SKIP >>> odoo.env.db 'db_name' .. doctest:: :hide: >>> odoo.env.db == DB True """ return self._db def commit(self): """Commit dirty records to the server. This method is automatically called when the `auto_commit` option is set to `True` (default). It can be useful to set the former option to `False` to get better performance by reducing the number of RPC requests generated. With `auto_commit` set to `True` (default behaviour), each time a value is set on a record field a RPC request is sent to the server to update the record: .. doctest:: >>> user = odoo.env.user >>> user.name = "Joe" # write({'name': "Joe"}) >>> user.email = "joe@odoo.net" # write({'email': "joe@odoo.net"}) With `auto_commit` set to `False`, changes on a record are sent all at once when calling the :func:`commit` method: .. doctest:: >>> odoo.config['auto_commit'] = False >>> user = odoo.env.user >>> user.name = "Joe" >>> user.email = "joe@odoo.net" >>> user in odoo.env.dirty True >>> odoo.env.commit() # write({'name': "Joe", 'email': "joe@odoo.net"}) >>> user in odoo.env.dirty False Only one RPC request is generated in the last case. """ # Iterate on a new set, as we remove record during iteration from the # original one for record in set(self.dirty): values = {} for field in record._values_to_write: if record.id in record._values_to_write[field]: value = record._values_to_write[field].pop(record.id) values[field] = value # Store the value in the '_values' dictionary. This # operation is delegated to each field descriptor as some # values can not be stored "as is" (e.g. magic tuples of # 2many fields need to be converted) record.__class__.__dict__[field].store(record, value) record.write(values) self.dirty.remove(record) def invalidate(self): """Invalidate the cache of records.""" self.dirty.clear() @property def lang(self): """Return the current language code. .. doctest:: >>> odoo.env.lang 'en_US' """ return self.context.get('lang', False) def ref(self, xml_id): """Return the record corresponding to the given `xml_id` (also called external ID). Raise an :class:`RPCError ` if no record is found. .. doctest:: >>> odoo.env.ref('base.lang_en') Recordset('res.lang', [1]) :return: a :class:`odoorpc.models.Model` instance (recordset) :raise: :class:`odoorpc.error.RPCError` """ model, id_ = self._odoo.execute( 'ir.model.data', 'xmlid_to_res_model_res_id', xml_id, True) return self[model].browse(id_) @property def uid(self): """The user ID currently logged. .. doctest:: :options: +SKIP >>> odoo.env.uid 1 .. doctest:: :hide: >>> odoo.env.uid in [1, 2] True """ return self._uid @property def user(self): """Return the current user (as a record). .. doctest:: :options: +SKIP >>> user = odoo.env.user >>> user Recordset('res.users', [2]) >>> user.name 'Mitchell Admin' .. doctest:: :hide: >>> user = odoo.env.user >>> user.id in [1, 2] True >>> 'Admin' in user.name True :return: a :class:`odoorpc.models.Model` instance :raise: :class:`odoorpc.error.RPCError` """ return self['res.users'].browse(self.uid) @property def registry(self): """The data model registry. It is a mapping between a model name and its corresponding proxy used to generate records. As soon as a model is needed the proxy is added to the registry. This way the model proxy is ready for a further use (avoiding costly `RPC` queries when browsing records through relations). .. doctest:: :hide: >>> odoo.env.registry.clear() >>> odoo.env.registry {} >>> odoo.env.user.company_id.name # 'res.users' and 'res.company' Model proxies will be fetched 'YourCompany' >>> from pprint import pprint >>> pprint(odoo.env.registry) {'res.company': Model('res.company'), 'res.users': Model('res.users')} If you need to regenerate the model proxy, simply delete it from the registry: >>> del odoo.env.registry['res.company'] To delete all model proxies: >>> odoo.env.registry.clear() >>> odoo.env.registry {} """ return self._registry def __getitem__(self, model): """Return the model class corresponding to `model`. >>> Partner = odoo.env['res.partner'] >>> Partner Model('res.partner') :return: a :class:`odoorpc.models.Model` class """ if model not in self.registry: #self.registry[model] = Model(self._odoo, self, model) self.registry[model] = self._create_model_class(model) return self.registry[model] def __call__(self, context=None): """Return an environment based on `self` with a different user context. """ context = self.context if context is None else context env = Environment(self._odoo, self._db, self._uid, context) env._dirty = self._dirty env._registry = self._registry return env def __contains__(self, model): """Check if the given `model` exists on the server. >>> 'res.partner' in odoo.env True :return: `True` or `False` """ model_exists = self._odoo.execute('ir.model', 'search', [('model', '=', model)]) return bool(model_exists) def _create_model_class(self, model): """Generate the model proxy class. :return: a :class:`odoorpc.models.Model` class """ cls_name = model.replace('.', '_') # Hack for Python 2 (no need to do this for Python 3) if sys.version_info[0] < 3: if isinstance(cls_name, unicode): cls_name = cls_name.encode('utf-8') # Retrieve server fields info and generate corresponding local fields attrs = { '_env': self, '_odoo': self._odoo, '_name': model, '_columns': {}, } fields_get = self._odoo.execute(model, 'fields_get') for field_name, field_data in fields_get.items(): if field_name not in FIELDS_RESERVED: Field = fields.generate_field(field_name, field_data) attrs['_columns'][field_name] = Field attrs[field_name] = Field # Case where no field 'name' exists, we generate one (which will be # in readonly mode) in purpose to be filled with the 'name_get' method if 'name' not in attrs['_columns']: field_data = {'type': 'text', 'string': 'Name', 'readonly': True} Field = fields.generate_field('name', field_data) attrs['_columns']['name'] = Field attrs['name'] = Field return type(cls_name, (Model,), attrs) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/models.py0000644000232200023220000003676513376743447017076 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """Provide the :class:`Model` class which allow to access dynamically to all methods proposed by a data model. """ __all__ = ['Model'] import sys from odoorpc import error # Python 2 if sys.version_info[0] < 3: NORMALIZED_TYPES = (int, long, str, unicode) # Python >= 3 else: NORMALIZED_TYPES = (int, str, bytes) FIELDS_RESERVED = ['id', 'ids', '__odoo__', '__osv__', '__data__', 'env'] def _normalize_ids(ids): """Normalizes the ids argument for ``browse``.""" if not ids: return [] if ids.__class__ in NORMALIZED_TYPES: return [ids] return list(ids) class IncrementalRecords(object): """A helper class used internally by __iadd__ and __isub__ methods. Afterwards, field descriptors can adapt their behaviour when an instance of this class is set. """ def __init__(self, tuples): self.tuples = tuples class MetaModel(type): """Define class methods for the :class:`Model` class.""" _env = None def __getattr__(cls, method): """Provide a dynamic access to a RPC method.""" if method.startswith('_'): return super(MetaModel, cls).__getattr__(method) def rpc_method(*args, **kwargs): """Return the result of the RPC request.""" if cls._odoo.config['auto_context'] \ and 'context' not in kwargs: kwargs['context'] = cls.env.context result = cls._odoo.execute_kw( cls._name, method, args, kwargs) return result return rpc_method def __repr__(cls): return "Model(%r)" % (cls._name) @property def env(cls): """The environment used for this model/recordset.""" return cls._env # An intermediate class used to associate the 'MetaModel' metaclass to the # 'Model' one with a Python 2 and Python 3 compatibility BaseModel = MetaModel('BaseModel', (), {}) class Model(BaseModel): """Base class for all data model proxies. .. note:: All model proxies (based on this class) are generated by an :class:`environment ` (see the :attr:`odoorpc.ODOO.env` property). .. doctest:: :options: +SKIP >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', port=8069) >>> odoo.login('db_name', 'admin', 'password') >>> User = odoo.env['res.users'] >>> User Model('res.users') .. doctest:: :hide: >>> import odoorpc >>> odoo = odoorpc.ODOO(HOST, protocol=PROTOCOL, port=PORT) >>> odoo.login(DB, USER, PWD) >>> User = odoo.env['res.users'] >>> User Model('res.users') Use this data model proxy to call any method: .. doctest:: :options: +SKIP >>> User.name_get([2]) # Use any methods from the model class [[1, 'Mitchell Admin']] .. doctest:: :hide: >>> from odoorpc.tools import v >>> uid = 1 >>> if v(VERSION) >= v('12.0'): ... uid = 2 >>> data = User.name_get([uid]) >>> 'Admin' in data[0][1] True Get a recordset: .. doctest:: :options: +SKIP >>> user = User.browse(2) >>> user.name 'Mitchell Admin' .. doctest:: :hide: >>> from odoorpc.tools import v >>> uid = 1 >>> if v(VERSION) >= v('12.0'): ... uid = 2 >>> user = User.browse(uid) >>> 'Admin' in user.name True And call any method from it, it will be automatically applied on the current record: .. doctest:: :options: +SKIP >>> user.name_get() # No IDs in parameter, the method is applied on the current recordset [[1, 'Mitchell Admin']] .. doctest:: :hide: >>> data = user.name_get() >>> 'Admin' in data[0][1] True .. warning:: Excepted the :func:`browse ` method, method calls are purely dynamic. As long as you know the signature of the model method targeted, you will be able to use it (see the :ref:`tutorial `). """ __metaclass__ = MetaModel _odoo = None _name = None _columns = {} # {field: field object} def __init__(self): super(Model, self).__init__() self._env_local = None self._from_record = None self._ids = [] self._values = {} # {field: {ID: value}} self._values_to_write = {} # {field: {ID: value}} for field in self._columns: self._values[field] = {} self._values_to_write[field] = {} self.with_context = self._with_context self.with_env = self._with_env @property def env(self): """The environment used for this model/recordset.""" if self._env_local: return self._env_local return self.__class__._env @property def id(self): """ID of the record (or the first ID of a recordset).""" return self._ids[0] if self._ids else None @property def ids(self): """IDs of the recorset.""" return self._ids @classmethod def _browse(cls, env, ids, from_record=None, iterated=None): """Create an instance (a recordset) corresponding to `ids` and attached to `env`. `from_record` parameter is used when the recordset is related to a parent record, and as such can take the value of a tuple (record, field). This is useful to update the parent record when the current recordset is modified. `iterated` can take the value of an iterated recordset, and no extra RPC queries are made to generate the resulting record (recordset and its record share the same values). """ records = cls() records._env_local = env records._ids = _normalize_ids(ids) if iterated: records._values = iterated._values records._values_to_write = iterated._values_to_write else: records._from_record = from_record records._values = {} records._values_to_write = {} for field in cls._columns: records._values[field] = {} records._values_to_write[field] = {} records._init_values() return records @classmethod def browse(cls, ids): """Browse one or several records (if `ids` is a list of IDs). .. doctest:: >>> odoo.env['res.partner'].browse(1) Recordset('res.partner', [1]) .. doctest:: :options: +SKIP >>> [partner.name for partner in odoo.env['res.partner'].browse([1, 3])] ['YourCompany', 'Mitchell Admin'] .. doctest:: :hide: >>> names = [partner.name for partner in odoo.env['res.partner'].browse([1, 3])] >>> 'YourCompany' in names[0] True >>> 'Admin' in names[1] True A list of data types returned by such record fields are available :ref:`here `. :return: a :class:`Model ` instance (recordset) :raise: :class:`odoorpc.error.RPCError` """ return cls._browse(cls.env, ids) @classmethod def with_context(cls, *args, **kwargs): """Return a model (or recordset) equivalent to the current model (or recordset) attached to an environment with another context. The context is taken from the current environment or from the positional arguments `args` if given, and modified by `kwargs`. Thus, the following two examples are equivalent: .. doctest:: >>> Product = odoo.env['product.product'] >>> Product.with_context(lang='fr_FR') Model('product.product') .. doctest:: >>> context = Product.env.context >>> Product.with_context(context, lang='fr_FR') Model('product.product') This method is very convenient for example to search records whatever their active status are (active/inactive): .. doctest:: >>> all_product_ids = Product.with_context(active_test=False).search([]) Or to update translations of a recordset: .. doctest:: >>> product_en = Product.browse(1) >>> product_en.env.lang 'en_US' >>> product_en.name = "My product" # Update the english translation >>> product_fr = product_en.with_context(lang='fr_FR') >>> product_fr.env.lang 'fr_FR' >>> product_fr.name = "Mon produit" # Update the french translation """ context = dict(args[0] if args else cls.env.context, **kwargs) return cls.with_env(cls.env(context=context)) def _with_context(self, *args, **kwargs): """As the `with_context` class method but for recordset.""" context = dict(args[0] if args else self.env.context, **kwargs) return self.with_env(self.env(context=context)) @classmethod def with_env(cls, env): """Return a model (or recordset) equivalent to the current model (or recordset) attached to `env`. """ new_cls = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) new_cls._env = env return new_cls def _with_env(self, env): """As the `with_env` class method but for recordset.""" res = self._browse(env, self._ids) return res def _init_values(self, context=None): """Retrieve field values from the server. May be used to restore the original values in the purpose to cancel all changes made. """ if context is None: context = self.env.context # Get basic fields (no relational ones) basic_fields = [] for field_name in self._columns: field = self._columns[field_name] if not getattr(field, 'relation', False): basic_fields.append(field_name) # Fetch values from the server if self.ids: rows = self.__class__.read( self.ids, basic_fields, context=context, load='_classic_write') ids_fetched = set() for row in rows: ids_fetched.add(row['id']) for field_name in row: if field_name == 'id': continue self._values[field_name][row['id']] = row[field_name] ids_in_error = set(self.ids) - ids_fetched if ids_in_error: raise ValueError( "There is no '{model}' record with IDs {ids}.".format( model=self._name, ids=list(ids_in_error))) # No ID: fields filled with default values else: default_get = self.__class__.default_get( list(self._columns), context=context) for field_name in self._columns: self._values[field_name][None] = default_get.get( field_name, False) def __getattr__(self, method): """Provide a dynamic access to a RPC *instance* method (which applies on the current recordset). .. doctest:: >>> Partner = odoo.env['res.partner'] >>> Partner.write([1], {'name': 'YourCompany'}) # Class method True >>> partner = Partner.browse(1) >>> partner.write({'name': 'YourCompany'}) # Instance method True """ if method.startswith('_'): return super(Model, self).__getattr__(method) def rpc_method(*args, **kwargs): """Return the result of the RPC request.""" args = tuple([self.ids]) + args if self._odoo.config['auto_context'] \ and 'context' not in kwargs: kwargs['context'] = self.env.context result = self._odoo.execute_kw( self._name, method, args, kwargs) return result return rpc_method def __getitem__(self, key): """If `key` is an integer or a slice, return the corresponding record selection as a recordset. """ if isinstance(key, int) or isinstance(key, slice): return self._browse(self.env, self._ids[key], iterated=self) else: return getattr(self, key) def __int__(self): return self.id def __eq__(self, other): return other.__class__ == self.__class__ and self.id == other.id # Need to explicitly declare '__hash__' in Python 3 # (because '__eq__' is defined) __hash__ = BaseModel.__hash__ def __ne__(self, other): return other.__class__ != self.__class__ or self.id != other.id def __repr__(self): return "Recordset(%r, %s)" % (self._name, self.ids) def __iter__(self): """Return an iterator over `self`.""" for id_ in self._ids: yield self._browse(self.env, id_, iterated=self) def __nonzero__(self): return bool(getattr(self, '_ids', True)) def __len__(self): return len(self.ids) def __iadd__(self, records): if not self._from_record: raise error.InternalError("No parent record to update") try: list(records) except TypeError: records = [records] parent = self._from_record[0] field = self._from_record[1] updated_values = parent._values_to_write[field.name] values = [] if updated_values.get(parent.id): values = updated_values[parent.id][:] # Copy from odoorpc import fields for id_ in fields.records2ids(records): if (3, id_) in values: values.remove((3, id_)) if (4, id_) not in values: values.append((4, id_)) return IncrementalRecords(values) def __isub__(self, records): if not self._from_record: raise error.InternalError("No parent record to update") try: list(records) except TypeError: records = [records] parent = self._from_record[0] field = self._from_record[1] updated_values = parent._values_to_write[field.name] values = [] if updated_values.get(parent.id): values = updated_values[parent.id][:] # Copy from odoorpc import fields for id_ in fields.records2ids(records): if (4, id_) in values: values.remove((4, id_)) if (3, id_) not in values: values.append((3, id_)) return values # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/error.py0000644000232200023220000001012013376743447016715 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module contains all exceptions raised by `OdooRPC` when an error occurred. """ import sys class Error(Exception): """Base class for exception.""" pass class RPCError(Error): """Exception raised for errors related to RPC queries. Error details (like the `Odoo` server traceback) are available through the `info` attribute: .. doctest:: :options: +SKIP >>> from pprint import pprint as pp >>> try: ... odoo.execute('res.users', 'wrong_method') ... except odoorpc.error.RPCError as exc: ... pp(exc.info) ... {'code': 200, 'data': {'arguments': ["type object 'res.users' has no attribute 'wrong_method'"], 'debug': 'Traceback (most recent call last):\\n File ...', 'exception_type': 'internal_error', 'message': "'res.users' object has no attribute 'wrong_method'", 'name': 'exceptions.AttributeError'} 'message': 'Odoo Server Error'} .. doctest:: :hide: >>> from pprint import pprint as pp >>> try: ... odoo.execute('res.users', 'wrong_method') ... except odoorpc.error.RPCError as exc: ... exc.info['code'] == 200 ... 'message' in exc.info ... exc.info['data']['arguments'] in [ ... ["'res.users' object has no attribute 'wrong_method'"], # >= 8.0 ... ["type object 'res.users' has no attribute 'wrong_method'"], # >= 10.0 ... ] ... exc.info['data']['debug'].startswith('Traceback (most recent call last):\\n File') ... exc.info['data']['message'] in [ ... "'res.users' object has no attribute 'wrong_method'", # >= 8.0 ... "type object 'res.users' has no attribute 'wrong_method'", # >= 10.0 ... ] ... exc.info['data']['name'] in [ ... 'exceptions.AttributeError', ... 'builtins.AttributeError', ... ] ... True True True True True True """ def __init__(self, message, info=False): # Ensure that the message is in unicode, # to be compatible both with Python2 and 3 try: message = message.decode('utf-8') except (UnicodeEncodeError, AttributeError): pass super(Error, self).__init__(message, info) self.info = info def __str__(self): # args[0] should always be a unicode object (see '__init__(...)') if sys.version_info[0] < 3 and self.args and self.args[0]: return self.args[0].encode('utf-8') return self.args and self.args[0] or '' def __unicode__(self): # args[0] should always be a unicode object (see '__init__(...)') return self.args and self.args[0] or u'' def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, repr(self.args[0])) class InternalError(Error): """Exception raised for errors occurring during an internal operation.""" pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/odoo.py0000644000232200023220000005263213376743447016542 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module contains the ``ODOO`` class which is the entry point to manage an `Odoo` server. """ from odoorpc import rpc, error, tools from odoorpc.env import Environment from odoorpc import session from odoorpc.db import DB from odoorpc.report import Report class ODOO(object): """Return a new instance of the :class:`ODOO` class. `JSON-RPC` protocol is used to make requests, and the respective values for the `protocol` parameter are ``jsonrpc`` (default) and ``jsonrpc+ssl``. .. doctest:: :options: +SKIP >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', protocol='jsonrpc', port=8069) `OdooRPC` will try by default to detect the server version in order to adapt its requests if necessary. However, it is possible to force the version to use with the `version` parameter: .. doctest:: :options: +SKIP >>> odoo = odoorpc.ODOO('localhost', version='12.0') You can also define a custom URL opener to handle HTTP requests. A use case is to manage a basic HTTP authentication in front of `Odoo`: .. doctest:: :options: +SKIP >>> import urllib.request >>> import odoorpc >>> pwd_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() >>> pwd_mgr.add_password(None, "http://example.net", "userName", "passWord") >>> auth_handler = urllib.request.HTTPBasicAuthHandler(pwd_mgr) >>> opener = urllib.request.build_opener(auth_handler) >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener) *Python 2:* :raise: :class:`odoorpc.error.InternalError` :raise: `ValueError` (wrong protocol, port value, timeout value) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.InternalError` :raise: `ValueError` (wrong protocol, port value, timeout value) :raise: `urllib.error.URLError` (connection error) """ def __init__(self, host='localhost', protocol='jsonrpc', port=8069, timeout=120, version=None, opener=None): if protocol not in ['jsonrpc', 'jsonrpc+ssl']: txt = ("The protocol '{0}' is not supported by the ODOO class. " "Please choose a protocol among these ones: {1}") txt = txt.format(protocol, ['jsonrpc', 'jsonrpc+ssl']) raise ValueError(txt) try: port = int(port) except ValueError: raise ValueError("The port must be an integer") try: if timeout is not None: timeout = float(timeout) except ValueError: raise ValueError("The timeout must be a float") self._host = host self._port = port self._protocol = protocol self._env = None self._login = None self._password = None self._db = DB(self) self._report = Report(self) # Instanciate the server connector try: self._connector = rpc.PROTOCOLS[protocol]( self._host, self._port, timeout, version, opener=opener) except rpc.error.ConnectorError as exc: raise error.InternalError(exc.message) # Dictionary of configuration options self._config = tools.Config( self, {'auto_commit': True, 'auto_context': True, 'timeout': timeout}) @property def config(self): """Dictionary of available configuration options. .. doctest:: :options: +SKIP >>> odoo.config {'auto_commit': True, 'auto_context': True, 'timeout': 120} .. doctest:: :hide: >>> 'auto_commit' in odoo.config True >>> 'auto_context' in odoo.config True >>> 'timeout' in odoo.config True - ``auto_commit``: if set to `True` (default), each time a value is set on a record field a RPC request is sent to the server to update the record (see :func:`odoorpc.env.Environment.commit`). - ``auto_context``: if set to `True` (default), the user context will be sent automatically to every call of a :class:`model ` method (default: `True`): .. doctest:: :options: +SKIP >>> odoo.env.context['lang'] = 'fr_FR' >>> Product = odoo.env['product.product'] >>> Product.name_get([2]) # Context sent by default ('lang': 'fr_FR' here) [[2, 'Surveillance sur site']] >>> odoo.config['auto_context'] = False >>> Product.name_get([2]) # No context sent, 'en_US' used [[2, 'On Site Monitoring']] - ``timeout``: set the maximum timeout in seconds for a RPC request (default: `120`): >>> odoo.config['timeout'] = 300 """ return self._config @property def version(self): """The version of the server. .. doctest:: :options: +SKIP >>> odoo.version '12.0' """ return self._connector.version @property def db(self): """The database management service. See the :class:`odoorpc.db.DB` class. """ return self._db @property def report(self): """The report management service. See the :class:`odoorpc.report.Report` class. """ return self._report host = property(lambda self: self._host, doc="Hostname of IP address of the the server.") port = property(lambda self: self._port, doc="The port used.") protocol = property(lambda self: self._protocol, doc="The protocol used.") @property def env(self): """The environment which wraps data to manage records such as the user context and the registry to access data model proxies. >>> Partner = odoo.env['res.partner'] >>> Partner Model('res.partner') See the :class:`odoorpc.env.Environment` class. """ self._check_logged_user() return self._env def json(self, url, params): """Low level method to execute JSON queries. It basically performs a request and raises an :class:`odoorpc.error.RPCError` exception if the response contains an error. You have to know the names of each parameter required by the function called, and set them in the `params` dictionary. Here an authentication request: .. doctest:: :options: +SKIP >>> data = odoo.json( ... '/web/session/authenticate', ... {'db': 'db_name', 'login': 'admin', 'password': 'admin'}) >>> from pprint import pprint >>> pprint(data) {'id': 645674382, 'jsonrpc': '2.0', 'result': {'db': 'db_name', 'session_id': 'fa740abcb91784b8f4750c5c5b14da3fcc782d11', 'uid': 1, 'user_context': {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 1}, 'username': 'admin'}} .. doctest:: :hide: >>> data = odoo.json( ... '/web/session/authenticate', ... {'db': DB, 'login': USER, 'password': PWD}) >>> data['result']['db'] == DB True >>> data['result']['uid'] in [1, 2] True >>> data['result']['username'] == USER True And a call to the ``read`` method of the ``res.users`` model: .. doctest:: :options: +SKIP >>> data = odoo.json( ... '/web/dataset/call', ... {'model': 'res.users', 'method': 'read', ... 'args': [[2], ['name']]}) >>> from pprint import pprint >>> pprint(data) {'id': ..., 'jsonrpc': '2.0', 'result': [{'id': 2, 'name': 'Mitchell Admin'}]} *Python 2:* :return: a dictionary (JSON response) :raise: :class:`odoorpc.error.RPCError` :raise: `urllib2.HTTPError` (if `params` is not a dictionary) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: a dictionary (JSON response) :raise: :class:`odoorpc.error.RPCError` :raise: `urllib.error.HTTPError` (if `params` is not a dictionary) :raise: `urllib.error.URLError` (connection error) """ data = self._connector.proxy_json(url, params) if data.get('error'): raise error.RPCError( data['error']['data']['message'], data['error']) return data def http(self, url, data=None, headers=None): """Low level method to execute raw HTTP queries. .. note:: For low level JSON-RPC queries, see the more convenient :func:`odoorpc.ODOO.json` method instead. You have to know the names of each POST parameter required by the URL, and set them in the `data` string/buffer. The `data` argument must be built by yourself, following the expected URL parameters (with :func:`urllib.urlencode` function for simple parameters, or multipart/form-data structure to handle file upload). E.g., the HTTP raw query to get the company logo on `Odoo 12.0`: .. doctest:: >>> response = odoo.http('web/binary/company_logo') >>> binary_data = response.read() *Python 2:* :return: `urllib.addinfourl` :raise: `urllib2.HTTPError` :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `http.client.HTTPResponse` :raise: `urllib.error.HTTPError` :raise: `urllib.error.URLError` (connection error) """ return self._connector.proxy_http(url, data, headers) # NOTE: in the past this function was implemented as a decorator for # methods needing to be checked, but Sphinx documentation generator is not # able to parse decorated methods. def _check_logged_user(self): """Check if a user is logged. Otherwise, an error is raised.""" if not self._env or not self._password or not self._login: raise error.InternalError("Login required") def login(self, db, login='admin', password='admin'): """Log in as the given `user` with the password `passwd` on the database `db`. .. doctest:: :options: +SKIP >>> odoo.login('db_name', 'admin', 'admin') >>> odoo.env.user.name 'Administrator' *Python 2:* :raise: :class:`odoorpc.error.RPCError` :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` :raise: `urllib.error.URLError` (connection error) """ # Get the user's ID and generate the corresponding user record data = self.json( '/web/session/authenticate', {'db': db, 'login': login, 'password': password}) uid = data['result']['uid'] if uid: context = data['result']['user_context'] self._env = Environment(self, db, uid, context=context) self._login = login self._password = password else: raise error.RPCError("Wrong login ID or password") def logout(self): """Log out the user. >>> odoo.logout() True *Python 2:* :return: `True` if the operation succeed, `False` if no user was logged :raise: :class:`odoorpc.error.RPCError` :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `True` if the operation succeed, `False` if no user was logged :raise: :class:`odoorpc.error.RPCError` :raise: `urllib.error.URLError` (connection error) """ if not self._env: return False self.json('/web/session/destroy', {}) self._env = None self._login = None self._password = None return True # ------------------------- # # -- Raw XML-RPC methods -- # # ------------------------- # def execute(self, model, method, *args): """Execute the `method` of `model`. `*args` parameters varies according to the `method` used. .. doctest:: :options: +SKIP >>> odoo.execute('res.partner', 'read', [1], ['name']) [{'id': 1, 'name': 'YourCompany'}] .. doctest:: :hide: >>> data = odoo.execute('res.partner', 'read', [1], ['name']) >>> data[0]['id'] == 1 True >>> data[0]['name'] == 'YourCompany' True *Python 2:* :return: the result returned by the `method` called :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: the result returned by the `method` called :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib.error.URLError` (connection error) """ self._check_logged_user() # Execute the query args_to_send = [self.env.db, self.env.uid, self._password, model, method] args_to_send.extend(args) data = self.json( '/jsonrpc', {'service': 'object', 'method': 'execute', 'args': args_to_send}) return data.get('result') def execute_kw(self, model, method, args=None, kwargs=None): """Execute the `method` of `model`. `args` is a list of parameters (in the right order), and `kwargs` a dictionary (named parameters). Both varies according to the `method` used. .. doctest:: :options: +SKIP >>> odoo.execute_kw('res.partner', 'read', [[1]], {'fields': ['name']}) [{'id': 1, 'name': 'YourCompany'}] .. doctest:: :hide: >>> data = odoo.execute_kw('res.partner', 'read', [[1]], {'fields': ['name']}) >>> data[0]['id'] == 1 True >>> data[0]['name'] == 'YourCompany' True *Python 2:* :return: the result returned by the `method` called :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: the result returned by the `method` called :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib.error.URLError` (connection error) """ self._check_logged_user() # Execute the query args = args or [] kwargs = kwargs or {} args_to_send = [self.env.db, self.env.uid, self._password, model, method] args_to_send.extend([args, kwargs]) data = self.json( '/jsonrpc', {'service': 'object', 'method': 'execute_kw', 'args': args_to_send}) return data.get('result') def exec_workflow(self, model, record_id, signal): """Execute the workflow `signal` on the instance having the ID `record_id` of `model`. *Python 2:* :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `urllib.error.URLError` (connection error) """ if tools.v(self.version)[0] >= 11: raise DeprecationWarning( u"Workflows have been removed in Odoo >= 11.0") self._check_logged_user() # Execute the workflow query args_to_send = [self.env.db, self.env.uid, self._password, model, signal, record_id] data = self.json( '/jsonrpc', {'service': 'object', 'method': 'exec_workflow', 'args': args_to_send}) return data.get('result') # ---------------------- # # -- Session methods -- # # ---------------------- # def save(self, name, rc_file='~/.odoorpcrc'): """Save the current :class:`ODOO ` instance (a `session`) inside `rc_file` (``~/.odoorpcrc`` by default). This session will be identified by `name`:: >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost', port=8069) >>> odoo.login('db_name', 'admin', 'admin') >>> odoo.save('foo') Use the :func:`list ` class method to list all stored sessions, and the :func:`load ` class method to retrieve an already-connected :class:`ODOO ` instance. *Python 2:* :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `IOError` *Python 3:* :raise: :class:`odoorpc.error.InternalError` (if not logged) :raise: `PermissionError` :raise: `FileNotFoundError` """ self._check_logged_user() data = { 'type': self.__class__.__name__, 'host': self.host, 'protocol': self.protocol, 'port': self.port, 'timeout': self.config['timeout'], 'user': self._login, 'passwd': self._password, 'database': self.env.db, } session.save(name, data, rc_file) @classmethod def load(cls, name, rc_file='~/.odoorpcrc'): """Return a connected :class:`ODOO` session identified by `name`: .. doctest:: :options: +SKIP >>> import odoorpc >>> odoo = odoorpc.ODOO.load('foo') Such sessions are stored with the :func:`save ` method. *Python 2:* :raise: :class:`odoorpc.error.RPCError` :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` :raise: `urllib.error.URLError` (connection error) """ data = session.get(name, rc_file) if data.get('type') != cls.__name__: raise error.InternalError( "'{0}' session is not of type '{1}'".format( name, cls.__name__)) odoo = cls( host=data['host'], protocol=data['protocol'], port=data['port'], timeout=data['timeout'], ) odoo.login( db=data['database'], login=data['user'], password=data['passwd']) return odoo @classmethod def list(cls, rc_file='~/.odoorpcrc'): """Return a list of all stored sessions available in the `rc_file` file: .. doctest:: :options: +SKIP >>> import odoorpc >>> odoorpc.ODOO.list() ['foo', 'bar'] Use the :func:`save ` and :func:`load ` methods to manage such sessions. *Python 2:* :raise: `IOError` *Python 3:* :raise: `PermissionError` :raise: `FileNotFoundError` """ sessions = session.get_all(rc_file) return [name for name in sessions if sessions[name].get('type') == cls.__name__] #return session.list(rc_file) @classmethod def remove(cls, name, rc_file='~/.odoorpcrc'): """Remove the session identified by `name` from the `rc_file` file: .. doctest:: :options: +SKIP >>> import odoorpc >>> odoorpc.ODOO.remove('foo') True *Python 2:* :raise: `ValueError` (if the session does not exist) :raise: `IOError` *Python 3:* :raise: `ValueError` (if the session does not exist) :raise: `PermissionError` :raise: `FileNotFoundError` """ data = session.get(name, rc_file) if data.get('type') != cls.__name__: raise error.InternalError( "'{0}' session is not of type '{1}'".format( name, cls.__name__)) return session.remove(name, rc_file) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tools.py0000644000232200023220000001025113376743447016731 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module contains the :class:`Config ` class which manage the configuration related to an instance of :class:`ODOO `, and some useful helper functions used internally in `OdooRPC`. """ import collections import re MATCH_VERSION = re.compile(r'[^\d.]') class Config(collections.MutableMapping): """Class which manage the configuration of an :class:`ODOO ` instance. .. note:: This class have to be used through the :attr:`odoorpc.ODOO.config` property. >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost') # doctest: +SKIP >>> type(odoo.config) """ def __init__(self, odoo, options): super(Config, self).__init__() self._odoo = odoo self._options = options or {} def __getitem__(self, key): return self._options[key] def __setitem__(self, key, value): """Handle ``timeout`` option to set the timeout on the connector.""" if key == 'timeout': self._odoo._connector.timeout = value self._options[key] = value def __delitem__(self, key): raise InternalError("Operation not allowed") def __iter__(self): return self._options.__iter__() def __len__(self): return len(self._options) def __str__(self): return self._options.__str__() def __repr__(self): return self._options.__repr__() def clean_version(version): """Clean a version string. >>> from odoorpc.tools import clean_version >>> clean_version('7.0alpha-20121206-000102') '7.0' :return: a cleaner version string """ version = MATCH_VERSION.sub('', version.split('-')[0]) return version def v(version): """Convert a version string to a tuple. The tuple can be use to compare versions between them. >>> from odoorpc.tools import v >>> v('7.0') [7, 0] >>> v('6.1') [6, 1] >>> v('7.0') < v('6.1') False :return: the version as tuple """ return [int(x) for x in clean_version(version).split(".")] def get_encodings(hint_encoding='utf-8'): """Used to try different encoding. Function copied from Odoo 11.0 (odoo.loglevels.get_encodings). This piece of code is licensed under the LGPL-v3 and so it is compatible with the LGPL-v3 license of OdooRPC:: - https://github.com/odoo/odoo/blob/11.0/LICENSE - https://github.com/odoo/odoo/blob/11.0/COPYRIGHT """ fallbacks = { 'latin1': 'latin9', 'iso-8859-1': 'iso8859-15', 'cp1252': '1252', } if hint_encoding: yield hint_encoding if hint_encoding.lower() in fallbacks: yield fallbacks[hint_encoding.lower()] # some defaults (also taking care of pure ASCII) for charset in ['utf8', 'latin1', 'ascii']: if not hint_encoding or (charset.lower() != hint_encoding.lower()): yield charset from locale import getpreferredencoding prefenc = getpreferredencoding() if prefenc and prefenc.lower() != 'utf-8': yield prefenc prefenc = fallbacks.get(prefenc.lower()) if prefenc: yield prefenc # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/rpc/0000755000232200023220000000000013376743447016004 5ustar debalancedebalanceodoorpc-0.7.0/odoorpc/rpc/__init__.py0000644000232200023220000002310213376743447020113 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module provides `Connector` classes to communicate with an `Odoo` server with the `JSON-RPC` protocol or through simple HTTP requests. Web controllers of `Odoo` expose two kinds of methods: `json` and `http`. These methods can be accessed from the connectors of this module. """ import sys # Python 2 if sys.version_info[0] < 3: from urllib2 import build_opener, HTTPCookieProcessor from cookielib import CookieJar # Python >= 3 else: from urllib.request import build_opener, HTTPCookieProcessor from http.cookiejar import CookieJar from odoorpc.rpc import error, jsonrpclib from odoorpc.tools import v class Connector(object): """Connector base class defining the interface used to interact with a server. """ def __init__(self, host, port=8069, timeout=120, version=None): self.host = host try: int(port) except ValueError: txt = "The port '{0}' is invalid. An integer is required." txt = txt.format(port) raise error.ConnectorError(txt) else: self.port = int(port) self._timeout = timeout self.version = version @property def ssl(self): """Return `True` if SSL is activated.""" return False @property def timeout(self): """Return the timeout.""" return self._timeout @timeout.setter def timeout(self, timeout): """Set the timeout.""" self._timeout = timeout class ConnectorJSONRPC(Connector): """Connector class using the `JSON-RPC` protocol. .. doctest:: :options: +SKIP >>> from odoorpc import rpc >>> cnt = rpc.ConnectorJSONRPC('localhost', port=8069) .. doctest:: :hide: >>> from odoorpc import rpc >>> cnt = rpc.ConnectorJSONRPC(HOST, port=PORT) Open a user session: .. doctest:: :options: +SKIP >>> cnt.proxy_json.web.session.authenticate(db='db_name', login='admin', password='password') {'id': 51373612, 'jsonrpc': '2.0', 'result': {'company_id': 1, 'currencies': {'1': {'digits': [69, 2], 'position': 'after', 'symbol': '\u20ac'}, '3': {'digits': [69, 2], 'position': 'before', 'symbol': '$'}}, 'db': 'db_name', 'is_admin': True, 'is_system': True, 'name': 'Mitchell Admin', 'partner_display_name': 'YourCompany, Mitchell Admin', 'partner_id': 3, 'server_version': '12.0', 'server_version_info': [12, 0, 0, 'final', 0, ''], 'session_id': '6dd7a34f16c1c67b38bfec413cca4962d5c01d53', 'show_effect': True, 'uid': 2, 'user_companies': False, 'user_context': {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2}, 'username': 'admin', 'web.base.url': 'http://localhost:8069', 'web_tours': []}} .. doctest:: :hide: :options: +NORMALIZE_WHITESPACE >>> from odoorpc.tools import v >>> data = cnt.proxy_json.web.session.authenticate(db=DB, login=USER, password=PWD) >>> keys = ['company_id', 'db', 'session_id', 'uid', 'user_context', 'username'] >>> if v(VERSION) >= v('10.0'): ... keys.extend([ ... 'currencies', 'is_admin', 'is_superuser', 'name', ... 'partner_id', 'server_version', 'server_version_info', ... 'user_companies', 'web.base.url', 'web_tours', ... ]) >>> if v(VERSION) >= v('11.0'): ... keys.extend([ ... 'is_system', ... ]) ... keys.remove('is_admin') >>> if v(VERSION) >= v('12.0'): ... keys.extend([ ... 'partner_display_name', ... 'show_effect', ... ]) ... keys.remove('is_superuser') >>> all([key in data['result'] for key in keys]) True Read data of a partner: .. doctest:: :options: +SKIP >>> cnt.proxy_json.web.dataset.call(model='res.partner', method='read', args=[[1]]) {'jsonrpc': '2.0', 'id': 454236230, 'result': [{'id': 1, 'comment': False, 'ean13': False, 'property_account_position': False, ...}]} .. doctest:: :hide: >>> data = cnt.proxy_json.web.dataset.call(model='res.partner', method='read', args=[[1]]) >>> 'jsonrpc' in data and 'id' in data and 'result' in data True You can send requests this way too: .. doctest:: :options: +SKIP >>> cnt.proxy_json['/web/dataset/call'](model='res.partner', method='read', args=[[1]]) {'jsonrpc': '2.0', 'id': 328686288, 'result': [{'id': 1, 'comment': False, 'ean13': False, 'property_account_position': False, ...}]} .. doctest:: :hide: >>> data = cnt.proxy_json['/web/dataset/call'](model='res.partner', method='read', args=[[1]]) >>> 'jsonrpc' in data and 'id' in data and 'result' in data True Or like this: .. doctest:: :options: +SKIP >>> cnt.proxy_json['web']['dataset']['call'](model='res.partner', method='read', args=[[1]]) {'jsonrpc': '2.0', 'id': 102320639, 'result': [{'id': 1, 'comment': False, 'ean13': False, 'property_account_position': False, ...}]} .. doctest:: :hide: >>> data = cnt.proxy_json['web']['dataset']['call'](model='res.partner', method='read', args=[[1]]) >>> 'jsonrpc' in data and 'id' in data and 'result' in data True """ def __init__(self, host, port=8069, timeout=120, version=None, deserialize=True, opener=None): super(ConnectorJSONRPC, self).__init__(host, port, timeout, version) self.deserialize = deserialize # One URL opener (with cookies handling) shared between # JSON and HTTP requests if opener is None: cookie_jar = CookieJar() opener = build_opener( HTTPCookieProcessor(cookie_jar)) self._opener = opener self._proxy_json, self._proxy_http = self._get_proxies() def _get_proxies(self): """Returns the :class:`ProxyJSON ` and :class:`ProxyHTTP ` instances corresponding to the server version used. """ proxy_json = jsonrpclib.ProxyJSON( self.host, self.port, self._timeout, ssl=self.ssl, deserialize=self.deserialize, opener=self._opener) proxy_http = jsonrpclib.ProxyHTTP( self.host, self.port, self._timeout, ssl=self.ssl, opener=self._opener) # Detect the server version if self.version is None: result = proxy_json('/web/webclient/version_info')['result'] if 'server_version' in result: self.version = result['server_version'] return proxy_json, proxy_http @property def proxy_json(self): """Return the JSON proxy.""" return self._proxy_json @property def proxy_http(self): """Return the HTTP proxy.""" return self._proxy_http @property def timeout(self): """Return the timeout.""" return self._proxy_json._timeout @timeout.setter def timeout(self, timeout): """Set the timeout.""" self._proxy_json._timeout = timeout self._proxy_http._timeout = timeout class ConnectorJSONRPCSSL(ConnectorJSONRPC): """Connector class using the `JSON-RPC` protocol over `SSL`. .. doctest:: :options: +SKIP >>> from odoorpc import rpc >>> cnt = rpc.ConnectorJSONRPCSSL('localhost', port=8069) .. doctest:: :hide: >>> if 'ssl' in PROTOCOL: ... from odoorpc import rpc ... cnt = rpc.ConnectorJSONRPCSSL(HOST, port=PORT) """ def __init__(self, host, port=8069, timeout=120, version=None, deserialize=True, opener=None): super(ConnectorJSONRPCSSL, self).__init__( host, port, timeout, version, opener=opener) self._proxy_json, self._proxy_http = self._get_proxies() @property def ssl(self): return True PROTOCOLS = { 'jsonrpc': ConnectorJSONRPC, 'jsonrpc+ssl': ConnectorJSONRPCSSL, } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/rpc/error.py0000644000232200023220000000230613376743447017510 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## class ConnectorError(BaseException): """Exception raised by the ``odoorpc.rpc`` package.""" def __init__(self, message, odoo_traceback=None): self.message = message self.odoo_traceback = odoo_traceback # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/rpc/jsonrpclib.py0000644000232200023220000001403113376743447020522 0ustar debalancedebalance# -*- coding: utf-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """Provides the :class:`ProxyJSON` class for JSON-RPC requests.""" import copy import json import logging import random import sys # Python 2 if sys.version_info[0] < 3: from urllib2 import build_opener, HTTPCookieProcessor, Request from cookielib import CookieJar def encode_data(data): return data def decode_data(data): return data # Python >= 3 else: from urllib.request import build_opener, HTTPCookieProcessor, Request from http.cookiejar import CookieJar import io def encode_data(data): try: return bytes(data, 'utf-8') except: return bytes(data) def decode_data(data): return io.StringIO(data.read().decode('utf-8')) LOG_HIDDEN_JSON_PARAMS = ['password'] LOG_JSON_SEND_MSG = u"(JSON,send) %(url)s %(data)s" LOG_JSON_RECV_MSG = u"(JSON,recv) %(url)s %(data)s => %(result)s" LOG_HTTP_SEND_MSG = u"(HTTP,send) %(url)s%(data)s" LOG_HTTP_RECV_MSG = u"(HTTP,recv) %(url)s%(data)s => %(result)s" logger = logging.getLogger(__name__) def get_json_log_data(data): """Returns a new `data` dictionary with hidden params for log purpose. """ log_data = data for param in LOG_HIDDEN_JSON_PARAMS: if param in data['params']: if log_data is data: log_data = copy.deepcopy(data) log_data['params'][param] = "**********" return log_data class Proxy(object): """Base class to implement a proxy to perform requests.""" def __init__(self, host, port, timeout=120, ssl=False, opener=None): self._root_url = "{http}{host}:{port}".format( http=(ssl and "https://" or "http://"), host=host, port=port) self._timeout = timeout self._builder = URLBuilder(self) self._opener = opener if not opener: cookie_jar = CookieJar() self._opener = build_opener(HTTPCookieProcessor(cookie_jar)) def __getattr__(self, name): return getattr(self._builder, name) def __getitem__(self, url): return self._builder[url] def _get_full_url(self, url): return '/'.join([self._root_url, url]) class ProxyJSON(Proxy): """The :class:`ProxyJSON` class provides a dynamic access to all JSON methods. """ def __init__(self, host, port, timeout=120, ssl=False, opener=None, deserialize=True): Proxy.__init__(self, host, port, timeout, ssl, opener) self._deserialize = deserialize def __call__(self, url, params=None): if params is None: params = {} data = { "jsonrpc": "2.0", "method": "call", "params": params, "id": random.randint(0, 1000000000), } if url.startswith('/'): url = url[1:] full_url = self._get_full_url(url) log_data = get_json_log_data(data) logger.debug( LOG_JSON_SEND_MSG, {'url': full_url, 'data': log_data}) data_json = json.dumps(data) request = Request(url=full_url, data=encode_data(data_json)) request.add_header('Content-Type', 'application/json') response = self._opener.open(request, timeout=self._timeout) if not self._deserialize: return response result = json.load(decode_data(response)) logger.debug( LOG_JSON_RECV_MSG, {'url': full_url, 'data': log_data, 'result': result}) return result class ProxyHTTP(Proxy): """The :class:`ProxyHTTP` class provides a dynamic access to all HTTP methods. """ def __call__(self, url, data=None, headers=None): if url.startswith('/'): url = url[1:] full_url = self._get_full_url(url) logger.debug( LOG_HTTP_SEND_MSG, {'url': full_url, 'data': data and u" (%s)" % data or u""}) kwargs = { 'url': full_url, } if data: kwargs['data'] = encode_data(data) request = Request(**kwargs) if headers: for hkey in headers: hvalue = headers[hkey] request.add_header(hkey, hvalue) response = self._opener.open(request, timeout=self._timeout) logger.debug( LOG_HTTP_RECV_MSG, {'url': full_url, 'data': data and u" (%s)" % data or u"", 'result': response}) return response class URLBuilder(object): """Auto-builds an URL while getting its attributes. Used by the :class:`ProxyJSON` and :class:`ProxyHTTP` classes. """ def __init__(self, rpc, url=None): self._rpc = rpc self._url = url def __getattr__(self, path): new_url = self._url and '/'.join([self._url, path]) or path return URLBuilder(self._rpc, new_url) def __getitem__(self, path): if path and path[0] == '/': path = path[1:] if path and path[-1] == '/': path = path[:-1] return getattr(self, path) def __call__(self, **kwargs): return self._rpc(self._url, kwargs) def __str__(self): return self._url # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/session.py0000644000232200023220000001441613376743447017263 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """This module contains some helper functions used to save and load sessions in `OdooRPC`. """ import os import stat import sys # Python 2 if sys.version_info[0] < 3: from ConfigParser import SafeConfigParser as ConfigParser # Python >= 3 else: from configparser import ConfigParser def get_all(rc_file='~/.odoorpcrc'): """Return all session configurations from the `rc_file` file. >>> import odoorpc >>> from pprint import pprint as pp >>> pp(odoorpc.session.get_all()) # doctest: +SKIP {'foo': {'database': 'db_name', 'host': 'localhost', 'passwd': 'password', 'port': 8069, 'protocol': 'jsonrpc', 'timeout': 120, 'type': 'ODOO', 'user': 'admin'}, ...} .. doctest:: :hide: >>> import odoorpc >>> session = '%s_session' % DB >>> odoo.save(session) >>> data = odoorpc.session.get_all() >>> data[session]['host'] == HOST True >>> data[session]['protocol'] == PROTOCOL True >>> data[session]['port'] == int(PORT) True >>> data[session]['database'] == DB True >>> data[session]['user'] == USER True >>> data[session]['passwd'] == PWD True >>> data[session]['type'] == 'ODOO' True """ conf = ConfigParser() conf.read([os.path.expanduser(rc_file)]) sessions = {} for name in conf.sections(): sessions[name] = { 'type': conf.get(name, 'type'), 'host': conf.get(name, 'host'), 'protocol': conf.get(name, 'protocol'), 'port': conf.getint(name, 'port'), 'timeout': conf.getfloat(name, 'timeout'), 'user': conf.get(name, 'user'), 'passwd': conf.get(name, 'passwd'), 'database': conf.get(name, 'database'), } return sessions def get(name, rc_file='~/.odoorpcrc'): """Return the session configuration identified by `name` from the `rc_file` file. >>> import odoorpc >>> from pprint import pprint as pp >>> pp(odoorpc.session.get('foo')) # doctest: +SKIP {'database': 'db_name', 'host': 'localhost', 'passwd': 'password', 'port': 8069, 'protocol': 'jsonrpc', 'timeout': 120, 'type': 'ODOO', 'user': 'admin'} .. doctest:: :hide: >>> import odoorpc >>> session = '%s_session' % DB >>> odoo.save(session) >>> data = odoorpc.session.get(session) >>> data['host'] == HOST True >>> data['protocol'] == PROTOCOL True >>> data['port'] == int(PORT) True >>> data['database'] == DB True >>> data['user'] == USER True >>> data['passwd'] == PWD True >>> data['type'] == 'ODOO' True :raise: `ValueError` (wrong session name) """ conf = ConfigParser() conf.read([os.path.expanduser(rc_file)]) if not conf.has_section(name): raise ValueError( "'%s' session does not exist in %s" % (name, rc_file)) return { 'type': conf.get(name, 'type'), 'host': conf.get(name, 'host'), 'protocol': conf.get(name, 'protocol'), 'port': conf.getint(name, 'port'), 'timeout': conf.getfloat(name, 'timeout'), 'user': conf.get(name, 'user'), 'passwd': conf.get(name, 'passwd'), 'database': conf.get(name, 'database'), } def save(name, data, rc_file='~/.odoorpcrc'): """Save the `data` session configuration under the name `name` in the `rc_file` file. >>> import odoorpc >>> odoorpc.session.save( ... 'foo', ... {'type': 'ODOO', 'host': 'localhost', 'protocol': 'jsonrpc', ... 'port': 8069, 'timeout': 120, 'database': 'db_name' ... 'user': 'admin', 'passwd': 'password'}) # doctest: +SKIP .. doctest:: :hide: >>> import odoorpc >>> session = '%s_session' % DB >>> odoorpc.session.save( ... session, ... {'type': 'ODOO', 'host': HOST, 'protocol': PROTOCOL, ... 'port': PORT, 'timeout': 120, 'database': DB, ... 'user': USER, 'passwd': PWD}) """ conf = ConfigParser() conf.read([os.path.expanduser(rc_file)]) if not conf.has_section(name): conf.add_section(name) for key in data: value = data[key] conf.set(name, key, str(value)) with open(os.path.expanduser(rc_file), 'w') as file_: os.chmod(os.path.expanduser(rc_file), stat.S_IREAD | stat.S_IWRITE) conf.write(file_) def remove(name, rc_file='~/.odoorpcrc'): """Remove the session configuration identified by `name` from the `rc_file` file. >>> import odoorpc >>> odoorpc.session.remove('foo') # doctest: +SKIP .. doctest:: :hide: >>> import odoorpc >>> session = '%s_session' % DB >>> odoorpc.session.remove(session) :raise: `ValueError` (wrong session name) """ conf = ConfigParser() conf.read([os.path.expanduser(rc_file)]) if not conf.has_section(name): raise ValueError( "'%s' session does not exist in %s" % (name, rc_file)) conf.remove_section(name) with open(os.path.expanduser(rc_file), 'wb') as file_: conf.write(file_) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/db.py0000644000232200023220000002500713376743447016163 0ustar debalancedebalance# -*- coding: UTF-8 -*- ############################################################################## # # OdooRPC # Copyright (C) 2014 Sébastien Alix. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ############################################################################## """Provide the :class:`DB` class to manage the server databases.""" import base64 import io import sys # Python 2 if sys.version_info[0] < 3: def encode2bytes(data): return data # Python >= 3 else: def encode2bytes(data): return bytes(data, 'ascii') from odoorpc import error from odoorpc.tools import v class DB(object): """The `DB` class represents the database management service. It provides functionalities such as list, create, drop, dump and restore databases. .. note:: This service have to be used through the :attr:`odoorpc.ODOO.db` property. >>> import odoorpc >>> odoo = odoorpc.ODOO('localhost') # doctest: +SKIP >>> odoo.db """ def __init__(self, odoo): self._odoo = odoo def dump(self, password, db, format_='zip'): """Backup the `db` database. Returns the dump as a binary ZIP file containing the SQL dump file alongside the filestore directory (if any). >>> dump = odoo.db.dump('super_admin_passwd', 'prod') # doctest: +SKIP .. doctest:: :hide: >>> dump = odoo.db.dump(SUPER_PWD, DB) If you get a timeout error, increase this one before performing the request: >>> timeout_backup = odoo.config['timeout'] >>> odoo.config['timeout'] = 600 # Timeout set to 10 minutes >>> dump = odoo.db.dump('super_admin_passwd', 'prod') # doctest: +SKIP >>> odoo.config['timeout'] = timeout_backup Write it on the file system: .. doctest:: :options: +SKIP >>> with open('dump.zip', 'wb') as dump_zip: ... dump_zip.write(dump.read()) ... .. doctest:: :hide: >>> with open('dump.zip', 'wb') as dump_zip: ... fileno = dump_zip.write(dump.read()) # Python 3 ... You can manipulate the file with the `zipfile` module for instance: .. doctest:: :options: +SKIP >>> import zipfile >>> zipfile.ZipFile('dump.zip').namelist() ['dump.sql', 'filestore/ef/ef2c882a36dbe90fc1e7e28d816ad1ac1464cfbb', 'filestore/dc/dcf00aacce882bbfd117c0277e514f829b4c5bf0', ...] .. doctest:: :hide: >>> import zipfile >>> zipfile.ZipFile('dump.zip').namelist() # doctest: +NORMALIZE_WHITESPACE ['dump.sql'...'filestore/...'...] The super administrator password is required to perform this method. *Python 2:* :return: `io.BytesIO` :raise: :class:`odoorpc.error.RPCError` (access denied / wrong database) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `io.BytesIO` :raise: :class:`odoorpc.error.RPCError` (access denied / wrong database) :raise: `urllib.error.URLError` (connection error) """ args = [password, db] if v(self._odoo.version)[0] >= 9: args.append(format_) data = self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'dump', 'args': args}) # Encode to bytes forced to be compatible with Python 3.2 # (its 'base64.standard_b64decode()' function only accepts bytes) result = encode2bytes(data['result']) content = base64.standard_b64decode(result) return io.BytesIO(content) def change_password(self, password, new_password): """Change the administrator password by `new_password`. >>> odoo.db.change_password('super_admin_passwd', 'new_admin_passwd') # doctest: +SKIP .. doctest: :hide: >>> odoo.db.change_password(SUPER_PWD, 'new_admin_passwd') >>> odoo.db.change_password('new_admin_passwd', SUPER_PWD) The super administrator password is required to perform this method. *Python 2:* :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib.error.URLError` (connection error) """ self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'change_admin_password', 'args': [password, new_password]}) def create(self, password, db, demo=False, lang='en_US', admin_password='admin'): """Request the server to create a new database named `db` which will have `admin_password` as administrator password and localized with the `lang` parameter. You have to set the flag `demo` to `True` in order to insert demonstration data. >>> odoo.db.create('super_admin_passwd', 'prod', False, 'fr_FR', 'my_admin_passwd') # doctest: +SKIP If you get a timeout error, increase this one before performing the request: >>> timeout_backup = odoo.config['timeout'] >>> odoo.config['timeout'] = 600 # Timeout set to 10 minutes >>> odoo.db.create('super_admin_passwd', 'prod', False, 'fr_FR', 'my_admin_passwd') # doctest: +SKIP >>> odoo.config['timeout'] = timeout_backup The super administrator password is required to perform this method. *Python 2:* :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib.error.URLError` (connection error) """ self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'create_database', 'args': [password, db, demo, lang, admin_password]}) def drop(self, password, db): """Drop the `db` database. Returns `True` if the database was removed, `False` otherwise (database did not exist): >>> odoo.db.drop('super_admin_passwd', 'test') # doctest: +SKIP True The super administrator password is required to perform this method. *Python 2:* :return: `True` or `False` :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `True` or `False` :raise: :class:`odoorpc.error.RPCError` (access denied) :raise: `urllib.error.URLError` (connection error) """ data = self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'drop', 'args': [password, db]}) return data['result'] def duplicate(self, password, db, new_db): """Duplicate `db' as `new_db`. >>> odoo.db.duplicate('super_admin_passwd', 'prod', 'test') # doctest: +SKIP The super administrator password is required to perform this method. *Python 2:* :raise: :class:`odoorpc.error.RPCError` (access denied / wrong database) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` (access denied / wrong database) :raise: `urllib.error.URLError` (connection error) """ self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'duplicate_database', 'args': [password, db, new_db]}) def list(self): """Return the list of the databases: >>> odoo.db.list() # doctest: +SKIP ['prod', 'test'] *Python 2:* :return: `list` of database names :raise: `urllib2.URLError` (connection error) *Python 3:* :return: `list` of database names :raise: `urllib.error.URLError` (connection error) """ data = self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'list', 'args': []}) return data.get('result', []) def restore(self, password, db, dump, copy=False): """Restore the `dump` database into the new `db` database. The `dump` file object can be obtained with the :func:`dump ` method. If `copy` is set to `True`, the restored database will have a new UUID. >>> odoo.db.restore('super_admin_passwd', 'test', dump_file) # doctest: +SKIP If you get a timeout error, increase this one before performing the request: >>> timeout_backup = odoo.config['timeout'] >>> odoo.config['timeout'] = 7200 # Timeout set to 2 hours >>> odoo.db.restore('super_admin_passwd', 'test', dump_file) # doctest: +SKIP >>> odoo.config['timeout'] = timeout_backup The super administrator password is required to perform this method. *Python 2:* :raise: :class:`odoorpc.error.RPCError` (access denied / database already exists) :raise: :class:`odoorpc.error.InternalError` (dump file closed) :raise: `urllib2.URLError` (connection error) *Python 3:* :raise: :class:`odoorpc.error.RPCError` (access denied / database already exists) :raise: :class:`odoorpc.error.InternalError` (dump file closed) :raise: `urllib.error.URLError` (connection error) """ if dump.closed: raise error.InternalError("Dump file closed") b64_data = base64.standard_b64encode(dump.read()).decode() self._odoo.json( '/jsonrpc', {'service': 'db', 'method': 'restore', 'args': [password, db, b64_data, copy]}) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/0000755000232200023220000000000013376743447016362 5ustar debalancedebalanceodoorpc-0.7.0/odoorpc/tests/test_execute_kw.py0000644000232200023220000000600013376743447022132 0ustar debalancedebalance# -*- coding: UTF-8 -*- import numbers import time from odoorpc.tests import LoginTestCase import odoorpc class TestExecuteKw(LoginTestCase): # ------ # Search # ------ def test_execute_kw_search_with_good_args(self): # Check the result returned result = self.odoo.execute_kw('res.users', 'search', [[]], {}) self.assertIsInstance(result, list) self.assertIn(self.user.id, result) result = self.odoo.execute_kw( 'res.users', 'search', [[('id', '=', self.user.id)]], {'order': 'name'}) self.assertIn(self.user.id, result) self.assertEqual(result[0], self.user.id) def test_execute_kw_search_without_args(self): # Handle exception (execute a 'search' without args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'search') def test_execute_kw_search_with_wrong_args(self): # Handle exception (execute a 'search' with wrong args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'search', False, False) # Wrong args def test_execute_kw_search_with_wrong_model(self): # Handle exception (execute a 'search' with a wrong model) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'wrong.model', # Wrong model 'search', [[]], {}) def test_execute_kw_search_with_wrong_method(self): # Handle exception (execute a 'search' with a wrong method) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'wrong_method', # Wrong method [[]], {}) # ------ # Create # ------ def test_execute_kw_create_with_good_args(self): login = "%s_%s" % ("foobar", time.time()) # Check the result returned result = self.odoo.execute_kw( 'res.users', 'create', [{'name': login, 'login': login}]) self.assertIsInstance(result, numbers.Number) # Handle exception (create another user with the same login) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'create', [{'name': login, 'login': login}]) def test_execute_kw_create_without_args(self): # Handle exception (execute a 'create' without args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'create') def test_execute_kw_create_with_wrong_args(self): # Handle exception (execute a 'create' with wrong args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute_kw, 'res.users', 'create', True, True) # Wrong args # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_model.py0000644000232200023220000001372113376743447021077 0ustar debalancedebalance# -*- coding: UTF-8 -*- import time from odoorpc.tests import LoginTestCase from odoorpc import error from odoorpc.models import Model from odoorpc.env import Environment class TestModel(LoginTestCase): def setUp(self): LoginTestCase.setUp(self) self.partner_obj = self.odoo.env['res.partner'] self.p0_id = self.partner_obj.create({'name': "Parent"}) self.p1_id = self.partner_obj.create({'name': "Child 1"}) self.p2_id = self.partner_obj.create({'name': "Child 2"}) self.group_obj = self.odoo.env['res.groups'] self.u0_id = self.user_obj.create( {'name': "TestOdooRPC", 'login': 'test_%s' % time.time()}) self.g1_id = self.group_obj.create({'name': "Group 1"}) self.g2_id = self.group_obj.create({'name': "Group 2"}) def test_create_model_class(self): partner_obj = self.odoo.env['res.partner'] self.assertEqual(partner_obj._name, 'res.partner') self.assertIn('name', partner_obj._columns) self.assertIsInstance(partner_obj.env, Environment) def test_model_browse(self): partner = self.partner_obj.browse(1) self.assertIsInstance(partner, Model) self.assertEqual(partner.id, 1) self.assertEqual(partner.ids, [1]) self.assertEqual(partner.env, self.partner_obj.env) partners = self.partner_obj.browse([1]) self.assertIsInstance(partners, Model) self.assertEqual(partners.id, 1) self.assertEqual(partners.ids, [1]) self.assertEqual(partners.env, self.partner_obj.env) self.assertEqual(partners.ids, partner.ids) def test_model_browse_false(self): partner = self.partner_obj.browse(False) self.assertEqual(len(partner), 0) def test_model_browse_wrong_id(self): self.assertRaises( ValueError, self.partner_obj.browse, 9999999) # Wrong ID self.assertRaises( error.RPCError, self.partner_obj.browse, "1") # Wrong ID type def test_model_browse_without_arg(self): self.assertRaises(TypeError, self.partner_obj.browse) def test_model_rpc_method(self): user_obj = self.odoo.env['res.users'] user_obj.name_get(self.odoo.env.uid) self.odoo.env['ir.sequence'].get('fake.code') # Return False def test_model_rpc_method_error_no_arg(self): # Handle exception (execute a 'name_get' with without args) user_obj = self.odoo.env['res.users'] self.assertRaises( error.RPCError, user_obj.name_get) # No arg def test_model_rpc_method_error_wrong_args(self): # Handle exception (execute a 'search' with wrong args) user_obj = self.odoo.env['res.users'] self.assertRaises( error.RPCError, user_obj.search, False) # Wrong arg def test_model_with_context(self): Product = self.odoo.env['product.product'] product_id = Product.create( {'name': u"Product invisible", 'active': False}) product_ids = Product.search([]) self.assertNotIn(product_id, product_ids) product_ids = Product.with_context(active_test=False).search([]) self.assertIn(product_id, product_ids) # Check that the previous environment has not been impacted product_ids = Product.search([]) self.assertNotIn(product_id, product_ids) def test_record_getitem_field(self): partner = self.partner_obj.browse(1) self.assertEqual(partner['id'], 1) self.assertEqual(partner['name'], partner.name) def test_record_getitem_integer(self): partner = self.partner_obj.browse(1) self.assertEqual(partner[0], partner) def test_record_getitem_slice(self): partner = self.partner_obj.browse(1) self.assertEqual([record.id for record in partner[:]], [1]) def test_record_iter(self): ids = self.partner_obj.search([])[:5] partners = self.partner_obj.browse(ids) self.assertEqual(set([partner.id for partner in partners]), set(ids)) partner = partners[0] self.assertIn(partner.id, partners.ids) self.assertEqual(id(partner._values), id(partners._values)) def test_record_with_context(self): user = self.odoo.env.user self.assertEqual(user.env.lang, 'en_US') user_fr = user.with_context(lang='fr_FR') self.assertEqual(user_fr.env.lang, 'fr_FR') # Install 'fr_FR' and test the use of context with it Wizard = self.odoo.env['base.language.install'] wiz_id = Wizard.create({'lang': 'fr_FR'}) Wizard.lang_install([wiz_id]) # Read data with two languages Country = self.odoo.env['res.country'] de_id = Country.search([('code', '=', 'DE')])[0] de = Country.browse(de_id) self.assertEqual(de.name, 'Germany') self.assertEqual(de.with_context(lang='fr_FR').name, 'Allemagne') # Write data with two languages Product = self.odoo.env['product.product'] self.assertEqual(Product.env.lang, 'en_US') name_en = "Product en_US" product_id = Product.create({'name': name_en}) product_en = Product.browse(product_id) self.assertEqual(product_en.name, name_en) product_fr = product_en.with_context(lang='fr_FR') self.assertEqual(product_fr.env.lang, 'fr_FR') name_fr = "Produit fr_FR" product_fr.write({'name': name_fr}) product_fr = product_fr.with_context() # Refresh the recordset self.assertEqual(product_fr.name, name_fr) self.assertEqual(Product.env.lang, 'en_US') product_en = Product.browse(product_id) self.assertEqual(product_en.name, name_en) new_name_fr = "%s (nouveau)" % name_fr product_fr.name = new_name_fr product_fr = product_fr.with_context() # Refresh the recordset self.assertEqual(product_fr.name, new_name_fr) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_login.py0000644000232200023220000000307413376743447021107 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import BaseTestCase import odoorpc from odoorpc.models import Model class TestLogin(BaseTestCase): def test_login(self): odoo = odoorpc.ODOO( self.env['host'], protocol=self.env['protocol'], port=self.env['port'], version=self.env['version']) odoo.login(self.env['db'], self.env['user'], self.env['pwd']) self.assertIsNotNone(odoo.env) self.assertIsInstance(odoo.env.user, Model) self.assertIn('res.users', odoo.env.registry) self.assertEqual(odoo.env.db, self.env['db']) def test_login_no_password(self): # login no password => Error odoo = odoorpc.ODOO( self.env['host'], protocol=self.env['protocol'], port=self.env['port'], version=self.env['version']) self.assertRaises( odoorpc.error.Error, odoo.login, self.env['db'], self.env['user'], False) def test_logout(self): odoo = odoorpc.ODOO( self.env['host'], protocol=self.env['protocol'], port=self.env['port'], version=self.env['version']) odoo.login(self.env['db'], self.env['user'], self.env['pwd']) success = odoo.logout() self.assertTrue(success) def test_logout_without_login(self): odoo = odoorpc.ODOO( self.env['host'], protocol=self.env['protocol'], port=self.env['port'], version=self.env['version']) success = odoo.logout() self.assertFalse(success) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_workflow.py0000644000232200023220000000375013376743447021652 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase from odoorpc import error, tools class TestWorkflow(LoginTestCase): def setUp(self): LoginTestCase.setUp(self) if tools.v(self.odoo.version)[0] >= 11: # Value doesn't matter for Odoo >= 11, we only test the # DeprecationWarning exception for workflow methods self.so_id = 1 return self.product_obj = self.odoo.env['product.product'] self.partner_obj = self.odoo.env['res.partner'] self.sale_order_obj = self.odoo.env['sale.order'] self.uom_obj = self.odoo.env['product.uom'] self.p_id = self.partner_obj.create({'name': "Child 1"}) prod_vals = { 'name': "PRODUCT TEST WORKFLOW", } self.product_id = self.product_obj.create(prod_vals) sol_vals = { 'name': "TEST WORKFLOW", 'product_id': self.product_id, 'product_uom': self.uom_obj.search([])[0], } so_vals = { 'partner_id': self.p_id, 'order_line': [(0, 0, sol_vals)], } self.so_id = self.sale_order_obj.create(so_vals) def test_exec_workflow(self): if tools.v(self.odoo.version)[0] >= 11: self.assertRaises( DeprecationWarning, self.odoo.exec_workflow, 'sale.order', self.so_id, 'order_confirm') return self.odoo.exec_workflow('sale.order', self.so_id, 'order_confirm') def test_exec_workflow_wrong_model(self): if tools.v(self.odoo.version)[0] >= 11: self.assertRaises( DeprecationWarning, self.odoo.exec_workflow, 'sale.order2', self.so_id, 'order_confirm') return self.assertRaises( error.RPCError, self.odoo.exec_workflow, 'sale.order2', self.so_id, 'order_confirm') # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_integer.py0000644000232200023220000000247013376743447022576 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase class TestFieldInteger(LoginTestCase): def test_field_integer_read(self): self.assertIsInstance(self.user.id, int) def test_field_integer_write(self): cron_obj = self.odoo.env['ir.cron'] cron = cron_obj.browse(cron_obj.search([])[0]) backup = cron.priority # False cron.priority = False data = cron.read(['priority'])[0] self.assertEqual(data['priority'], 0) self.assertEqual(cron.priority, 0) # None cron.priority = None data = cron.read(['priority'])[0] self.assertEqual(data['priority'], 0) self.assertEqual(cron.priority, 0) # 0 cron.priority = 0 data = cron.read(['priority'])[0] self.assertEqual(data['priority'], 0) self.assertEqual(cron.priority, 0) # 100 cron.priority = 100 data = cron.read(['priority'])[0] self.assertEqual(data['priority'], 100) self.assertEqual(cron.priority, 100) # Restore original value cron.priority = backup data = cron.read(['priority'])[0] self.assertEqual(data['priority'], backup) self.assertEqual(cron.priority, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_req_json.py0000644000232200023220000000125113376743447021612 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import BaseTestCase class TestReqJSON(BaseTestCase): def _req_json(self, url): data = self.odoo.json( url, {'db': self.env['db'], 'login': self.env['user'], 'password': self.env['pwd']}) self.assertEqual(data['result']['db'], self.env['db']) self.assertTrue(data['result']['uid']) self.assertEqual(data['result']['username'], self.env['user']) def test_req_json_with_leading_slash(self): self._req_json('/web/session/authenticate') def test_req_json_without_leading_slash(self): self._req_json('web/session/authenticate') odoorpc-0.7.0/odoorpc/tests/__init__.py0000644000232200023220000000472513376743447020503 0ustar debalancedebalance# -*- coding: UTF-8 -*- try: import unittest2 as unittest except: import unittest import os import odoorpc class BaseTestCase(unittest.TestCase): """Instanciates an ``odoorpc.ODOO`` object, nothing more.""" def setUp(self): try: port = int(os.environ.get('ORPC_TEST_PORT', 8069)) except ValueError: raise ValueError("The port must be an integer") self.env = { 'protocol': os.environ.get('ORPC_TEST_PROTOCOL', 'jsonrpc'), 'host': os.environ.get('ORPC_TEST_HOST', 'localhost'), 'port': port, 'db': os.environ.get('ORPC_TEST_DB', 'odoorpc_test'), 'user': os.environ.get('ORPC_TEST_USER', 'admin'), 'pwd': os.environ.get('ORPC_TEST_PWD', 'admin'), 'version': os.environ.get('ORPC_TEST_VERSION', None), 'super_pwd': os.environ.get('ORPC_TEST_SUPER_PWD', 'admin'), } self.odoo = odoorpc.ODOO( self.env['host'], protocol=self.env['protocol'], port=self.env['port'], version=self.env['version']) # Create the database default_timeout = self.odoo.config['timeout'] self.odoo.config['timeout'] = 600 if self.env['db'] not in self.odoo.db.list(): self.odoo.db.create( self.env['super_pwd'], self.env['db'], True) self.odoo.config['timeout'] = default_timeout class LoginTestCase(BaseTestCase): """Instanciates an ``odoorpc.ODOO`` object and perform the user login.""" def setUp(self): BaseTestCase.setUp(self) default_timeout = self.odoo.config['timeout'] self.odoo.login(self.env['db'], self.env['user'], self.env['pwd']) # Install 'sale' + 'crm_claim' on Odoo < 10.0, # and 'sale' + 'subscription' on Odoo >= 10.0 self.odoo.config['timeout'] = 600 module_obj = self.odoo.env['ir.module.module'] modules = ['sale', 'crm_claim'] if self.odoo.version == '10.0': modules = ['sale', 'subscription'] module_ids = module_obj.search([('name', 'in', modules)]) module_obj.button_immediate_install(module_ids) self.odoo.config['timeout'] = default_timeout # Get user record and model after the installation of modules # to get all available fields (avoiding test failures) self.user = self.odoo.env.user self.user_obj = self.odoo.env['res.users'] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_execute.py0000644000232200023220000000564413376743447021446 0ustar debalancedebalance# -*- coding: UTF-8 -*- import numbers import time from odoorpc.tests import LoginTestCase import odoorpc class TestExecute(LoginTestCase): # ------ # Search # ------ def test_execute_search_with_good_args(self): # Check the result returned result = self.odoo.execute('res.users', 'search', []) self.assertIsInstance(result, list) self.assertIn(self.user.id, result) result = self.odoo.execute('res.users', 'search', [('id', '=', self.user.id)]) self.assertIn(self.user.id, result) self.assertEqual(result[0], self.user.id) def test_execute_search_without_args(self): # Handle exception (execute a 'search' without args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'search') def test_execute_search_with_wrong_args(self): # Handle exception (execute a 'search' with wrong args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'search', False) # Wrong arg def test_execute_search_with_wrong_model(self): # Handle exception (execute a 'search' with a wrong model) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'wrong.model', # Wrong model 'search', []) def test_execute_search_with_wrong_method(self): # Handle exception (execute a 'search' with a wrong method) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'wrong_method', # Wrong method []) # ------ # Create # ------ def test_execute_create_with_good_args(self): login = "%s_%s" % ("foobar", time.time()) # Check the result returned result = self.odoo.execute( 'res.users', 'create', {'name': login, 'login': login}) self.assertIsInstance(result, numbers.Number) # Handle exception (create another user with the same login) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'create', {'name': login, 'login': login}) def test_execute_create_without_args(self): # Handle exception (execute a 'create' without args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'create') def test_execute_create_with_wrong_args(self): # Handle exception (execute a 'create' with wrong args) self.assertRaises( odoorpc.error.RPCError, self.odoo.execute, 'res.users', 'create', False) # Wrong arg # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_report.py0000644000232200023220000000271313376743447021311 0ustar debalancedebalance# -*- coding: UTF-8 -*- import tempfile from odoorpc.tests import LoginTestCase from odoorpc import error from odoorpc.tools import v class TestReport(LoginTestCase): def test_report_download_pdf(self): model = 'res.company' report_name = 'web.preview_internalreport' if v(self.odoo.version)[0] < 11: report_name = 'preview.report' ids = self.odoo.env[model].search([])[:20] report = self.odoo.report.download(report_name, ids) with tempfile.TemporaryFile(mode='wb', suffix='.pdf') as file_: file_.write(report.read()) def test_report_download_qweb_pdf(self): model = 'account.invoice' report_name = 'account.report_invoice' ids = self.odoo.env[model].search([])[:10] report = self.odoo.report.download(report_name, ids) with tempfile.TemporaryFile(mode='wb', suffix='.pdf') as file_: file_.write(report.read()) def test_report_download_wrong_report_name(self): self.assertRaises( ValueError, self.odoo.report.download, 'wrong_report', [1]) def test_report_list(self): res = self.odoo.report.list() self.assertIsInstance(res, dict) self.assertIn('account.invoice', res) self.assertTrue( any('account.report_invoice' in data['report_name'] for data in res['account.invoice'])) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_binary.py0000644000232200023220000000264613376743447022432 0ustar debalancedebalance# -*- coding: UTF-8 -*- import base64 from odoorpc.tests import LoginTestCase class TestFieldBinary(LoginTestCase): def test_field_binary_read(self): img = self.user.image base64.b64decode(img.encode('ascii')) def test_field_binary_write(self): backup = self.user.image jpeg_file = ( b"\xff\xd8\xff\xdb\x00\x43\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02" b"\x02\x03\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05" b"\x06\x09\x08\x0a\x0a\x09\x08\x09\x09\x0a\x0c\x0f\x0c\x0a\x0b\x0e" b"\x0b\x09\x09\x0d\x11\x0d\x0e\x0f\x10\x10\x11\x10\x0a\x0c\x12\x13" b"\x12\x10\x13\x0f\x10\x10\x10\xff\xc9\x00\x0b\x08\x00\x01\x00\x01" b"\x01\x01\x11\x00\xff\xcc\x00\x06\x00\x10\x10\x05\xff\xda\x00\x08" b"\x01\x01\x00\x00\x3f\x00\xd2\xcf\x20\xff\xd9" ) # https://github.com/mathiasbynens/small/blob/master/jpeg.jpg self.user.image = base64.b64encode(jpeg_file).decode('ascii') data = self.user.read(['image'])[0] decoded = base64.b64decode(data['image'].encode('ascii')) self.assertEqual(decoded, jpeg_file) # Restore original value self.user.image = backup data = self.user.read(['image'])[0] self.assertEqual(data['image'], backup) self.assertEqual(self.user.image, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_init.py0000644000232200023220000000545713376743447020751 0ustar debalancedebalance# -*- coding: UTF-8 -*- import sys # Python 2 if sys.version_info.major < 3: from urllib2 import BaseHandler, URLError, build_opener # Python >= 3 else: from urllib.error import URLError from urllib.request import BaseHandler, build_opener from odoorpc.tests import BaseTestCase import odoorpc class TestInit(BaseTestCase): def test_init1(self): # Host + Protocol + Port odoo = odoorpc.ODOO( self.env['host'], self.env['protocol'], self.env['port']) self.assertIsInstance(odoo, odoorpc.ODOO) self.assertIsNotNone(odoo) self.assertEqual(odoo.host, self.env['host']) self.assertEqual(odoo.protocol, self.env['protocol']) self.assertEqual(odoo.port, self.env['port']) def test_init2(self): # Host + Protocol + Port + Timeout odoo = odoorpc.ODOO( self.env['host'], self.env['protocol'], self.env['port'], 42) self.assertIsInstance(odoo, odoorpc.ODOO) self.assertIsNotNone(odoo) self.assertEqual(odoo.host, self.env['host']) self.assertEqual(odoo.protocol, self.env['protocol']) self.assertEqual(odoo.port, self.env['port']) self.assertEqual(odoo.config['timeout'], 42) def test_init_opener(self): # Opener opener = build_opener(BaseHandler) odoo = odoorpc.ODOO( self.env['host'], self.env['protocol'], self.env['port'], opener=opener) connector = odoo._connector self.assertIs(connector._opener, opener) self.assertIs(connector._proxy_http._opener, opener) self.assertIs(connector._proxy_json._opener, opener) def test_init_timeout_none(self): odoo = odoorpc.ODOO( self.env['host'], self.env['protocol'], self.env['port'], None) self.assertIs(odoo.config['timeout'], None) def test_init_timeout_float(self): odoo = odoorpc.ODOO( self.env['host'], self.env['protocol'], self.env['port'], 23.42) self.assertEqual(odoo.config['timeout'], 23.42) def test_init_wrong_protocol(self): self.assertRaises( ValueError, odoorpc.ODOO, self.env['host'], "wrong", self.env['port']) def test_init_wrong_port(self): self.assertRaises( URLError, odoorpc.ODOO, self.env['host'], self.env['protocol'], 65000) def test_init_wrong_port_as_string(self): self.assertRaises( ValueError, odoorpc.ODOO, self.env['host'], self.env['protocol'], "wrong") def test_init_wrong_timeout_as_string(self): self.assertRaises( ValueError, odoorpc.ODOO, self.env['host'], self.env['protocol'], self.env['port'], "wrong") # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_session.py0000644000232200023220000000577613376743447021475 0ustar debalancedebalance# -*- coding: UTF-8 -*- import tempfile import os from odoorpc.tests import LoginTestCase import odoorpc class TestSession(LoginTestCase): def setUp(self): LoginTestCase.setUp(self) self.session_name = self.env['db'] self.file_path = tempfile.mkstemp(suffix='.cfg', prefix='odoorpc_')[1] def tearDown(self): os.remove(self.file_path) def test_session_odoo_list(self): result = odoorpc.ODOO.list(rc_file=self.file_path) self.assertIsInstance(result, list) other_file_path = tempfile.mkstemp()[1] result = odoorpc.ODOO.list(rc_file=other_file_path) self.assertIsInstance(result, list) def test_session_odoo_save_and_remove(self): self.odoo.save(self.session_name, rc_file=self.file_path) result = odoorpc.ODOO.list(rc_file=self.file_path) self.assertIn(self.session_name, result) odoorpc.ODOO.remove(self.session_name, rc_file=self.file_path) def test_session_odoo_load(self): self.odoo.save(self.session_name, rc_file=self.file_path) odoo = odoorpc.ODOO.load(self.session_name, rc_file=self.file_path) self.assertIsInstance(odoo, odoorpc.ODOO) self.assertEqual(self.odoo.host, odoo.host) self.assertEqual(self.odoo.port, odoo.port) self.assertEqual(self.odoo.protocol, odoo.protocol) self.assertEqual(self.odoo.env.db, odoo.env.db) self.assertEqual(self.odoo.env.uid, odoo.env.uid) odoorpc.ODOO.remove(self.session_name, rc_file=self.file_path) def test_session_get(self): self.odoo.save(self.session_name, rc_file=self.file_path) data = { 'type': self.odoo.__class__.__name__, 'host': self.odoo.host, 'protocol': self.odoo.protocol, 'port': int(self.odoo.port), 'timeout': self.odoo.config['timeout'], 'user': self.odoo._login, 'passwd': self.odoo._password, 'database': self.odoo.env.db, } result = odoorpc.session.get( self.session_name, rc_file=self.file_path) self.assertEqual(data, result) odoorpc.ODOO.remove(self.session_name, rc_file=self.file_path) def test_session_get_all(self): self.odoo.save(self.session_name, rc_file=self.file_path) data = { self.session_name: { 'type': self.odoo.__class__.__name__, 'host': self.odoo.host, 'protocol': self.odoo.protocol, 'port': int(self.odoo.port), 'timeout': self.odoo.config['timeout'], 'user': self.odoo._login, 'passwd': self.odoo._password, 'database': self.odoo.env.db, } } result = odoorpc.session.get_all(rc_file=self.file_path) self.assertIn(self.session_name, result) self.assertEqual(data, result) odoorpc.ODOO.remove(self.session_name, rc_file=self.file_path) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_db.py0000644000232200023220000001210613376743447020360 0ustar debalancedebalance# -*- coding: UTF-8 -*- from datetime import datetime import zipfile from odoorpc.tests import BaseTestCase import odoorpc class TestDB(BaseTestCase): def setUp(self): BaseTestCase.setUp(self) self.odoo.logout() self.databases = [] # Keep databases created during tests def test_db_dump(self): dump = self.odoo.db.dump(self.env['super_pwd'], self.env['db']) self.assertIn('dump.sql', zipfile.ZipFile(dump).namelist()) def test_db_dump_wrong_database(self): self.assertRaises( odoorpc.error.RPCError, self.odoo.db.dump, self.env['super_pwd'], 'wrong_db') def test_db_dump_wrong_password(self): self.assertRaises( odoorpc.error.RPCError, self.odoo.db.dump, 'wrong_password', self.env['db']) def test_db_create(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.odoo.db.create(self.env['super_pwd'], new_database) def test_db_create_existing_database(self): self.assertRaises( odoorpc.error.RPCError, self.odoo.db.create, self.env['super_pwd'], self.env['db']) def test_db_create_wrong_password(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.create, 'wrong_password', new_database) def test_db_drop(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.odoo.db.duplicate( self.env['super_pwd'], self.env['db'], new_database) res = self.odoo.db.drop(self.env['super_pwd'], new_database) self.assertTrue(res) def test_db_drop_wrong_database(self): res = self.odoo.db.drop(self.env['super_pwd'], 'wrong_database') self.assertFalse(res) def test_db_drop_wrong_password(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.odoo.db.duplicate( self.env['super_pwd'], self.env['db'], new_database) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.drop, 'wrong_password', new_database) def test_db_duplicate(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.odoo.db.duplicate( self.env['super_pwd'], self.env['db'], new_database) def test_db_duplicate_wrong_database(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.duplicate, self.env['super_pwd'], 'wrong_database', new_database) def test_db_duplicate_wrong_password(self): date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.duplicate, 'wrong_password', self.env['db'], new_database) def test_db_list(self): res = self.odoo.db.list() self.assertIsInstance(res, list) self.assertIn(self.env['db'], res) def test_db_restore_new_database(self): dump = self.odoo.db.dump(self.env['super_pwd'], self.env['db']) date = datetime.strftime(datetime.today(), '%Y-%m-%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.odoo.db.restore( self.env['super_pwd'], new_database, dump) def test_db_restore_existing_database(self): dump = self.odoo.db.dump(self.env['super_pwd'], self.env['db']) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.restore, self.env['super_pwd'], self.env['db'], dump) def test_db_restore_wrong_password(self): dump = self.odoo.db.dump(self.env['super_pwd'], self.env['db']) date = datetime.strftime(datetime.today(), '%Y%m%d_%Hh%Mm%S') new_database = "%s_%s" % (self.env['db'], date) self.databases.append(new_database) self.assertRaises( odoorpc.error.RPCError, self.odoo.db.restore, 'wrong_password', new_database, dump) def tearDown(self): """Clean up databases created during tests.""" for db in self.databases: try: self.odoo.db.drop(self.env['super_pwd'], db) except: pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_tools.py0000644000232200023220000000231113376743447021130 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import BaseTestCase from odoorpc import tools class TestTools(BaseTestCase): def test_clean_version_numeric(self): version = tools.clean_version('6.1') self.assertEqual(version, '6.1') def test_clean_version_alphanumeric(self): version = tools.clean_version('7.0alpha-20121206-000102') self.assertEqual(version, '7.0') def test_v_numeric(self): self.assertEqual(tools.v('7.0'), [7, 0]) def test_v_alphanumeric(self): self.assertEqual(tools.v('7.0alpha'), [7, 0]) def test_v_cmp(self): # [(v1, v2, is_inferior), ...] versions = [ ('7.0', '6.1', False), ('6.1', '7.0', True), ('7.0alpha', '6.1', False), ('6.1beta', '7.0', True), ('6.1beta', '5.0.16', False), ('5.0.16alpha', '6.1', True), ('8.0dev-20131102-000101', '7.0-20131014-231047', False), ] for v1, v2, is_inferior in versions: result = tools.v(v1) < tools.v(v2) if is_inferior: self.assertTrue(result) else: self.assertFalse(result) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_char.py0000644000232200023220000000226213376743447022055 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase class TestFieldChar(LoginTestCase): def test_field_char_read(self): self.assertEqual(self.user.login, self.env['user']) def test_field_char_write(self): # TODO: split in several unit tests partner = self.user.partner_id backup = partner.street # "A street" partner.street = "A street" data = partner.read(['street'])[0] self.assertEqual(data['street'], "A street") self.assertEqual(partner.street, "A street") # False partner.street = False data = partner.read(['street'])[0] self.assertEqual(data['street'], False) self.assertEqual(partner.street, False) # None partner.street = None data = partner.read(['street'])[0] self.assertEqual(data['street'], False) self.assertEqual(partner.street, False) # Restore original value partner.street = backup data = partner.read(['street'])[0] self.assertEqual(data['street'], backup) self.assertEqual(partner.street, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_many2one.py0000644000232200023220000000265413376743447022675 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase from odoorpc.models import Model class TestFieldMany2one(LoginTestCase): def test_field_many2one_read(self): User = self.odoo.env['res.users'] user = User.browse(1) company = user.company_id self.assertIsInstance(company, Model) self.assertEqual(company.id, 1) self.assertEqual(company.ids, [1]) # The environment object can be different as the field could have a # custom context defined, so we just check 'db' and 'uid' self.assertEqual(company.env.db, user.env.db) self.assertEqual(company.env.uid, user.env.uid) # Test if empty field returns an empty recordset, and not False title = user.title self.assertIsInstance(title, Model) self.assertEqual(title.id, None) self.assertFalse(bool(title)) def test_field_many2one_write(self): self.user.action_id = 1 self.assertEqual(self.user.action_id.id, 1) action = self.odoo.env['ir.actions.actions'].browse(1) self.user.action_id = action self.assertEqual(self.user.action_id.id, 1) # False self.user.action_id = False self.assertIsInstance(self.user.action_id, Model) self.assertEqual(self.user.action_id.id, None) self.assertFalse(bool(self.user.action_id)) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_env.py0000644000232200023220000000562413376743447020572 0ustar debalancedebalance# -*- coding: UTF-8 -*- import time from odoorpc.tests import LoginTestCase from odoorpc.models import Model from odoorpc.env import Environment class TestEnvironment(LoginTestCase): def test_env_init(self): self.assertIsInstance(self.odoo.env, Environment) def test_env_context(self): self.assertIn('lang', self.odoo.env.context) self.assertIn('tz', self.odoo.env.context) self.assertIn('uid', self.odoo.env.context) def test_env_lang(self): self.assertEqual(self.odoo.env.lang, self.odoo.env.context.get('lang')) def test_env_db(self): self.assertEqual(self.odoo.env.db, self.env['db']) def test_env_user(self): self.assertEqual(self.odoo.env.user.login, self.env['user']) def test_env_dirty(self): self.odoo.config['auto_commit'] = False def test_record_garbarge_collected(): user_ids = self.odoo.env['res.users'].search([('id', '!=', 1)]) user = self.user_obj.browse(user_ids[0]) self.assertNotIn(user, self.odoo.env.dirty) self.assertNotIn(user, user.env.dirty) user.name = "Joe" self.assertIn(user, self.odoo.env.dirty) self.assertIn(user, user.env.dirty) test_record_garbarge_collected() # Ensure the record has been garbage collected for the next test import gc gc.collect() self.assertEqual(list(self.odoo.env.dirty), []) def test_env_registry(self): self.odoo.env['res.partner'] self.assertIn('res.partner', self.odoo.env.registry) del self.odoo.env.registry['res.partner'] self.assertNotIn('res.partner', self.odoo.env.registry) self.user.partner_id self.assertIn('res.partner', self.odoo.env.registry) def test_env_commit(self): # We test with 'auto_commit' deactivated since the commit is implicit # by default and sufficiently tested in the 'test_field_*' modules. self.odoo.config['auto_commit'] = False user_id = self.user_obj.create( {'name': "TestCommit", 'login': "test_commit_%s" % time.time()}) user = self.user_obj.browse(user_id) self.assertNotIn(user, self.odoo.env.dirty) user.name = "Bob" self.assertIn(user, self.odoo.env.dirty) self.odoo.env.commit() data = user.read(['name'])[0] self.assertEqual(data['name'], "Bob") self.assertEqual(user.name, "Bob") self.assertNotIn(user, self.odoo.env.dirty) def test_env_ref(self): record = self.odoo.env.ref('base.lang_en') self.assertIsInstance(record, Model) self.assertEqual(record._name, 'res.lang') self.assertEqual(record.code, 'en_US') def test_env_contains(self): self.assertIn('res.partner', self.odoo.env) self.assertNotIn('does.not.exist', self.odoo.env) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_text.py0000644000232200023220000000146213376743447022125 0ustar debalancedebalance# -*- coding: UTF-8 -*- import sys from odoorpc.tests import LoginTestCase # Python 2 if sys.version_info.major < 3: def is_string(arg): return isinstance(arg, str) or isinstance(arg, unicode) # Python >= 3 else: def is_string(arg): return isinstance(arg, str) class TestFieldText(LoginTestCase): def test_field_text_read(self): # Test empty field self.assertFalse(self.user.comment) # Test field containing a value Module = self.odoo.env['ir.module.module'] sale_id = Module.search([('name', '=', 'sale')]) sale_mod = Module.browse(sale_id) self.assertTrue(is_string(sale_mod.views_by_module)) def test_field_text_write(self): # TODO pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_one2many.py0000644000232200023220000002504213376743447022671 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase from odoorpc.models import Model class TestFieldOne2many(LoginTestCase): def setUp(self): LoginTestCase.setUp(self) self.partner_obj = self.odoo.env['res.partner'] self.p0_id = self.partner_obj.create({'name': "Parent"}) self.p1_id = self.partner_obj.create({'name': "Child 1"}) self.p2_id = self.partner_obj.create({'name': "Child 2"}) def test_field_one2many_read(self): # Test if empty field returns an empty recordset, and not False self.assertIsInstance(self.user.child_ids, Model) self.assertEqual(self.user.child_ids.ids, []) def test_field_one2many_write_set_false(self): partner = self.partner_obj.browse(self.p0_id) # = False partner.child_ids = False data = partner.read(['child_ids'])[0] self.assertEqual(data['child_ids'], []) self.assertEqual(list(partner.child_ids), []) def test_field_one2many_write_set_empty_list(self): partner = self.partner_obj.browse(self.p0_id) # = [] partner.child_ids = [] data = partner.read(['child_ids'])[0] self.assertEqual(data['child_ids'], []) self.assertEqual(list(partner.child_ids), []) def test_field_one2many_write_set_magic_tuples(self): partner = self.partner_obj.browse(self.p0_id) # = [(6, 0, IDS)] data = partner.read(['child_ids'])[0] self.assertEqual(data['child_ids'], []) partner.child_ids = [(6, 0, [self.p1_id])] data = partner.read(['child_ids'])[0] self.assertEqual(data['child_ids'], [self.p1_id]) partner_ids = [acc.id for acc in partner.child_ids] self.assertEqual(partner_ids, [self.p1_id]) def test_field_one2many_write_iadd_id(self): partner = self.partner_obj.browse(self.p0_id) # += ID data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += self.p1_id partner.child_ids += self.p2_id data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) self.assertIn(self.p1_id, [pt.id for pt in partner.child_ids]) self.assertIn(self.p2_id, [pt.id for pt in partner.child_ids]) self.assertEqual(len(partner.child_ids), 2) def test_field_one2many_write_iadd_record(self): partner = self.partner_obj.browse(self.p0_id) # += Record data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += self.partner_obj.browse(self.p2_id) data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertIn(self.p2_id, partner_ids) self.assertEqual(len(partner.child_ids), 1) def test_field_one2many_write_iadd_recordset(self): partner = self.partner_obj.browse(self.p0_id) # += Recordset data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += self.partner_obj.browse([self.p1_id, self.p2_id]) data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertIn(self.p1_id, partner_ids) self.assertIn(self.p2_id, partner_ids) self.assertEqual(len(partner.child_ids), 2) def test_field_one2many_write_iadd_list_ids(self): partner = self.partner_obj.browse(self.p0_id) # += List of IDs data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += [self.p1_id, self.p2_id] data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertIn(self.p1_id, partner_ids) self.assertIn(self.p2_id, partner_ids) self.assertEqual(len(partner.child_ids), 2) def test_field_one2many_write_iadd_list_records(self): partner = self.partner_obj.browse(self.p0_id) # += List of records data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += [self.partner_obj.browse(self.p1_id), self.partner_obj.browse(self.p2_id)] data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertIn(self.p1_id, partner_ids) self.assertIn(self.p2_id, partner_ids) self.assertEqual(len(partner.child_ids), 2) def test_field_one2many_write_iadd_id_and_list_ids(self): partner = self.partner_obj.browse(self.p0_id) # += ID and += [ID] data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner.child_ids += self.p1_id partner.child_ids += [self.p2_id] data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertIn(self.p1_id, partner_ids) self.assertIn(self.p2_id, partner_ids) self.assertEqual(len(partner.child_ids), 2) def test_field_one2many_write_isub_id(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] # -= ID data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= self.p1_id partner.child_ids -= self.p2_id data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) def test_field_one2many_write_isub_record(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] # -= Record data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= self.partner_obj.browse(self.p1_id) partner.child_ids -= self.partner_obj.browse(self.p2_id) data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) def test_field_one2many_write_isub_recordset(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] # -= Recordset data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= self.partner_obj.browse([self.p1_id, self.p2_id]) data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) def test_field_one2many_write_isub_list_ids(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] childs = self.partner_obj.browse([self.p1_id, self.p2_id]) # -= List of IDs data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= childs.ids data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) def test_field_one2many_write_isub_list_records(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] childs = [self.partner_obj.browse(self.p1_id), self.partner_obj.browse(self.p2_id)] # -= List of records data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= childs data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) def test_field_one2many_write_isub_id_and_list_ids(self): partner = self.partner_obj.browse(self.p0_id) partner.child_ids = [self.p1_id, self.p2_id] # -= ID and -= [ID] data = partner.read(['child_ids'])[0] self.assertIn(self.p1_id, data['child_ids']) self.assertIn(self.p2_id, data['child_ids']) partner.child_ids -= self.p1_id partner.child_ids -= [self.p2_id] data = partner.read(['child_ids'])[0] self.assertNotIn(self.p1_id, data['child_ids']) self.assertNotIn(self.p2_id, data['child_ids']) partner_ids = [pt.id for pt in partner.child_ids] self.assertNotIn(self.p1_id, partner_ids) self.assertNotIn(self.p2_id, partner_ids) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_error.py0000644000232200023220000000103613376743447021124 0ustar debalancedebalance# -*- coding: UTF-8 -*- try: import unittest2 as unittest except ImportError: import unittest import sys from odoorpc.error import RPCError class TestError(unittest.TestCase): def test_rpcerror_unicode_message(self): message = u"é" exc = RPCError(message) str(exc) if sys.version_info[0] < 3: unicode(exc) def test_rpcerror_str_message(self): message = "é" exc = RPCError(message) str(exc) if sys.version_info[0] < 3: unicode(exc) odoorpc-0.7.0/odoorpc/tests/test_field_reference.py0000644000232200023220000000472213376743447023101 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase from odoorpc.models import Model from odoorpc.tools import v class TestFieldReference(LoginTestCase): def test_field_reference_read(self): # 8.0 and 9.0 if v(self.odoo.version) < v('10'): Claim = self.odoo.env['crm.claim'] claim_id = Claim.search([])[0] # Test field containing a value self.odoo.execute( 'crm.claim', 'write', [claim_id], {'ref': 'res.partner,1'}) claim = Claim.browse(claim_id) self.assertIsInstance(claim.ref, Model) self.assertEqual(claim.ref._name, 'res.partner') self.assertEqual(claim.ref.id, 1) # Test if empty field returns False (unable to guess the model to use) self.odoo.execute( 'crm.claim', 'write', [claim_id], {'ref': None}) claim = Claim.browse(claim_id) self.assertEqual(claim.ref, False) # 10.0 elif v(self.odoo.version) < v('11'): Subscription = self.odoo.env['subscription.subscription'] fields_list = list(Subscription.fields_get([])) vals = Subscription.default_get(fields_list) vals['name'] = "ODOORPC TEST (fields.Reference)" vals['doc_source'] = 'res.partner,1' subscription_id = Subscription.create(vals) # Test field containing a value subscription = Subscription.browse(subscription_id) self.assertIsInstance(subscription.doc_source, Model) self.assertEqual(subscription.doc_source._name, 'res.partner') self.assertEqual(subscription.doc_source.id, 1) # 11.0 else: Menu = self.odoo.env['ir.ui.menu'] fields_list = list(Menu.fields_get([])) vals = Menu.default_get(fields_list) vals['name'] = "ODOORPC TEST (fields.Reference)" action = self.odoo.env.ref('base.action_partner_form') vals['action'] = '%s,%s' % (action._name, action.id) menu_id = Menu.create(vals) # Test field containing a value menu = Menu.browse(menu_id) self.assertIsInstance(menu.action, Model) self.assertEqual(menu.action._name, action._name) self.assertEqual(menu.action.id, action.id) def test_field_reference_write(self): # TODO pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_req_http.py0000644000232200023220000000067613376743447021632 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import BaseTestCase class TestReqHTTP(BaseTestCase): def _req_http(self, url): response = self.odoo.http(url) binary_data = response.read() self.assertTrue(binary_data) def test_req_http_with_leading_slash(self): self._req_http('/web/binary/company_logo') def test_req_http_without_leading_slash(self): self._req_http('web/binary/company_logo') odoorpc-0.7.0/odoorpc/tests/test_field_datetime.py0000644000232200023220000000117713376743447022740 0ustar debalancedebalance# -*- coding: UTF-8 -*- import datetime from odoorpc.tests import LoginTestCase class TestFieldDatetime(LoginTestCase): def test_field_datetime_read(self): SaleOrder = self.odoo.env['sale.order'] order_id = SaleOrder.search([('date_order', '!=', False)], limit=1) order = SaleOrder.browse(order_id) self.assertIsInstance(order.date_order, datetime.datetime) def test_field_datetime_write(self): # TODO # No common model found in every versions of OpenERP with a # fields.datetime writable pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_many2many.py0000644000232200023220000002166413376743447023062 0ustar debalancedebalance# -*- coding: UTF-8 -*- import time from odoorpc.tests import LoginTestCase from odoorpc.models import Model class TestFieldMany2many(LoginTestCase): def setUp(self): LoginTestCase.setUp(self) self.group_obj = self.odoo.env['res.groups'] self.u0_id = self.user_obj.create({ 'name': "TestMany2many User 1", 'login': 'test_m2m_u1_%s' % time.time(), }) self.g1_id = self.group_obj.create({'name': "Group 1"}) self.g2_id = self.group_obj.create({'name': "Group 2"}) self.u1_id = self.user_obj.create({ 'name': "TestMany2many User 2", 'login': 'test_m2m_u2_%s' % time.time(), 'groups_id': [(4, self.g1_id), (4, self.g2_id)], }) def test_field_many2many_read(self): self.assertIsInstance(self.user.company_ids, Model) self.assertEqual(self.user.company_ids._name, 'res.company') # Test if empty field returns an empty recordset, and not False self.assertIsInstance(self.user.message_follower_ids, Model) self.assertEqual(self.user.message_follower_ids.ids, []) self.assertFalse(bool(self.user.message_follower_ids)) def test_field_many2many_write_set_false(self): user = self.user_obj.browse(self.u0_id) # False user.groups_id = False data = user.read(['groups_id'])[0] self.assertEqual(data['groups_id'], []) self.assertEqual(list(user.groups_id), []) def test_field_many2many_write_set_empty_list(self): user = self.user_obj.browse(self.u0_id) # = [] user.groups_id = [] data = user.read(['groups_id'])[0] self.assertEqual(data['groups_id'], []) self.assertEqual(list(user.groups_id), []) def test_field_many2many_write_set_magic_tuples(self): user = self.user_obj.browse(self.u0_id) # [(6, 0, IDS)] user.groups_id = [(6, 0, [self.g1_id, self.g2_id])] data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) self.assertEqual(len(data['groups_id']), 2) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) self.assertEqual(len(group_ids), 2) def test_field_many2many_write_iadd_id(self): user = self.user_obj.browse(self.u0_id) # += ID user.groups_id += self.g1_id user.groups_id += self.g2_id data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_iadd_record(self): user = self.user_obj.browse(self.u0_id) # += Record user.groups_id += self.group_obj.browse(self.g2_id) data = user.read(['groups_id'])[0] self.assertNotIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_iadd_recordset(self): user = self.user_obj.browse(self.u0_id) # += Recordset user.groups_id += self.group_obj.browse([self.g1_id, self.g2_id]) data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_iadd_list_ids(self): user = self.user_obj.browse(self.u0_id) # += List of IDs user.groups_id += [self.g1_id, self.g2_id] data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_iadd_list_records(self): user = self.user_obj.browse(self.u0_id) # += List of records user.groups_id += [self.group_obj.browse(self.g1_id), self.group_obj.browse(self.g2_id)] data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_iadd_id_and_list_ids(self): user = self.user_obj.browse(self.u0_id) # += ID and += [ID] user.groups_id += self.g1_id user.groups_id += [self.g2_id] data = user.read(['groups_id'])[0] self.assertIn(self.g1_id, data['groups_id']) self.assertIn(self.g2_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertIn(self.g1_id, group_ids) self.assertIn(self.g2_id, group_ids) def test_field_many2many_write_isub_id(self): user = self.user_obj.browse(self.u1_id) self.assertIn(self.g1_id, user.groups_id.ids) # -= ID user.groups_id -= self.g1_id data = user.read(['groups_id'])[0] self.assertNotIn(self.g1_id, data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(self.g1_id, group_ids) def test_field_many2many_write_isub_record(self): user = self.user_obj.browse(self.u1_id) self.assertIn(self.g1_id, user.groups_id.ids) # -= Record group = self.group_obj.browse(self.g1_id) user.groups_id -= group data = user.read(['groups_id'])[0] self.assertNotIn(group.id, data['groups_id']) self.assertNotIn(group.id, user.groups_id.ids) def test_field_many2many_write_isub_recordset(self): user = self.user_obj.browse(self.u1_id) groups = self.group_obj.browse([self.g1_id, self.g2_id]) # -= Recordset data = user.read(['groups_id'])[0] self.assertIn(groups.ids[0], data['groups_id']) self.assertIn(groups.ids[1], data['groups_id']) user.groups_id -= groups data = user.read(['groups_id'])[0] self.assertNotIn(groups.ids[0], data['groups_id']) self.assertNotIn(groups.ids[1], data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(groups.ids[0], group_ids) self.assertNotIn(groups.ids[1], group_ids) def test_field_many2many_write_isub_list_ids(self): user = self.user_obj.browse(self.u1_id) groups = self.group_obj.browse([self.g1_id, self.g2_id]) # -= List of IDs data = user.read(['groups_id'])[0] self.assertIn(groups.ids[0], data['groups_id']) self.assertIn(groups.ids[1], data['groups_id']) user.groups_id -= groups.ids data = user.read(['groups_id'])[0] self.assertNotIn(groups.ids[0], data['groups_id']) self.assertNotIn(groups.ids[1], data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(groups.ids[0], group_ids) self.assertNotIn(groups.ids[1], group_ids) def test_field_many2many_write_isub_list_records(self): user = self.user_obj.browse(self.u1_id) groups = self.group_obj.browse([self.g1_id, self.g2_id]) # -= List of records data = user.read(['groups_id'])[0] self.assertIn(groups.ids[0], data['groups_id']) self.assertIn(groups.ids[1], data['groups_id']) user.groups_id -= [grp for grp in groups] data = user.read(['groups_id'])[0] self.assertNotIn(groups.ids[0], data['groups_id']) self.assertNotIn(groups.ids[1], data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(groups.ids[0], group_ids) self.assertNotIn(groups.ids[1], group_ids) def test_field_many2many_write_isub_id_and_list_ids(self): user = self.user_obj.browse(self.u1_id) groups = self.group_obj.browse([self.g1_id, self.g2_id]) # -= ID and -= [ID] data = user.read(['groups_id'])[0] self.assertIn(groups.ids[0], data['groups_id']) self.assertIn(groups.ids[1], data['groups_id']) user.groups_id -= groups.ids[0] user.groups_id -= [groups.ids[1]] data = user.read(['groups_id'])[0] self.assertNotIn(groups.ids[0], data['groups_id']) self.assertNotIn(groups.ids[1], data['groups_id']) group_ids = [grp.id for grp in user.groups_id] self.assertNotIn(groups.ids[0], group_ids) self.assertNotIn(groups.ids[1], group_ids) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_boolean.py0000644000232200023220000000226313376743447022560 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase class TestFieldBoolean(LoginTestCase): def test_field_boolean_read(self): self.assertTrue(self.user.active) def test_field_boolean_write(self): # TODO: split in several unit tests partner = self.user.partner_id backup = partner.customer # True partner.customer = True data = partner.read(['customer'])[0] self.assertEqual(data['customer'], True) self.assertEqual(partner.customer, True) # False partner.customer = False data = partner.read(['customer'])[0] self.assertEqual(data['customer'], False) self.assertEqual(partner.customer, False) # None partner.customer = None data = partner.read(['customer'])[0] self.assertEqual(data['customer'], False) self.assertEqual(partner.customer, False) # Restore original value partner.customer = backup data = partner.read(['customer'])[0] self.assertEqual(data['customer'], backup) self.assertEqual(partner.customer, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_float.py0000644000232200023220000000270313376743447022245 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase class TestFieldFloat(LoginTestCase): def test_field_float_read(self): self.assertEqual(self.user.credit_limit, 0.0) def test_field_float_write(self): # TODO: split in several unit tests partner = self.user.partner_id backup = partner.credit_limit # False partner.credit_limit = False data = partner.read(['credit_limit'])[0] self.assertEqual(data['credit_limit'], 0.0) self.assertEqual(partner.credit_limit, 0.0) # None partner.credit_limit = None data = partner.read(['credit_limit'])[0] self.assertEqual(data['credit_limit'], 0.0) self.assertEqual(partner.credit_limit, 0.0) # 0.0 partner.credit_limit = 0.0 data = partner.read(['credit_limit'])[0] self.assertEqual(data['credit_limit'], 0.0) self.assertEqual(partner.credit_limit, 0.0) # 100.0 partner.credit_limit = 100.0 data = partner.read(['credit_limit'])[0] self.assertEqual(data['credit_limit'], 100.0) self.assertEqual(partner.credit_limit, 100.0) # Restore original value partner.credit_limit = backup data = partner.read(['credit_limit'])[0] self.assertEqual(data['credit_limit'], backup) self.assertEqual(partner.credit_limit, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_date.py0000644000232200023220000000266713376743447022066 0ustar debalancedebalance# -*- coding: UTF-8 -*- import datetime from odoorpc.tests import LoginTestCase class TestFieldDate(LoginTestCase): def test_field_date_read(self): self.assertIsInstance(self.user.login_date, datetime.date) def test_field_date_write(self): partner = self.user.company_id.partner_id backup = partner.date # False partner.date = False data = partner.read(['date'])[0] self.assertEqual(data['date'], False) self.assertEqual(partner.date, False) # None partner.date = None data = partner.read(['date'])[0] self.assertEqual(data['date'], False) self.assertEqual(partner.date, False) # 2012-01-01 (string) partner.date = '2012-01-01' data = partner.read(['date'])[0] self.assertEqual(data['date'], '2012-01-01') self.assertEqual(partner.date, datetime.date(2012, 1, 1)) # 2012-01-01 (date object) partner.date = datetime.date(2012, 1, 1) data = partner.read(['date'])[0] self.assertEqual(data['date'], '2012-01-01') self.assertEqual(partner.date, datetime.date(2012, 1, 1)) # Restore original value partner.date = backup data = partner.read(['date'])[0] self.assertEqual(data['date'], backup and backup.strftime('%Y-%m-%d')) self.assertEqual(partner.date, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_field_selection.py0000644000232200023220000000254213376743447023126 0ustar debalancedebalance# -*- coding: UTF-8 -*- from odoorpc.tests import LoginTestCase class TestFieldSelection(LoginTestCase): def test_field_selection_read(self): self.assertEqual(self.user.state, 'active') def test_field_selection_write(self): # TODO: split in several unit tests #record = self.user #data = record.__class__.fields_get() #for f in data: # if data[f]['type'] == 'selection': # print("%s" % (f)) # #print("%s - %s" % (f, self.user[f])) backup = self.user.tz # False self.user.tz = False data = self.user.read(['tz'])[0] self.assertEqual(data['tz'], False) self.assertEqual(self.user.tz, False) # None self.user.tz = None data = self.user.read(['tz'])[0] self.assertEqual(data['tz'], False) self.assertEqual(self.user.tz, False) # Europe/Paris self.user.tz = 'Europe/Paris' data = self.user.read(['tz'])[0] self.assertEqual(data['tz'], 'Europe/Paris') self.assertEqual(self.user.tz, 'Europe/Paris') # Restore original value self.user.tz = backup data = self.user.read(['tz'])[0] self.assertEqual(data['tz'], backup) self.assertEqual(self.user.tz, backup) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/odoorpc/tests/test_timeout.py0000644000232200023220000000171513376743447021465 0ustar debalancedebalance# -*- coding: UTF-8 -*- import socket from odoorpc.tests import LoginTestCase from odoorpc.tools import v class TestTimeout(LoginTestCase): def test_increased_timeout(self): # Set the timeout self.odoo.config['timeout'] = 120 # Execute a time consuming query: no exception report_name = 'web.preview_internalreport' if v(self.odoo.version)[0] < 11: report_name = 'preview.report' self.odoo.report.download(report_name, [1]) def test_reduced_timeout(self): # Set the timeout self.odoo.config['timeout'] = 0.005 # Execute a time consuming query: handle exception report_name = 'web.preview_internalreport' if v(self.odoo.version)[0] < 11: report_name = 'preview.report' self.assertRaises( socket.timeout, self.odoo.report.download, report_name, [1]) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: odoorpc-0.7.0/README.rst0000644000232200023220000000702713376743447015250 0ustar debalancedebalance======= OdooRPC ======= .. image:: https://img.shields.io/pypi/v/OdooRPC.svg :target: https://pypi.python.org/pypi/OdooRPC/ :alt: Latest Version .. image:: https://travis-ci.org/OCA/odoorpc.svg?branch=master :target: https://travis-ci.org/OCA/odoorpc :alt: Build Status .. image:: https://img.shields.io/pypi/pyversions/OdooRPC.svg :target: https://pypi.python.org/pypi/OdooRPC/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/l/OdooRPC.svg :target: https://pypi.python.org/pypi/OdooRPC/ :alt: License **OdooRPC** is a Python package providing an easy way to pilot your **Odoo** servers through `RPC`. Features supported: - access to all data model methods (even ``browse``) with an API similar to the server-side API, - use named parameters with model methods, - user context automatically sent providing support for internationalization, - browse records, - execute workflows, - manage databases, - reports downloading, - JSON-RPC protocol (SSL supported), How does it work? See below: .. code-block:: python import odoorpc # Prepare the connection to the server odoo = odoorpc.ODOO('localhost', port=8069) # Check available databases print(odoo.db.list()) # Login odoo.login('db_name', 'user', 'passwd') # Current user user = odoo.env.user print(user.name) # name of the user connected print(user.company_id.name) # the name of its company # Simple 'raw' query user_data = odoo.execute('res.users', 'read', [user.id]) print(user_data) # Use all methods of a model if 'sale.order' in odoo.env: Order = odoo.env['sale.order'] order_ids = Order.search([]) for order in Order.browse(order_ids): print(order.name) products = [line.product_id.name for line in order.order_line] print(products) # Update data through a record user.name = "Brian Jones" See the documentation for more details and features. Supported Odoo server versions ============================== `OdooRPC` is tested on all major releases of `Odoo` (starting from 8.0). Supported Python versions ========================= `OdooRPC` support Python 2.7, 3.4, 3.5 and 3.6. License ======= This software is made available under the `LGPL v3` license. Generate the documentation ========================== To generate the documentation, you have to install `Sphinx` documentation generator:: pip install sphinx Then, you can use the ``build_doc`` option of the ``setup.py``:: python setup.py build_doc The generated documentation will be in the ``./doc/build/html`` directory. Changes in this version ======================= Consult the ``CHANGELOG`` file. Bug Tracker =========== Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smash it by providing detailed and welcomed feedback. Credits ======= Contributors ------------ * Sébastien Alix Do not contact contributors directly about support or help with technical issues. Maintainer ---------- .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org This package is maintained by the OCA. OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. odoorpc-0.7.0/bump_version.sh0000755000232200023220000000063013376743447016621 0ustar debalancedebalance#!/bin/sh # Update the version and commit: # # $ ./bump_version.sh X.Y.Z # HERE=$(dirname $(readlink -m $0)) sed -i "s/^__version__.*/__version__ = '$1'/" $HERE/odoorpc/__init__.py sed -i "s/^version =.*/version = '$1'/" $HERE/setup.py sed -i "s/^version =.*/version = '$1'/" $HERE/doc/source/conf.py sed -i "s/^release =.*/release = '$1'/" $HERE/doc/source/conf.py git ci -a -m "[IMP] Bump version to $1" odoorpc-0.7.0/travis/0000755000232200023220000000000013376743447015063 5ustar debalancedebalanceodoorpc-0.7.0/travis/docker_odoo_fix.sh0000644000232200023220000000076013376743447020557 0ustar debalancedebalance#!/bin/sh # Upgrade postgresql-client to get a recent version of pg_dump # (required by Odoo 12 to get backup methods working on PostgreSQL 10) if [ "$ODOO_VERSION" = "12.0" ]; then apt-get update apt-get install -y gnupg2 curl -qq https://www.postgresql.org/media/keys/ACCC4CF8.asc -o - | apt-key add - echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list apt-get update apt-get upgrade -y -t stretch-pgdg postgresql-client-10 fi odoorpc-0.7.0/run_tests_docker.sh0000755000232200023220000000351613376743447017474 0ustar debalancedebalance#!/bin/sh # Run tests locally with Python 3 against the official Odoo Docker image. # Run all tests: # # $ ./run_tests_docker.sh # # Run tests partially: # # $ ./run_tests_docker.sh odoorpc.tests.test_env # # Docker containers are removed only when running tests globally, this way # you can run tests partially several times without time penalty: # # Depends on: # - docker.io # - python3-venv # Optional environment variables: # - ORPC_TEST_VERSION=12.0 # - ORPC_TEST_PORT=8069 # - ORPC_TEST_DB # HERE=$(dirname $(readlink -m $0)) VENV=$HERE/venv_test [ -z ${ORPC_TEST_VERSION+x} ] && export ORPC_TEST_VERSION="12.0" [ -z ${ORPC_TEST_PORT+x} ] && export ORPC_TEST_PORT=8069 # Pull images PG_IMAGE="postgres:9.4" if [ "$(echo $ORPC_TEST_VERSION'>='12.0 | bc -l)" -eq 1 ]; then PG_IMAGE="postgres:10" fi PG_CT=pg_odoorpc_test_${ORPC_TEST_VERSION} ODOO_IMAGE="odoo:$ORPC_TEST_VERSION" ODOO_CT=odoo_odoorpc_test_${ORPC_TEST_VERSION} printf "Using Docker images$ODOO_IMAGE + $PG_IMAGE ...\n" docker pull $PG_IMAGE docker pull $ODOO_IMAGE # Run containers docker run -d -e POSTGRES_USER=odoo -e POSTGRES_PASSWORD=odoo -e POSTGRES_DB=postgres --name $PG_CT $PG_IMAGE docker run -d -p $ORPC_TEST_PORT:8069 --name $ODOO_CT --link $PG_CT:db -t $ODOO_IMAGE docker cp $HERE/travis/docker_odoo_fix.sh $ODOO_CT:/docker_odoo_fix.sh docker exec -it -u root $ODOO_CT sh /docker_odoo_fix.sh # Install OdooRPC in a Python 3 environment python3 -m venv $VENV $VENV/bin/pip install -e $HERE $VENV/bin/pip install sphinx # Run tests against the Odoo container printf "Running tests...\n" if [ -z ${1+x} ]; then # Clean up $VENV/bin/python3 -m unittest discover -v $VENV/bin/sphinx-build -b doctest -d doc/build/doctrees doc/source build/doctest else $VENV/bin/python3 -m unittest $1 -v fi printf "Removing containers...\n" docker rm -f $ODOO_CT $PG_CT odoorpc-0.7.0/AUTHORS0000644000232200023220000000013713376743447014624 0ustar debalancedebalanceOriginal Author --------------- Sébastien Alix ,