odoo创建一个模块,创建odoo模块,odoo创建模块,odoo二次开发
启动/停止Odoo服务器
Odoo使用客户端/服务器体系结构,其中客户端是通过RPC访问Odoo服务器的Web浏览器。
尽管可以将支持客户端功能(例如,新数据表示,例如交互式地图)添加到客户端,但是业务逻辑和扩展通常在服务器端执行。
为了启动服务器,只需在外壳中调用命令odoo-bin,并在必要时将完整路径添加到文件中:
0 |
odoo - Bin |
通过Ctrl-C
从终端单击两次或终止相应的OS进程来停止服务器。
构建一个Odoo模块
服务器扩展和客户端扩展都打包为模块,可以选择将其加载到数据库中。
Odoo模块可以向Odoo系统添加全新的业务逻辑,也可以更改和扩展现有的业务逻辑:可以创建一个模块以将您所在国家/地区的会计规则添加到Odoo的通用会计支持中,而下一个模块增加了对车队的实时可视化支持。
因此,Odoo中的所有内容都以模块开始和结束。
模块组成
Odoo模块可以包含许多元素:
- 业务对象
- 声明为Python类,Odoo会根据其配置自动保留这些资源
- 对象视图
- 定义业务对象的UI显示
- 资料档案
- 声明模型元数据的XML或CSV文件:
- Web控制器
- 处理来自网络浏览器的请求
- 静态网页数据
- 网络界面或网站使用的图像,CSS或javascript文件
模块结构
每个模块都是模块目录中的一个目录。使用--addons-path
选项指定模块目录。
大多数命令行选项也可以使用配置文件设置
Odoo模块由其清单声明。
模块也是带有文件的 Python包__init__.py
,其中包含模块中各种Python文件的导入说明。
例如,如果模块具有单个mymodule.py
文件,则 __init__.py
可能包含:
0 |
from . import mymodule |
Odoo提供了一种帮助建立新模块的机制,odoo-bin有一个子命令脚手架可以创建一个空模块:
0 |
$ odoo-bin scaffold <模块名称> <放置位置,一般为addons> |
该命令为您的模块创建一个子目录,并自动为模块创建一堆标准文件。它们中的大多数仅包含注释的代码或XML。本教程将说明大多数这些文件的用法。
行使
模块创建
使用上面的命令行创建一个空模块Open Academy,并将其安装在Odoo中。
- 调用命令
odoo-bin scaffold openacademy addons
。 - 使清单文件适应您的模块。
- 不要理会其他文件。
openacademy/__manifest__.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# -*- 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/14.0/odoo/addons/base/data/ir_module_category_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
0 1 2 |
# -*- coding: utf-8 -*- from . import controllers from . import models |
openacademy/controllers/controllers.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# -*- 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/demo.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<odoo> <!-- --> <!-- <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> --> <!-- --> </odoo> |
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# -*- coding: utf-8 -*- # from odoo import models, fields, api # class openacademy(models.Model): # _name = 'openacademy.openacademy' # _description = 'openacademy.openacademy' # name = fields.Char() # value = fields.Integer() # value2 = fields.Float(compute="_value_pc", store=True) # description = fields.Text() # # @api.depends('value') # def _value_pc(self): # for record in self: # record.value2 = float(record.value) / 100 |
openacademy/security/ir.model.access.csv
0 1 |
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,1,1,1 |
openacademy/views/templates.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<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> |
openacademy/views/views.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<odoo> <data> <!-- explicit list view definition --> <!-- <record model="ir.ui.view" id="openacademy.list"> <field name="name">openacademy list</field> <field name="model">openacademy.openacademy</field> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="value"/> <field name="value2"/> </tree> </field> </record> --> <!-- actions opening views on models --> <!-- <record model="ir.actions.act_window" id="openacademy.action_window"> <field name="name">openacademy window</field> <field name="res_model">openacademy.openacademy</field> <field name="view_mode">tree,form</field> </record> --> <!-- server action to the one above --> <!-- <record model="ir.actions.server" id="openacademy.action_server"> <field name="name">openacademy server</field> <field name="model_id" ref="model_openacademy_openacademy"/> <field name="state">code</field> <field name="code"> action = { "type": "ir.actions.act_window", "view_mode": "tree,form", "res_model": model._name, } </field> </record> --> <!-- Top menu item --> <!-- <menuitem name="openacademy" id="openacademy.menu_root"/> --> <!-- menu categories --> <!-- <menuitem name="Menu 1" id="openacademy.menu_1" parent="openacademy.menu_root"/> <menuitem name="Menu 2" id="openacademy.menu_2" parent="openacademy.menu_root"/> --> <!-- actions --> <!-- <menuitem name="List" id="openacademy.menu_1_list" parent="openacademy.menu_1" action="openacademy.action_window"/> <menuitem name="Server to list" id="openacademy" parent="openacademy.menu_2" action="openacademy.action_server"/> --> </data> </odoo> |
对象关系映射
Odoo的关键组件是ORM层。该层避免了必须手动编写大多数SQL的情况,并提供了可扩展性和安全性服务。(可以编写原始SQL查询,但需要注意,因为它绕过了所有Odoo身份验证和安全机制。)
将业务对象声明为Python类扩展 Model
,将其集成到自动化持久性系统中。
可以通过在定义时设置多个属性来配置模型。最重要的属性是_name,是
必需的, 它定义了Odoo系统中模型的名称。这是模型的最低限度的完整定义:
0 1 2 |
from odoo import models class MinimalModel(models.Model): _name = 'test.model' |
模型领域
字段用于定义模型可以存储的内容和位置。字段被定义为模型类的属性:
0 1 2 3 4 5 |
from odoo import models, fields class LessMinimalModel(models.Model): _name = 'test.model2' name = fields.Char() |
共同属性
与模型本身非常相似,可以通过将配置属性作为参数传递来配置其字段:
0 |
name = field.Char(required=True) |
一些属性在所有字段上都可用,这是最常见的属性:
string
(unicode
,默认值:字段名称)- UI中字段的标签(用户可见)。
required
(bool
,默认:False
)- 如果
True
,该字段不能为空,则它必须具有默认值或在创建记录时始终被赋予一个值。 help
(unicode
,默认:''
)- 长格式,为用户界面中的用户提供帮助工具提示。
index
(bool
,默认:False
)- 请求Odoo在列上创建数据库索引。
简单的字段
字段分为两大类:“简单”字段是直接存储在模型表中的原子值,而“关系”字段则链接(相同模型或不同模型的)记录。
保留栏位
Odoo在所有模型中创建了一些字段(可以禁用某些字段的自动创建)。这些字段由系统管理,不应该写入。如有需要或有用,可以阅读它们:
id
(Id
)- 其模型中记录的唯一标识符。
create_date
(Datetime
)- 记录的创建日期。
create_uid
(Many2one
)- 创建记录的用户。
write_date
(Datetime
)- 记录的最后修改日期。
write_uid
(Many2one
)- 最后修改记录的用户。
特殊字段
默认情况下,Odoo还要求name
所有模型上都有一个字段,用于显示和搜索行为。可以通过设置覆盖用于这些目的的字段_rec_name
。
行使
定义模型
在openacademy模块中定义一个新的数据模型Course。一门课程有标题和描述。课程必须有标题。
编辑文件openacademy/models/models.py
以包含课程类。
openacademy/models/models.py
0 1 2 3 4 5 6 7 |
from odoo import models, fields, api class Course(models.Model): _name = 'openacademy.course' _description = "OpenAcademy Courses" name = fields.Char(string="Title", required=True) description = fields.Text() |
资料档案
Odoo是一个高度数据驱动的系统。尽管行为是使用Python代码自定义的 ,但模块的部分值是在它加载时设置的数据中。
有些模块的存在只是为了添加数据到Odoo
通过数据文件(带有<record>
元素的XML文件 )声明模块数据。每个<record>
元素都会创建或更新数据库记录。
0 1 2 3 4 5 6 |
<odoo> <record model="{model name}" id="{record identifier}"> <field name="{a field name}">{a value}</field> </record> </odoo> |
model
是记录的Odoo模型的名称。id
是一个外部标识符,它允许引用记录(而不必知道其数据库内标识符)。<field>
元素具有一个name
,这是模型中字段的名称(例如description
)。他们的主体是字段值。
必须在清单文件(即模块模板文件)中声明要加载的数据文件,可以在'data'
列表中(始终加载)或在'demo'
列表中声明数据文件(仅在演示模式下加载)。
行使
定义演示数据
创建演示数据,并用一些演示课程填充“课程”模型。
编辑文件openacademy/demo/demo.xml
以包含一些数据。
openacademy/demo/demo.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<odoo> <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> </odoo> |
仅在安装或更新模块时才加载数据文件的内容。
进行一些更改后,请不要忘记使用 odoo-bin -u openacademy将更改保存到数据库中。
操作和菜单
操作和菜单是数据库中的常规记录,通常通过数据文件声明。可以通过三种方式触发动作:
- 通过单击菜单项(链接到特定操作)
- 通过单击视图中的按钮(如果这些按钮已连接到动作)
- 作为对对象的上下文动作
由于菜单的声明有些复杂,因此有一个<menuitem>
快捷方式来声明ir.ui.menu
,并将其更轻松地连接到相应的动作。
0 1 2 3 4 5 6 |
<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菜单项下的课程。用户应该能够:
- 显示所有课程的清单
- 创建/修改课程
openacademy/views/openacademy.xml
通过动作创建并触发该动作的菜单- 将它添加到
data
列表openacademy/__manifest__.py
openacademy/__manifest__.py
0 1 2 3 4 5 6 |
'data': [ # 'security/ir.model.access.csv', 'templates.xml', 'views/openacademy.xml', ], # only loaded in demonstration mode 'demo': [ |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?xml version="1.0" encoding="UTF-8"?> <odoo> <!-- 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_mode">tree,form</field> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face">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 --> </odoo> |
基本观点
视图定义了模型记录的显示方式。每种视图类型都代表一种可视化模式(记录列表,它们的聚合图等等)。可以通过视图的类型(例如,合作伙伴列表)来通用地请求视图,也可以通过其ID来请求视图。对于一般请求,将使用具有正确类型和最低优先级的视图(因此,每种类型的最低优先级视图是该类型的默认视图)。
视图继承允许更改在其他地方声明的视图(添加或删除内容)。
通用视图声明
视图被声明为模型的记录ir.ui.view
。视图类型由arch
字段的根元素隐含:
0 1 2 3 4 5 6 7 |
<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"
才能正确解析。
树视图
树视图(也称为列表视图)以表格形式显示记录。
他们的根本要素是<tree>
。树形视图的最简单形式只是列出了要在表中显示的所有字段(每个字段作为一列):
0 1 2 3 |
<tree string="Idea list"> <field name="name"/> <field name="inventor_id"/> </tree> |
form视图
表单用于创建和编辑单个记录。
他们的根本要素是<form>
。它们由高级结构元素(组-group,笔记本-notebook)和交互元素(按钮和字段-field)组成:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<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自定义表单视图
为课程对象创建自己的表单视图。显示的数据应为:课程的名称和说明。
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="UTF-8"?> <odoo> <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", |
行使
笔记本 – 标签<notebook>
在“课程表”视图中,将“说明”字段放在选项卡下,这样以后可以轻松添加包含其他信息的其他选项卡。
修改“课程”表单视图,如下所示:
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<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以获得更灵活的布局:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<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>
并且由定义可以在哪些字段上搜索的字段组成:
0 1 2 3 |
<search> <field name="name"/> <field name="inventor_id"/> </search> |
如果该模型不存在搜索视图,则Odoo会生成一个仅允许在name
字段中搜索的视图。
行使
搜索课程
允许根据标题或描述搜索课程。
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
</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", |
模型之间的关系
来自模型的记录可以与来自另一个模型的记录相关。例如,销售订单记录与包含客户数据的客户记录相关;它也与其销售订单项记录有关。
行使
创建会话模型
对于模块开放学院,我们考虑了模型的会话:会话是在给定时间教了给观众一个疗程的发生。
对于Open Academy模块(openacademy),我们考虑了一个会话模型。会话是发生在给定的时间为给定的听众讲授的课程的。
创建会话模型。会话具有名称,开始日期,持续时间和座位数。添加一个动作和一个菜单项以显示它们。通过菜单项使新模型可见。
- 在
openacademy/models/models.py中创建会话类
。 - 添加对
openacademy/view/openacademy.xml
中会话对象的访问权限。
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 |
name = fields.Char(string="Title", required=True) description = fields.Text() class Session(models.Model): _name = 'openacademy.session' _description = "OpenAcademy Sessions" 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
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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_mode">tree,form</field> </record> <menuitem id="session_menu" name="Sessions" parent="openacademy_menu" action="session_list_action"/> </odoo> |
digits=(6, 2)
指定浮点数的精度:6是数字的总数,而2是逗号后的数字。请注意,它导致逗号之前的数字位数最大为4
关系领域
关系字段链接相同模型(层次结构)或不同模型之间的记录。
关系字段类型为:
Many2one(other_model, ondelete='set null')
- 与其他对象的简单链接:
0print foo.other_id.name也可以看看
One2many(other_model, related_field)
- 虚关系,与
Many2one
(多对一)相反。One2many
充当记录的容器,访问它会导致一个(可能是空的)记录集:
01for other in foo.other_ids:print other.name危险
因为
One2many
是一个虚拟关系,所以在另一个模型中必须有一个Many2one
字段other_model
,其名称必须为related_field
Many2many(other_model)
- 双向多重关系,一侧的任何记录都可以与另一侧的任何数量的记录相关。表现为记录的容器,对其进行访问也会导致记录集可能为空:
01for other in foo.other_ids:print other.name
行使
一对多关系
使用many2one,修改“课程”和“会话”模型以反映它们与其他模型的关系:
- 课程有负责任的用户;该字段的值是内置模型的记录
res.users
。 - 一次会议有一位老师; 该字段的值是内置模型的记录
res.partner
。 - 会议与课程有关; 该字段的值是模型的记录,
openacademy.course
是必填项。 - 调整视图。
- 将相关
Many2one
字段添加到模型; - 在视图中添加它们。
openacademy/models/models.py
0 1 2 3 4 5 6 7 |
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' |
0 1 2 3 4 5 6 |
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
0 1 2 3 4 5 6 |
<sheet> <group> <field name="name"/> <field name="responsible_id"/> </group> <notebook> <page string="Description"> |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
</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", |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<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对模型进行修改,以反映课程与会话之间的关系。
- 修改
Course
类; - 在课程表视图中添加字段。
openacademy/models/models.py
0 1 2 3 4 5 |
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
0 1 2 3 4 5 6 7 8 9 10 11 12 |
<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> |
行使
多个多对多关系
使用多个关系字段,修改会话模型以将每个会话与一组参与者相关联。参加者将由合作伙伴记录代表,因此我们将涉及内置模型res.partner
。相应地调整视图。
使用关系字段many2many,修改会话模型,将每个会话与一组参与者关联起来。参会者将由合作伙伴记录表示,因此我们将关联到内置模型res.partner。相应地调整视图。
- 修改
Session
类; - 在表单视图中添加字段。
openacademy/models/models.py
0 1 2 3 |
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
0 1 2 3 4 5 6 7 |
<field name="seats"/> </group> </group> <label for="attendee_ids"/> <field name="attendee_ids"/> </sheet> </form> </field> |
遗产
模型继承
Odoo提供了两种继承机制,以模块化方式扩展现有模型。
第一个继承机制允许模块修改另一个模块中定义的模型的行为:
- 向模型添加字段,
- 覆盖模型上字段的定义,
- 向模型添加约束,
- 向模型添加方法
- 覆盖模型上的现有方法。
第二种继承机制(委托)允许将模型的每个记录链接到父模型中的记录,并提供对父记录的字段的透明访问。
也可以看看
_inherit
_inherits
查看继承
Odoo不需要修改现有的视图(通过覆盖它们),它提供了视图继承,在根视图之上应用子“扩展”视图,并且可以从父视图添加或删除内容。
扩展视图使用inherit_id
字段引用其父视图,而不是单个视图,它的arch
字段由任意数量的xpath
元素组成,这些 元素选择和更改其父视图的内容:
0 1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- 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
- 应用于匹配元素的操作:
inside
xpath
在匹配元素的末尾附加的主体replace
- 用
xpath
的主体替换匹配的元素,用$0
原始元素替换新主体中出现的任何节点 before
- 将
xpath
的主体作为兄弟元素插入到匹配的元素之前 after
- 将
xpath
的主体作为兄弟元素插入到匹配的元素之后 attributes
- 使用
xpath
主体中的特殊的attribute
元素修改匹配元素的属性
匹配单个元素时,position
可以直接在要查找的元素上设置属性。下面的两个继承将给出相同的结果。
0 1 2 3 4 5 6 |
<xpath expr="//field[@name='description']" position="after"> <field name="idea_ids" /> </xpath> <field name="description" position="after"> <field name="idea_ids" /> </field> |
行使
更改现有内容
- 使用模型继承,修改现有的Partner 模型以添加一个
instructor
布尔字段和一个多对多字段,该字段对应于会话-伙伴关系 - 使用视图继承,在伙伴表单视图中显示此字段
这是引入开发人员模式检查视图,找到其外部ID以及放置新字段的位置的机会。
- 创建一个文件
openacademy/models/partner.py
并将其导入__init__.py
- 创建一个文件
openacademy/views/partner.xml
并将其添加到__manifest__.py
openacademy/__init__.py
0 1 2 3 |
# -*- coding: utf-8 -*- from . import controllers from . import models from . import partner |
openacademy/__manifest__.py
0 1 2 3 4 5 6 |
# 'security/ir.model.access.csv', 'templates.xml', 'views/openacademy.xml', 'views/partner.xml', ], # only loaded in demonstration mode 'demo': [ |
openacademy/partner.py
0 1 2 3 4 5 6 7 8 9 10 |
# -*- coding: utf-8 -*- from odoo import fields, models class Partner(models.Model): _inherit = 'res.partner' # 向res.partner模型添加一个新列,默认情况下是伙伴不是讲师 instructor = fields.Boolean("Instructor", default=False) session_ids = fields.Many2many('openacademy.session', string="Attended Sessions", readonly=True) |
openacademy/views/partner.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?xml version="1.0" encoding="UTF-8"?> <odoo> <!-- Add instructor field to existing view --> <record model="ir.ui.view" id="partner_instructor_form_view"> <field name="name">partner.instructor</field> <field name="model">res.partner</field> <field name="inherit_id" ref="base.view_partner_form"/> <field name="arch" type="xml"> <notebook position="inside"> <page string="Sessions"> <group> <field name="instructor"/> <field name="session_ids"/> </group> </page> </notebook> </field> </record> <record model="ir.actions.act_window" id="contact_list_action"> <field name="name">Contacts</field> <field name="res_model">res.partner</field> <field name="view_mode">tree,form</field> </record> <menuitem id="configuration_menu" name="Configuration" parent="main_openacademy_menu"/> <menuitem id="contact_menu" name="Contacts" parent="configuration_menu" action="contact_list_action"/> </odoo> |
域
在Odoo中,搜索域是对记录条件进行编码的值。域是用于选择模型记录子集的条件列表。每个条件都是一个三元组,其中包含一个字段名称,一个运算符和一个值。
例如,当在产品模型上使用时,以下域会选择单价超过1000的所有服务:
0 |
[('product_type', '=', 'service'), ('unit_price', '>', 1000)] |
默认情况下,标准与隐式AND组合。逻辑运算符 &
(AND),|
(OR)和!
(NOT)可用于显式组合条件。它们在前缀位置使用(运算符插入在其参数之前而不是参数之间)。例如,选择“服务或单价不在1000到2000之间的产品”:
0 1 2 3 4 |
['|', ('product_type', '=', 'service'), '!', '&', ('unit_price', '>=', 1000), ('unit_price', '<', 2000)] |
一个domain
参数可以被添加到相关的领域,限制有效记录试图选择在客户端界面记录时的关系。
行使
关系字段上的域
当为Session选择讲师时,只有讲师(讲师设置为True
的合作伙伴)应该是可见的。
openacademy/models/models.py
0 1 2 3 4 5 6 7 |
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", domain=[('instructor', '=', True)]) course_id = fields.Many2one('openacademy.course', ondelete='cascade', string="Course", required=True) attendee_ids = fields.Many2many('res.partner', string="Attendees") |
声明为文本列表的域在服务器端计算,不能引用右侧的动态值。在客户端计算声明为字符串的域,并允许在右侧计算字段名称。
行使
更复杂的域
创建新的合作伙伴类别教师/一级和教师/二级。会话的讲师可以是任何级别的讲师或老师。
- 修改会话模型的域
- 进行修改
openacademy/view/partner.xml
以访问 合作伙伴类别:
openacademy/models/models.py
0 1 2 3 4 5 6 7 |
seats = fields.Integer(string="Number of seats") 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
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
parent="configuration_menu" action="contact_list_action"/> <record model="ir.actions.act_window" id="contact_cat_list_action"> <field name="name">Contact Tags</field> <field name="res_model">res.partner.category</field> <field name="view_mode">tree,form</field> </record> <menuitem id="contact_cat_menu" name="Contact Tags" parent="configuration_menu" action="contact_cat_list_action"/> <record model="res.partner.category" id="teacher1"> <field name="name">Teacher / Level 1</field> </record> <record model="res.partner.category" id="teacher2"> <field name="name">Teacher / Level 2</field> </record> </odoo> |
计算字段和默认值
到目前为止,字段已直接存储在数据库中或直接从数据库中检索。也可以计算字段。在这种情况下,不会从数据库中检索字段的值,而是通过调用模型的方法即时计算出该值。
要创建计算字段,请创建字段并将其属性compute
设置为方法名。计算方法应该简单地在self
中的每个记录上设置要计算的字段值。
危险
self
是一个集合
对象self
是一个记录集,即记录的有序集合。它支持集合上的标准Python操作(例如 len(self)
和iter(self)
)以及额外的集合操作(例如)recs1 + recs2
。
在self
上迭代会一个接一个地给出记录,其中每个记录本身就是一个大小为1的集合。您可以使用点符号访问/分配单个记录上的字段,比如record.name
。
0 1 2 3 4 5 6 7 8 9 10 |
import random from odoo import models, fields, api class ComputedModel(models.Model): _name = 'test.computed' name = fields.Char(compute='_compute_name') def _compute_name(self): for record in self: record.name = str(random.randint(1, 1e6)) |
依存关系
计算字段的值通常取决于计算记录上的其他字段的值。ORM希望开发人员使用装饰器depends()
指定对计算方法的依赖关系。ORM使用给定的依赖项来触发字段的重新计算,每当它的一些依赖项被修改时:
0 1 2 3 4 5 6 7 8 9 10 11 |
from odoo import models, fields, api class ComputedModel(models.Model): _name = 'test.computed' name = fields.Char(compute='_compute_name') value = fields.Integer() @api.depends('value') def _compute_name(self): for record in self: record.name = "Record with value %s" % record.value |
行使
计算字段
- 将占用席位的百分比添加到会话模型中
- 在树视图和表单视图中显示该字段
- 将该字段显示为进度条
- 向会话添加计算字段
- 在“会话”视图中显示该字段:
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 11 12 |
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="Taken seats", compute='_taken_seats') @api.depends('seats', 'attendee_ids') def _taken_seats(self): for r in self: if not r.seats: r.taken_seats = 0.0 else: r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 |
<field name="start_date"/> <field name="duration"/> <field name="seats"/> <field name="taken_seats" widget="progressbar"/> </group> </group> <label for="attendee_ids"/> |
0 1 2 3 4 5 6 |
<tree string="Session Tree"> <field name="name"/> <field name="course_id"/> <field name="taken_seats" widget="progressbar"/> </tree> </field> </record> |
默认值
可以给任何字段一个默认值。在字段定义中,添加选项default=X
,其中X要么是一个Python文字值(布尔值、整数、浮点值、字符串),要么是一个获取记录集并返回值的函数:
0 1 |
name = fields.Char(default="Unknown") user_id = fields.Many2one('res.users', default=lambda self: self.env.user) |
该对象self.env
可以访问请求参数和其他有用的东西:
self.env.cr
还是self._cr
数据库游标对象;或者 它用于查询数据库self.env.uid
还是self._uid
当前用户的数据库IDself.env.user
是当前用户的记录self.env.context
还是self._context
上下文字典self.env.ref(xml_id)
返回与XML id对应的记录self.env[model_name]
返回给定模型的实例
行使
活动对象–默认值
- 将start_date默认值定义为今天(请参阅参考资料
Date
)。 active
在“会话”类中添加一个字段,并将会话默认设置为活动状态。
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 |
_description = "OpenAcademy Sessions" name = fields.Char(required=True) start_date = fields.Date(default=fields.Date.today) duration = fields.Float(digits=(6, 2), help="Duration in days") seats = fields.Integer(string="Number of seats") active = fields.Boolean(default=True) instructor_id = fields.Many2one('res.partner', string="Instructor", domain=['|', ('instructor', '=', True), |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 |
<field name="course_id"/> <field name="name"/> <field name="instructor_id"/> <field name="active"/> </group> <group string="Schedule"> <field name="start_date"/> |
Odoo具有内置规则,可以将active
字段设置为False
不可见来进行记录。
不断变化
“ onchange”机制为客户端界面提供了一种方法,只要用户在字段中填写了值,便无需用户将任何内容保存到数据库即可更新表单。
例如,假设一个模型有三个字段amount
,unit_price
和 price
,并且您想在修改任何其他字段时更新表单上的价格。为此,请定义一个方法,该方法在self
表单视图中表示记录,并用其修饰onchange()
以指定必须在哪个字段上触发记录。您所做的任何更改 self
都会反映在表格上。
0 1 2 3 |
<!-- content of form view --> <field name="amount"/> <field name="unit_price"/> <field name="price" readonly="1"/> |
0 1 2 3 4 5 6 7 8 9 10 11 |
# onchange handler @api.onchange('amount', 'unit_price') def _onchange_price(self): # set auto-changing field self.price = self.amount * self.unit_price # Can optionally return a warning and domains return { 'warning': { 'title': "Something bad happened", 'message': "It was very bad indeed", } } |
对于计算字段,onchange
可以通过使用“会话”表单来看到有价值的行为:更改席位或参与者的数量,并taken_seats
会自动更新进度栏。
行使
警告
添加一个明确的onchange来警告无效值,例如席位数为负数,或参与者多于席位。openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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): if self.seats < 0: return { 'warning': { 'title': "Incorrect 'seats' value", 'message': "The number of available seats may not be negative", }, } if self.seats < len(self.attendee_ids): return { 'warning': { 'title': "Too many attendees", 'message': "Increase seats or remove excess attendees", }, } |
模型约束
Odoo提供了两种方法来设置自动验证的不变量: Python constraints
和 SQL constraints
。
Python约束被定义为修饰了constrains()
的方法,并在记录集上调用。装饰器指定约束中涉及哪些字段,以便在修改其中一个字段时自动计算约束。如果不满足其不变性,则该方法将会抛出异常:
0 1 2 3 4 5 6 7 |
from odoo.exceptions import ValidationError @api.constrains('age') def _check_something(self): for record in self: if record.age > 20: raise ValidationError("Your record is too old: %s" % record.age) # all records passed the test, don't return anything |
行使
添加Python约束
添加一个约束来检查讲师是否出现在他/她自己会议的与会者中。
openacademy/models/models.py
0 1 2 3 4 5 |
# -*- coding: utf-8 -*- from odoo import models, fields, api, exceptions class Course(models.Model): _name = 'openacademy.course' |
0 1 2 3 4 5 6 7 8 |
'message': "Increase seats or remove excess attendees", }, } @api.constrains('instructor_id', 'attendee_ids') def _check_instructor_not_in_attendees(self): for r in self: if r.instructor_id and r.instructor_id in r.attendee_ids: raise exceptions.ValidationError("A session's instructor can't be an attendee") |
SQL约束是通过模型属性_sql_constraints
定义的。后者被分配给一个字符串的三组列表(name
、sql_definition
、message
),其中name
是一个有效的SQL约束名,sql_definition
是一个table_constraint表达式,并且message
是错误消息。
行使
添加SQL约束
在PostgreSQL的文档的帮助下,添加以下约束:
- 检查课程说明和课程标题是否不同
- 使课程名称唯一
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
session_ids = fields.One2many( 'openacademy.session', 'course_id', string="Sessions") _sql_constraints = [ ('name_description_check', 'CHECK(name != description)', "The title of the course should not be the description"), ('name_unique', 'UNIQUE(name)', "The course title must be unique"), ] class Session(models.Model): _name = 'openacademy.session' |
行使
练习6-添加重复选项
由于我们为课程名称的唯一性添加了一个约束,因此无法再使用“重复”功能(表格‣重复)。
重新实现自己的“copy”方法,该方法可以复制“Course”对象,将原始名称更改为“ [原始名称]的副本”。openacademy/models/models.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
session_ids = fields.One2many( 'openacademy.session', 'course_id', string="Sessions") def copy(self, default=None): default = dict(default or {}) copied_count = self.search_count( [('name', '=like', u"Copy of {}%".format(self.name))]) if not copied_count: new_name = u"Copy of {}".format(self.name) else: new_name = u"Copy of {} ({})".format(self.name, copied_count) default['name'] = new_name return super(Course, self).copy(default) _sql_constraints = [ ('name_description_check', 'CHECK(name != description)', |
进阶检视
树视图
树视图可以采用补充属性来进一步自定义其行为:
decoration-{$name}
- 允许根据相应记录的属性更改行文本的样式。
值是Python表达式。对于每条记录,将使用记录的属性作为上下文值评估表达式,如果值为
true
,则将相应样式应用于行。以下是上下文中可用的其他一些值:uid
:当前用户的ID,today
:当前本地日期,形式为字符串YYYY-MM-DD
,now
:与today
当前时间的增加相同。该值的格式为YYYY-MM-DD hh:mm:ss
。
{$name}
可以是bf
(font-weight: bold
), (it
),font-style: italic
或任何自举上下文颜色(danger
,info
,muted
,primary
,success
或warning
)。01234<tree string="Idea Categories" decoration-info="state=='draft'"decoration-danger="state=='trashed'"><field name="name"/><field name="state"/></tree> editable
- 无论是
"top"
或"bottom"
。使树视图可就地编辑(而不是必须通过表单视图),该值是新行出现的位置。
行使
列表着色
修改会话树视图,使持续时间少于5天的会话显示为蓝色,持续时间超过15天的会话显示为红色。
修改会话树视图:
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 |
<field name="name">session.tree</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <tree string="Session Tree" decoration-info="duration<5" decoration-danger="duration>15"> <field name="name"/> <field name="course_id"/> <field name="duration" invisible="1"/> <field name="taken_seats" widget="progressbar"/> </tree> </field> |
行事历
将记录显示为日历事件。它们的根元素是<calendar>
,最常见的属性是:
color
- 用于颜色分割的字段名称。颜色会自动分配给事件,但相同颜色段(其
@color
字段具有相同值的记录)中的事件将被赋予相同的颜色。 date_start
- 记录中包含事件开始日期/时间的字段
date_stop
(可选的)- 记录的字段,其中包含事件的结束日期/时间
string
- 记录的字段以定义每个日历事件的标签
0 1 2 |
<calendar string="Ideas" date_start="invent_date" color="inventor_id"> <field name="name"/> </calendar> |
行使
日历视图
将日历视图添加到会话模型,使用户可以查看与Open Academy相关的事件。
- 添加从
start_date
和duration
计算的end_date
字段。
逆函数使字段可写,并允许在日历视图中移动会话(通过拖放)。 - 将日历视图添加到会话模型
- 并将日历视图添加到会话模型的操作中
openacademy/models/models.py
0 1 2 3 4 5 |
# -*- coding: utf-8 -*- from datetime import timedelta from odoo import models, fields, api, exceptions class Course(models.Model): |
0 1 2 3 4 5 6 7 |
attendee_ids = fields.Many2many('res.partner', string="Attendees") taken_seats = fields.Float(string="Taken seats", compute='_taken_seats') end_date = fields.Date(string="End Date", store=True, compute='_get_end_date', inverse='_set_end_date') @api.depends('seats', 'attendee_ids') def _taken_seats(self): |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
}, } @api.depends('start_date', 'duration') def _get_end_date(self): for r in self: if not (r.start_date and r.duration): r.end_date = r.start_date continue # Add duration to start_date, but: Monday + 5 days = Saturday, so # subtract one second to get on Friday instead duration = timedelta(days=r.duration, seconds=-1) r.end_date = r.start_date + duration def _set_end_date(self): for r in self: if not (r.start_date and r.end_date): continue # Compute the difference between dates, but: Friday - Monday = 4 days, # so add one day to get 5 days instead r.duration = (r.end_date - r.start_date).days + 1 @api.constrains('instructor_id', 'attendee_ids') def _check_instructor_not_in_attendees(self): for r in self: |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
</field> </record> <!-- calendar view --> <record model="ir.ui.view" id="session_calendar_view"> <field name="name">session.calendar</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <calendar string="Session Calendar" date_start="start_date" date_stop="end_date" color="instructor_id"> <field name="name"/> </calendar> </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_mode">tree,form,calendar</field> </record> <menuitem id="session_menu" name="Sessions" |
搜索视图
搜索视图<field>
元素可以具有@filter_domain
,以覆盖为在给定字段上进行搜索而生成的域。在给定的域中, self
代表用户输入的值。在下面的例子中,它是用来在两个字段进行搜索name
和description
。
搜索视图还可以包含<filter>
元素,用作预定义搜索的切换。筛选器必须具有以下属性之一:
domain
- 将给定的域添加到当前搜索中
context
- 在当前搜索中添加一些上下文;使用键
group_by
将结果按给定的字段名称分组
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
<search string="Ideas"> <field name="name"/> <field name="description" string="Name and description" filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/> <field name="inventor_id"/> <field name="country_id" widget="selection"/> <filter name="my_ideas" string="My Ideas" domain="[('inventor_id', '=', uid)]"/> <group string="Group By"> <filter name="group_by_inventor" string="Inventor" context="{'group_by': 'inventor_id'}"/> </group> </search> |
要在操作中使用非默认搜索视图,应使用search_view_id
操作记录的字段将其链接 。
该操作还可以通过context
字段设置搜索字段的默认值:search_default_field_name
表单的上下文键将使用提供的值初始化field_name。搜索过滤器必须有一个可选的@name
才能有一个默认值,并表现为布尔值(它们只能在默认情况下启用)。
行使
搜索视图
- 添加一个按钮以在课程搜索视图中过滤当前用户负责的课程。使其默认为选中状态。
- 添加按钮以按负责任的用户对课程进行分组。
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 |
<search> <field name="name"/> <field name="description"/> <filter name="my_courses" string="My Courses" domain="[('responsible_id', '=', uid)]"/> <group string="Group By"> <filter name="by_responsible" string="Responsible" context="{'group_by': 'responsible_id'}"/> </group> </search> </field> </record> |
0 1 2 3 4 5 6 |
<field name="name">Courses</field> <field name="res_model">openacademy.course</field> <field name="view_mode">tree,form</field> <field name="context" eval="{'search_default_my_courses': 1}"/> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face">Create the first course </p> |
甘特
警告
甘特图视图需要企业版版本中存在的web_gantt模块 。
水平条形图通常用于显示项目计划和进度,其根元素为<gantt>
。
0 1 2 3 4 |
<gantt string="Ideas" date_start="invent_date" date_stop="date_finished" progress="progress" default_group_by="inventor_id" /> |
行使
甘特图
添加一个甘特图,使用户能够查看链接到Open Academy模块的会话计划。这些课程应由讲师分组。
- 添加甘特图视图的定义,并将甘特图视图添加到 会话模型的操作中
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
</field> </record> <record model="ir.ui.view" id="session_gantt_view"> <field name="name">session.gantt</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <gantt string="Session Gantt" date_start="start_date" date_stop="end_date" default_group_by='instructor_id'> </gantt> </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_mode">tree,form,calendar,gantt</field> </record> <menuitem id="session_menu" name="Sessions" |
图形视图
图形视图允许对模型进行汇总概述和分析,其根元素为<graph>
。
透视<pivot>
表(元素)是一个多维表,允许选择文件管理器和维度,以获取正确的聚合数据集,然后再移至更具图形化的概览。数据透视图与图视图共享相同的内容定义。
图形视图具有4种显示模式,使用@type
属性可以选择默认模式 。
- 条形(默认)
- 在条形图中,第一维用于定义水平轴上的组,其他维用于定义每个组内的聚合条。
默认情况下,并排显示的条形图可以通过
@stacked="True"
在<graph>
- 线
- 二维折线图
- 馅饼
- 二维派
图形视图包含<field>
带有强制@type
属性的值:
row
(默认)- 该字段应默认汇总
measure
- 该字段应汇总而不是分组
0 1 2 3 |
<graph string="Total idea score by Inventor"> <field name="inventor_id"/> <field name="score" type="measure"/> </graph> |
警告
图形视图对数据库值执行聚合,但不适用于非存储的计算字段。
行使
图形视图
在“会话”对象中添加一个“图形”视图,该视图以条形图的形式显示每个课程的参加人数。
- 将与会者人数添加为存储的计算字段
- 然后添加相关视图
openacademy/models/models.py
0 1 2 3 4 5 6 7 8 |
end_date = fields.Date(string="End Date", store=True, compute='_get_end_date', inverse='_set_end_date') attendees_count = fields.Integer( string="Attendees count", compute='_get_attendees_count', store=True) @api.depends('seats', 'attendee_ids') def _taken_seats(self): for r in self: |
0 1 2 3 4 5 6 7 8 9 10 |
# so add one day to get 5 days instead r.duration = (r.end_date - r.start_date).days + 1 @api.depends('attendee_ids') def _get_attendees_count(self): for r in self: r.attendees_count = len(r.attendee_ids) @api.constrains('instructor_id', 'attendee_ids') def _check_instructor_not_in_attendees(self): for r in self: |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
</field> </record> <record model="ir.ui.view" id="openacademy_session_graph_view"> <field name="name">openacademy.session.graph</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <graph string="Participations by Courses"> <field name="course_id"/> <field name="attendees_count" type="measure"/> </graph> </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_mode">tree,form,calendar,gantt,graph</field> </record> <menuitem id="session_menu" name="Sessions" |
看板
用于组织任务,生产过程等……其根本要素是 <kanban>
。
看板视图显示了一组可能按列分组的卡片。每张卡代表一条记录,每列代表一个聚合字段的值。
例如,项目任务可以按阶段(每个列是一个阶段)或负责任的方式(每个列是用户)进行组织,等等。
看板视图将每张卡的结构定义为表单元素(包括基本HTML)和QWeb的混合。
行使
看板视图
添加一个看板视图,该视图显示按课程分组的会话(因此,列就是课程)。
- 将一个整数
color
字段添加到会话模型 - 添加看板视图并更新操作
openacademy/models/models.py
0 1 2 3 4 5 6 |
duration = fields.Float(digits=(6, 2), help="Duration in days") seats = fields.Integer(string="Number of seats") active = fields.Boolean(default=True) color = fields.Integer() instructor_id = fields.Many2one('res.partner', string="Instructor", domain=['|', ('instructor', '=', True), |
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
</field> </record> <record model="ir.ui.view" id="view_openacad_session_kanban"> <field name="name">openacademy.session.kanban</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <kanban default_group_by="course_id"> <field name="color"/> <templates> <t t-name="kanban-box"> <div t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}} oe_kanban_global_click_edit oe_semantic_html_override oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}"> <div class="oe_dropdown_kanban"> <!-- dropdown menu --> <div class="oe_dropdown_toggle"> <i class="fa fa-bars fa-lg" title="Manage" aria-label="Manage"/> <ul class="oe_dropdown_menu"> <li> <a type="delete">Delete</a> </li> <li> <ul class="oe_kanban_colorpicker" data-field="color"/> </li> </ul> </div> <div class="oe_clear"></div> </div> <div t-attf-class="oe_kanban_content"> <!-- title --> Session name: <field name="name"/> <br/> Start date: <field name="start_date"/> <br/> duration: <field name="duration"/> </div> </div> </t> </templates> </kanban> </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_mode">tree,form,calendar,gantt,graph,kanban</field> </record> <menuitem id="session_menu" name="Sessions" |
安全
必须配置访问控制机制以实现一致的安全策略。
基于组的访问控制机制
在模型上将组创建为常规记录res.groups
,并通过菜单定义授予菜单访问权限。但是,即使没有菜单,也仍然可以间接访问对象,因此必须为组定义实际的对象级别权限(读取,写入,创建,取消链接)。它们通常是通过CSV文件插入模块内部的。也可以使用字段的groups属性来限制对视图或对象上特定字段的访问。
访问权限
访问权限定义为模型的记录ir.model.access
。每个访问权限都与一个模型、一个组(或对于全局访问没有一个组)和一组权限相关联:读取,写入,创建,取消链接。此类访问权限通常由以其型号命名的CSV文件创建 ir.model.access.csv
。
0 1 2 |
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 |
行使
通过Odoo界面添加访问控制
创建一个新用户“ John Smith”。然后创建一个对会话模型具有读取访问权限的组“ OpenAcademy /会话读取” 。
- 通过 设置‣用户‣用户,创建新用户John Smith
session_read
通过 “设置”‣“用户”‣“组”创建一个新组,它应该对“会话”模型具有读取权限- 编辑约翰·史密斯(John Smith)以使他们成为
session_read
- 以John Smith身份登录以检查访问权限是否正确
行使
通过模块中的数据文件添加访问控制
使用数据文件
- 创建一个具有对所有OpenAcademy模型的完全访问权限的组OpenAcademy/Manager
- 使会话和课程对所有用户可读
- 创建一个新文件
openacademy/security/security.xml
来保存OpenAcademy Manager组 - 编辑
openacademy/security/ir.model.access.csv
具有模型访问权限的文件 - 最后更新
openacademy/__manifest__.py
以向其中添加新的数据文件
openacademy/__manifest__.py
0 1 2 3 4 5 6 |
# always loaded 'data': [ 'security/security.xml', 'security/ir.model.access.csv', 'templates.xml', 'views/openacademy.xml', 'views/partner.xml', |
openacademy/security/ir.model.access.csv
0 1 2 3 4 |
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1 session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1 course_read_all,course all,model_openacademy_course,,1,0,0,0 session_read_all,session all,model_openacademy_session,,1,0,0,0 |
openacademy/security/security.xml
0 1 2 3 4 5 6 |
<odoo> <record id="group_manager" model="res.groups"> <field name="name">OpenAcademy / Manager</field> </record> </odoo> |
记录规则
记录规则将访问权限限制为给定模型的记录的子集。规则是模型的记录ir.rule
,并且与模型,多个组(many2many字段),应用限制的权限以及域相关联。域指定访问权限限制到哪些记录。
这是一个规则示例,可防止删除状态不佳的潜在客户cancel
。请注意,该字段的值groups
必须遵循与write()
ORM方法相同的约定。
0 1 2 3 4 5 6 7 8 9 |
<record id="delete_cancelled_only" model="ir.rule"> <field name="name">Only cancelled leads may be deleted</field> <field name="model_id" ref="crm.model_crm_lead"/> <field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/> <field name="perm_read" eval="0"/> <field name="perm_write" eval="0"/> <field name="perm_create" eval="0"/> <field name="perm_unlink" eval="1" /> <field name="domain_force">[('state','=','cancel')]</field> </record> |
行使
记录规则
为模型课程和“OpenAcademy / Manager”组添加一个记录规则,限制对课程负责人的写(write
)和断开访问(unlink
)。如果一个课程没有负责人,组的所有用户必须能够修改它。
在中创建新规则openacademy/security/security.xml
:
openacademy/security/security.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<record id="group_manager" model="res.groups"> <field name="name">OpenAcademy / Manager</field> </record> <record id="only_responsible_can_modify" model="ir.rule"> <field name="name">Only Responsible can modify Course</field> <field name="model_id" ref="model_openacademy_course"/> <field name="groups" eval="[(4, ref('openacademy.group_manager'))]"/> <field name="perm_read" eval="0"/> <field name="perm_write" eval="1"/> <field name="perm_create" eval="0"/> <field name="perm_unlink" eval="1"/> <field name="domain_force"> ['|', ('responsible_id','=',False), ('responsible_id','=',user.id)] </field> </record> </odoo> |
奇才队
向导通过动态表单描述与用户(或对话框)的交互会话。向导只是一个模型,它扩展了类TransientModel
而不是Model
。类TransientModel
扩展了Model
并重用了它所有现有的机制,有以下特点:
- 向导记录并非永久性的;一定时间后,它们会自动从数据库中删除。这就是为什么它们被称为 瞬态的原因。
- 向导记录可以通过关系字段(many2one或many2many)引用常规记录或向导记录,但是常规记录不能通过many2one字段引用向导记录。
我们想要创建一个向导,该向导允许用户为特定会话或一次会话列表创建与会者。
行使
定义向导
创建一个向导模型,该模型与会话 模型具有多对一关系,与伙伴模型具有多对多关系。
添加一个新文件openacademy/wizard.py
:openacademy/__init__.py
0 1 2 3 |
from . import controllers from . import models from . import partner from . import wizard |
openacademy/wizard.py
0 1 2 3 4 5 6 7 8 9 10 |
# -*- coding: utf-8 -*- from odoo import models, fields, api class Wizard(models.TransientModel): _name = 'openacademy.wizard' _description = "Wizard: Quick Registration of Attendees to Sessions" session_id = fields.Many2one('openacademy.session', string="Session", required=True) attendee_ids = fields.Many2many('res.partner', string="Attendees") |
openacademy/security/ir.model.access.csv
0 1 2 3 4 5 |
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1 session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1 wizard_manager,wizard session manager,model_openacademy_wizard,group_manager,1,1,1,1 course_read_all,course all,model_openacademy_course,,1,0,0,0 session_read_all,session all,model_openacademy_session,,1,0,0,0 |
启动向导
向导只是将目标字段设置为新值的窗口操作,它将在单独的对话框中打开视图(通常是form)。该动作可以通过菜单项触发,但通常是通过一个按钮触发。
启动向导的另一种方法是通过树或窗体视图的“操作”菜单。这是通过binding_model_id
操作字段完成的。设置此字段将使该动作出现在该动作“绑定”到的模型的视图上。
0 1 2 3 4 5 6 |
<record id="launch_the_wizard" model="ir.actions.act_window"> <field name="name">Launch the Wizard</field> <field name="model">wizard.model.name</field> <field name="view_mode">form</field> <field name="target">new<field> <field name="binding_model_id" ref="model_context_model_ref"/> </record> |
向导使用常规视图和按钮时,通常单击表单中的任何按钮会先保存表单,然后关闭对话框。因为这在向导中通常是不可取的,所以可以使用特殊属性,该属性special="cancel"
立即关闭向导而不保存表单。
行使
启动向导
- 定义向导的表单视图。
- 添加操作以在会话模型的上下文中启动它。
- 在向导中为会话字段定义默认值;使用context参数
self._context
检索当前会话。
openacademy/wizard.py
0 1 2 3 4 5 6 7 8 |
_name = 'openacademy.wizard' _description = "Wizard: Quick Registration of Attendees to Sessions" def _default_session(self): return self.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
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
parent="openacademy_menu" action="session_list_action"/> <record model="ir.ui.view" id="wizard_form_view"> <field name="name">wizard.form</field> <field name="model">openacademy.wizard</field> <field name="arch" type="xml"> <form string="Add Attendees"> <group> <field name="session_id"/> <field name="attendee_ids"/> </group> </form> </field> </record> <record id="launch_session_wizard" model="ir.actions.act_window"> <field name="name">Add Attendees</field> <field name="res_model">openacademy.wizard</field> <field name="view_mode">form</field> <field name="target">new</field> <field name="binding_model_id" ref="model_openacademy_session"/> </record> </odoo> |
行使
注册参加者
在向导中添加按钮,并实现将与会者添加到给定会话的相应方法。
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 7 8 9 10 11 |
<field name="session_id"/> <field name="attendee_ids"/> </group> <footer> <button name="subscribe" type="object" string="Subscribe" class="oe_highlight"/> or <button special="cancel" string="Cancel"/> </footer> </form> </field> </record> |
openacademy/wizard.py
0 1 2 3 4 5 6 |
session_id = fields.Many2one('openacademy.session', string="Session", required=True, default=_default_session) attendee_ids = fields.Many2many('res.partner', string="Attendees") def subscribe(self): self.session_id.attendee_ids |= self.attendee_ids return {} |
行使
注册参加多个会议的与会者
修改向导模型,以便可以将与会者注册到多个会话。
openacademy/views/openacademy.xml
0 1 2 3 4 5 6 |
<field name="arch" type="xml"> <form string="Add Attendees"> <group> <field name="session_ids"/> <field name="attendee_ids"/> </group> <footer> |
openacademy/wizard.py
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
_name = 'openacademy.wizard' _description = "Wizard: Quick Registration of Attendees to Sessions" def _default_sessions(self): return self.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") def subscribe(self): for session in self.session_ids: session.attendee_ids |= self.attendee_ids return {} |
国际化
通过使用名为LANG.po的文件,每个模块都可以在i18n目录中提供自己的翻译,其中LANG是语言的语言环境代码,或语言和国家/地区组合(如果它们不同)(例如pt.po或pt_BR.po)。Odoo会自动为所有启用的语言加载翻译。开发人员在创建模块时始终使用英语,然后使用Odoo的gettext POT导出功能(设置‣翻译‣导入/导出‣导出翻译而不指定语言)导出模块术语,以创建模块模板POT文件,然后导出已翻译的PO文件。许多IDE都有用于编辑和合并PO/POT文件的插件或模式。
由Odoo生成的Portable Object文件在Transifex上发布 ,使翻译软件变得容易。
0 1 2 3 4 5 |
|-idea /#模块目录 |-i18n /#翻译文件 | -idea.pot#翻译模板(从Odoo导出) | -fr.po#法语翻译 | -pt_BR.po#巴西葡萄牙语翻译 | (...) |
默认情况下,Odoo的POT导出仅提取XML文件内的标签或Python代码中的字段定义内的标签,但是任何Python字符串都可以通过将其包围在函数中来进行翻译odoo._()
(例如_("Label")
)
行使
翻译模块
为您的Odoo安装选择第二种语言。使用Odoo提供的功能来翻译您的模块。
- 建立目录
openacademy/i18n/
- 您需要激活开发人员模式才能访问下面提到的菜单( 设置‣激活开发人员模式 )
- 安装所需的任何语言( 设置‣翻译‣语言)
- 生成缺少的术语(设置‣翻译‣应用程序术语‣生成缺少的术语)
- 通过导出(设置‣翻译‣导入/导出‣导出翻译)来创建模板翻译文件, 而无需指定语言,保存在
openacademy/i18n/
- 通过导出(设置‣翻译‣导入/导出‣导出翻译)并指定语言来创建翻译文件 。保存在
openacademy/i18n/
- 打开导出的翻译文件(使用基本文本编辑器或专用的PO文件编辑器(例如POEdit)并翻译缺少的术语
- 在
models.py
中,为函数添加导入语句,odoo._
并将缺少的字符串标记为可翻译 - 重复步骤3-6
openacademy/models.py
0 1 2 3 4 5 6 |
# -*- coding: utf-8 -*- from datetime import timedelta from odoo import models, fields, api, exceptions, _ class Course(models.Model): _name = 'openacademy.course' |
0 1 2 3 4 5 6 7 8 9 10 |
default = dict(default or {}) copied_count = self.search_count( [('name', '=like', _(u"Copy of {}%").format(self.name))]) if not copied_count: new_name = _(u"Copy of {}").format(self.name) else: new_name = _(u"Copy of {} ({})").format(self.name, copied_count) default['name'] = new_name return super(Course, self).copy(default) |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
if self.seats < 0: return { 'warning': { 'title': _("Incorrect 'seats' value"), 'message': _("The number of available seats may not be negative"), }, } if self.seats < len(self.attendee_ids): return { 'warning': { 'title': _("Too many attendees"), 'message': _("Increase seats or remove excess attendees"), }, } |
0 1 2 3 |
def _check_instructor_not_in_attendees(self): for r in self: if r.instructor_id and r.instructor_id in r.attendee_ids: raise exceptions.ValidationError(_("A session's instructor can't be an attendee")) |
报告中
印刷报告
Odoo使用基于QWeb, Twitter Bootstrap和Wkhtmltopdf的报告引擎。
报告是两个元素的组合:
- 一个
ir.actions.report
为报告配置各种基本参数(默认类型,是否应在生成报告后将报告保存到数据库中,…)
01234567891011<record id="account_invoices" model="ir.actions.report"><field name="name">Invoices</field><field name="model">account.invoice</field><field name="report_type">qweb-pdf</field><field name="report_name">account.report_invoice</field><field name="report_file">account.report_invoice</field><field name="attachment_use" eval="True"/><field name="attachment">(object.state in ('open','paid')) and('INV'+(object.number or '').replace('/','')+'.pdf')</field><field name="binding_model_id" ref="model_account_invoice"/><field name="binding_type">report</field></record>
由于它是标准操作,因此与Wizards(向导)一样 ,通常将报告作为上下文项添加到树上和/或通过binding_model_id
字段形成要报告的模型的视图,通常很有用的。在这里,我们还使用了
binding_type
,以使报表位于报表上下文菜单中,而不是动作菜单中。没有技术差异,但是将元素放在正确的位置会对用户有所帮助。 - 实际报告的标准QWeb视图:
012345678<t t-call="web.html_container"><t t-foreach="docs" t-as="o"><t t-call="web.external_layout"><div class="page"><h2>Report title</h2></div></t></t></t>
标准渲染上下文提供了许多元素,其中最重要的是:
docs
- 打印报告的记录
user
- 用户打印报告
由于报告是标准网页,因此可以通过URL使用它们,并且可以通过此URL操作输出参数,例如,可以通过http://localhost:8069/report/html/account.report_invoice/1获得Invoice报告 的HTML版本。(如果account
已安装)和通过http://localhost:8069/report/pdf/account.report_invoice/1的PDF版本 。
危险
如果您的PDF报告似乎缺少样式(即,出现了文本,但样式/布局与html版本不同),则可能是您的wkhtmltopdf进程无法访问Web服务器下载它们。
如果检查服务器日志,并在生成PDF报告时发现CSS样式没有下载,则肯定是问题所在。
该wkhtmltopdf过程将使用web.base.url
系统参数的根路径到所有链接的文件,但此参数是每个管理员的登录时间自动更新。如果你的服务器所在背后的一些类型的代理,这可能是不可到达的。您可以通过添加以下系统参数之一来解决此问题:
report.url
,指向您的服务器可以访问的URL(可能http://localhost:8069
是类似的URL )。它将仅用于此特定目的。web.base.url.freeze
设置为时True
,将停止对web.base.url
的自动更新。
行使
为会话模型创建报告
对于每个会话,它应该显示会话的名称,其开始和结束,并列出该会话的参与者。openacademy/__manifest__.py
0 1 2 3 4 5 6 |
'templates.xml', 'views/openacademy.xml', 'views/partner.xml', 'reports.xml', ], # only loaded in demonstration mode 'demo': [ |
openacademy/reports.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<odoo> <record id="report_session" model="ir.actions.report"> <field name="name">Session Report</field> <field name="model">openacademy.session</field> <field name="report_type">qweb-pdf</field> <field name="report_name">openacademy.report_session_view</field> <field name="report_file">openacademy.report_session</field> <field name="binding_model_id" ref="model_openacademy_session"/> <field name="binding_type">report</field> </record> <template id="report_session_view"> <t t-call="web.html_container"> <t t-foreach="docs" t-as="doc"> <t t-call="web.external_layout"> <div class="page"> <h2 t-field="doc.name"/> <p>From <span t-field="doc.start_date"/> to <span t-field="doc.end_date"/></p> <h3>Attendees:</h3> <ul> <t t-foreach="doc.attendee_ids" t-as="attendee"> <li><span t-field="attendee.name"/></li> </t> </ul> </div> </t> </t> </t> </template> </odoo> |
仪表板
行使
定义仪表板
定义一个仪表板,其中包含您创建的图形视图,会话日历视图和课程列表视图(可切换到表单视图)。该仪表板应该通过菜单中的菜单项可用,并且在选择OpenAcademy主菜单后自动显示在Web客户端中。
- 创建一个文件
openacademy/views/session_board.xml
。它应包含面板视图,该视图中引用的操作,打开仪表板的操作以及对主菜单项的重新定义,以添加仪表板操作。
可用仪表盘样式是1
,1-1
,1-2
,2-1
和1-1-1
- 更新
openacademy/__manifest__.py
以引用新数据文件
openacademy/__manifest__.py
0 1 2 3 4 5 6 |
'version': '0.1', # any module necessary for this one to work correctly 'depends': ['base', 'board'], # always loaded 'data': [ |
0 1 2 3 4 5 6 |
'templates.xml', 'views/openacademy.xml', 'views/partner.xml', 'views/session_board.xml', 'reports.xml', ], # only loaded in demonstration mode |
openacademy/views/session_board.xml
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?xml version="1.0"?> <odoo> <record model="ir.actions.act_window" id="act_session_graph"> <field name="name">Attendees by course</field> <field name="res_model">openacademy.session</field> <field name="view_mode">graph</field> <field name="view_id" ref="openacademy.openacademy_session_graph_view"/> </record> <record model="ir.actions.act_window" id="act_session_calendar"> <field name="name">Sessions</field> <field name="res_model">openacademy.session</field> <field name="view_mode">calendar</field> <field name="view_id" ref="openacademy.session_calendar_view"/> </record> <record model="ir.actions.act_window" id="act_course_list"> <field name="name">Courses</field> <field name="res_model">openacademy.course</field> <field name="view_mode">tree,form</field> </record> <record model="ir.ui.view" id="board_session_form"> <field name="name">Session Dashboard Form</field> <field name="model">board.board</field> <field name="type">form</field> <field name="arch" type="xml"> <form string="Session Dashboard"> <board style="2-1"> <column> <action string="Attendees by course" 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> <record model="ir.actions.act_window" id="open_board_session"> <field name="name">Session Dashboard</field> <field name="res_model">board.board</field> <field name="view_mode">form</field> <field name="usage">menu</field> <field name="view_id" ref="board_session_form"/> </record> <menuitem name="Session Dashboard" parent="base.menu_reporting_dashboard" action="open_board_session" sequence="1" id="menu_board_session"/> </odoo> |
网页服务
Web服务模块为所有Web服务提供一个公共接口:
- XML-RPC
- JSON-RPC
也可以通过分布式对象机制访问业务对象。它们都可以通过带有上下文视图的客户端界面进行修改。
Odoo可通过XML-RPC/JSON-RPC接口访问,该接口存在多种语言的库。
XML-RPC库
以下示例是一个Python 3程序,该程序与具有该库的Odoo服务器进行交互xmlrpc.client
:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import xmlrpc.client root = 'http://%s:%d/xmlrpc/' % (HOST, PORT) uid = xmlrpc.client.ServerProxy(root + 'common').login(DB, USER, PASS) print("Logged in as %s (uid: %d)" % (USER, uid)) # Create a new note sock = xmlrpc.client.ServerProxy(root + 'object') args = { 'color' : 8, 'memo' : 'This is a note', 'create_uid': uid, } note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args) |
行使
向客户端添加新服务
编写一个Python程序,该程序能够将XML-RPC请求发送到运行Odoo的PC(您或您的讲师的)。该程序应显示所有会议及其相应的座位数。它还应为其中一门课程创建新的会话。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import functools import xmlrpc.client HOST = 'localhost' PORT = 8069 DB = 'openacademy' USER = 'admin' PASS = 'admin' ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT) # 1. Login uid = xmlrpc.client.ServerProxy(ROOT + 'common').login(DB,USER,PASS) print("Logged in as %s (uid:%d)" % (USER,uid)) call = functools.partial( xmlrpc.client.ServerProxy(ROOT + 'object').execute, DB, uid, PASS) # 2. Read the sessions sessions = call('openacademy.session','search_read', [], ['name','seats']) for session in sessions: print("Session %s (%s seats)" % (session['name'], session['seats'])) # 3.create a new session session_id = call('openacademy.session', 'create', { 'name' : 'My session', 'course_id' : 2, }) |
可以使用名称来查找课程,而不是使用硬编码的课程ID:
0 1 2 3 4 5 |
# 3.create a new session for the "Functional" course course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0] session_id = call('openacademy.session', 'create', { 'name' : 'My session', 'course_id' : course_id, }) |
JSON-RPC库
以下示例是一个Python 3程序,该程序使用标准Python库urllib.request
和json
与Odoo服务器进行交互。本示例假定已安装了生产力(Productivity)应用(note
):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import json import random import urllib.request HOST = 'localhost' PORT = 8069 DB = 'openacademy' USER = 'admin' PASS = 'admin' def json_rpc(url, method, params): data = { "jsonrpc": "2.0", "method": method, "params": params, "id": random.randint(0, 1000000000), } req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={ "Content-Type":"application/json", }) reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8')) if reply.get("error"): raise Exception(reply["error"]) return reply["result"] def call(url, service, method, *args): return json_rpc(url, "call", {"service": service, "method": method, "args": args}) # log in the given database url = "http://%s:%s/jsonrpc" % (HOST, PORT) uid = call(url, "common", "login", DB, USER, PASS) # create a new note args = { 'color': 8, 'memo': 'This is another note', 'create_uid': uid, } note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args) |
可以轻松地将示例从XML-RPC修改为JSON-RPC。
有许多使用各种语言编写的高级API可以访问Odoo系统,而无需显式地通过XML-RPC或JSON-RPC,例如:
- https://github.com/akretion/ooor
- https://github.com/OCA/odoorpc
- https://github.com/nicolas-van/openerp-client-lib
- http://pythonhosted.org/OdooRPC
- https://github.com/abhishek-jaiswal/php-openerp-lib
odoo创建一个模块,创建odoo模块,odoo创建模块,odoo二次开发