odoo框架初识,简单小应用
odoo12版本学习
一·odoo简介
odoo是快速开发ERP系统的框架,适合商用. 内置crud,丰富的组件:看板,日历,图表.
odoo采用mvc架构模式. m即model,数据层, v及view,视图层(展示层),c即controller,逻辑层
odoo结构:
数据层:
持久化层,负责存储.odoo借用PostgreSQL来实现. 不支持MySQL数据(可借用的第三方集成MySQL)
文件附件,图片一类的二进制存储在filestore目录下
逻辑层
负责与数据层交互 . odoo核心代码种,提供了ORM引擎,ORM提供插件模块与数据交互的API
展示层
展示数据与用户交互, 自带web客户端.
包含CMS框架,支持灵活创建网页
二·odoo环境配置
本实验采用ubuntu系统
步骤一
安装PostgreSQL数据库
sudo apt update
sudo apt install postgresql -y # 安装PostgreSQL
sudo su -c "createuser -s $USER" postgres # 创建数据库超级用户
步骤二
安装python3环境,以及其他依赖
sudo apt update
sudo apt upgrade
# 安装Git
sudo apt install git -y
# 安装python3
sudo apt install python3-dev python3-pip -y # Python 3 for dev
# 安装依赖
sudo apt install build-essential libxslt-dev libzip-dev libldap2-dev libsasl2-dev libssl-dev -y
# 安装Node.js和包管理器
sudo apt install npm
sudo ln -s /usr/bin/nodejs /usr/bin/node # 通过node运行Node.js
sudo npm install -g less less-plugin-clean-css # 安装less
步骤三
安装odoo源码
1.在 Home 目录创建工作目录
# 创建工作目录
mkdir ~/odoo-dev
# 进入工作目录
cd ~/odoo-dev
2.克隆odoo12版本源码
git clone https://github.com/odoo/odoo.git -b 12.0 --depth=1 # 获取 Odoo 源码
3.安装odoo所需的依赖
pip3 install -r ~/odoo-dev/odoo/requirements.txt
4.安装odoo其他依赖包
pip3 install num2words phonenumbers psycopg2-binary watchdog xlwt
步骤四
启动odoo服务
~/odoo-dev/odoo/odoo-bin
# 默认监听端口8069 ,访问 http://localhost:8069
步骤五
管理odoo数据库
# 由于采用的是psql数据库,需要手动创建
1.创建PostgreSQL数据库
createdb MyDB
2.创建odoo数据库
createdb --template=MyDB MyDB2
3.删除数据库 # dropdb是不可撤销的,永久性删除
dropdb MyDB2
# 访问odoo客户端 页面会出现以下内容
Database Name:数据库的标识名称,在同一台服务器上可以有多个数据库
Email:管理员的登录用户名,可以不是 email 地址
Password:管理员登录的密码
Language:数据库的默认语言
Country:数据库中公司数据所使用的国家,这个是可选项,与发票和财务等带有本地化特征的应用中会用到
Demo data:勾选此选项会在数据库中创建演示数据,通常在开发和测试环境中可以勾选
## 服务端添加了master password , 要求输入密码,目的是阻止未经授权的管理员操作
odoo其他配置
### 修改监听的端口 , 即可运行多个odoo实例
~/odoo-dev/odoo/odoo-bin --http-port=8070
~/odoo-dev/odoo/odoo-bin --http-port=8071
### 数据库选项
Odoo 开发时,经常会使用多个数据库,有时还会用到不同版本。在同一端口上停止、启动不同服务实例,或在不同数据库间切换,会导致网页客户端会话异常。因为浏览器会存储会话的 Cookie。
# 它接收一个正则表达式来过滤可用数据库名,要精确匹配一个名称,表达式需要以^开头并以$结束
~/odoo-dev/odoo/odoo-bin --db-filter=^testdb$
### 管理服务器日志消息
–log-level=debug参数
参数如下:
debug_sql:查看服务中产生的 SQL 查询
debug_rpc:服务器接收到的请求详情
debug_rpc_answer:服务器发送的响应详情
安装第三方插件
# Odoo应用商店可以下载一系列模块安装到系统中, 为 Odoo 添加模块,仅需将其拷贝到官方插件的 addons 文件夹中即可
例子:
# 1.拷贝library模块
cd ~/odoo-dev
git clone https://github.com/alanhou/odoo12-development.git library
# 2.配置插件(add-ons)路径
"""
Odoo 服务有一个addons_path参数可设置查找插件的路径,默认指向Odoo 服务所运行处的/addons文件夹。我们可以指定多个插件目录,这样就可以把自定义模块放到另一个目录下,无需与官方插件混到一起。
"""
cd ~/odoo-dev/odoo
./odoo-bin -d 12-library --addons-path="../library,./addons"
激活开发者模式
# 方式一 在地址栏添加参数
未添加: http://127.0.0.1:8069/web#
添加: http://127.0.0.1:8069/web?debug=1#
# 方式二
找到设置(Settings),最底下有激活开发者模式选项
三·odoo模块结构
# 原博客地址:
https://alanhou.org/odoo12-first-application/
#### 在odoo开发中,一个应用就相当于是一个模块
分析模块结构
# odoo 的结构
- modelname
- controllers
- __init__.py
- main.py
- i18n #翻译文件
- zh-cn.po # po翻译文件
- data # 数据文件
- book_demo.xml
- library.book.csv
- models # 模型
- __init__.py
- library_book.py
- reports # 报表
- __init__.py
- library_book_report.py
- library_book_report.xml
- security # 安全 权限
- ir.model.access.csv
- library_security.xml
- static # 静态资源
- src
- js
- css
- tests # 测试
- __init__.py
- test_book.py
- views # 展示视图
- book_list_template.xml
- __init__.py
- __manifest__.py # 模块信息描述文件
四·创建应用
添加顶级菜单
菜单项是使用 XML 文件中添加的视图组件,通过创建views/library_menu.xml来定义菜单项:
<?xml version="1.0"?>
<odoo>
<!-- Library App Menu -->
<menuitem id="menu_library" name="Library" />
</odoo>
需要在__manifest__.py
中使用 data 属性来添加安装或更新时需要加载的模块列表
'data': [
'views/library_menu.xml',
],
添加权限组
Odoo 中使用安全组来实现,权限授予组,组中分配用户。Odoo 应用通常有两个组:针对普通用户的用户组,包含额外应用配置权限的管理员组。
权限安全相关的文件通常放在模块下/security子目录中,创建security/library_security.xml 文件来进行权限定义
<?xml version="1.0" ?>
<odoo>
<record id="module_library_category" model="ir.module.category">
<field name="name">Library</field>
</record>
</odoo>
添加两个安全组,首先添加用户组
<?xml version="1.0" ?>
<odoo>
<record id="module_library_category" model="ir.module.category">
<field name="name">Library</field>
</record>
<!-- 加入安全组 Library User Group -->
<record id="library_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_library_category" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
</record>
</odoo>
### 字段说明
name: 组名
category_id:关联应用,这是一个关联字段,因此使用了 ref 属性来通过 XML ID 连接已创建的分类
implied_ids:这是一个one-to-many关联字段,包含一系列组来对组内用户生效。这里使用了一个特殊语法,
创建管理员组,授予用户组的所有权限以及为应用管理员保留的其它权限
<?xml version="1.0" ?>
<odoo>
<record id="module_library_category" model="ir.module.category">
<field name="name">Library</field>
</record>
<!-- 添加用户组 Library User Group -->
<record id="library_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_library_category" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
</record>
<!--创建管理员组 Library Manager Group -->
<record id="library_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_library_category" />
<field name="implied_ids" eval="[(4, ref('library_group_user'))]" />
<field name="users" eval="[
(4, ref('base.user_root')),
(4, ref('base.user_admin'))
]" />
</record>
</odoo>
同样需要在声明文件中添加该 XML 文件:
'data': [
'security/library_security.xml',
'views/library_menu.xml',
],
添加自动化测试
# 1.测试应放在tests/子目录中,在tests/__init__.py
from . import test_book
# 2.在tests/test_book.py文件中添加实际的测试代码:
from odoo.tests.common import TransactionCase
class TestBook(TransactionCase):
def setUp(self, *args, **kwargs):
result = super().setUp(*args, **kwargs)
self.Book = self.env['library.book']
self.book_ode = self.Book.create({
'name': 'Odoo Development Essentials',
'isbn': '879-1-78439-279-6'})
return result
def test_create(self):
"Test Books are active by default"
self.assertEqual(self.book_ode.active, True)
模型层
创建数据模型
1. 在模块主__init__.py文件添加
from . import models
2. 在models/__init__.py文件种引入模型
from . import library_book
# 创建模型 models/library_book.py
from odoo import fields, models
class Book(models.Model):
_name = 'library.book' # 在视图中能够引用到
_description = 'Book' # 模型描述
name = fields.Char('Title', required=True)
isbn = fields.Char('ISBN')
active = fields.Boolean('Active?', default=True)
date_published = fields.Date()
image = fields.Binary('Cover')
publisher_id = fields.Many2one('res.partner', string='Publisher')
author_ids = fields.Many2many('res.partner', string='Authors')
#### odoo数据库基本字段类型
Binary:二进制类型,用于保存图片、视频、文件、附件等,在视图层显示为一个文件上传按钮。【Odoo底层对该类型字段的容量作了限制,最多能容纳20M内容】
Char:字符型,size属性定义字符串长度。
Boolean:布尔型
Float:浮点型,如 rate = fields.float('Relative Change rate',digits=(12,6)), digits定义数字总长和小数部分的位数。
Integer:整型
Date:短日期,年月日,在view层以日历选择框显示。
Datetime:时间戳。
Text:文本型,多用于多行文本框,可以用widget属性为它添加样式。
Html:与text类似,用于多行文本编辑,不过自带编辑器样式,并且会把内容以html解析。
Selection:下拉列表,枚举类型。
#### 关联字段类型
one2one: 一对一关系。 在V5.0以后的版本中不建议使用,而是用many2one替代。
格式:
fields.one2one(关联对象Name, 字段显示名, ... )
many2one: 多对一关系
格式:
fields.many2one(关联对象Name, 字段显示名, ... )
参数:
comodel_name(string) -- 目标模型名称,除非是关联字段否则该参数必选
domain -- 可选,用于在客户端筛选数据的domain表达式
context -- 可选,用于在客户端处理时使用
ondelete -- 当所引用的数据被删除时采取的操作,取值:'set null', 'restrict', 'cascade'
auto_join -- 在搜索该字段时是否自动生成JOIN条件,默认False
delegate -- 设置为True时可以通过当前model访问目标model的字段,与_inherits功能相同
one2many: 一对多关系
格式:
fields.one2many(关联对象Name, 关联字段, 字段显示名, ... )
参数:
comodel_name -- 目标模型名称,
inverse_name -- 在comodel_name 中对应的Many2one字段
domain -- 可选,用于在客户端筛选数据的domain表达式
context -- 可选,用于在客户端处理时使用
auto_join -- 在搜索该字段时是否自动生成JOIN条件,默认False
limit(integer) -- 可选,在读取时限制数量
many2many: 多对多关系
格式: (生成第三表: ...._ref表)
'category_id'=fields.many2many('res.partner.category','res_partner_category_rel','partner_id','category_id','Categories')
# 表示以多对多关系关联到对象res.partner.category,关联表为'res_partner_category_rel',关联字段为 'partner_id'和'category_id'。
# 当定义上述字段时,OpenERP会自动创建关联表为 'res_partner_category_rel',它含有关联字'partner_id'和'category_id'
参数:
comodel_name -- 目标模型名称,除非是关联字段否则该参数必选
relation -- 关联的model在数据库存储的表名,默认采用comodel_name获取数据
column1 -- 与relation表记录相关联的列名
column2 --与relation表记录相关联的列名
domain -- 用于在客户端筛选数据的domain表达式
context -- 用于在客户端处理时使用
limit(integer) --在读取时限制数量
#### 引用类型
related字段
格式:
字段=fields.类型(related="某个字段.类字段",store=true/false)
# related字段可以简记为“带出字段”,由当前模型的某个关联类型字段的某个字段带出值。
reference字段
reference是比related更高级的引用字段,可以指定该字段引用那些模型范围内的模型的哪些字段的值,范围更广。
#### Odoo保留字段
name(Char) -- _rec_name的默认值,在需要用来展示的时候使用
active(Boolean) -- 设置记录的全局可见性,当值为False时通过search和list是获取不到的
sequence(Integer) -- 可修改的排序,可以在列表视图里通过拖拽进行排序
state(Selection) -- 对象的生命周期阶段,通过fileds的states属性使用
parent_id(Many2one) -- 用来对树形结构的记录排序,并激活domain表达式的child_of运算符
parent_left,parent_right -- 与 _parent_store结合使用,提供更好的树形结构数据读取
#### 自动化属性
id Identifier field 是模型中每条记录的唯一数字标识符
_log_access 是否创建日期字段,默认创建(default:True)
create_date 记录创建日期 Type:Datetime
create_uid 第一创建人 Type:res.users
write_date 最后一次修改日期 Type:Datetime
write_uid 最后一次修改人 Type:res.users
_last_update 最后一次修改日期 它不存储在数据库,用于做并发检测
display_name 对外显示的名称
# 不想在model自动添加这些属性 ,在类种添加:
_log_access=False
#### Compute字段
ompute字段不是一种字段类型,而是指某个字段的值是计算出来的。
一个字段的值,可以通过一个函数来动态计算出来。定义格式如下:
字段名=fields.类型(compute="函数名",store=True/false) #store定义了该动态改变的字段值是否保存到数据库表中
@api.depends(依赖的字段值)#depend的字段值一旦发生变化,就会触发该函数,从而更新compute字段值。
def 函数(self):
self.字段=计算字段值
# 原博客https://www.cnblogs.com/ygj0930/p/10826099.html
设置访问权限
添加访问权限控制
权限通过security/ir.model.access.csv文件来实现,添加该文件并加入如下内容:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_user,BookUser,model_library_book,library_group_user,1,0,0,0
access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
注解:
# 1. 应注意该文件第一行后不要留有空格,否则会导致报错
# 2. csv列解读
id 是记录的外部标识符(也称为XML ID),需在模块中唯一
name 是描述性标题,仅在保证唯一时提供有用信息
model_id 是赋权模型的外部标识符,模型有ORM自动生成的XML ID,对于library.book,标识符为model_library_book
group_id 指明授权的安全组,我们给前文创建的安全组授权:library_group_user和library_group_manager
perm_… 字段标记read读, write写, create创建, 或unlink删除权限,我们授予普通用户读权限、管理员所有权限
# 3. __manifest__.py的 data 属性中添加对新文件的引用
'data': [
'security/library_security.xml',
'security/ir.model.access.csv',
'views/library_menu.xml',
],
行级权限规则
添加记录规则,需编辑security/library_security.xml文件
记录规则在ir.rule中定义,和往常一样我们选择一个唯一名称。还应获取操作的模型及使用权限限制的域过滤器。域过滤器使用 Odoo 中常用的元组列表,添加域表达式.
<data noupdate="1">
<record id="book_user_rule" model="ir.rule">
<field name="name">Library Book User Access</field>
<field name="model_id" ref="model_library_book" />
<field name="domain_force">
[('active','=',True)]
</field>
<field name="groups" eval="[(4,ref('library_group_user'))]" />
</record>
</data>
视图层
视图类别:
tree视图,form表单视图,search搜索视图 等
添加菜单项
添加相应菜单项。编辑views/library_menu.xml文件,在 XML 元素中定义菜单项以及执行的操作:
<!-- Action to open the Book list -->
<act_window id="action_library_book"
name="Library Books"
res_model="library.book"
view_mode="tree,form"
/>
<!-- Menu item to open the Book list -->
<menuitem id="menu_library_book"
name="Books"
parent="menu_library"
action="action_library_book"
/>
注释:
1.<act_window> 元素定义客户端窗口操作,按找顺序通过启用列表和表单视图打开library.book模型
2.<menuitem> 定义一个调用 action_library_book操作的定义菜单
# act 是行为动作, menu是菜单. 页面上生成的菜单-->行为动作指向action
创建表单视图
添加views/book_view.xml文件来定义表单视图:
<?xml version="1.0"?>
<odoo>
<record id="view_form_book" model="ir.ui.view">
<field name="name">Book Form</field>
<field name="model">library.book</field>
<field name="arch" type="xml">
<form string="Book">
<group>
<field name="name" />
<field name="author_ids" widget="many2many_tags" />
<field name="publisher_id" />
<field name="date_published" />
<field name="isbn" />
<field name="active" />
<field name="image" widget="image" />
</group>
</form>
</field>
</record>
</odoo>
业务文件表单视图
header 和 sheet将form表单分成 两部分
header部分包含:可操作的button按钮, 状态栏
sheet部分包含: 数据列
<form string="Book">
<header>
<!-- 此处添加按钮 -->
<button name="button_check_isbn" type="object"
string="Check ISBN" />
</header>
<sheet>
<group>
<field name="name" />
...
</group>
</sheet>
</form>
组来组织表单
<group>
标签,用来组织表单视图的布局
推荐在group 元素中添加 name 属性,更易于其它模块对其进行继承
<sheet>
<!-- 分组展示 -->
<group name="group_top">
<!-- 左侧部分展会的字段 -->
<group name="group_left">
<field name="name" />
<field name="author_ids" widget="many2many_tags" />
<field name="publisher_id" />
<field name="date_published" />
</group>
<!-- 右侧部分展示的字段 -->
<group name="group_right">
<field name="isbn" />
<field name="active" />
<field name="image" widget="image" />
</group>
</group>
</sheet>
注释
# 1. ir.ui.view 和record是固定搭配
# 2. name 视图名称 , id 当前视图唯一的xml ID标识符 , model依赖的模型 arch form视图固定搭配
# 3. 视图具有继承视图,即:在原有的视图上添加新的字段,新的展示内容
# 4. __manifest__.py 的 data中声明
'views/book_view.xml',
列表视图
列表视图即 : <tree>
视图 (初代结构)
<record id="view_tree_book" model="ir.ui.view">
<field name="name">Book List</field>
<field name="model">library.book</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="author_ids" widget="many2many_tags" />
<field name="publisher_id" />
<field name="date_published" />
</tree>
</field>
</record>
搜索视图
<search>
搜索视图
<record id="view_search_book" model="ir.ui.view">
<field name="name">Book Filters</field>
<field name="model">library.book</field>
<field name="arch" type="xml">
<search>
<!-- 可搜索的字段值 -->
<field name="publisher_id" />
<!-- domain 过滤条件 -->
<filter name="filter_active"
string="Active"
domain="[('active','=',True)]" />
<filter name="filter_inactive"
string="Inactive"
domain="[('active','=',False)]" />
</search>
</field>
</record>
业务逻辑层
业务逻辑层编写应用的业务规则,如验证和自动计算。
添加业务逻辑
# 模型中有isbn字段, 上文在表单视图中添加了button按钮 name为:button_check_isbn . 现在为其添加上业务校验逻辑
# 在models的 Book 模型中添加 校验逻辑
#1. 检验 isbn有效行
def _check_isbn(self):
self.ensure_one()
isbn = self.isbn.replace('-', '') # 为保持兼容性 Alan 自行添加
digits = [int(x) for x in isbn if x.isdigit()]
if len(digits) == 13:
ponderations = [1, 3] * 6
terms = [a * b for a,b in zip(digits[:12], ponderations)]
remain = sum(terms) % 10
check = 10 - remain if remain !=0 else 0
return digits[-1] == check
# 2. 按钮点击时触发此方法 button_check_isbn 方法与button的name必须同名
from odoo import api, fields, models
from odoo.exceptions import Warning
def button_check_isbn(self):
for book in self:
if not book.isbn:
raise Warning('Please provide an ISBN for %s' % book.name)
if book.isbn and not book._check_isbn():
raise Warning('%s is an invalid ISBN' % book.isbn)
return True
总结:
1.在处理业务逻辑是,添加异常后,odoo自动做事务回滚.
2.业务逻辑一般放在models定义的模型类中 , 本身是由orm去操作的,需要成熟面向对象思想
网页和控制器
在页面上响应数据
# 1. 在library_app/__init__.py导入控制器模块目录加入控制器
from . import models
from . import controllers
# 2.创建main.py,在library_app/controllers/__init__.py文件来让目录可被 Python 导入
from . import main
# 3.定义代码
from odoo import http
class Books(http.Controller):
@http.route('/library/books', auth='user')
def list(self, **kwargs):
Book = http.request.env['library.book']
books = Book.search([])
return http.request.render(
'library_app.book_list_template', {'books':books})
### 注释:
# 1. http.request.env获取环境,从目录中获取有效图书记录集
# 2. http.request.render() 来处理 library_app.index_template Qweb 模板并生成输出 HTML
# 3. auth 制定访问的授权模式
# 4. {'books':books} book图书集合传递到Qweb中
QWeb模板是视图类型
放在/views子目录下,创建views/book_list_template.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="book_list_template" name="Book List">
<div id="wrap" class="container">
<h1>Books</h1>
<t t-foreach="books" t-as="book">
<div class="row">
<span t-field="book.name" />,
<span t-field="book.date_published" />,
<span t-field="book.publisher_id" />
</div>
</t>
</div>
</template>
</odoo>
注释:
1.<template>
2. t-foreach用于遍历变量 books的每一项
3. t-field用于渲染记录字段的内容