From 46d2ad6c03be95a2e5494d096244e2f1bf184ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 6 Jun 2026 22:19:25 +0800 Subject: [PATCH] =?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/__manifest__.py | 1 + yuthon_ai_agent/models/ai_report_template.py | 151 +++++++++++++++++++ yuthon_ai_agent/views/ai_report_views.xml | 5 + 3 files changed, 157 insertions(+) diff --git a/yuthon_ai_agent/__manifest__.py b/yuthon_ai_agent/__manifest__.py index dbce95320..54899dd20 100644 --- a/yuthon_ai_agent/__manifest__.py +++ b/yuthon_ai_agent/__manifest__.py @@ -39,6 +39,7 @@ 'yuthon_ai_agent/static/src/xml/ai_chat_template.xml', 'yuthon_ai_agent/static/src/css/ai_chat.css', 'yuthon_ai_agent/static/src/css/ai_floating.css', + 'yuthon_ai_agent/static/src/css/ai_report_backend.css', ], }, 'installable': True, diff --git a/yuthon_ai_agent/models/ai_report_template.py b/yuthon_ai_agent/models/ai_report_template.py index 43d39dbab..e157524e1 100644 --- a/yuthon_ai_agent/models/ai_report_template.py +++ b/yuthon_ai_agent/models/ai_report_template.py @@ -6,6 +6,7 @@ import re from odoo import models, fields, api from datetime import timedelta +from lxml import etree _logger = logging.getLogger(__name__) @@ -308,6 +309,156 @@ class AiReportTemplate(models.Model): else: t.fixed_report_url = False + # ===== 初始化列:从模型 tree 视图批量导入 ===== + def action_init_columns(self): + """从模型的默认 tree 视图中提取可见字段,批量生成列定义 + + - 查找模型优先级最高的 tree 视图 + - 解析 arch,跳过 invisible / column_invisible / optional=hide 的字段 + - 跳过系统技术字段(id, create_uid, create_date, write_uid, write_date, display_name) + - 仅添加当前 column_ids 中不存在的字段 + """ + self.ensure_one() + if not self.model_name: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '无法初始化', + 'message': '请先选择数据模型', + 'type': 'warning', + } + } + + # 1. 查找默认 tree 视图 + View = self.env['ir.ui.view'] + views = View.sudo().search([ + ('model', '=', self.model_name), + ('type', '=', 'tree'), + ('mode', 'in', ('primary', 'extension')), + ], order='priority desc, id asc') + + # 优先取 primary 视图;若无则取 extension 中第一个 + tree_view = views.filtered(lambda v: v.mode == 'primary')[:1] or views[:1] + if not tree_view: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '未找到视图', + 'message': f'模型 {self.model_name} 没有 tree 视图,无法自动初始化列', + 'type': 'warning', + } + } + + # 2. 解析 arch(处理 view inheritance) + try: + arch_et = tree_view.sudo()._get_combined_arch() + except Exception as e: + _logger.warning('解析 tree 视图 arch 失败: view=%s, error=%s', tree_view.xml_id or tree_view.id, e) + arch_et = etree.fromstring(tree_view.arch.encode('utf-8')) if tree_view.arch else None + + if arch_et is None: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '解析失败', + 'message': '无法解析 tree 视图的 XML 结构', + 'type': 'danger', + } + } + + # 3. 提取可见字段 + tech_fields = {'id', 'create_uid', 'create_date', 'write_uid', 'write_date', 'display_name'} + existing_names = set(self.column_ids.mapped('field_name')) + new_columns = [] + seq_base = max(self.column_ids.mapped('sequence') or [0]) + 10 + + # tree 中所有 节点 + for idx, field_node in enumerate(arch_et.iter('field')): + fname = field_node.get('name') + if not fname: + continue + if fname in tech_fields: + continue + if fname in existing_names: + continue + + # 判断是否为隐藏字段(静态属性 + modifiers JSON) + invisible = field_node.get('invisible') or field_node.get('column_invisible') + if invisible and invisible.strip() not in ('0', 'false', 'False', ''): + continue + modifiers = field_node.get('modifiers') + if modifiers: + try: + mods = json.loads(modifiers) + if mods.get('column_invisible') or mods.get('invisible'): + continue + except (json.JSONDecodeError, TypeError): + pass + if field_node.get('optional') == 'hide': + continue + + # 获取显示名称:优先 arch 中的 string 属性,其次字段元数据 + label = field_node.get('string') + field_rec = self.env['ir.model.fields'].sudo().search([ + ('model_id', '=', self.model_id.id), + ('name', '=', fname), + ], limit=1) + if not label: + label = field_rec.field_description if field_rec else fname + + col_data = { + 'field_name': fname, + 'label': label, + 'width': 120, + 'sequence': seq_base + idx * 10, + } + if field_rec: + col_data['field_id'] = field_rec.id + new_columns.append(col_data) + + if not new_columns: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '无需初始化', + 'message': '所有可见字段已存在于列定义中,或该模型 tree 视图无可用字段', + 'type': 'info', + } + } + + # 4. 批量创建列定义 + Column = self.env['ai.report.template.column'] + created = 0 + for col_vals in new_columns: + try: + Column.create(dict(col_vals, template_id=self.id)) + created += 1 + except Exception as e: + _logger.warning('创建列定义失败: template=%s, field=%s, error=%s', self.name, col_vals['field_name'], e) + + # 5. 同步更新 field_list / field_labels(兼容性) + all_fields = self.column_ids.mapped('field_name') + self.field_list = json.dumps(all_fields, ensure_ascii=False) + field_labels = {} + for c in self.column_ids: + if c.field_name and c.label: + field_labels[c.field_name] = c.label + self.field_labels = json.dumps(field_labels, ensure_ascii=False) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '初始化完成', + 'message': f'已从 tree 视图「{tree_view.name or tree_view.xml_id or tree_view.id}」导入 {created} 个字段到列定义', + 'type': 'success', + } + } + # ===== 固定报表:生成/刷新动作 ===== def action_generate_fixed_report(self): """生成固定报表链接(按钮动作) diff --git a/yuthon_ai_agent/views/ai_report_views.xml b/yuthon_ai_agent/views/ai_report_views.xml index f5c6c1bd9..e9efba0f5 100644 --- a/yuthon_ai_agent/views/ai_report_views.xml +++ b/yuthon_ai_agent/views/ai_report_views.xml @@ -140,6 +140,11 @@