模型约束
Odoo提供了两种设置自动验证不变量的方法: Python constraints
和SQL constraints
.
Python约束定义为用constrains()
修饰的方法,并在记录集上调用。修饰符指定约束中涉及哪些字段,以便在修改其中一个约束时自动评估约束。如果不满足该不变量,则期望该方法引发异常:
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约束
添加一个约束,该约束检查instructor在他/她自己的session的attendees中不存在。
openacademy/models.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
class Course(models.Model):
_name = 'openacademy.course'
'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's documentation的帮助下,添加以下约束:
- CHECK (检查)课程描述和课程名称是不同的
- 使Course的name UNIQUE(唯一的)
openacademy/models.py
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 - 添加重复选项
因为我们为课程名称的唯一性添加了一个约束,所以不再可能使用“复制”函数 (Form ‣ Duplicate)。
重新实现你自己的“复制”方法,允许复制课程对象,把原来的名字改成“[原始名字]的拷贝”。
openacademy/models.py
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
@api.multi
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) 和current_date
(当前日期为yyyy-MM-dd形式的字符串)。
{$name}
可以是 bf
(font-weight: bold
), it
(font-style: italic
),或任何的bootstrap contextual color (danger
,info
, muted
, primary
, success
or warning
).
<tree string="Idea Categories" decoration-info="state=='draft'"
decoration-danger="state=='trashed'">
<field name="name"/>
<field name="state"/>
</tree>
editable
要么是 "top"
要么是 "bottom"
。使树视图可就地编辑(而不是必须通过表单视图),该值是新行出现的位置
练习
列表颜色
修改Session树视图的方式是持续少于5天的会话是蓝色的,而持续超过15天的则是红色的。
修改Session树视图:
openacademy/views/openacademy.xml
<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
(可选的)
记录字段保存事件的结束日期/时间
字段(定义每个日历事件的标签 )
<calendar string="Ideas" date_start="invent_date" color="inventor_id">
<field name="name"/>
</calendar>
练习
日历视图
向Session模型添加日历视图,使用户能够查看与Open Academy相关的事件。
-
添加从start_date和duration计算的AA字段end_date
提示
逆函数使字段可写,并允许在日历视图中移动会话(通过拖放)
- 向Session 模型添加日历视图
- 并将日历视图添加到Session 模型的动作中
openacademy/models.py
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo import models, fields, api, exceptions
class Course(models.Model):
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):
},
}
@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
start = fields.Datetime.from_string(r.start_date)
duration = timedelta(days=r.duration, seconds=-1)
r.end_date = start + duration
def _set_end_date(self):
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
start_date = fields.Datetime.from_string(r.start_date)
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - 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
</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_type">form</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在给定字段名上分组结果
<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
<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>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context" eval="{'search_default_my_courses': 1}"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first course
</p>
甘特图视图
警告
甘特图视图需要在企业版版本中存在的web_gantt模块。
水平柱状图通常用于显示项目规划和进度,它们的根元素是<gantt>
。
<gantt string="Ideas"
date_start="invent_date"
date_stop="date_finished"
progress="progress"
default_group_by="inventor_id" />
练习
甘特图
添加甘特图,使用户能够查看与Open Academy模块相关联的session调度。session应由instructor分组。
- 创建一个以小时为单位表示session持续时间的计算字
- 添加甘特图视图的定义,并将甘特图视图添加到Session模型的操作中
openacademy/models.py
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.depends('duration')
def _get_hours(self):
for r in self:
r.hours = r.duration * 24
def _set_hours(self):
for r in self:
r.duration = r.hours / 24
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
openacademy/views/openacademy.xml
</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" color="course_id"
date_start="start_date" date_delay="hours"
default_group_by='instructor_id'>
<field name="name"/>
</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_type">form</field>
<field name="view_mode">tree,form,calendar,gantt</field>
</record>
<menuitem id="session_menu" name="Sessions"
图形视图
图形视图允许聚合的概述和模型的分析,它们的根元素是 <graph>
。
注
数据透视图(元素<pivot>)是一个多维表,它允许选择文件和维度来获得正确的聚合数据集,然后移动到更为图形化的概观。数据透视图共享与图视图相同的内容定义。
图形视图有4种显示模式,默认模式是使用@typ属性来选择的。
柱状图(默认的)
柱状图,第一维用于定义水平轴上的组,其他维度定义每个组内的聚合条。
默认情况下,柱状图是并排的,它们可以通过使用@stacked="True"在<graph>上进行堆叠
曲线图
二维线图
饼图
二维的饼图
图形视图包含具有强制@type属性的<field>值:
row
(默认的)
默认情况下应聚合字段
measure
字段应该聚合而不是分组
<graph string="Total idea score by Inventor">
<field name="inventor_id"/>
<field name="score" type="measure"/>
</graph>
警告
图形视图对数据库值执行聚合,它们不与非存储的计算字段一起使用
练习
图形视图
在Session对象中添加一个图形视图,它显示每个course中在柱形图表单下attendees的数量。
- 将attendees的数量作为存储的计算字段添加
- 然后添加相关视图
openacademy/models.py
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
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:
for r in self:
r.duration = r.hours / 24
@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
</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_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph</field>
</record>
<menuitem id="session_menu" name="Sessions"
看板视图
用于组织任务、生产过程等。它们的根元素是 <kanban>
。
看板视图显示了一组可能列在列中的卡。每个卡代表一个记录,每个列代表聚合字段的值。
例如,项目任务可以按阶段(每个列是一个阶段),或由负责的(每个列是用户)来组织,等等。
看板视图将每个卡的结构定义为表单元素(包括基本HTML)和QWEB的混合。
练习
看板视图
添加一个看板视图,显示按course分组的sessions(列是course)。
- 向Session模型添加整数color字段
- 添加看板视图并更新动作
openacademy/models.py
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
</record>
<record model="ir.ui.view" id="view_openacad_session_kanban">
<field name="name">openacad.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"/>
<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_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph,kanban</field>
</record>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
工作流
工作流是与描述其动态的业务对象相关联的模型。工作流也用于跟踪随着时间推移而演变的进程。
练习
工作流
添加一个state
字段到Session模型。它将被用来定义一个workflow-ish。
sesion可以有三种可能的状态:Draft (默认的), Confirmed 和Done。
在session表单中,添加一个(只读)字段来可视化状态,以及按钮来更改状态。有效的转换是:
- Draft -> Confirmed
- Confirmed -> Draft
- Confirmed -> Done
- Done -> Draft
- 添加一个新的
state
字段 - 添加状态转换方法,可以从视图按钮调用这些方法来改变记录的状态
- 并将相关按钮添加到会话的表单视图中
openacademy/models.py
attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)
state = fields.Selection([
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
], default='draft')
@api.multi
def action_draft(self):
self.state = 'draft'
@api.multi
def action_confirm(self):
self.state = 'confirmed'
@api.multi
def action_done(self):
self.state = 'done'
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
openacademy/views/openacademy.xml
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<header>
<button name="action_draft" type="object"
string="Reset to draft"
states="confirmed,done"/>
<button name="action_confirm" type="object"
string="Confirm" states="draft"
class="oe_highlight"/>
<button name="action_done" type="object"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group string="General">
工作流可以与Odoo中的任何对象相关联,并且是完全可定制的。工作流用于构造和管理业务对象和文档的生命周期,并用图形工具定义转换、触发器等。像往常一样,工作流、活动(节点或动作)和转换(条件)被声明为XML记录。在工作流中导航的令牌被称为工作项。
警告
与模型相关联的工作流仅在创建模型的记录时创建。因此,在工作流定义之前没有与工作流实例相关联的工作流实例
练习
工作流
通过实际工作流替换特设Session工作流。转换Session 表单视图,因此它的按钮调用工作流而不是模型的方法。
openacademy/__manifest__.py
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
],
# only loaded in demonstration mode
'demo': [
openacademy/models.py
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
])
@api.multi
def action_draft(self):
openacademy/views/openacademy.xml
<field name="arch" type="xml">
<form string="Session Form">
<header>
<button name="draft" type="workflow"
string="Reset to draft"
states="confirmed,done"/>
<button name="confirm" type="workflow"
string="Confirm" states="draft"
class="oe_highlight"/>
<button name="done" type="workflow"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
openacademy/views/session_workflow.xml
<odoo>
<data>
<record model="workflow" id="wkf_session">
<field name="name">OpenAcademy sessions workflow</field>
<field name="osv">openacademy.session</field>
<field name="on_create">True</field>
</record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">function</field>
<field name="action">action_draft()</field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_confirm()</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_done()</field>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="signal">confirm</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_draft">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_done_to_draft">
<field name="act_from" ref="done"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_done">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
</data>
</odoo>
提示
为了检查工作流的实例是否与session一起正确创建,可以到设置‣ 技术‣ 工作流‣ 实例中查看
练习
自动转换
当超过一半的session席位被使用时,自动将session从Draft 转换为Confirmed 。
openacademy/views/session_workflow.xml
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
<record model="workflow.transition" id="session_auto_confirm_half_filled">
<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="condition">taken_seats > 50</field>
</record>
</data>
</odoo>
练习
服务器动作
替换Python方法,通过服务器操作同步session状态。
工作流和服务器操作都可以完全由UI创建。
openacademy/views/session_workflow.xml
<field name="on_create">True</field>
</record>
<record model="ir.actions.server" id="set_session_to_draft">
<field name="name">Set session to Draft</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_draft()
</field>
</record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_draft"/>
</record>
<record model="ir.actions.server" id="set_session_to_confirmed">
<field name="name">Set session to Confirmed</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_confirm()
</field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_confirmed"/>
</record>
<record model="ir.actions.server" id="set_session_to_done">
<field name="name">Set session to Done</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_done()
</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_done"/>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
安全性
访问控制机制必须被配置来实现一致的安全策略。
基于组的访问控制机制
在模型res.groups上创建组作为正常记录,并通过菜单定义授予菜单访问权限。然而,即使没有菜单,对象仍然可以被间接访问,因此必须为组定义实际的对象级权限(读、写、创建、断开)。它们通常通过CSV文件插入模块内部。还可以使用字段的组属性限制对视图或对象的特定字段的访问。
访问权限
访问权限被定义为ir.model.access模型的记录。每个访问权限与一个模型、一个组(或者没有全局访问组)以及一组权限相关:读、写、创建、解锁。这样的访问权限通常由以其模型ir.model.access.csv命名的CSV文件创建:
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0
access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0
练习
通过Odoo 接口添加访问控制
创建新用户"John Smith",然后创建一个组"OpenAcademy / Session Read",其具有对Session 模型的读访问权限。
- 通过设置 ‣ 用户 ‣ 用户 创建新用户John Smith
- 通过设置 ‣ 用户 ‣ 群组创建一个新的组session_read,它应该具有Session模型上的读访问权限
- 编辑John Smith使他们成为session_read成员
- 使用John Smith登录 检查访问权限是否正确
练习
通过模块中的数据文件添加访问控制
使用数据文件,
- 创建一个完全访问所有OpenAcademy模型的OpenAcademy / Manager组
- 使Session和Course可由所有用户阅读
- 创建一个新文件openacademy/security/security.xml来保存OpenAcademy 管理器组
- 使用模型的访问权限编辑文件
openacademy/security/ir.model.access.csv
- 最后更新openacademy/__manifest__.py,将新的数据文件添加到它里面
openacademy/__manifest__.py
# 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
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
<odoo>
<data>
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
</data>
</odoo>
记录规则
记录规则限制对给定模型的记录子集的访问权限。规则是模型ir.rule的记录,与模型、多个组(many2many 字段)、限制应用的权限以及域相关联。指定访问权限受限的记录的域。
下面是一个防止删除不处于状态cancel的引线的规则的示例。请注意,字段groups
的值必须遵循与ORM方法write()
相同的约定。
<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>
练习
记录规则
为模型Course和"OpenAcademy / Manager"组添加记录规则,该规则限制write
和unlink
访问course的responsible。如果course没有responsible,小组的所有用户必须能够修改它。
在openacademy/security/security.xml中创建新规则:
openacademy/security/security.xml
<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>
</data>
</odoo>
向导
向导通过动态表单描述与用户(或对话框)的交互会话。向导只是一个扩展类TransientModel而不是Model
的模型。类TransientModel
扩展了Model
并重用其现有的机制,具有以下特殊性:
- 向导记录不意味着持久性;它们在一定时间后自动从数据库中删除。这就是为什么它们被称为瞬变的原因
- 向导模型不需要显式访问权限:用户拥有向导记录的所有权限。
- 向导记录可以通过many2one字段引用常规记录或向导记录,但常规记录不能通过many2one字段引用向导记录
我们希望创建一个向导,允许用户为特定sessio创建attendees,或者同时为session列表创建向导。
练习
定义向导
创建一个与Session模型具有many2one关系的向导模型,以及与Partner模型具有many2many 的关系。
添加新文件 openacademy/wizard.py
:
openacademy/__init__.py
from . import controllers
from . import models
from . import partner
from . import wizard
openacademy/wizard.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
session_id = fields.Many2one('openacademy.session',
string="Session", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
启动向导
向导是由ir.actions.act_window记录发起的,其中字段target
设置为值new。后者将向导视图打开到弹出窗口中。动作可以由菜单项触发。
还有另一种方法来启动向导:使用像上面那样的ir.actions.act_window记录,但是有一个额外的字段src_model,它指定了在哪个模型的上下文中该动作是可用的。向导将出现在模型的上下文动作中,位于主视图的上方。由于ORM中的一些内部钩子,这样的动作用标签act_window声明为XML。
<act_window id="launch_the_wizard"
name="Launch the Wizard"
src_model="context.model.name"
res_model="wizard.model.name"
view_mode="form"
target="new"
key2="client_action_multi"/>
向导使用常规视图,它们的按钮可以使用属性special="cancel"
关闭向导窗口而不保存。
练习
启动向导
- 为向导定义表单视图
- 添加动作以在Session模型的上下文中启动它
- 在向导中定义session字段的默认值;使用上下文参数self._context检索当前session
openacademy/wizard.py
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
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
<menuitem id="session_menu" name="Sessions"
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>
<act_window id="launch_session_wizard"
name="Add Attendees"
src_model="openacademy.session"
res_model="openacademy.wizard"
view_mode="form"
target="new"
key2="client_action_multi"/>
</data>
</odoo>
练习
注册attendees
向向导添加按钮,并实现将attendees 添加到给定session的相应方法
openacademy/views/openacademy.xml
<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
session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
@api.multi
def subscribe(self):
self.session_id.attendee_ids |= self.attendee_ids
return {}
练习
注册参会者多个会话
修改向导模型,以便参会者可以注册到多个会话
openacademy/views/openacademy.xml
<field name="arch" type="xml">
<form string="Add Attendees">
<group>
<field name="session_ids"/>
<field name="attendee_ids"/>
</group>
<footer>
openacademy/wizard.py
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
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")
@api.multi
def subscribe(self):
for session in self.session_ids:
session.attendee_ids |= self.attendee_ids
return {}
国际化
每个模块都可以在i18n目录内提供自己的翻译,通过将文件命名为LANG.po,其中LANG是语言的区域代码,或者当它们不同时,语言和国家组合(例如pt.po或者pt_BR.po)。对于所有启用的语言,翻译将由Odoo自动加载。开发人员在创建模块时总是使用英语,然后使用Odoo的获取文本POT导出功能(设置‣ 翻译‣ 导入/导出‣ 导出翻译 不指定语言)导出模块术语,以创建模块模板POT文件,然后导出翻译的PO文件。许多IDE有用于编辑和合并PO/POT文件的插件或模型。
提示
由Odoo生成的轻便对象文件在Transifex上发布,使其易于翻译软件
|- idea/ # The module directory
|- i18n/ # Translation files
| - idea.pot # Translation Template (exported from Odoo)
| - fr.po # French translation
| - pt_BR.po # Brazilian Portuguese translation
| (...)
提示
默认情况下,Odoo的POT导出只在Python代码中提取XML文件内的标签或字段定义,但是任何Python字符串可以通过将其与函数odoo._()(例如 _("Label")
)包围起来来进行翻译
练习
翻译模块
为您的Odoo安装选择第二种语言。使用Odoo提供的工具翻译您的模块。(以下步骤需激活开发者模式)
- 新建目录
openacademy/i18n/
- 安装你想要的任何语言( 翻译‣ 加载翻译)
- 同步可译术语(翻译‣ 应用程序术语 ‣ 同步术语)
- 在不指定语言的情况下通过导出创建模板翻译文件( 翻译‣ 导入/导出 ‣ 导出翻译) ,请保存在
openacademy/i18n/
- 通过导出和(翻译 ‣ 导入/导出 ‣ 导出翻译)指定语言来创建翻译文件。请保存到
openacademy/i18n/
- 用基本的文本编辑器或专用的PO-file编辑器打开导出的翻译文件,例如POEdit并翻译缺失的术语
- 在models.py中,为函数odoo._添加一个导入语句,并将缺少的字符串作为可翻译的标记
- 重复步骤3-6
openacademy/models.py
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo import models, fields, api, exceptions, _
class Course(models.Model):
_name = 'openacademy.course'
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)
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"),
},
}
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 8附带了一个基于QWeb、Twitter Bootstrap和Wkhtmltopdf的新报表引擎。
报表是两个要素的组合:
-
一个ir.actions.report.xml,它提供了一个<report>快捷方式元素,它为报表建立了各种基本参数(默认类型,报告是否应该在生成后保存到数据库中,…)
<report id="account_invoices" model="account.invoice" string="Invoices" report_type="qweb-pdf" name="account.report_invoice" file="account.report_invoice" attachment_use="True" attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/','')+'.pdf')" />
-
实际报表的标准QWeb view:
<t t-call="report.html_container"> <t t-foreach="docs" t-as="o"> <t t-call="report.external_layout"> <div class="page"> <h2>Report title</h2> </div> </t> </t> </t> the standard rendering context provides a number of elements, the most important being: ``docs`` the records for which the report is printed ``user`` the user printing the report
因为报表是标准网页,它们可以通过URL获得,并且可以通过该URL操作输出参数,例如,通过http://localhost:8069/report/html/account.report_invoice/1(如果安装了account)和通过http://localhost:8069/report/pdf/account.report_invoice/1的PDF版本,可以提供发票报表的HTML版本。
危险
如果您的PDF报表缺少样式(即文本出现,但样式/布局与HTML版本不同),可能您的wkhtmltopdf进程无法到达Web服务器下载。
如果在生成PDF报表时检查服务器日志并发现CSS样式没有被下载,则这肯定是问题所在。
wkhtmltopdf 进程将使用web.base.url系统参数作为所有链接文件的根路径,但是每次管理员登录时,该参数都会自动更新。如果您的服务器驻留在某种代理后面,则无法到达。您可以通过添加这些系统参数中的一个来修复:
report.url
, 指向一个可从服务器访问的URL (可能是http://localhost:8069或类似的东西)。它只用于这个特殊目的web.base.url.freeze
, 设置为True
, 将停止自动更新web.base.url
练习
为session模型创建报表
对于每个会话,它应该显示会话的名称、它的开始和结束,并列出会话的参与者。
openacademy/__manifest__.py
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'report'],
# always loaded
'data': [
openacademy/__manifest__.py
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'reports.xml',
],
# only loaded in demonstration mode
'demo': [
openacademy/reports.xml
<odoo>
<data>
<report
id="report_session"
model="openacademy.session"
string="Session Report"
name="openacademy.report_session_view"
file="openacademy.report_session"
report_type="qweb-pdf" />
<template id="report_session_view">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="report.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>
</data>
</odoo>
仪表板
练习
定义仪表板
定义一个仪表板,其中包含您创建的图形视图、会话日历视图和课程列表视图(可切换到表单视图)。此仪表板应通过菜单中的菜单项提供,并在选择OpenAcademy主菜单时自动显示在Web客户端中
注:此模块需要您odoo已经安装了系统的仪表板模块,此练习是菜单附加在系统仪表板中。
-
新建文件
openacademy/views/session_board.xml
。它应该包含板视图、在该视图中引用的动作、打开仪表板的动作以及重新定义主菜单项以添加仪表板动作注
可用的仪表板样式是
1
,1-1
,1-2
,2-1
和1-1-1
- 更新
openacademy/__manifest__.py
以引用新的数据文件
openacademy/__manifest__.py
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'report', 'board'],
# always loaded
'data': [
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'views/session_board.xml',
'reports.xml',
],
# only loaded in demonstration mode
openacademy/views/session_board.xml
<?xml version="1.0"?>
<odoo>
<data>
<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_type">form</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_type">form</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_type">form</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_type">form</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" icon="terp-graph"/>
</data>
</odoo>
WebServices
Web服务模块为所有Web服务提供一个公共接口:
- XML-RPC
- JSON-RPC
业务对象也可以通过分布式对象机制访问。它们都可以通过与上下文视图的客户端接口进行修改。
Odoo 可通过XML-RPC/JSON-RPC接口访问,其中库以多种语言存在。
XML-RPC库
下面的例子是一个Python程序,它与Odoo服务器与xmlrpclib库交互:
import xmlrpclib
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)
print "Logged in as %s (uid: %d)" % (USER, uid)
# Create a new note
sock = xmlrpclib.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 (你的,或者你的指导者的)。这个程序应该显示所有的sessions,以及它们对应的seats数目。它还应该为一个course创建一个新的session
import functools
import xmlrpclib
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)
# 1. Login
uid = xmlrpclib.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print "Logged in as %s (uid:%d)" % (USER,uid)
call = functools.partial(
xmlrpclib.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,代码可以按名称查找课程:
# 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程序,它与标准的Python库urllib2和json交互,与Odoo服务器交互:
import json
import random
import urllib2
def json_rpc(url, method, params):
data = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": random.randint(0, 1000000000),
}
req = urllib2.Request(url=url, data=json.dumps(data), headers={
"Content-Type":"application/json",
})
reply = json.load(urllib2.urlopen(req))
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)
这里是同一个程序,使用jsonrpclib库:
import jsonrpclib
# server proxy object
url = "http://%s:%s/jsonrpc" % (HOST, PORT)
server = jsonrpclib.Server(url)
# log in the given database
uid = server.call(service="common", method="login", args=[DB, USER, PASS])
# helper function for invoking model methods
def invoke(model, method, *args):
args = [DB, uid, PASS, model, method] + list(args)
return server.call(service="object", method="execute", args=args)
# create a new note
args = {
'color' : 8,
'memo' : 'This is another note',
'create_uid': uid,
}
note_id = invoke('note.note', 'create', args)
实例可以很容易地从XML- RPC适应JSON-RPC。
注
在不显式地通过XML- RPC或JSON-RPC的情况下,在各种语言中有许多高级API来访问Odoo系统,例如:
- https://github.com/akretion/ooor
- https://github.com/syleam/openobject-library
- https://github.com/nicolas-van/openerp-client-lib
- http://pythonhosted.org/OdooRPC
- https://github.com/abhishek-jaiswal/php-openerp-lib
上一篇:Odoo10教程---模块化二:模型间关系,继承,计算字段等
代码下载地址:https://download.youkuaiyun.com/download/mzl87/10405778
ps:有翻译不当之处,欢迎留言指正。
原文地址:https://www.odoo.com/documentation/10.0/howtos/backend.html