0% found this document useful (0 votes)
255 views65 pages

Building A Module - Odoo 8

The document provides information about creating and working with Odoo modules. It describes the structure and components of a module including business objects, data files, web controllers, and static web assets. It explains how to create an empty module using the odoo.py scaffold command and the basic files and structure this generates. It also gives an overview of Odoo's object-relational mapping system where business objects are defined as Python model classes that integrate with the persistence system. Key aspects like model, field and common field attributes are defined.

Uploaded by

kros123_3
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
255 views65 pages

Building A Module - Odoo 8

The document provides information about creating and working with Odoo modules. It describes the structure and components of a module including business objects, data files, web controllers, and static web assets. It explains how to create an empty module using the odoo.py scaffold command and the basic files and structure this generates. It also gives an overview of Odoo's object-relational mapping system where business objects are defined as Python model classes that integrate with the persistence system. Key aspects like model, field and common field attributes are defined.

Uploaded by

kros123_3
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 65

tart/StoptheOdooserver

Odoousesaclient/serverarchitectureinwhichclientsarewebbrowsersaccessingtheOdooserverviaRPC.

Businesslogicandextensionisgenerallyperformedontheserverside,althoughsupportingclientfeatures(e.g.
newdatarepresentationsuchasinteractivemaps)canbeaddedtotheclient.

Inordertostarttheserver,simplyinvokethecommandodoo.pyintheshell,addingthefullpathtothefileif
necessary:

odoo.py

Theserverisstoppedbyhitting CtrlC twicefromtheterminal,orbykillingthecorrespondingOSprocess.

BuildanOdoomodule
Bothserverandclientextensionsarepackagedasmoduleswhichareoptionallyloadedinadatabase.

OdoomodulescaneitheraddbrandnewbusinesslogictoanOdoosystem,oralterandextendexistingbusiness
logic:amodulecanbecreatedtoaddyourcountry'saccountingrulestoOdoo'sgenericaccountingsupport,while
thenextmoduleaddssupportforrealtimevisualisationofabusfleet.

EverythinginOdoothusstartsandendswithmodules.

Compositionofamodule
AnOdoomodulecancontainanumberofelements:

Businessobjects
declaredasPythonclasses,theseresourcesareautomaticallypersistedbyOdoobasedontheir
configuration
Datafiles
XMLorCSVfilesdeclaringmetadata(viewsorworkflows),configurationdata(modulesparameterization),
demonstrationdataandmore
Webcontrollers
Handlerequestsfromwebbrowsers
Staticwebdata
Images,CSSorjavascriptfilesusedbythewebinterfaceorwebsite

Modulestructure
Eachmoduleisadirectorywithinamoduledirectory.Moduledirectoriesarespecifiedbyusingthe
Eachmoduleisadirectorywithinamoduledirectory.Moduledirectoriesarespecifiedbyusingthe addons
path option.

Tip
mostcommandlineoptionscanalsobesetusingaconfigurationfile

AnOdoomoduleisdeclaredbyitsmanifest.Seethemanifestdocumentationinformationaboutit.

AmoduleisalsoaPythonpackagewitha __init__.py file,containingimportinstructionsforvariousPython


filesinthemodule.

Forinstance,ifthemodulehasasingle mymodule.py file __init__.py mightcontain:

from.importmymodule

Odooprovidesamechanismtohelpsetupanewmodule,odoo.pyhasasubcommandscaffoldtocreatean
emptymodule:

$odoo.pyscaffold<modulename><wheretoputit>

Thecommandcreatesasubdirectoryforyourmodule,andautomaticallycreatesabunchofstandardfilesfora
module.MostofthemsimplycontaincommentedcodeorXML.Theusageofmostofthosefileswillbeexplained
alongthistutorial.

Exercise
Modulecreation
UsethecommandlineabovetocreateanemptymoduleOpenAcademy,andinstallitin
Odoo.

1.Invokethecommand odoo.pyscaffoldopenacademyaddons .
2.Adaptthemanifestfiletoyourmodule.
3.Don'tbotherabouttheotherfiles.

openacademy/__openerp__.py
#*coding:utf8*
{
'name':"OpenAcademy",

'summary':"""Managetrainings""",

'description':"""
OpenAcademymoduleformanagingtrainings:
trainingcourses
trainingsessions
trainingsessions
attendeesregistration
""",

'author':"YourCompany",
'website':"http://www.yourcompany.com",

#Categoriescanbeusedtofiltermodulesinmoduleslisting
#Checkhttps://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
#forthefulllist
'category':'Test',
'version':'0.1',

#anymodulenecessaryforthisonetoworkcorrectly
'depends':['base'],

#alwaysloaded
'data':[
#'security/ir.model.access.csv',
'templates.xml',
],
#onlyloadedindemonstrationmode
'demo':[
'demo.xml',
],
}

openacademy/__init__.py
#*coding:utf8*
from.importcontrollers
from.importmodels
openacademy/controllers.py
#*coding:utf8*
fromopenerpimporthttp

#classOpenacademy(http.Controller):
#@http.route('/openacademy/openacademy/',auth='public')
#defindex(self,**kw):
#return"Hello,world"

#@http.route('/openacademy/openacademy/objects/',auth='public')
#deflist(self,**kw):
#returnhttp.request.render('openacademy.listing',{
#'root':'/openacademy/openacademy',
#'objects':http.request.env['openacademy.openacademy'].search([]),
#})

#@http.route('/openacademy/openacademy/objects/<model("openacademy.openacademy"):obj>/',auth='public')
#defobject(self,obj,**kw):
#returnhttp.request.render('openacademy.object',{
#'object':obj
#})

openacademy/demo.xml
<openerp>
<data>
<!>
<!<recordid="object0"model="openacademy.openacademy">>
<!<fieldname="name">Object0</field>>
<!</record>>
<!>
<!<recordid="object1"model="openacademy.openacademy">>
<!<fieldname="name">Object1</field>>
<!</record>>
<!>
<!>
<!<recordid="object2"model="openacademy.openacademy">>
<!<fieldname="name">Object2</field>>
<!</record>>
<!>
<!<recordid="object3"model="openacademy.openacademy">>
<!<fieldname="name">Object3</field>>
<!</record>>
<!>
<!<recordid="object4"model="openacademy.openacademy">>
<!<fieldname="name">Object4</field>>
<!</record>>
<!>
</data>
</openerp>
openacademy/models.py
#*coding:utf8*

fromopenerpimportmodels,fields,api

#classopenacademy(models.Model):
#_name='openacademy.openacademy'

#name=fields.Char()
openacademy/security/ir.model.access.csv
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
openacademy/templates.xml
<openerp>
<data>
<!<templateid="listing">>
<!<ul>>
<!<litforeach="objects"tas="object">>
<!<atattfhref="{{root}}/objects/{{object.id}}">>
<!<ttesc="object.display_name"/>>
<!</a>>
<!</li>>
<!</ul>>
<!</template>>
<!<templateid="object">>
<!<h1><ttesc="object.display_name"/></h1>>
<!<dl>>
<!<ttforeach="object._fields"tas="field">>
<!<dt><ttesc="field"/></dt>>
<!<dd><ttesc="object[field]"/></dd>>
<!</t>>
<!</dl>>
<!</template>>
</data>
</openerp>

ObjectRelationalMapping
AkeycomponentofOdooistheORMlayer.ThislayeravoidshavingtowritemostSQLbyhandandprovides
extensibilityandsecurityservices2.

BusinessobjectsaredeclaredasPythonclassesextending Model whichintegratesthemintotheautomated


persistencesystem.
Modelscanbeconfiguredbysettinganumberofattributesattheirdefinition.Themostimportantattribute
is _name whichisrequiredanddefinesthenameforthemodelintheOdoosystem.Hereisaminimallycomplete
definitionofamodel:

fromopenerpimportmodels
classMinimalModel(models.Model):
_name='test.model'

Modelfields
Fieldsareusedtodefinewhatthemodelcanstoreandwhere.Fieldsaredefinedasattributesonthemodelclass:

fromopenerpimportmodels,fields

classLessMinimalModel(models.Model):
_name='test.model2'

name=fields.Char()

CommonAttributes

Muchlikethemodelitself,itsfieldscanbeconfigured,bypassingconfigurationattributesasparameters:

name=field.Char(required=True)

Someattributesareavailableonallfields,herearethemostcommonones:

