报表
This commit is contained in:
@@ -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>🔍</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){
|
||||
|
||||
@@ -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="🔀 分组 & 筛选" name="group_filters">
|
||||
<page string="分组 & 筛选" 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>
|
||||
|
||||
Reference in New Issue
Block a user