模块组成
-
业务对象
业务对象声明为Python类, 由Odoo自己主动加载.
-
数据文件
XML或CSV文件格式, 在当中声明了元数据(视图或工作流)、配置数据(模块參数)、演示数据等.
-
Web控制器
处理Web浏览器发来的requests.
-
静态web数据
Web用到的图像, CSS或JavaScript文件.
模块结构
一个Odoo模块也是一个Python模块, 存放在一个文件夹中, 包括一个__init__.py文件, 用于导入其它Python模块.
from . import mymodule
odoo.py提供了一个子命令scaffold能够方便地创建一个空的模块.
$ odoo.py scaffold <module name> <where to put it>
命令运行后, 将会创建一个子文件夹而且当中包含了Odoo模块所需的一些基本文件.
练习 #1
运行 ./odoo.py scaffold openacademy addons, 在addons文件夹下创建一个名为openacademy的模块, 生成的文件夹文件结构例如以下.
openacademy
├── __init__.py
├── __openerp__.py
├── controllers.py
├── demo.xml
├── models.py
├── security
│ └── ir.model.access.csv
└── templates.xml
各文件内容请查看文件或查看原文, 然后对__openerp__.py中的几种标识文本进行改动,
至少须要加入'installable':True, 'application':True。
对象关系映射
ORM层是Odoo的一个关键组件, 它能够避免大部分的SQL语句编写从而提高扩展性和安全性.
业务对象用派生自Model的Python类(模型)来编写, 该类的_name属性定义了模型在Odoo系统中的名称.
from openerp import models
class MinimalModel(models.Model):
_name = 'test.model'
字段
字段定义模型可以存储什么以及在哪里存储, 字段在模型类中用属性来定义.
from openerp import models, fields
class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()
通用属性
与模型类似, 字段也能够通过參数传递对其进行设定:
name = field.Char(required=True)
字段的经常使用属性有:
-
string (unicode, default: field’s name)
字段标签名称,会显示在界面上(对用户可见)。
-
required (bool, default: False)
假设值为True,此字段值不能为空,设置默认值或者在创建记录时提供。
-
help (unicode, default: ‘’)
界面上显示提示语。
-
index (bool, default: False)
假设值为True,创建表时将为此列加入索引。
简单字段
字段能够分为两类: 简单字段和关系字段. 前者为原子值, 直接保存在模型相应的数据库表中; 后者连接到其它的记录上(能够是同样的模型也能够是不同的模型).
Boolean, Date, Char这些都是简单字段.
保留字段
Odoo在模型中自己主动创建并维护一些字段, 这些字段就是保留字段, 这些字段数据不须要也不应该手动去改动.
-
id (Id)
一条记录的唯一id。
-
create_date (Datetime)
记录创建时间。
-
create_uid (Many2one)
谁创建的记录。
-
write_date (Datetime)
最后改动时间。
-
write_uid (Many2one)
谁最后改动的记录。
特殊字段
默认情况下, Odoo要求模型中有一个name字段, 用于显示和搜索, 通过设置_rec_name也能够达到这种目的.
练习 #2
在openacademy模块中定义一个新的模型Course, openacademy/models.py内容例如以下:
# -*- coding: utf-8 -*-
from openerp import models, fields, api
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()
数据文件
Odoo是一个高度数据驱动的系统, 尽管使用Python代码来定制模块行为, 但非常多模块数据是在其加载时setup的, 而且有些模块只为Odoo加入数据.
通过数据文件来定义模块数据, 比如能够使用XML文件里的<record>元素定义数据, 每个<record>元素创建或者更新数据库中的一条记录, 形式例如以下:
<openerp>
<data>
<record model="{model name}" id="{record identifier}">
<field name="{a field name}">{a value}</field>
</record>
</data>
<openerp>
-
model
Odoo模型名.
-
id
外部ID(External Identifier), 通过它能够引用到记录(而且不须要知道记录所在的数据库ID).
-
元素
name属性用于确定字段名称(比如description), 该元素的body给出字段的值.
数据文件必须在模块加载清单文件列表中, 也就是__openerp__.py的’data’列表(所有加载)或’demo’列表(仅仅有设定为加载演示数据才会加载)中.
练习 #3
创建一个数据文件来向Course中加入数据, 编辑openacademy/demo.xml, 并确认__openerp__.py的’demo’列表中有该文件.
<openerp>
<data>
<record model="openacademy.course" id="course0">
<field name="name">Course 0</field>
<field name="description">Course 0's description
Can have multiple lines
</field>
</record>
<record model="openacademy.course" id="course1">
<field name="name">Course 1</field>
<!-- no description for this one -->
</record>
<record model="openacademy.course" id="course2">
<field name="name">Course 2</field>
<field name="description">Course 2's description</field>
</record>
</data>
</openerp>
动作和菜单
在Odoo中, 动作和菜单都是定义在数据库中的数据记录, 一般通过数据文件来定义.
动作能够由三种方式触发:
- 点击菜单项(菜单项链接到特定动作)
- 点击视图上的button(假设button连接到动作)
- 作为对象的上下文动作
使用<menuitem>声明一个ir.ui.menu并将其连接到一个action, 能够用以下的形式的代码.
<record model="ir.actions.act_window" id="action_list_ideas">
<field name="name">Ideas</field>
<field name="res_model">idea.idea</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_ideas" parent="menu_root" name="Ideas" sequence="10"
action="action_list_ideas"/>
注意: action必须先于menu的连接使用定义, 数据文件在加载时顺序地运行, 所以动作的ID必须首先存在于数据库中才干使用.
练习 #4
定义一个新的菜单项訪问OpenAcademy课程.
创建openacademy/views/openacademy.xml文件, 并在当中加入动作和菜单.
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
that is an action opening a view or a set of views
-->
<record model="ir.actions.act_window" id="course_list_action">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first course
</p>
</field>
</record>
<!-- top level menu: no parent -->
<menuitem id="main_openacademy_menu" name="Open Academy"/>
<!-- A first level in the left side menu is needed
before using action= attribute -->
<menuitem id="openacademy_menu" name="Open Academy"
parent="main_openacademy_menu"/>
<!-- the following menuitem should appear *after*
its parent openacademy_menu and *after* its
action course_list_action -->
<menuitem id="courses_menu" name="Courses" parent="openacademy_menu"
action="course_list_action"/>
<!-- Full id location:
action="openacademy.course_list_action"
It is not required when it is the same module -->
</data>
</openerp>
在__openerp__.py中加入这个数据文件名称到’data’.
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
],
更新模块后能够看到菜单, 操作看看效果.
基本视图
视图定义了模型数据怎样显示, 每种类型的视图代表一种数据可视化模式.
主要的视图定义
一个视图是以一条ir.ui.view模型数据的形式定义的.
<record model="ir.ui.view" id="view_id">
<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<!-- view content: <form>, <tree>, <graph>, ... -->
</field>
</record>
Tree views
Tree view也被称为list views, 在一个表格中显示记录. 根元素是<tree>, 最简形式的tree view仅仅是简单地列出每条记录的多个字段, 每一个字段为一列.
<tree string="Idea list">
<field name="name"/>
<field name="inventor_id"/>
</tree>
Form views
Form用于创建或编辑单条记录, 根元素是<form>, 能够在form中组合各种高层结构元素(如groups, notebooks)以及交互元素(如buttons, fields).
<form string="Idea form">
<group colspan="4">
<group colspan="2" col="2">
<separator string="General stuff" colspan="2"/>
<field name="name"/>
<field name="inventor_id"/>
</group>
<group colspan="2" col="2">
<separator string="Dates" colspan="2"/>
<field name="active"/>
<field name="invent_date" readonly="1"/>
</group>
<notebook colspan="4">
<page string="Description">
<field name="description" nolabel="1"/>
</page>
</notebook>
<field name="state"/>
</group>
</form>
练习 #5
为openacademy创建form view, views/openacademy.xml数据文件里添加<record model=”ir.ui.view”…>内容.
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="course_form_view">
<field name="name">course.form</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<form string="Course Form">
<sheet>
<group>
<field name="name"/>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
更新模块, 创建一个Course, 能够看到form view变了.
练习 #6
使用notebook. 在form view中, 将description字段放在一个tab中, 方便随后加入其它tabs, 对练习#5的form view数据做例如以下改动.
<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="About">
This is an example of notebooks
</page>
</notebook>
</sheet>
</form>
</field>
更新模块, 看效果.
More
还能够使用HTML为form view提供更加灵活的布局, 比如以下的样例.
<form string="Idea Form">
<header>
<button string="Confirm" type="object" name="action_confirm"
states="draft" class="oe_highlight" />
<button string="Mark as done" type="object" name="action_done"
states="confirmed" class="oe_highlight"/>
<button string="Reset to draft" type="object" name="action_draft"
states="confirmed,done" />
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only" string="Idea Name" />
<h1><field name="name" /></h1>
</div>
<separator string="General" colspan="2" />
<group colspan="2" col="2">
<field name="description" placeholder="Idea description..." />
</group>
</sheet>
</form>
Search views
Search views用来自己定义list views及其他统计/多条记录视图中的搜索字段. 根元素为<search>, 其子元素定义了在哪些字段上进行搜索.
<search>
<field name="name"/>
<field name="inventor_id"/>
</search>
假设一个模型未定义相应的Search view, odoo自己主动创建一个仅搜索name字段的search view.
练习 #7
加入title以及description搜索, 在views/openacademy.xml中定义search view.
</field>
</record>
<record model="ir.ui.view" id="course_search_view">
<field name="name">course.search</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="description"/>
</search>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
更新模块, 搜索框输入字符后可以看到下方可以选择搜索description字段.
模型中的关联
概述
一个模型中的记录可能关联到其它模型的记录, 比如销售订单记录会关联到一个包括客户信息的客户记录.
练习 #8
为了说明数据关联, 首先添加新的模型.
Open Academy模块中, 一个session是一个在特定时间针对特定听众讲授课程的过程. 须要为session创建对应的模型.
session具有name, 開始日期, 持续时间以及座位数量等. 此外还须要加入对应的action和menuitem显示模型数据.
首先在openacademy/models.py中创建Session类.
class Session(models.Model):
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
然后在openacademy/view/openacademy.xml中加入用于訪问session模型的action和menuitem定义.
<!-- Full id location:
action="openacademy.course_list_action"
It is not required when it is the same module -->
<!-- session form view -->
<record model="ir.ui.view" id="session_form_view">
<field name="name">session.form</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<sheet>
<group>
<field name="name"/>
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</data>
</openerp>
digits=(6,2)确定浮点数的精度, 6表示总的数字位数(不包含小数点), 2表示小数点后的位数. 所以, digits=(6,2)小数点前最多4位.
关联字段
练习 #9
概述
- 使用many2one改动Course和Session模型(model),反映出与其它模型(model)的关联:
- 每一个Course有一个负责人。other_model值为res.users
- 每一个Session有一个老师。other_model值为res.partner
- 一个Session关联一个Course,other_model值为openacademy.course,必填
- 调整view。
name = fields.Char(string="Title", required=True)
description = fields.Text()
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
class Session(models.Model):
_name = 'openacademy.session'
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
</field>
</record>
<!-- override the automatically generated list view for courses -->
<record model="ir.ui.view" id="course_tree_view">
<field name="name">course.tree</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<tree string="Course Tree">
<field name="name"/>
<field name="responsible_id"/>
</tree>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
<form string="Session Form">
<sheet>
<group>
<group string="General">
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
</group>
<group string="Schedule">
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- session tree/list view -->
<record model="ir.ui.view" id="session_tree_view">
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
Exercise
Inverse one2many relations
Using the inverse relational field one2many, modify the models to reflect the relation between courses and sessions.
- Modify the
Courseclass, and - add the field in the course form view.
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
class Session(models.Model):
<page string="Description">
<field name="description"/>
</page>
<page string="Sessions">
<field name="session_ids">
<tree string="Registered sessions">
<field name="name"/>
<field name="instructor_id"/>
</tree>
</field>
</page>
</notebook>
</sheet>
Exercise
Multiple many2many relations
Using the relational field many2many, modify the Session model to relate every session to a set of attendees. Attendees will be represented
by partner records, so we will relate to the built-in model res.partner.
Adapt the views accordingly.
- Modify the
Sessionclass, and - add the field in the form view.
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")
<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>
Inheritance
Model inheritance
Odoo provides two inheritance mechanisms to extend an existing model in a modular way.
The first inheritance mechanism allows a module to modify the behavior of a model defined in another module:
- add fields to a model,
- override the definition of fields on a model,
- add constraints to a model,
- add methods to a model,
- override existing methods on a model.
The second inheritance mechanism (delegation) allows to link every record of a model to a record in a parent model, and provides transparent access to the fields of the parent record.
View inheritance
Instead of modifying existing views in place (by overwriting them), Odoo provides view inheritance where children "extension" views are applied on top of root views, and can add or remove content from their parent.
An extension view references its parent using the inherit_id field,
and instead of a single view its arch field is composed
of any number of xpath elements selecting and altering
the content of their parent view:
<!-- improved idea categories list -->
<record id="idea_category_list2" model="ir.ui.view">
<field name="name">id.category.list2</field>
<field name="model">idea.category</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">
<!-- find field description and add the field
idea_ids after it -->
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
</record>
-
expr - An XPath expression selecting a single element in the parent view. Raises an error if it matches no element or more than one
position-
Operation to apply to the matched element:
-
inside - appends
xpath's body at the end of the matched element replace- replaces the matched element by the
xpath's body before- inserts the
xpath's body as a sibling before the matched element after- inserts the
xpaths's body as a sibling after the matched element attributes- alters the attributes of the matched element using special
attributeelements in thexpath's body
-
Tip
When matching a single element, the position attribute
can be set directly on the element to be found. Both inheritances below will give the same result.
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
Exercise
Alter existing content
- Using model inheritance, modify the existing Partner model to add an
instructorboolean field, and a many2many field that corresponds to the session-partner relation - Using view inheritance, display this fields in the partner form view
Note
This is the opportunity to introduce the developer mode to inspect the view, find its external ID and the place to put the new field.
- Create a file
openacademy/partner.pyand import it in__init__.py - Create a file
openacademy/views/partner.xmland add it to__openerp__.py