.. queue:: backend/series
-=======
-Backend
-=======
+=================
+Building a Module
+=================
Start/Stop the Odoo server
==========================
Odoo uses a client/server architecture in which clients are web browsers
-accessing the odoo server via RPC.
+accessing the Odoo server via RPC.
Business logic and extension is generally performed on the server side,
although supporting client features (e.g. new data representation such as
If ``True``, the field can not be empty, it must either have a default
value or always be given a value when creating a record.
:attr:`~openerp.fields.Field.help` (``unicode``, default: ``''``)
- Long-formm, provides a help tooltip to users in the UI.
+ Long-form, provides a help tooltip to users in the UI.
:attr:`~openerp.fields.Field.index` (``bool``, default: ``False``)
Requests that Odoo create a `database index`_ on the column
model to a record in a parent model, and provides transparent access to the
fields of the parent record.
-.. image:: backend/inheritance_methods.png
+.. image:: ../images/inheritance_methods.png
:align: center
.. seealso::
name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
+.. note::
+
+ The object ``self.env`` gives access to request parameters and other useful
+ things:
+
+ - ``self.env.cr`` or ``self._cr`` is the database *cursor* object; it is
+ used for querying the database
+ - ``self.env.uid`` or ``self._uid`` is the current user's database id
+ - ``self.env.user`` is the current user's record
+ - ``self.env.context`` or ``self._context`` is the context dictionary
+ - ``self.env.ref(xml_id)`` returns the record corresponding to an XML id
+ - ``self.env[model_name]`` returns an instance of the given model
+
.. exercise:: Active objects – Default values
* Define the start_date default value as today (see
Odoo provides two ways to set up automatically verified invariants:
:func:`Python constraints <openerp.api.constrains>` and
-:attr:`SQL constaints <openerp.models.Model._sql_constraints>`.
+:attr:`SQL constraints <openerp.models.Model._sql_constraints>`.
A Python constraint is defined as a method decorated with
:func:`~openerp.api.constrains`, and invoked on a recordset. The decorator
Search views
------------
-Search view fields can take custom operators or :ref:`reference/orm/domains`
-for more flexible matching of results.
+Search view ``<field>`` elements can have a ``@filter_domain`` that overrides
+the domain generated for searching on the given field. In the given domain,
+``self`` represents the value entered by the user. In the example below, it is
+used to search on both fields ``name`` and ``description``.
-Search views can also contain *filters* which act as toggles for predefined
-searches (defined using :ref:`reference/orm/domains`):
+Search views can also contain ``<filter>`` elements, which act as toggles for
+predefined searches. Filters must have one of the following attributes:
+
+``domain``
+ add the given domain to the current search
+``context``
+ add some context to the current search; use the key ``group_by`` to group
+ results on the given field name
.. code-block:: xml
<search string="Ideas">
- <filter name="my_ideas" domain="[('inventor_id','=',uid)]"
- string="My Ideas" icon="terp-partner"/>
<field name="name"/>
- <field name="description"/>
+ <field name="description" string="Name and description"
+ filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
<field name="inventor_id"/>
<field name="country_id" widget="selection"/>
+
+ <filter name="my_ideas" string="My Ideas"
+ domain="[('inventor_id', '=', uid)]"/>
+ <group string="Group By">
+ <filter name="group_by_inventor" string="Inventor"
+ context="{'group_by': 'inventor'}"/>
+ </group>
</search>
To use a non-default search view in an action, it should be linked using the
.. exercise:: Search views
- Add a button to filter the courses for which the current user is the
- responsible in the course search view. Make it selected by default.
+ #. Add a button to filter the courses for which the current user is the
+ responsible in the course search view. Make it selected by default.
+ #. Add a button to group courses by responsible user.
.. only:: solutions
In the session form, add a (read-only) field to
visualize the state, and buttons to change it. The valid transitions are:
- * Draft ➔ Confirmed
- * Confirmed ➔ Draft
- * Confirmed ➔ Done
- * Done ➔ Draft
+ * Draft -> Confirmed
+ * Confirmed -> Draft
+ * Confirmed -> Done
+ * Done -> Draft
.. only:: solutions
(conditions) are declared as XML records, as usual. The tokens that navigate
in workflows are called workitems.
+.. warning::
+
+ A workflow associated with a model is only created when the
+ model's records are created. Thus there is no workflow instance
+ associated with session instances created before the workflow's
+ definition
+
.. exercise:: Workflow
Replace the ad-hoc *Session* workflow by a real workflow. Transform the
.. patch::
- .. warning::
-
- A workflow associated with a model is only created when the
- model's records are created. Thus there is no workflow instance
- associated with session instances created before the workflow's
- definition
-
.. tip::
In order to check if instances of the workflow are correctly
Group-based access control mechanisms
-------------------------------------
-Groups are created as normal records on the model “res.groups”, and granted
+Groups are created as normal records on the model ``res.groups``, and granted
menu access via menu definitions. However even without a menu, objects may
still be accessible indirectly, so actual object-level permissions (read,
write, create, unlink) must be defined for groups. They are usually inserted
Access rights
-------------
-Access rights are defined as records of the model “ir.model.access”. Each
+Access rights are defined as records of the model ``ir.model.access``. Each
access right is associated to a model, a group (or no group for global
access), and a set of permissions: read, write, create, unlink. Such access
rights are usually created by a CSV file named after its model:
------------
A record rule restricts the access rights to a subset of records of the given
-model. A rule is a record of the model “ir.rule”, and is associated to a
+model. A rule is a record of the model ``ir.rule``, and is associated to a
model, a number of groups (many2many field), permissions to which the
restriction applies, and a domain. The domain specifies to which records the
access rights are limited.
Here is an example of a rule that prevents the deletion of leads that are not
-in state “cancel”. Notice that the value of the field “groups” must follow
-the same convention as the method “write” of the ORM.
+in state ``cancel``. Notice that the value of the field ``groups`` must follow
+the same convention as the method :meth:`~openerp.models.Model.write` of the ORM.
.. code-block:: xml
.. patch::
+Wizards
+=======
+
+Wizards describe interactive sessions with the user (or dialog boxes) through
+dynamic forms. A wizard is simply a model that extends the class
+:class:`~openerp.models.TransientModel` instead of
+:class:`~openerp.models.Model`. The class
+:class:`~openerp.models.TransientModel` extends :class:`~openerp.models.Model`
+and reuse all its existing mechanisms, with the following particularities:
+
+- Wizard records are not meant to be persistent; they are automatically deleted
+ from the database after a certain time. This is why they are called
+ *transient*.
+- Wizard models do not require explicit access rights: users have all
+ permissions on wizard records.
+- Wizard records may refer to regular records or wizard records through many2one
+ fields, but regular records *cannot* refer to wizard records through a
+ many2one field.
+
+We want to create a wizard that allow users to create attendees for a particular
+session, or for a list of sessions at once.
+
+.. exercise:: Define the wizard
+
+ Create a wizard model with a many2one relationship with the *Session*
+ model and a many2many relationship with the *Partner* model.
+
+ .. only:: solutions
+
+ Add a new file ``openacademy/wizard.py``:
+
+ .. patch::
+
+Launching wizards
+-----------------
+
+Wizards are launched by ``ir.actions.act_window`` records, with the field
+``target`` set to the value ``new``. The latter opens the wizard view into a
+popup window. The action may be triggered by a menu item.
+
+There is another way to launch the wizard: using an ``ir.actions.act_window``
+record like above, but with an extra field ``src_model`` that specifies in the
+context of which model the action is available. The wizard will appear in the
+contextual actions of the model, above the main view. Because of some internal
+hooks in the ORM, such an action is declared in XML with the tag ``act_window``.
+
+.. code:: xml
+
+ <act_window id="launch_the_wizard"
+ name="Launch the Wizard"
+ src_model="context_model_name"
+ res_model="wizard_model_name"
+ view_mode="form"
+ target="new"
+ key2="client_action_multi"/>
+
+Wizards use regular views and their buttons may use the attribute
+``special="cancel"`` to close the wizard window without saving.
+
+.. exercise:: Launch the wizard
+
+ #. Define a form view for the wizard.
+ #. Add the action to launch it in the context of the *Session* model.
+ #. Define a default value for the session field in the wizard; use the
+ context parameter ``self._context`` to retrieve the current session.
+
+ .. only:: solutions
+
+ .. patch::
+
+.. exercise:: Register attendees
+
+ Add buttons to the wizard, and implement the corresponding method for adding
+ the attendees to the given session.
+
+ .. only:: solutions
+
+ .. patch::
+
+.. exercise:: Register attendees to multiple sessions
+
+ Modify the wizard model so that attendees can be registered to multiple
+ sessions.
+
+ .. only:: solutions
+
+ .. patch::
+
Internationalization
====================
dedicated PO-file editor e.g. POEdit_ and translate the missing
terms
- #. Add ``from openerp import _`` to ``course.py`` and
- mark missing strings as translatable
+ #. In ``models.py``, add an import statement for the function
+ ``openerp._`` and mark missing strings as translatable
#. Repeat steps 3-6
---------------
The following example is a Python program that interacts with an Odoo
-server with the library xmlrpclib.
-
-::
+server with the library ``xmlrpclib``::
import xmlrpclib
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
- uid = xmlrpclib.ServerProxy(root + 'common').login(db, username, password)
+ uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)
print "Logged in as %s (uid: %d)" % (USER, uid)
- # Create a new idea
+ # Create a new note
sock = xmlrpclib.ServerProxy(root + 'object')
args = {
- 'name' : 'Another idea',
- 'description' : 'This is another idea of mine',
- 'inventor_id': uid,
+ 'color' : 8,
+ 'memo' : 'This is a note',
+ 'create_uid': uid,
}
- idea_id = sock.execute(db, uid, password, 'idea.idea', 'create', args)
+ note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
.. exercise:: Add a new service to the client
print "Logged in as %s (uid:%d)" % (USER,uid)
call = functools.partial(
- xmlcprlib.ServerProxy(ROOT + 'object').execute,
+ xmlrpclib.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)
# 2. Read the sessions
sessions = call('openacademy.session','search_read', [], ['name','seats'])
- for session in sessions :
+ for session in sessions:
print "Session %s (%s seats)" % (session['name'], session['seats'])
# 3.create a new session
session_id = call('openacademy.session', 'create', {
'course_id' : course_id,
})
-.. note:: there are also a number of high-level APIs in various languages to
- access Odoo systems without *explicitly* going through XML-RPC e.g.
+JSON-RPC Library
+----------------
+
+The following example is a Python program that interacts with an Odoo server
+with the standard Python libraries ``urllib2`` and ``json``::
+
+ import json
+ import random
+ import urllib2
+
+ def json_rpc(url, method, params):
+ data = {
+ "jsonrpc": "2.0",
+ "method": method,
+ "params": params,
+ "id": random.randint(0, 1000000000),
+ }
+ req = urllib2.Request(url=url, data=json.dumps(data), headers={
+ "Content-Type":"application/json",
+ })
+ reply = json.load(urllib2.urlopen(req))
+ if reply.get("error"):
+ raise Exception(reply["error"])
+ return reply["result"]
+
+ def call(url, service, method, *args):
+ return json_rpc(url, "call", {"service": service, "method": method, "args": args})
+
+ # log in the given database
+ url = "http://%s:%s/jsonrpc" % (HOST, PORT)
+ uid = call(url, "common", "login", DB, USER, PASS)
+
+ # create a new note
+ args = {
+ 'color' : 8,
+ 'memo' : 'This is another note',
+ 'create_uid': uid,
+ }
+ note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)
+
+Here is the same program, using the library
+`jsonrpclib <https://pypi.python.org/pypi/jsonrpclib>`::
+
+ import jsonrpclib
+
+ # server proxy object
+ url = "http://%s:%s/jsonrpc" % (HOST, PORT)
+ server = jsonrpclib.Server(url)
+
+ # log in the given database
+ uid = server.call(service="common", method="login", args=[DB, USER, PASS])
+
+ # helper function for invoking model methods
+ def invoke(model, method, *args):
+ args = [DB, uid, PASS, model, method] + list(args)
+ return server.call(service="object", method="execute", args=args)
+
+ # create a new note
+ args = {
+ 'color' : 8,
+ 'memo' : 'This is another note',
+ 'create_uid': uid,
+ }
+ note_id = invoke('note.note', 'create', args)
+
+Examples can be easily adapted from XML-RPC to JSON-RPC.
+
+.. note::
+
+ There are a number of high-level APIs in various languages to access Odoo
+ systems without *explicitly* going through XML-RPC or JSON-RPC, such as:
* https://github.com/akretion/ooor
* https://github.com/syleam/openobject-library