Sie sind auf Seite 1von 65

tart/StoptheOdooserver

Odoousesaclient/serverarchitectureinwhichclientsarewebbrowsersaccessingtheOdooserverviaRPC.

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

Inordertostarttheserver,simplyinvokethecommandodoo.pyintheshell,addingthefullpathtothefileif
necessary:

odoo.py

Theserverisstoppedbyhitting CtrlC twicefromtheterminal,orbykillingthecorrespondingOSprocess.

BuildanOdoomodule
Bothserverandclientextensionsarepackagedasmoduleswhichareoptionallyloadedinadatabase.

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

EverythinginOdoothusstartsandendswithmodules.

Compositionofamodule
AnOdoomodulecancontainanumberofelements:

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

Modulestructure
Eachmoduleisadirectorywithinamoduledirectory.Moduledirectoriesarespecifiedbyusingthe
Eachmoduleisadirectorywithinamoduledirectory.Moduledirectoriesarespecifiedbyusingthe addons
path option.

Tip
mostcommandlineoptionscanalsobesetusingaconfigurationfile

AnOdoomoduleisdeclaredbyitsmanifest.Seethemanifestdocumentationinformationaboutit.

AmoduleisalsoaPythonpackagewitha __init__.py file,containingimportinstructionsforvariousPython


filesinthemodule.

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

from.importmymodule

Odooprovidesamechanismtohelpsetupanewmodule,odoo.pyhasasubcommandscaffoldtocreatean
emptymodule:

$odoo.pyscaffold<modulename><wheretoputit>

Thecommandcreatesasubdirectoryforyourmodule,andautomaticallycreatesabunchofstandardfilesfora
module.MostofthemsimplycontaincommentedcodeorXML.Theusageofmostofthosefileswillbeexplained
alongthistutorial.

Exercise
Modulecreation
UsethecommandlineabovetocreateanemptymoduleOpenAcademy,andinstallitin
Odoo.

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

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

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

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

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

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

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

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

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

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

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

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

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

fromopenerpimportmodels,fields,api

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

#name=fields.Char()
openacademy/security/ir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0
openacademy/templates.xml
<openerp>
<data>
<!<templateid="listing">>
<!<ul>>
<!<litforeach="objects"tas="object">>
<!<atattfhref="{{root}}/objects/{{object.id}}">>
<!<ttesc="object.display_name"/>>
<!</a>>
<!</li>>
<!</ul>>
<!</template>>
<!<templateid="object">>
<!<h1><ttesc="object.display_name"/></h1>>
<!<dl>>
<!<ttforeach="object._fields"tas="field">>
<!<dt><ttesc="field"/></dt>>
<!<dd><ttesc="object[field]"/></dd>>
<!</t>>
<!</dl>>
<!</template>>
</data>
</openerp>

ObjectRelationalMapping
AkeycomponentofOdooistheORMlayer.ThislayeravoidshavingtowritemostSQLbyhandandprovides
extensibilityandsecurityservices2.

BusinessobjectsaredeclaredasPythonclassesextending Model whichintegratesthemintotheautomated


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

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

Modelfields
Fieldsareusedtodefinewhatthemodelcanstoreandwhere.Fieldsaredefinedasattributesonthemodelclass:

fromopenerpimportmodels,fields

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

name=fields.Char()

CommonAttributes

Muchlikethemodelitself,itsfieldscanbeconfigured,bypassingconfigurationattributesasparameters:

name=field.Char(required=True)

Someattributesareavailableonallfields,herearethemostcommonones:

