This commit is contained in:
李鹏宇
2026-06-05 00:00:37 +08:00
parent 8c11aebee7
commit 27c69175ff
5 changed files with 126 additions and 38 deletions
@@ -10,7 +10,6 @@
<field name="model_name">sale.order</field>
<field name="field_list">["name","partner_id","date_order","amount_total","state","user_id"]</field>
<field name="field_labels">{"name":"订单号","partner_id":"客户","date_order":"下单日期","amount_total":"金额","state":"状态","user_id":"销售员"}</field>
<field name="default_domain">[["state","!=","cancel"]]</field>
<field name="default_order">date_order desc</field>
<field name="default_visible">5</field>
<field name="report_config"><![CDATA[{"columns":[{"k":"name","l":"订单号","w":130},{"k":"partner_id","l":"客户","w":120},{"k":"date_order","l":"下单日期","w":110},{"k":"amount_total","l":"金额","w":110},{"k":"state","l":"状态","w":80}],"cards":[{"label":"总金额","field":"amount_total","calc":"sum","prefix":"¥"},{"label":"订单数","field":"id","calc":"count"}],"groupBy":[{"key":"state","label":"状态"}],"defaultVisible":5,"colorRules":[{"field":"state","rules":[{"op":"eq","value":"sale","color":"#5DCAA5"},{"op":"eq","value":"draft","color":"#EFC87E"},{"op":"eq","value":"done","color":"#85B7EB"},{"op":"eq","value":"cancel","color":"#ED93B1"}]}],"filters":[{"field":"state","type":"select","label":"状态","options":["draft","sale","done","cancel"]}]}]]></field>
@@ -25,7 +24,6 @@
<field name="model_name">crm.lead</field>
<field name="field_list">["name","partner_name","expected_revenue","probability","stage_id","create_date","user_id"]</field>
<field name="field_labels">{"name":"线索名称","partner_name":"客户","expected_revenue":"预期收入","probability":"赢单率","stage_id":"阶段","create_date":"创建时间","user_id":"负责人"}</field>
<field name="default_domain">[["active","=",True]]</field>
<field name="default_order">create_date desc</field>
<field name="default_visible">6</field>
<field name="report_config"><![CDATA[{"columns":[{"k":"name","l":"线索名称","w":140},{"k":"partner_name","l":"客户","w":120},{"k":"expected_revenue","l":"预期收入","w":110},{"k":"probability","l":"赢单率","w":80},{"k":"stage_id","l":"阶段","w":100},{"k":"create_date","l":"创建时间","w":120}],"cards":[{"label":"预期总收入","field":"expected_revenue","calc":"sum","prefix":"¥"},{"label":"线索数","field":"id","calc":"count"}],"groupBy":[{"key":"stage_id","label":"阶段"}],"defaultVisible":6,"colorRules":[],"filters":[{"field":"probability","type":"input","label":"最低赢单率"}]}]]></field>
+61 -13
View File
@@ -28,8 +28,11 @@ class AiReportTemplate(models.Model):
'template_id', 'field_id', string='显示字段',
domain="[('model_id','=',model_id)]",
help='选择要显示的字段。选择后自动填充"字段列表"和"字段标签"')
default_domain = fields.Text('默认条件', default='[]',
help='JSON 格式的默认查询 domain')
default_domain = fields.Text('默认条件', compute='_compute_default_domain',
inverse='_inverse_default_domain', store=True,
help='自动从查询条件行生成,也可手动编辑 JSON')
domain_line_ids = fields.One2many('ai.report.template.domain.line', 'template_id',
string='查询条件行', copy=True)
default_order = fields.Char('默认排序', default='id desc')
description = fields.Text('使用说明', translate=True,
help='告诉 AI 何时使用此模板')
@@ -92,19 +95,13 @@ class AiReportTemplate(models.Model):
cr = []
for col in t.column_ids:
if col.color_value and col.cell_color:
# 解析操作符前缀: >= <= != > < = ~
cv = col.color_value.strip()
m = re.match(r'^(>=|<=|!=|>|<|=|~)(.+)$', cv)
if m:
op_prefix, val = m.group(1), m.group(2)
op_map = {'=': 'eq', '~': 'contains', '!=': 'neq',
'>': '>', '<': '<', '>=': '>=', '<=': '<='}
op = op_map.get(op_prefix, 'eq')
else:
op, val = 'eq', cv
op_map = {'eq': 'eq', 'neq': 'neq', 'gt': '>', 'lt': '<',
'gte': '>=', 'lte': '<=', 'contains': 'contains'}
op = op_map.get(col.color_operator or 'eq', 'eq')
cr.append({
'field': col.field_name,
'rules': [{'op': op, 'value': val.strip(), 'color': col.cell_color}],
'rules': [{'op': op, 'value': col.color_value.strip(),
'color': col.cell_color}],
})
if cr:
# 合并相同 field 的规则
@@ -154,6 +151,57 @@ class AiReportTemplate(models.Model):
def _inverse_param_definitions(self):
pass
# ===== Computed: default_domain =====
@api.depends('domain_line_ids', 'domain_line_ids.field_id',
'domain_line_ids.operator', 'domain_line_ids.value')
def _compute_default_domain(self):
for t in self:
if not t.domain_line_ids:
t.default_domain = '[]'
continue
domain = []
for line in t.domain_line_ids.sorted('sequence'):
if not line.field_id or not line.value:
continue
val = line.value.strip()
if line.operator in ('in', 'not in'):
val = [v.strip() for v in val.split(',')]
domain.append([line.field_id.name, line.operator, val])
t.default_domain = json.dumps(domain, ensure_ascii=False)
def _inverse_default_domain(self):
"""手动编辑 JSON 时反向解析到 domain_line_ids(简单实现:清空后重建)"""
for t in self:
if not t.default_domain:
continue
try:
domain = json.loads(t.default_domain)
except (json.JSONDecodeError, TypeError):
continue
cmds = [(5, 0, 0)]
seq = 10
for cond in domain:
if len(cond) >= 3:
op = cond[1]
val = cond[2]
if isinstance(val, list):
val = ', '.join(str(v) for v in val)
else:
val = str(val)
field = t.env['ir.model.fields'].search([
('model', '=', t.model_name),
('name', '=', cond[0]),
], limit=1)
if field:
cmds.append((0, 0, {
'field_id': field.id,
'operator': op,
'value': val,
'sequence': seq,
}))
seq += 10
t.domain_line_ids = cmds
# ===== 预览动作 =====
def action_preview(self):
"""预览报表:使用模板配置生成一个临时报表并打开 URL"""
@@ -24,15 +24,17 @@ class AiReportTemplateColumn(models.Model):
label = fields.Char('显示名称',
help='不填则默认使用字段名')
width = fields.Integer('列宽', default=120)
color_operator = fields.Selection([
('eq', '等于'),
('neq', '不等于'),
('gt', '大于'),
('lt', '小于'),
('gte', '大于等于'),
('lte', '小于等于'),
('contains', '包含'),
], string='操作符', default='eq')
color_value = fields.Char('匹配值',
help='格式:[操作符]值。不写操作符默认等于。\n'
' =已完成 / 已完成 → 等于"已完成"\n'
' >100 → 大于 100\n'
' <50 → 小于 50\n'
' >=100 → 大于等于 100\n'
' <=50 → 小于等于 50\n'
' !=已完成 → 不等于"已完成"\n'
' ~张三 → 包含"张三"')
help='匹配的值,如"已完成"或"100"')
cell_color = fields.Char('显示颜色', default='#5DCAA5',
help='匹配成功时显示的颜色,如 #E74C3C(红)、#5DCAA5(绿)')
sequence = fields.Integer('排序', default=10)
@@ -44,18 +46,11 @@ class AiReportTemplateColumn(models.Model):
if not self.label:
self.label = self.field_id.field_description
@api.constrains('color_value')
@api.constrains('color_value', 'cell_color')
def _check_color_value(self):
import re
for rec in self:
if not rec.color_value:
continue
# 合法格式: [>=|<=|!=|>|<|=|~]值
if not re.match(r'^(>=|<=|!=|>|<|=|~)?\S', rec.color_value):
raise ValidationError(
'匹配值格式错误:%s\n'
'正确示例:已完成、>100、<50、>=100、<=50、!=已完成、~张三' % rec.color_value
)
if rec.cell_color and not rec.color_value:
raise ValidationError('设置了显示颜色时,匹配值不能为空')
class AiReportTemplateCard(models.Model):
@@ -160,3 +155,35 @@ class AiReportTemplateParamDef(models.Model):
self.domain_field = self.field_id.name
if not self.label:
self.label = self.field_id.field_description
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
@@ -33,3 +33,5 @@ access_ai_report_template_filter_user,筛选器-用户只读,model_ai_report_tem
access_ai_report_template_filter_admin,筛选器-管理员,model_ai_report_template_filter,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_param_def_user,参数定义-用户只读,model_ai_report_template_param_def,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_param_def_admin,参数定义-管理员,model_ai_report_template_param_def,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
access_ai_report_template_domain_line_user,查询条件行-用户只读,model_ai_report_template_domain_line,yuthon_ai_agent.group_ai_agent_user,1,0,0,0
access_ai_report_template_domain_line_admin,查询条件行-管理员,model_ai_report_template_domain_line,yuthon_ai_agent.group_ai_agent_admin,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
33 access_ai_report_template_filter_admin 筛选器-管理员 model_ai_report_template_filter yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
34 access_ai_report_template_param_def_user 参数定义-用户只读 model_ai_report_template_param_def yuthon_ai_agent.group_ai_agent_user 1 0 0 0
35 access_ai_report_template_param_def_admin 参数定义-管理员 model_ai_report_template_param_def yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
36 access_ai_report_template_domain_line_user 查询条件行-用户只读 model_ai_report_template_domain_line yuthon_ai_agent.group_ai_agent_user 1 0 0 0
37 access_ai_report_template_domain_line_admin 查询条件行-管理员 model_ai_report_template_domain_line yuthon_ai_agent.group_ai_agent_admin 1 1 1 1
+18 -5
View File
@@ -164,8 +164,6 @@
placeholder="选择要显示的字段(选择后自动生成下方配置)"/>
<field name="field_list" invisible="1"/>
<field name="field_labels" invisible="1"/>
<field name="default_domain"
placeholder="[['state','=','sale']]"/>
<field name="default_order"
placeholder="date_order desc"/>
<field name="default_visible"/>
@@ -196,15 +194,30 @@
<field name="field_id"
placeholder="选择字段..."
options="{'no_create': True}"/>
<field name="label" placeholder="显示名称(可选)"/>
<field name="color_value" placeholder="匹配值"/>
<field name="cell_color" widget="color"/>
<field name="label" placeholder="显示名称(可选)"/>
<field name="color_operator"/>
<field name="color_value" placeholder="匹配值"/>
<field name="cell_color" widget="color"/>
<field name="width"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="查询条件" name="domain">
<field name="domain_line_ids" nolabel="1">
<tree string="查询条件" editable="bottom">
<field name="model_id" column_invisible="1"/>
<field name="field_id"
placeholder="选择字段..."
options="{'no_create': True}"/>
<field name="operator"/>
<field name="value" placeholder="值"/>
<field name="sequence" widget="handle"/>
</tree>
</field>
</page>
<page string="AI 参数" name="params" invisible="1">
<field name="param_def_ids" nolabel="1">
<tree string="参数定义" editable="bottom">