构建网站¶
危险
This tutorial is outdated. We recommend reading Server framework 101 instead.
警告
本指南假设您具备 Python 基础知识
本指南假设已经 安装了Odoo
创建一个基本模块¶
在Odoo中,通过创建模块来执行任务。
模块可以自定义Odoo安装的行为,可以通过添加新的行为或更改现有的行为(包括其他模块添加的行为)来实现。
Odoo的脚手架可以设置一个基本模块。要快速开始,请简单地调用: Odoo’s scaffolding 。
$ ./odoo-bin scaffold Academy my-modules
这将自动创建一个名为 my-modules
的 模块目录 ,其中包含一个名为 academy
的模块。如果您愿意,该目录可以是现有的模块目录,但模块名称必须在目录内唯一。
一个演示模块¶
我们有一个“完整”的模块可以安装。
虽然它什么也不做,但我们可以安装它:
启动Odoo服务器
$ ./odoo-bin --addons-path addons,my-modules
创建一个包含演示数据的新数据库
前往
在右上角移除 Installed 过滤器并搜索 academy
点击 安装 按钮以安装 Academy 模块
发送到浏览器¶
Controllers interpret browser requests and send data back.
添加一个简单的控制器并确保它被 __init__.py
导入(这样 Odoo 才能找到它):
# -*- coding: utf-8 -*-
from odoo import http
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
return "Hello, world"
关闭服务器(^C)然后重新启动它:
$ ./odoo-bin --addons-path addons,my-modules
打开 http://localhost:8069/academy/academy/ 页面,你应该可以看到你的“页面”:
模板¶
在Python中生成HTML并不是很愉快。
通常的解决方案是 templates,带有占位符和显示逻辑的伪文档。Odoo 允许使用任何 Python 模板系统,但提供了自己的 QWeb 模板系统,可以与其他功能集成。
创建一个模板,并确保在 __manifest__.py
文件中注册该模板文件,然后修改控制器以使用我们的模板:
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
return http.request.render('academy.index', {
'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"],
})
<odoo>
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher"/></p>
</t>
</template>
</odoo>
模板在所有教师上进行迭代( t-foreach
)(通过 模板上下文 传递),并将每个教师打印在自己的段落中。
最后重新启动Odoo并更新模块的数据(以安装模板),方法是进入 升级。
然后点击小技巧
或者,可以重新启动Odoo并同时更新模块 <odoo-bin -u>
:
$ odoo-bin --addons-path addons,my-modules -d academy -u academy
现在访问 http://localhost:8069/academy/academy/ 应该会得到以下结果:
在Odoo中存储数据¶
Odoo模型 <reference/orm/model> 对应数据库表。
在前一节中,我们只是在Python代码中静态显示了一些字符串列表。这样做不允许修改或持久存储,因此我们现在将数据移动到数据库中。
定义数据模型¶
定义一个教师模型,并确保从 __init__.py
导入,以便正确加载:
from odoo import models, fields, api
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
然后为模型设置 基本访问控制 并将其添加到清单中:
# always loaded
'data': [
'security/ir.model.access.csv',
'templates.xml',
],
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
这只是给予所有用户(group_id:id
留空)读取权限(perm_read
)。
注解
Data files (XML or CSV) must be added to the
module manifest, Python files (models or controllers) don’t but have to
be imported from __init__.py
(directly or indirectly)
警告
管理员用户绕过访问控制,即使未被授权,也可以访问所有模型
演示数据¶
第二步是向系统中添加一些演示数据,以便可以轻松地进行测试。这是通过添加一个 demo
data file 来完成的,该文件必须从清单中链接:
<odoo>
<record id="padilla" model="academy.teachers">
<field name="name">Diana Padilla</field>
</record>
<record id="carroll" model="academy.teachers">
<field name="name">Jody Carroll</field>
</record>
<record id="vaughn" model="academy.teachers">
<field name="name">Lester Vaughn</field>
</record>
</odoo>
小技巧
Data files can be used for demo and non-demo data. Demo data are only loaded in “demonstration mode” and can be used for flow testing and demonstration, non-demo data are always loaded and used as initial system setup.
在这种情况下,我们使用演示数据,因为实际用户会想要输入或导入自己的教师列表,这个列表只对测试有用。
访问数据¶
最后一步是修改模型和模板以使用我们的演示数据:
从数据库中获取记录,而不是使用静态列表
因为
search()
返回与过滤器匹配的一组记录 (这里是”所有记录”), 修改模板以打印每个教师的name
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
'teachers': Teachers.search([])
})
<odoo>
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
</t>
</template>
</odoo>
重启服务器并更新模块(以更新清单和模板并加载演示文件),然后导航到 http://localhost:8069/academy/academy/。页面应该略有不同:名称应该只是以数字为前缀(教师的数据库标识符)。
网站支持¶
Odoo捆绑了一个专门用于构建网站的模块。
到目前为止,我们已经相当直接地使用了控制器,但是Odoo 8通过 website
模块添加了更深入的集成和一些其他服务(例如默认样式,主题)。
首先,将
website
添加为academy
的依赖项然后在控制器上添加
website=True
标志,这会在 请求对象 上设置一些新的变量,并允许在我们的模板中使用网站布局在模板中使用网站布局
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['website'],
# always loaded
'data': [
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public', website=True)
def index(self, **kw):
Teachers = http.request.env['academy.teachers']
return http.request.render('academy.index', {
'teachers': Teachers.search([])
})
<odoo>
<template id="index">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure">
<div class="container">
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p>
</t>
</div>
</div>
</t>
</template>
</odoo>
在更新模块时重新启动服务器(以更新清单和模板),访问http://localhost:8069/academy/academy/ 应该会呈现出更漂亮的页面,带有品牌和许多内置页面元素(顶部菜单,页脚等)
网站布局还提供了编辑工具的支持:点击 登录`(在右上角),填写凭据(``admin` / admin
默认)然后点击 登录。
您现在进入了Odoo“proper”:管理界面。现在点击 Website 菜单项(左上角)。
我们已经回到网站,但是作为管理员,可以访问由 网站 支持提供的高级编辑功能:
一个模板代码编辑器(
),您可以在其中查看和编辑当前页面使用的所有模板在左上角的 Edit 按钮切换到“编辑模式”,可以使用块(片段)和富文本编辑
一些其他功能,如移动预览或 SEO
URL和路由¶
控制器方法通过 routes 与 route()
装饰器关联,该装饰器接受一个路由字符串和一些属性来自定义其行为或安全性。
我们已经看到了一个“字面”路由字符串,它精确匹配URL的部分,但路由字符串也可以使用 转换器模式
_,它可以匹配URL的一部分,并将其作为本地变量可用。例如,我们可以创建一个新的控制器方法,它接受URL的一部分并将其打印出来:
# New route
@http.route('/academy/<name>/', auth='public', website=True)
def teacher(self, name):
return '<h1>{}</h1>'.format(name)
重新启动Odoo,访问http://localhost:8069/academy/Alice/和http://localhost:8069/academy/Bob/,并查看差异。
如其名所示, 转换器模式
_ 不仅仅进行提取,还进行 验证 和 转换 ,因此我们可以将新控制器更改为仅接受整数:
@http.route('/academy/<int:id>/', auth='public', website=True)
def teacher(self, id):
return '<h1>{} ({})</h1>'.format(id, type(id).__name__)
重新启动Odoo,访问http://localhost:8069/academy/2,注意旧值是字符串,但新值已转换为整数。尝试访问http://localhost:8069/academy/Carol/,注意页面未找到:由于“Carol”不是整数,路由被忽略,找不到任何路由。
Odoo 提供了一个名为 model
的额外转换器,可以直接提供给定 id 的记录。让我们使用它来创建一个通用的教师传记页面:
@http.route('/academy/<model("academy.teachers"):teacher>/', auth='public', website=True)
def teacher(self, teacher):
return http.request.render('academy.biography', {
'person': teacher
})
<template id="biography">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
<h3><t t-esc="person.name"/></h3>
</div>
</div>
<div class="oe_structure"/>
</t>
</template>
然后将模型列表更改为链接到我们的新控制器:
<template id="index">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure">
<div class="container">
<t t-foreach="teachers" t-as="teacher">
<p>
<a t-attf-href="/academy/{{ slug(teacher) }}">
<t t-esc="teacher.name"/></a>
</p>
</t>
</div>
</div>
</t>
</template>
重启Odoo并升级模块,然后您可以访问每个教师的页面。作为练习,尝试在教师的页面上添加块以编写传记,然后转到另一个教师的页面,依此类推。您会发现,您的传记在所有教师之间共享,因为块被添加到 模板 中,而 传记 模板在所有教师之间共享,当编辑一个页面时,它们都会同时被编辑。
字段编辑¶
数据应该保存在特定的记录中,因此让我们向我们的教师添加一个新的传记字段:
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
biography = fields.Html()
<template id="biography">
<t t-call="website.layout">
<t t-set="title">Academy</t>
<div class="oe_structure"/>
<div class="oe_structure">
<div class="container">
<h3><t t-esc="person.name"/></h3>
<div><t t-esc="person.biography"/></div>
</div>
</div>
<div class="oe_structure"/>
</t>
</template>
重启Odoo并更新视图,重新加载教师页面,…由于它不包含任何内容,因此该字段是不可见的。
对于记录字段,模板可以使用特殊的 t-field
指令,允许使用字段特定的界面从网站上编辑字段内容。将 person 模板更改为使用 t-field
:
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<div t-field="person.biography"/>
</div>
</div>
重启Odoo并升级模块,现在在教师姓名下有一个占位符,在 编辑 模式下有一个新的块区域。在那里放置的内容存储在相应的教师 biography
字段中,因此特定于该教师。
教师的姓名也可以进行编辑,保存后更改将在首页上显示。
t-field
can also take formatting options which depend on the exact field.
For instance if we display the modification date for a teacher’s record:
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date"/></p>
<div t-field="person.biography"/>
</div>
</div>
它以非常”计算机”的方式显示,很难阅读,但我们可以要求一个易读的版本:
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date" t-options='{"format": "long"}'/></p>
<div t-field="person.biography"/>
</div>
</div>
或相对显示:
<div class="oe_structure">
<div class="container">
<h3 t-field="person.name"/>
<p>Last modified: <i t-field="person.write_date" t-options='{"widget": "relative"}'/></p>
<div t-field="person.biography"/>
</div>
</div>
管理和ERP集成¶
Odoo管理简介:简短而不完整¶
在 网站支持 部分中,我们简要地看到了Odoo管理界面。我们可以通过菜单中的 )返回到它。
Odoo 后端的概念结构很简单:
首先是菜单,一棵记录树(菜单可以有子菜单)。没有子菜单的菜单映射到…
actions。Actions 有各种类型:链接、报表、Odoo 应该执行的代码或数据显示。数据显示 actions 被称为 window actions,并告诉 Odoo 根据一组视图显示给定的 model…
视图有类型,对应于一个广泛的类别(列表、图表、日历),以及一个 架构 ,该架构定制了模型在视图中的显示方式。
在Odoo管理界面中进行编辑¶
默认情况下,Odoo模型对用户来说基本上是不可见的。要使其可见,必须通过操作使其可用,而操作本身需要可达,通常是通过菜单实现。
让我们为我们的模型创建一个菜单:
# always loaded
'data': [
'security/ir.model.access.csv',
'templates.xml',
'views.xml',
],
<odoo>
<record id="action_academy_teachers" model="ir.actions.act_window">
<field name="name">Academy teachers</field>
<field name="res_model">academy.teachers</field>
</record>
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>
</odoo>
然后在左上角访问 http://localhost:8069/web/ 应该有一个菜单 Academy ,默认选中,因为它是第一个菜单,并打开了一个教师列表。从列表中可以 Create 新的教师记录,并切换到“form” by-record 视图。
If there is no definition of how to present records (a
view) Odoo will automatically create a basic one
on-the-fly. In our case it works for the “list” view for now (only displays
the teacher’s name) but in the “form” view the HTML biography
field is
displayed side-by-side with the name
field and not given enough space.
Let’s define a custom form view to make viewing and editing teacher records
a better experience:
<record id="academy_teacher_form" model="ir.ui.view">
<field name="name">Academy teachers: form</field>
<field name="model">academy.teachers</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="biography"/>
</sheet>
</form>
</field>
</record>
模型之间的关系¶
我们已经看到了一对直接存储在记录中的”基本”字段。这里有 一些基本字段。第二个广泛的字段类别是 关系字段,用于将记录链接到彼此(在模型内或跨模型)。
为了演示,让我们创建一个 courses 模型。每个课程应该有一个 teacher
字段,链接到一个单独的教师记录,但每个教师可以教授多门课程:
class Courses(models.Model):
_name = 'academy.courses'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0
我们还需要添加视图,以便查看和编辑课程的教师:
<record id="action_academy_courses" model="ir.actions.act_window">
<field name="name">Academy courses</field>
<field name="res_model">academy.courses</field>
</record>
<record id="academy_course_search" model="ir.ui.view">
<field name="name">Academy courses: search</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="teacher_id"/>
</search>
</field>
</record>
<record id="academy_course_list" model="ir.ui.view">
<field name="name">Academy courses: list</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<list string="Courses">
<field name="name"/>
<field name="teacher_id"/>
</list>
</field>
</record>
<record id="academy_course_form" model="ir.ui.view">
<field name="name">Academy courses: form</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="teacher_id"/>
</sheet>
</form>
</field>
</record>
<menuitem sequence="0" id="menu_academy" name="Academy"/>
<menuitem id="menu_academy_content" parent="menu_academy"
name="Academy Content"/>
<menuitem id="menu_academy_content_courses"
parent="menu_academy_content"
action="action_academy_courses"/>
<menuitem id="menu_academy_content_teachers"
parent="menu_academy_content"
action="action_academy_teachers"/>
也应该可以直接从教师的页面创建新的课程,或者查看他们教授的所有课程,因此在 teachers 模型中添加 the inverse relationship
:
class Teachers(models.Model):
_name = 'academy.teachers'
name = fields.Char()
biography = fields.Html()
course_ids = fields.One2many('academy.courses', 'teacher_id', string="Courses")
class Courses(models.Model):
_name = 'academy.courses'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
<record id="academy_teacher_form" model="ir.ui.view">
<field name="name">Academy teachers: form</field>
<field name="model">academy.teachers</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="biography"/>
<field name="course_ids">
<list string="Courses" editable="bottom">
<field name="name"/>
</list>
</field>
</sheet>
</form>
</field>
</record>
讨论和通知¶
Odoo提供技术模型,这些模型不直接满足业务需求,但可以为业务对象添加功能,而无需手动构建它们。
其中之一是 Chatter 系统,它是 Odoo 的电子邮件和消息系统的一部分,可以为任何模型添加通知和讨论线程。模型只需 _inherit
mail.thread
,并在其表单视图中添加 message_ids
字段以显示讨论线程。讨论线程是每个记录独立的。
对于我们的学院来说,允许讨论课程以处理例如时间表更改或教师和助手之间的讨论是有意义的:
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['website', 'mail'],
# always loaded
'data': [
class Courses(models.Model):
_name = 'academy.courses'
_inherit = 'mail.thread'
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
<record id="academy_course_form" model="ir.ui.view">
<field name="name">Academy courses: form</field>
<field name="model">academy.courses</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="name"/>
<field name="teacher_id"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
在每个课程表单的底部,现在有一个讨论主题和系统用户留言、关注或取消关注与特定课程相关的讨论的可能性。
销售课程¶
Odoo 还提供了业务模型,允许更直接地使用或选择业务需求。例如, website_sale
模块基于 Odoo 系统中的产品设置了一个电子商务网站。我们可以通过将我们的课程设定为特定类型的产品,轻松地将课程订阅变为可销售的产品。
与其以前的经典继承方式不同,这意味着用 product 模型替换我们的 course 模型,并在原地扩展产品(以添加我们需要的任何内容)。
首先,我们需要添加对 website_sale
的依赖,这样我们就可以同时获得产品(通过 sale
)和电子商务界面:
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['mail', 'website_sale'],
# always loaded
'data': [
重新启动 Odoo,更新您的模块,现在网站上有一个 Shop 部分,列出了一些预填充的(通过演示数据)产品。
第二步是将 courses 模型替换为 product.template
,并为课程添加一个新的产品类别:
'security/ir.model.access.csv',
'templates.xml',
'views.xml',
'data.xml',
],
# only loaded in demonstration mode
'demo': [
<odoo>
<record model="product.public.category" id="category_courses">
<field name="name">Courses</field>
<field name="parent_id" ref="website_sale.categ_others"/>
</record>
</odoo>
<record id="course0" model="product.template">
<field name="name">Course 0</field>
<field name="teacher_id" ref="padilla"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
<record id="course1" model="product.template">
<field name="name">Course 1</field>
<field name="teacher_id" ref="padilla"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
<record id="course2" model="product.template">
<field name="name">Course 2</field>
<field name="teacher_id" ref="vaughn"/>
<field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/>
<field name="website_published">True</field>
<field name="list_price" type="float">0</field>
<field name="type">service</field>
</record>
class Courses(models.Model):
_name = 'academy.courses'
_inherit = ['mail.thread', 'product.template']
name = fields.Char()
teacher_id = fields.Many2one('academy.teachers', string="Teacher")
安装完成后, 商店 中现在有一些课程可供选择,但可能需要搜索。
注解
要在原地扩展模型,可以使用
inherited
而不给它一个新的_name
product.template
already uses the discussions system, so we can remove it from our extension model我们默认将我们的课程设置为 发布 状态,这样就可以在不必登录的情况下查看它们。
修改现有视图¶
到目前为止,我们简要地看到了:
创建新模型
创建新视图
创建新记录
修改现有模型
我们剩下的是对现有记录和现有视图的修改。我们将在 Shop 页面上同时进行。
通过创建 扩展 视图来进行视图修改,这些视图应用于原始视图之上并对其进行修改。这些修改视图可以添加或删除,而不必修改原始视图,这使得尝试事物和回滚更加容易。
由于我们的课程是免费的,所以没有理由在商店页面上显示它们的价格,因此我们将修改视图并在价格为0时隐藏价格。第一个任务是找出显示价格的视图,可以通过
来完成,这样我们就可以阅读与渲染页面相关的各种模板。浏览了其中几个模板后,”产品项目” 看起来是一个可能的罪魁祸首。更改视图架构需要分为三个步骤:
创建一个新视图
通过将新视图的
inherit_id
设置为修改后的视图的外部ID来扩展要修改的视图在架构中,使用
xpath
标签从修改后的视图中选择和更改元素
<template id="product_item_hide_no_price" inherit_id="website_sale.products_item">
<xpath expr="//div[hasclass('product_price')]/b" position="attributes">
<attribute name="t-if">product.price > 0</attribute>
</xpath>
</template>
我们要做的第二件事是将产品类别侧边栏默认设置为可见:
可以让您切换产品类别树(用于过滤主显示)的开启和关闭。这是通过扩展模板的 customize_show
和 active
字段来完成的:扩展模板(例如我们刚刚创建的模板)可以设置为 customize_show=True。这个选择将在 自定义 菜单中显示一个复选框,允许管理员激活或禁用它们(并轻松自定义网站页面)。
我们只需要修改 Product Categories 记录并将其默认设置为 active=”True”:
<record id="website_sale.products_categories" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
使用此功能,当安装了 Academy 模块时, Product Categories 侧边栏将自动启用。