https://www.odoo.com/documentation/11.0/howtos/backend.html
本文主要内容
- 创建模型
- 添加数据
- 添加菜单
- 基本视图
- 模型关联
- 继承(模型继承、视图继承)
- 作用域(Domain)
- 计算字段和默认值
- Onchange
- 高级视图
- 列表视图
- 日历视图
- 搜索视图
- 甘特图
- 图表视图
- 看板视图
- 工作流
- 安全与权限
- 向导
- 多语言
- 报表
- 仪表盘
- Web 服务(XML RPC & JSON RPC)
一个模块主要包含:
- 业务对象(Business objects):声明为 Python 类
- 数据文件(Data files)声明元数据、配置数据、演示数据等的XML 或 CSV 文件
- Web控制器(Web controllers):处理浏览器请求
- 静态 web 数据(Static web data):Web 接口或网站使用的图片、CSS 或 JS 文件
使用脚手架初始化 Open Academy 模板目录结构
1 |
./odoo-bin scaffold openacademy myaddons |
myaddons/openacademy/文件结构:
├── controllers
│ ├── controllers.py
│ └── __init__.py
├── demo
│ └── demo.xml
├── __init__.py
├── __manifest__.py
├── models
│ ├── __init__.py
│ └── models.py
├── security
│ └── ir.model.access.csv
└── views
├── templates.xml
└── views.xml
首先可以尝试修改__manifest__.py的容并进行安装查看(Manifest 参数详解),示例如下
最基本的模型内容
1 2 3 4 5 6 |
from odoo import models, fields class openacademy(models.Model): _name = 'openacademy.openacademy' name = fields.Char() |
字段内可添加的属性有:
required=True 必填字段
string UI界面中的 label 值
help 界面中的提示信息
index 是否创建数据库索引
此外其它的有groups, digits, size, translate, sanitize, selection, comodel_name, domain, context
字段类型有 Boolean, Integer, Float, Char, Text, Date等
保留字段:
id 模型中记录的唯一标识符
create_date 记录创建日期
create_uid 创建记录的用户
write_date 记录最近一次修改的日期
write_uid 最近一次修改记录的用户
创建模型
models.py
注:涉及模型类等 Python 代码修改时请在更新前重启服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# -*- coding: utf-8 -*- from odoo import models, fields, api class openacademy(models.Model): _name = 'openacademy.openacademy' name = fields.Char() class Course(models.Model): _name = 'openacademy.course' name = fields.Char(string="Title", required=True) description = fields.Text() |
模型除了通过 Web 界面安装和更新外,还可使用类似odoo-bin -u openacademy来进行更新
添加数据
demo.xml(demo为安装时可选演示数据,此处因已安装模块,添加到 __manifest__.py的 data 下,对应数据表为openacademy_course)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<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> |
添加菜单
views/openacademy.xml(同样需要在__manifest__.py的 data 下进行添加,生效后存ir_ui_menu表中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?xml version="1.0" encoding="UTF-8"?> <odoo> <record model="ir.actions.act_window" id="course_list_action"> <field name="name">Courses</field> <field name="res_model">openacademy.course</field> <field name="view_type">form</field> <field name="view_mode">tree,form</field> <field name="help" type="html"> <p class="oe_view_nocontent_create">Create the first course </p> </field> </record> <menuitem id="main_openacademy_menu" name="Open Academy"/> <menuitem id="openacademy_menu" name="Open Academy" parent="main_openacademy_menu"/> <menuitem id="courses_menu" name="Courses" parent="openacademy_menu" action="course_list_action"/> </odoo> |
注意:action 应在调用前进行声明
基本视图
包含<form>表单视图、<tree>列表视图、<graph>图表视图、<search>搜索视图等,对应的数据表为 ir_ui_view
表单视图中有一些特殊的可选标签<sheet>纸张效果,<group>分组内自动在输入框前加上字段名称(字段名或已指定的 string 标签),<notebook>配合<page>产生 Tab 效果
views/openacademy.xml
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 |
<record model="ir.ui.view" id="openacademy_course_tree_view"> <field name="name">openacademy.course.tree.view</field> <field name="model">openacademy.course</field> <field name="priority" eval="0"/> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="description"/> </tree> </field> </record> <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> <notebook> <page string="Description"> <field name="description"/> </page> <page string="About"> This is an example of notebooks </page> </notebook> </sheet> </form> </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> |
列表视图 & 搜索视图
表单视图
模型关联
添加课程表
model.py
1 2 3 4 5 6 7 |
class Session(models.Model): _name = 'openacademy.session' name = fields.Char(required=True) start_date = fields.Date() duration = fields.Float(digits=(6, 2), help="Duration in days") seats = fields.Integer(string="Number of seats") |
添加视图和菜单
views/openacademy.xml
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 |
<!-- session form view --> <record model="ir.ui.view" id="session_form_view"> <field name="name">session.form</field> <field name="model">openacademy.session</field> <field name="arch" type="xml"> <form string="Session Form"> <sheet> <group> <field name="name"/> <field name="start_date"/> <field name="duration"/> <field name="seats"/> </group> </sheet> </form> </field> </record> <record model="ir.actions.act_window" id="session_list_action"> <field name="name">Sessions</field> <field name="res_model">openacademy.session</field> <field name="view_type">form</field> <field name="view_mode">tree,form</field> </record> <menuitem id="session_menu" name="Sessions" parent="openacademy_menu" action="session_list_action"/> |
关联字段:
Many2one:与其它对象的简单关联,如下例的 course_id
One2many:虚拟关联,与 Many2one 相反,如下例的 session_ids
Many2many:双向多记录关联,如下例中的attendee_ids,并且会生成一张名为openacademy_session_res_partner_rel的关联表(table1_table2_rel)
为课程表和课时表添加关联字段
model.py
1 2 3 4 5 6 7 8 9 10 |
class Course(models.Model): ... 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): ... 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") |
views/openacademy.xml
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 |
<tree string="Course list"> <field name="name"/> <field name="responsible_id"/> </tree> <form string="Course Form"> <sheet> <group> <field name="name"/> <field name="responsible_id"/> </group> <notebook> <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> </form> <tree string="Session Tree"> <field name="name"/> <field name="course_id"/> </tree> <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> <label for="attendee_ids"/> <field name="attendee_ids"/> </sheet> </form> |
继承(模型继承、视图继承)
模型继承
如上图模型继承包含传统继承和委托继承两种,传统继承又包含经典继承和原型继承两种方式。经典继承在原数据表上进行添加,原型继承则会复制原特性并生成新的数据表
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 |
# 经典继承 class SessionExtenstion(models.Model): _inherit = 'openacademy.session' note = fields.Text() # 原型继承,生成新的 openacademy_session_copy 表 class SessionCopy(models.Model): _name = 'openacademy.session.copy' _inherit = 'openacademy.session' copy = fields.Text() # 委托继承 class Child0(models.Model): _name = 'delegation.child0' field_0 = fields.Integer() class Child1(models.Model): _name = 'delegation.child1' field_1 = fields.Integer() class Delegating(models.Model): _name = 'delegation.parent' _inherits = { 'delegation.child0': 'child0_id', 'delegation.child1': 'child1_id', } child0_id = fields.Many2one('delegation.child0', required=True, ondelete='cascade') child1_id = fields.Many2one('delegation.child1', required=True, ondelete='cascade') # 进入 Odoo Shell ./odoo-bin shell -c odoo.conf -d odoo11 # 可通过输入self来验证,默认输出为res.users(1,) # 插入数据(数据表:delegation_child0,delegation_child1,delegation_parent) record = self.env['delegation.parent'].create({ 'child0_id': env['delegation.child0'].create({'field_0': 0}).id, 'child1_id': env['delegation.child1'].create({'field_1': 1}).id, }) # 执行写入 self.env.cr.commit() |
1 2 3 4 5 6 7 |
<xpath expr="//field[@name='description']" position="after"> <field name="idea_ids" /> </xpath> <field name="description" position="after"> <field name="idea_ids" /> </field> |
可通过 xpath 或明确的 field 内加入继承的视图,以上两段代码的效果相同,position参数常用属性值有 inside, replace, before, after, attributes 等
models/partner.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# -*- coding: utf-8 -*- from odoo import fields, models class Partner(models.Model): _inherit = 'res.partner' # Add a new column to the res.partner model, by default partners are not # instructors instructor = fields.Boolean("Instructor", default=False) session_ids = fields.Many2many('openacademy.session', string="Attended Sessions", readonly=True) Partner() |
views/partner.xml
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 |
<?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> |
上述在__init__.py 和__manifest__.py 中的配置请自行加入
注:psycopg2.ProgrammingError: column res_partner.instructor does not exist
我们在上述代码models/partner.py的尾部加入 Partner()可以解决这一报错,另一种方法是通过 SQL 来进行字段的添加
1 2 3 |
sudo -u postgres psql \c odoo11 ALTER TABLE res_partner ADD COLUMN instructor boolean default False; |
作用域(Domain)
作用域可以使用逻辑运算符& (AND), | (OR) 和 ! (NOT),也可以是它们的组合,如:
1 2 3 4 5 |
['|', ('product_type', '=', 'service'), '!', '&', ('unit_price', '>=', 1000), ('unit_price', '<', 2000)] |
上述过滤的是单价不在1000和2000之间或类型为 service 的产品
models/model.py
修改后创建或编辑 Session 时 Instructor 下拉列表中只显示勾选了 Instructor 或 Tag 包含 Teacher 的联系人
1 2 3 |
instructor_id = fields.Many2one('res.partner', string="Instructor", domain=['|', ('instructor', '=', True), ('category_id.name', 'ilike', "Teacher")]) |
views/partner.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<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> |
计算字段和默认值
models/models.py
compute 属性后指定计算的函数,该函数一般采用@api.multi装饰器,而depends(依赖)装饰器表示在指定参数值修改后进行重新计算
1 2 3 4 5 6 7 8 9 10 11 |
class Session(models.Model): ... 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 |
views/openacademy.xml
相应部分加入
1 |
<field name="taken_seats" widget="progressbar"/> |
默认值
active 字段为 False 时在前台默认不显示该条记录
models/models.py
1 2 3 4 |
class Session(models.Model): .... start_date = fields.Date(default=fields.Date.today) active = fields.Boolean(default=True) |
如果无论 active 值是 True 还是 False都需要显示,则加入content=”{‘active_test’: False}”(.py 代码中),如本例中 的Session:
views/openacademy.xml
1 2 3 4 |
<record model="ir.actions.act_window" id="session_list_action"> ... <field name="context">{'active_test': False}</field> </record> |
Onchange
onchange 的 self 是form 视图中的单条记录,它可作用于记录中一个或多个字段。并且 onchange 可独立于字段声明(不同于上述需添加 compute参数),它是一个内置行为,用于在指定条件发生变化时客户界面 form 表单的更新。
经测试onchange 执行先于前述的 depends,但其它代码只在失去焦点时执行,而在保存时并没有进行判断。
下面对座席数小于0或席位数少于报名人数时进行 onchange 的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Session(models.Model): ... @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", }, } |
模型约束
模型约束包含 Python 代码级别的约束和 SQL 级别的约束
导师不可同时为学员(不对原有数据进行校验,在保存是进行检查):
1 2 3 4 5 6 7 |
class Session(models.Model): ... @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约束检查名称和描述不能相同,名称不可重复:
models/models.py
1 2 3 4 5 6 7 8 9 10 11 |
class Course(models.Model): ... _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"), ] |
在表单视图下点击 duplicate 默认为标题加一个 Copy of 前缀
models/models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Course(models.Model): ... @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) |
高级视图
列表视图
根据条件可显示为不同颜色样式,如decoration-info=”state==’draft'”,decoration-danger=”state==’trashed'”,条件中也可以用 uid, current_date等
基本样式的定义为decoration-{$name},其中的{$name}可以是bf (font-weight: bold), it (font-style: italic)或 Bootstrap 的上下文颜色danger, info, muted, primary, success或warning。注意设置条件时符号需进行转义,见下例中的大于和小于号
设置editable 允许进行行内编辑,值为 top 或 bottom,表明新增一行时在上方还是下方
以下修改 Session 的列表视图,当 duration 小于5时显示为蓝色,大于15时显示为红色,并且实现行内编辑。duration 字段必须在视图中才能作为判断条件,这里使用了 invisible 属性在列表视图中隐藏该字段
views/openacademy.xml
1 2 3 4 5 |
<tree string="Session Tree" decoration-info="duration<5" decoration-danger="duration>15" editable="top"> ... <field name="duration" invisible="1"/> ... </tree> |
日历视图
models/models.py
这里添加一个 inverse 属性用于在修改 end_date 时,时长天数也进行对应的更改
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 |
class Session(models.Model): ... end_date = fields.Date(string="End Date", store=True, compute='_get_end_date', inverse='_set_end_date') @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 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 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 |
views/openacademy.xml
color 属性用于在日历视图中对指定字段的不同来按不同颜色显示,默认颜色为随机,也可在模型中添加一个 color 字段来指定显示的颜色
1 2 3 4 5 6 7 8 9 10 |
<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> # view_mode 中添加 calendar |
搜索视图
搜索视图可在 xml 中使用 filter_domain, domain, context 来进行设置,如以下设置可以在搜索字段时同时对 name 或 description 字段进行搜索。这里我们新建了一个 id为course_search_view_new的搜索视图来说明可以通过<field name=”search_view_id” ref=”course_search_view_new”/>的方式来指定搜索视图。以下实现的功能为默认过滤出自己负责的课程,并可通过 responsible_id 来进行分组
views/openacademy.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<record model="ir.ui.view" id="course_search_view_new"> <field name="name">course.search</field> <field name="model">openacademy.course</field> <field name="arch" type="xml"> <search> <field name="description" string="Name or description" filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/> <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> <record model="ir.actions.act_window" id="course_list_action"> ... <field name="search_view_id" ref="course_search_view_new"/> <field name="context" eval="{'search_default_my_courses': 1}"/> </record> |
甘特图
models/models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 添加 hours 字段以及计算方法 class Session(models.Model): ... hours = fields.Float(string="Duration in hours", compute='_get_hours', inverse='_set_hours') @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 |
views/openacademy.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<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_delay="hours" default_group_by='instructor_id'> <!-- <field name="name"/> this is not required after Odoo 10.0 --> </gantt> </field> </record> # 自行在 view_mode 中加入 gantt |
注:甘特图目前仅在企业版中使用,暂无演示效果,官方解释是因web_gantt开放协议问题而在社区版8.0版本后下线
图表视图
图表视图默认为柱状图(添加@stacked=”True”使用堆叠效果),此外还有饼状图,线状图。图表视图不支持不保存存在数据库的计算字段,即需要有 store=True,在 xml 中通过 type 属性可指定默认的图表类型
models/models.py
添加 Session 报名人数
1 2 3 4 5 6 7 8 9 |
class Session(models.Model): ... attendees_count = fields.Integer( string="Attendees count", compute='_get_attendees_count', store=True) @api.depends('attendee_ids') def _get_attendees_count(self): for r in self: r.attendees_count = len(r.attendee_ids) |
views/openacademy.xml
1 2 3 4 5 6 7 8 9 10 11 |
<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" type="bar"> <field name="course_id"/> <field name="attendees_count" type="measure"/> </graph> </field> </record> # 自行在 view_mode 中添加 graph |
看板视图
看板视图以卡片布局在页面上进行显示,看板视图是 html 基础元素配合 Qweb 来实现的
models/models.py
添加一个 color 字段
1 2 3 |
class Session(models.Model): ... color = fields.Integer() |
views/openacademy.xml
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 |
<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> |
工作流
模拟工作流
models/models.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Session(models.Model): ... 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' |
views/openacademy.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<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> ... </form> |
真实 Workflow 创建
真实的工作流需要在模型记录在 Workflow 设置好之后创建,否则不会生效。本例基于模拟工作流部分,这里需要
创建一个session_workflow.xml文件
删除模拟工作流中将 models.py 中的default=’draft’
修改 openacademy.xml 中button 中的内容:去除 name 属性值的 action 前缀,将 object 更改为 workflow。如name=”action_done” type=”object”修改为name=”done” type=”workflow”,这里的 name 属性与以下代码的 signal 相对应
views/session_workflow.xml
以下略过,因 Odoo 11已彻底删除掉工作流,查看具体原因
安全权限
后台创建
- 新建用户 John Smith
session_read
权限- 将 John Smith 添加至该组即使用该用户登录进行查看
通过代码文件控制权限
实现功能:添加OpenAcademy / Manager组对Open Academy拥有所有限,向所有用户开放 Session 和 Course 只读权限(csv 中group_id/id留空)
- 创建
security/security.xml
文件来配置 OpenAcademy Manager 组 - 编辑
security/ir.model.access.csv
文件设置模型权限 - 在
_manifest__.py
中添加上述文件
security/ir.model.access.csv
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 course_read_all,course all,model_openacademy_course,,1,0,0,0 session_read_all,session all,model_openacademy_session,,1,0,0,0 |
security/security.xml
1 2 3 4 5 6 7 |
<odoo> <record id="group_manager" model="res.groups"> <field name="name">OpenAcademy / Manager</field> </record> </odoo> |
记录规则控制权限
在OpenAcademy / Manage组中添加一个控制,仅课程负责人可编辑和删除课程,若课程无负责人,则所有人可编辑和删除
security/security.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<odoo> ... <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
- 向导记录是临时存放的,经过一定的时间后会自从数据库中删除,因而是过渡的数据表
- 向导模型无须权限控制,用户拥有对向导记录的所有权限
- 向导记录可通过 many2one 字段引用普通记录或另一个向导记录,但普通记录无法通过 many2one 字段引用向导记录
创建 wizard.py(在__init__.py 中引入 wizard.py)
1 2 3 4 5 6 7 8 9 10 |
from odoo import models, fields, api 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") |
views/openacademy.xml
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 |
<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> <footer> <button name="subscribe" type="object" string="Subscribe" class="oe_highlight"/> or <button special="cancel" string="Cancel"/> </footer> </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"/> |
wizard.py中添加对应的 subscribe 功能
1 2 3 4 5 6 7 |
class Wizard(models.TransientModel): ... @api.multi def subscribe(self): self.session_id.attendee_ids |= self.attendee_ids return {} |
允许同时向多个课时添加学员
views/openacademy.xml
1 2 3 4 5 6 |
<form string="Add Attendees"> <group> <field name="session_ids"/> <field name="attendee_ids"/> </group> </form> |
wizard.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 {} |
多语言
默认导出的 po 文件只包含 XML 文件中的内容,对.py 文件需使用 odoo._函数包裹,如_(“Label”)),对 models.py 进行以上操作
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 |
from odoo import models, fields, api, exceptions, _ class Course(models.Model): ... [('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) ... 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"), }, } ... raise exceptions.ValidationError(_("A session's instructor can't be an attendee")) |
创建翻译文件
- 创建
i18n/
文件夹,这个文件下使用语言或语言下划线国家来命名 po 文件,如 zh_CN.po - 载入所需语言:
- 导出翻译文件
- 使用文本编辑器或POEdit打开 po 文件进行翻译并放置到
i18n/
文件夹中 - 重复第二步载入翻译
注:需在首选项中选择语言才能显示翻译的版本
报表
Odoo 的报表基于QWeb, Twitter Bootstrap和Wkhtmltopdf.
reports.xml(加入__manifest__.py 的 data 中)
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 |
<odoo> <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="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> |
报表也可通过网页端获取 pdf 和 html 版,如
http://your.ip.address:8069/report/html/openacademy.report_session_view/3
http://your.ip.address:8069/report/pdf/openacademy.report_session_view/3
仪表盘
仪表盘的样式包含1, 1-1, 1-2, 2-1 和 1-1-1
views/session_board.xml
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 62 63 64 65 66 |
<?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_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"/> </odoo> |
Web 服务
XML RPC
根据实际能数录入,并执行该 Python 文件即可为 Course 0添加一个 My Session 的课时,注意 USER和 PASS 指的不是数据库而是后台登录的用户名和密码
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 |
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 for the "Course 0" course course_id = call('openacademy.course', 'search', [('name','ilike','Course 0')])[0] session_id = call('openacademy.session', 'create', { 'name' : 'My session', 'course_id' : course_id, }) |
当然我们也可以通过 POSTMAN来进行测试,如
以上在 POST 请求的 BODY 中要填入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<methodCall> <methodName>login</methodName> <!-- method name --> <params> <param> <value><string>odoo11</string></value> <!-- database name --> </param> <param> <value><string>admin</string></value> <!-- username --> </param> <param> <value><string>xxxxxx</string></value> <!-- user’s password --> </param> </params> </methodCall> |
JSON RPC
创建一个 Python 文件来执行操作,以下假设已安装Productivity
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 |
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) |
Postman 验证操作