odoo脚手架
为了方便开发odoo模块,可以在项目的终端中输入
python odoo-bin scaffold addons myaddons
这样就在项目的myaddons文件夹中生成了名称addons的模块。
odoo规范
odoo开发规范 | 同欣数字化落地 (txodoo.cn)
odoo结构
- controllers: 主要是一些HTTP请求相关的业务逻辑。
- models: 模型文件夹,业务的模型都放在这里。
- security: 权限相关的文件。
- views: 视图文件夹,关于界面布局的逻辑在这里。
- static: 静态文件夹,前端使用的qweb文件和js、css文件和图片等静态文件放到这个文件夹内。
- __init__:熟悉python的都懂,不说了
- __manifest__: 9.0之前叫_openerp_.py,Odoo模块必须的文件,包含必要的模块信息。
模型(model)
odoo模块的模型通常在models/目录下,当新增模型时,需要在models/__init__.py中增加模型名称。
odoo所有的持久化模型都要继承自models.Model类。
在脚手架生成的事例中,_name是这个models的名称,而_description是models的描述,当我们要继承某个models的时候,我们可以用_inherit表示继承。
继承:被继承models会拥有继承的models的所有字段。
字段(fields)
上述例子中,给一个变量初始化定义后被称为字段。
odoo有许多字段:
- 字符串字段(Char):用于存储文本数据,如名称、标题等。
- 整数字段(Integer):用于存储整数值,如计数器、年龄等。
- 浮点数字段(Float):用于存储浮点数值,如价格、百分比等。
- 布尔字段(Boolean):用于存储布尔值(True或False),如开关状态、勾选框等。
- 日期字段(Date):用于存储日期值,如生日、合同日期等。
- 日期时间字段(Datetime):用于存储日期和时间值,如创建时间、更新时间等。
- 二进制字段(Binary):用于存储二进制数据,如图像、文件等。
- 选择字段(Selection):用于从预定义选项中选择一个值,如状态、类型等。
- 文本字段(Text):用于存储多行文本数据,如描述、备注等。
- HTML字段(Html):用于存储富文本数据,支持HTML格式,如富文本公告、邮件正文等。
- 关系字段(Many2one、One2many、Many2many):用于建立与其他模型之间的关联关系,如产品与供应商的关系、订单与产品的关系等。
方法
模型中存在方法,可以进行较为复杂数据处理。
常见的方法有:
- create(self, values):用于创建新记录,接收一个字典参数values,包含要创建记录的字段值。
- write(self, values):用于更新现有记录,接收一个字典参数values,包含要更新记录的字段值。
- unlink(self):用于删除记录。
- read(self, fields=None):用于读取记录的字段值,可以指定要读取的字段列表。
- search(self, domain=None, offset=0, limit=None, order=None):用于根据条件搜索符合要求的记录。
- browse(self, ids):用于获取指定记录ID的记录对象。
- copy(self, default=None):用于复制记录,可以指定默认值来更改复制记录的字段值。
- onchange(self, field_name):用于在字段值发生变化时触发的方法,可以在该方法中处理其他字段的变化或执行其他操作。
- compute(self):用于计算字段的值,通过该方法可以定义计算字段的逻辑。
- action_xxx(self):用于执行自定义的动作,其中"xxx"表示动作的名称,可以根据需要定义不同的动作方法。
- button_xxx(self):用于执行与按钮相关的操作,通常用于处理用户交互。
- _get_xxx(self):用于获取计算字段的值,其中"xxx"表示计算字段的名称。
- _set_xxx(self, value):用于设置计算字段的值,其中"xxx"表示计算字段的名称。
注解
如上述图中的方法上面的称为注解。
注解一般有以下几种:
- @api.one:用于标记实例方法,表示该方法将在单个记录上执行。
- @api.depends('field_name'):用于指定计算字段(computed field)所依赖的字段。当被依赖的字段发生变化时,计算字段将被重新计算。
- @api.onchange('field_name'):用于标记字段变化触发的方法。当指定的字段值发生变化时,该方法将被自动调用。
- @api.constrains('field_name'):用于标记约束方法,用于执行字段级别的约束检查。
- @api.constraints('constraint_name'):用于标记约束方法,并指定约束名称。可以在模型中定义多个约束方法。
- @api.model_create_multi:用于标记创建多个记录的方法。通常用于批量创建记录的情况。
- @api.model_create_single:用于标记创建单个记录的方法。
- @api.returns('model_name'):用于指定方法的返回类型,其中'model_name'表示返回模型的名称。
视图(view)
odoo模块下的视图文件通常在view/目录下,当新增视图文件时,需要在__manifast__.py的'data'中添加。
视图文件有先后顺序,请务必检查,否则会报错。
动作视图(action)
在系统上每一个交互就是一个动作,动作视图是交互时的一个引导。
图中定义了addons.action_window的动作视图,模型是addons.addons,包含的视图为tree,form视图。
列表视图(tree)
用于以表格形式显示多个记录,每行代表一个记录,可以选择显示的字段和排序方式。
例
表单视图(form)
用于显示单个记录的详细信息,通常包含各个字段和相关操作按钮。
例
搜索视图(search)
用于定义搜索记录的条件和过滤器,用户可以根据特定条件搜索符合要求的记录。
例
报表视图(report)
用于生成和显示打印或导出的报表,可以基于模板定义报表的结构和样式。一般将报表视图存放在report/目录下。
看板视图(kanban)
用于以卡片形式显示记录,每个卡片代表一个记录,可以根据状态进行分组和拖放操作。
例
菜单(menuitem)
菜单是显示模块的入口。
例
小部件(widget)
url
Char类型
radio
selection类型
statusbar
Selection类型
upgrade_boolean
Boolean类型
statinfo
int,float类型
res_partner_many2one
many2one (res.partner)类型
section_and_note_one2many
one2many类型
handle
integer类型
section_and_note_one2many
char,text类型
many2manyattendee
many2many类型
many2many_checkboxes
many2many类型
many2many_tags_avatar
many2many类型
float_time
显示HH:MM的时间格式
float类型
one2many
one2many类型
website_publish_button
boolean类型
priority
selection类型
char类型
phone
char类型
selection
many2one类型
website_redirect_button
boolean类型
image
binary类型
boolean_toggle
boolean类型
state_selection
selection类型
field_float_rating
float类型
reference
reference类型(显示引用字段的链接)
web_ribbon
13版本新增的这个widget可以自定义文本、背景颜色等,可以通过属性:bg primary、bg secondary、bg success、bg danger、bg warning、bg info、bg light、bg dark、bg white等设置
date
datetime类型(只显示日期不显示时间)
kanban_activity
many2many类型(activity_ids)
在看板视图中显示活动按钮
label_selection
在看板上显示值标签
selection类型
activity_exception
char类型
kanban_state_selection
在看板视图修改选择值
selection类型
many2many_tags
many2many类型
many2one_avatar_user
many2one类型
badge
selection类型
char_emojis, text_emojis
char、text类型
remaining_day
datetime类型
Countdown
在网站设计里可以拖拽实现
color_picker
integer类型
kanban_activity
这个widget依赖于 mail.activity的one2many关系字段, 示例代码如下
<field name="activity_ids" widget="kanban_activity" optional="show"/>
many2one_avatar_employee
many2one类型
依赖于hr.employee的关系字段
product_discount
float类型
account_resequence_widget
text类型
grouped_view_widget
text类型
task_with_hours
many2one类型
timesheet_uom_no_toggle
float类型
forecast_widget
float类型
stock_rescheduling_popover
char类型
mrp_should_consume
float类型
mrp_workorder_popover
char类型
percentage
float类型
popover_widget
char类型
account_hierarchy_selection
many2one类型
boolean_favorite
boolean类型
kanban_vat_activity
boolean类型
many2one_barcode
many2one类型
text
text、char类型
domain
char类型
many2manyattendee
关联res.partner的many2many类型
many2many_tags_avatar
关联res.partner的many2many类型
website_urls
Many2many类型
小部件开发
向导(wizard)
向导模式是通过动态的表单与用户进行交互的方式。一个向导的实体不再从 Model 扩展,而是从 TransientModel 扩展。 TransientModel 类扩展了 Model ,除了重用所有已经存在的机制外,扩展了以下特性:
- 向导实体的数据记录并不被持久化;经过一段时间,这些记录将被自动删除。这也是为什么它们称为 transient 的原因。
- 向导实体不需要显式的访问权限:用户拥有向导实体数据记录的所有权限。
- 向导实体可以引用常规的数据记录,或者通过 many2one 字段与其他向导实体关联,但是常规的数据记录 不能 引用向导实体的数据记录。
第一个模块
定义模型
odoo开发的第一步就是要先建立自己的数据模型,也就是我们常说的models,模型对应的是数据库中的数据表,但是模型的概念比表更多,很多属性并不存储在数据库中。现在,我们先建一个基础的图书信息,假设目前我们的图书只采集了如下几个属性:
- 名称
- 作者
- 日期
- 定价
名称是字符串,因此使用Char类型,作者这里先设置为Char类型,后期会更改为Many2one类型,日期是Datetime,定价就是Float。因此,我们的模型可以写成下面这样:
class Book(models.Model):
_name = 'book_store.book'
name = fields.Char('名称', help='书名')
author = fields.Char('作者', help='作者')
date = fields.Date("出版日期", help="日期")
price = fields.Float("定价", help="定价")
odoo所有的持久化模型都要继承自models.Model类,当然也存在于另外一种非持久化的类,这个等到后面再详细介绍。这里,我们先简单记住,所有的要在数据库中建立表结构的对象,都要继承自models.Model类。
定义视图文件
odoo能够快速开发应用的一个很重要的原因就是它封装了很多常见的操作,像创建、编辑、搜索和删除等,默认情况下,我们不用重写这些逻辑就可以使用,甚至不用写视图文件。但是为了说明起见,我们这里还是要编写一些视图文件。
编辑views文件夹下的view.xml文件,添加如下代码:
<odoo>
<data>
<!-- explicit list view definition -->
<record model="ir.ui.view" id="book_store.list">
<field name="name">图书列表</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="author"/>
<field name="date"/>
<field name="price"/>
</tree>
</field>
</record>
<record id="book_store.book_form" model="ir.ui.view">
<field name="name">图书</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<form string="图书详情">
<sheet>
<h1>
<field name="name"/>
</h1>
<group>
<field name="author"/>
<field name="date"/>
<field name="price"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- actions opening views on models -->
<record model="ir.actions.act_window" id="book_store.action_window">
<field name="name">图书</field>
<field name="res_model">book_store.book</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Top menu item -->
<menuitem name="西西弗斯书店" id="book_store.menu_root"/>
<!-- menu categories -->
<menuitem name="书店" id="book_store.menu_book" action="book_store.action_window" parent="book_store.menu_root"/>
<menuitem name="图书" id="book_store.menu_book" action="book_store.action_window" parent="book_store.menu_root"/>
</data>
</odoo>
加载更改后的模块
XML文件写完以后,在应用-模块中点击升级模块对模块进行升级。
当修改了py文件时,需要重启odoo进程,而修改了xml等静态文件则需要升级模块。
升级完成后,进入odoo发现没有我们写完的menu。这是因为,我们只创建了model却没有给任何一个组赋予访问这个模型的权限,因此我们无权查看这个模块的界面。
添加模型的访问权限
默认情况下,我们不拥有对新创建的对象的访问权限,因此我们需要在安装模块时,给指定的用户添加访问权限。odoo提供的方式之一,就是按照一定的格式编写一个csv文件,声明在manifest文件中。这样当升级模块时,odoo就能够读取csv文件中的权限设置,并写入到数据库中。
给book模型添加权限,编辑security文件夹中的ir.model.access.csv文件,csv文件从左右到依次是,id,名称,模型id,组ID,读权限,写权限,创建权限、删除权限。
其中模型是model_加上模型名组成的,其中的点号要换成下划线。
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_store_book,book_store.book,model_book_store_book_store,base.group_user,1,1,1,1
- id: 该权限记录的唯一标示
- name: 访问权限的名字
- model_id:id: 模型名称,格式为 模块名.模型名
- group_id:id: 设置的用户组,格式为 模块名.组xml_id
- perm_read: 对应访问权限
- perm_write: 对应编辑权限
- perm_create: 对应创建权限
- perm_unlink: 对应删除权限
后四列,当为1时表示拥有这个权限,0代表没有权限。
设置完权限,我们重新升级模块,就可以看见我们自定义的菜单出来了:
点击菜单我们可以创建图书:
查看图书列表:
视图开发
<record id="book_store.book_form" model="ir.ui.view">
<field name="name">图书</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<form string="图书详情">
<header>
<button name="button_create" type="object" string="随机添加一个作者" class="oe_highlight"/>
<button name="button_update" type="object" string="给作者们加颗❤️" class="oe_highlight"/>
<button name="button_delete" type="object" string="删除一个作者" class="oe_highlight"/>
<button name="button_clear" type="object" string="删除所有作者" class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<label for="name" string="名称" class="oe_edit_only"/>
<field name="name"/>
</h1>
</div>
<group>
<field name="authors" widget="many2many_tags"/>
<field name="date"/>
<field name="price"/>
</group>
</sheet>
</form>
</field>
</record>
在odoo中,所有的预定义的数据结构都放在record节点中,id是这些数据的唯一标识,叫做xml_id,xml_id在模块内部要保持唯一, 当模块安装以后, xml_id在系统中的唯一标识是模块名.xml_id。xml_id在开发过程中是一个很重要的概念, 这个等后面使用的时候会详细讲到。
model指明本记录所属的对象模型名称,odoo中的视图模型是ir.ui.view,因此这里的值也就是ir.ui.view。
record的子节点,由若干个field节点组成。
- name: 代表的是模型的字段,值根据字段类型的不同,设置方式也不同。本例中,我们给视图命名"图书"。
- model: 视图所属的模型名称,这里我们用到的是自己定义的图书模型,因此写的是book_store.book。
- arch: 视图的布局结构,属性type为固定值xml
每种视图的arch结构都不一样, 对于表单视图来说:
- header: 表头,通常放置按钮和状态栏
- footer:底部,可以放置按钮,也可以放置一些需要的小部件。
- sheet:页面主体。
以原生的销售单部分来说,1 部分即是header 2 是sheet 3是footer。
header
Header的作用通常用来放置一些按钮、动作和状态栏。如上图所示的1部分。
Header的位置通常位于form节点下的第一个。
<form>
<header>
...
</header>
</form>
Sheet
sheet节点虽然不是form表单的必须组成部分,但却是最常用的部分,我们见到的大多数页面都是用到了sheet节点。
Chatter
我们常见的Form表单都有一个类似留言板的讨论区。 这个讨论区的使用方法:
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
想要使用这个功能的模型必须要继承mail.thread对象。
分组
在表单视图中使用group标签,可以将多个field分组排列并自动生成字段相对应的标签label。
<group>
<field name="serial"/>
<field name="authors" color="red" widget="many2many_tags"/>
<field name="date"/>
<field name="price"/>
<field name="img"/>
</group>
字段
表单视图中使用最多的语法是字段的布局:
<field name="field1" string="description" class="..." attrs="{...}"/>
字段布局的string属性不是必须的, 如果没有明确声明, 那么系统将默认使用字段的name属性作为它的string属性的值.
如果字段的布局中使用了nolabel属性并设置为True, 那么该字段在显示的时候将缺省描述标签label.
odoo17.0 取消了attrs的属性 使用属性直接代替了。
tree视图
把表单视图代码中的form替换成tree就成了我们的列表视图,这里称之为列表视图是因为tree视图并不是真正的"树视图",真正的树视图是可以展开的。
<record model="ir.ui.view" id="book_store.list">
<field name="name">图书列表</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="author"/>
<field name="date"/>
<field name="price"/>
</tree>
</field>
</record>
tree视图相对比较简单,一般就是对本对象要显示的属性的罗列。
列表视图的可编辑属性
默认情况下,列表视图是不可以被编辑的,只能单击进入表单视图进行编辑。这种行为可以通过tree的editable属性进行改变。
<record model="ir.ui.view" id="book_store.list">
<field name="name">图书列表</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="name"/>
<field name="author"/>
<field name="date"/>
<field name="price"/>
</tree>
</field>
</record>
editable的可选值有两个:top和bottom。top是默认值,即不可编辑模式。bottom即可以编辑模式。
使用bottom的效果如下图:
Tree视图的字段的可见性
如果我们想要Tree视图中的某一列不可见,那么我们可以使用invisible属性将其设置为不可见.
<field name="name" invisible="1">
17.0+ 列表视图已不能再使用invisible属性,而应该使用column_invisble。
隐藏或显示属性
13.0+版本后列表视图支持字段的隐藏或显示。设置方法:
<field name="name" optional="hide">
<field name="name" optional="show">
xpath中的position操作attributes对optional属性无效。
设置颜色
我们可以根据不同的情况对Tree的行或者某一列进行颜色设置。
例如,我们想要给列表视图中name字段设置红色,那么我们的代码可以这样写:
<tree>
<field name="name" decoration-danger="red:1">
</tree>
而如果我们希望整个一行都变成红色,那么可以使用下面的方法:
<tree decoration-danger="red:1">
<field name="name">
...
</tree>
- decoration-bf: 粗体
- decoration-it: 斜体
- decoration-danger: 红色
- decoration-info: 蓝色
- decoration-muted: 灰色
- decoration-primary: 深绿色
- decoration-success: 浅绿色
- decoration-warnin: 黄色
Seach视图
search视图的基本写法:
<search>
...
</serach>
字段过滤
如果想要在搜索菜单中添加搜索字段,那么只需要在field列表中添加即可。
<field name="name">
字段筛选器
如果我们希望把常用的搜索固定在搜索面板中,那么我们可以使用filter属性:
<filter name="name" domain="[('type','=','sale')]"/>
字段分组
如果想要对字段进行分组,同样适用filter属性,不同的是需要使用context来对字段进行分组。
<filter name="name" string="Name" context="{'group_by':'name'}">
多条件搜索
如果我们希望在多个字段中对某个输入值进行匹配,那么我们可以使用filter_domain属性这种方式:
<fitler name="Order" string="Order" filter_domain="[('name','ilike',self),('serie_name','ilike',self)]">
明细行搜索
对于X2Many类型的字段来说也可以进行搜索,方式同filter_domain。可以参考销售订单的明细搜索:
<field name="order_line" string="Product" filter_domain="[('order_line.product_id', 'ilike', self)]"/>
kanban视图
看板视图是一种可以在面板上显示诸多信息的一种视图结构,是odoo最常见的几种视图结构之一。
看板视图的要求在kanban节点内将用到的字段列出来, 看板的布局使用QWEB技术在templates标签内完成。
我们来给图书(book_store.book)添加看板视图:
<record id="book_kanban" model="ir.ui.view">
<field name="name">图书看板视图</field>
<field name="model">book_store.book</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<field name="authors"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_record_top mb16">
<strong class="o_kanban_record_title">
<span>
<t t-esc="record.name.value"/>
</span>
</strong>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left text-muted">
<field name="authors" widget="many2many_tags"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
其效果如下:
报表引擎
odoo的报表引擎使用的是QWeb,QWeb是odoo自己开发的一套模板渲染引擎。
创建报表
下面介绍一下编写报表的步骤
定义报表动作
首先,我们需要定义一个报表:
<record model="ir.actions.report" id="sale_tag_report.report">
<field name="name">标签打印</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">sale_tag_report.tag</field>
<field name="print_report_name">(object.name)</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
</record>
报表定义有简化的写法:
<report id="report_product_packaging"
string="Product Packaging (PDF)"
model="product.packaging"
report_type="qweb-pdf"
name="product.report_packagingbarcode"
file="product.report_packagingbarcode"
print_report_name="'Products packaging - %s' % (object.name)"/>
这在系统中将会生成一条记录(系统设置-技术-报表):
其中,binding_model_id决定了该按钮绑定在那个模型上面显示。
绘制报表页面
Odoo的报表页面想要自己画还是比较繁琐的,通常我们可以参考既有的报表格式进行参照修改。
看一个我自己写的报表页面:
<template id="sale_tag_report.tag" name="sale_tag_report.tag">
<t t-call="web.html_container">
<div class="article o_report_layout_clean">
<t t-raw="0"/>
<t t-foreach="docs" t-as="doc">
<t t-foreach="doc.order_line" t-as="line">
<div class="page" style="page-break-before: always;">
<h1>
<t t-esc="line.sale_collection"/>
</h1>
<div>
<strong>面料号:</strong>
<p t-field="line.product_id.name"/>
</div>
<div>
<strong>数量:</strong>
<p t-field="line.product_uom_qty"/>
</div>
<div>
<strong>客户名称:</strong>
<p t-field="doc.partner_id.name"/>
</div>
<div>
<strong>备注:</strong>
<p t-field="line.client_description"/>
</div>
</div>
</t>
</t>
</div>
</t>
</template>
- web.html_container: 是我们编写报表最外层的嵌套容器,直接引用即可。
- docs:报表默认会将我们绑定的模型当前记录赋值给变量docs,我们在编写报表时可以直接引用
- style="page-break-before: always;" : 将页面分隔,每页一条记录。
这样就完成了一个简单的报表编写过程。关于报表,其实还有纸张格式、变量赋值等更多更深的内容,后边会讲到。
修改报表
对于我们需要修改的报表,可以在设置中-动作-报表中根据模型找到对应的模板文件。
由上图中的按钮Qweb视图可以找到对应的Qweb代码文件,我们可以通过修改Qweb代码的方式来修改报表。
修改报表字体
想要修改PDF的报表字体,只需要在div的样式表中添加如下的样式:
<div class="font-size: 35px !important;">
</div>
在报表中添加条码
odoo中内置了在报表中添加条码的功能,原理很简单,即使用img标签,将带有条码的URL链接添加到img的url属性中。
@http.route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="public")
def report_barcode(self, type, value, width=600, height=100, humanreadable=0, quiet=1):
pass
从源代码层面上分析可以得出,条码的引用接受6个参数:
- type: 可选的参数有 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39',
'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', 'UPCA', 'USPS_4State'
- value: 生成的条码内容
- width: 条码宽度
- height: 条码高度
- humanreadable: 0 或 1,是否在条码中添加可读性文本
- quiet: 0 或 1,是否添加页面留白
例如:
<td>
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s&humanreadable=1' % ('Code128', object.quantity, 600, 50)" style="width:100%;height:4rem" alt="Barcode"/>
</td>
报表中的特殊字符
xml中不能出现特殊字符,像&、>,< 等符号需要转义:
字符 | 转移字符 |
---|---|
& | \& amp; |
< | \& lt; |
> | \& gt; |
" | \& quot; |
' | \& apos; |
&和amp;中间没有空格
报表中日期字段的格式化
如果想要把模型中的datetime类型的字段格式化成date类型,那么可以使用t-options选项。
例子:
<span t-field="o.date_invoice" t-options='{"format": "MM/dd/yyyy"}'/>
报表样式文件
我们可以自己定义报表的样式,但是不是像普通的后台样式文件一样的继承方式,而是需要继承自web.report_assets_common。