0% found this document useful (0 votes)
156 views43 pages

Building A Module - Odoo 9

The document provides information about building an Odoo module. It describes how to create an empty module called "Open Academy" using the Odoo scaffolding command. It then summarizes the standard files and structure created for the new module, including: 1) The manifest file (__openerp__.py) which declares metadata for the module. 2) An __init__.py file importing models and controllers. 3) Template controller and model files with example code commented out. 4) Additional files like a demo data file, security rules, and template views. The document then provides an overview of the Odoo object-relational mapper and how to define models and fields to structure the data in a module.

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)
156 views43 pages

Building A Module - Odoo 9

The document provides information about building an Odoo module. It describes how to create an empty module called "Open Academy" using the Odoo scaffolding command. It then summarizes the standard files and structure created for the new module, including: 1) The manifest file (__openerp__.py) which declares metadata for the module. 2) An __init__.py file importing models and controllers. 3) Template controller and model files with example code commented out. 4) Additional files like a demo data file, security rules, and template views. The document then provides an overview of the Odoo object-relational mapper and how to define models and fields to structure the data in a module.

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/ 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.

You might also like