string ( unicode ,default:field'sname)


ThelabelofthefieldinUI(visiblebyusers).
required ( bool ,default: False )
If True ,thefieldcannotbeempty,itmusteitherhaveadefaultvalueoralwaysbegivenavaluewhen
creatingarecord.
help ( unicode ,default: '' )
Longform,providesahelptooltiptousersintheUI.
index ( bool ,default: False )
RequeststhatOdoocreateadatabaseindexonthecolumn

Simplefields

Therearetwobroadcategoriesoffields:"simple"fieldswhichareatomicvaluesstoreddirectlyinthemodel's
tableand"relational"fieldslinkingrecords(ofthesamemodelorofdifferentmodels).

Exampleofsimplefieldsare Boolean , Date , Char .


Reservedfields

Odoocreatesafewfieldsinallmodels1.Thesefieldsaremanagedbythesystemandshouldn'tbewrittento.
Theycanbereadifusefulornecessary:

id ( Id )
theuniqueidentifierforarecordinitsmodel
create_date ( Datetime )
creationdateoftherecord
create_uid ( Many2one )
userwhocreatedtherecord
write_date ( Datetime )
lastmodificationdateoftherecord
write_uid ( Many2one )
userwholastmodifiedtherecord

Specialfields

Bydefault,Odooalsorequiresa name fieldonallmodelsforvariousdisplayandsearchbehaviors.Thefieldused


forthesepurposescanbeoverriddenbysetting _rec_name .

Exercise
Defineamodel
DefineanewdatamodelCourseintheopenacademymodule.Acoursehasatitleanda
description.Coursesmusthaveatitle.
Editthefile openacademy/models.py toincludeaCourseclass.
openacademy/models.py

fromopenerpimportmodels,fields,api

classCourse(models.Model):
_name='openacademy.course'

name=fields.Char(string="Title",required=True)
description=fields.Text()

Datafiles
Odooisahighlydatadrivensystem.AlthoughbehavioriscustomizedusingPythoncodepartofamodule'svalue
isinthedataitsetsupwhenloaded.
Tip
somemodulesexistsolelytoadddataintoOdoo

Moduledataisdeclaredviadatafiles,XMLfileswith <record> elements.Each <record> elementcreatesor


updatesadatabaserecord.

<openerp>
<data>
<recordmodel="{modelname}"id="{recordidentifier}">
<fieldname="{afieldname}">{avalue}</field>
</record>
</data>
<openerp>

model isthenameoftheOdoomodelfortherecord
id isanexternalidentifier,itallowsreferringtotherecord(withouthavingtoknowitsindatabaseidentifier)
<field> elementshavea name whichisthenameofthefieldinthemodel(e.g. description ).Theirbodyis
thefield'svalue.

Datafileshavetobedeclaredinthemanifestfiletobeloaded,theycanbedeclaredinthe 'data' list(always


loaded)orinthe 'demo' list(onlyloadedindemonstrationmode).
Exercise
Definedemonstrationdata
CreatedemonstrationdatafillingtheCoursesmodelwithafewdemonstrationcourses.
Editthefile openacademy/demo.xml toincludesomedata.
openacademy/demo.xml
<openerp>
<data>
<recordmodel="openacademy.course"id="course0">
<fieldname="name">Course0</field>
<fieldname="description">Course0'sdescription

Canhavemultiplelines
</field>
</record>
<recordmodel="openacademy.course"id="course1">
<fieldname="name">Course1</field>
<!nodescriptionforthisone>
</record>
<recordmodel="openacademy.course"id="course2">
<fieldname="name">Course2</field>
<fieldname="description">Course2'sdescription</field>
</record>
</data>
</openerp>

ActionsandMenus
Actionsandmenusareregularrecordsindatabase,usuallydeclaredthroughdatafiles.Actionscanbetriggered
inthreeways:

1.byclickingonmenuitems(linkedtospecificactions)
2.byclickingonbuttonsinviews(iftheseareconnectedtoactions)
3.ascontextualactionsonobject

Becausemenusaresomewhatcomplextodeclarethereisa <menuitem> shortcuttodeclarean ir.ui.menu and


connectittothecorrespondingactionmoreeasily.

<recordmodel="ir.actions.act_window"id="action_list_ideas">
<fieldname="name">Ideas</field>
<fieldname="res_model">idea.idea</field>
<fieldname="view_mode">tree,form</field>
</record>
<menuitemid="menu_ideas"parent="menu_root"name="Ideas"sequence="10"
action="action_list_ideas"/>
Danger
TheactionmustbedeclaredbeforeitscorrespondingmenuintheXMLfile.
Datafilesareexecutedsequentially,theaction's id mustbepresentinthedatabasebefore
themenucanbecreated.

Exercise
Definenewmenuentries
DefinenewmenuentriestoaccesscoursesandsessionsundertheOpenAcademymenu
entry.Ausershouldbeableto
displayalistofallthecourses
create/modifycourses

1.Create openacademy/views/openacademy.xml withanactionandthemenustriggeringthe


action
2.Addittothe data listof openacademy/__openerp__.py

openacademy/__openerp__.py
'data':[
#'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
],
#onlyloadedindemonstrationmode
'demo':[
openacademy/views/openacademy.xml
<?xmlversion="1.0"encoding="UTF8"?>
<openerp>
<data>
<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",
thatisanactionopeningavieworasetofviews
>
<recordmodel="ir.actions.act_window"id="course_list_action">
<fieldname="name">Courses</field>
<fieldname="res_model">openacademy.course</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form</field>
<fieldname="help"type="html">
<pclass="oe_view_nocontent_create">Createthefirstcourse
</p>
</field>
</record>

<!toplevelmenu:noparent>
<menuitemid="main_openacademy_menu"name="OpenAcademy"/>
<!Afirstlevelintheleftsidemenuisneeded
beforeusingaction=attribute>
<menuitemid="openacademy_menu"name="OpenAcademy"
parent="main_openacademy_menu"/>
<!thefollowingmenuitemshouldappear*after*
itsparentopenacademy_menuand*after*its
actioncourse_list_action>
actioncourse_list_action>
<menuitemid="courses_menu"name="Courses"parent="openacademy_menu"
action="course_list_action"/>
<!Fullidlocation:
action="openacademy.course_list_action"
Itisnotrequiredwhenitisthesamemodule>
</data>
</openerp>

Basicviews
Viewsdefinethewaytherecordsofamodelaredisplayed.Eachtypeofviewrepresentsamodeofvisualization
(alistofrecords,agraphoftheiraggregation,).Viewscaneitherberequestedgenericallyviatheirtype(e.g.a
listofpartners)orspecificallyviatheirid.Forgenericrequests,theviewwiththecorrecttypeandthelowest
prioritywillbeused(sothelowestpriorityviewofeachtypeisthedefaultviewforthattype).

Viewinheritanceallowsalteringviewsdeclaredelsewhere(addingorremovingcontent).

Genericviewdeclaration
Aviewisdeclaredasarecordofthemodel ir.ui.view .Theviewtypeisimpliedbytherootelementof
the arch field:

<recordmodel="ir.ui.view"id="view_id">
<fieldname="name">view.name</field>
<fieldname="model">object_name</field>
<fieldname="priority"eval="16"/>
<fieldname="arch"type="xml">
<!viewcontent:<form>,<tree>,<graph>,...>
</field>
</record>

Danger
Theview'scontentisXML.
The arch fieldmustthusbedeclaredas type="xml" tobeparsedcorrectly.

Treeviews
Treeviews,alsocalledlistviews,displayrecordsinatabularform.

Theirrootelementis <tree> .Thesimplestformofthetreeviewsimplylistsallthefieldstodisplayinthetable


(eachfieldasacolumn):
<treestring="Idealist">
<fieldname="name"/>
<fieldname="inventor_id"/>
</tree>

Formviews
Formsareusedtocreateandeditsinglerecords.

Theirrootelementis <form> .Theycomposedofhighlevelstructureelements(groups,notebooks)and


interactiveelements(buttonsandfields):

<formstring="Ideaform">
<groupcolspan="4">
<groupcolspan="2"col="2">
<separatorstring="Generalstuff"colspan="2"/>
<fieldname="name"/>
<fieldname="inventor_id"/>
</group>

<groupcolspan="2"col="2">
<separatorstring="Dates"colspan="2"/>
<fieldname="active"/>
<fieldname="invent_date"readonly="1"/>
</group>

<notebookcolspan="4">
<pagestring="Description">
<fieldname="description"nolabel="1"/>
</page>
</notebook>

<fieldname="state"/>
</group>
</form>
Exercise
CustomiseformviewusingXML
CreateyourownformviewfortheCourseobject.Datadisplayedshouldbe:thenameand
thedescriptionofthecourse.
openacademy/views/openacademy.xml
<?xmlversion="1.0"encoding="UTF8"?>
<openerp>
<data>
<recordmodel="ir.ui.view"id="course_form_view">
<fieldname="name">course.form</field>
<fieldname="model">openacademy.course</field>
<fieldname="arch"type="xml">
<formstring="CourseForm">
<sheet>
<group>
<fieldname="name"/>
<fieldname="description"/>
</group>
</sheet>
</form>
</field>
</record>

<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",

Exercise
Notebooks
IntheCourseformview,putthedescriptionfieldunderatab,suchthatitwillbeeasiertoadd
othertabslater,containingadditionalinformation.
ModifytheCourseformviewasfollows:
openacademy/views/openacademy.xml
<sheet>
<group>
<fieldname="name"/>
</group>
<notebook>
<pagestring="Description">
<fieldname="description"/>
</page>
<pagestring="About">
Thisisanexampleofnotebooks
</page>
</notebook>
</sheet>
</form>
</field>

FormviewscanalsouseplainHTMLformoreflexiblelayouts:
<formstring="IdeaForm">
<header>
<buttonstring="Confirm"type="object"name="action_confirm"
states="draft"class="oe_highlight"/>
<buttonstring="Markasdone"type="object"name="action_done"
states="confirmed"class="oe_highlight"/>
<buttonstring="Resettodraft"type="object"name="action_draft"
states="confirmed,done"/>
<fieldname="state"widget="statusbar"/>
</header>
<sheet>
<divclass="oe_title">
<labelfor="name"class="oe_edit_only"string="IdeaName"/>
<h1><fieldname="name"/></h1>
</div>
<separatorstring="General"colspan="2"/>
<groupcolspan="2"col="2">
<fieldname="description"placeholder="Ideadescription..."/>
</group>
</sheet>
</form>

Searchviews
Searchviewscustomizethesearchfieldassociatedwiththelistview(andotheraggregatedviews).Theirroot
elementis <search> andthey'recomposedoffieldsdefiningwhichfieldscanbesearchedon:

<search>
<fieldname="name"/>
<fieldname="inventor_id"/>
</search>

Ifnosearchviewexistsforthemodel,Odoogeneratesonewhichonlyallowssearchingonthe name field.


Exercise
Searchcourses
Allowsearchingforcoursesbasedontheirtitleortheirdescription.
openacademy/views/openacademy.xml
</field>
</record>

<recordmodel="ir.ui.view"id="course_search_view">
<fieldname="name">course.search</field>
<fieldname="model">openacademy.course</field>
<fieldname="arch"type="xml">
<search>
<fieldname="name"/>
<fieldname="description"/>
</search>
</field>
</record>

<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",

Relationsbetweenmodels
Arecordfromamodelmayberelatedtoarecordfromanothermodel.Forinstance,asaleorderrecordisrelated
toaclientrecordthatcontainstheclientdataitisalsorelatedtoitssaleorderlinerecords.

Exercise
Createasessionmodel
ForthemoduleOpenAcademy,weconsideramodelforsessions:asessionisan
occurrenceofacoursetaughtatagiventimeforagivenaudience.
Createamodelforsessions.Asessionhasaname,astartdate,adurationandanumberof
seats.Addanactionandamenuitemtodisplaythem.Makethenewmodelvisibleviaa
menuitem.

1.CreatetheclassSessionin openacademy/models.py .
2.Addaccesstothesessionobjectin openacademy/view/openacademy.xml .

openacademy/models.py

name=fields.Char(string="Title",required=True)
description=fields.Text()


classSession(models.Model):
_name='openacademy.session'


name=fields.Char(required=True)
start_date=fields.Date()
duration=fields.Float(digits=(6,2),help="Durationindays")
seats=fields.Integer(string="Numberofseats")
openacademy/views/openacademy.xml
<!Fullidlocation:
action="openacademy.course_list_action"
Itisnotrequiredwhenitisthesamemodule>

<!sessionformview>
<recordmodel="ir.ui.view"id="session_form_view">
<fieldname="name">session.form</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<formstring="SessionForm">
<sheet>
<group>
<fieldname="name"/>
<fieldname="start_date"/>
<fieldname="duration"/>
<fieldname="seats"/>
</group>
</sheet>
</form>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form</field>
</record>

<menuitemid="session_menu"name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</data>
</openerp>

Note
digits=(6,2) specifiestheprecisionofafloatnumber:6isthetotal
numberofdigits,while2isthenumberofdigitsafterthecomma.Note
thatitresultsinthenumberdigitsbeforethecommaisamaximum4

Relationalfields
Relationalfieldslinkrecords,eitherofthesamemodel(hierarchies)orbetweendifferentmodels.

Relationalfieldtypesare:

Many2one(other_model,ondelete='setnull')
Asimplelinktoanotherobject:

printfoo.other_id.name

Seealso
foreignkeys

One2many(other_model,related_field)

Avirtualrelationship,inverseofa Many2one .A One2many behavesasacontainerofrecords,accessingit


resultsina(possiblyempty)setofrecords:

forotherinfoo.other_ids:
printother.name

Danger
Becausea One2many isavirtualrelationship,theremustbea Many2one fieldin
the other_model ,anditsnamemustbe related_field

Many2many(other_model)

Bidirectionalmultiplerelationship,anyrecordononesidecanberelatedtoanynumberofrecordsonthe
otherside.Behavesasacontainerofrecords,accessingitalsoresultsinapossiblyemptysetofrecords:

forotherinfoo.other_ids:
printother.name

Exercise
Many2onerelations
Usingamany2one,modifytheCourseandSessionmodelstoreflecttheirrelationwithother
models:
Acoursehasaresponsibleuserthevalueofthatfieldisarecordofthebuiltin
model res.users .
Asessionhasaninstructorthevalueofthatfieldisarecordofthebuiltin
model res.partner .
Asessionisrelatedtoacoursethevalueofthatfieldisarecordofthe
model openacademy.course andisrequired.
Adapttheviews.
1.Addtherelevant Many2one fieldstothemodels,and
2.addthemintheviews.

openacademy/models.py
name=fields.Char(string="Title",required=True)
description=fields.Text()

responsible_id=fields.Many2one('res.users',
ondelete='setnull',string="Responsible",index=True)

classSession(models.Model):
_name='openacademy.session'
start_date=fields.Date()
duration=fields.Float(digits=(6,2),help="Durationindays")
seats=fields.Integer(string="Numberofseats")

instructor_id=fields.Many2one('res.partner',string="Instructor")
course_id=fields.Many2one('openacademy.course',
ondelete='cascade',string="Course",required=True)
openacademy/views/openacademy.xml
<sheet>
<group>
<fieldname="name"/>
<fieldname="responsible_id"/>
</group>
<notebook>
<pagestring="Description">
</field>
</record>

<!overridetheautomaticallygeneratedlistviewforcourses>
<recordmodel="ir.ui.view"id="course_tree_view">
<fieldname="name">course.tree</field>
<fieldname="model">openacademy.course</field>
<fieldname="arch"type="xml">
<treestring="CourseTree">
<fieldname="name"/>
<fieldname="responsible_id"/>
</tree>
</field>
</record>

<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",
<formstring="SessionForm">
<sheet>
<group>
<groupstring="General">
<fieldname="course_id"/>
<fieldname="name"/>
<fieldname="instructor_id"/>
</group>
<groupstring="Schedule">
<fieldname="start_date"/>
<fieldname="duration"/>
<fieldname="seats"/>
</group>
</group>
</sheet>
</form>
</form>
</field>
</record>

<!sessiontree/listview>
<recordmodel="ir.ui.view"id="session_tree_view">
<fieldname="name">session.tree</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<treestring="SessionTree">
<fieldname="name"/>
<fieldname="course_id"/>
</tree>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>

Exercise
Inverseone2manyrelations
Usingtheinverserelationalfieldone2many,modifythemodelstoreflecttherelationbetween
coursesandsessions.

1.Modifythe Course class,and


2.addthefieldinthecourseformview.

openacademy/models.py

responsible_id=fields.Many2one('res.users',
ondelete='setnull',string="Responsible",index=True)
session_ids=fields.One2many(
'openacademy.session','course_id',string="Sessions")

classSession(models.Model):
openacademy/views/openacademy.xml
<pagestring="Description">
<fieldname="description"/>
</page>
<pagestring="Sessions">
<fieldname="session_ids">
<treestring="Registeredsessions">
<fieldname="name"/>
<fieldname="instructor_id"/>
</tree>
</field>
</page>
</notebook>
</sheet>
Exercise
Multiplemany2manyrelations
Usingtherelationalfieldmany2many,modifytheSessionmodeltorelateeverysessiontoa
setofattendees.Attendeeswillberepresentedbypartnerrecords,sowewillrelatetothe
builtinmodel res.partner .Adapttheviewsaccordingly.

1.Modifythe Session class,and


2.addthefieldintheformview.

openacademy/models.py
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")
openacademy/views/openacademy.xml
<fieldname="seats"/>
</group>
</group>
<labelfor="attendee_ids"/>
<fieldname="attendee_ids"/>
</sheet>
</form>
</field>

Inheritance
Modelinheritance
Odooprovidestwoinheritancemechanismstoextendanexistingmodelinamodularway.

Thefirstinheritancemechanismallowsamoduletomodifythebehaviorofamodeldefinedinanothermodule:

addfieldstoamodel,
overridethedefinitionoffieldsonamodel,
addconstraintstoamodel,
addmethodstoamodel,
overrideexistingmethodsonamodel.

Thesecondinheritancemechanism(delegation)allowstolinkeveryrecordofamodeltoarecordinaparent
model,andprovidestransparentaccesstothefieldsoftheparentrecord.
Seealso
_inherit
_inherits

Viewinheritance
Insteadofmodifyingexistingviewsinplace(byoverwritingthem),Odooprovidesviewinheritancewherechildren
"extension"viewsareappliedontopofrootviews,andcanaddorremovecontentfromtheirparent.

Anextensionviewreferencesitsparentusingthe inherit_id field,andinsteadofasingleviewits arch fieldis


composedofanynumberof xpath elementsselectingandalteringthecontentoftheirparentview:

<!improvedideacategorieslist>
<recordid="idea_category_list2"model="ir.ui.view">
<fieldname="name">id.category.list2</field>
<fieldname="model">idea.category</field>
<fieldname="inherit_id"ref="id_category_list"/>
<fieldname="arch"type="xml">
<!findfielddescriptionandaddthefield
idea_idsafterit>
<xpathexpr="//field[@name='description']"position="after">
<fieldname="idea_ids"string="Numberofideas"/>
</xpath>
</field>
</record>

expr
AnXPathexpressionselectingasingleelementintheparentview.Raisesanerrorifitmatchesnoelement
ormorethanone
position

Operationtoapplytothematchedelement:

inside
appends xpath 'sbodyattheendofthematchedelement
replace
replacesthematchedelementbythe xpath 'sbody
before
insertsthe xpath 'sbodyasasiblingbeforethematchedelement
after
insertsthe xpaths 'sbodyasasiblingafterthematchedelement
attributes
alterstheattributesofthematchedelementusingspecial attribute elementsinthe xpath 'sbody

Tip
Whenmatchingasingleelement,the position attributecanbesetdirectlyontheelement
tobefound.Bothinheritancesbelowwillgivethesameresult.
<xpathexpr="//field[@name='description']"position="after">
<fieldname="idea_ids"/>
</xpath>

<fieldname="description"position="after">
<fieldname="idea_ids"/>
</field>

Exercise
Alterexistingcontent
Usingmodelinheritance,modifytheexistingPartnermodeltoaddan instructor boolean
field,andamany2manyfieldthatcorrespondstothesessionpartnerrelation
Usingviewinheritance,displaythisfieldsinthepartnerformview

Note
Note
Thisistheopportunitytointroducethedevelopermodetoinspectthe
view,finditsexternalIDandtheplacetoputthenewfield.

1.Createafile openacademy/partner.py andimportitin __init__.py


2.Createafile openacademy/views/partner.xml andadditto __openerp__.py

openacademy/__init__.py
#*coding:utf8*
from.importcontrollers
from.importmodels
from.importpartner
openacademy/__openerp__.py
#'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
#onlyloadedindemonstrationmode
'demo':[
openacademy/partner.py
#*coding:utf8*
fromopenerpimportfields,models

classPartner(models.Model):
_inherit='res.partner'

#Addanewcolumntotheres.partnermodel,bydefaultpartnersarenot
#instructors
instructor=fields.Boolean("Instructor",default=False)

session_ids=fields.Many2many('openacademy.session',
string="AttendedSessions",readonly=True)
openacademy/views/partner.xml
<?xmlversion="1.0"encoding="UTF8"?>
<openerp>
<data>
<!Addinstructorfieldtoexistingview>
<recordmodel="ir.ui.view"id="partner_instructor_form_view">
<fieldname="name">partner.instructor</field>
<fieldname="model">res.partner</field>
<fieldname="inherit_id"ref="base.view_partner_form"/>
<fieldname="arch"type="xml">
<notebookposition="inside">
<pagestring="Sessions">
<group>
<fieldname="instructor"/>
<fieldname="session_ids"/>
</group>
</page>
</notebook>
</field>
</record>

<recordmodel="ir.actions.act_window"id="contact_list_action">
<fieldname="name">Contacts</field>
<fieldname="res_model">res.partner</field>
<fieldname="view_mode">tree,form</field>
</record>
<menuitemid="configuration_menu"name="Configuration"
<menuitemid="configuration_menu"name="Configuration"
parent="main_openacademy_menu"/>
<menuitemid="contact_menu"name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
</data>
</openerp>

Domains

InOdoo,Domainsarevaluesthatencodeconditionsonrecords.Adomainisalistofcriteriausedtoselecta
subsetofamodel'srecords.Eachcriteriaisatriplewithafieldname,anoperatorandavalue.

Forinstance,whenusedontheProductmodelthefollowingdomainselectsallserviceswithaunitprice
over1000:

[('product_type','=','service'),('unit_price','>',1000)]

BydefaultcriteriaarecombinedwithanimplicitAND.Thelogicaloperators & (AND), | (OR)and ! (NOT)can


beusedtoexplicitlycombinecriteria.Theyareusedinprefixposition(theoperatorisinsertedbeforeits
argumentsratherthanbetween).Forinstancetoselectproducts"whichareservicesORhaveaunitpricewhich
isNOTbetween1000and2000":

['|',
('product_type','=','service'),
'!','&',
('unit_price','>=',1000),
('unit_price','<',2000)]

A domain parametercanbeaddedtorelationalfieldstolimitvalidrecordsfortherelationwhentryingtoselect
recordsintheclientinterface.
Exercise
Domainsonrelationalfields
WhenselectingtheinstructorforaSession,onlyinstructors(partnerswith instructor set
to True )shouldbevisible.
openacademy/models.py
duration=fields.Float(digits=(6,2),help="Durationindays")
seats=fields.Integer(string="Numberofseats")

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
Adomaindeclaredasaliterallistisevaluatedserversideandcan'trefer
todynamicvaluesontherighthandside,adomaindeclaredasastringis
evaluatedclientsideandallowsfieldnamesontherighthandside

Exercise
Morecomplexdomains
CreatenewpartnercategoriesTeacher/Level1andTeacher/Level2.Theinstructorfora
sessioncanbeeitheraninstructororateacher(ofanylevel).

1.ModifytheSessionmodel'sdomain
2.Modify openacademy/view/partner.xml togetaccesstoPartnercategories:

openacademy/models.py
seats=fields.Integer(string="Numberofseats")

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")
openacademy/views/partner.xml
<menuitemid="contact_menu"name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>

<recordmodel="ir.actions.act_window"id="contact_cat_list_action">
<fieldname="name">ContactTags</field>
<fieldname="res_model">res.partner.category</field>
<fieldname="view_mode">tree,form</field>
</record>
<menuitemid="contact_cat_menu"name="ContactTags"
parent="configuration_menu"
parent="configuration_menu"
action="contact_cat_list_action"/>

<recordmodel="res.partner.category"id="teacher1">
<fieldname="name">Teacher/Level1</field>
</record>
<recordmodel="res.partner.category"id="teacher2">
<fieldname="name">Teacher/Level2</field>
</record>
</data>
</openerp>

Computedfieldsanddefaultvalues
Sofarfieldshavebeenstoreddirectlyinandretrieveddirectlyfromthedatabase.Fieldscanalsobecomputed.In
thatcase,thefield'svalueisnotretrievedfromthedatabasebutcomputedontheflybycallingamethodofthe
model.

Tocreateacomputedfield,createafieldandsetitsattribute compute tothenameofamethod.Thecomputation


methodshouldsimplysetthevalueofthefieldtocomputeoneveryrecordin self .

Danger
self isacollection
Theobject self isarecordset,i.e.,anorderedcollectionofrecords.Itsupportsthestandard
Pythonoperationsoncollections,like len(self) and iter(self) ,plusextrasetoperations
like recs1+recs2 .
Iteratingover self givestherecordsonebyone,whereeachrecordisitselfacollectionof
size1.Youcanaccess/assignfieldsonsinglerecordsbyusingthedotnotation,
like record.name .

importrandom
fromopenerpimportmodels,fields

classComputedModel(models.Model):
_name='test.computed'

name=fields.Char(compute='_compute_name')

@api.multi
def_compute_name(self):
forrecordinself:
record.name=str(random.randint(1,1e6))

Dependencies
Dependencies
Thevalueofacomputedfieldusuallydependsonthevaluesofotherfieldsonthecomputedrecord.TheORM
expectsthedevelopertospecifythosedependenciesonthecomputemethodwiththedecorator depends() .The
givendependenciesareusedbytheORMtotriggertherecomputationofthefieldwheneversomeofits
dependencieshavebeenmodified:

fromopenerpimportmodels,fields,api

classComputedModel(models.Model):
_name='test.computed'

name=fields.Char(compute='_compute_name')
value=fields.Integer()

@api.depends('value')
def_compute_name(self):
forrecordinself:
self.name="Recordwithvalue%s"%self.value
Exercise
Computedfields
AddthepercentageoftakenseatstotheSessionmodel
Displaythatfieldinthetreeandformviews
Displaythefieldasaprogressbar

1.AddacomputedfieldtoSession
2.ShowthefieldintheSessionview:

openacademy/models.py
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="Takenseats",compute='_taken_seats')

@api.depends('seats','attendee_ids')
def_taken_seats(self):
forrinself:
ifnotr.seats:
r.taken_seats=0.0
else:
r.taken_seats=100.0*len(r.attendee_ids)/r.seats
openacademy/views/openacademy.xml
<fieldname="start_date"/>
<fieldname="duration"/>
<fieldname="seats"/>
<fieldname="taken_seats"widget="progressbar"/>
</group>
</group>
<labelfor="attendee_ids"/>
<treestring="SessionTree">
<fieldname="name"/>
<fieldname="course_id"/>
<fieldname="taken_seats"widget="progressbar"/>
</tree>
</field>
</record>

Defaultvalues
Anyfieldcanbegivenadefaultvalue.Inthefielddefinition,addtheoption default=X where X iseitheraPython
literalvalue(boolean,integer,float,string),orafunctiontakingarecordsetandreturningavalue:

name=fields.Char(default="Unknown")
user_id=fields.Many2one('res.users',default=lambdaself:self.env.user)
Note
Theobject self.env givesaccesstorequestparametersandotherusefulthings:
self.env.cr or self._cr isthedatabasecursorobjectitisusedforqueryingthe
database
self.env.uid or self._uid isthecurrentuser'sdatabaseid
self.env.user isthecurrentuser'srecord
self.env.context or self._context isthecontextdictionary
self.env.ref(xml_id) returnstherecordcorrespondingtoanXMLid
self.env[model_name] returnsaninstanceofthegivenmodel

Exercise
ActiveobjectsDefaultvalues
Definethestart_datedefaultvalueastoday(see Date ).
Addafield active intheclassSession,andsetsessionsasactivebydefault.
openacademy/models.py
_name='openacademy.session'

name=fields.Char(required=True)
start_date=fields.Date(default=fields.Date.today)
duration=fields.Float(digits=(6,2),help="Durationindays")
seats=fields.Integer(string="Numberofseats")
active=fields.Boolean(default=True)

instructor_id=fields.Many2one('res.partner',string="Instructor",
domain=['|',('instructor','=',True),
openacademy/views/openacademy.xml
<fieldname="course_id"/>
<fieldname="name"/>
<fieldname="instructor_id"/>
<fieldname="active"/>
</group>
<groupstring="Schedule">
<fieldname="start_date"/>

Note
Odoohasbuiltinrulesmakingfieldswithan active fieldset
to False invisible.

Onchange
The"onchange"mechanismprovidesawayfortheclientinterfacetoupdateaformwhenevertheuserhasfilled
inavalueinafield,withoutsavinganythingtothedatabase.

Forinstance,supposeamodelhasthreefields amount , unit_price and price ,andyouwanttoupdatetheprice


ontheformwhenanyoftheotherfieldsismodified.Toachievethis,defineamethodwhere self representsthe
recordintheformview,anddecorateitwith onchange() tospecifyonwhichfieldithastobetriggered.Any
changeyoumakeon self willbereflectedontheform.

<!contentofformview>
<fieldname="amount"/>
<fieldname="unit_price"/>
<fieldname="price"readonly="1"/>

#onchangehandler
@api.onchange('amount','unit_price')
def_onchange_price(self):
#setautochangingfield
self.price=self.amount*self.unit_price
#Canoptionallyreturnawarninganddomains
return{
'warning':{
'title':"Somethingbadhappened",
'message':"Itwasverybadindeed",
}
}

Forcomputedfields,valued onchange behaviorisbuiltinascanbeseenbyplayingwiththeSessionform:


changethenumberofseatsorparticipants,andthe taken_seats progressbarisautomaticallyupdated.
Exercise
Warning
Addanexplicitonchangetowarnaboutinvalidvalues,likeanegativenumberofseats,or
moreparticipantsthanseats.
openacademy/models.py
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):
ifself.seats<0:
return{
'warning':{
'title':"Incorrect'seats'value",
'message':"Thenumberofavailableseatsmaynotbenegative",
},
}
ifself.seats<len(self.attendee_ids):
return{
'warning':{
'title':"Toomanyattendees",
'message':"Increaseseatsorremoveexcessattendees",
},
}

Modelconstraints
Odooprovidestwowaystosetupautomaticallyverifiedinvariants: Pythonconstraints and SQLconstraints .

APythonconstraintisdefinedasamethoddecoratedwith constrains() ,andinvokedonarecordset.The


decoratorspecifieswhichfieldsareinvolvedintheconstraint,sothattheconstraintisautomaticallyevaluated
whenoneofthemismodified.Themethodisexpectedtoraiseanexceptionifitsinvariantisnotsatisfied:

fromopenerp.exceptionsimportValidationError

@api.constrains('age')
def_check_something(self):
forrecordinself:
ifrecord.age>20:
raiseValidationError("Yourrecordistooold:%s"%record.age)
#allrecordspassedthetest,don'treturnanything
Exercise
AddPythonconstraints
Addaconstraintthatchecksthattheinstructorisnotpresentintheattendeesofhis/herown
session.
openacademy/models.py
#*coding:utf8*

fromopenerpimportmodels,fields,api,exceptions

classCourse(models.Model):
_name='openacademy.course'
'message':"Increaseseatsorremoveexcessattendees",
},
}

@api.constrains('instructor_id','attendee_ids')
def_check_instructor_not_in_attendees(self):
forrinself:
ifr.instructor_idandr.instructor_idinr.attendee_ids:
raiseexceptions.ValidationError("Asession'sinstructorcan'tbeanattendee")

SQLconstraintsaredefinedthroughthemodelattribute _sql_constraints .Thelatterisassignedtoalistoftriples


ofstrings (name,sql_definition,message) ,where name isavalidSQLconstraintname, sql_definition is
atable_constraintexpression,and message istheerrormessage.
Exercise
AddSQLconstraints
WiththehelpofPostgreSQL'sdocumentation,addthefollowingconstraints:

1.CHECKthatthecoursedescriptionandthecoursetitlearedifferent
2.MaketheCourse'snameUNIQUE

openacademy/models.py
session_ids=fields.One2many(
'openacademy.session','course_id',string="Sessions")

_sql_constraints=[
('name_description_check',
'CHECK(name!=description)',
"Thetitleofthecourseshouldnotbethedescription"),

('name_unique',
'UNIQUE(name)',
"Thecoursetitlemustbeunique"),
]

classSession(models.Model):
_name='openacademy.session'

Exercise
Exercise6Addaduplicateoption
SinceweaddedaconstraintfortheCoursenameuniqueness,itisnotpossibletousethe
"duplicate"functionanymore(FormDuplicate).
Reimplementyourown"copy"methodwhichallowstoduplicatetheCourseobject,changing
theoriginalnameinto"Copyof[originalname]".
openacademy/models.py
session_ids=fields.One2many(
'openacademy.session','course_id',string="Sessions")

@api.multi
defcopy(self,default=None):
default=dict(defaultor{})

copied_count=self.search_count(
[('name','=like',u"Copyof{}%".format(self.name))])
ifnotcopied_count:
new_name=u"Copyof{}".format(self.name)
else:
new_name=u"Copyof{}({})".format(self.name,copied_count)

default['name']=new_name
returnsuper(Course,self).copy(default)

_sql_constraints=[
('name_description_check',
'CHECK(name!=description)',
AdvancedViews
Treeviews
Treeviewscantakesupplementaryattributestofurthercustomizetheirbehavior:

colors

mappingsofcolorstoconditions.Iftheconditionevaluatesto True ,thecorrespondingcolorisappliedto


therow:

<treestring="IdeaCategories"colors="blue:state=='draft';red:state=='trashed'">
<fieldname="name"/>
<fieldname="state"/>
</tree>

Clausesareseparatedby ; ,thecolorandconditionareseparatedby : .

editable
Either "top" or "bottom" .Makesthetreevieweditableinplace(ratherthanhavingtogothroughtheform
view),thevalueisthepositionwherenewrowsappear.

Exercise
Listcoloring
ModifytheSessiontreeviewinsuchawaythatsessionslastinglessthan5daysarecolored
blue,andtheoneslastingmorethan15daysarecoloredred.
Modifythesessiontreeview:
openacademy/views/openacademy.xml
<fieldname="name">session.tree</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<treestring="SessionTree"colors="#0000ff:duration&lt;5;red:duration&gt;15">
<fieldname="name"/>
<fieldname="course_id"/>
<fieldname="duration"invisible="1"/>
<fieldname="taken_seats"widget="progressbar"/>
</tree>
</field>

Calendars
Calendars
Displaysrecordsascalendarevents.Theirrootelementis <calendar> andtheirmostcommonattributesare:

color
Thenameofthefieldusedforcolorsegmentation.Colorsareautomaticallydistributedtoevents,butevents
inthesamecolorsegment(recordswhichhavethesamevaluefortheir @color field)willbegiventhe
samecolor.
date_start
record'sfieldholdingthestartdate/timefortheevent
date_stop (optional)
record'sfieldholdingtheenddate/timefortheevent

field(todefinethelabelforeachcalendarevent)

<calendarstring="Ideas"date_start="invent_date"color="inventor_id">
<fieldname="name"/>
</calendar>

Exercise
Calendarview
AddaCalendarviewtotheSessionmodelenablingtheusertoviewtheeventsassociatedto
theOpenAcademy.

1.Addan end_date fieldcomputedfrom start_date and duration

Tip
theinversefunctionmakesthefieldwritable,andallowsmovingthe
sessions(viadraganddrop)inthecalendarview

2.AddacalendarviewtotheSessionmodel
3.AndaddthecalendarviewtotheSessionmodel'sactions

openacademy/models.py
#*coding:utf8*

fromdatetimeimporttimedelta
fromopenerpimportmodels,fields,api,exceptions

classCourse(models.Model):
attendee_ids=fields.Many2many('res.partner',string="Attendees")

taken_seats=fields.Float(string="Takenseats",compute='_taken_seats')
end_date=fields.Date(string="EndDate",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):
forrinself:
ifnot(r.start_dateandr.duration):
r.end_date=r.start_date
continue

#Adddurationtostart_date,but:Monday+5days=Saturday,so
#subtractonesecondtogetonFridayinstead
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):
forrinself:
ifnot(r.start_dateandr.end_date):
continue

#Computethedifferencebetweendates,but:FridayMonday=4days,
#soaddonedaytoget5daysinstead
start_date=fields.Datetime.from_string(r.start_date)
end_date=fields.Datetime.from_string(r.end_date)
r.duration=(end_datestart_date).days+1

@api.constrains('instructor_id','attendee_ids')
def_check_instructor_not_in_attendees(self):
forrinself:
openacademy/views/openacademy.xml
</field>
</record>

<!calendarview>
<recordmodel="ir.ui.view"id="session_calendar_view">
<fieldname="name">session.calendar</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<calendarstring="SessionCalendar"date_start="start_date"
date_stop="end_date"
color="instructor_id">
<fieldname="name"/>
</calendar>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form,calendar</field>
</record>

<menuitemid="session_menu"name="Sessions"

Searchviews

Searchview elementscanhavea thatoverridesthedomaingeneratedforsearching


Searchview <field> elementscanhavea @filter_domain thatoverridesthedomaingeneratedforsearching
onthegivenfield.Inthegivendomain, self representsthevalueenteredbytheuser.Intheexamplebelow,itis
usedtosearchonbothfields name and description .

Searchviewscanalsocontain <filter> elements,whichactastogglesforpredefinedsearches.Filtersmust


haveoneofthefollowingattributes:

domain
addthegivendomaintothecurrentsearch
context
addsomecontexttothecurrentsearchusethekey group_by togroupresultsonthegivenfieldname

<searchstring="Ideas">
<fieldname="name"/>
<fieldname="description"string="Nameanddescription"
filter_domain="['|',('name','ilike',self),('description','ilike',self)]"
<fieldname="inventor_id"/>
<fieldname="country_id"widget="selection"/>

<filtername="my_ideas"string="MyIdeas"
domain="[('inventor_id','=',uid)]"/>
<groupstring="GroupBy">
<filtername="group_by_inventor"string="Inventor"
context="{'group_by':'inventor_id'}"/>
</group>
</search>

Touseanondefaultsearchviewinanaction,itshouldbelinkedusingthe search_view_id fieldoftheaction


record.

Theactioncanalsosetdefaultvaluesforsearchfieldsthroughits context field:contextkeysofthe


form search_default_field_name willinitializefield_namewiththeprovidedvalue.Searchfiltersmusthavean
optional @name tohaveadefaultandbehaveasbooleans(theycanonlybeenabledbydefault).
Exercise
Searchviews

1.Addabuttontofilterthecoursesforwhichthecurrentuseristheresponsibleinthe
coursesearchview.Makeitselectedbydefault.
2.Addabuttontogroupcoursesbyresponsibleuser.

openacademy/views/openacademy.xml
<search>
<fieldname="name"/>
<fieldname="description"/>
<filtername="my_courses"string="MyCourses"
domain="[('responsible_id','=',uid)]"/>
<groupstring="GroupBy">
<filtername="by_responsible"string="Responsible"
context="{'group_by':'responsible_id'}"/>
</group>
</search>
</field>
</record>
<fieldname="res_model">openacademy.course</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form</field>
<fieldname="context"eval="{'search_default_my_courses':1}"/>
<fieldname="help"type="html">
<pclass="oe_view_nocontent_create">Createthefirstcourse
</p>

Gantt
Horizontalbarchartstypicallyusedtoshowprojectplanningandadvancement,theirrootelementis <gantt> .

<ganttstring="Ideas"date_start="invent_date"color="inventor_id">
<levelobject="idea.idea"link="id"domain="[]">
<fieldname="inventor_id"/>
</level>
</gantt>

Exercise
Ganttcharts
AddaGanttChartenablingtheusertoviewthesessionsschedulinglinkedtotheOpen
Academymodule.Thesessionsshouldbegroupedbyinstructor.

1.Createacomputedfieldexpressingthesession'sdurationinhours
2.Addtheganttview'sdefinition,andaddtheganttviewtotheSessionmodel'saction

openacademy/models.py
openacademy/models.py
end_date=fields.Date(string="EndDate",store=True,
compute='_get_end_date',inverse='_set_end_date')

hours=fields.Float(string="Durationinhours",
compute='_get_hours',inverse='_set_hours')

@api.depends('seats','attendee_ids')
def_taken_seats(self):
forrinself:
end_date=fields.Datetime.from_string(r.end_date)
r.duration=(end_datestart_date).days+1

@api.depends('duration')
def_get_hours(self):
forrinself:
r.hours=r.duration*24

def_set_hours(self):
forrinself:
r.duration=r.hours/24

@api.constrains('instructor_id','attendee_ids')
def_check_instructor_not_in_attendees(self):
forrinself:
openacademy/views/openacademy.xml
</field>
</record>

<recordmodel="ir.ui.view"id="session_gantt_view">
<fieldname="name">session.gantt</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<ganttstring="SessionGantt"color="course_id"
date_start="start_date"date_delay="hours"
default_group_by='instructor_id'>
<fieldname="name"/>
</gantt>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form,calendar,gantt</field>
</record>

<menuitemid="session_menu"name="Sessions"

Graphviews
Graphviewsallowaggregatedoverviewandanalysisofmodels,theirrootelementis <graph> .

Graphviewshave4displaymodes,thedefaultmodeisselectedusingthe @type attribute.

Pivot
amultidimensionaltable,allowstheselectionoffilersanddimensionstogettherightaggregateddataset
beforemovingtoamoregraphicaloverview
Bar(default)

abarchart,thefirstdimensionisusedtodefinegroupsonthehorizontalaxis,otherdimensionsdefine
aggregatedbarswithineachgroup.

Bydefaultbarsaresidebyside,theycanbestackedbyusing @stacked="True" onthe <graph>

Line
2dimensionallinechart
Pie
2dimensionalpie

Graphviewscontain <field> withamandatory @type attributetakingthevalues:

row (default)
thefieldshouldbeaggregatedbydefault
measure
thefieldshouldbeaggregatedratherthangroupedon

<graphstring="TotalideascorebyInventor">
<fieldname="inventor_id"/>
<fieldname="score"type="measure"/>
</graph>
Warning
Graphviewsperformaggregationsondatabasevalues,theydonotworkwithnonstored
computedfields.

Exercise
Graphview
AddaGraphviewintheSessionobjectthatdisplays,foreachcourse,thenumberof
attendeesundertheformofabarchart.

1.Addthenumberofattendeesasastoredcomputedfield
2.Thenaddtherelevantview

openacademy/models.py
hours=fields.Float(string="Durationinhours",
compute='_get_hours',inverse='_set_hours')

attendees_count=fields.Integer(
string="Attendeescount",compute='_get_attendees_count',store=True)

@api.depends('seats','attendee_ids')
def_taken_seats(self):
forrinself:
forrinself:
r.duration=r.hours/24

@api.depends('attendee_ids')
def_get_attendees_count(self):
forrinself:
r.attendees_count=len(r.attendee_ids)

@api.constrains('instructor_id','attendee_ids')
def_check_instructor_not_in_attendees(self):
forrinself:
openacademy/views/openacademy.xml
</field>
</record>

<recordmodel="ir.ui.view"id="openacademy_session_graph_view">
<fieldname="name">openacademy.session.graph</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<graphstring="ParticipationsbyCourses">
<fieldname="course_id"/>
<fieldname="attendees_count"type="measure"/>
</graph>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form,calendar,gantt,graph</field>
</record>
</record>

<menuitemid="session_menu"name="Sessions"

Kanban
Usedtoorganizetasks,productionprocesses,etctheirrootelementis <kanban> .

Akanbanviewshowsasetofcardspossiblygroupedincolumns.Eachcardrepresentsarecord,andeach
columnthevaluesofanaggregationfield.

Forinstance,projecttasksmaybeorganizedbystage(eachcolumnisastage),orbyresponsible(eachcolumn
isauser),andsoon.

Kanbanviewsdefinethestructureofeachcardasamixofformelements(includingbasicHTML)andQWeb.

Exercise
Kanbanview
AddaKanbanviewthatdisplayssessionsgroupedbycourse(columnsarethuscourses).

1.Addaninteger color fieldtotheSessionmodel


2.Addthekanbanviewandupdatetheaction

openacademy/models.py
duration=fields.Float(digits=(6,2),help="Durationindays")
seats=fields.Integer(string="Numberofseats")
active=fields.Boolean(default=True)
color=fields.Integer()

instructor_id=fields.Many2one('res.partner',string="Instructor",
domain=['|',('instructor','=',True),
openacademy/views/openacademy.xml
</record>

<recordmodel="ir.ui.view"id="view_openacad_session_kanban">
<fieldname="name">openacad.session.kanban</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<kanbandefault_group_by="course_id">
<fieldname="color"/>
<templates>
<ttname="kanbanbox">
<div
tattfclass="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
oe_kanban_global_click_editoe_semantic_html_override
oe_kanban_card{{record.group_fancy==1?'oe_kanban_card_fanc
<divclass="oe_dropdown_kanban">
<!dropdownmenu>
<divclass="oe_dropdown_toggle">
<spanclass="oe_e">#</span>
<ulclass="oe_dropdown_menu">
<li>
<atype="delete">Delete</a>
<atype="delete">Delete</a>
</li>
<li>
<ulclass="oe_kanban_colorpicker"
datafield="color"/>
</li>
</ul>
</div>
<divclass="oe_clear"></div>
</div>
<divtattfclass="oe_kanban_content">
<!title>
Sessionname:
<fieldname="name"/>
<br/>
Startdate:
<fieldname="start_date"/>
<br/>
duration:
<fieldname="duration"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>

<recordmodel="ir.actions.act_window"id="session_list_action">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form,calendar,gantt,graph,kanban</field>
</record>

<menuitemid="session_menu"name="Sessions"
parent="openacademy_menu"

Workflows
Workflowsaremodelsassociatedtobusinessobjectsdescribingtheirdynamics.Workflowsarealsousedtotrack
processesthatevolveovertime.

Exercise
Almostaworkflow
Adda state fieldtotheSessionmodel.Itwillbeusedtodefineaworkflowish.
Asesioncanhavethreepossiblestates:Draft(default),ConfirmedandDone.
Inthesessionform,adda(readonly)fieldtovisualizethestate,andbuttonstochangeit.
Thevalidtransitionsare:
Draft>Confirmed
Confirmed>Draft
Confirmed>Done
Done>Draft

1.Addanew state field


2.Addstatetransitioningmethods,thosecanbecalledfromviewbuttonstochangethe
record'sstate
3.Andaddtherelevantbuttonstothesession'sformview

openacademy/models.py
attendees_count=fields.Integer(
string="Attendeescount",compute='_get_attendees_count',store=True)

state=fields.Selection([
('draft',"Draft"),
('confirmed',"Confirmed"),
('done',"Done"),
],default='draft')

@api.multi
defaction_draft(self):
self.state='draft'

@api.multi
defaction_confirm(self):
self.state='confirmed'

@api.multi
defaction_done(self):
self.state='done'

@api.depends('seats','attendee_ids')
def_taken_seats(self):
forrinself:
openacademy/views/openacademy.xml
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<formstring="SessionForm">
<header>
<buttonname="action_draft"type="object"
string="Resettodraft"
states="confirmed,done"/>
<buttonname="action_confirm"type="object"
string="Confirm"states="draft"
class="oe_highlight"/>
<buttonname="action_done"type="object"
string="Markasdone"states="confirmed"
class="oe_highlight"/>
<fieldname="state"widget="statusbar"/>
</header>

<sheet>
<group>
<groupstring="General">

WorkflowsmaybeassociatedwithanyobjectinOdoo,andareentirelycustomizable.Workflowsareusedto
structureandmanagethelifecyclesofbusinessobjectsanddocuments,anddefinetransitions,triggers,etc.with
graphicaltools.Workflows,activities(nodesoractions)andtransitions(conditions)aredeclaredasXMLrecords,
asusual.Thetokensthatnavigateinworkflowsarecalledworkitems.

Warning
Aworkflowassociatedwithamodelisonlycreatedwhenthemodel'srecordsarecreated.
Thusthereisnoworkflowinstanceassociatedwithsessioninstancescreatedbeforethe
workflow'sdefinition

Exercise
Workflow
ReplacetheadhocSessionworkflowbyarealworkflow.TransformtheSessionformviewso
itsbuttonscalltheworkflowinsteadofthemodel'smethods.
openacademy/__openerp__.py
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
],
#onlyloadedindemonstrationmode
'demo':[
openacademy/models.py
('draft',"Draft"),
('confirmed',"Confirmed"),
('done',"Done"),
])

@api.multi
defaction_draft(self):
openacademy/views/openacademy.xml
<fieldname="arch"type="xml">
<formstring="SessionForm">
<header>
<buttonname="draft"type="workflow"
string="Resettodraft"
states="confirmed,done"/>
<buttonname="confirm"type="workflow"
string="Confirm"states="draft"
class="oe_highlight"/>
<buttonname="done"type="workflow"
string="Markasdone"states="confirmed"
class="oe_highlight"/>
<fieldname="state"widget="statusbar"/>
openacademy/views/session_workflow.xml
<openerp>
<data>
<recordmodel="workflow"id="wkf_session">
<fieldname="name">OpenAcademysessionsworkflow</field>
<fieldname="osv">openacademy.session</field>
<fieldname="on_create">True</field>
</record>

<recordmodel="workflow.activity"id="draft">
<fieldname="name">Draft</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="flow_start"eval="True"/>
<fieldname="kind">function</field>
<fieldname="action">action_draft()</field>
</record>
<recordmodel="workflow.activity"id="confirmed">
<fieldname="name">Confirmed</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="kind">function</field>
<fieldname="action">action_confirm()</field>
</record>
<recordmodel="workflow.activity"id="done">
<fieldname="name">Done</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="kind">function</field>
<fieldname="action">action_done()</field>
</record>

<recordmodel="workflow.transition"id="session_draft_to_confirmed">
<fieldname="act_from"ref="draft"/>
<fieldname="act_to"ref="confirmed"/>
<fieldname="signal">confirm</field>
</record>
<recordmodel="workflow.transition"id="session_confirmed_to_draft">
<fieldname="act_from"ref="confirmed"/>
<fieldname="act_to"ref="draft"/>
<fieldname="signal">draft</field>
</record>
<recordmodel="workflow.transition"id="session_done_to_draft">
<fieldname="act_from"ref="done"/>
<fieldname="act_to"ref="draft"/>
<fieldname="signal">draft</field>
</record>
<recordmodel="workflow.transition"id="session_confirmed_to_done">
<fieldname="act_from"ref="confirmed"/>
<fieldname="act_to"ref="done"/>
<fieldname="signal">done</field>
</record>
</data>
</openerp>

Tip
Inordertocheckifinstancesoftheworkflowarecorrectlycreated
alongsidesessions,gotoSettingsTechnicalWorkflowsInstances
Exercise
Automatictransitions
AutomaticallytransitionsessionsfromDrafttoConfirmedwhenmorethanhalfthesession's
seatsarereserved.
openacademy/views/session_workflow.xml
<fieldname="act_to"ref="done"/>
<fieldname="signal">done</field>
</record>

<recordmodel="workflow.transition"id="session_auto_confirm_half_filled">
<fieldname="act_from"ref="draft"/>
<fieldname="act_to"ref="confirmed"/>
<fieldname="condition">taken_seats&gt;50</field>
</record>
</data>
</openerp>
Exercise
Serveractions
ReplacethePythonmethodsforsynchronizingsessionstatebyserveractions.
BoththeworkflowandtheserveractionscouldhavebeencreatedentirelyfromtheUI.
openacademy/views/session_workflow.xml
<fieldname="on_create">True</field>
</record>

<recordmodel="ir.actions.server"id="set_session_to_draft">
<fieldname="name">SetsessiontoDraft</field>
<fieldname="model_id"ref="model_openacademy_session"/>
<fieldname="code">
model.search([('id','in',context['active_ids'])]).action_draft()
</field>
</record>
<recordmodel="workflow.activity"id="draft">
<fieldname="name">Draft</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="flow_start"eval="True"/>
<fieldname="kind">dummy</field>
<fieldname="action"></field>
<fieldname="action_id"ref="set_session_to_draft"/>
</record>

<recordmodel="ir.actions.server"id="set_session_to_confirmed">
<fieldname="name">SetsessiontoConfirmed</field>
<fieldname="model_id"ref="model_openacademy_session"/>
<fieldname="code">
model.search([('id','in',context['active_ids'])]).action_confirm()
</field>
</record>
<recordmodel="workflow.activity"id="confirmed">
<fieldname="name">Confirmed</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="kind">dummy</field>
<fieldname="action"></field>
<fieldname="action_id"ref="set_session_to_confirmed"/>
</record>

<recordmodel="ir.actions.server"id="set_session_to_done">
<fieldname="name">SetsessiontoDone</field>
<fieldname="model_id"ref="model_openacademy_session"/>
<fieldname="code">
model.search([('id','in',context['active_ids'])]).action_done()
</field>
</record>
<recordmodel="workflow.activity"id="done">
<fieldname="name">Done</field>
<fieldname="wkf_id"ref="wkf_session"/>
<fieldname="kind">dummy</field>
<fieldname="action"></field>
<fieldname="action_id"ref="set_session_to_done"/>
</record>

<recordmodel="workflow.transition"id="session_draft_to_confirmed">
Security
Accesscontrolmechanismsmustbeconfiguredtoachieveacoherentsecuritypolicy.

Groupbasedaccesscontrolmechanisms
Groupsarecreatedasnormalrecordsonthemodel res.groups ,andgrantedmenuaccessviamenudefinitions.
Howeverevenwithoutamenu,objectsmaystillbeaccessibleindirectly,soactualobjectlevelpermissions(read,
write,create,unlink)mustbedefinedforgroups.TheyareusuallyinsertedviaCSVfilesinsidemodules.Itisalso
possibletorestrictaccesstospecificfieldsonavieworobjectusingthefield'sgroupsattribute.

Accessrights
Accessrightsaredefinedasrecordsofthemodel ir.model.access .Eachaccessrightisassociatedtoamodel,a
group(ornogroupforglobalaccess),andasetofpermissions:read,write,create,unlink.Suchaccessrightsare
usuallycreatedbyaCSVfilenamedafteritsmodel: 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
AddaccesscontrolthroughtheOpenERPinterface
Createanewuser"JohnSmith".Thencreateagroup"OpenAcademy/SessionRead"with
readaccesstotheSessionmodel.

1.CreateanewuserJohnSmiththroughSettingsUsersUsers

2.Createanewgroup session_read throughSettingsUsersGroups,itshouldhave


readaccessontheSessionmodel
3.EditJohnSmithtomakethemamemberof session_read
4.LoginasJohnSmithtochecktheaccessrightsarecorrect

Exercise
Addaccesscontrolthroughdatafilesinyourmodule
Usingdatafiles,
CreateagroupOpenAcademy/ManagerwithfullaccesstoallOpenAcademymodels
MakeSessionandCoursereadablebyallusers

1.Createanewfile openacademy/security/security.xml toholdtheOpenAcademy


Managergroup
2.Editthefile openacademy/security/ir.model.access.csv withtheaccessrightstothe
models
3.Finallyupdate openacademy/__openerp__.py toaddthenewdatafilestoit

openacademy/__openerp__.py

#alwaysloaded
'data':[
'security/security.xml',
'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
openacademy/security/ir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
course_manager,coursemanager,model_openacademy_course,group_manager,1,1,1,1
session_manager,sessionmanager,model_openacademy_session,group_manager,1,1,1,1
course_read_all,courseall,model_openacademy_course,,1,0,0,0
session_read_all,sessionall,model_openacademy_session,,1,0,0,0
openacademy/security/security.xml
<openerp>
<data>
<recordid="group_manager"model="res.groups">
<fieldname="name">OpenAcademy/Manager</field>
</record>
</data>
</openerp>
Recordrules
Arecordrulerestrictstheaccessrightstoasubsetofrecordsofthegivenmodel.Aruleisarecordofthe
model ir.rule ,andisassociatedtoamodel,anumberofgroups(many2manyfield),permissionstowhichthe
restrictionapplies,andadomain.Thedomainspecifiestowhichrecordstheaccessrightsarelimited.

Hereisanexampleofarulethatpreventsthedeletionofleadsthatarenotinstate cancel .Noticethatthevalue


ofthefield groups mustfollowthesameconventionasthemethod write() oftheORM.

<recordid="delete_cancelled_only"model="ir.rule">
<fieldname="name">Onlycancelledleadsmaybedeleted</field>
<fieldname="model_id"ref="crm.model_crm_lead"/>
<fieldname="groups"eval="[(4,ref('base.group_sale_manager'))]"/>
<fieldname="perm_read"eval="0"/>
<fieldname="perm_write"eval="0"/>
<fieldname="perm_create"eval="0"/>
<fieldname="perm_unlink"eval="1"/>
<fieldname="domain_force">[('state','=','cancel')]</field>
</record>

Exercise
Recordrule
AddarecordruleforthemodelCourseandthegroup"OpenAcademy/Manager",that
restricts write and unlink accessestotheresponsibleofacourse.Ifacoursehasno
responsible,allusersofthegroupmustbeabletomodifyit.
Createanewrulein openacademy/security/security.xml :
openacademy/security/security.xml
<recordid="group_manager"model="res.groups">
<fieldname="name">OpenAcademy/Manager</field>
</record>

<recordid="only_responsible_can_modify"model="ir.rule">
<fieldname="name">OnlyResponsiblecanmodifyCourse</field>
<fieldname="model_id"ref="model_openacademy_course"/>
<fieldname="groups"eval="[(4,ref('openacademy.group_manager'))]"/>
<fieldname="perm_read"eval="0"/>
<fieldname="perm_write"eval="1"/>
<fieldname="perm_create"eval="0"/>
<fieldname="perm_unlink"eval="1"/>
<fieldname="domain_force">
['|',('responsible_id','=',False),
('responsible_id','=',user.id)]
</field>
</record>
</data>
</openerp>
Wizards
Wizardsdescribeinteractivesessionswiththeuser(ordialogboxes)throughdynamicforms.Awizardissimplya
modelthatextendstheclass TransientModel insteadof Model .Theclass TransientModel extends Model and
reuseallitsexistingmechanisms,withthefollowingparticularities:

Wizardrecordsarenotmeanttobepersistenttheyareautomaticallydeletedfromthedatabaseafteracertain
time.Thisiswhytheyarecalledtransient.
Wizardmodelsdonotrequireexplicitaccessrights:usershaveallpermissionsonwizardrecords.
Wizardrecordsmayrefertoregularrecordsorwizardrecordsthroughmany2onefields,butregular
recordscannotrefertowizardrecordsthroughamany2onefield.

Wewanttocreateawizardthatallowuserstocreateattendeesforaparticularsession,orforalistofsessionsat
once.

Exercise
Definethewizard
Createawizardmodelwithamany2onerelationshipwiththeSessionmodelanda
many2manyrelationshipwiththePartnermodel.
Addanewfile openacademy/wizard.py :
openacademy/__init__.py
from.importcontrollers
from.importmodels
from.importpartner
from.importwizard
openacademy/wizard.py
#*coding:utf8*

fromopenerpimportmodels,fields,api

classWizard(models.TransientModel):
_name='openacademy.wizard'

session_id=fields.Many2one('openacademy.session',
string="Session",required=True)
attendee_ids=fields.Many2many('res.partner',string="Attendees")

Launchingwizards
Wizardsarelaunchedby ir.actions.act_window records,withthefield target settothevalue new .Thelatter
opensthewizardviewintoapopupwindow.Theactionmaybetriggeredbyamenuitem.

Thereisanotherwaytolaunchthewizard:usingan ir.actions.act_window recordlikeabove,butwithanextra


field src_model thatspecifiesinthecontextofwhichmodeltheactionisavailable.Thewizardwillappearinthe
contextualactionsofthemodel,abovethemainview.BecauseofsomeinternalhooksintheORM,suchanaction
isdeclaredinXMLwiththetag act_window .
<act_windowid="launch_the_wizard"
name="LaunchtheWizard"
src_model="context_model_name"
res_model="wizard_model_name"
view_mode="form"
target="new"
key2="client_action_multi"/>

Wizardsuseregularviewsandtheirbuttonsmayusetheattribute special="cancel" toclosethewizardwindow


withoutsaving.
Exercise
Launchthewizard

1.Defineaformviewforthewizard.
2.AddtheactiontolaunchitinthecontextoftheSessionmodel.
3.Defineadefaultvalueforthesessionfieldinthewizardusethecontext
parameter self._context toretrievethecurrentsession.

openacademy/wizard.py
classWizard(models.TransientModel):
_name='openacademy.wizard'

def_default_session(self):
returnself.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")
openacademy/views/openacademy.xml
parent="openacademy_menu"
action="session_list_action"/>

<recordmodel="ir.ui.view"id="wizard_form_view">
<fieldname="name">wizard.form</field>
<fieldname="model">openacademy.wizard</field>
<fieldname="arch"type="xml">
<formstring="AddAttendees">
<group>
<fieldname="session_id"/>
<fieldname="attendee_ids"/>
</group>
</form>
</field>
</record>

<act_windowid="launch_session_wizard"
name="AddAttendees"
src_model="openacademy.session"
res_model="openacademy.wizard"
view_mode="form"
target="new"
key2="client_action_multi"/>
</data>
</openerp>

Exercise
Registerattendees
Addbuttonstothewizard,andimplementthecorrespondingmethodforaddingthe
attendeestothegivensession.
openacademy/views/openacademy.xml
<fieldname="attendee_ids"/>
</group>
<footer>
<buttonname="subscribe"type="object"
string="Subscribe"class="oe_highlight"/>
or
<buttonspecial="cancel"string="Cancel"/>
</footer>
</form>
</field>
</record>
openacademy/wizard.py
session_id=fields.Many2one('openacademy.session',
string="Session",required=True,default=_default_session)
attendee_ids=fields.Many2many('res.partner',string="Attendees")

@api.multi
defsubscribe(self):
self.session_id.attendee_ids|=self.attendee_ids
return{}

Exercise
Registerattendeestomultiplesessions
Modifythewizardmodelsothatattendeescanberegisteredtomultiplesessions.
openacademy/views/openacademy.xml
<formstring="AddAttendees">
<group>
<fieldname="session_ids"/>
<fieldname="attendee_ids"/>
</group>
<footer>
<buttonname="subscribe"type="object"
openacademy/wizard.py
classWizard(models.TransientModel):
_name='openacademy.wizard'

def_default_sessions(self):
returnself.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
defsubscribe(self):
forsessioninself.session_ids:
session.attendee_ids|=self.attendee_ids
return{}

Internationalization
Eachmodulecanprovideitsowntranslationswithinthei18ndirectory,byhavingfilesnamedLANG.powhere
LANGisthelocalecodeforthelanguage,orthelanguageandcountrycombinationwhentheydiffer(e.g.pt.poor
pt_BR.po).TranslationswillbeloadedautomaticallybyOdooforallenabledlanguages.Developersalwaysuse
Englishwhencreatingamodule,thenexportthemoduletermsusingOdoo'sgettextPOTexportfeature(Settings
TranslationsImport/ExportExportTranslationwithoutspecifyingalanguage),tocreatethemodule
templatePOTfile,andthenderivethetranslatedPOfiles.ManyIDE'shavepluginsormodesforeditingand
mergingPO/POTfiles.

Tip
TheGNUgettextformat(PortableObject)usedbyOdooisintegratedintoLaunchPad,
makingitanonlinecollaborativetranslationplatform.

|idea/#Themoduledirectory
|i18n/#Translationfiles
|idea.pot#TranslationTemplate(exportedfromOdoo)
|fr.po#Frenchtranslation
|pt_BR.po#BrazilianPortuguesetranslation
|(...)

Tip
BydefaultOdoo'sPOTexportonlyextractslabelsinsideXMLfilesorinsidefielddefinitionsin
Pythoncode,butanyPythonstringcanbetranslatedthiswaybysurroundingitwiththe
function openerp._() (e.g. _("Label") )

Exercise
Translateamodule
ChooseasecondlanguageforyourOdooinstallation.Translateyourmoduleusingthe
facilitiesprovidedbyOdoo.

1.Createadirectory openacademy/i18n/
2.Installwhicheverlanguageyouwant(AdministrationTranslationsLoadanOfficial
Translation)
3.Synchronizetranslatableterms(AdministrationTranslationsApplicationTerms
SynchronizeTranslations)
4.Createatemplatetranslationfilebyexporting(AdministrationTranslations>
Import/ExportExportTranslation)withoutspecifyingalanguage,save
in openacademy/i18n/
5.Createatranslationfilebyexporting(AdministrationTranslationsImport/Export
ExportTranslation)andspecifyingalanguage.Saveitin openacademy/i18n/
6.Opentheexportedtranslationfile(withabasictexteditororadedicatedPOfileeditor
e.g.POEditandtranslatethemissingterms
7.In models.py ,addanimportstatementforthefunction openerp._ andmarkmissing
stringsastranslatable
stringsastranslatable
8.Repeatsteps36

openacademy/models.py
#*coding:utf8*

fromdatetimeimporttimedelta
fromopenerpimportmodels,fields,api,exceptions,_

classCourse(models.Model):
_name='openacademy.course'
default=dict(defaultor{})

copied_count=self.search_count(
[('name','=like',_(u"Copyof{}%").format(self.name))])
ifnotcopied_count:
new_name=_(u"Copyof{}").format(self.name)
else:
new_name=_(u"Copyof{}({})").format(self.name,copied_count)

default['name']=new_name
returnsuper(Course,self).copy(default)
ifself.seats<0:
return{
'warning':{
'title':_("Incorrect'seats'value"),
'message':_("Thenumberofavailableseatsmaynotbenegative"),
},
}
ifself.seats<len(self.attendee_ids):
return{
'warning':{
'title':_("Toomanyattendees"),
'message':_("Increaseseatsorremoveexcessattendees"),
},
}

def_check_instructor_not_in_attendees(self):
forrinself:
ifr.instructor_idandr.instructor_idinr.attendee_ids:
raiseexceptions.ValidationError(_("Asession'sinstructorcan'tbeanattendee"

Reporting
Printedreports
Odoo8.0comeswithanewreportenginebasedonQWeb,TwitterBootstrapandWkhtmltopdf.

Areportisacombinationtwoelements:

an ir.actions.report.xml ,forwhicha <report> shortcutelementisprovided,itsetsupvariousbasic


parametersforthereport(defaulttype,whetherthereportshouldbesavedtothedatabaseaftergeneration,
)
<report
id="account_invoices"
model="account.invoice"
string="Invoices"
report_type="qwebpdf"
name="account.report_invoice"
file="account.report_invoice"
attachment_use="True"
attachment="(object.statein('open','paid'))and
('INV'+(object.numberor'').replace('/','')+'.pdf')"
/>

AstandardQWebviewfortheactualreport:
<ttcall="report.html_container">
<ttforeach="docs"tas="o">
<ttcall="report.external_layout">
<divclass="page">
<h2>Reporttitle</h2>
</div>
</t>
</t>
</t>

thestandardrenderingcontextprovidesanumberofelements,themost
importantbeing:

``docs``
therecordsforwhichthereportisprinted
``user``
theuserprintingthereport

Becausereportsarestandardwebpages,theyareavailablethroughaURLandoutputparameterscanbe
manipulatedthroughthisURL,forinstancetheHTMLversionoftheInvoicereportisavailable
throughhttp://localhost:8069/report/html/account.report_invoice/1(if account isinstalled)andthePDF
versionthroughhttp://localhost:8069/report/pdf/account.report_invoice/1.
Danger
IfitappearsthatyourPDFreportismissingthestyles(i.e.thetextappearsbutthe
style/layoutisdifferentfromthehtmlversion),probablyyourwkhtmltopdfprocesscannot
reachyourwebservertodownloadthem.
IfyoucheckyourserverlogsandseethattheCSSstylesarenotbeingdownloadedwhen
generatingaPDFreport,mostsurelythisistheproblem.
Thewkhtmltopdfprocesswillusethe web.base.url systemparameterastherootpathtoall
linkedfiles,butthisparameterisautomaticallyupdatedeachtimetheAdministratorislogged
in.Ifyourserverresidesbehindsomekindofproxy,thatcouldnotbereachable.Youcanfix
thisbyaddingoneofthesesystemparameters:
report.url ,pointingtoanURLreachablefromyourserver
(probably http://localhost:8069 orsomethingsimilar).Itwillbeusedforthisparticular
purposeonly.
web.base.url.freeze ,whensetto True ,willstoptheautomaticupdates
to web.base.url .

Exercise
CreateareportfortheSessionmodel
Foreachsession,itshoulddisplaysession'sname,itsstartandend,andlistthesession's
attendees.
openacademy/__openerp__.py
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'reports.xml',
],
#onlyloadedindemonstrationmode
'demo':[
openacademy/reports.xml
<openerp>
<data>
<report
id="report_session"
model="openacademy.session"
string="SessionReport"
name="openacademy.report_session_view"
file="openacademy.report_session"
report_type="qwebpdf"/>

<templateid="report_session_view">
<ttcall="report.html_container">
<ttforeach="docs"tas="doc">
<ttcall="report.external_layout">
<divclass="page">
<h2tfield="doc.name"/>
<p>From<spantfield="doc.start_date"/>to<spantfield="doc.end_date"
<h3>Attendees:</h3>
<ul>
<ttforeach="doc.attendee_ids"tas="attendee">
<li><spantfield="attendee.name"/></li>
</t>
</ul>
</div>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

Dashboards

Exercise
DefineaDashboard
Defineadashboardcontainingthegraphviewyoucreated,thesessionscalendarviewanda
listviewofthecourses(switchabletoaformview).Thisdashboardshouldbeavailable
throughamenuiteminthemenu,andautomaticallydisplayedinthewebclientwhenthe
OpenAcademymainmenuisselected.

1.Createafile openacademy/views/session_board.xml .Itshouldcontaintheboardview,the


actionsreferencedinthatview,anactiontoopenthedashboardandaredefinitionofthe
mainmenuitemtoaddthedashboardaction

Note
Availabledashboardstylesare 1 , 11 , 12 , 21 and 111

2.Update openacademy/__openerp__.py toreferencethenewdatafile

openacademy/__openerp__.py
'version':'0.1',

#anymodulenecessaryforthisonetoworkcorrectly
'depends':['base','board'],

#alwaysloaded
'data':[
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'views/session_board.xml',
'reports.xml',
],
#onlyloadedindemonstrationmode
openacademy/views/session_board.xml
<?xmlversion="1.0"?>
<openerp>
<data>
<recordmodel="ir.actions.act_window"id="act_session_graph">
<fieldname="name">Attendeesbycourse</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">graph</field>
<fieldname="view_mode">graph</field>
<fieldname="view_id"
ref="openacademy.openacademy_session_graph_view"/>
</record>
<recordmodel="ir.actions.act_window"id="act_session_calendar">
<fieldname="name">Sessions</field>
<fieldname="res_model">openacademy.session</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">calendar</field>
<fieldname="view_id"ref="openacademy.session_calendar_view"/>
</record>
<recordmodel="ir.actions.act_window"id="act_course_list">
<fieldname="name">Courses</field>
<fieldname="res_model">openacademy.course</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">tree,form</field>
</record>
<recordmodel="ir.ui.view"id="board_session_form">
<fieldname="name">SessionDashboardForm</field>
<fieldname="model">board.board</field>
<fieldname="type">form</field>
<fieldname="arch"type="xml">
<formstring="SessionDashboard">
<boardstyle="21">
<column>
<action
string="Attendeesbycourse"
name="%(act_session_graph)d"
height="150"
width="510"/>
<action
string="Sessions"
name="%(act_session_calendar)d"/>
</column>
<column>
<action
string="Courses"
name="%(act_course_list)d"/>
</column>
</board>
</form>
</field>
</record>
<recordmodel="ir.actions.act_window"id="open_board_session">
<fieldname="name">SessionDashboard</field>
<fieldname="res_model">board.board</field>
<fieldname="view_type">form</field>
<fieldname="view_mode">form</field>
<fieldname="usage">menu</field>
<fieldname="view_id"ref="board_session_form"/>
</record>

<menuitem
name="SessionDashboard"parent="base.menu_reporting_dashboard"
action="open_board_session"
sequence="1"
id="menu_board_session"icon="terpgraph"/>
</data>
</openerp>
WebServices
Thewebservicemoduleofferacommoninterfaceforallwebservices:

XMLRPC
JSONRPC

Businessobjectscanalsobeaccessedviathedistributedobjectmechanism.Theycanallbemodifiedviathe
clientinterfacewithcontextualviews.

OdooisaccessiblethroughXMLRPC/JSONRPCinterfaces,forwhichlibrariesexistinmanylanguages.

XMLRPCLibrary
ThefollowingexampleisaPythonprogramthatinteractswithanOdooserverwiththelibrary xmlrpclib :

importxmlrpclib

root='http://%s:%d/xmlrpc/'%(HOST,PORT)

uid=xmlrpclib.ServerProxy(root+'common').login(DB,USER,PASS)
print"Loggedinas%s(uid:%d)"%(USER,uid)

#Createanewnote
sock=xmlrpclib.ServerProxy(root+'object')
args={
'color':8,
'memo':'Thisisanote',
'create_uid':uid,
}
note_id=sock.execute(DB,uid,PASS,'note.note','create',args)
Exercise
Addanewservicetotheclient
WriteaPythonprogramabletosendXMLRPCrequeststoaPCrunningOdoo(yours,or
yourinstructor's).Thisprogramshoulddisplayallthesessions,andtheircorresponding
numberofseats.Itshouldalsocreateanewsessionforoneofthecourses.
importfunctools
importxmlrpclib
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"Loggedinas%s(uid:%d)"%(USER,uid)

call=functools.partial(
xmlrpclib.ServerProxy(ROOT+'object').execute,
DB,uid,PASS)

#2.Readthesessions
sessions=call('openacademy.session','search_read',[],['name','seats'])
forsessioninsessions:
print"Session%s(%sseats)"%(session['name'],session['seats'])
#3.createanewsession
session_id=call('openacademy.session','create',{
'name':'Mysession',
'course_id':2,
})

Insteadofusingahardcodedcourseid,thecodecanlookupacoursebyname:
#3.createanewsessionforthe"Functional"course
course_id=call('openacademy.course','search',[('name','ilike','Functional')])[0]
session_id=call('openacademy.session','create',{
'name':'Mysession',
'course_id':course_id,
})

JSONRPCLibrary
ThefollowingexampleisaPythonprogramthatinteractswithanOdooserverwiththestandardPython
libraries urllib2 and json :

importjson
importrandom
importurllib2

defjson_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={
"ContentType":"application/json",
})
reply=json.load(urllib2.urlopen(req))
ifreply.get("error"):
raiseException(reply["error"])
returnreply["result"]

defcall(url,service,method,*args):
returnjson_rpc(url,"call",{"service":service,"method":method,"args":args})

#loginthegivendatabase
url="http://%s:%s/jsonrpc"%(HOST,PORT)
uid=call(url,"common","login",DB,USER,PASS)

#createanewnote
args={
'color':8,
'memo':'Thisisanothernote',
'create_uid':uid,
}
note_id=call(url,"object","execute",DB,uid,PASS,'note.note','create',args)

Hereisthesameprogram,usingthelibraryjsonrpclib:

importjsonrpclib

#serverproxyobject
url="http://%s:%s/jsonrpc"%(HOST,PORT)
server=jsonrpclib.Server(url)

#loginthegivendatabase
uid=server.call(service="common",method="login",args=[DB,USER,PASS])

#helperfunctionforinvokingmodelmethods
definvoke(model,method,*args):
args=[DB,uid,PASS,model,method]+list(args)
returnserver.call(service="object",method="execute",args=args)

#createanewnote
args={
'color':8,
'memo':'Thisisanothernote',
'create_uid':uid,
}
note_id=invoke('note.note','create',args)

ExamplescanbeeasilyadaptedfromXMLRPCtoJSONRPC.

Note
ThereareanumberofhighlevelAPIsinvariouslanguagestoaccessOdoosystems
withoutexplicitlygoingthroughXMLRPCorJSONRPC,suchas:
https://github.com/akretion/ooor
https://github.com/syleam/openobjectlibrary
https://github.com/nicolasvan/openerpclientlib
https://pypi.python.org/pypi/oersted/

[1]itispossibleto disabletheautomaticcreationofsomefields
[2]writingrawSQLqueriesispossible,butrequirescareasitbypassesallOdooauthenticationandsecurity
mechanisms.

You might also like