Вы находитесь на странице: 1из 43

Building a Module

Warning
ThistutorialrequireshavinginstalledOdoo(../setup/install.html#setupinstall)

Start/Stop the Odoo server


Odoousesaclient/serverarchitectureinwhichclientsarewebbrowsersaccessingtheOdooserverviaRPC.
Businesslogicandextensionisgenerallyperformedontheserverside,althoughsupportingclientfeatures(e.g.newdatarepresentationsuchas
interactivemaps)canbeaddedtotheclient.
Inordertostarttheserver,simplyinvokethecommandodoo.py(../reference/cmdline.html#referencecmdline)intheshell,addingthefullpathtothefile
ifnecessary:
odoo.py

Theserverisstoppedbyhitting CtrlC twicefromtheterminal,orbykillingthecorrespondingOSprocess.

Build an Odoo module


Bothserverandclientextensionsarepackagedasmoduleswhichareoptionallyloadedinadatabase.
OdoomodulescaneitheraddbrandnewbusinesslogictoanOdoosystem,oralterandextendexistingbusinesslogic:amodulecanbecreatedtoadd
yourcountry'saccountingrulestoOdoo'sgenericaccountingsupport,whilethenextmoduleaddssupportforrealtimevisualisationofabusfleet.
EverythinginOdoothusstartsandendswithmodules.

Composition of a module
AnOdoomodulecancontainanumberofelements:
Businessobjects
declaredasPythonclasses,theseresourcesareautomaticallypersistedbyOdoobasedontheirconfiguration
Datafiles
XMLorCSVfilesdeclaringmetadata(viewsorworkflows),configurationdata(modulesparameterization),demonstrationdataandmore
Webcontrollers
Handlerequestsfromwebbrowsers
Staticwebdata
Images,CSSorjavascriptfilesusedbythewebinterfaceorwebsite

Module structure
Eachmoduleisadirectorywithinamoduledirectory.Moduledirectoriesarespecifiedbyusingthe addonspath (../reference/cmdline.html#cmdoption
odoo.pyaddonspath)option.

Tip
mostcommandlineoptionscanalsobesetusingaconfigurationfile(../reference/cmdline.html#referencecmdlineconfig)

AnOdoomoduleisdeclaredbyitsmanifest(../reference/module.html#referencemodulemanifest).Seethemanifestdocumentation
(../reference/module.html#referencemodulemanifest)informationaboutit.
AmoduleisalsoaPythonpackage(http://docs.python.org/2/tutorial/modules.html#packages)witha __init__.py file,containingimportinstructionsfor
variousPythonfilesinthemodule.
Forinstance,ifthemodulehasasingle mymodule.py file __init__.py mightcontain:
from.importmymodule

Odooprovidesamechanismtohelpsetupanewmodule,odoo.py(../reference/cmdline.html#referencecmdlineserver)hasasubcommandscaffold
(../reference/cmdline.html#referencecmdlinescaffold)tocreateanemptymodule:
$odoo.pyscaffold<modulename><wheretoputit>

Thecommandcreatesasubdirectoryforyourmodule,andautomaticallycreatesabunchofstandardfilesforamodule.Mostofthemsimplycontain
commentedcodeorXML.Theusageofmostofthosefileswillbeexplainedalongthistutorial.

Exercise
Modulecreation
UsethecommandlineabovetocreateanemptymoduleOpenAcademy,andinstallitinOdoo.
1.Invokethecommand odoo.pyscaffoldopenacademyaddons .
2.Adaptthemanifestfiletoyourmodule.
3.Don'tbotherabouttheotherfiles.

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

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

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

'author':"MyCompany",
'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">>
<!<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>

Object-Relational Mapping
AkeycomponentofOdooistheORM(ObjectRelationalMapping)layer.ThislayeravoidshavingtowritemostSQL(StructuredQueryLanguage)by
handandprovidesextensibilityandsecurityservices2.
BusinessobjectsaredeclaredasPythonclassesextending Model (../reference/orm.html#openerp.models.Model)whichintegratesthemintothe
automatedpersistencesystem.
Modelscanbeconfiguredbysettinganumberofattributesattheirdefinition.Themostimportantattributeis _name
(../reference/orm.html#openerp.models.Model._name)whichisrequiredanddefinesthenameforthemodelintheOdoosystem.Hereisaminimally
completedefinitionofamodel:
fromopenerpimportmodels
classMinimalModel(models.Model):
_name='test.model'

Model elds
Fieldsareusedtodefinewhatthemodelcanstoreandwhere.Fieldsaredefinedasattributesonthemodelclass:
fromopenerpimportmodels,fields

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

name=fields.Char()

Common Attributes
Muchlikethemodelitself,itsfieldscanbeconfigured,bypassingconfigurationattributesasparameters:
name=field.Char(required=True)

Someattributesareavailableonallfields,herearethemostcommonones:
string ( unicode ,default:field'sname)
ThelabelofthefieldinUI(visiblebyusers).
required ( bool ,default: False )
If True ,thefieldcannotbeempty,itmusteitherhaveadefaultvalueoralwaysbegivenavaluewhencreatingarecord.
help ( unicode ,default: '' )
Longform,providesahelptooltiptousersintheUI.
index ( bool ,default: False )
RequeststhatOdoocreateadatabaseindex(http://usetheindexluke.com/sql/preface)onthecolumn

Simple elds
Therearetwobroadcategoriesoffields:"simple"fieldswhichareatomicvaluesstoreddirectlyinthemodel'stableand"relational"fieldslinkingrecords
(ofthesamemodelorofdifferentmodels).
Exampleofsimplefieldsare Boolean (../reference/orm.html#openerp.fields.Boolean), Date (../reference/orm.html#openerp.fields.Date), Char
(../reference/orm.html#openerp.fields.Char).

Reserved elds
Odoocreatesafewfieldsinallmodels1.Thesefieldsaremanagedbythesystemandshouldn'tbewrittento.Theycanbereadifusefulornecessary:
id ( Id )
theuniqueidentifierforarecordinitsmodel
create_date ( Datetime (../reference/orm.html#openerp.fields.Datetime))
creationdateoftherecord
create_uid ( Many2one (../reference/orm.html#openerp.fields.Many2one))
userwhocreatedtherecord
write_date ( Datetime (../reference/orm.html#openerp.fields.Datetime))
lastmodificationdateoftherecord
write_uid ( Many2one (../reference/orm.html#openerp.fields.Many2one))
userwholastmodifiedtherecord

Special elds
Bydefault,Odooalsorequiresa name fieldonallmodelsforvariousdisplayandsearchbehaviors.Thefieldusedforthesepurposescanbeoverridden
bysetting _rec_name (../reference/orm.html#openerp.models.Model._rec_name).

Exercise

Defineamodel
DefineanewdatamodelCourseintheopenacademymodule.Acoursehasatitleandadescription.Coursesmusthaveatitle.
Editthefile openacademy/models/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()

Data les
Odooisahighlydatadrivensystem.AlthoughbehavioriscustomizedusingPython(http://python.org)codepartofamodule'svalueisinthedataitsets
upwhenloaded.

Tip

somemodulesexistsolelytoadddataintoOdoo

Moduledataisdeclaredviadatafiles(../reference/data.html#referencedata),XMLfileswith <record> elements.Each <record> elementcreatesor


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

model isthenameoftheOdoomodelfortherecord
id isanexternalidentifier(../glossary.html#termexternalidentifier),itallowsreferringtotherecord(withouthavingtoknowitsindatabase
identifier)
<field> elementshavea name whichisthenameofthefieldinthemodel(e.g. description ).Theirbodyisthefield'svalue.
Datafileshavetobedeclaredinthemanifestfiletobeloaded,theycanbedeclaredinthe 'data' list(alwaysloaded)orinthe 'demo' list(onlyloaded
indemonstrationmode).

Exercise

Definedemonstrationdata
CreatedemonstrationdatafillingtheCoursesmodelwithafewdemonstrationcourses.
Editthefile openacademy/demo/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>

Actions and Menus


Actionsandmenusareregularrecordsindatabase,usuallydeclaredthroughdatafiles.Actionscanbetriggeredinthreeways:
1.byclickingonmenuitems(linkedtospecificactions)
2.byclickingonbuttonsinviews(iftheseareconnectedtoactions)
3.ascontextualactionsonobject

Becausemenusaresomewhatcomplextodeclarethereisa <menuitem> shortcuttodeclarean ir.ui.menu andconnectittothecorrespondingaction


moreeasily.
<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 mustbepresentinthedatabasebeforethemenucanbecreated.

Exercise

Definenewmenuentries
DefinenewmenuentriestoaccesscoursesundertheOpenAcademymenuentry.Ausershouldbeableto
displayalistofallthecourses
create/modifycourses
1.Create openacademy/views/openacademy.xml withanactionandthemenustriggeringtheaction
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>
<menuitemid="courses_menu"name="Courses"parent="openacademy_menu"
action="course_list_action"/>
<!Fullidlocation:
action="openacademy.course_list_action"
Itisnotrequiredwhenitisthesamemodule>
</data>
</openerp>

Basic views
Viewsdefinethewaytherecordsofamodelaredisplayed.Eachtypeofviewrepresentsamodeofvisualization(alistofrecords,agraphoftheir
aggregation,).Viewscaneitherberequestedgenericallyviatheirtype(e.g.alistofpartners)orspecificallyviatheirid.Forgenericrequests,theview
withthecorrecttypeandthelowestprioritywillbeused(sothelowestpriorityviewofeachtypeisthedefaultviewforthattype).
Viewinheritance(../reference/views.html#referenceviewsinheritance)allowsalteringviewsdeclaredelsewhere(addingorremovingcontent).

Generic view declaration


Aviewisdeclaredasarecordofthemodel ir.ui.view .Theviewtypeisimpliedbytherootelementofthe 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.

Tree views
Treeviews,alsocalledlistviews,displayrecordsinatabularform.
Theirrootelementis <tree> .Thesimplestformofthetreeviewsimplylistsallthefieldstodisplayinthetable(eachfieldasacolumn):
<treestring="Idealist">
<fieldname="name"/>
<fieldname="inventor_id"/>
</tree>

Form views
Formsareusedtocreateandeditsinglerecords.
Theirrootelementis <form> .Theycomposedofhighlevelstructureelements(groups,notebooks)andinteractiveelements(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:thenameandthedescriptionofthecourse.
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,suchthatitwillbeeasiertoaddothertabslater,containingadditional
information.
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>

Search views
Searchviewscustomizethesearchfieldassociatedwiththelistview(andotheraggregatedviews).Theirrootelementis <search> andthey're
composedoffieldsdefiningwhichfieldscanbesearchedon:
<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",

Relations between models


Arecordfromamodelmayberelatedtoarecordfromanothermodel.Forinstance,asaleorderrecordisrelatedtoaclientrecordthatcontainsthe
clientdataitisalsorelatedtoitssaleorderlinerecords.
Exercise
Createasessionmodel
ForthemoduleOpenAcademy,weconsideramodelforsessions:asessionisanoccurrenceofacoursetaughtatagiventimefora
givenaudience.
Createamodelforsessions.Asessionhasaname,astartdate,adurationandanumberofseats.Addanactionandamenuitemto
displaythem.Makethenewmodelvisibleviaamenuitem.
1.CreatetheclassSessionin openacademy/models/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:6isthetotalnumberofdigits,while2isthenumberofdigitsafterthe
comma.Notethatitresultsinthenumberdigitsbeforethecommaisamaximum4

Relational elds
Relationalfieldslinkrecords,eitherofthesamemodel(hierarchies)orbetweendifferentmodels.
Relationalfieldtypesare:
Many2one(other_model,ondelete='setnull') (../reference/orm.html#openerp.fields.Many2one)
Asimplelinktoanotherobject:
printfoo.other_id.name
See also

foreignkeys(http://www.postgresql.org/docs/9.3/static/tutorialfk.html)

One2many(other_model,related_field) (../reference/orm.html#openerp.fields.One2many)
Avirtualrelationship,inverseofa Many2one (../reference/orm.html#openerp.fields.Many2one).A One2many
(../reference/orm.html#openerp.fields.One2many)behavesasacontainerofrecords,accessingitresultsina(possiblyempty)setofrecords:
forotherinfoo.other_ids:
printother.name

Danger

Becausea One2many (../reference/orm.html#openerp.fields.One2many)isavirtualrelationship,theremustbea Many2one


(../reference/orm.html#openerp.fields.Many2one)fieldinthe other_model ,anditsnamemustbe related_field

Many2many(other_model) (../reference/orm.html#openerp.fields.Many2many)
Bidirectionalmultiplerelationship,anyrecordononesidecanberelatedtoanynumberofrecordsontheotherside.Behavesasacontainerof
records,accessingitalsoresultsinapossiblyemptysetofrecords:
forotherinfoo.other_ids:
printother.name

Exercise

Many2onerelations
Usingamany2one,modifytheCourseandSessionmodelstoreflecttheirrelationwithothermodels:
Acoursehasaresponsibleuserthevalueofthatfieldisarecordofthebuiltinmodel res.users .
Asessionhasaninstructorthevalueofthatfieldisarecordofthebuiltinmodel res.partner .
Asessionisrelatedtoacoursethevalueofthatfieldisarecordofthemodel 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>
</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,modifythemodelstoreflecttherelationbetweencoursesandsessions.
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,modifytheSessionmodeltorelateeverysessiontoasetofattendees.Attendeeswillberepresented
bypartnerrecords,sowewillrelatetothebuiltinmodel 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
Model inheritance
Odooprovidestwoinheritancemechanismstoextendanexistingmodelinamodularway.
Thefirstinheritancemechanismallowsamoduletomodifythebehaviorofamodeldefinedinanothermodule:
addfieldstoamodel,
overridethedefinitionoffieldsonamodel,
addconstraintstoamodel,
addmethodstoamodel,
overrideexistingmethodsonamodel.

Thesecondinheritancemechanism(delegation)allowstolinkeveryrecordofamodeltoarecordinaparentmodel,andprovidestransparentaccessto
thefieldsoftheparentrecord.

See also
_inherit (../reference/orm.html#openerp.models.Model._inherit)
_inherits (../reference/orm.html#openerp.models.Model._inherits)

View inheritance
Insteadofmodifyingexistingviewsinplace(byoverwritingthem),Odooprovidesviewinheritancewherechildren"extension"viewsareappliedontop
ofrootviews,andcanaddorremovecontentfromtheirparent.
Anextensionviewreferencesitsparentusingthe inherit_id field,andinsteadofasingleviewits arch fieldiscomposedofanynumberof 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

AnXPath(http://w3.org/TR/xpath)expressionselectingasingleelementintheparentview.Raisesanerrorifitmatchesnoelementormorethan
one
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 attributecanbesetdirectlyontheelementtobefound.Bothinheritancesbelowwillgivethe
sameresult.

<xpathexpr="//field[@name='description']"position="after">
<fieldname="idea_ids"/>
</xpath>

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

Exercise
Alterexistingcontent
Usingmodelinheritance,modifytheexistingPartnermodeltoaddan instructor booleanfield,andamany2manyfieldthat
correspondstothesessionpartnerrelation
Usingviewinheritance,displaythisfieldsinthepartnerformview

Note
Thisistheopportunitytointroducethedevelopermodetoinspecttheview,finditsexternalIDandtheplacetoputthenew
field.

1.Createafile openacademy/models/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"
parent="main_openacademy_menu"/>
parent="main_openacademy_menu"/>
<menuitemid="contact_menu"name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
</data>
</openerp>

Domains
InOdoo,Domains(../reference/orm.html#referenceormdomains)arevaluesthatencodeconditionsonrecords.Adomainisalistofcriteriausedto
selectasubsetofamodel'srecords.Eachcriteriaisatriplewithafieldname,anoperatorandavalue.
Forinstance,whenusedontheProductmodelthefollowingdomainselectsallserviceswithaunitpriceover1000:
[('product_type','=','service'),('unit_price','>',1000)]

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


Theyareusedinprefixposition(theoperatorisinsertedbeforeitsargumentsratherthanbetween).Forinstancetoselectproducts"whichareservices
ORhaveaunitpricewhichisNOTbetween1000and2000":
['|',
('product_type','=','service'),
'!','&',
('unit_price','>=',1000),
('unit_price','<',2000)]

A domain parametercanbeaddedtorelationalfieldstolimitvalidrecordsfortherelationwhentryingtoselectrecordsintheclientinterface.
Exercise
Domainsonrelationalfields
WhenselectingtheinstructorforaSession,onlyinstructors(partnerswith instructor setto 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'trefertodynamicvaluesontherighthandside,adomain
declaredasastringisevaluatedclientsideandallowsfieldnamesontherighthandside

Exercise

Morecomplexdomains
CreatenewpartnercategoriesTeacher/Level1andTeacher/Level2.Theinstructorforasessioncanbeeitheraninstructororateacher
(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"
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>

Computed elds and default values


Sofarfieldshavebeenstoreddirectlyinandretrieveddirectlyfromthedatabase.Fieldscanalsobecomputed.Inthatcase,thefield'svalueisnot
retrievedfromthedatabasebutcomputedontheflybycallingamethodofthemodel.
Tocreateacomputedfield,createafieldandsetitsattribute compute tothenameofamethod.Thecomputationmethodshouldsimplysetthevalueof
thefieldtocomputeoneveryrecordin self .

Danger
self isacollection
Theobject self isarecordset,i.e.,anorderedcollectionofrecords.ItsupportsthestandardPythonoperationsoncollections,like
len(self) and iter(self) ,plusextrasetoperationslike recs1+recs2 .

Iteratingover self givestherecordsonebyone,whereeachrecordisitselfacollectionofsize1.Youcanaccess/assignfieldsonsingle


recordsbyusingthedotnotation,like record.name .
importrandom
fromopenerpimportmodels,fields,api

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
Thevalueofacomputedfieldusuallydependsonthevaluesofotherfieldsonthecomputedrecord.TheORMexpectsthedevelopertospecifythose
dependenciesonthecomputemethodwiththedecorator depends() (../reference/orm.html#openerp.api.depends).Thegivendependenciesareusedby
theORMtotriggertherecomputationofthefieldwheneversomeofitsdependencieshavebeenmodified:
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:
record.name="Recordwithvalue%s"%record.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>

Default values
Anyfieldcanbegivenadefaultvalue.Inthefielddefinition,addtheoption default=X where X iseitheraPythonliteralvalue(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 isthedatabasecursorobjectitisusedforqueryingthedatabase
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 (../reference/orm.html#openerp.fields.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 fieldsetto False invisible.

Onchange
The"onchange"mechanismprovidesawayfortheclientinterfacetoupdateaformwhenevertheuserhasfilledinavalueinafield,withoutsaving
anythingtothedatabase.
Forinstance,supposeamodelhasthreefields amount , unit_price and price ,andyouwanttoupdatethepriceontheformwhenanyoftheother
fieldsismodified.Toachievethis,defineamethodwhere self representstherecordintheformview,anddecorateitwith onchange()
(../reference/orm.html#openerp.api.onchange)tospecifyonwhichfieldithastobetriggered.Anychangeyoumakeon self willbereflectedonthe
form.
<!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,ormoreparticipantsthanseats.
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",
},
}

Model constraints
Odooprovidestwowaystosetupautomaticallyverifiedinvariants: Pythonconstraints (../reference/orm.html#openerp.api.constrains)and SQL
constraints (../reference/orm.html#openerp.models.Model._sql_constraints).
APythonconstraintisdefinedasamethoddecoratedwith constrains() (../reference/orm.html#openerp.api.constrains),andinvokedonarecordset.
Thedecoratorspecifieswhichfieldsareinvolvedintheconstraint,sothattheconstraintisautomaticallyevaluatedwhenoneofthemismodified.The
methodisexpectedtoraiseanexceptionifitsinvariantisnotsatisfied:
fromopenerp.exceptionsimportValidationError

@api.constrains('age')
def_check_something(self):
forrecordinself:
ifrecord.age>20:
raiseValidationError("Yourrecordistooold:%s"%record.age)
#allrecordspassedthetest,don'treturnanything

Exercise
AddPythonconstraints
Addaconstraintthatchecksthattheinstructorisnotpresentintheattendeesofhis/herownsession.
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 (../reference/orm.html#openerp.models.Model._sql_constraints).Thelatteris


assignedtoalistoftriplesofstrings (name,sql_definition,message) ,where name isavalidSQLconstraintname, sql_definition isatable_constraint
(http://www.postgresql.org/docs/9.3/static/ddlconstraints.html)expression,and message istheerrormessage.
Exercise
AddSQLconstraints
WiththehelpofPostgreSQL'sdocumentation(http://www.postgresql.org/docs/9.3/static/ddlconstraints.html),addthefollowing
constraints:
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(Form
Duplicate).
Reimplementyourown"copy"methodwhichallowstoduplicatetheCourseobject,changingtheoriginalnameinto"Copyof[original
name]".
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)',

Advanced Views
Tree views
Treeviewscantakesupplementaryattributestofurthercustomizetheirbehavior:
decoration{$name}

allowchangingthestyleofarow'stextbasedonthecorrespondingrecord'sattributes.
ValuesarePythonexpressions.Foreachrecord,theexpressionisevaluatedwiththerecord'sattributesascontextvaluesandif true ,the
correspondingstyleisappliedtotherow.Othercontextvaluesare uid (theidofthecurrentuser)and current_date (thecurrentdateasastring
oftheform yyyyMMdd ).
{$name} canbe bf ( fontweight:bold ), it ( fontstyle:italic ),oranybootstrapcontextualcolor
(http://getbootstrap.com/components/#availablevariations)( danger , info , muted , primary , success or warning ).
<treestring="IdeaCategories"decorationinfo="state=='draft'"
decorationdanger="state=='trashed'">
<fieldname="name"/>
<fieldname="state"/>
</tree>

editable

Either "top" or "bottom" .Makesthetreevieweditableinplace(ratherthanhavingtogothroughtheformview),thevalueisthepositionwhere


newrowsappear.
Exercise

Listcoloring
ModifytheSessiontreeviewinsuchawaythatsessionslastinglessthan5daysarecoloredblue,andtheoneslastingmorethan15days
arecoloredred.
Modifythesessiontreeview:
openacademy/views/openacademy.xml
<fieldname="name">session.tree</field>
<fieldname="model">openacademy.session</field>
<fieldname="arch"type="xml">
<treestring="SessionTree"decorationinfo="duration&lt;5"decorationdanger="duration&gt;15">
<fieldname="name"/>
<fieldname="course_id"/>
<fieldname="duration"invisible="1"/>
<fieldname="taken_seats"widget="progressbar"/>
</tree>
</field>

Calendars
Displaysrecordsascalendarevents.Theirrootelementis <calendar> andtheirmostcommonattributesare:
color

Thenameofthefieldusedforcolorsegmentation.Colorsareautomaticallydistributedtoevents,buteventsinthesamecolorsegment(records
whichhavethesamevaluefortheir @color field)willbegiventhesamecolor.
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
AddaCalendarviewtotheSessionmodelenablingtheusertoviewtheeventsassociatedtotheOpenAcademy.
1.Addan end_date fieldcomputedfrom start_date and duration

Tip
theinversefunctionmakesthefieldwritable,andallowsmovingthesessions(viadraganddrop)inthe
calendarview

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
#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"

Search views
Searchview <field> elementscanhavea @filter_domain thatoverridesthedomaingeneratedforsearchingonthegivenfield.Inthegivendomain,
self representsthevalueenteredbytheuser.Intheexamplebelow,itisusedtosearchonbothfields name and description .
Searchviewscanalsocontain <filter> elements,whichactastogglesforpredefinedsearches.Filtersmusthaveoneofthefollowingattributes:
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 fieldoftheactionrecord.


Theactioncanalsosetdefaultvaluesforsearchfieldsthroughits context field:contextkeysoftheform search_default_field_name willinitialize
field_namewiththeprovidedvalue.Searchfiltersmusthaveanoptional @name tohaveadefaultandbehaveasbooleans(theycanonlybeenabledby
default).
Exercise
Searchviews
1.Addabuttontofilterthecoursesforwhichthecurrentuseristheresponsibleinthecoursesearchview.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"
date_stop="date_finished"
progress="progress"
default_group_by="inventor_id"/>
Exercise

Ganttcharts
AddaGanttChartenablingtheusertoviewthesessionsschedulinglinkedtotheOpenAcademymodule.Thesessionsshouldbe
groupedbyinstructor.
1.Createacomputedfieldexpressingthesession'sdurationinhours
2.Addtheganttview'sdefinition,andaddtheganttviewtotheSessionmodel'saction

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"

Graph views
Graphviewsallowaggregatedoverviewandanalysisofmodels,theirrootelementis <graph> .

Note
Pivotviews(element <pivot> )amultidimensionaltable,allowstheselectionoffilersanddimensionstogettherightaggregateddataset
beforemovingtoamoregraphicaloverview.Thepivotviewsharesthesamecontentdefinitionasgraphviews.

Graphviewshave4displaymodes,thedefaultmodeisselectedusingthe @type attribute.


Bar(default)
abarchart,thefirstdimensionisusedtodefinegroupsonthehorizontalaxis,otherdimensionsdefineaggregatedbarswithineachgroup.
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,theydonotworkwithnonstoredcomputedfields.

Exercise
Graphview
AddaGraphviewintheSessionobjectthatdisplays,foreachcourse,thenumberofattendeesundertheformofabarchart.
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>

<menuitemid="session_menu"name="Sessions"

Kanban
Usedtoorganizetasks,productionprocesses,etctheirrootelementis <kanban> .
Akanbanviewshowsasetofcardspossiblygroupedincolumns.Eachcardrepresentsarecord,andeachcolumnthevaluesofanaggregationfield.
Forinstance,projecttasksmaybeorganizedbystage(eachcolumnisastage),orbyresponsible(eachcolumnisauser),andsoon.
Kanbanviewsdefinethestructureofeachcardasamixofformelements(includingbasicHTML)andQWeb(../reference/qweb.html#referenceqweb).
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_fancy':''}}">
<divclass="oe_dropdown_kanban">
<!dropdownmenu>
<divclass="oe_dropdown_toggle">
<iclass="fafabarsfalg"/>
<ulclass="oe_dropdown_menu">
<li>
<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"

Workows
Workflowsaremodelsassociatedtobusinessobjectsdescribingtheirdynamics.Workflowsarealsousedtotrackprocessesthatevolveovertime.
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,thosecanbecalledfromviewbuttonstochangetherecord'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.Workflowsareusedtostructureandmanagethelifecyclesof
businessobjectsanddocuments,anddefinetransitions,triggers,etc.withgraphicaltools.Workflows,activities(nodesoractions)andtransitions
(conditions)aredeclaredasXMLrecords,asusual.Thetokensthatnavigateinworkflowsarecalledworkitems.

Warning
Aworkflowassociatedwithamodelisonlycreatedwhenthemodel'srecordsarecreated.Thusthereisnoworkflowinstanceassociated
withsessioninstancescreatedbeforetheworkflow'sdefinition

Exercise

Workflow
ReplacetheadhocSessionworkflowbyarealworkflow.TransformtheSessionformviewsoitsbuttonscalltheworkflowinsteadofthe
model'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="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

Inordertocheckifinstancesoftheworkflowarecorrectlycreatedalongsidesessions,gotoSettings Technical
Workflows Instances
Exercise

Automatictransitions
AutomaticallytransitionsessionsfromDrafttoConfirmedwhenmorethanhalfthesession'sseatsarereserved.
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.

Group-based access control mechanisms


Groupsarecreatedasnormalrecordsonthemodel res.groups ,andgrantedmenuaccessviamenudefinitions.Howeverevenwithoutamenu,objects
maystillbeaccessibleindirectly,soactualobjectlevelpermissions(read,write,create,unlink)mustbedefinedforgroups.Theyareusuallyinserted
viaCSVfilesinsidemodules.Itisalsopossibletorestrictaccesstospecificfieldsonavieworobjectusingthefield'sgroupsattribute.

Access rights
Accessrightsaredefinedasrecordsofthemodel ir.model.access .Eachaccessrightisassociatedtoamodel,agroup(ornogroupforglobal
access),andasetofpermissions:read,write,create,unlink.SuchaccessrightsareusuallycreatedbyaCSVfilenamedafteritsmodel:
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"withreadaccesstotheSessionmodel.
1.CreateanewuserJohnSmiththroughSettings Users Users
2.Createanewgroup session_read throughSettings Users Groups,itshouldhavereadaccessontheSessionmodel
3.EditJohnSmithtomakethemamemberof session_read
4.LoginasJohnSmithtochecktheaccessrightsarecorrect

Exercise
Addaccesscontrolthroughdatafilesinyourmodule
Usingdatafiles,
CreateagroupOpenAcademy/ManagerwithfullaccesstoallOpenAcademymodels
MakeSessionandCoursereadablebyallusers
1.Createanewfile openacademy/security/security.xml toholdtheOpenAcademyManagergroup
2.Editthefile openacademy/security/ir.model.access.csv withtheaccessrightstothemodels
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>

Record rules
Arecordrulerestrictstheaccessrightstoasubsetofrecordsofthegivenmodel.Aruleisarecordofthemodel ir.rule ,andisassociatedtoamodel,
anumberofgroups(many2manyfield),permissionstowhichtherestrictionapplies,andadomain.Thedomainspecifiestowhichrecordstheaccess
rightsarelimited.
Hereisanexampleofarulethatpreventsthedeletionofleadsthatarenotinstate cancel .Noticethatthevalueofthefield groups mustfollowthe
sameconventionasthemethod write() (../reference/orm.html#openerp.models.Model.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",thatrestricts write and unlink accessestothe
responsibleofacourse.Ifacoursehasnoresponsible,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.Awizardissimplyamodelthatextendstheclass
TransientModel insteadof Model (../reference/orm.html#openerp.models.Model).Theclass TransientModel extends Model
(../reference/orm.html#openerp.models.Model)andreuseallitsexistingmechanisms,withthefollowingparticularities:
Wizardrecordsarenotmeanttobepersistenttheyareautomaticallydeletedfromthedatabaseafteracertaintime.Thisiswhytheyarecalled
transient.
Wizardmodelsdonotrequireexplicitaccessrights:usershaveallpermissionsonwizardrecords.
Wizardrecordsmayrefertoregularrecordsorwizardrecordsthroughmany2onefields,butregularrecordscannotrefertowizardrecords
throughamany2onefield.

Wewanttocreateawizardthatallowuserstocreateattendeesforaparticularsession,orforalistofsessionsatonce.

Exercise

Definethewizard
Createawizardmodelwithamany2onerelationshipwiththeSessionmodelandamany2manyrelationshipwiththePartnermodel.
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")

Launching wizards
Wizardsarelaunchedby ir.actions.act_window records,withthefield target settothevalue new .Thelatteropensthewizardviewintoapopup
window.Theactionmaybetriggeredbyamenuitem.
Thereisanotherwaytolaunchthewizard:usingan ir.actions.act_window recordlikeabove,butwithanextrafield src_model thatspecifiesinthe
contextofwhichmodeltheactionisavailable.Thewizardwillappearinthecontextualactionsofthemodel,abovethemainview.Becauseofsome
internalhooksintheORM,suchanactionisdeclaredinXMLwiththetag 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" toclosethewizardwindowwithoutsaving.

Exercise
Launchthewizard
1.Defineaformviewforthewizard.
2.AddtheactiontolaunchitinthecontextoftheSessionmodel.
3.Defineadefaultvalueforthesessionfieldinthewizardusethecontextparameter 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,andimplementthecorrespondingmethodforaddingtheattendeestothegivensession.
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.powhereLANGisthelocalecodeforthelanguage,
orthelanguageandcountrycombinationwhentheydiffer(e.g.pt.poorpt_BR.po).TranslationswillbeloadedautomaticallybyOdooforallenabled
languages.DevelopersalwaysuseEnglishwhencreatingamodule,thenexportthemoduletermsusingOdoo'sgettextPOTexportfeature(Settings
Translations Import/Export ExportTranslationwithoutspecifyingalanguage),tocreatethemoduletemplatePOTfile,andthenderivethe
translatedPOfiles.ManyIDE'shavepluginsormodesforeditingandmergingPO/POTfiles.

Tip

ThePortableObjectfilesgeneratedbyOdooarepublishedonTransifex(https://www.transifex.com/odoo/public/),makingiteasyto
translatethesoftware.

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

BydefaultOdoo'sPOTexportonlyextractslabelsinsideXMLfilesorinsidefielddefinitionsinPythoncode,butanyPythonstringcanbe
translatedthiswaybysurroundingitwiththefunction openerp._() (e.g. _("Label") )

Exercise

Translateamodule
ChooseasecondlanguageforyourOdooinstallation.TranslateyourmoduleusingthefacilitiesprovidedbyOdoo.
1.Createadirectory openacademy/i18n/
2.Installwhicheverlanguageyouwant(Administration Translations LoadanOfficialTranslation)
3.Synchronizetranslatableterms(Administration Translations ApplicationTerms SynchronizeTranslations)
4.Createatemplatetranslationfilebyexporting(Administration Translations>Import/Export ExportTranslation)without
specifyingalanguage,savein openacademy/i18n/
5.Createatranslationfilebyexporting(Administration Translations Import/Export ExportTranslation)andspecifyinga
language.Saveitin openacademy/i18n/
6.Opentheexportedtranslationfile(withabasictexteditororadedicatedPOfileeditore.g.POEdit(http://poedit.net)andtranslate
themissingterms
7.In models.py ,addanimportstatementforthefunction openerp._ andmarkmissingstringsastranslatable
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
Printed reports
Odoo8.0comeswithanewreportenginebasedonQWeb(../reference/qweb.html#referenceqweb),TwitterBootstrap(http://getbootstrap.com)and
Wkhtmltopdf(http://wkhtmltopdf.org).
Areportisacombinationtwoelements:
an ir.actions.report.xml ,forwhicha <report> shortcutelementisprovided,itsetsupvariousbasicparametersforthereport(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')"
/>

AstandardQWebview(../reference/views.html#referenceviewsqweb)fortheactualreport:
<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,theyareavailablethroughaURLandoutputparameterscanbemanipulatedthroughthisURL,forinstance
theHTMLversionoftheInvoicereportisavailablethroughhttp://localhost:8069/report/html/account.report_invoice/1
(http://localhost:8069/report/html/account.report_invoice/1)(if account isinstalled)andthePDFversionthrough
http://localhost:8069/report/pdf/account.report_invoice/1(http://localhost:8069/report/pdf/account.report_invoice/1).
Danger
IfitappearsthatyourPDFreportismissingthestyles(i.e.thetextappearsbutthestyle/layoutisdifferentfromthehtmlversion),probably
yourwkhtmltopdf(http://wkhtmltopdf.org)processcannotreachyourwebservertodownloadthem.
IfyoucheckyourserverlogsandseethattheCSSstylesarenotbeingdownloadedwhengeneratingaPDFreport,mostsurelythisisthe
problem.
Thewkhtmltopdf(http://wkhtmltopdf.org)processwillusethe web.base.url systemparameterastherootpathtoalllinkedfiles,butthis
parameterisautomaticallyupdatedeachtimetheAdministratorisloggedin.Ifyourserverresidesbehindsomekindofproxy,thatcouldnot
bereachable.Youcanfixthisbyaddingoneofthesesystemparameters:
report.url ,pointingtoanURLreachablefromyourserver(probably http://localhost:8069 orsomethingsimilar).Itwillbeusedfor
thisparticularpurposeonly.
web.base.url.freeze ,whensetto True ,willstoptheautomaticupdatesto web.base.url .

Exercise

CreateareportfortheSessionmodel
Foreachsession,itshoulddisplaysession'sname,itsstartandend,andlistthesession'sattendees.
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"/></p>
<h3>Attendees:</h3>
<ul>
<ttforeach="doc.attendee_ids"tas="attendee">
<li><spantfield="attendee.name"/></li>
</t>
</ul>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

Dashboards

Exercise
DefineaDashboard
Defineadashboardcontainingthegraphviewyoucreated,thesessionscalendarviewandalistviewofthecourses(switchabletoaform
view).Thisdashboardshouldbeavailablethroughamenuiteminthemenu,andautomaticallydisplayedinthewebclientwhenthe
OpenAcademymainmenuisselected.
1.Createafile openacademy/views/session_board.xml .Itshouldcontaintheboardview,theactionsreferencedinthatview,anactionto
openthedashboardandaredefinitionofthemainmenuitemtoaddthedashboardaction

Note

Availabledashboardstylesare 1 , 11 , 12 , 21 and 111

2.Update openacademy/__openerp__.py toreferencethenewdatafile

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

#anymodulenecessaryforthisonetoworkcorrectly
#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_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.Theycanallbemodifiedviatheclientinterfacewithcontextualviews.
OdooisaccessiblethroughXMLRPC/JSONRPCinterfaces,forwhichlibrariesexistinmanylanguages.

XML-RPC Library
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,oryourinstructor's).Thisprogramshoulddisplay
allthesessions,andtheircorrespondingnumberofseats.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,
})

JSON-RPC Library
ThefollowingexampleisaPythonprogramthatinteractswithanOdooserverwiththestandardPythonlibraries 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(https://pypi.python.org/pypi/jsonrpclib):
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
ThereareanumberofhighlevelAPIsinvariouslanguagestoaccessOdoosystemswithoutexplicitlygoingthroughXMLRPCorJSON
RPC,suchas:
https://github.com/akretion/ooor(https://github.com/akretion/ooor)
https://github.com/syleam/openobjectlibrary(https://github.com/syleam/openobjectlibrary)
https://github.com/nicolasvan/openerpclientlib(https://github.com/nicolasvan/openerpclientlib)
https://pypi.python.org/pypi/oersted/(https://pypi.python.org/pypi/oersted/)
https://github.com/abhishekjaiswal/phpopenerplib(https://github.com/abhishekjaiswal/phpopenerplib)

[1]itispossibleto disabletheautomaticcreationofsomefields (../reference/orm.html#openerp.models.Model._log_access)


[2]writingrawSQLqueriesispossible,butrequirescareasitbypassesallOdooauthenticationandsecuritymechanisms.