添加功能
This commit is contained in:
@@ -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
|
||||
// },
|
||||
// })
|
||||
+54
@@ -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,
|
||||
};
|
||||
+15
@@ -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>
|
||||
+27
-10
@@ -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,
|
||||
};
|
||||
+37
@@ -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>
|
||||
+15
@@ -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){
|
||||
+1
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
@@ -13,4 +13,6 @@ export const standardFieldFilterProps = {
|
||||
type: {type: String, required: true},
|
||||
// 字段变换监听函数
|
||||
fieldOnchange: { type: Function, required: true },
|
||||
// 提示
|
||||
placeholder: {type: String, optional: true},
|
||||
};
|
||||
+2
-2
@@ -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">
|
||||
+12
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -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
|
||||
|
@@ -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>
|
||||
-30
@@ -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>
|
||||
Reference in New Issue
Block a user