警告
该教程需要 已安装odoo
开启/停止Odoo服务
odoo使用客户端/服务器架构,客户端是通过RPC(远程过程调用协议)访问Odoo服务的web浏览器。
业务逻辑和扩展通常在服务器端执行,尽管支持客户端功能(如,想交互式地图的新数据表示)可以添加到客户端。
为了启动服务器,在shell内简单的调用命令Odoo,如果必要添加文件的完整路径
odoo-bin
服务器按Ctrl-C两次从终端停止,或杀死相应的OS进程。
建立一个Odoo模块
服务器和客户端扩展都打包为可选择的加载在数据库中的模块。
Odoo模块既可以添加新的业务逻辑到Odoo系统,也可以修改和扩展现有的业务逻辑:一个模块可以被创建为Odoo通用会计支持添加你的国家的会计准则,而下一个模块,增加了对公车的实时可视化支持。
因此,odoo的一切都随着模块的开始结束而开始结束。
1.模块的组成
一个Odoo模块可以包含多个元素:
业务对象:
声明为Python的类,这些资源是自动持续的,通过基于配置的Odoo。
数据文件
XML或CSV文件声明元数据(视图或工作流),配置数据(模块参数化),演示数据和更多
Web controllers
处理Web浏览器的请求
静态web数据
Web界面或网站使用的图片,CSS或JS文件
2.模块结构
每个模块是模块目录内的目录。模块目录通过–addons-path 选项被指定。
提示
大多数命令行选项也可以使用配置文件来设置
odoo模块是通过它的manifest声明的。一个模块也是一个带__init__.py
的Python包,包含模块中各种Python文件的导入说明。
例如,如果模块有一个mymodule.py
, __init__.py
应该包含文件:
from . import mymodule
Odoo提供了一种机制来帮助建立一个新的模块,odoo-bin有一子命令scaffold 创建一个空的模块:
odoo-bin scaffold <module name> <where to put it>
该命令为模块创建一个子目录,并自动为模块创建一堆标准文件。其中大部分只包含注释代码或xml。这些文件的使用将在本教程中解释。
练习
模块创建
使用命令行上创建一个空的模块Open Academy,并安装在Odoo上。
1. 调用命令odoo-bin scaffold openacademy odoo/addons
2. 将manifest文件更新到模块。
3. 不要改变其他文件。
openacademy/__manifest__.py::
# -*- coding: utf-8 -*-
{
'name': "Open Academy",
'summary': """Manage trainings""",
'description': """
Open Academy module for managing trainings:
- training courses
- training sessions
- attendees registration
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml
# for the full list
'category': 'Test',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo.xml',
],
}
openacademy/__init__.py:
# -*- coding: utf-8 -*-
from . import controllers
from . import models
openacademy/controllers.py:
# -*- coding: utf-8 -*-
from odoo import http
# class Openacademy(http.Controller):
# @http.route('/openacademy/openacademy/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/openacademy/openacademy/objects/', auth='public')
# def list(self, **kw):
# return http.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')
# def object(self, obj, **kw):
# return http.request.render('openacademy.object', {
# 'object': obj
# })
openacademy/demo.xml:
<odoo>
<data>
<!-- -->
<!-- <record id="object0" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 0</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object1" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 1</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object2" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 2</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object3" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 3</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object4" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 4</field> -->
<!-- </record> -->
<!-- -->
</data>
</odoo>
openacademy/models.py:
# -*- coding: utf-8 -*-
from odoo import models, fields, api
# class openacademy(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:
<odoo>
<data>
<!-- <template id="listing"> -->
<!-- <ul> -->
<!-- <li t-foreach="objects" t-as="object"> -->
<!-- <a t-attf-href="{{ root }}/objects/{{ object.id }}"> -->
<!-- <t t-esc="object.display_name"/> -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </template> -->
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->
<!-- <t t-foreach="object._fields" t-as="field"> -->
<!-- <dt><t t-esc="field"/></dt> -->
<!-- <dd><t t-esc="object[field]"/></dd> -->
<!-- </t> -->
<!-- </dl> -->
<!-- </template> -->
</data>
</odoo>
3.对象关系映射
Odoo的一个关键组成部分是ORM层。这一层可以避免手工编写大多数SQL并提供了可扩展性和安全性的服务。
业务对象被声明为Python类扩展模型,该模型将它们集成到自动持久化系统中。
模型可以通过在它们的定义中设置一些属性进行配置。最重要的属性是_name,这是必要的,在Odoo中定义系统模型的名字。这里是一个模型的最小完整定义:
from odoo import models
class MinimalModel(models.Model):
_name = 'test.model'
4.模型文件
字段用于定义模型可以存储和在哪里。字段被定义为模型类的属性:
from odoo import models, fields
class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()
(1) 共同属性
就像模型本身一样,它的字段可以通过将配置属性作为参数来配置:
name = field.Char(required=True)
一些属性在所有领域都可用,这里是最常见的:
string (unicode, default: field's name)
用户界面中字段的标签(用户可见)。
required (bool, default: False)
如果为TRUE,字段不能为空,则必须具有默认值或在创建记录时始终给予值。
help (unicode, default: '')
Long-form, 为用户提供了一个帮助提示。
index (bool, default: False)
要求 Odoo创建列上的数据库索引。
(2) 简单字段
有两种类型的字段:“simple”字段,它们直接存储在模型表中的原子值,以及连接记录(同一模型或不同模型)的“relational”字段。
简单字段的例子:Boolean, Date, Char.
(3) 保留字段
Odoo在所有模型创造了一些字段。这些字段由系统管理,不应写入。如果有用或必要,它们可以被读取:
id (Id)
模型中记录的唯一标识符。
create_date (Datetime)
记录的创建日期.
create_uid (Many2one)
创建记录的用户。
write_date (Datetime)
记录的最后修改日期。
write_uid (Many2one)
最后修改记录的用户。
(4) 特殊字段
默认情况下,Odoo所有的模型也需要一个name字段,以便各种显示和搜索行为。被用于这些目的的字段能通过设置_rec_name
重写。
练习
定义一个模型
在openacademy 模块里 定义一个新的数据模型Course ,course 有标题(title)和描述(description) ,必须有标题
编辑openacademy/models/models.py文件,包含到Course 类中。
openacademy/models.py:
from odoo import models, fields, api
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()
5.数据字段
虽然使用Python代码定制行为,但模块的值的一部分在加载时设置的数据中。
提示
一些模块的存在只为数据添加到Odoo。
模块数据通过数据文件,带有<record>
元素的xml文件声明。每个元素创建或更新数据库记录。
<odoo>
<data>
<record model="{model name}" id="{record identifier}">
<field name="{a field name}">{a value}</field>
</record>
</data>
</odoo>
· model是用于记录Odoo模型名称。
· id是一个外部标识符,它允许引用到记录(而不必知道它的内部数据库标识符)。
· <field>
元素的name是模型中字段的name(例如描述)。他们的body是字段的值。
必须在清单文件中声明要加载的数据文件。它们可以在“data”列表中(总是加载)或在“demo”列表中声明(仅在演示模式下加载)。
练习
定义演示数据 ,用演示课程创建演示数据,完善课程模式。
编辑openacademy/demo/demo.xml文件来包含一些数据
openacademy/demo.xml:
<odoo>
<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>
</odoo>
- Actions和Menus
动作和菜单是数据库中的常规记录,通常通过数据文件声明。动作可以用三种方式触发:
(1) 通过点击菜单项(链接到特定的动作)
(2) 通过点击视图的按钮 (如果被连接到动作)
(3) 作为对象的上下文操作
因为菜单的声明有点复杂,有一个<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"/>
危险
必须在xml文件的相应菜单前声明该操作。
数据文件是按顺序执行的,在菜单能创建前,动作的id必须存在于数据库中
-
练习
定义新的菜单入口
在OpenAcademy 菜单入口定义新的菜单入口来访问课程,用户应该能够:
· 显示所有课程列表
· 创建/修改课程
(1) 创建openacademy/views/openacademy.xml,附带一个action和触发action的菜单。
(2) 把它添加到openacademy/__manifest__.py
的data列表。
openacademy/__manifest__.py
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
],
# only loaded in demonstration mode
'demo': [
----------
openacademy/views/openacademy.xml:
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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>
</odoo>
基本视图
视图定义显示模型记录的方式。每种类型的视图表示一个可视化模式(一个记录列表,一个聚合的图形,…)。视图可以被要求通过他们的类型(例如类合作伙伴名单)或专门通过它们的ID的通用要求,具有正确的类型和优先级最低的视图将被使用(所以各类型优先级最低的视图是默认视图为型)。
视图继承允许改变在别处声明的视图(添加或删除内容)。
- 通用视图声明
视图被声明为ir.ui.view模型的记录,视图类型由arch字段的根元素说明:
<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>
危险
视图的内容是XML.因此,arch字段必须声明为type="xml"
,以便正确解析。
2.树视图
树视图,也被称为列表视图。以表单形式显示记录。
根元素是<tree>
,树视图的最简单形式只列出了表中显示的所有字段(每个字段列):
<tree string="Idea list">
<field name="name"/>
<field name="inventor_id"/>
</tree>
3.表单视图
表单被用于创建和编辑单个记录。
根元素是<form>
,它们由高层次结构元素(groups、notebooks)和交互元素(按钮和字段)组成:
<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>
练习
使用XML制作form视图
给Course 对象创建form视图,数据应该显示:Course 的name和description。
openacademy/views/openacademy.xml:
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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",
练习
Notebooks
在Course 表单视图里,将“description”字段置于选项卡之下,以便稍后添加其他选项卡,包含附加信息。
如下修改Course 表单视图:
openacademy/views/openacademy.xml
<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>
表单视图也可以使用纯HTML来进行更灵活的布局:
<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>
4.搜索视图
搜索视图自定义与列表视图关联的搜索字段(以及其他聚合视图)。它们的根元素是<search>
,它们由定义哪些字段可以被搜索的字段组成:
<search>
<field name="name"/>
<field name="inventor_id"/>
</search>
如果模型没有搜索视图,Odoo产生一个只允许用名称字段检索的搜索视图。
练习
搜索视图
允许根据他们的标题或描述来搜索课程。
openacademy/views/openacademy.xml
</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",
模型之间的关系
例如,销售订单记录与包含客户数据的客户机记录有关,它还与销售订单线记录有关。
练习
创建一个session 模型
对于Open Academy 模块,我们考虑一个session 模型:一个session 是一个给定的时间给定的观众在一个给定的时间发生的课程。
创建session 模型。session 具有名称、开始日期、持续时间和若干个座位。添加一个动作和菜单项来显示它们。通过菜单项使新模型可见。
1、在openacademy/models/models.py中创建session 类
2、在openacademy/view/openacademy.xml中给session 对象添加入口
openacademy/models.py
name = fields.Char(string="Title", required=True)
description = fields.Text()
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/views/openacademy.xml
<!-- 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>
</odoo>
注意
digits=(6, 2)指定浮点数的精度:6是数字的总数,而2是小数点后的位数。请注意,它的结果在小数点前是最大值4位数字。
1.关系字段
关系字段链接记录,包括相同的模型(层次结构)或不同模型之间的链接记录。
关系字段类型:
(1) Many2one(other_model, ondelete='set null')
到另一个对象的简单链接:
print foo.other_id.name
(2) One2many(other_model, related_field)
一个虚拟的关系,一个many2one的逆。一个one2many作为容器的记录,访问的结果是一个(可能为空)的记录集:
for other in foo.other_ids:
print other.name
危险
因为One2many 是一个虚拟的关系,在other_model里必须有一个Many2one字段,name必须是related_field 。
(3)Many2many(other_model)
双向多重关系,一方的任何记录都可以与另一方的任何记录有关。作为记录的容器,访问它也会导致一组可能空的记录:
for other in foo.other_ids:
print other.name
练习
Many2one 关系
使用Many2one ,修改Course 和Session 模型,反映他们与其他模型的关系:
· course有一个responsible 用户,该字段的值是内建模型res.users的一个记录。
· Session 有一个instructor,该字段的值是内建模型res.partner的一个记录。
· 一个session被连接到一个course,该字段的值是模型openacademy.course的一个记录,并且是必须的。
· 更新视图添加Many2one字段到模型里, 并将它们添加到视图中。
openacademy/models.py
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)
openacademy/views/openacademy.xml
<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>
练习
one2many 逆关系
使用one2many逆关系字段,修改模型,以反映course和session之间的关系
修改course类 ,在course表单视图添加该字段。
openacademy/models.py
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):
openacademy/views/openacademy.xml
<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>
练习
多重many2many 关系
利用关系字段many2many,修改session模型 ,将一组人与每一个课程关联。出席者将通过伙伴的记录作为代表,所以我们将与内置的模型res.partner关联。相应地调整视图。
修改session类,并添加到表单视图
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
<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>
继承
1.模型继承
Odoo提供两继承机制,以模块化的方式扩展现有的模型。
第一继承机制允许模块修改在其他模块中定义的模型的行为:
· 添加字段到模块
· 重写模型上字段的定义
· 向模型添加约束,
· 向模型添加方法,
· 在模型上重写现有方法。
第二继承机制(委托)允许将模型的每个记录与父模型中的记录连接起来,并提供对父记录字段的透明访问。
2.视图继承
与其修改现有的视图(通过重写他们),Odoo提供的视图继承让子“拓展”视图应用在顶部的根视图,并可以从父内容添加或删除。
拓展视图使用inherit_id字段参考它的父视图。而不是单一的,arch字段是由任意数量的xpath元素选择组成,并改变它们父视图的内容的视图:
<!-- 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
在父视图中选择单个元素的XPath表达式。如果不匹配任何元素或多于一个元素,则引发错误
· position
用于匹配元素的操作: