From b34913ea8a720037d9a91d90ad6fc41b51d59c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Fri, 5 Jun 2026 14:52:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_ai_agent/models/ai_report_template.py | 274 ++----------------- 1 file changed, 27 insertions(+), 247 deletions(-) diff --git a/yuthon_ai_agent/models/ai_report_template.py b/yuthon_ai_agent/models/ai_report_template.py index d9eaf5ac9..14fbe9ccb 100644 --- a/yuthon_ai_agent/models/ai_report_template.py +++ b/yuthon_ai_agent/models/ai_report_template.py @@ -4,8 +4,7 @@ import json import logging import re -from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError +from odoo import models, fields, api from datetime import timedelta _logger = logging.getLogger(__name__) @@ -21,17 +20,17 @@ class AiReportTemplate(models.Model): model_name = fields.Char('数据模型', required=True, help='Odoo 模型技术名称,如 sale.order') model_id = fields.Many2one('ir.model', string='数据模型', ondelete='cascade', - help='选择数据模型,选择后自动填充"数据模型"字段') + help='选择数据模型,选择后自动填充"数据模型"字段') field_list = fields.Text('字段列表', help='JSON 数组,如 ["name","date_order","partner_id","amount_total"]') field_labels = fields.Text('字段标签', help='JSON 对象,如 {"name":"订单号","amount_total":"金额"}') field_ids = fields.Many2many('ir.model.fields', 'ai_report_template_field_rel', - 'template_id', 'field_id', string='显示字段', - domain="[('model_id','=',model_id)]", - help='选择要显示的字段。选择后自动填充"字段列表"和"字段标签"') + 'template_id', 'field_id', string='显示字段', + domain="[('model_id','=',model_id)]", + help='选择要显示的字段。选择后自动填充"字段列表"和"字段标签"') default_domain = fields.Char('默认查询条件', default="[]", - help='Odoo domain 格式,如 [("state","=","sale"),("amount_total",">",1000)]') + help='Odoo domain 格式,如 [("state","=","sale"),("amount_total",">",1000)]') default_order = fields.Char('默认排序', default='id desc') description = fields.Text('使用说明', translate=True, help='告诉 AI 何时使用此模板') @@ -40,34 +39,33 @@ class AiReportTemplate(models.Model): # ===== 视觉配置(拆分为 One2many) ===== column_ids = fields.One2many('ai.report.template.column', 'template_id', - string='列定义', copy=True) + string='列定义', copy=True) card_ids = fields.One2many('ai.report.template.card', 'template_id', - string='汇总卡片', copy=True) + string='汇总卡片', copy=True) group_by_ids = fields.Many2many('ir.model.fields', - 'ai_report_template_group_rel', 'template_id', 'field_id', - string='分组设置', copy=True, - domain="[('model_id', '=', model_id)]") + 'ai_report_template_group_rel', 'template_id', 'field_id', + string='分组设置', copy=True, + domain="[('model_id', '=', model_id)]") filter_ids = fields.Many2many('ir.model.fields', - 'ai_report_template_filter_rel', 'template_id', 'field_id', - string='筛选器', copy=True, - domain="[('model_id', '=', model_id)]") + 'ai_report_template_filter_rel', 'template_id', 'field_id', + string='筛选器', copy=True, + domain="[('model_id', '=', model_id)]") column_field_ids = fields.Many2many('ir.model.fields', - string='列定义字段集合', - compute='_compute_column_field_ids', - help='列定义中已配置的字段,用于限制分组/筛选器的可选范围') + string='列定义字段集合', + compute='_compute_column_field_ids', + help='列定义中已配置的字段,用于限制分组/筛选器的可选范围') param_def_ids = fields.One2many('ai.report.template.param.def', 'template_id', - string='AI 参数定义', copy=True) + 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') + 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') + compute='_compute_param_definitions', inverse='_inverse_param_definitions', store=True, + help='自动从参数定义行生成,也可手动编辑 JSON') # ===== Computed: column_field_ids ===== @api.depends('column_ids.field_id') @@ -212,7 +210,7 @@ class AiReportTemplate(models.Model): param_defs = json.loads(t.param_definitions) if param_defs: param_info = ',参数: ' + ', '.join([ - f'{p.get("name", "")}({p.get("label", "")}:{p.get("type", "")})' + f'{p.get("name","")}({p.get("label","")}:{p.get("type","")})' for p in param_defs ]) base_info += param_info @@ -227,189 +225,19 @@ class AiReportTemplate(models.Model): if self.model_id: self.model_name = self.model_id.model - @api.onchange('field_ids') - def _onchange_field_ids(self): - if self.field_ids: - 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 - ) - # 同步 column_ids:去重 + 删不在的 + 补缺失的 + 修复空字段 - self._sync_column_ids_from_field_ids() - - # ===== create / write 同步 ===== - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get('model_id') and not vals.get('model_name'): - model = self.env['ir.model'].browse(vals['model_id']) - vals['model_name'] = model.model - if vals.get('field_ids'): - self._sync_field_list_from_ids(vals) - records = super().create(vals_list) - # 使用 sudo 避免权限问题,使用 with_context 避免约束检查冲突 - for record in records: - if record.field_ids: - record.with_context(sync_columns=True)._sync_column_ids_from_field_ids() - return records - - def write(self, vals): - # 处理 model_id 变化 - if vals.get('model_id') and 'model_name' not in vals: - model = self.env['ir.model'].browse(vals['model_id']) - vals['model_name'] = model.model - - # 处理 field_ids 变化 - if vals.get('field_ids'): - self._sync_field_list_from_ids(vals) - - result = super().write(vals) - - # 同步 column_ids(使用 context 标记避免循环) - for record in self: - if record.field_ids and not self.env.context.get('syncing_columns'): - record.with_context(syncing_columns=True)._sync_column_ids_from_field_ids() - - return result - - def _sync_column_ids_from_field_ids(self): - """ - 根据 field_ids 同步 column_ids: - 1. 删除不在 field_ids 中的列 - 2. 添加 field_ids 中缺失的列 - 3. 清理重复的列 - 4. 修复空 field_name - - 使用安全的删除策略避免约束冲突 - """ - self.ensure_one() - - if not self.field_ids: - # 如果没有选择字段,清空所有列定义 - if self.column_ids: - self.column_ids.unlink() - return - - new_field_ids = set(self.field_ids.ids) - - # 第一步:收集所有有效的列定义 - valid_columns = {} - columns_to_remove = self.env['ai.report.template.column'] - - for col in self.column_ids: - fid = col.field_id.id if col.field_id else None - - # 没有关联 field_id 的列直接标记删除 - if not fid: - columns_to_remove |= col - continue - - # 如果字段不在 field_ids 中,标记删除 - if fid not in new_field_ids: - columns_to_remove |= col - continue - - # 处理重复:同一个 field_id 只保留第一个 - if fid in valid_columns: - columns_to_remove |= col - else: - valid_columns[fid] = col - - # 第二步:安全删除无效的列(使用 unlink 而不是命令删除) - if columns_to_remove: - try: - columns_to_remove.unlink() - except Exception as e: - _logger.warning(f"删除列定义失败: {e}") - # 如果直接删除失败,尝试逐个删除 - for col in columns_to_remove: - try: - col.unlink() - except Exception as e2: - _logger.warning(f"删除列 {col.id} 失败: {e2}") - - # 第三步:更新保留的列,确保 field_name 不为空 - for fid, col in valid_columns.items(): - if not col.field_name: - field = self.field_ids.filtered(lambda r: r.id == fid) - if field: - col.field_name = field.name - - # 第四步:添加缺失的字段 - existing_fids = set(valid_columns.keys()) - missing_fids = new_field_ids - existing_fids - - if missing_fids: - # 计算新的序列值 - max_seq = max([col.sequence for col in self.column_ids], default=0) - seq = max_seq + 10 - - for fid in missing_fids: - field = self.field_ids.filtered(lambda r: r.id == fid) - if field: - # 直接创建新的 column 记录 - self.env['ai.report.template.column'].create({ - 'template_id': self.id, - 'field_id': fid, - 'field_name': field.name, - 'label': field.field_description, - 'sequence': seq, - 'width': 120, - }) - seq += 10 - - # 第五步:清理重复(如果有重复的 field_name,保留一个) - self._cleanup_duplicate_columns() - - def _cleanup_duplicate_columns(self): - """ - 清理重复的列定义(基于 field_name) - """ - self.ensure_one() - - field_name_map = {} - duplicates = self.env['ai.report.template.column'] - - for col in self.column_ids: - if not col.field_name: - continue - if col.field_name in field_name_map: - duplicates |= col - else: - field_name_map[col.field_name] = col - - if duplicates: - try: - duplicates.unlink() - except Exception as e: - _logger.warning(f"清理重复列失败: {e}") - @api.model def _sync_field_list_from_ids(self, vals): - """从 field_ids 同步 field_list 和 field_labels""" import json ids = [] - field_cmds = vals.get('field_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]) - elif cmd[0] == 5: # remove all - ids = [] - break - elif cmd[0] == 3: # remove - # 需要从现有记录中移除,这里简化处理 - pass - elif isinstance(field_cmds, int): - ids = [field_cmds] - if ids: fields_records = self.env['ir.model.fields'].browse(ids) if not vals.get('field_list'): @@ -418,52 +246,4 @@ class AiReportTemplate(models.Model): vals['field_labels'] = json.dumps( {f.name: f.field_description for f in fields_records}, ensure_ascii=False - ) - else: - # 如果没有字段,清空 - if not vals.get('field_list'): - vals['field_list'] = '[]' - if not vals.get('field_labels'): - vals['field_labels'] = '{}' - - -class AiReportTemplateColumn(models.Model): - _name = 'ai.report.template.column' - _description = '报表模板列定义' - _order = 'sequence, id' - - template_id = fields.Many2one('ai.report.template', string='模板', required=True, ondelete='cascade') - field_id = fields.Many2one('ir.model.fields', string='字段', - domain="[('model_id', '=', template_id.model_id)]") - field_name = fields.Char('字段名', required=True) # 改为必填 - label = fields.Char('显示名称') - width = fields.Integer('宽度', default=120) - sequence = fields.Integer('排序', default=10) - - # 颜色配置 - cell_color = fields.Char('单元格颜色', help='十六进制颜色值,如 #FF0000') - color_operator = fields.Selection([ - ('eq', '等于'), - ('neq', '不等于'), - ('gt', '大于'), - ('lt', '小于'), - ('gte', '大于等于'), - ('lte', '小于等于'), - ('contains', '包含'), - ], string='条件运算符', default='eq') - color_value = fields.Char('条件值') - - @api.constrains('field_name') - def _check_field_name(self): - """确保 field_name 不为空""" - for record in self: - if not record.field_name: - raise ValidationError(_("列定义的字段名不能为空")) - - @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 \ No newline at end of file + ) \ No newline at end of file From 021f58c3d90cbe1f7a960cdca8ce046ba63e19d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Fri, 5 Jun 2026 14:53:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_ai_agent/views/ai_report_views.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yuthon_ai_agent/views/ai_report_views.xml b/yuthon_ai_agent/views/ai_report_views.xml index cba9f6dee..ea0dd1ae2 100644 --- a/yuthon_ai_agent/views/ai_report_views.xml +++ b/yuthon_ai_agent/views/ai_report_views.xml @@ -159,10 +159,10 @@ placeholder="添加筛选器..."/> - + + + +