ai动态报表

This commit is contained in:
李鹏宇
2026-05-29 10:51:36 +08:00
parent d2c58b30a6
commit 1eed1eb71f
5 changed files with 242 additions and 36 deletions
@@ -12,6 +12,7 @@
<field name="field_labels">{"name":"订单号","partner_id":"客户","date_order":"下单日期","amount_total":"金额","state":"状态","user_id":"销售员"}</field>
<field name="default_domain">[["state","!=","cancel"]]</field>
<field name="default_order">date_order desc</field>
<field name="default_visible">5</field>
<field name="report_config"><![CDATA[{"columns":[{"k":"name","l":"订单号","w":130},{"k":"partner_id","l":"客户","w":120},{"k":"date_order","l":"下单日期","w":110},{"k":"amount_total","l":"金额","w":110},{"k":"state","l":"状态","w":80}],"cards":[{"label":"总金额","field":"amount_total","calc":"sum","prefix":"¥"},{"label":"订单数","field":"id","calc":"count"}],"groupBy":[{"key":"state","label":"状态"}],"defaultVisible":5,"colorRules":[{"field":"state","rules":[{"op":"eq","value":"sale","color":"#5DCAA5"},{"op":"eq","value":"draft","color":"#EFC87E"},{"op":"eq","value":"done","color":"#85B7EB"},{"op":"eq","value":"cancel","color":"#ED93B1"}]}],"filters":[{"field":"state","type":"select","label":"状态","options":["draft","sale","done","cancel"]}]}]]></field>
<field name="param_definitions"><![CDATA[[{"name":"date_from","label":"开始日期","type":"date","domain_field":"date_order","op":">=","description":"查询起始日期"},{"name":"date_to","label":"结束日期","type":"date","domain_field":"date_order","op":"<=","description":"查询截止日期"}]]]></field>
<field name="description">当用户查询销售订单列表或销售业绩时使用此模板。支持按日期范围筛选。</field>
@@ -26,6 +27,7 @@
<field name="field_labels">{"name":"线索名称","partner_name":"客户","expected_revenue":"预期收入","probability":"赢单率","stage_id":"阶段","create_date":"创建时间","user_id":"负责人"}</field>
<field name="default_domain">[["active","=",True]]</field>
<field name="default_order">create_date desc</field>
<field name="default_visible">6</field>
<field name="report_config"><![CDATA[{"columns":[{"k":"name","l":"线索名称","w":140},{"k":"partner_name","l":"客户","w":120},{"k":"expected_revenue","l":"预期收入","w":110},{"k":"probability","l":"赢单率","w":80},{"k":"stage_id","l":"阶段","w":100},{"k":"create_date","l":"创建时间","w":120}],"cards":[{"label":"预期总收入","field":"expected_revenue","calc":"sum","prefix":"¥"},{"label":"线索数","field":"id","calc":"count"}],"groupBy":[{"key":"stage_id","label":"阶段"}],"defaultVisible":6,"colorRules":[],"filters":[{"field":"probability","type":"input","label":"最低赢单率"}]}]]></field>
<field name="description">当用户查询销售线索列表或销售管道时使用此模板。</field>
<field name="sequence">20</field>
+1
View File
@@ -6,3 +6,4 @@ from . import ai_rule
from . import ai_manual
from . import ai_report
from . import ai_report_template
from . import ai_report_template_config
+120 -13
View File
@@ -13,6 +13,7 @@ class AiReportTemplate(models.Model):
_description = '报表模板'
_order = 'sequence, id'
# ===== 基础配置 =====
name = fields.Char('模板名称', required=True)
model_name = fields.Char('数据模型', required=True,
help='Odoo 模型技术名称,如 sale.order')
@@ -29,19 +30,125 @@ class AiReportTemplate(models.Model):
default_domain = fields.Text('默认条件', default='[]',
help='JSON 格式的默认查询 domain')
default_order = fields.Char('默认排序', default='id desc')
description = fields.Text('使用说明',
help='告诉 AI 何时使用此模板,如"当用户查询销售订单列表时使用此模板"')
description = fields.Text('使用说明', translate=True,
help='告诉 AI 何时使用此模板')
sequence = fields.Integer('排序', default=10)
report_config = fields.Text('报表视觉配置', default='{}',
help='JSON格式的报表视觉配置,包含columns(列定义)、cards(汇总卡片)、groupBy(分组)、colorRules(颜色规则)、filters(筛选器)、defaultVisible(默认显示列数)')
param_definitions = fields.Text('模板参数定义', default='[]',
help='JSON数组,定义模板可接受的参数。每项包含name(参数名)、label(显示名)、type(参数类型:date/char/select/float)、domain_field(映射模型字段)、op(操作符)、description(说明)、options(select选项)')
active = fields.Boolean(default=True)
# ===== 视觉配置(拆分为 One2many) =====
column_ids = fields.One2many('ai.report.template.column', 'template_id',
string='列定义', copy=True)
card_ids = fields.One2many('ai.report.template.card', 'template_id',
string='汇总卡片', copy=True)
group_by_ids = fields.One2many('ai.report.template.group.by', 'template_id',
string='分组设置', copy=True)
color_rule_ids = fields.One2many('ai.report.template.color.rule', 'template_id',
string='颜色规则', copy=True)
filter_ids = fields.One2many('ai.report.template.filter', 'template_id',
string='筛选器', copy=True)
param_def_ids = fields.One2many('ai.report.template.param.def', 'template_id',
string='AI 参数定义', copy=True)
default_visible = fields.Integer('默认可见列数', default=10)
# ===== 计算字段(兼容旧版 JSON) =====
report_config = fields.Text('报表视觉配置', default='{}', compute='_compute_report_config',
inverse='_inverse_report_config', store=True,
help='自动从列/卡片/分组/颜色/筛选器生成,也可手动编辑 JSON')
param_definitions = fields.Text('模板参数定义', default='[]',
compute='_compute_param_definitions', inverse='_inverse_param_definitions', store=True,
help='自动从参数定义行生成,也可手动编辑 JSON')
# ===== Computed: report_config =====
@api.depends('column_ids', 'card_ids', 'group_by_ids',
'color_rule_ids', 'filter_ids', 'default_visible')
def _compute_report_config(self):
for t in self:
# 如果没有任何配置行,保留现有值(兼容旧数据)
if not any([t.column_ids, t.card_ids, t.group_by_ids,
t.color_rule_ids, t.filter_ids]):
continue
cfg = {}
cols = [{'k': c.field_name, 'l': c.label or c.field_name, 'w': c.width or 120}
for c in t.column_ids]
if cols:
cfg['columns'] = cols
cards = []
for c in t.card_ids:
d = {'label': c.label, 'field': c.field_name, 'calc': c.calc}
if c.prefix:
d['prefix'] = c.prefix
if c.unit:
d['unit'] = c.unit
cards.append(d)
if cards:
cfg['cards'] = cards
gb = [{'key': g.field_name, 'label': g.label or g.field_name}
for g in t.group_by_ids]
if gb:
cfg['groupBy'] = gb
cr = []
for r in t.color_rule_ids:
# 按 field_name 分组
cr.append({
'field': r.field_name,
'rules': [{'op': r.op, 'value': r.value, 'color': r.color}],
})
if cr:
# 合并相同 field 的规则
merged = {}
for r in cr:
f = r['field']
if f not in merged:
merged[f] = {'field': f, 'rules': []}
merged[f]['rules'].append(r['rules'][0])
cfg['colorRules'] = list(merged.values())
flt = [{'field': f.field_name, 'type': f.filter_type,
'label': f.label or f.field_name}
for f in t.filter_ids]
for f, fdef in zip(flt, t.filter_ids):
if fdef.options:
f['options'] = [o.strip() for o in fdef.options.split(',') if o.strip()]
if flt:
cfg['filters'] = flt
if t.default_visible:
cfg['defaultVisible'] = t.default_visible
t.report_config = json.dumps(cfg, ensure_ascii=False) if cfg else '{}'
def _inverse_report_config(self):
"""反向:手动编辑 JSON 时回写旧字段值(不自动拆分到行,保持兼容)"""
pass # report_config 仍可手动编辑,但不反向拆分到行
# ===== Computed: param_definitions =====
@api.depends('param_def_ids')
def _compute_param_definitions(self):
for t in self:
if not t.param_def_ids:
continue
defs = []
for p in t.param_def_ids:
d = {
'name': p.name,
'label': p.label or p.name,
'type': p.param_type,
'domain_field': p.domain_field or p.name,
'op': p.op or '=',
'description': p.description or '',
}
if p.options:
d['options'] = [o.strip() for o in p.options.split(',') if o.strip()]
if p.required:
d['required'] = True
defs.append(d)
t.param_definitions = json.dumps(defs, ensure_ascii=False) if defs else '[]'
def _inverse_param_definitions(self):
pass
# ===== 预览动作 =====
def action_preview(self):
"""预览报表:使用模板配置生成一个临时报表并打开 URL"""
self.ensure_one()
import json as _json
report = self.env['ai.report'].sudo().create({
'name': f'[预览] {self.name}',
'model_name': self.model_name,
@@ -59,9 +166,10 @@ class AiReportTemplate(models.Model):
'target': 'new',
}
# ===== AI 模板注入 =====
@api.model
def get_templates_for_ai(self):
"""获取所有启用模板的摘要,供 AI system_msg 注入(增强版:包含参数信息)"""
"""获取所有启用模板的摘要,供 AI system_msg 注入"""
templates = self.sudo().search([('active', '=', True)])
if not templates:
return ''
@@ -72,7 +180,6 @@ class AiReportTemplate(models.Model):
f'- 模板"{t.name}":ID={t.id},模型={t.model_name},'
f'字段={fields_str},说明={t.description or "无"}'
)
# 注入参数定义信息
if t.param_definitions:
try:
param_defs = json.loads(t.param_definitions)
@@ -87,6 +194,7 @@ class AiReportTemplate(models.Model):
lines.append(base_info)
return '\n'.join(lines)
# ===== onchange =====
@api.onchange('model_id')
def _onchange_model_id(self):
if self.model_id:
@@ -95,13 +203,13 @@ class AiReportTemplate(models.Model):
@api.onchange('field_ids')
def _onchange_field_ids(self):
if self.field_ids:
import json
self.field_list = json.dumps([f.name for f in self.field_ids], ensure_ascii=False)
self.field_labels = json.dumps(
{f.name: f.field_description for f in self.field_ids},
ensure_ascii=False
)
# ===== create / write 同步 =====
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@@ -122,17 +230,16 @@ class AiReportTemplate(models.Model):
@api.model
def _sync_field_list_from_ids(self, vals):
"""从 field_ids (M2M command list) 中提取字段名并同步到 field_list 和 field_labels"""
import json
ids = []
field_cmds = vals['field_ids']
if isinstance(field_cmds, list):
for cmd in field_cmds:
if isinstance(cmd, (list, tuple)):
if cmd[0] == 6: # Replace all
if cmd[0] == 6:
ids = cmd[2]
break
elif cmd[0] == 4: # Add
elif cmd[0] == 4:
ids.append(cmd[1])
if ids:
fields_records = self.env['ir.model.fields'].browse(ids)
@@ -21,3 +21,15 @@ access_ai_report_user,动态报表-用户,model_ai_report,yuthon_ai_agent.group_
access_ai_report_admin,动态报表-管理员,model_ai_report,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_user,报表模板-用户只读,model_ai_report_template,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_admin,报表模板-管理员,model_ai_report_template,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_column_user,列定义-用户只读,model_ai_report_template_column,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_column_admin,列定义-管理员,model_ai_report_template_column,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_card_user,汇总卡片-用户只读,model_ai_report_template_card,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_card_admin,汇总卡片-管理员,model_ai_report_template_card,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_group_by_user,分组-用户只读,model_ai_report_template_group_by,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_group_by_admin,分组-管理员,model_ai_report_template_group_by,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_color_rule_user,颜色规则-用户只读,model_ai_report_template_color_rule,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_color_rule_admin,颜色规则-管理员,model_ai_report_template_color_rule,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_filter_user,筛选器-用户只读,model_ai_report_template_filter,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
21 access_ai_report_admin 动态报表-管理员 model_ai_report yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
22 access_ai_report_template_user 报表模板-用户只读 model_ai_report_template yuthon_ai_agent.group_ai_agent_user 1 0 0 0
23 access_ai_report_template_admin 报表模板-管理员 model_ai_report_template yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
24 access_ai_report_template_column_user 列定义-用户只读 model_ai_report_template_column yuthon_ai_agent.group_ai_agent_user 1 0 0 0
25 access_ai_report_template_column_admin 列定义-管理员 model_ai_report_template_column yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
26 access_ai_report_template_card_user 汇总卡片-用户只读 model_ai_report_template_card yuthon_ai_agent.group_ai_agent_user 1 0 0 0
27 access_ai_report_template_card_admin 汇总卡片-管理员 model_ai_report_template_card yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
28 access_ai_report_template_group_by_user 分组-用户只读 model_ai_report_template_group_by yuthon_ai_agent.group_ai_agent_user 1 0 0 0
29 access_ai_report_template_group_by_admin 分组-管理员 model_ai_report_template_group_by yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
30 access_ai_report_template_color_rule_user 颜色规则-用户只读 model_ai_report_template_color_rule yuthon_ai_agent.group_ai_agent_user 1 0 0 0
31 access_ai_report_template_color_rule_admin 颜色规则-管理员 model_ai_report_template_color_rule yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
32 access_ai_report_template_filter_user 筛选器-用户只读 model_ai_report_template_filter yuthon_ai_agent.group_ai_agent_user 1 0 0 0
33 access_ai_report_template_filter_admin 筛选器-管理员 model_ai_report_template_filter yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
34 access_ai_report_template_param_def_user 参数定义-用户只读 model_ai_report_template_param_def yuthon_ai_agent.group_ai_agent_user 1 0 0 0
35 access_ai_report_template_param_def_admin 参数定义-管理员 model_ai_report_template_param_def yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
+107 -23
View File
@@ -128,51 +128,135 @@
bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<h1>
<field name="name" placeholder="如:销售订单列表模板"/>
</h1>
<table style="width:100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="width:60%">
<h1><field name="name" placeholder="如:销售订单列表模板"/></h1>
</td>
<td style="width:40%;text-align:right;vertical-align:bottom">
<button name="action_preview" type="object"
string="📊 预览报表" class="oe_highlight"
invisible="not model_name"/>
</td>
</tr>
</table>
</div>
<group>
<group string="模板配置">
<group string="数据源配置">
<field name="model_id"
placeholder="选择数据模型,如 sale.order"
options="{'no_create': True}"/>
<field name="model_name"
readonly="model_id != False" required="model_id == False"
placeholder="如 model_id 不含所需模型时可手动输入"/>
<field name="default_domain"
placeholder="[['state','=','sale']]"/>
<field name="default_order"
placeholder="date_order desc"/>
<field name="default_visible"/>
</group>
<group string="显示字段">
<field name="field_ids" widget="many2many_tags"
domain="[('model_id','=',model_id)]"
options="{'no_create': True}"
placeholder="选择要显示的字段"/>
<field name="field_list" invisible="1"/>
<field name="field_labels" invisible="1"/>
<field name="default_domain"
placeholder="[['state','=','sale']]"/>
<field name="default_order"
placeholder="date_order desc"/>
<field name="report_config" placeholder='{"columns":[{"k":"name","l":"字段名","w":120}],"cards":[{"label":"汇总","field":"id","calc":"count"}],"groupBy":[{"key":"state","label":"状态"}],"defaultVisible":10,"colorRules":[],"filters":[]}'/>
<field name="description" nolabel="1"
placeholder="描述此模板的用途,如:当用户查询销售订单列表时使用此模板"/>
</group>
<group string="其他">
<group string="基本设置">
<field name="sequence"/>
<field name="active"/>
</group>
</group>
<group>
<button name="action_preview" type="object"
string="📊 预览报表" class="oe_highlight"
invisible="not model_name"/>
</group>
<notebook>
<page string="📋 列定义" name="columns">
<field name="column_ids" nolabel="1">
<tree string="列定义" editable="bottom">
<field name="field_name" placeholder="如 name"/>
<field name="label" placeholder="显示名称"/>
<field name="width"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<group string="使用说明">
<field name="description" nolabel="1"
placeholder="描述此模板的用途,如:当用户查询销售订单列表时使用此模板"/>
</group>
<group string="参数定义">
<field name="param_definitions" nolabel="1"
placeholder='[{"name":"date_from","label":"开始日期","type":"date","domain_field":"date_order","op":">=","description":"查询起始日期"}]'/>
</group>
<page string="📊 汇总卡片" name="cards">
<field name="card_ids" nolabel="1">
<tree string="汇总卡片" editable="bottom">
<field name="label" placeholder="卡片标签"/>
<field name="field_name" placeholder="统计字段名"/>
<field name="calc"/>
<field name="prefix" placeholder="¥"/>
<field name="unit" placeholder="万"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="🔀 分组 & 筛选" name="group_filters">
<group string="分组设置">
<field name="group_by_ids" nolabel="1">
<tree string="分组" editable="bottom">
<field name="field_name" placeholder="分组字段"/>
<field name="label" placeholder="显示名称"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</group>
<group string="筛选器" style="margin-top:16px">
<field name="filter_ids" nolabel="1">
<tree string="筛选器" editable="bottom">
<field name="field_name" placeholder="字段名"/>
<field name="label" placeholder="显示名称"/>
<field name="filter_type"/>
<field name="options" placeholder="逗号分隔选项"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</group>
</page>
<page string="🎨 颜色规则" name="colors">
<field name="color_rule_ids" nolabel="1">
<tree string="颜色规则" editable="bottom">
<field name="field_name" placeholder="字段名"/>
<field name="op"/>
<field name="value" placeholder="匹配值"/>
<field name="color" widget="color_picker"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="🤖 AI 参数定义" name="params">
<field name="param_def_ids" nolabel="1">
<tree string="参数定义" editable="bottom">
<field name="name" placeholder="如 date_from"/>
<field name="label" placeholder="显示名"/>
<field name="param_type"/>
<field name="domain_field" placeholder="映射字段"/>
<field name="op" placeholder=">="/>
<field name="description" placeholder="AI 提取说明"/>
<field name="required"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="⚙️ JSON 配置(高级)" name="json_config"
groups="yuthon_ai_agent.group_ai_agent_admin">
<group string="报告配置 JSON(自动生成,可手动编辑覆盖)">
<field name="report_config" nolabel="1"/>
</group>
<group string="参数定义 JSON(自动生成,可手动编辑覆盖)">
<field name="param_definitions" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>