string ( unicode ,default:field'sname)


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

Simplefields

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

Exampleofsimplefieldsare Boolean , Date , Char .


Reservedfields

Odoocreatesafewfieldsinallmodels1.Thesefieldsaremanagedbythesystemandshouldn'tbewrittento.
Theycanbereadifusefulornecessary:

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

Specialfields

Bydefault,Odooalsorequiresa name fieldonallmodelsforvariousdisplayandsearchbehaviors.Thefieldused


forthesepurposescanbeoverriddenbysetting _rec_name .

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

fromopenerpimportmodels,fields,api

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

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

Datafiles
Odooisahighlydatadrivensystem.AlthoughbehavioriscustomizedusingPythoncodepartofamodule'svalue
isinthedataitsetsupwhenloaded.
Tip
somemodulesexistsolelytoadddataintoOdoo

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


updatesadatabaserecord.

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

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

Datafileshavetobedeclaredinthemanifestfiletobeloaded,theycanbedeclaredinthe 'data' list(always


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

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

ActionsandMenus
Actionsandmenusareregularrecordsindatabase,usuallydeclaredthroughdatafiles.Actionscanbetriggered
inthreeways:

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

Becausemenusaresomewhatcomplextodeclarethereisa <menuitem> shortcuttodeclarean ir.ui.menu and


connectittothecorrespondingactionmoreeasily.

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

Exercise
Definenewmenuentries
DefinenewmenuentriestoaccesscoursesandsessionsundertheOpenAcademymenu
entry.Ausershouldbeableto
displayalistofallthecourses
create/modifycourses

1.Create openacademy/views/openacademy.xml withanactionandthemenustriggeringthe


action
2.Addittothe data listof openacademy/__openerp__.py

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

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

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

Viewinheritanceallowsalteringviewsdeclaredelsewhere(addingorremovingcontent).

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

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

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

Treeviews
Treeviews,alsocalledlistviews,displayrecordsinatabularform.

Theirrootelementis <tree> .Thesimplestformofthetreeviewsimplylistsallthefieldstodisplayinthetable


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

Formviews
Formsareusedtocreateandeditsinglerecords.

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


interactiveelements(buttonsandfields):

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

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

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

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

<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",

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

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

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

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

Ifnosearchviewexistsforthemodel,Odoogeneratesonewhichonlyallowssearchingonthe name field.


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

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

<!windowaction>
<!
Thefollowingtagisanactiondefinitionfora"windowaction",

Relationsbetweenmodels
Arecordfromamodelmayberelatedtoarecordfromanothermodel.Forinstance,asaleorderrecordisrelated
toaclientrecordthatcontainstheclientdataitisalsorelatedtoitssaleorderlinerecords.

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

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

openacademy/models.py

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


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


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

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

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

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

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

Relationalfields
Relationalfieldslinkrecords,eitherofthesamemodel(hierarchies)orbetweendifferentmodels.

Relationalfieldtypesare:

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

printfoo.other_id.name

Seealso
foreignkeys

One2many(other_model,related_field)

Avirtualrelationship,inverseofa Many2one .A One2many behavesasacontainerofrecords,accessingit


resultsina(possiblyempty)setofrecords:

forotherinfoo.other_ids:
printother.name

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

Many2many(other_model)

Bidirectionalmultiplerelationship,anyrecordononesidecanberelatedtoanynumberofrecordsonthe
otherside.Behavesasacontainerofrecords,accessingitalsoresultsinapossiblyemptysetofrecords:

forotherinfoo.other_ids:
printother.name

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

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

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

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

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

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

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

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

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

Exercise
Inverseone2manyrelations
Usingtheinverserelationalfieldone2many,modifythemodelstoreflecttherelationbetween
coursesandsessions.

1.Modifythe Course class,and


2.addthefieldinthecourseformview.

openacademy/models.py

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

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

1.Modifythe Session class,and


2.addthefieldintheformview.

openacademy/models.py
instructor_id=fields.Many2one('res.partner',string="Instructor")
course_id=fields.Many2one('openacademy.course',
ondelete='cascade',string="Course",required=True)
attendee_ids=fields.Many2many('res.partner',string="Attendees")
openacademy/views/openacademy.xml
<fieldname="seats"/>
</group>
</group>
<labelfor="attendee_ids"/>
<fieldname="attendee_ids"/>
</sheet>
</form>
</field>

Inheritance
Modelinheritance
Odooprovidestwoinheritancemechanismstoextendanexistingmodelinamodularway.

Thefirstinheritancemechanismallowsamoduletomodifythebehaviorofamodeldefinedinanothermodule:

addfieldstoamodel,
overridethedefinitionoffieldsonamodel,
addconstraintstoamodel,
addmethodstoamodel,
overrideexistingmethodsonamodel.

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

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

Anextensionviewreferencesitsparentusingthe inherit_id field,andinsteadofasingleviewits arch fieldis


composedofanynumberof xpath elementsselectingandalteringthecontentoftheirparentview:

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

expr
AnXPathexpressionselectingasingleelementintheparentview.Raisesanerrorifitmatchesnoelement
ormorethanone
position

Operationtoapplytothematchedelement:

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

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

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

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

Note
Note
Thisistheopportunitytointroducethedevelopermodetoinspectthe
view,finditsexternalIDandtheplacetoputthenewfield.

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


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

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

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

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

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

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

Domains

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

Forinstance,whenusedontheProductmodelthefollowingdomainselectsallserviceswithaunitprice
over1000:

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

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


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

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

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

instructor_id=fields.Many2one('res.partner',string="Instructor",
domain=[('instructor','=',True)])
course_id=fields.Many2one('openacademy.course',
ondelete='cascade',string="Course",required=True)
attendee_ids=fields.Many2many('res.partner',string="Attendees")

Note
Adomaindeclaredasaliterallistisevaluatedserversideandcan'trefer
todynamicvaluesontherighthandside,adomaindeclaredasastringis
evaluatedclientsideandallowsfieldnamesontherighthandside

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

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

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

instructor_id=fields.Many2one('res.partner',string="Instructor",
domain=['|',('instructor','=',True),
('category_id.name','ilike',"Teacher")])
course_id=fields.Many2one('openacademy.course',
ondelete='cascade',string="Course",required=True)
attendee_ids=fields.Many2many('res.partner',string="Attendees")
openacademy/views/partner.xml
<menuitemid="contact_menu"name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>

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

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

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

Tocreateacomputedfield,createafieldandsetitsattribute compute tothenameofamethod.Thecomputation


methodshouldsimplysetthevalueofthefieldtocomputeoneveryrecordin self .

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

importrandom
fromopenerpimportmodels,fields

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

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

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

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

fromopenerpimportmodels,fields,api

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

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

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

1.AddacomputedfieldtoSession
2.ShowthefieldintheSessionview:

openacademy/models.py
course_id=fields.Many2one('openacademy.course',
ondelete='cascade',string="Course",required=True)
attendee_ids=fields.Many2many('res.partner',string="Attendees")

taken_seats=fields.Float(string="Takenseats",compute='_taken_seats')

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

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

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

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

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

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

Note
Odoohasbuiltinrulesmakingfieldswithan active fieldset
to False invisible.

Onchange
The"onchange"mechanismprovidesawayfortheclientinterfacetoupdateaformwhenevertheuserhasfilled
inavalueinafield,withoutsavinganythingtothedatabase.

Forinstance,supposeamodelhasthreefields amount , unit_price and price ,andyouwanttoupdatetheprice


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

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

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

Forcomputedfields,valued onchange behaviorisbuiltinascanbeseenbyplayingwiththeSessionform:


changethenumberofseatsorparticipants,andthe taken_seats progressbarisautomaticallyupdated.
Exercise
Warning
Addanexplicitonchangetowarnaboutinvalidvalues,likeanegativenumberofseats,or
moreparticipantsthanseats.
openacademy/models.py
r.taken_seats=0.0
else:
r.taken_seats=100.0*len(r.attendee_ids)/r.seats

@api.onchange('seats','attendee_ids')
def_verify_valid_seats(self):
ifself.seats<0:
return{
'warning':{
'title':"Incorrect'seats'value",
'message':"Thenumberofavailableseatsmaynotbenegative",
},
}
ifself.seats<len(self.attendee_ids):
return{
'warning':{
'title':"Toomanyattendees",
'message':"Increaseseatsorremoveexcessattendees",
},
}

Modelconstraints
Odooprovidestwowaystosetupautomaticallyverifiedinvariants: Pythonconstraints and SQLconstraints .

APythonconstraintisdefinedasamethoddecoratedwith constrains() ,andinvokedonarecordset.The


decoratorspecifieswhichfieldsareinvolvedintheconstraint,sothattheconstraintisautomaticallyevaluated
whenoneofthemismodified.Themethodisexpectedtoraiseanexceptionifitsinvariantisnotsatisfied:

fromopenerp.exceptionsimportValidationError

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

fromopenerpimportmodels,fields,api,exceptions

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

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

SQLconstraintsaredefinedthroughthemodelattribute _sql_constraints .Thelatterisassignedtoalistoftriples


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

1.CHECKthatthecoursedescriptionandthecoursetitlearedifferent
2.MaketheCourse'snameUNIQUE

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

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

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

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

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

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

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

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

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

colors

mappingsofcolorstoconditions.Iftheconditionevaluatesto True ,thecorrespondingcolorisappliedto


therow:

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

Clausesareseparatedby ; ,thecolorandconditionareseparatedby : .

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

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

Calendars
Calendars
Displaysrecordsascalendarevents.Theirrootelementis <calendar> andtheirmostcommonattributesare:

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

field(todefinethelabelforeachcalendarevent)

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

Exercise
Calendarview
AddaCalendarviewtotheSessionmodelenablingtheusertoviewtheeventsassociatedto
theOpenAcademy.

1.Addan end_date fieldcomputedfrom start_date and duration

Tip
theinversefunctionmakesthefieldwritable,andallowsmovingthe
sessions(viadraganddrop)inthecalendarview

2.AddacalendarviewtotheSessionmodel
3.AndaddthecalendarviewtotheSessionmodel'sactions

openacademy/models.py
#*coding:utf8*

fromdatetimeimporttimedelta
fromopenerpimportmodels,fields,api,exceptions

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

taken_seats=fields.Float(string="Takenseats",compute='_taken_seats')
end_date=fields.Date(string="EndDate",store=True,
compute='_get_end_date',inverse='_set_end_date')

@api.depends('seats','attendee_ids')
def_taken_seats(self):
},
}

