添加功能

This commit is contained in:
李鹏宇
2025-12-19 15:24:31 +08:00
parent ac001d444e
commit 7cff04e0de
45 changed files with 636 additions and 58 deletions
@@ -6,7 +6,7 @@
- 快捷筛选(quick_filter)
- 自选时间筛选(select_date_filter)(待移植)
- 字段搜索(field)(待移植)
- 字段搜索(field)(已移植)
通过这些筛选标签,用户可以更灵活地对列表视图中的数据进行筛选。
@@ -20,9 +20,9 @@
#### 支持的属性:
| 属性名 | 类型 | 说明 | 是否可选 |
| ------- | ------ | ---------- | ---- |
| `class` | String | 自定义 CSS 类名 | ✅ |
| 属性名 | 类型 | 说明 | 是否可选 |
| ------- | ------ |-------------------------------------------------------------------| ---- |
| `class` | String | 自定义 CSS 类名,<br/>可使用class="header-search-inline"使所有筛选在同一行并根据宽度自动换行 | ✅ |
#### 子标签支持:
@@ -56,7 +56,7 @@
| ----------- | ------- | ----------- | ------------ |
| `class` | String | 自定义 CSS 类名 | ✅ |
| `string` | String | 显示的标头名称 | ✅ |
| `invisible` | Boolean | 是否默认隐藏 | ✅ |
| `invisible` | Boolean | 是否默认隐藏(废弃) | ✅ |
| `options` | JSON | 扩展参数,支持以下参数: | ✅ |
| | | `radio`: 是否单选 | |
| | | `no_label`: 不显示标题 | |
@@ -77,16 +77,16 @@
---
### 3. 自选时间筛选 (select_date_filter)
### 3. 自选时间筛选 (select_date_filter)(废弃)
**标签名:** `select_date_filter`
#### 支持的属性:
| 属性名 | 类型 | 说明 | 是否可选 |
| ------------------- | ------- | -------------- | ---- |
| ------------------- | ------- |----------------| ---- |
| `class` | String | 自定义 CSS 类名 | ✅ |
| `invisible` | Boolean | 是否默认隐藏 | ✅ |
| `invisible` | Boolean | 是否默认隐藏(废弃) | ✅ |
| `allow_date_fields` | String | 允许筛选的时间字段,逗号分隔 | ✅ |
#### 示例:
@@ -109,6 +109,9 @@
| `string` | String | 显示的标头名称 | ✅ |
| `class` | String | 自定义 CSS | ✅ |
| `invisible` | Boolean | 是否默认隐藏 | ✅ |
| `options` | JSON | 扩展参数,支持以下参数: | ✅ |
| | | `radio`: 是否单选 | |
| | | `no_label`: 不显示标题 | |
#### 示例:
@@ -0,0 +1,33 @@
/** @odoo-module **/
//
// import {patch} from "@web/core/utils/patch";
// import * as backend_utils from "@web/legacy/backend_utils";
//
// const originalSearchModelStateToLegacy = backend_utils.searchModelStateToLegacy;
// const originalGetGlobalState = backend_utils.getGlobalState;
//
// patch(backend_utils, "header_search_panel.backend_utils", {
// searchModelStateToLegacy(state) {
// // 原始state
// const originalState = JSON.parse(state);
// // 获取原始state
// let legacyState = JSON.parse(originalSearchModelStateToLegacy(state));
// if (originalState.headerSearchInfo) {
// legacyState.HeaderSearchInfo = originalState.headerSearchInfo;
// }
// return JSON.stringify(legacyState)
// },
//
// getGlobalState(legacyControllerState) {
// // 原始search model
// const originalSearchModel = JSON.parse(legacyControllerState.searchModel)
// let globalState = originalGetGlobalState(legacyControllerState);
// let searchModel = JSON.parse(globalState?.searchModel || "{}");
// if (originalSearchModel.HeaderSearchInfo) {
// searchModel.headerSearchInfo = originalSearchModel.HeaderSearchInfo;
// globalState.searchModel = JSON.stringify(searchModel)
// }
// console.log('headerSearchInfo: getGlobalState')
// return globalState
// },
// })
@@ -0,0 +1,54 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { Domain } from "@web/core/domain";
import { useService } from "@web/core/utils/hooks";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { standardFieldFilterProps } from "../standard_field_filter_props.js"
export class CharFilter extends Component {
setup() {
super.setup();
this.rpc = useService("rpc");
this.activeValues = this.env.searchModel.headerSearchInfo.FieldsFilter[this.props.id]?.activeValues || undefined;
}
/**
* 设置筛选字段的值
*/
setActiveValues(ev) {
this.activeValues = ev.target.value;
}
/**
* 获取筛选字段的domain
*/
get domain() {
return this.activeValues ? [[this.props.name, "ilike", this.activeValues]]: [];
}
/**
* 点击清空筛选时触发
*/
resetFilter() {
// 清空筛选字段的值
this.activeValues = undefined;
this.props.fieldOnchange({fieldDomainList: this.domain, activeValues: this.activeValues});
}
getDomain() {
return new Domain(this.props.domain).toList({});
}
async onInputChange(ev) {
this.setActiveValues(ev);
this.props.fieldOnchange({fieldDomainList: this.domain, activeValues: this.activeValues});
}
}
CharFilter.template = "header_search_panel.CharFilter";
CharFilter.components = {};
CharFilter.props = {
...standardFieldFilterProps,
};
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="header_search_panel.CharFilter" owl="1">
<div>
<input class="o_input"
t-att-id="props.id"
type="text"
t-ref="input"
t-att-value="activeValues"
t-on-input="onInputChange"/>
</div>
</t>
</templates>
@@ -1,8 +1,9 @@
/** @odoo-module **/
import {Component} from "@odoo/owl";
import {Domain} from "@web/core/domain";
import {standardFieldFilterProps} from "../standard_field_filter_props.js"
import { Component } from "@odoo/owl";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { deserializeDate } from "@web/core/l10n/dates";
import { standardFieldFilterProps } from "../standard_field_filter_props.js"
export class DateFilter extends Component {
setup() {
@@ -13,13 +14,21 @@ export class DateFilter extends Component {
this.endDate = this.env.searchModel.headerSearchInfo.FieldsFilter[this.props.id]?.endDate;
}
get dateFrom() {
return this.startDate ? deserializeDate(this.startDate): undefined;
}
get dateTo() {
return this.endDate ? deserializeDate(this.endDate): undefined;
}
// 设置筛选字段的值
setActiveValues(ev) {
const value = ev.target.value;
if (ev.target.id === this.props.id + "_start_date") {
setActiveValues(dateValue, type) {
const value = dateValue ? (this.props.type === "datetime" ? dateValue.toFormat("yyyy-MM-dd HH:mm:ss") : dateValue.toFormat("yyyy-MM-dd")) : undefined;
if (type === "startDate") {
this.startDate = value
}
if (ev.target.id === this.props.id + "_end_date") {
if (type === "endDate") {
this.endDate = value
}
}
@@ -41,9 +50,14 @@ export class DateFilter extends Component {
return domain
}
// 点击日期选择器时触发
dateOnchange(ev) {
this.setActiveValues(ev);
startDateOnchange (startDate) {
this.setActiveValues(startDate, "startDate");
this.props.fieldOnchange({fieldDomainList: this.domain, startDate: this.startDate, endDate: this.endDate});
}
endDateOnchange (endDate) {
this.setActiveValues(endDate, "endDate");
this.props.fieldOnchange({fieldDomainList: this.domain, startDate: this.startDate, endDate: this.endDate});
}
@@ -67,6 +81,9 @@ export class DateFilter extends Component {
}
DateFilter.template = "header_search_panel.DateFilter";
DateFilter.components = {
DateTimeInput,
};
DateFilter.props = {
...standardFieldFilterProps,
};
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- selection字段筛选框 -->
<t t-name="header_search_panel.DateFilter" owl="1">
<!-- 选择字段选择项 -->
<div class="date-filter">
<DateTimeInput value="dateFrom"
type="props.type"
onChange.bind="startDateOnchange"/>
<i class="oi oi-arrow-right"/>
<DateTimeInput value="dateTo"
type="props.type"
onChange.bind="endDateOnchange"/>
<!-- <input -->
<!-- t-att-type="props.type" -->
<!-- t-attf-id="{{ props.id + '_start_date' }}" -->
<!-- name="start_date" -->
<!-- t-att-value="this.startDate" -->
<!-- t-on-change="dateOnchange"/> -->
<!-- <input -->
<!-- t-att-type="props.type" -->
<!-- t-attf-id="{{ props.id + '_end_date' }}" -->
<!-- name="end_date" -->
<!-- t-att-value="this.endDate" -->
<!-- t-on-change="dateOnchange"/> -->
</div>
<!-- <div class="fast-date-filter"> -->
<!-- <a href="#" id="one-month" t-on-click="() => this.setDateRange(1)">近1个月</a> -->
<!-- <a href="#" id="three-months" t-on-click="() => this.setDateRange(3)">近3个月</a> -->
<!-- <a href="#" id="half-year" t-on-click="() => this.setDateRange(6)">近半年</a> -->
<!-- <a href="#" id="one-year" t-on-click="() => this.setDateRange(12)">近1年</a> -->
<!-- <a href="#" id="two-years" t-on-click="() => this.setDateRange(24)">近2年</a> -->
<!-- </div> -->
</t>
</templates>
@@ -8,6 +8,7 @@ import { BooleanFilter } from "./boolean_filter/boolean_filter";
import { DateFilter } from "./date_filter/date_filter";
import { Many2oneFilter } from "./many2one_filter/many2one_filter";
import { X2ManyFilterFilter } from "./x2many_filter/x2many_filter";
import { CharFilter } from "./char_filter/char_filter";
const fieldRegistry = registry.category("fields");
@@ -41,6 +42,9 @@ export class FieldsFilter extends Component {
case "one2many":
this.addX2ManyFieldDependencies(fieldInfo)
break;
case "char":
this.addCharFieldDependencies(fieldInfo)
break;
}
return fieldInfo;
}
@@ -65,6 +69,9 @@ export class FieldsFilter extends Component {
case "many2many":
case "one2many":
return X2ManyFilterFilter;
case "char":
return CharFilter;
}
}
@@ -128,6 +135,7 @@ export class FieldsFilter extends Component {
domain: field.fieldDomain || "[]",
fieldString: field.fieldString,
searchLimit: field.fieldOptions?.search_limit || 10,
placeholder: field.placeholder,
}
this._addFieldBaseProps(field.props, field);
}
@@ -144,6 +152,13 @@ export class FieldsFilter extends Component {
this._addFieldBaseProps(field.props, field);
}
addCharFieldDependencies(field) {
field.Component = this._getComponent(field);
// 组件props
field.props = {};
this._addFieldBaseProps(field.props, field);
}
// 筛选字段的onChange事件,将筛选字段的id和domain传给searchModel
fieldFilterOnchange(fieldsFilterId, fieldsFilterInfo) {
if (fieldsFilterInfo?.fieldDomainList){
@@ -104,6 +104,7 @@ export class Many2oneFilter extends Component {
activeActions: {create: false, createEdit: false, write: false},
fieldString: this.props.fieldString,
searchLimit: this.props.searchLimit,
placeholder: this.props.placeholder,
};
}
}
@@ -13,4 +13,6 @@ export const standardFieldFilterProps = {
type: {type: String, required: true},
// 字段变换监听函数
fieldOnchange: { type: Function, required: true },
// 提示
placeholder: {type: String, optional: true},
};
@@ -2,11 +2,11 @@
<templates xml:space="preserve">
<!-- 快速筛选组件 -->
<t t-name="header_search_panel.QuickFilter" owl="1">
<!-- <div t-if="props.showLabel" class="header-filter-label">
<div t-if="props.showLabel" class="header-filter-label">
<span>
<t t-esc="props.string" />
</span>
</div> -->
</div>
<!-- quick filter 内的筛选循环 -->
<div class="header-filter-value">
<div class="selection-options">
@@ -74,10 +74,12 @@
.fast-date-filter {
display: flex;
flex-wrap: wrap;
align-items: center;
}
input[type="date"],
.o_datetime_input,
.fast-date-filter a {
width: auto;
margin: 0.2rem;
padding: 0.1rem 1rem;
border: 0.1rem solid $o-community-color; /* 将边框颜色改为黑色 */
@@ -149,6 +151,14 @@
}
}
}
.header-search-inline {
display: flex;
flex-wrap: wrap;
align-items: start;
gap: 5px;
}
.header_search_toggle {
width: 6rem;
height: 2rem;
@@ -161,4 +171,4 @@
line-height: 2rem;
margin-left: 0.5rem;
}
}
}
@@ -165,6 +165,8 @@ patch(SearchArchParser.prototype, {
const fieldString = node.getAttribute("string");
// 自定义css类
const cssClass = node.getAttribute("class");
// placeholder
const placeholder = node.getAttribute("placeholder");
// 获取字段的options属性,该属性代表字段选项
const fieldOptions = evaluateExpr(node.getAttribute("options") || "{}");
// 获取widget
@@ -173,7 +175,7 @@ patch(SearchArchParser.prototype, {
if (fieldName) {
const field = this.fields[fieldName];
// 如果字段类型为selection或者bool,将字段名和选项添加到headerSearchInfo的fields中
if (!["selection", "boolean", "date", "datetime", "many2one", "one2many", "many2many"].includes(field.type)) {
if (!["selection", "boolean", "date", "datetime", "many2one", "one2many", "many2many", "char"].includes(field.type)) {
return false;
}
const fieldInfo = {
@@ -186,6 +188,7 @@ patch(SearchArchParser.prototype, {
fieldOptions: fieldOptions,
modifiers: JSON.parse(node.getAttribute("modifiers") || "{}"),
class: cssClass,
placeholder: placeholder || undefined,
}
if (["many2one", "many2many"].includes(field.type)) {
fieldInfo.fieldDomain = node.getAttribute("domain");
@@ -237,17 +240,16 @@ patch(SearchArchParser.prototype, {
for (const node of headerSearchNode.children) {
switch (node.tagName) {
case "field":
// // 字段筛选
// this.visitHeaderField(node);
// 字段筛选
this.visitHeaderField(node);
break;
case "quick_filter":
// 快捷筛选
this.visitHeaderQuickFilter(node);
break;
case "select_date_filter":
// // 自选日期筛选
// this.visitHeaderSelectDateFilter(node);
break;
// 自选日期筛选
this.visitHeaderSelectDateFilter(node);
}
}
return false;
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
{
'name': '模型测试',
'summary': '',
'description': '',
'author': 'XSK',
'website': '',
'category': 'Uncategorized',
'version': '0.1',
'license': 'LGPL-3',
'depends': ['base', 'web', 'mail'],
'data': [
'security/ir.model.access.csv',
'views/views.xml',
],
'assets': {
'web.assets_backend': [
'model_test/static/src/**/*',
],
},
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# from odoo import http
# class FilePreviewTest(http.Controller):
# @http.route('/file_preview_test/file_preview_test', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/file_preview_test/file_preview_test/objects', auth='public')
# def list(self, **kw):
# return http.request.render('file_preview_test.listing', {
# 'root': '/file_preview_test/file_preview_test',
# 'objects': http.request.env['file_preview_test.file_preview_test'].search([]),
# })
# @http.route('/file_preview_test/file_preview_test/objects/<model("file_preview_test.file_preview_test"):obj>', auth='public')
# def object(self, obj, **kw):
# return http.request.render('file_preview_test.object', {
# 'object': obj
# })
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
from datetime import datetime, date, timedelta
import string
import random
from odoo import models, fields, api
class ModelTestManyOne(models.Model):
_name = 'model.test.many.one'
_description = '测试模块'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='字符串字段', tracking=True)
test_id = fields.Many2one(string='多对一字段', comodel_name='model.test', required=True, ondelete='cascade')
file_id = fields.Many2one(string='多对一文件', comodel_name='ir.attachment', tracking=True)
file_ids = fields.Many2many(string='多对多文件', comodel_name='ir.attachment', relation='file_preview_test_many_one_rel', column1='test_id', column2='test_id_2')
class ModelTest(models.Model):
_name = 'model.test'
_description = '测试模块'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='字符串字段', tracking=True)
int_field = fields.Integer(string='整数', tracking=True)
float_field = fields.Float(string='浮点数', tracking=True)
date_field = fields.Date(string='日期字段', tracking=True)
datetime_field = fields.Datetime(string='日期时间字段', tracking=True)
state = fields.Selection(string='状态', selection=[('draft', '草稿'), ('done', '完成')], default='draft')
file_id = fields.Many2one(string='多对一文件', comodel_name='ir.attachment')
file_ids = fields.Many2many(string='多对多文件', comodel_name='ir.attachment', relation='file_preview_test_rel', column1='test_id', column2='test_id_2')
text = fields.Text(string='长文本测试')
one_to_many_field = fields.One2many(string='one2many字段', comodel_name='model.test.many.one', inverse_name='test_id', tracking=True)
bool_test = fields.Boolean(string='布尔字段')
selection_test = fields.Selection(string='选择字段', selection=[('1', '选项一'), ('2', '选项二')])
def action_generate_sample_text(self):
"""生成长文本样例"""
sample_text = """
# 这是Markdown格式的样例文本
## 项目概述
这是一个测试项目,用于展示Odoo中各种字段类型的功能和显示效果。
### 功能特点
1. 支持多种数据类型
2. 自动生成测试数据
3. 便于开发和测试
### 技术规格
- 数据库:PostgreSQL
- 框架:Odoo 14.0+
- 语言:Python/XML/JavaScript
---
**注意**:此内容为自动生成,仅用于测试目的。
"""
self.write({'text': sample_text})
# 辅助方法
def _generate_random_string(self, length):
"""生成随机字符串"""
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for _ in range(length))
def _generate_random_date(self):
"""生成随机日期(最近365天内)"""
days_ago = random.randint(0, 365)
return date.today() - timedelta(days=days_ago)
def _generate_random_datetime(self):
"""生成随机日期时间(最近30天内)"""
hours_ago = random.randint(0, 720) # 30天*24小时
return datetime.now() - timedelta(hours=hours_ago)
# 生成随机数据的方法
def action_insert_random_data(self):
"""生成50条随机测试数据"""
# 预先生成一些附件供选择
attachment_ids = self.env['ir.attachment'].search([], limit=10)
for i in range(50):
# 生成随机数据
random_name = f"测试项目_{i + 1}_{self._generate_random_string(6)}"
random_int = random.randint(1, 1000)
random_float = round(random.uniform(1.0, 10000.0), 2)
random_date = self._generate_random_date()
random_datetime = self._generate_random_datetime()
random_state = random.choice(['draft', 'done'])
random_selection = random.choice(['1', '2'])
random_bool = random.choice([True, False])
# 生成随机文本
random_text = f"""
这是第{i + 1}条测试记录的长文本内容。
生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
随机ID:{self._generate_random_string(10)}
描述:这是一条自动生成的测试数据,用于演示各种字段类型的显示效果。
"""
# 创建主记录
test_record = self.create({
'name': random_name,
'int_field': random_int,
'float_field': random_float,
'date_field': random_date,
'datetime_field': random_datetime,
'state': random_state,
'bool_test': random_bool,
'selection_test': random_selection,
'text': random_text,
})
# 随机选择附件
if attachment_ids:
# 随机选择1-3个附件
num_attachments = random.randint(1, min(3, len(attachment_ids)))
random_attachments = random.sample(attachment_ids.ids, num_attachments)
test_record.write({
'file_id': random.choice(attachment_ids).id if attachment_ids else False,
'file_ids': [(6, 0, random_attachments)]
})
# 返回成功消息
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '数据生成成功',
'message': f'已成功生成50条测试数据!',
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}
def action_to_dialog(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'model.test',
'view_mode': 'form',
'res_id': self.id,
'views': [[self.env.ref('model_test.view_file_preview_test_form').id, 'form']],
}
def cancel(self):
self.state = 'done'
def test_approval(self):
self.verify_rules(self.cancel)
# print(asd)
# class TimeCalculation(models.Model):
# _name = 'example.time.calculation'
# _description = 'Example for Start Time, End Time, Duration'
#
# start_time = fields.Datetime(string='Start Time', required=True)
# end_time = fields.Datetime(string='End Time', required=True)
# duration = fields.Float(string='Duration (hours)', compute='_compute_duration', inverse='_inverse_duration', store=True)
#
# @api.depends('start_time', 'end_time')
# def _compute_duration(self):
# """根据开始时间和结束时间计算时间间隔"""
# for record in self:
# if record.start_time and record.end_time:
# delta = record.end_time - record.start_time
# record.duration = delta.total_seconds() / 3600 # 转换为小时
#
# @api.depends('start_time', 'duration')
# def _compute_end_time(self):
# """根据开始时间和时间间隔计算结束时间"""
# for record in self:
# if record.start_time and record.duration is not None:
# record.end_time = record.start_time + timedelta(hours=record.duration)
#
# @api.depends('end_time', 'duration')
# def _compute_start_time(self):
# """根据结束时间和时间间隔计算开始时间"""
# for record in self:
# if record.end_time and record.duration is not None:
# record.start_time = record.end_time - timedelta(hours=record.duration)
#
# def _inverse_duration(self):
# """反向更新结束时间"""
# for record in self:
# if record.start_time and record.duration is not None:
# record.end_time = record.start_time + timedelta(hours=record.duration)
#
# @api.onchange('start_time', 'end_time')
# def _onchange_duration(self):
# """确保在界面交互中,时间关系自动更新"""
# for record in self:
# if record.start_time and record.end_time:
# delta = record.end_time - record.start_time
# record.duration = delta.total_seconds() / 3600
#
# @api.onchange('start_time', 'duration')
# def _onchange_end_time(self):
# """确保在界面交互中,时间关系自动更新"""
# for record in self:
# if record.start_time and record.duration is not None:
# record.end_time = record.start_time + timedelta(hours=record.duration)
#
# @api.onchange('end_time', 'duration')
# def _onchange_start_time(self):
# """确保在界面交互中,时间关系自动更新"""
# for record in self:
# if record.end_time and record.duration is not None:
# record.start_time = record.end_time - timedelta(hours=record.duration)
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_model_test,测试模块,model_model_test,,1,1,1,1
access_model_test_many_one,测试模块多,model_model_test_many_one,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_model_test 测试模块 model_model_test 1 1 1 1
3 access_model_test_many_one 测试模块多 model_model_test_many_one 1 1 1 1
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ModelTest.buttons" t-inherit="web.ListView.Buttons" owl="1">
<xpath expr="(//div/*)[last()]" position="after">
<button type="button" class="btn btn-primary"
t-on-click="(ev) => this.triggerDynamicColumnsChanged('btn_1', ['name','int_field','float_field'])">
方案1
</button>
<button type="button" class="btn btn-primary"
t-on-click="(ev) => this.triggerDynamicColumnsChanged('btn_2', ['selection_test','text','one_to_many_field'])">
方案2
</button>
</xpath>
</t>
</templates>
@@ -0,0 +1,10 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
import { listView } from '@web/views/list/list_view';
export const ModelTestListView = {
...listView,
buttonTemplate: 'ModelTest.buttons'
};
registry.category('views').add('model_test_list', ModelTestListView);
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<!-- 项目总览搜索 -->
<record id="view_model_test_search_view" model="ir.ui.view">
<field name="model">model.test</field>
<field name="arch" type="xml">
<search string="项目总览">
<separator/>
<filter string="真" name="filter_by_true" domain="[('bool_test', '=', True)]"/>
<separator/>
<filter string="假" name="filter_by_false" domain="[('bool_test', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="多对一未见" name="group_by_file_id" domain="[]" context="{'group_by': 'file_id'}" />
<filter string="布尔值" name="group_by_bool_test" domain="[]" context="{'group_by': 'bool_test'}" />
</group>
<searchpanel>
<field name="file_id"/>
</searchpanel>
<!--
<header_search class="header-search-inline"> 所有搜索在一行
-->
<header_search>
<field name="name"/>
<field name="file_id"/>
<field name="selection_test" options="{'radio': True}"/>
<field name="bool_test" options="{'radio': True, 'no_label': True}"/>
<select_date_filter allow_date_fields="create_date,write_date" />
<quick_filter string="快捷筛选一" options="{'radio': True, 'no_label': True}">
<filter string="真" name="filter_by_bool_true" domain="[('bool_test', '=', True)]"/>
<filter string="最近的二十" name="filter_by_bool_true"/>
</quick_filter>
<field name="date_field" options="{'radio': True, 'no_label': True}"/>
<field name="datetime_field"/>
<quick_filter string="快捷筛选二" options="{'radio': True, 'no_label': True}">
<filter string="假" name="filter_by_bool_false" domain="[('bool_test', '=', False)]"/>
</quick_filter>
</header_search>
</search>
</field>
</record>
<!-- tree视图 -->
<record id="view_model_test_tree" model="ir.ui.view">
<field name="name">model.test.tree</field>
<field name="model">model.test</field>
<field name="arch" type="xml">
<tree string="">
<header>
<button name="action_insert_random_data" string="随机插入50条数据" type="object" class="btn-primary ms-1" display="always"/>
</header>
<field name="name"/>
<field name="int_field"/>
<field name="float_field"/>
<field name="selection_test"/>
<field name="text" widget="text_nowrap"/>
<field name="one_to_many_field"/>
<field name="state"/>
</tree>
</field>
</record>
<!-- form视图 -->
<record id="view_model_test_form" model="ir.ui.view">
<field name="name">model.test.form</field>
<field name="model">model.test</field>
<field name="arch" type="xml">
<form string="Job">
<header>
<button string="测试审批" type="object" name="test_approval" class="btn-primary"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="int_field"/>
<field name="float_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="state"/>
<field name="text"/>
<field name="file_id"/>
<field name="selection_test"/>
<field name="bool_test" widget="boolean_toggle"/>
<field name="file_ids"/>
</group>
<notebook>
<page string="多对多字段图片预览测试">
<field name="file_ids"/>
</page>
<page string="one2many字段">
<field name="one_to_many_field" mode="tree">
<tree editable="1">
<field name="name"/>
<field name="file_id"/>
<field name="file_ids"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<!-- Attachment preview -->
<div class="o_attachment_preview" groups="hr_recruitment.group_applicant_cv_display"/>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- 跳转菜单 -->
<record model="ir.actions.act_window" id="file_preview_menu_action_window">
<field name="name">文件测试</field>
<field name="target">main</field>
<field name="res_model">model.test</field>
<field name="view_mode">tree,form</field>
</record>
<!-- 二级菜单 -->
<menuitem
id="model_test_menu"
name="测试模块"
sequence="10"
action="file_preview_menu_action_window"/>
</data>
</odoo>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- selection字段筛选框 -->
<t t-name="header_search_panel.DateFilter" owl="1">
<!-- 选择字段选择项 -->
<div class="date-filter">
<input
type="date"
t-attf-id="{{ props.id + '_start_date' }}"
name="start_date"
t-att-value="this.startDate"
t-on-change="dateOnchange"/>
<input
type="date"
t-attf-id="{{ props.id + '_end_date' }}"
name="end_date"
t-att-value="this.endDate"
t-on-change="dateOnchange"/>
</div>
<div class="fast-date-filter">
<a href="#" id="one-month" t-on-click="() => this.setDateRange(1)">近1个月</a>
<a href="#" id="three-months" t-on-click="() => this.setDateRange(3)">近3个月</a>
<a href="#" id="half-year" t-on-click="() => this.setDateRange(6)">近半年</a>
<a href="#" id="one-year" t-on-click="() => this.setDateRange(12)">近1年</a>
<a href="#" id="two-years" t-on-click="() => this.setDateRange(24)">近2年</a>
</div>
</t>
</templates>