diff --git a/yuthon_ai_agent/data/demo_report_template.xml b/yuthon_ai_agent/data/demo_report_template.xml
index da2f0d25f..14f3430da 100644
--- a/yuthon_ai_agent/data/demo_report_template.xml
+++ b/yuthon_ai_agent/data/demo_report_template.xml
@@ -10,7 +10,6 @@
sale.order
["name","partner_id","date_order","amount_total","state","user_id"]
{"name":"订单号","partner_id":"客户","date_order":"下单日期","amount_total":"金额","state":"状态","user_id":"销售员"}
- [["state","!=","cancel"]]
date_order desc
5
@@ -25,7 +24,6 @@
crm.lead
["name","partner_name","expected_revenue","probability","stage_id","create_date","user_id"]
{"name":"线索名称","partner_name":"客户","expected_revenue":"预期收入","probability":"赢单率","stage_id":"阶段","create_date":"创建时间","user_id":"负责人"}
- [["active","=",True]]
create_date desc
6
diff --git a/yuthon_ai_agent/models/ai_report_template.py b/yuthon_ai_agent/models/ai_report_template.py
index 6b18d2b48..181399aac 100644
--- a/yuthon_ai_agent/models/ai_report_template.py
+++ b/yuthon_ai_agent/models/ai_report_template.py
@@ -28,8 +28,11 @@ class AiReportTemplate(models.Model):
'template_id', 'field_id', string='显示字段',
domain="[('model_id','=',model_id)]",
help='选择要显示的字段。选择后自动填充"字段列表"和"字段标签"')
- default_domain = fields.Text('默认条件', default='[]',
- help='JSON 格式的默认查询 domain')
+ default_domain = fields.Text('默认条件', compute='_compute_default_domain',
+ inverse='_inverse_default_domain', store=True,
+ help='自动从查询条件行生成,也可手动编辑 JSON')
+ domain_line_ids = fields.One2many('ai.report.template.domain.line', 'template_id',
+ string='查询条件行', copy=True)
default_order = fields.Char('默认排序', default='id desc')
description = fields.Text('使用说明', translate=True,
help='告诉 AI 何时使用此模板')
@@ -92,19 +95,13 @@ class AiReportTemplate(models.Model):
cr = []
for col in t.column_ids:
if col.color_value and col.cell_color:
- # 解析操作符前缀: >= <= != > < = ~
- cv = col.color_value.strip()
- m = re.match(r'^(>=|<=|!=|>|<|=|~)(.+)$', cv)
- if m:
- op_prefix, val = m.group(1), m.group(2)
- op_map = {'=': 'eq', '~': 'contains', '!=': 'neq',
- '>': '>', '<': '<', '>=': '>=', '<=': '<='}
- op = op_map.get(op_prefix, 'eq')
- else:
- op, val = 'eq', cv
+ op_map = {'eq': 'eq', 'neq': 'neq', 'gt': '>', 'lt': '<',
+ 'gte': '>=', 'lte': '<=', 'contains': 'contains'}
+ op = op_map.get(col.color_operator or 'eq', 'eq')
cr.append({
'field': col.field_name,
- 'rules': [{'op': op, 'value': val.strip(), 'color': col.cell_color}],
+ 'rules': [{'op': op, 'value': col.color_value.strip(),
+ 'color': col.cell_color}],
})
if cr:
# 合并相同 field 的规则
@@ -154,6 +151,57 @@ class AiReportTemplate(models.Model):
def _inverse_param_definitions(self):
pass
+ # ===== Computed: default_domain =====
+ @api.depends('domain_line_ids', 'domain_line_ids.field_id',
+ 'domain_line_ids.operator', 'domain_line_ids.value')
+ def _compute_default_domain(self):
+ for t in self:
+ if not t.domain_line_ids:
+ t.default_domain = '[]'
+ continue
+ domain = []
+ for line in t.domain_line_ids.sorted('sequence'):
+ if not line.field_id or not line.value:
+ continue
+ val = line.value.strip()
+ if line.operator in ('in', 'not in'):
+ val = [v.strip() for v in val.split(',')]
+ domain.append([line.field_id.name, line.operator, val])
+ t.default_domain = json.dumps(domain, ensure_ascii=False)
+
+ def _inverse_default_domain(self):
+ """手动编辑 JSON 时反向解析到 domain_line_ids(简单实现:清空后重建)"""
+ for t in self:
+ if not t.default_domain:
+ continue
+ try:
+ domain = json.loads(t.default_domain)
+ except (json.JSONDecodeError, TypeError):
+ continue
+ cmds = [(5, 0, 0)]
+ seq = 10
+ for cond in domain:
+ if len(cond) >= 3:
+ op = cond[1]
+ val = cond[2]
+ if isinstance(val, list):
+ val = ', '.join(str(v) for v in val)
+ else:
+ val = str(val)
+ field = t.env['ir.model.fields'].search([
+ ('model', '=', t.model_name),
+ ('name', '=', cond[0]),
+ ], limit=1)
+ if field:
+ cmds.append((0, 0, {
+ 'field_id': field.id,
+ 'operator': op,
+ 'value': val,
+ 'sequence': seq,
+ }))
+ seq += 10
+ t.domain_line_ids = cmds
+
# ===== 预览动作 =====
def action_preview(self):
"""预览报表:使用模板配置生成一个临时报表并打开 URL"""
diff --git a/yuthon_ai_agent/models/ai_report_template_config.py b/yuthon_ai_agent/models/ai_report_template_config.py
index c34136b1f..1fa7ca014 100644
--- a/yuthon_ai_agent/models/ai_report_template_config.py
+++ b/yuthon_ai_agent/models/ai_report_template_config.py
@@ -24,15 +24,17 @@ class AiReportTemplateColumn(models.Model):
label = fields.Char('显示名称',
help='不填则默认使用字段名')
width = fields.Integer('列宽', default=120)
+ color_operator = fields.Selection([
+ ('eq', '等于'),
+ ('neq', '不等于'),
+ ('gt', '大于'),
+ ('lt', '小于'),
+ ('gte', '大于等于'),
+ ('lte', '小于等于'),
+ ('contains', '包含'),
+ ], string='操作符', default='eq')
color_value = fields.Char('匹配值',
- help='格式:[操作符]值。不写操作符默认等于。\n'
- ' =已完成 / 已完成 → 等于"已完成"\n'
- ' >100 → 大于 100\n'
- ' <50 → 小于 50\n'
- ' >=100 → 大于等于 100\n'
- ' <=50 → 小于等于 50\n'
- ' !=已完成 → 不等于"已完成"\n'
- ' ~张三 → 包含"张三"')
+ help='匹配的值,如"已完成"或"100"')
cell_color = fields.Char('显示颜色', default='#5DCAA5',
help='匹配成功时显示的颜色,如 #E74C3C(红)、#5DCAA5(绿)')
sequence = fields.Integer('排序', default=10)
@@ -44,18 +46,11 @@ class AiReportTemplateColumn(models.Model):
if not self.label:
self.label = self.field_id.field_description
- @api.constrains('color_value')
+ @api.constrains('color_value', 'cell_color')
def _check_color_value(self):
- import re
for rec in self:
- if not rec.color_value:
- continue
- # 合法格式: [>=|<=|!=|>|<|=|~]值
- if not re.match(r'^(>=|<=|!=|>|<|=|~)?\S', rec.color_value):
- raise ValidationError(
- '匹配值格式错误:%s\n'
- '正确示例:已完成、>100、<50、>=100、<=50、!=已完成、~张三' % rec.color_value
- )
+ if rec.cell_color and not rec.color_value:
+ raise ValidationError('设置了显示颜色时,匹配值不能为空')
class AiReportTemplateCard(models.Model):
@@ -160,3 +155,35 @@ class AiReportTemplateParamDef(models.Model):
self.domain_field = self.field_id.name
if not self.label:
self.label = self.field_id.field_description
+
+
+class AiReportTemplateDomainLine(models.Model):
+ _name = 'ai.report.template.domain.line'
+ _description = '报表模板-查询条件行'
+ _order = 'sequence, id'
+
+ template_id = fields.Many2one('ai.report.template', string='模板',
+ required=True, ondelete='cascade')
+ model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
+ field_id = fields.Many2one('ir.model.fields', string='字段',
+ domain="[('model_id', '=', model_id)]",
+ help='选择模型字段')
+ operator = fields.Selection([
+ ('=', '等于'),
+ ('!=', '不等于'),
+ ('>', '大于'),
+ ('<', '小于'),
+ ('>=', '大于等于'),
+ ('<=', '小于等于'),
+ ('ilike', '包含'),
+ ('not ilike', '不包含'),
+ ('in', '在列表中'),
+ ('not in', '不在列表中'),
+ ], string='操作符', default='=')
+ value = fields.Char('值', help='匹配的值,多个值用逗号分隔(用于in/not in)')
+ sequence = fields.Integer('排序', default=10)
+
+ @api.onchange('field_id')
+ def _onchange_field_id(self):
+ if self.field_id and not self.value:
+ pass
diff --git a/yuthon_ai_agent/security/ir.model.access.csv b/yuthon_ai_agent/security/ir.model.access.csv
index 92580664a..dbdf8b9e7 100644
--- a/yuthon_ai_agent/security/ir.model.access.csv
+++ b/yuthon_ai_agent/security/ir.model.access.csv
@@ -33,3 +33,5 @@ access_ai_report_template_filter_user,筛选器-用户只读,model_ai_report_tem
access_ai_report_template_filter_admin,筛选器-管理员,model_ai_report_template_filter,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_param_def_user,参数定义-用户只读,model_ai_report_template_param_def,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_param_def_admin,参数定义-管理员,model_ai_report_template_param_def,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
+access_ai_report_template_domain_line_user,查询条件行-用户只读,model_ai_report_template_domain_line,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
+access_ai_report_template_domain_line_admin,查询条件行-管理员,model_ai_report_template_domain_line,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
diff --git a/yuthon_ai_agent/views/ai_report_views.xml b/yuthon_ai_agent/views/ai_report_views.xml
index 8306a00f9..907c53fc2 100644
--- a/yuthon_ai_agent/views/ai_report_views.xml
+++ b/yuthon_ai_agent/views/ai_report_views.xml
@@ -164,8 +164,6 @@
placeholder="选择要显示的字段(选择后自动生成下方配置)"/>
-
@@ -196,15 +194,30 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+