@api.depends('start_date','duration')
def_get_end_date(self):
forrinself:
ifnot(r.start_dateandr.duration):
r.end_date=r.start_date
continue

#Adddurationtostart_date,but:Monday+5days=Saturday,so
#subtractonesecondtogetonFridayinstead
start=fields.Datetime.from_string(r.start_date)
duration=timedelta(days=r.duration,seconds=1)
r.end_date=start+duration

def_set_end_date(self):
forrinself:
ifnot(r.start_dateandr.end_date):
continue

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

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

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

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

<menuitemid="session_menu"name="Sessions"

Searchviews

Searchview elementscanhavea thatoverridesthedomaingeneratedforsearching


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

Searchviewscanalsocontain <filter> elements,whichactastogglesforpredefinedsearches.Filtersmust


haveoneofthefollowingattributes:

domain
addthegivendomaintothecurrentsearch
context
addsomecontexttothecurrentsearchusethekey group_by togroupresultsonthegivenfieldname

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

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

Touseanondefaultsearchviewinanaction,itshouldbelinkedusingthe search_view_id fieldoftheaction


record.

Theactioncanalsosetdefaultvaluesforsearchfieldsthroughits context field:contextkeysofthe


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

1.Addabuttontofilterthecoursesforwhichthecurrentuseristheresponsibleinthe
coursesearchview.Makeitselectedbydefault.
2.Addabuttontogroupcoursesbyresponsibleuser.

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

