Odoo uses a client/server architecture in which clients are web browsers 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 interactive maps) can be added to the client.
In order to start the server, simply invoke the command odoo.py in the shell, adding the full path to the file if necessary:
odoo.py
The server is stopped by hitting Ctrl-C
twice from the terminal, or by killing the corresponding OS process.
Both server and client extensions are packaged as modules which are optionally loaded in a database.
Odoo modules can either add brand new business logic to an Odoo system, or alter and extend existing business logic: a module can be created to add your country's accounting rules to Odoo's generic accounting support, while the next module adds support for real-time visualisation of a bus fleet.
Everything in Odoo thus starts and ends with modules.
An Odoo module can contain a number of elements:
Each module is a directory within a module directory. Module directories are specified by using the --addons-path
option.
Tip
most command-line options can also be set using a configuration file
An Odoo module is declared by its manifest. See the manifest documentation information about it.
A module is also a Python package with a __init__.py
file, containing import instructions for various Python files in the module.
For instance, if the module has a single mymodule.py
file __init__.py
might contain:
from . import mymodule
Odoo provides a mechanism to help set up a new module, odoo.py has a subcommand scaffold to create an empty module:
$ odoo.py scaffold
The command creates a subdirectory for your module, and automatically creates a bunch of standard files for a module. Most of them simply contain commented code or XML. The usage of most of those files will be explained along this tutorial.
Exercise
Module creation
Use the command line above to create an empty module Open Academy, and install it in Odoo.
odoo.py scaffold openacademy addons
.# -*- coding: utf-8 -*-
{
'name': "Open Academy",
'summary': """Manage trainings""",
'description': """
Open Academy module for managing trainings:
- training courses
- training sessions
- attendees registration
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
# for the full list
'category': 'Test',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo.xml',
],
}
# -*- coding: utf-8 -*-
from . import controllers
from . import models
# -*- coding: utf-8 -*-
from openerp import http
# class Openacademy(http.Controller):
# @http.route('/openacademy/openacademy/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/openacademy/openacademy/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('openacademy.listing', {
# 'root': '/openacademy/openacademy',
# 'objects': http.request.env['openacademy.openacademy'].search([]),
# })
# @http.route('/openacademy/openacademy/objects//', auth='public')
# def object(self, obj, **kw):
# return http.request.render('openacademy.object', {
# 'object': obj
# })
# -*- coding: utf-8 -*-
from openerp import models, fields, api
# class openacademy(models.Model):
# _name = 'openacademy.openacademy'
# name = fields.Char()
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0
A key component of Odoo is the ORM layer. This layer avoids having to write most SQL by hand and provides extensibility and security services2.
Business objects are declared as Python classes extending Model
which integrates them into the automated persistence system.
Models can be configured by setting a number of attributes at their definition. The most important attribute is _name
which is required and defines the name for the model in the Odoo system. Here is a minimally complete definition of a model:
from openerp import models
class MinimalModel(models.Model):
_name = 'test.model'
Fields are used to define what the model can store and where. Fields are defined as attributes on the model class:
from openerp import models, fields
class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()
Much like the model itself, its fields can be configured, by passing configuration attributes as parameters:
name = field.Char(required=True)
Some attributes are available on all fields, here are the most common ones:
string
(
unicode
, default: field's name)
required
(
bool
, default:
False
)
True
, the field can not be empty, it must either have a default value or always be given a value when creating a record.
help
(
unicode
, default:
''
)
index
(
bool
, default:
False
)
There are two broad categories of fields: "simple" fields which are atomic values stored directly in the model's table and "relational" fields linking records (of the same model or of different models).
Example of simple fields are Boolean
, Date
, Char
.
Odoo creates a few fields in all models1. These fields are managed by the system and shouldn't be written to. They can be read if useful or necessary:
id
(
Id
)
create_date
(
Datetime
)
create_uid
(
Many2one
)
write_date
(
Datetime
)
write_uid
(
Many2one
)
By default, Odoo also requires a name
field on all models for various display and search behaviors. The field used for these purposes can be overridden by setting _rec_name
.
Exercise
Define a model
Define a new data model Course in the openacademy module. A course has a title and a description. Courses must have a title.
Edit the file openacademy/models/models.py
to include a Course class.
from openerp import models, fields, api
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()
Odoo is a highly data driven system. Although behavior is customized using Python code part of a module's value is in the data it sets up when loaded.
Tip
some modules exist solely to add data into Odoo
Module data is declared via data files, XML files with
elements. Each
element creates or updates a database record.
model="{model name}" id="{record identifier}">
name="{a field name}">{a value}
model
is the name of the Odoo model for the recordid
is an external identifier, it allows referring to the record (without having to know its in-database identifier)
elements have a name
which is the name of the field in the model (e.g. description
). Their body is the field's value.Data files have to be declared in the manifest file to be loaded, they can be declared in the 'data'
list (always loaded) or in the 'demo'
list (only loaded in demonstration mode).
Exercise
Define demonstration data
Create demonstration data filling the Courses model with a few demonstration courses.
Edit the file openacademy/demo/demo.xml
to include some data.
model="openacademy.course" id="course0">
name="name">Course 0
name="description">Course 0's description
Can have multiple lines
model="openacademy.course" id="course1">
name="name">Course 1
model="openacademy.course" id="course2">
name="name">Course 2
name="description">Course 2's description
Actions and menus are regular records in database, usually declared through data files. Actions can be triggered in three ways:
Because menus are somewhat complex to declare there is a shortcut to declare an
ir.ui.menu
and connect it to the corresponding action more easily.
model="ir.actions.act_window" id="action_list_ideas">
name="name">Ideas
name="res_model">idea.idea
name="view_mode">tree,form
Danger
The action must be declared before its corresponding menu in the XML file.
Data files are executed sequentially, the action's id
must be present in the database before the menu can be created.
Exercise
Define new menu entries
Define new menu entries to access courses under the OpenAcademy menu entry. A user should be able to
openacademy/views/openacademy.xml
with an action and the menus triggering the actiondata
list of openacademy/__openerp__.py
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
],
# only loaded in demonstration mode
'demo': [
model="ir.actions.act_window" id="course_list_action">
name="name">Courses
name="res_model">openacademy.course
name="view_type">form
name="view_mode">tree,form
name="help" type="html">
class="oe_view_nocontent_create">Create the first course
parent="main_openacademy_menu"/>
action="course_list_action"/>
Views define the way the records of a model are displayed. Each type of view represents a mode of visualization (a list of records, a graph of their aggregation, …). Views can either be requested generically via their type (e.g. a list of partners) or specifically via their id. For generic requests, the view with the correct type and the lowest priority will be used (so the lowest-priority view of each type is the default view for that type).
View inheritance allows altering views declared elsewhere (adding or removing content).
A view is declared as a record of the model ir.ui.view
. The view type is implied by the root element of the arch
field:
model="ir.ui.view" id="view_id">
name="name">view.name
name="model">object_name
name="priority" eval="16"/>
name="arch" type="xml">
Danger
The view's content is XML.
The arch
field must thus be declared as type="xml"
to be parsed correctly.
Tree views, also called list views, display records in a tabular form.
Their root element is
. The simplest form of the tree view simply lists all the fields to display in the table (each field as a column):
string="Idea list">
name="name"/>
name="inventor_id"/>
Forms are used to create and edit single records.
Their root element is . They composed of high-level structure elements (groups, notebooks) and interactive elements (buttons and fields):
Exercise
Customise form view using XML
Create your own form view for the Course object. Data displayed should be: the name and the description of the course.
model="ir.ui.view" id="course_form_view">
name="name">course.form
name="model">openacademy.course
name="arch" type="xml">
name="name"/>
name="description"/>
model="ir.ui.view" id="session_form_view">
name="name">session.form
name="model">openacademy.session
name="arch" type="xml">
name="name"/>
name="start_date"/>
name="duration"/>
name="seats"/>
model="ir.actions.act_window" id="session_list_action">
name="name">Sessions
name="res_model">openacademy.session
name="view_type">form
name="view_mode">tree,form
parent="openacademy_menu"
action="session_list_action"/>
Note
digits=(6, 2)
specifies the precision of a float number: 6 is the total number of digits, while 2 is the number of digits after the comma. Note that it results in the number digits before the comma is a maximum 4
Relational fields link records, either of the same model (hierarchies) or between different models.
Relational field types are:
Many2one(other_model, ondelete='set null')
A simple link to an other object:
print foo.other_id.name
See also
foreign keys
One2many(other_model, related_field)
A virtual relationship, inverse of a Many2one
. A One2many
behaves as a container of records, accessing it results in a (possibly empty) set of records:
for other in foo.other_ids:
print other.name
Danger
Because a One2many
is a virtual relationship, there must be a Many2one
field in theother_model
, and its name must be related_field
Many2many(other_model)
Bidirectional multiple relationship, any record on one side can be related to any number of records on the other side. Behaves as a container of records, accessing it also results in a possibly empty set of records:
for other in foo.other_ids:
print other.name
Exercise
Many2one relations
Using a many2one, modify the Course and Session models to reflect their relation with other models:
res.users
.res.partner
.openacademy.course
and is required.Many2one
fields to the models, and name = fields.Char(string="Title", required=True)
description = fields.Text()
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
class Session(models.Model):
_name = 'openacademy.session'
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
name="name"/>
name="responsible_id"/>
string="Description">
model="ir.ui.view" id="course_tree_view">
name="name">course.tree
name="model">openacademy.course
name="arch" type="xml">
string="Course Tree">
name="name"/>
name="responsible_id"/>
model="ir.ui.view" id="session_tree_view">
name="name">session.tree
name="model">openacademy.session
name="arch" type="xml">
string="Session Tree">
name="name"/>
name="course_id"/>
model="ir.actions.act_window" id="session_list_action">
name="name">Sessions
name="res_model">openacademy.session
Exercise
Inverse one2many relations
Using the inverse relational field one2many, modify the models to reflect the relation between courses and sessions.
Course
class, and
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
class Session(models.Model):
string="Description">
name="description"/>
string="Sessions">
name="session_ids">
string="Registered sessions">
name="name"/>
name="instructor_id"/>
Exercise
Multiple many2many relations
Using the relational field many2many, modify the Session model to relate every session to a set of attendees. Attendees will be represented by partner records, so we will relate to the built-in model res.partner
. Adapt the views accordingly.
Session
class, and instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
name="seats"/>
name="attendee_ids"/>
Odoo provides two inheritance mechanisms to extend an existing model in a modular way.
The first inheritance mechanism allows a module to modify the behavior of a model defined in another module:
The second inheritance mechanism (delegation) allows to link every record of a model to a record in a parent model, and provides transparent access to the fields of the parent record.
See also
_inherit
_inherits
Instead of modifying existing views in place (by overwriting them), Odoo provides view inheritance where children "extension" views are applied on top of root views, and can add or remove content from their parent.
An extension view references its parent using the inherit_id
field, and instead of a single view its arch
field is composed of any number of xpath
elements selecting and altering the content of their parent view:
id="idea_category_list2" model="ir.ui.view">
name="name">id.category.list2
name="model">idea.category
name="inherit_id" ref="id_category_list"/>
name="arch" type="xml">
expr="//field[@name='description']" position="after">
name="idea_ids" string="Number of ideas"/>
expr
position
Operation to apply to the matched element:
inside
xpath
's body at the end of the matched element
replace
xpath
's body
before
xpath
's body as a sibling before the matched element
after
xpaths
's body as a sibling after the matched element
attributes
attribute
elements in the
xpath
's body
Tip
When matching a single element, the position
attribute can be set directly on the element to be found. Both inheritances below will give the same result.
expr="//field[@name='description']" position="after">
name="idea_ids" />
name="description" position="after">
name="idea_ids" />
Exercise
Alter existing content
instructor
boolean field, and a many2many field that corresponds to the session-partner relationNote
This is the opportunity to introduce the developer mode to inspect the view, find its external ID and the place to put the new field.
openacademy/models/partner.py
and import it in __init__.py
openacademy/views/partner.xml
and add it to __openerp__.py
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import partner
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
# only loaded in demonstration mode
'demo': [
# -*- coding: utf-8 -*-
from openerp import fields, models
class Partner(models.Model):
_inherit = 'res.partner'
# Add a new column to the res.partner model, by default partners are not
# instructors
instructor = fields.Boolean("Instructor", default=False)
session_ids = fields.Many2many('openacademy.session',
string="Attended Sessions", readonly=True)
model="ir.ui.view" id="partner_instructor_form_view">
name="name">partner.instructor
name="model">res.partner
name="inherit_id" ref="base.view_partner_form"/>
name="arch" type="xml">
position="inside">
string="Sessions">
name="instructor"/>
name="session_ids"/>
model="ir.actions.act_window" id="contact_list_action">
name="name">Contacts
name="res_model">res.partner
name="view_mode">tree,form
parent="main_openacademy_menu"/>
parent="configuration_menu"
action="contact_list_action"/>
In Odoo, Domains are values that encode conditions on records. A domain is a list of criteria used to select a subset of a model's records. Each criteria is a triple with a field name, an operator and a value.
For instance, when used on the Product model the following domain selects all services with a unit price over 1000:
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
By default criteria are combined with an implicit AND. The logical operators &
(AND), |
(OR) and !
(NOT) can be used to explicitly combine criteria. They are used in prefix position (the operator is inserted before its arguments rather than between). For instance to select products "which are services OR have a unit price which is NOT between 1000 and 2000":
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
A domain
parameter can be added to relational fields to limit valid records for the relation when trying to select records in the client interface.
Exercise
Domains on relational fields
When selecting the instructor for a Session, only instructors (partners with instructor
set to True
) should be visible.
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=[('instructor', '=', True)])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
Note
A domain declared as a literal list is evaluated server-side and can't refer to dynamic values on the right-hand side, a domain declared as a string is evaluated client-side and allows field names on the right-hand side
Exercise
More complex domains
Create new partner categories Teacher / Level 1 and Teacher / Level 2. The instructor for a session can be either an instructor or a teacher (of any level).
openacademy/view/partner.xml
to get access to Partner categories: seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
('category_id.name', 'ilike', "Teacher")])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
So far fields have been stored directly in and retrieved directly from the database. Fields can also be computed. In that case, the field's value is not retrieved from the database but computed on-the-fly by calling a method of the model.
To create a computed field, create a field and set its attribute compute
to the name of a method. The computation method should simply set the value of the field to compute on every record in self
.
Danger
self
is a collection
The object self
is a recordset, i.e., an ordered collection of records. It supports the standard Python operations on collections, like len(self)
and iter(self)
, plus extra set operations like recs1 + recs2
.
Iterating over self
gives the records one by one, where each record is itself a collection of size 1. You can access/assign fields on single records by using the dot notation, like record.name
.
import random
from openerp import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
@api.multi
def _compute_name(self):
for record in self:
record.name = str(random.randint(1, 1e6))
The value of a computed field usually depends on the values of other fields on the computed record. The ORM expects the developer to specify those dependencies on the compute method with the decorator depends()
. The given dependencies are used by the ORM to trigger the recomputation of the field whenever some of its dependencies have been modified:
from openerp import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
value = fields.Integer()
@api.depends('value')
def _compute_name(self):
for record in self:
record.name = "Record with value %s" % record.value
Exercise
Computed fields
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
if not r.seats:
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
name="start_date"/>
name="duration"/>
name="seats"/>
name="taken_seats" widget="progressbar"/>
string="Session Tree">
name="name"/>
name="course_id"/>
name="taken_seats" widget="progressbar"/>
Any field can be given a default value. In the field definition, add the option default=X
where X
is either a Python literal value (boolean, integer, float, string), or a function taking a recordset and returning a value:
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 databaseself.env.uid
or self._uid
is the current user's database idself.env.user
is the current user's recordself.env.context
or self._context
is the context dictionaryself.env.ref(xml_id)
returns the record corresponding to an XML idself.env[model_name]
returns an instance of the given modelExercise
Active objects – Default values
Date
).active
in the class Session, and set sessions as active by default. _name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date(default=fields.Date.today)
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
name="course_id"/>
name="name"/>
name="instructor_id"/>
name="active"/>
string="Schedule">
name="start_date"/>
Note
Odoo has built-in rules making fields with an active
field set to False
invisible.
The "onchange" mechanism provides a way for the client interface to update a form whenever the user has filled in a value in a field, without saving anything to the database.
For instance, suppose a model has three fields amount
, unit_price
and price
, and you want to update the price on the form when any of the other fields is modified. To achieve this, define a method where self
represents the record in the form view, and decorate it with onchange()
to specify on which field it has to be triggered. Any change you make on self
will be reflected on the form.
name="amount"/>
name="unit_price"/>
name="price" readonly="1"/>
# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
# set auto-changing field
self.price = self.amount * self.unit_price
# Can optionally return a warning and domains
return {
'warning': {
'title': "Something bad happened",
'message': "It was very bad indeed",
}
}
For computed fields, valued onchange
behavior is built-in as can be seen by playing with the Session form: change the number of seats or participants, and the taken_seats
progressbar is automatically updated.
Exercise
Warning
Add an explicit onchange to warn about invalid values, like a negative number of seats, or more participants than seats.
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
@api.onchange('seats', 'attendee_ids')
def _verify_valid_seats(self):
if self.seats < 0:
return {
'warning': {
'title': "Incorrect 'seats' value",
'message': "The number of available seats may not be negative",
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': "Too many attendees",
'message': "Increase seats or remove excess attendees",
},
}
Odoo provides two ways to set up automatically verified invariants: Python constraints
and SQL constraints
.
A Python constraint is defined as a method decorated with constrains()
, and invoked on a recordset. The decorator specifies which fields are involved in the constraint, so that the constraint is automatically evaluated when one of them is modified. The method is expected to raise an exception if its invariant is not satisfied:
from openerp.exceptions import ValidationError
@api.constrains('age')
def _check_something(self):
for record in self:
if record.age > 20:
raise ValidationError("Your record is too old: %s" % record.age)
# all records passed the test, don't return anything
Exercise
Add Python constraints
Add a constraint that checks that the instructor is not present in the attendees of his/her own session.
# -*- coding: utf-8 -*-
from openerp import models, fields, api, exceptions
class Course(models.Model):
_name = 'openacademy.course'
'message': "Increase seats or remove excess attendees",
},
}
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError("A session's instructor can't be an attendee")
SQL constraints are defined through the model attribute _sql_constraints
. The latter is assigned to a list of triples of strings (name, sql_definition, message)
, where name
is a valid SQL constraint name, sql_definition
is a table_constraint expression, and message
is the error message.
Exercise
Add SQL constraints
With the help of PostgreSQL's documentation , add the following constraints:
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
"The title of the course should not be the description"),
('name_unique',
'UNIQUE(name)',
"The course title must be unique"),
]
class Session(models.Model):
_name = 'openacademy.session'
Exercise
Exercise 6 - Add a duplicate option
Since we added a constraint for the Course name uniqueness, it is not possible to use the "duplicate" function anymore (
).Re-implement your own "copy" method which allows to duplicate the Course object, changing the original name into "Copy of [original name]".
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
@api.multi
def copy(self, default=None):
default = dict(default or {})
copied_count = self.search_count(
[('name', '=like', u"Copy of {}%".format(self.name))])
if not copied_count:
new_name = u"Copy of {}".format(self.name)
else:
new_name = u"Copy of {} ({})".format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
Tree views can take supplementary attributes to further customize their behavior:
decoration-{$name}
allow changing the style of a row's text based on the corresponding record's attributes.
Values are Python expressions. For each record, the expression is evaluated with the record's attributes as context values and if true
, the corresponding style is applied to the row. Other context values are uid
(the id of the current user) and current_date
(the current date as a string of the form yyyy-MM-dd
).
{$name}
can be bf
(font-weight: bold
), it
(font-style: italic
), or any bootstrap contextual color (danger
,info
, muted
, primary
, success
or warning
).
string="Idea Categories" decoration-info="state=='draft'"
decoration-danger="state=='trashed'">
name="name"/>
name="state"/>
editable
"top"
or
"bottom"
. Makes the tree view editable in-place (rather than having to go through the form view), the value is the position where new rows appear.
Exercise
List coloring
Modify the Session tree view in such a way that sessions lasting less than 5 days are colored blue, and the ones lasting more than 15 days are colored red.
Modify the session tree view:
name="name">session.tree
name="model">openacademy.session
name="arch" type="xml">
string="Session Tree" decoration-info="duration<5" decoration-danger="duration>15">
name="name"/>
name="course_id"/>
name="duration" invisible="1"/>
name="taken_seats" widget="progressbar"/>
Displays records as calendar events. Their root element is
and their most common attributes are:
color
@color
field) will be given the same color.
date_start
date_stop
(optional)
field (to define the label for each calendar event)
string="Ideas" date_start="invent_date" color="inventor_id">
name="name"/>
Exercise
Calendar view
Add a Calendar view to the Session model enabling the user to view the events associated to the Open Academy.
Add an end_date
field computed from start_date
and duration
Tip
the inverse function makes the field writable, and allows moving the sessions (via drag and drop) in the calendar view
# -*- coding: utf-8 -*-
from datetime import timedelta
from openerp import models, fields, api, exceptions
class Course(models.Model):
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
},
}
@api.depends('start_date', 'duration')
def _get_end_date(self):
for r in self:
if not (r.start_date and r.duration):
r.end_date = r.start_date
continue
# Add duration to start_date, but: Monday + 5 days = Saturday, so
# subtract one second to get on Friday instead
start = fields.Datetime.from_string(r.start_date)
duration = timedelta(days=r.duration, seconds=-1)
r.end_date = start + duration
def _set_end_date(self):
for r in self:
if not (r.start_date and r.end_date):
continue
# Compute the difference between dates, but: Friday - Monday = 4 days,
# so add one day to get 5 days instead
start_date = fields.Datetime.from_string(r.start_date)
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
model="ir.ui.view" id="session_calendar_view">
name="name">session.calendar
name="model">openacademy.session
name="arch" type="xml">
string="Session Calendar" date_start="start_date"
date_stop="end_date"
color="instructor_id">
name="name"/>
model="ir.actions.act_window" id="session_list_action">
name="name">Sessions
name="res_model">openacademy.session
name="view_type">form
name="view_mode">tree,form,calendar
id="session_menu" name="Sessions"
Search view
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
elements, which act as toggles for predefined searches. Filters must have one of the following attributes:
domain
context
group_by
to group results on the given field name
string="Ideas">
name="name"/>
name="description" string="Name and description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
name="inventor_id"/>
name="country_id" widget="selection"/>
name="my_ideas" string="My Ideas"
domain="[('inventor_id', '=', uid)]"/>
string="Group By">
name="group_by_inventor" string="Inventor"
context="{'group_by': 'inventor_id'}"/>
To use a non-default search view in an action, it should be linked using the search_view_id
field of the action record.
The action can also set default values for search fields through its context
field: context keys of the formsearch_default_field_name
will initialize field_name with the provided value. Search filters must have an optional @name
to have a default and behave as booleans (they can only be enabled by default).
Exercise
Search views
name="name"/>
name="description"/>
name="my_courses" string="My Courses"
domain="[('responsible_id', '=', uid)]"/>
string="Group By">
name="by_responsible" string="Responsible"
context="{'group_by': 'responsible_id'}"/>
name="res_model">openacademy.course
name="view_type">form
name="view_mode">tree,form
name="context" eval="{'search_default_my_courses': 1}"/>
name="help" type="html">
class="oe_view_nocontent_create">Create the first course
Horizontal bar charts typically used to show project planning and advancement, their root element is
.
string="Ideas"
date_start="invent_date"
date_stop="date_finished"
progress="progress"
default_group_by="inventor_id" />
Exercise
Gantt charts
Add a Gantt Chart enabling the user to view the sessions scheduling linked to the Open Academy module. The sessions should be grouped by instructor.
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.depends('duration')
def _get_hours(self):
for r in self:
r.hours = r.duration * 24
def _set_hours(self):
for r in self:
r.duration = r.hours / 24
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
model="ir.ui.view" id="session_gantt_view">
name="name">session.gantt
name="model">openacademy.session
name="arch" type="xml">
string="Session Gantt" color="course_id"
date_start="start_date" date_delay="hours"
default_group_by='instructor_id'>
name="name"/>
model="ir.actions.act_window" id="session_list_action">
name="name">Sessions
name="res_model">openacademy.session
name="view_type">form
name="view_mode">tree,form,calendar,gantt
id="session_menu" name="Sessions"
Graph views allow aggregated overview and analysis of models, their root element is
.
Note
Pivot views (element
) a multidimensional table, allows the selection of filers and dimensions to get the right aggregated dataset before moving to a more graphical overview. The pivot view shares the same content definition as graph views.
Graph views have 4 display modes, the default mode is selected using the @type
attribute.
a bar chart, the first dimension is used to define groups on the horizontal axis, other dimensions define aggregated bars within each group.
By default bars are side-by-side, they can be stacked by using @stacked="True"
on the
Graph views contain
with a mandatory @type
attribute taking the values:
row
(default)
measure
string="Total idea score by Inventor">
name="inventor_id"/>
name="score" type="measure"/>
Warning
Graph views perform aggregations on database values, they do not work with non-stored computed fields.
Exercise
Graph view
Add a Graph view in the Session object that displays, for each course, the number of attendees under the form of a bar chart.
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
for r in self:
r.duration = r.hours / 24
@api.depends('attendee_ids')
def _get_attendees_count(self):
for r in self:
r.attendees_count = len(r.attendee_ids)
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
model="ir.ui.view" id="openacademy_session_graph_view">
name="name">openacademy.session.graph
name="model">openacademy.session
name="arch" type="xml">
string="Participations by Courses">
name="course_id"/>
name="attendees_count" type="measure"/>
model="ir.actions.act_window" id="session_list_action">
name="name">Sessions
name="res_model">openacademy.session
name="view_type">form
name="view_mode">tree,form,calendar,gantt,graph
id="session_menu" name="Sessions"
Used to organize tasks, production processes, etc… their root element is
.
A kanban view shows a set of cards possibly grouped in columns. Each card represents a record, and each column the values of an aggregation field.
For instance, project tasks may be organized by stage (each column is a stage), or by responsible (each column is a user), and so on.
Kanban views define the structure of each card as a mix of form elements (including basic HTML) and QWeb.
Exercise
Kanban view
Add a Kanban view that displays sessions grouped by course (columns are thus courses).
color
field to the Session model duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
color = fields.Integer()
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
openacad.session.kanban
openacademy.session
t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
oe_kanban_global_click_edit oe_semantic_html_override
oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
Session name:
Start date:
duration:
Sessions
openacademy.session
form
tree,form,calendar,gantt,graph,kanban
Workflows are models associated to business objects describing their dynamics. Workflows are also used to track processes that evolve over time.
Exercise
Almost a workflow
Add a state
field to the Session model. It will be used to define a workflow-ish.
A sesion can have three possible states: Draft (default), Confirmed and Done.
In the session form, add a (read-only) field to visualize the state, and buttons to change it. The valid transitions are:
state
field attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)
state = fields.Selection([
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
], default='draft')
@api.multi
def action_draft(self):
self.state = 'draft'
@api.multi
def action_confirm(self):
self.state = 'confirmed'
@api.multi
def action_done(self):
self.state = 'done'
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
name="model">openacademy.session
name="arch" type="xml">
Workflows may be associated with any object in Odoo, and are entirely customizable. Workflows are used to structure and manage the lifecycles of business objects and documents, and define transitions, triggers, etc. with graphical tools. Workflows, activities (nodes or actions) and transitions (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 Session form view so its buttons call the workflow instead of the model's methods.
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
],
# only loaded in demonstration mode
'demo': [
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
])
@api.multi
def action_draft(self):
name="arch" type="xml">
model="workflow" id="wkf_session">
name="name">OpenAcademy sessions workflow
name="osv">openacademy.session
name="on_create">True
model="workflow.activity" id="draft">
name="name">Draft
name="wkf_id" ref="wkf_session"/>
name="flow_start" eval="True"/>
name="kind">function
name="action">action_draft()
model="workflow.activity" id="confirmed">
name="name">Confirmed
name="wkf_id" ref="wkf_session"/>
name="kind">function
name="action">action_confirm()
model="workflow.activity" id="done">
name="name">Done
name="wkf_id" ref="wkf_session"/>
name="kind">function
name="action">action_done()
model="workflow.transition" id="session_draft_to_confirmed">
name="act_from" ref="draft"/>
name="act_to" ref="confirmed"/>
name="signal">confirm
model="workflow.transition" id="session_confirmed_to_draft">
name="act_from" ref="confirmed"/>
name="act_to" ref="draft"/>
name="signal">draft
model="workflow.transition" id="session_done_to_draft">
name="act_from" ref="done"/>
name="act_to" ref="draft"/>
name="signal">draft
model="workflow.transition" id="session_confirmed_to_done">
name="act_from" ref="confirmed"/>
name="act_to" ref="done"/>
name="signal">done
Tip
In order to check if instances of the workflow are correctly created alongside sessions, go to
Exercise
Automatic transitions
Automatically transition sessions from Draft to Confirmed when more than half the session's seats are reserved.
name="act_to" ref="done"/>
name="signal">done
model="workflow.transition" id="session_auto_confirm_half_filled">
name="act_from" ref="draft"/>
name="act_to" ref="confirmed"/>
name="condition">taken_seats > 50
Exercise
Server actions
Replace the Python methods for synchronizing session state by server actions.
Both the workflow and the server actions could have been created entirely from the UI.
name="on_create">True
model="ir.actions.server" id="set_session_to_draft">
name="name">Set session to Draft
name="model_id" ref="model_openacademy_session"/>
name="code">
model.search([('id', 'in', context['active_ids'])]).action_draft()
model="workflow.activity" id="draft">
name="name">Draft
name="wkf_id" ref="wkf_session"/>
name="flow_start" eval="True"/>
name="kind">dummy
name="action">
name="action_id" ref="set_session_to_draft"/>
model="ir.actions.server" id="set_session_to_confirmed">
name="name">Set session to Confirmed
name="model_id" ref="model_openacademy_session"/>
name="code">
model.search([('id', 'in', context['active_ids'])]).action_confirm()
model="workflow.activity" id="confirmed">
name="name">Confirmed
name="wkf_id" ref="wkf_session"/>
name="kind">dummy
name="action">
name="action_id" ref="set_session_to_confirmed"/>
model="ir.actions.server" id="set_session_to_done">
name="name">Set session to Done
name="model_id" ref="model_openacademy_session"/>
name="code">
model.search([('id', 'in', context['active_ids'])]).action_done()
model="workflow.activity" id="done">
name="name">Done
name="wkf_id" ref="wkf_session"/>
name="kind">dummy
name="action">
name="action_id" ref="set_session_to_done"/>
model="workflow.transition" id="session_draft_to_confirmed">
Access control mechanisms must be configured to achieve a coherent security policy.
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 via CSV files inside modules. It is also possible to restrict access to specific fields on a view or object using the field's groups attribute.
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: ir.model.access.csv
.
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0
access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0
Exercise
Add access control through the OpenERP interface
Create a new user "John Smith". Then create a group "OpenAcademy / Session Read" with read access to the Session model.
session_read
through , it should have read access on the Session modelsession_read
Exercise
Add access control through data files in your module
Using data files,
openacademy/security/security.xml
to hold the OpenAcademy Manager groupopenacademy/security/ir.model.access.csv
with the access rights to the modelsopenacademy/__openerp__.py
to add the new data files to it
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1
session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1
course_read_all,course all,model_openacademy_course,,1,0,0,0
session_read_all,session all,model_openacademy_session,,1,0,0,0
id="group_manager" model="res.groups">
name="name">OpenAcademy / Manager
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 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.
id="delete_cancelled_only" model="ir.rule">
name="name">Only cancelled leads may be deleted
name="model_id" ref="crm.model_crm_lead"/>
name="groups" eval="[(4, ref('base.group_sale_manager'))]"/>
name="perm_read" eval="0"/>
name="perm_write" eval="0"/>
name="perm_create" eval="0"/>
name="perm_unlink" eval="1" />
name="domain_force">[('state','=','cancel')]
Exercise
Record rule
Add a record rule for the model Course and the group "OpenAcademy / Manager", that restricts write
and unlink
accesses to the responsible of a course. If a course has no responsible, all users of the group must be able to modify it.
Create a new rule in openacademy/security/security.xml
:
id="group_manager" model="res.groups">
name="name">OpenAcademy / Manager
id="only_responsible_can_modify" model="ir.rule">
name="name">Only Responsible can modify Course
name="model_id" ref="model_openacademy_course"/>
name="groups" eval="[(4, ref('openacademy.group_manager'))]"/>
name="perm_read" eval="0"/>
name="perm_write" eval="1"/>
name="perm_create" eval="0"/>
name="perm_unlink" eval="1"/>
name="domain_force">
['|', ('responsible_id','=',False),
('responsible_id','=',user.id)]
Wizards describe interactive sessions with the user (or dialog boxes) through dynamic forms. A wizard is simply a model that extends the class TransientModel
instead of Model
. The class TransientModel
extends Model
and reuse all its existing mechanisms, with the following particularities:
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.
Add a new file openacademy/wizard.py
:
from . import controllers
from . import models
from . import partner
from . import wizard
# -*- coding: utf-8 -*-
from openerp import models, fields, api
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
session_id = fields.Many2one('openacademy.session',
string="Session", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
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
.
Wizards use regular views and their buttons may use the attribute special="cancel"
to close the wizard window without saving.
Exercise
Launch the wizard
self._context
to retrieve the current session.class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
def _default_session(self):
return self.env['openacademy.session'].browse(self._context.get('active_id'))
session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
parent="openacademy_menu"
action="session_list_action"/>
model="ir.ui.view" id="wizard_form_view">
name="name">wizard.form
name="model">openacademy.wizard
name="arch" type="xml">
name="session_id"/>
name="attendee_ids"/>
id="launch_session_wizard"
name="Add Attendees"
src_model="openacademy.session"
res_model="openacademy.wizard"
view_mode="form"
target="new"
key2="client_action_multi"/>
Exercise
Register attendees
Add buttons to the wizard, and implement the corresponding method for adding the attendees to the given session.
name="attendee_ids"/>
string="Subscribe" class="oe_highlight"/>
or
session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
@api.multi
def subscribe(self):
self.session_id.attendee_ids |= self.attendee_ids
return {}
Exercise
Register attendees to multiple sessions
Modify the wizard model so that attendees can be registered to multiple sessions.
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
def _default_sessions(self):
return self.env['openacademy.session'].browse(self._context.get('active_ids'))
session_ids = fields.Many2many('openacademy.session',
string="Sessions", required=True, default=_default_sessions)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
@api.multi
def subscribe(self):
for session in self.session_ids:
session.attendee_ids |= self.attendee_ids
return {}
Each module can provide its own translations within the i18n directory, by having files named LANG.po where LANG is the locale code for the language, or the language and country combination when they differ (e.g. pt.po or pt_BR.po). Translations will be loaded automatically by Odoo for all enabled languages. Developers always use English when creating a module, then export the module terms using Odoo's gettext POT export feature (
without specifying a language), to create the module template POT file, and then derive the translated PO files. Many IDE's have plugins or modes for editing and merging PO/POT files.Tip
The Portable Object files generated by Odoo are published on Transifex, making it easy to translate the software.
|- idea/ # The module directory
|- i18n/ # Translation files
| - idea.pot # Translation Template (exported from Odoo)
| - fr.po # French translation
| - pt_BR.po # Brazilian Portuguese translation
| (...)
Tip
By default Odoo's POT export only extracts labels inside XML files or inside field definitions in Python code, but any Python string can be translated this way by surrounding it with the function openerp._()
(e.g. _("Label")
)
Exercise
Translate a module
Choose a second language for your Odoo installation. Translate your module using the facilities provided by Odoo.
openacademy/i18n/
openacademy/i18n/
openacademy/i18n/
models.py
, add an import statement for the function openerp._
and mark missing strings as translatable# -*- coding: utf-8 -*-
from datetime import timedelta
from openerp import models, fields, api, exceptions, _
class Course(models.Model):
_name = 'openacademy.course'
default = dict(default or {})
copied_count = self.search_count(
[('name', '=like', _(u"Copy of {}%").format(self.name))])
if not copied_count:
new_name = _(u"Copy of {}").format(self.name)
else:
new_name = _(u"Copy of {} ({})").format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)
if self.seats < 0:
return {
'warning': {
'title': _("Incorrect 'seats' value"),
'message': _("The number of available seats may not be negative"),
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': _("Too many attendees"),
'message': _("Increase seats or remove excess attendees"),
},
}
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError(_("A session's instructor can't be an attendee"))
Odoo 8.0 comes with a new report engine based on QWeb, Twitter Bootstrap and Wkhtmltopdf.
A report is a combination two elements:
an ir.actions.report.xml
, for which a
shortcut element is provided, it sets up various basic parameters for the report (default type, whether the report should be saved to the database after generation,…)
id="account_invoices"
model="account.invoice"
string="Invoices"
report_type="qweb-pdf"
name="account.report_invoice"
file="account.report_invoice"
attachment_use="True"
attachment="(object.state in ('open','paid')) and
('INV'+(object.number or '').replace('/','')+'.pdf')"
/>
A standard QWeb view for the actual report:
t-call="report.html_container">
t-foreach="docs" t-as="o">
t-call="report.external_layout">
class="page">
Report title
the standard rendering context provides a number of elements, the most
important being:
``docs``
the records for which the report is printed
``user``
the user printing the report
Because reports are standard web pages, they are available through a URL and output parameters can be manipulated through this URL, for instance the HTML version of the Invoice report is available throughhttp://localhost:8069/report/html/account.report_invoice/1 (if account
is installed) and the PDF version throughhttp://localhost:8069/report/pdf/account.report_invoice/1.
Danger
If it appears that your PDF report is missing the styles (i.e. the text appears but the style/layout is different from the html version), probably your wkhtmltopdf process cannot reach your web server to download them.
If you check your server logs and see that the CSS styles are not being downloaded when generating a PDF report, most surely this is the problem.
The wkhtmltopdf process will use the web.base.url
system parameter as the root path to all linked files, but this parameter is automatically updated each time the Administrator is logged in. If your server resides behind some kind of proxy, that could not be reachable. You can fix this by adding one of these system parameters:
report.url
, pointing to an URL reachable from your server (probably http://localhost:8069
or something similar). It will be used for this particular purpose only.web.base.url.freeze
, when set to True
, will stop the automatic updates to web.base.url
.Exercise
Create a report for the Session model
For each session, it should display session's name, its start and end, and list the session's attendees.
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'reports.xml',
],
# only loaded in demonstration mode
'demo': [
id="report_session"
model="openacademy.session"
string="Session Report"
name="openacademy.report_session_view"
file="openacademy.report_session"
report_type="qweb-pdf" />
id="report_session_view">
t-call="report.html_container">
t-foreach="docs" t-as="doc">
t-call="report.external_layout">
class="page">
t-field="doc.name"/>
From t-field="doc.start_date"/> to t-field="doc.end_date"/>
Attendees:
t-foreach="doc.attendee_ids" t-as="attendee">
t-field="attendee.name"/>
Exercise
Define a Dashboard
Define a dashboard containing the graph view you created, the sessions calendar view and a list view of the courses (switchable to a form view). This dashboard should be available through a menuitem in the menu, and automatically displayed in the web client when the OpenAcademy main menu is selected.
Create a file openacademy/views/session_board.xml
. It should contain the board view, the actions referenced in that view, an action to open the dashboard and a re-definition of the main menu item to add the dashboard action
Note
Available dashboard styles are 1
, 1-1
, 1-2
, 2-1
and 1-1-1
openacademy/__openerp__.py
to reference the new data file 'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'board'],
# always loaded
'data': [
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'views/session_board.xml',
'reports.xml',
],
# only loaded in demonstration mode
model="ir.actions.act_window" id="act_session_graph">
name="name">Attendees by course
name="res_model">openacademy.session
name="view_type">form
name="view_mode">graph
name="view_id"
ref="openacademy.openacademy_session_graph_view"/>
model="ir.actions.act_window" id="act_session_calendar">
name="name">Sessions
name="res_model">openacademy.session
name="view_type">form
name="view_mode">calendar
name="view_id" ref="openacademy.session_calendar_view"/>
model="ir.actions.act_window" id="act_course_list">
name="name">Courses
name="res_model">openacademy.course
name="view_type">form
name="view_mode">tree,form
model="ir.ui.view" id="board_session_form">
name="name">Session Dashboard Form
name="model">board.board
name="type">form
name="arch" type="xml">
style="2-1">
string="Attendees by course"
name="%(act_session_graph)d"
height="150"
width="510"/>
string="Sessions"
name="%(act_session_calendar)d"/>
string="Courses"
name="%(act_course_list)d"/>
model="ir.actions.act_window" id="open_board_session">
name="name">Session Dashboard
name="res_model">board.board
name="view_type">form
name="view_mode">form
name="usage">menu
name="view_id" ref="board_session_form"/>
name="Session Dashboard" parent="base.menu_reporting_dashboard"
action="open_board_session"
sequence="1"
id="menu_board_session" icon="terp-graph"/>
The web-service module offer a common interface for all web-services :
Business objects can also be accessed via the distributed object mechanism. They can all be modified via the client interface with contextual views.
Odoo is accessible through XML-RPC/JSON-RPC interfaces, for which libraries exist in many languages.
The following example is a Python program that interacts with an Odoo server with the library xmlrpclib
:
import xmlrpclib
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)
print "Logged in as %s (uid: %d)" % (USER, uid)
# Create a new note
sock = xmlrpclib.ServerProxy(root + 'object')
args = {
'color' : 8,
'memo' : 'This is a note',
'create_uid': uid,
}
note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
Exercise
Add a new service to the client
Write a Python program able to send XML-RPC requests to a PC running Odoo (yours, or your instructor's). This program should display all the sessions, and their corresponding number of seats. It should also create a new session for one of the courses.
import functools
import xmlrpclib
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)
# 1. Login
uid = xmlrpclib.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print "Logged in as %s (uid:%d)" % (USER,uid)
call = functools.partial(
xmlrpclib.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)
# 2. Read the sessions
sessions = call('openacademy.session','search_read', [], ['name','seats'])
for session in sessions:
print "Session %s (%s seats)" % (session['name'], session['seats'])
# 3.create a new session
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : 2,
})
Instead of using a hard-coded course id, the code can look up a course by name:
# 3.create a new session for the "Functional" course
course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0]
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : course_id,
})
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:
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:
disable the automatic creation of some fields