278 lines
11 KiB
Python
278 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
import json
|
||
import logging
|
||
|
||
from odoo import models, fields, api, _
|
||
from odoo.exceptions import ValidationError
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
|
||
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')
|
||
model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
|
||
field_id = fields.Many2one('ir.model.fields', string='字段',
|
||
domain="[('model_id', '=', model_id)]",
|
||
help='选择模型字段,自动填充字段名和显示名称')
|
||
field_name = fields.Char('字段名', required=True) # 保持 required=True
|
||
label = fields.Char('显示名称',
|
||
help='不填则默认使用字段名')
|
||
width = fields.Integer('列宽', default=120)
|
||
color_operator = fields.Selection([
|
||
('eq', '等于'),
|
||
('neq', '不等于'),
|
||
('gt', '大于'),
|
||
('lt', '小于'),
|
||
('gte', '大于等于'),
|
||
('lte', '小于等于'),
|
||
('contains', '包含'),
|
||
], string='操作符')
|
||
color_value = fields.Char('匹配值',
|
||
help='匹配的值,如"已完成"或"100"')
|
||
cell_color = fields.Char('显示颜色', default='#5DCAA5',
|
||
help='匹配成功时显示的颜色,如 #E74C3C(红)、#5DCAA5(绿)')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@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
|
||
|
||
# 添加 create 和 write 方法确保 field_name 有值
|
||
@api.model_create_multi
|
||
def create(self, vals_list):
|
||
for vals in vals_list:
|
||
# 如果有 field_id 但没有 field_name,自动填充
|
||
if vals.get('field_id') and not vals.get('field_name'):
|
||
field = self.env['ir.model.fields'].browse(vals['field_id'])
|
||
if field.exists():
|
||
vals['field_name'] = field.name
|
||
if not vals.get('label'):
|
||
vals['label'] = field.field_description
|
||
# 如果既没有 field_id 也没有 field_name,抛出友好错误
|
||
if not vals.get('field_name'):
|
||
raise ValidationError(_('列定义必须指定字段名(field_name)或选择字段(field_id)'))
|
||
return super().create(vals_list)
|
||
|
||
def write(self, vals):
|
||
# 处理 write 操作
|
||
if vals.get('field_id') and not vals.get('field_name'):
|
||
field = self.env['ir.model.fields'].browse(vals['field_id'])
|
||
if field.exists():
|
||
vals['field_name'] = field.name
|
||
if vals.get('label') is None and not self.label:
|
||
vals['label'] = field.field_description
|
||
return super().write(vals)
|
||
|
||
|
||
|
||
class AiReportTemplateCard(models.Model):
|
||
"""报表模板 - 汇总卡片"""
|
||
_name = 'ai.report.template.card'
|
||
_description = '汇总卡片'
|
||
_order = 'sequence, id'
|
||
|
||
template_id = fields.Many2one('ai.report.template', string='模板',
|
||
required=True, ondelete='cascade')
|
||
model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
|
||
field_id = fields.Many2one('ir.model.fields', string='统计字段',
|
||
domain="[('model_id', '=', model_id), ('ttype', 'in', ('integer', 'float', 'monetary'))]",
|
||
help='选择数值型字段,自动填充字段名')
|
||
field_name = fields.Char('统计字段名', required=True) # 保持 required=True
|
||
label = fields.Char('卡片标签', required=True)
|
||
calc = fields.Selection([
|
||
('sum', '求和'),
|
||
('count', '计数'),
|
||
('distinct', '去重'),
|
||
], string='计算方式', default='sum')
|
||
prefix = fields.Char('前缀', help='如 ¥、$ 等')
|
||
unit = fields.Char('单位', help='如 万、% 等')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@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
|
||
|
||
@api.model_create_multi
|
||
def create(self, vals_list):
|
||
for vals in vals_list:
|
||
if vals.get('field_id') and not vals.get('field_name'):
|
||
field = self.env['ir.model.fields'].browse(vals['field_id'])
|
||
if field.exists():
|
||
vals['field_name'] = field.name
|
||
if not vals.get('label'):
|
||
vals['label'] = field.field_description
|
||
if not vals.get('field_name'):
|
||
raise ValidationError(_('汇总卡片必须指定统计字段名(field_name)或选择统计字段(field_id)'))
|
||
return super().create(vals_list)
|
||
|
||
def write(self, vals):
|
||
if vals.get('field_id') and not vals.get('field_name'):
|
||
field = self.env['ir.model.fields'].browse(vals['field_id'])
|
||
if field.exists():
|
||
vals['field_name'] = field.name
|
||
if vals.get('label') is None and not self.label:
|
||
vals['label'] = field.field_description
|
||
return super().write(vals)
|
||
|
||
|
||
class AiReportTemplateColorRule(models.Model):
|
||
"""报表模板 - 颜色规则(隶属于列定义)"""
|
||
_name = 'ai.report.template.color.rule'
|
||
_description = '颜色规则'
|
||
_order = 'sequence, id'
|
||
|
||
column_id = fields.Many2one('ai.report.template.column', string='所属列',
|
||
required=True, ondelete='cascade')
|
||
template_id = fields.Many2one('ai.report.template', string='模板',
|
||
compute='_compute_template_id', store=True)
|
||
op = fields.Selection([
|
||
('eq', '等于'),
|
||
('>=', '大于等于'),
|
||
('<=', '小于等于'),
|
||
('>', '大于'),
|
||
('<', '小于'),
|
||
('contains', '包含'),
|
||
('neq', '不等于'),
|
||
], string='操作符', default='eq')
|
||
value = fields.Char('匹配值', required=True,
|
||
help='匹配的值,如"已完成"或"100"')
|
||
color = fields.Char('颜色', default='#5DCAA5',
|
||
help='十六进制颜色代码,如 #E74C3C(红)、#5DCAA5(绿)、#F39C12(橙)')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@api.depends('column_id.template_id')
|
||
def _compute_template_id(self):
|
||
for r in self:
|
||
r.template_id = r.column_id.template_id if r.column_id else False
|
||
|
||
|
||
class AiReportTemplateParamDef(models.Model):
|
||
"""报表模板 - AI参数定义"""
|
||
_name = 'ai.report.template.param.def'
|
||
_description = 'AI参数定义'
|
||
_order = 'sequence, id'
|
||
|
||
template_id = fields.Many2one('ai.report.template', string='模板',
|
||
required=True, ondelete='cascade')
|
||
model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
|
||
name = fields.Char('参数名', required=True,
|
||
help='AI 识别参数用的键名,如 date_from')
|
||
label = fields.Char('显示名',
|
||
help='给 AI 看的说明,如"开始日期"')
|
||
param_type = fields.Selection([
|
||
('date', '日期'),
|
||
('char', '文本'),
|
||
('select', '选择'),
|
||
('float', '数字'),
|
||
('int', '整数'),
|
||
], string='参数类型', default='char')
|
||
field_id = fields.Many2one('ir.model.fields', string='映射字段',
|
||
domain="[('model_id', '=', model_id)]",
|
||
help='选择要映射的模型字段,自动填充映射字段名')
|
||
domain_field = fields.Char('映射字段名',
|
||
help='映射到模型字段名,如 date_order')
|
||
op = fields.Char('操作符', default='=',
|
||
help='Odoo domain 操作符,如 >=、<=、ilike、=')
|
||
description = fields.Text('说明',
|
||
help='详细说明 AI 如何提取此参数')
|
||
options = fields.Char('选项',
|
||
help='select 类型时,逗号分隔的选项')
|
||
required = fields.Boolean('必填')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@api.onchange('field_id')
|
||
def _onchange_field_id(self):
|
||
if self.field_id:
|
||
self.domain_field = self.field_id.name
|
||
if not self.label:
|
||
self.label = self.field_id.field_description
|
||
|
||
|
||
class AiReportTemplateChart(models.Model):
|
||
"""报表模板 - 图表配置"""
|
||
_name = 'ai.report.template.chart'
|
||
_description = '图表配置'
|
||
_order = 'sequence, id'
|
||
|
||
template_id = fields.Many2one('ai.report.template', string='模板',
|
||
required=True, ondelete='cascade')
|
||
model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
|
||
chart_type = fields.Selection([
|
||
('bar', '柱状图'),
|
||
('line', '折线图'),
|
||
('pie', '饼图'),
|
||
('doughnut', '环形图'),
|
||
('polarArea', '玫瑰图'),
|
||
], string='图表类型', default='bar', required=True)
|
||
label_field_id = fields.Many2one('ir.model.fields', string='分组字段',
|
||
domain="[('model_id', '=', model_id)]", ondelete='set null',
|
||
help='作为图表分类轴的字段(必填)')
|
||
label_field_name = fields.Char('分组字段名',
|
||
compute='_compute_label_field_name', store=True)
|
||
value_field_id = fields.Many2one('ir.model.fields', string='数值字段',
|
||
domain="[('model_id', '=', model_id), ('ttype', 'in', ('integer', 'float', 'monetary'))]",
|
||
ondelete='set null', help='留空则使用计数')
|
||
value_field_name = fields.Char('数值字段名',
|
||
compute='_compute_value_field_name', store=True)
|
||
calc_method = fields.Selection([
|
||
('count', '计数'),
|
||
('sum', '求和'),
|
||
('avg', '平均值'),
|
||
], string='计算方式', default='count')
|
||
title = fields.Char('图表标题')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@api.depends('label_field_id')
|
||
def _compute_label_field_name(self):
|
||
for r in self:
|
||
r.label_field_name = r.label_field_id.name if r.label_field_id else ''
|
||
|
||
@api.depends('value_field_id')
|
||
def _compute_value_field_name(self):
|
||
for r in self:
|
||
r.value_field_name = r.value_field_id.name if r.value_field_id else ''
|
||
|
||
|
||
class AiReportTemplateDomainLine(models.Model):
|
||
_name = 'ai.report.template.domain.line'
|
||
_description = '报表模板-查询条件行'
|
||
_order = 'sequence, id'
|
||
|
||
template_id = fields.Many2one('ai.report.template', string='模板',
|
||
required=True, ondelete='cascade')
|
||
model_id = fields.Many2one('ir.model', related='template_id.model_id', store=True)
|
||
field_id = fields.Many2one('ir.model.fields', string='字段',
|
||
domain="[('model_id', '=', model_id)]",
|
||
help='选择模型字段')
|
||
operator = fields.Selection([
|
||
('=', '等于'),
|
||
('!=', '不等于'),
|
||
('>', '大于'),
|
||
('<', '小于'),
|
||
('>=', '大于等于'),
|
||
('<=', '小于等于'),
|
||
('ilike', '包含'),
|
||
('not ilike', '不包含'),
|
||
('in', '在列表中'),
|
||
('not in', '不在列表中'),
|
||
], string='操作符', default='=')
|
||
value = fields.Char('值', help='匹配的值,多个值用逗号分隔(用于in/not in)')
|
||
sequence = fields.Integer('排序', default=10)
|
||
|
||
@api.onchange('field_id')
|
||
def _onchange_field_id(self):
|
||
if self.field_id and not self.value:
|
||
pass
|