Gantt
Horizontalbarchartstypicallyusedtoshowprojectplanningandadvancement,theirrootelementis <gantt> .

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

Exercise
Ganttcharts
AddaGanttChartenablingtheusertoviewthesessionsschedulinglinkedtotheOpen
Academymodule.Thesessionsshouldbegroupedbyinstructor.

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

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

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

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

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

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

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

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

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

<menuitemid="session_menu"name="Sessions"

Graphviews
Graphviewsallowaggregatedoverviewandanalysisofmodels,theirrootelementis <graph> .

Graphviewshave4displaymodes,thedefaultmodeisselectedusingthe @type attribute.

Pivot
amultidimensionaltable,allowstheselectionoffilersanddimensionstogettherightaggregateddataset
beforemovingtoamoregraphicaloverview
Bar(default)

abarchart,thefirstdimensionisusedtodefinegroupsonthehorizontalaxis,otherdimensionsdefine
aggregatedbarswithineachgroup.

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

Line
2dimensionallinechart
Pie
2dimensionalpie

Graphviewscontain <field> withamandatory @type attributetakingthevalues:

row (default)
thefieldshouldbeaggregatedbydefault
measure
thefieldshouldbeaggregatedratherthangroupedon

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

Exercise
Graphview
AddaGraphviewintheSessionobjectthatdisplays,foreachcourse,thenumberof
attendeesundertheformofabarchart.

