ai动态报表
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user