Odoo技术补充
脚手架
# 创建一个新的空白模块
./odoo-bin scaffold my_modul
模型补充:
1. 防止递归调用函数:_check_recursion
if not self._check_recursion():
raise models.ValidationError('Error! You cannot create recursive categories.')
2. 模型自关联
parent_id = fields.Many2one(
'library.book.category',
string='Parent Category',
ondelete='restrict',
index=True)
child_ids = fields.One2many( # 自关联
'library.book.category', 'parent_id',
string='Child Categories')
- AbstractModel 抽象模型 : 不会被安装到数据库中,不能存储任何数据
class BaseArchive(models.AbstractModel):
_name = 'base.archive'
active = fields.Boolean(default=True)
def do_archive(self):
for record in self:
record.active = not record.active
其他模型类型
class TransientModel(Model): # 临时模型
class Model(AbstractModel): # 基础模型
AbstractModel = BaseModel
class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
用户错误
# from odoo.exceptions import UserError
# UserError
UserError(_('Moving from to is not allowed') )
# from odoo.tools.translate import _
_(),它由odoo.tools.translate定义。这个函数用于标记字符串为可翻译
元组 | 效果 |
---|---|
(0, 0, dict_val) | 它会新建一条与主记录关联的记录。 |
(1, id, dict_val) | 它会以所提供值更新所指定 ID 的关联记录。 |
(2, id) | 它会从关联记录中删除指定 ID 的记录并在数据库中删除它。 |
(3, id) | 它会从关联记录中删除指定 ID 的记录。记录在数据库中并不会被删除。 |
(4, id) | 它会向关联记录列表添加给定 ID 的已有记录。 |
(5, ) | 它会删除所有关联记录,等价于对每个关联 id 调用(3, id) 。 |
(6, 0, id_list) | 它会创建所更新记录与已有记录间的关联,它们的 ID 在 Python 列表中名为 id_list。 |
字段
Reference # 引用字段
Monetary # 存储币种的数量值 。 目的:维护多个单子的币种,
增改方法
# self.env[model_name] # 返回空的记录集合
# cr属性 传递原生SQL查询的数据库游标 , 执行SQL语句
# user属性,调用当前调用的用户
# context属性,上下文字典
# create 创建记录集,
self.env['library.book.category'].create([categ1, categ2])
# 更新记录集
self.update({
'date_updated': fields.Datetime.now(),
'another_field': 'value'
})
合并记录集
# 合并记录集合 +
result = recordset1 + recordset2
# 合并两个记录集并确保结果中没有重复内容
result = recordset1 | recordset2
# 交集 两个记录集中共同的记录
result = recordset1 & recordset2
其他运算符
运算符 | 执行操作 |
---|---|
R1 + R2 | 它返回一个包含 R1中记录的新记录集,后面跟 R2中的记录。这可能会产生记录集中的重复记录 |
R1 - R2 | 它返回一个包含 R1中记录但不包含 R2中记录的新记录集。保留原有排序。 |
R1 & R2 | 它返回一个既属于 R1又属于 R2的记录的新记录集(记录集的交集)。不保留原有排序。 |
R1 | R2 | 它返回一个或属于 R1或属于 R2的记录的新记录集(记录集的并集)。不保留原有排序,且没有重复值。 |
R1 == R2 | 如果两个记录集中包含相同的记录则返回 True。 |
R1 <= R2 R1 in R2 | 如果 R1的记录也存在于 R2中返回 True。两种语法异曲同工。 |
R1 >= R2 R2 in R1 | 如果 R2的记录也存在于 R1中返回 True。两种语法异曲同工。 |
R1 != R2 | 如果R1和 R2不包含相同记录返回 True。 |
搜索方法 search
search_count(domain) # 返回记录的数量
# domain
domain = [
'|',
'&', ('name', 'ilike', 'Book Name'),
('category_id.name', 'ilike', 'Category Name'),
'&', ('name', 'ilike', 'Book Name 2'),
('category_id.name', 'ilike', 'Category Name 2')
]
domain 的关键字参数
offset =N ,跳过前N条
limit =N,限制返回的条数
order = sort_specification 强制返回的记录集合进行排序。
count=Boolean ,返回的是记录数
过滤 filter
filter()是在内存中进行运算。
all_books=self.env['模型'].search([]) # 查询到所有记录集合
# 可以加 匿名函数
all_books.filter(lambda b: len(b.author_ids) > 1)
# 可以加 方法
def predicate(book):
pass
all_books.filter(predicate) #
mapped() 方法来遍历记录集关联
### 解释:生成一个包含该字段所关联当前记录的所有元素集合记录。 也在内存中记性操作
# author_ids 是关联到res.user模型的字段.
books.mapped('author_ids.name')
sorted(key='字段',reverse=True) 排序记录集
# 默认是 正序,reverse是逆序
# key不传参时,默认使用的是模型中的_order属性
books.sorted(key='release_date')
write 防止递归
# super().wriee() 后 ,任何修改操作都会出发write函数,造成无限递归
# 规避方法: 添加上下文标 来标记。
if self.env.context.get('MyModelLoopBreaker'):
return
self= self.with_context(MyModelLoopBreaker='True')
read_group() 获取组中的数据
read_group(domain,fields,groupby,offset,limit,orderby,lazy) # 分组 + 聚合
### 参数详解;
domain:用于为分组过滤记录
fields:传递你希望获取分组数据的字段名称。
groupby:这个参数接收一个字段描述列表。记录将根据这些字段分组。
offset:表示可以跳过可选记录数量
limit:表示可选的返回记录最大数量
orderby:如果传递了该选项,结果会根据给定字段进行排序
lazy:它接收布尔值,并且默认值为True。如果传递了True,结果仅通过第一个groupby进行分组,剩余的groupby会被放到__context键中。若为False,所有的groupby在一次调用中完成。
代码中访问 XML ID记录
# self.env.ref() 函数访问XML ID记录
完整XML ID的示例 : <module_name>.<record_id>
# 读取动作XML视图
action = self.env.ref('mrp_plm.XXXXXX').read()[0]
data数据加载
### eval 和 ref
# 针对 many2one 和many2many 需要使用eval 属性。
<field eval="[(6,0,[ref('xxxxxxx')])]" name="tag_ids"/>
# 最简单的一种是使用ref属性
<field name="chart_template_id" ref="xxxxxx"/>
# 应避免在你的XML文件中使用这 : (1, id, {‘key’: value}) 语法
### noupdate forcecreate
noupdate : 由第一次读取所包含的数据记录创建 , 用户跟新模块时,删除的数据会再次被加载
forcecreate : 更新模块时,不会加载这条数据
- 添加在安装时会创建但后续更新不同更新的出版商。但在用户删除它时会被重新创建:
<odoo noupdate="1">
<record id="res_partner_packt" model="res.partner">
<field name="name">Packt publishing</field>
<field name="city">Birmingham</field>
<field name="country_id" ref="base.uk"/>
</record>
</odoo>
- 添加一个在插件更新时不会修改且用户删除后不会重建的图书分类:
<odoo noupdate="1">
<record id="book_category_all"
model="library.book.category"
forcecreate="false">
<field name="name">All books</field>
</record>
</odoo>
插件的更新和数据迁移
# migrations/12.0.1.0.1/pre-migrate.py
# 迁移函数,执行数据命令
def migrate(cr, version):
cr.execute('ALTER TABLE library_book RENAME COLUMN date_release TO date_release_char')
XML文件中调用函数
# 模型定义函数
@api.model
def _update_book_price(self):
all_books = self.search([])
for book in all_books:
book.cost_price += 10
# 视图调用函数
<function model="library.book" name="_update_book_price"/>
#### 视图 向 模型中的函数传参
<function model="library.book" name="update_book_price" eval="(ref('category_xml_id'), 20)"/>
# 需要 有参数去接受
@api.model
def update_book_price(self, category, amount_to_increase):
'''
category : category_xml_id
amount_to_increase : 20
'''
category_books = self.search([('category_id', '=', category.id)])
for book in category_books:
book.cost_price += amount_to_increase
测试CIYML脚本
CI会在GITHUB上注册一个钩子。 对仓库的push 和pull 都会出发CI构建。
拉去请求基于临时合并构建来确保合并后的分之能通过测试
# .travis.yml
language: python
sudo: false
cache:
apt: true
directories:
- $HOME/.cache/pip
python:
- "3.5"
addons:
apt:
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- python-simplejson
- python-serial
- python-yaml
virtualenv:
system_site_packages: true
env:
global:
- VERSION="12.0" TESTS="0" LINT_CHECK="0"
matrix:
- LINT_CHECK="1"
- TESTS="1" ODOO_REPO="odoo/odoo"
- TESTS="1" ODOO_REPO="OCA/OCB"
install:
- git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-qualitytools/travis:${PATH}
- travis_install_nightly
script:
- travis_run_tests
after_success:
- travis_after_tests_success
# 参数解释
addons : 与Odoo的插件模块毫无关系。用于要求Travis安装一些用于在测试环境里分发包的Ubuntu包
env : 这个版本定义环境变量和构建矩阵。
- version:测试的odoo版本
- lint_check :在构建中使用0来取消flake8或Pylint测试,1则为启用。
- tests : 对无需运行模块测试的构建使用0,否则使用1.
- odoo_repo : 在TESTS为1时这是用于测试的Odoo的GitHub仓库
install : 在构建环境中下载 maintainer-quality-tools并调用travis_install_nightly工具,用于在Travis中设置Odoo
script : 从maintainer-quality-tools中调用travis_run_tests。该脚本负责从构建矩阵中检查环境变量并执行相应的动作。
after_success : 在script部分中的测试成功运行后,会运行travis_after_test_success脚本。在本节的上下文中,该脚本会使用https://coveralls.io查看模块的测试范围并在构建中生成报告。
sudo()方法
# 跳过 权限组和权限规则 进行操作
# 如: 普通用户可以更改状态等操作
#1. sudo() 会创建出一个新的环境的记录集。 与当前用户self环境是不同的
# 2. sudo() 会关联到 Odoo的超级用户Administrator的环境中。
# 危险: 多公司时
1. 环境中新建的记录会与公司的超级用户相关联。
2. 该环境的搜索记录会与数据库中的当前公司相关联,这表示你可能会向真实用户泄漏信息,更糟糕的是你可能通过将记录关联到不同的公司而默默地导致数据库的损坏
# 注意:
# 使用 sudo()时,确认search函数的调用不依赖标准记录规则来过滤结果。确保create的调用不依赖当前用户的company_id来进行计算
# sudo 会创建新的环境,尽量避免在循环中使用
修改上下文with_context()
对象.with_context(上下文字段=值)
# with_context() 会覆盖当前上下文。 推荐写法
new_context=self.env.context.copy() # copy 拷贝 一份新的上下文环境
new_context.update({'字段':'值'}) # 跟新上下文内容
book_with_different_context = self.book_id.with_context(new_context) #
book_with_different_context.make_lost()
# 注意: 避免循环中使用,with_context会创建新的实例环境
执行原声SQL语句
sql_query='SQL语句'
# 执行 SQL
self.env.cr.execute(sql_query) # execute()执行sql语句 , cr 相当于游标。查询并保存查询的记录
result = self.env.cr.fatchall() # 查询所有数据
# execute(query, params):通过元组中参数值替换查询中标记为%s的参数来执行SQL查询。
# fetchone() 查询一条,元祖返回
# fetchall() 查询所有 ,元祖列表形式返回
# fetchalldict() 以列明对值的映射字典列表返回数据控中所有的行
# 注意:防止SQL注入
向导
# models.TransientModel 临时模型
# 1. 数据库中会定时删除,数据表不会随时间不停而增大
# 2. 任何都可以创建记录, 但创建该记录的人只能读取对应的记录。
# 3. 不能引用One2Many字段。 One2Many是要被持久化的字段
优化向导方法
# 1. 使用上下文来计算默认值
def _default_member(self):
if self.context.get('active_model') == 'res.partner':
return self.context.get('active_id',False)
# 2. 向导和代码复用
代码复用
# 3. 重定向用户 BaseModel中定义 : get_formview_action()方法
def add_book_rents(self):
rentModel = self.env['library.book.rent']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
'borrower_id': wiz.borrower_id.id,
'book_id': book.id
})
members = self.mapped('borrower_id')
action = members.get_formview_action() # 当前 对象获取 form视图
if len(borrowers.ids) > 1:
action['domain'] = [('id', 'in', tuple(members.ids))]
action['view_mode'] = 'tree,form'
return action
使用上下文来计算默认值
键 | 值 |
---|---|
active_model | 这是与动作相关联的模型名。通常为在屏幕上所展示的模型。 |
active_id | 这表明单条记录是活跃的并且提供了该记录的 ID。 |
active_ids | 如果选择了多条记录,这将是带有 ID 的列表。这在树状视图中选择多项并触发动作时发生。在表单视图中,所获取的为 active_id。 |
active_domain | 这是向导将要操作的其它域。 |
onchange
@api.onchange('member_id')
def onchange_memeber(self):
result={}
return result
# 返回值
# waring : 值 为一个字典,包含title 和message
result['warning'] = {
'title': 'Late books',
'message': message + '\n'.join(titles)
}
# domain : 值 为一个字典,字段作为key,域作为value
result = {
'domain': {
'book_ids': [ ('id', 'in', self.book_ids.ids)]
}
}
# 注意: onchange值针对内存中的数据。做临时计算 NEWID
### 向导调用onchange(values,['borrwer_id',specs])
# 参数 values 对记录设置的值的列表
# field_name 希望触发onchange的字段列表,可以传递一个空的列表。ORM会使用values值中定义的字段
# field_onchange 计算onchange详情 ,返回一个字典包含{value: 新计算字段的字典,包含传递给onchangevalues参数的键,Many2one会映射到元组,waring:,domain:}
_onchange_spec() # 为向导获取 onchange的详情
specs = wizard._onchange_spec() # 不传递参数,获得由另一个字段的变更所触发的更新
values={
'borrower_id':self.env.user.partner_id.id,
}
updates = wizard.onchange(values, ['borrower_id'], specs)
基于SQL视图定义模型
# init() 方法来创建视图
class LibraryBookRentStatistics(models.Model):
_name = 'library.book.rent.statistics'
_auto = False # 告知 odoo 由我们自己管理这张表
@api.model_cr
def init(self): # 在 _auto_init() 之后调用 ,来创建新的SQL视图
tools.drop_view_if_exists(self.env.cr, self._table)
query = """
CREATE OR REPLACE VIEW library_book_rent_statistics AS
(
SELECT
min(lbr.id) as id,
lbr.book_id as book_id,
count(lbr.id) as rent_count,
avg((EXTRACT(epoch from age(return_date,
rent_date)) / 86400))::int as average_occupation
FROM
library_book_rent AS lbr
JOIN
library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = 'returned'
GROUP BY lbr.book_id
);
"""
self.env.cr.execute(query)
添加自定义设置选项
# res.config.settings 设置模型
# 1. 定义一个组
<record id="group_self_borrow" model="res.groups">
<field name="name">Self borrow</field>
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
</record>
# 2. 继承 模型 res.config.settings
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_self_borrow = fields.Boolean(string="Self borrow", implied_group='my_library.group_self_borrow')
# implied_group 指的是我们写的用户组,只有这个设置字段的值为1时,就是被选中时,这个权限组才会生效;
# 3. 视图继承 base.res_config_settings_view_form
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.library</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="5"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block"
data-string="Library" string="Library" datakey="my_library"
groups="my_library.group_librarian">
<h2>Library</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box" id="library">
<div class="o_setting_left_pane">
<field name="group_self_borrow"/>
</div>
<div class="o_setting_right_pane">
<label for="group_self_borrow"/>
<div class="text-muted">
Allow users to borrow and return books by themself
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
钩子函数
# 类型:
pre_init_hook : 开始安装模块时触发
post_init_hook : 安装后触发
uninstall_hook : 卸载该模块时触发 ,多用于模块需要垃圾回收机制
# 1. 在__manifest__.py 文件中 注册 钩子函数
'post_init_hook':'add_book_hook',
#2. 在 __init__.py 文件中 添加对应的方法
def add_book_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
book_data1 = {'name': 'Book 1', 'date_release': fields.Date.today()}
book_data2 = {'name': 'Book 2', 'date_release': fields.Date.today()}
env['library.book'].create([book_data1, book_data2])
窗口动作
<record id='action_all_customers' model='ir.actions.act_window'>
<field name="name">All customers</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('customer', '=', True)]</field>
<field name="context">{'default_customer': True}</field>
<field name="limit">20</field>
</record>
####
target:
current 在客户端主内容区域打开视图
new 在一个弹窗中打开视图
inline 类似于 current,但是编辑模式打开表单并禁止了action菜单
fullscreen 动作会覆盖整个浏览器,遮住菜单。 平板模式
main 类似于current ,但还是会清理掉面包屑导航
#### ir.actions.act_window.view 和 ir.actions.act_window
共同:都是窗体动作
区别 : ir.actions.act_window.view 是更细化分化 ,优先级更高
<record id="view_all_customers_form" model="ir.ui.view">
<field name="name">All customers</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name" />
</group>
</form>
</field>
</record>
<record id="action_all_customers_form" model="ir.actions.act_window.view">
<field name="act_window_id" ref="action_all_customers" />
<field name="view_id" ref="view_all_customers_form" />
<field name="view_mode">form</field>
<field name="sequence" eval="2"/>
</record>
xml内容
### xml属性
ref 特殊属性用于引用外部标识符
arch 定义结构
### xml 标签
<form></form> 表单组件
<Header></Header> 表单头部组件
<button></button> button 按钮
<group></group> 组,用来组织内容。 包含标题`String`属性
<notebook></notebook>
<page></page> # <notebook>和<page>标签来创建选项卡
<label></label> 一定要有for属性 label, class='oe_read_only,oe_edit_only' 仅读取/仅编辑模式下可见
<newline> 新的一行 强制换行布局
<field></field> 字段
### 表单添加按钮
<button type="action" name="%(base.action_partner_category_form)d" string="Open partner categories" />
# type='action' 指定ir.actions.* 定义动作
# object='' 调用当前模型的一个方法
### context 上下文
context="{'lang':语言,'default_lang':'默认语言','active_test':False}"
active_test 是一个特殊的语法,对带有名为active字段的模型,odoo会自动过滤掉False的记录
uid 是当前用户的id 'default_user_id': uid,
#### tree 视图中的 颜色分布 , 这些字段 都要在当前视图体现出来,即使不用也要invisible='1'
decoration-danger="字段1 < 字段2"
decoration-success="字段3== 字段4"
decoration-info="name=='decoration-info'"
decoration-muted="name=='decoration-muted'"
decoration-primary="name=='decoration-primary'"
decoration-warning="name=='decoration-warning'"
decoration-bf (加粗)
decoration-it (斜体)
### 定义过滤器-域
域 : ('字段','条件符号','值')
条件符号: = , != ,
<field name="domain">['|', ('customer', '=', True), ('supplier', '=', True)] </field>
# 注意:
搜索one2many和many2many字段的处理, [(‘category_id.name’, ‘!=’, ‘B’)] 值是唯一匹配则遵循,若值不唯一,则不遵循匹配
### 数值字段
可以添加一个sum属性来让该字段进行加总。不常见的由avg,min 和max
### tree 元素上的属性 editable 行内编辑
top : 顶部添加
bottom : 底部添加
### tree default_order=""
默认是按照 模型中定义的_order='字段'
可以指定其他字段
定义过滤器-域 运算符
运算符(等价符号) | 语法 |
---|---|
!ERROR! illegal character '!' | 第一个为精准匹配,第二为不等于,最一个是已淘汰的不等于标记。 |
in, not in | 这会检查值是否在右侧运算项列表中。以 Python 列表的开式给出:[('uid', 'in', [1, 2, 3])]或[('uid', 'not in', [1, 2, 3])]。 |
<, <= | 小于,小于等于 |
>, >= | 大于,大于等于 |
like, not like | 检查右侧运算项是否包含在值中(子字符串)。 |
ilike, not ilike | 与上面一个相同,但忽略大小写。 |
!ERROR! unexpected comma | 可以在这里搜索模式:%柳丁任意字符串,_匹配单个字符。这与 PostgreSQL中的 like 等价。 |
child_of | ('partner_id','child_of', [partner_id] 对带有 parent_id 字段的模型,它搜索右侧运算项的子项。右侧运算项包含于结果中。 |
!ERROR! illegal character '?' | 如果右侧运算项为 false 运行结果为 true;否则它和=相同。这用于由程序生成域并在想要由所设置值过滤的时候,否则忽略该值。 |
attrs动态属性
# 包含的字段
invisible 隐藏 bool类型
required
表单侧边栏附件
<div class="o_attachment_preview" options="{types: ['image', 'pdf'], 'order': 'desc'}" />
# 仅在模型继承mail.thread模型才会有作用
# type 允许的文档类型列表
# order :asc 和 desc 按照创建日期来排序
### 一般才state为草稿状态时,隐藏。 通过设置attrs来
<div class="o_attachment_preview"
attrs="{'invisible': [('state', '!=', 'draft')]/>
kanban看板
# 1. 为 元素生成一个随机颜色
kanban_color(some_variable) 函数
t-attf-class # 添加样式
如: t-attf-class="#{!selection_mode ? kanban_color(record.color.raw_value) : ''}"
# 2. 存储在二进制中的图像
使用 kanban_image(modelname,fieldname,record.id,raw.id,raw_value)
如: <img t-att-src="kanban_image('res.users', 'image_128', record.create_uid.raw_value)" class="oe_kanban_avatar"/>
# 3. 看板显示卡片类型布局
需要对 看板标签添加 默认分组 default_group_by='state'
<kanban default_group_by='state' ></kanban>
# 看板分组可用 选项:
group_create : 用于在看板的看板中义仓或者显示 Add a new column选项
group_delete:该选项在看板组上下文菜单中启用或禁用Column delete选项。默认值为true。
group_edit:该选项在看板组上下文菜单中启用或禁用Column edit选项。默认值为true。
archivable:该选项在看板组上下文菜单中启用或禁用存档和恢复记录的选项。仅用于存在active布尔字段的模型。
quick_create:通过这一选项,可以直接从看板视图中创建记录。
quick_create_view:quick_create默认仅在看板中显示name字段。但通过quick_create_view选项,可以给出最小化表单视图的引用 来在看板中显示。
on_create:如果想要在新建记录时使用quick_create并且不想要将用户重定向到表单视图,可以给出向导的引用来在点击Create按钮时打开向导
日历和甘特图
日历标签 <calendar></calendar>
# 日历 标签属性 ,需要带有Datetime或者Date类型的字段。 按小时表示增长的浮点字段
data_start='date_start' # 起始日期 ,必填项 开始日期的date或datetime类型字段。
date_stop='date_end' # 终止日期 可留空 开始日期的date或datetime类型字段。
date_delay 计算gantt记录的时长
duration_unit minute,hour,day,weak,month ,year
default_group_by 是gantt记录分组的字段名
event_open_popup 设置为1 ,以弹窗的形式打开日历视图
all_day 设置为 true 表示模型涵盖一整天
quick_add 设置为0,表示禁用 只需填写文件来创建新项
流存图cohort
# 发现指定时间段的记录的生命周期 。 可以看到指定时间任意对象的流失率和存留率。 企业版功能
<cohort
date_start="date_start"
date_stop="date_deadline"
interval="month"
string="Task Cohort" />
# mode : 默认是留存模式 retention 另一个是 流失 churn
# timeline 默认值是forward 另一个 backward。你需要使用向前的时间轴。但如果date_start是未来的时间,会需要 使用回退的时间轴。使用回退时间轴的一个示例为活动参与人员的注册,这时活动日期在未来而注册日期为过去
# interval 默认流存 按月分组,还可以有年,日。
# measure 类似于图标和透视表。 measure用于显示给定字段的总计值
仪表盘视图
<dashboard>
<view ref="my_project.view_project_tasks_graph" type="graph" />
<group>
<aggregate name="all_task"
string="Total Tasks"
group_operator="count"
field="id" measure="__count__"/>
<aggregate name="progress_task"
string="In Progress Tasks"
domain="[('stage_id.name', 'ilike', 'In Progress')]"
group_operator="count"
field="id" measure="__count__"/>
<aggregate name="done_task"
string="Completed Tasks"
domain="[('stage_id.name', 'ilike', 'Done')]"
group_operator="count" field="id"
Chapter 10
[ 335 ]
measure="__count__"/>
<formula name="price_average"
string="Overall Progress"
value="record.done_task / record.all_task"
widget="percentage"/>
</group>
<view ref="my_project.view_project_tasks_pivot" type="pivot"/>
</dashboard>
# aggregate 不同的KPI
# domain 属性来显示指定记录集合的总计
# group_operator 属性来指定一个SQL总计函数 avg或max
# widget 来将该值展示为百分比