!11921 Merge remote-tracking branch 'origin/dev' into dev

Merge pull request !11921 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-06-05 07:49:57 +00:00
committed by Gitee
4 changed files with 68 additions and 18 deletions
+36 -11
View File
@@ -3,7 +3,7 @@ import json
import logging
import os
from odoo import http, api, SUPERUSER_ID
from odoo import http, api, SUPERUSER_ID, fields
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
@@ -20,16 +20,28 @@ class AiReportController(http.Controller):
def _get_report_by_token(self, token):
"""根据 token 查找并验证报表"""
db_name = request.env.cr.dbname if hasattr(request.env.cr, 'dbname') else 'unknown'
_logger.info(
'报表访问请求: token=%s, db=%s, uid=%s, remote=%s',
token[:12], db_name, request.env.uid,
request.httprequest.remote_addr if hasattr(request, 'httprequest') else 'unknown'
)
report = request.env['ai.report'].sudo().search([
('access_token', '=', token),
], limit=1)
if not report:
_logger.warning('报表不存在: token=%s', token[:12])
_logger.warning(
'报表不存在: token=%s, db=%s, 可能原因: token错误/数据库不匹配/记录已被删除',
token[:12], db_name
)
return None, '报表不存在或已被删除'
if not report.is_valid():
_logger.warning('报表已过期: token=%s, expire=%s, active=%s',
token[:12], report.expire_date, report.active)
return None, '报表已过期或已停用'
_logger.warning('报表已失效: token=%s, db=%s, expire=%s, active=%s',
token[:12], db_name, report.expire_date, report.active)
reason = '报表已过期' if (report.expire_date and report.expire_date < fields.Datetime.now()) else '报表已停用'
return None, f'{reason},请联系分享者重新生成'
_logger.info('报表验证通过: token=%s, report_id=%s, name=%s',
token[:12], report.id, report.name)
return report, None
@http.route('/ai/report/<string:token>', type='http', auth='none',
@@ -83,22 +95,35 @@ class AiReportController(http.Controller):
first = data_records[0]
col_keys = {c.get('k', '') for c in config['columns']}
data_keys = set(first.keys())
# 排除 id 字段(Odoo search_read 总是返回 id)
display_keys = data_keys - {'id'}
missing = col_keys - data_keys
extra = display_keys - col_keys
if missing:
_logger.warning(
'模板 %s report_config 列 key 与数据不匹配: missing=%s, col_keys=%s, data_keys=%s',
template.id, missing, col_keys, data_keys)
# 从数据字段重建列
fields = data.get('fields', list(data_keys - {'id'}))
'模板 %s (id=%s) report_config 列 key 与数据不匹配: '
'missing=%s, extra=%s, col_keys=%s, data_keys=%s',
template.name, template.id, missing, extra, col_keys, data_keys)
# 从数据字段重建列,但尝试保留模板中的 label 和 width
old_col_map = {c.get('k', ''): c for c in config['columns']}
labels = {}
if report.field_labels:
labels = json.loads(report.field_labels)
elif template and template.field_labels:
labels = json.loads(template.field_labels)
fields = data.get('fields', list(display_keys))
config['columns'] = [
{'k': f, 'l': labels.get(f, f), 'w': 120}
for f in fields
{
'k': f,
'l': labels.get(f, old_col_map.get(f, {}).get('l', f)),
'w': old_col_map.get(f, {}).get('w', 120),
}
for f in fields if f != 'id'
]
config['_column_mismatch'] = True # 标记列被重建,前端可用此提示
_logger.info(
'模板 %s: 列已从数据字段重建,共 %s 列', template.name, len(config['columns'])
)
# 2d. 处理数据加载失败
if not data.get('success'):
+5 -1
View File
@@ -489,7 +489,11 @@ class AiConversation(models.Model):
return result
except Exception as e:
_logger.exception('generate_report failed: %s', e)
return {'success': True, 'url': '', 'message': f'报表生成完毕,请检查'} # 失败不报错
return {
'success': False,
'error': f'报表生成失败:{str(e)}',
'message': f'报表生成失败,请稍后重试或联系管理员检查服务端日志。'
}
def _call_chat_completion(self, provider, messages, tools=None):
"""封装一次 HTTP 调用,返回 (assistant_message_dict, error_str)。
+13 -3
View File
@@ -81,13 +81,23 @@ class AiReport(models.Model):
if not base_url:
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
# 如果 web.base.url 是内网地址,检查是否有配置公共 URL
if 'localhost' in base_url or base_url.startswith('http://127.') or \
base_url.startswith('http://10.') or base_url.startswith('http://172.') or \
base_url.startswith('http://192.168'):
_is_internal = (
'localhost' in base_url or
base_url.startswith('http://127.') or base_url.startswith('https://127.') or
base_url.startswith('http://10.') or base_url.startswith('https://10.') or
base_url.startswith('http://172.') or base_url.startswith('https://172.') or
base_url.startswith('http://192.168') or base_url.startswith('https://192.168')
)
if _is_internal:
public_url = self.env['ir.config_parameter'].sudo().get_param(
'yuthon.public.url', '')
if public_url:
base_url = public_url
else:
_logger.warning(
'web.base.url 是内网地址 %s,但未配置 yuthon.public.url,'
'生成的报表链接外部可能无法访问', base_url
)
return f"{base_url}/ai/report/{self.access_token}"
@api.onchange('model_id')
+14 -3
View File
@@ -78,19 +78,30 @@ class AiReportTemplate(models.Model):
'filter_ids', 'default_visible')
def _compute_report_config(self):
for t in self:
# 如果没有任何配置行,保留现有值(兼容旧数据)
if not any([t.column_ids, t.card_ids, t.group_by_ids, t.filter_ids]):
continue
cfg = {}
cols = []
skipped_cols = 0
for c in t.column_ids:
fname = c.field_name or (c.field_id.name if c.field_id else '')
flabel = c.label or fname
if not fname:
skipped_cols += 1
continue
cols.append({'k': fname, 'l': flabel, 'w': c.width or 120})
if skipped_cols > 0:
_logger.warning(
'模板 %s (id=%s): %s 个列定义的字段名为空,已被跳过。'
'请检查列定义中的字段名或字段选择。',
t.name, t.id, skipped_cols
)
# 如果 column_ids 有数据但全部被跳过,需要重置 report_config
if cols:
cfg['columns'] = cols
elif t.column_ids:
# column_ids 存在但全部无效 → 清除旧的 report_config
_logger.warning(
'模板 %s (id=%s): 所有列定义无效,report_config 将被清空。', t.name, t.id
)
cards = []
for c in t.card_ids:
fname_c = c.field_name or (c.field_id.name if c.field_id else '')