1.Addthenumberofattendeesasastoredcomputedfield
2.Thenaddtherelevantview

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

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

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

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

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

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

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

<menuitemid="session_menu"name="Sessions"

Kanban
Usedtoorganizetasks,productionprocesses,etctheirrootelementis <kanban> .

Akanbanviewshowsasetofcardspossiblygroupedincolumns.Eachcardrepresentsarecord,andeach
columnthevaluesofanaggregationfield.

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

Kanbanviewsdefinethestructureofeachcardasamixofformelements(includingbasicHTML)andQWeb.

Exercise
Kanbanview
AddaKanbanviewthatdisplayssessionsgroupedbycourse(columnsarethuscourses).

1.Addaninteger color fieldtotheSessionmodel


2.Addthekanbanviewandupdatetheaction

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

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

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

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

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

Workflows
Workflowsaremodelsassociatedtobusinessobjectsdescribingtheirdynamics.Workflowsarealsousedtotrack
processesthatevolveovertime.

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

1.Addanew state field


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

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

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

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

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

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

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

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

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

Warning
Aworkflowassociatedwithamodelisonlycreatedwhenthemodel'srecordsarecreated.
Thusthereisnoworkflowinstanceassociatedwithsessioninstancescreatedbeforethe
workflow'sdefinition

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

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

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

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

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

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

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

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

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

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

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

Accessrights
Accessrightsaredefinedasrecordsofthemodel ir.model.access .Eachaccessrightisassociatedtoamodel,a
group(ornogroupforglobalaccess),andasetofpermissions:read,write,create,unlink.Suchaccessrightsare
usuallycreatedbyaCSVfilenamedafteritsmodel: ir.model.access.csv .

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0
access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0
Exercise
AddaccesscontrolthroughtheOpenERPinterface
Createanewuser"JohnSmith".Thencreateagroup"OpenAcademy/SessionRead"with
readaccesstotheSessionmodel.

1.CreateanewuserJohnSmiththroughSettingsUsersUsers

2.Createanewgroup session_read throughSettingsUsersGroups,itshouldhave


readaccessontheSessionmodel
3.EditJohnSmithtomakethemamemberof session_read
4.LoginasJohnSmithtochecktheaccessrightsarecorrect

Exercise
Addaccesscontrolthroughdatafilesinyourmodule
Usingdatafiles,
CreateagroupOpenAcademy/ManagerwithfullaccesstoallOpenAcademymodels
MakeSessionandCoursereadablebyallusers

1.Createanewfile openacademy/security/security.xml toholdtheOpenAcademy


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

openacademy/__openerp__.py

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

Hereisanexampleofarulethatpreventsthedeletionofleadsthatarenotinstate cancel .Noticethatthevalue


ofthefield groups mustfollowthesameconventionasthemethod write() oftheORM.

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

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

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

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

Wewanttocreateawizardthatallowuserstocreateattendeesforaparticularsession,orforalistofsessionsat
once.

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

fromopenerpimportmodels,fields,api

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

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

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

Thereisanotherwaytolaunchthewizard:usingan ir.actions.act_window recordlikeabove,butwithanextra


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

Wizardsuseregularviewsandtheirbuttonsmayusetheattribute special="cancel" toclosethewizardwindow


withoutsaving.
Exercise
Launchthewizard

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

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

