报表
This commit is contained in:
@@ -4,7 +4,8 @@ import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -20,17 +21,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 何时使用此模板')
|
||||
@@ -39,33 +40,34 @@ 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')
|
||||
@@ -210,7 +212,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
|
||||
@@ -225,108 +227,243 @@ class AiReportTemplate(models.Model):
|
||||
if self.model_id:
|
||||
self.model_name = self.model_id.model
|
||||
|
||||
# ===== field_ids 变化 → 联动 field_list / field_labels / column_ids =====
|
||||
@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_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)
|
||||
else:
|
||||
self.field_list = '[]'
|
||||
self.field_labels = '{}'
|
||||
self._sync_columns()
|
||||
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 'field_ids' in vals:
|
||||
if vals.get('field_ids'):
|
||||
self._sync_field_list_from_ids(vals)
|
||||
records = super().create(vals_list)
|
||||
# 使用 sudo 避免权限问题,使用 with_context 避免约束检查冲突
|
||||
for record in records:
|
||||
record._sync_columns()
|
||||
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
|
||||
changed = 'field_ids' in vals
|
||||
if changed:
|
||||
|
||||
# 处理 field_ids 变化
|
||||
if vals.get('field_ids'):
|
||||
self._sync_field_list_from_ids(vals)
|
||||
|
||||
result = super().write(vals)
|
||||
if changed:
|
||||
for record in self:
|
||||
record._sync_columns()
|
||||
|
||||
# 同步 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_columns(self):
|
||||
"""显示字段多了 → 列定义跟着加;显示字段少了 → 列定义跟着删"""
|
||||
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()
|
||||
|
||||
# 现有列按 field_id 索引(重复取第一条,多余的记下来删掉)
|
||||
col_by_fid = {}
|
||||
trash = []
|
||||
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 0
|
||||
fid = col.field_id.id if col.field_id else None
|
||||
|
||||
# 没有关联 field_id 的列直接标记删除
|
||||
if not fid:
|
||||
trash.append(col.id)
|
||||
elif fid in col_by_fid:
|
||||
trash.append(col.id)
|
||||
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:
|
||||
col_by_fid[fid] = col
|
||||
valid_columns[fid] = col
|
||||
|
||||
field_ids = set(self.field_ids.ids)
|
||||
cmds = [(3, cid) for cid in trash]
|
||||
# 第二步:安全删除无效的列(使用 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}")
|
||||
|
||||
# 应该保留的列 → (4, id) 告诉 Odoo "这条别删"
|
||||
for fid, col in col_by_fid.items():
|
||||
if fid in field_ids:
|
||||
cmds.append((4, col.id))
|
||||
# 第三步:更新保留的列,确保 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
|
||||
|
||||
# 显示字段有、列定义没有 → 加
|
||||
seq = max((c.sequence for c in self.column_ids if c.sequence), default=0) + 10
|
||||
for f in self.field_ids:
|
||||
if f.id not in col_by_fid:
|
||||
cmds.append((0, 0, {
|
||||
'field_id': f.id,
|
||||
'field_name': f.name,
|
||||
'label': f.field_description,
|
||||
'sequence': seq,
|
||||
}))
|
||||
seq += 10
|
||||
# 第四步:添加缺失的字段
|
||||
existing_fids = set(valid_columns.keys())
|
||||
missing_fids = new_field_ids - existing_fids
|
||||
|
||||
# 列定义有、显示字段没有 → 删
|
||||
for fid, col in col_by_fid.items():
|
||||
if fid not in field_ids:
|
||||
cmds.append((3, col.id))
|
||||
if missing_fids:
|
||||
# 计算新的序列值
|
||||
max_seq = max([col.sequence for col in self.column_ids], default=0)
|
||||
seq = max_seq + 10
|
||||
|
||||
if cmds:
|
||||
self.column_ids = cmds
|
||||
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 = []
|
||||
for cmd in (vals['field_ids'] if isinstance(vals['field_ids'], list) else []):
|
||||
if isinstance(cmd, (list, tuple)):
|
||||
if cmd[0] == 6:
|
||||
ids = list(cmd[2]) if cmd[2] else []
|
||||
break
|
||||
elif cmd[0] == 4:
|
||||
ids.append(cmd[1])
|
||||
elif cmd[0] == 5:
|
||||
ids = []
|
||||
break
|
||||
field_cmds = vals.get('field_ids', [])
|
||||
|
||||
# 解析命令格式
|
||||
if isinstance(field_cmds, list):
|
||||
for cmd in field_cmds:
|
||||
if isinstance(cmd, (list, tuple)):
|
||||
if cmd[0] == 6: # replace all
|
||||
ids = cmd[2]
|
||||
break
|
||||
elif cmd[0] == 4: # add
|
||||
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 = self.env['ir.model.fields'].browse(ids)
|
||||
vals['field_list'] = json.dumps([f.name for f in fields], ensure_ascii=False)
|
||||
vals['field_labels'] = json.dumps(
|
||||
{f.name: f.field_description for f in fields}, ensure_ascii=False)
|
||||
fields_records = self.env['ir.model.fields'].browse(ids)
|
||||
if not vals.get('field_list'):
|
||||
vals['field_list'] = json.dumps([f.name for f in fields_records], ensure_ascii=False)
|
||||
if not vals.get('field_labels'):
|
||||
vals['field_labels'] = json.dumps(
|
||||
{f.name: f.field_description for f in fields_records},
|
||||
ensure_ascii=False
|
||||
)
|
||||
else:
|
||||
vals['field_list'] = '[]'
|
||||
vals['field_labels'] = '{}'
|
||||
# 如果没有字段,清空
|
||||
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
|
||||
Reference in New Issue
Block a user