This commit is contained in:
李鹏宇
2026-06-04 21:59:27 +08:00
parent c0b9693612
commit 6934bd8141
3 changed files with 146 additions and 58 deletions
@@ -15,12 +15,23 @@ class AiReportTemplateColumn(models.Model):
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='选择模型字段,自动填充字段名和显示名称')
field_name = fields.Char('字段名', required=True)
label = fields.Char('显示名称',
help='不填则默认使用字段名')
width = fields.Integer('列宽', default=120)
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.field_name = self.field_id.name
if not self.label:
self.label = self.field_id.field_description
class AiReportTemplateCard(models.Model):
"""报表模板 - 汇总卡片"""
@@ -30,8 +41,12 @@ class AiReportTemplateCard(models.Model):
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), ('ttype', 'in', ('integer', 'float', 'monetary'))]",
help='选择数值型字段,自动填充字段名')
field_name = fields.Char('统计字段名', required=True)
label = fields.Char('卡片标签', required=True)
field_name = fields.Char('统计字段', required=True)
calc = fields.Selection([
('sum', '求和'),
('count', '计数'),
@@ -41,6 +56,11 @@ class AiReportTemplateCard(models.Model):
unit = fields.Char('单位', help='如 万、% 等')
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.field_name = self.field_id.name
class AiReportTemplateGroupBy(models.Model):
"""报表模板 - 分组设置"""
@@ -50,10 +70,21 @@ class AiReportTemplateGroupBy(models.Model):
template_id = fields.Many2one('ai.report.template', string='模板',
required=True, ondelete='cascade')
field_name = fields.Char('分组字段', required=True)
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='选择分组字段,自动填充字段名和显示名称')
field_name = fields.Char('分组字段名', required=True)
label = fields.Char('显示名称')
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.field_name = self.field_id.name
if not self.label:
self.label = self.field_id.field_description
class AiReportTemplateColorRule(models.Model):
"""报表模板 - 颜色规则"""
@@ -63,6 +94,10 @@ class AiReportTemplateColorRule(models.Model):
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='选择要应用颜色规则的字段')
field_name = fields.Char('字段名', required=True)
op = fields.Selection([
('eq', '等于'),
@@ -71,12 +106,19 @@ class AiReportTemplateColorRule(models.Model):
('>', '大于'),
('<', '小于'),
('contains', '包含'),
('neq', '不等于'),
], string='操作符', default='eq')
value = fields.Char('值')
value = fields.Char('匹配值', required=True,
help='匹配的值,如"已完成"或"100"')
color = fields.Char('颜色', default='#5DCAA5',
help='十六进制颜色代码')
help='十六进制颜色代码,如 #E74C3C(红)、#5DCAA5(绿)、#F39C12(橙)')
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.field_name = self.field_id.name
class AiReportTemplateFilter(models.Model):
"""报表模板 - 筛选器"""
@@ -86,6 +128,10 @@ class AiReportTemplateFilter(models.Model):
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='选择筛选字段,自动填充字段名和显示名称')
field_name = fields.Char('字段名', required=True)
filter_type = fields.Selection([
('input', '输入框'),
@@ -96,6 +142,13 @@ class AiReportTemplateFilter(models.Model):
help='下拉选择时,逗号分隔的选项列表。如:草稿,已确认,已完成')
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.field_name = self.field_id.name
if not self.label:
self.label = self.field_id.field_description
class AiReportTemplateParamDef(models.Model):
"""报表模板 - AI参数定义"""
@@ -105,6 +158,7 @@ class AiReportTemplateParamDef(models.Model):
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)
name = fields.Char('参数名', required=True,
help='AI 识别参数用的键名,如 date_from')
label = fields.Char('显示名',
@@ -116,7 +170,10 @@ class AiReportTemplateParamDef(models.Model):
('float', '数字'),
('int', '整数'),
], string='参数类型', default='char')
domain_field = fields.Char('映射字段',
field_id = fields.Many2one('ir.model.fields', string='映射字段',
domain="[('model_id', '=', model_id)]",
help='选择要映射的模型字段,自动填充映射字段名')
domain_field = fields.Char('映射字段名',
help='映射到模型字段名,如 date_order')
op = fields.Char('操作符', default='=',
help='Odoo domain 操作符,如 >=、<=、ilike、=')
@@ -126,3 +183,8 @@ class AiReportTemplateParamDef(models.Model):
help='select 类型时,逗号分隔的选项')
required = fields.Boolean('必填')
sequence = fields.Integer('排序', default=10)
@api.onchange('field_id')
def _onchange_field_id(self):
if self.field_id:
self.domain_field = self.field_id.name
@@ -294,11 +294,12 @@ function buildFilters(){
const filters=REPORT_CONFIG.filters||[];
let html='<div class="flt-g wide"><label>&#128269;</label><input id="fG" placeholder="全局搜索..." oninput="rf()"></div>';
filters.forEach((f,i)=>{
if(f.type==='select'||f.filter_type==='select'){
const opts=(f.options||'').split(',').map(o=>o.trim()).filter(Boolean);
html+=`<div class="flt-g"><label>${f.label||f.field_name}</label><select id="flt_${i}" onchange="rf()"><option value="">全部</option>${opts.map(o=>`<option>${o}</option>`).join('')}</select></div>`;
const lbl=f.label||COLS.find(c=>c.k===f.field)?.l||f.field||'';
if(f.type==='select'){
const opts=Array.isArray(f.options)?f.options:(f.options||'').split(',').map(o=>o.trim()).filter(Boolean);
html+=`<div class="flt-g"><label>${lbl}</label><select id="flt_${i}" onchange="rf()"><option value="">全部</option>${opts.map(o=>`<option>${o}</option>`).join('')}</select></div>`;
}else{
html+=`<div class="flt-g"><label>${f.label||f.field_name}</label><input id="flt_${i}" placeholder="全部" oninput="rf()"></div>`;
html+=`<div class="flt-g"><label>${lbl}</label><input id="flt_${i}" placeholder="全部" oninput="rf()"></div>`;
}
});
if(filters.length>0){
@@ -311,15 +312,13 @@ function flt(){
const g=$('fG')?$('fG').value.trim().toLowerCase():'';
const filters=REPORT_CONFIG.filters||[];
return data.filter(r=>{
// 动态筛选器
for(let i=0;i<filters.length;i++){
const f=filters[i];
const el=document.getElementById('flt_'+i);
if(!el||!el.value)continue;
const v=String(r[f.field_name]||'').toLowerCase();
const v=String(r[f.field]||'').toLowerCase();
if(!v.includes(el.value.toLowerCase()))return false;
}
// 全局搜索
if(g&&!Object.values(r).some(v=>String(v).toLowerCase().includes(g)))return false;
return true;
});
@@ -388,8 +387,6 @@ function renderT(dd,fd){
if(r._ancestors&&isAnyAncestorCollapsed(r._ancestors,grpExp))return '';
// Regular data row
const odd=(i%2)?'re':'';
const sa=r.status||'';
const tc=sa==='超额完成'||sa==='超额'?'tg-green':sa==='已完成'||sa==='完成'?'tg-blue':sa==='进行中'||sa==='进行'?'tg-amber':sa==='滞后'?'tg-red':'tg-gray';
// Get active group field keys to auto-hide
const groupHideKeys=grpFields.filter(g=>g.active).map(g=>g.key);
// Row number - restart within each group
@@ -408,7 +405,9 @@ function renderT(dd,fd){
stLeft=calcLeft+'px';
}
if(st.includes('sticky-l'))st='sticky-left-offset';
let h=c.k==='status'?`<span class="tag ${tc}">${v}</span>`:v;
// 应用颜色规则
const cc=getCellColor(c.k, v);
let h=cc?`<span class="tag" style="background:${cc}22;color:${cc}">${v}</span>`:v;
return `<td class="${st}" style="width:${c.w||90}px;${st!=''?`left:${stLeft}`:''}">${h}</td>`;
}).join('')}</tr>`;
}).filter(Boolean).join('');
@@ -511,6 +510,29 @@ function removeGrpField(idx){
r();
}
// ===== 颜色规则匹配 =====
function getCellColor(fieldKey, val){
if(!val&&val!==0)return null;
const rules=REPORT_CONFIG.colorRules||[];
for(const cr of rules){
if(cr.field!==fieldKey)continue;
for(const r of cr.rules||[]){
const v=String(val);
const cv=r.value||'';
switch(r.op){
case 'eq': if(v===cv)return r.color;break;
case 'neq': if(v!==cv)return r.color;break;
case '>': if(parseFloat(v)>parseFloat(cv))return r.color;break;
case '<': if(parseFloat(v)<parseFloat(cv))return r.color;break;
case '>=': if(parseFloat(v)>=parseFloat(cv))return r.color;break;
case '<=': if(parseFloat(v)<=parseFloat(cv))return r.color;break;
case 'contains': if(v.includes(cv))return r.color;break;
}
}
}
return null;
}
// ===== N-level Group Data =====
function grpData(fd,fields){
function buildLevel(items,level,fAncestors){
+47 -43
View File
@@ -41,9 +41,7 @@
<group string="查询参数">
<field name="model_id"
options="{'no_create': True}"/>
<field name="model_name"
readonly="model_id != False" required="model_id == False"
placeholder="如 model_id 不含所需模型时可手动输入"/>
<field name="model_name" invisible="1"/>
<field name="domain"/>
<field name="field_list"/>
<field name="field_labels"/>
@@ -135,7 +133,7 @@
</td>
<td style="width:40%;text-align:right;vertical-align:bottom">
<button name="action_preview" type="object"
string="📊 预览报表" class="oe_highlight"
string="预览报表" class="oe_highlight"
invisible="not model_name"/>
</td>
</tr>
@@ -143,18 +141,13 @@
</div>
<group>
<group string="数据源配置">
<group string="数据源">
<field name="model_id"
placeholder="选择数据模型,如 sale.order"
placeholder="选择数据模型"
options="{'no_create': True}"/>
<field name="model_name"
readonly="model_id != False" required="model_id == False"
placeholder="如 model_id 不含所需模型时可手动输入" invisible="1"/>
<field name="default_domain"
placeholder="[['state','=','sale']]"/>
<field name="default_order"
placeholder="date_order desc"/>
<field name="default_visible"/>
<field name="model_name" invisible="1"/>
<field name="description"
placeholder="描述此模板用途,AI 会根据描述判断何时使用"/>
<field name="sequence"/>
<field name="active"/>
</group>
@@ -162,31 +155,40 @@
<field name="field_ids" widget="many2many_tags"
domain="[('model_id','=',model_id)]"
options="{'no_create': True}"
placeholder="选择要显示的字段"/>
placeholder="选择要显示的字段(选择后自动生成下方配置)"/>
<field name="field_list" invisible="1"/>
<field name="field_labels" invisible="1"/>
<field name="description"
placeholder="描述此模板的用途,如:当用户查询销售订单列表时使用此模板"/>
<field name="default_domain"
placeholder="[['state','=','sale']]"/>
<field name="default_order"
placeholder="date_order desc"/>
<field name="default_visible"/>
</group>
</group>
<notebook>
<page string="📋 列定义" name="columns">
<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="field_id"
placeholder="选择字段..."
options="{'no_create': True}"/>
<field name="field_name" invisible="1"/>
<field name="label" placeholder="显示名称(可选)"/>
<field name="width"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="📊 汇总卡片" name="cards">
<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="field_id"
placeholder="选择数值字段..."
options="{'no_create': True}"/>
<field name="field_name" invisible="1"/>
<field name="calc"/>
<field name="prefix" placeholder="¥"/>
<field name="unit" placeholder="万"/>
@@ -195,12 +197,15 @@
</field>
</page>
<page string="🔀 分组 &amp; 筛选" name="group_filters">
<page string="分组 &amp; 筛选" name="group_filters">
<group string="分组设置">
<field name="group_by_ids">
<tree string="分组" editable="bottom">
<field name="field_name" placeholder="分组字段"/>
<field name="label" placeholder="显示名称"/>
<field name="field_id"
placeholder="选择分组字段..."
options="{'no_create': True}"/>
<field name="field_name" invisible="1"/>
<field name="label" placeholder="显示名称(可选)"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
@@ -208,8 +213,11 @@
<group string="筛选器">
<field name="filter_ids">
<tree string="筛选器" editable="bottom">
<field name="field_name" placeholder="字段名"/>
<field name="label" placeholder="显示名称"/>
<field name="field_id"
placeholder="选择筛选字段..."
options="{'no_create': True}"/>
<field name="field_name" invisible="1"/>
<field name="label" placeholder="显示名称(可选)"/>
<field name="filter_type"/>
<field name="options" placeholder="逗号分隔选项"/>
<field name="sequence" widget="handle"/>
@@ -218,42 +226,38 @@
</group>
</page>
<page string="🎨 颜色规则" name="colors">
<page string="颜色规则" name="colors">
<field name="color_rule_ids" nolabel="1">
<tree string="颜色规则" editable="bottom">
<field name="field_name" placeholder="字段名"/>
<field name="field_id"
placeholder="选择字段..."
options="{'no_create': True}"/>
<field name="field_name" invisible="1"/>
<field name="op"/>
<field name="value" placeholder="匹配值"/>
<field name="color" widget="color_picker"/>
<field name="color"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="🤖 AI 参数定义" name="params">
<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="field_id"
placeholder="选择映射字段..."
options="{'no_create': True}"/>
<field name="domain_field" invisible="1"/>
<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"/>
</group>
<group string="参数定义 JSON(自动生成,可手动编辑覆盖)">
<field name="param_definitions"/>
</group>
</page>
</notebook>
</sheet>
</form>