def_default_session(self):
returnself.env['openacademy.session'].browse(self._context.get('active_id'))

session_id=fields.Many2one('openacademy.session',
string="Session",required=True,default=_default_session)
attendee_ids=fields.Many2many('res.partner',string="Attendees")
openacademy/views/openacademy.xml
parent="openacademy_menu"
action="session_list_action"/>

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

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

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

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

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

def_default_sessions(self):
returnself.env['openacademy.session'].browse(self._context.get('active_ids'))

session_ids=fields.Many2many('openacademy.session',
string="Sessions",required=True,default=_default_sessions)
attendee_ids=fields.Many2many('res.partner',string="Attendees")

@api.multi
defsubscribe(self):
forsessioninself.session_ids:
session.attendee_ids|=self.attendee_ids
return{}

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

Tip
TheGNUgettextformat(PortableObject)usedbyOdooisintegratedintoLaunchPad,
makingitanonlinecollaborativetranslationplatform.

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

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

Exercise
Translateamodule
ChooseasecondlanguageforyourOdooinstallation.Translateyourmoduleusingthe
facilitiesprovidedbyOdoo.

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

openacademy/models.py
#*coding:utf8*

fromdatetimeimporttimedelta
fromopenerpimportmodels,fields,api,exceptions,_

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

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

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

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

Reporting
Printedreports
Odoo8.0comeswithanewreportenginebasedonQWeb,TwitterBootstrapandWkhtmltopdf.

Areportisacombinationtwoelements:

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


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

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

thestandardrenderingcontextprovidesanumberofelements,themost
importantbeing:

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

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

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

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

Dashboards

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

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


actionsreferencedinthatview,anactiontoopenthedashboardandaredefinitionofthe
mainmenuitemtoaddthedashboardaction

Note
Availabledashboardstylesare 1 , 11 , 12 , 21 and 111

2.Update openacademy/__openerp__.py toreferencethenewdatafile

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

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

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

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

XMLRPC
JSONRPC

Businessobjectscanalsobeaccessedviathedistributedobjectmechanism.Theycanallbemodifiedviathe
clientinterfacewithcontextualviews.

OdooisaccessiblethroughXMLRPC/JSONRPCinterfaces,forwhichlibrariesexistinmanylanguages.

XMLRPCLibrary
ThefollowingexampleisaPythonprogramthatinteractswithanOdooserverwiththelibrary xmlrpclib :

importxmlrpclib

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

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

#Createanewnote
sock=xmlrpclib.ServerProxy(root+'object')
args={
'color':8,
'memo':'Thisisanote',
'create_uid':uid,
}
note_id=sock.execute(DB,uid,PASS,'note.note','create',args)
Exercise
Addanewservicetotheclient
WriteaPythonprogramabletosendXMLRPCrequeststoaPCrunningOdoo(yours,or
yourinstructor's).Thisprogramshoulddisplayallthesessions,andtheircorresponding
numberofseats.Itshouldalsocreateanewsessionforoneofthecourses.
importfunctools
importxmlrpclib
HOST='localhost'
PORT=8069
DB='openacademy'
USER='admin'
PASS='admin'
ROOT='http://%s:%d/xmlrpc/'%(HOST,PORT)

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

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

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

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

JSONRPCLibrary
ThefollowingexampleisaPythonprogramthatinteractswithanOdooserverwiththestandardPython
libraries urllib2 and json :

importjson
importrandom
importurllib2

defjson_rpc(url,method,params):
data={
"jsonrpc":"2.0",
"method":method,
"params":params,
"id":random.randint(0,1000000000),
}
req=urllib2.Request(url=url,data=json.dumps(data),headers={
"ContentType":"application/json",
})
reply=json.load(urllib2.urlopen(req))
ifreply.get("error"):
raiseException(reply["error"])
returnreply["result"]

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

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

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

Hereisthesameprogram,usingthelibraryjsonrpclib:

importjsonrpclib

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

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

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

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

ExamplescanbeeasilyadaptedfromXMLRPCtoJSONRPC.

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

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

Das könnte Ihnen auch gefallen