diff --git a/yuthon_ai_agent/models/ai_report_template_config.py b/yuthon_ai_agent/models/ai_report_template_config.py index cffa03850..16cb990be 100644 --- a/yuthon_ai_agent/models/ai_report_template_config.py +++ b/yuthon_ai_agent/models/ai_report_template_config.py @@ -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 diff --git a/yuthon_ai_agent/static/src/html/report_template.html b/yuthon_ai_agent/static/src/html/report_template.html index 413f885bc..51c3c2191 100644 --- a/yuthon_ai_agent/static/src/html/report_template.html +++ b/yuthon_ai_agent/static/src/html/report_template.html @@ -294,11 +294,12 @@ function buildFilters(){ const filters=REPORT_CONFIG.filters||[]; let html='
'; filters.forEach((f,i)=>{ - if(f.type==='select'||f.filter_type==='select'){ - const opts=(f.options||'').split(',').map(o=>o.trim()).filter(Boolean); - html+=`
`; + 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+=`
`; }else{ - html+=`
`; + html+=`
`; } }); 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;iString(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'?`${v}`:v; + // 应用颜色规则 + const cc=getCellColor(c.k, v); + let h=cc?`${v}`:v; return `${h}`; }).join('')}`; }).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)=': 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){ diff --git a/yuthon_ai_agent/views/ai_report_views.xml b/yuthon_ai_agent/views/ai_report_views.xml index d92d1d3dc..b6802970c 100644 --- a/yuthon_ai_agent/views/ai_report_views.xml +++ b/yuthon_ai_agent/views/ai_report_views.xml @@ -41,9 +41,7 @@ - + @@ -135,7 +133,7 @@