报表
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 中所有 <field> 节点
|
||||
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):
|
||||
"""生成固定报表链接(按钮动作)
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
<button name="action_preview" type="object"
|
||||
string="预览报表" class="oe_highlight"
|
||||
invisible="not model_name"/>
|
||||
<button name="action_init_columns" type="object"
|
||||
string="初始化列" class="btn-secondary"
|
||||
icon="fa-magic"
|
||||
invisible="not model_name"
|
||||
help="从模型的 tree 视图中提取可见字段,批量生成列定义"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user