diff --git a/yuthon_ai_agent/__manifest__.py b/yuthon_ai_agent/__manifest__.py
index 9f14a510a..08310b46c 100644
--- a/yuthon_ai_agent/__manifest__.py
+++ b/yuthon_ai_agent/__manifest__.py
@@ -28,6 +28,8 @@
'views/ai_conversation_views.xml',
'data/demo_provider.xml',
'data/demo_rule.xml',
+ 'data/demo_manual.xml',
+ 'data/demo_report_template.xml',
],
'assets': {
'web.assets_backend': [
diff --git a/yuthon_ai_agent/data/demo_manual.xml b/yuthon_ai_agent/data/demo_manual.xml
new file mode 100644
index 000000000..4f68e4fe2
--- /dev/null
+++ b/yuthon_ai_agent/data/demo_manual.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+ 创建销售订单流程
+ 销售管理
+ 10
+
+操作步骤:
+1. 进入「销售」模块 → 点击「订单」→「创建」
+2. 选择客户(必填),系统自动带入客户价格表
+3. 添加产品行:点击「添加一行」,选择产品,输入数量
+4. 确认订单:点击「确认」按钮
+5. 交货:订单确认后点击「交付」
+6. 开票:交货完成后点击「创建发票」
+
+注意事项:
+- 订单确认后不可直接删除,需先取消
+- 价格默认取客户价格表,可手动调整
+- 数量支持小数,精度在产品设置中配置
+
+
+
+
+
+ 采购订单创建流程
+ 采购管理
+ 20
+
+操作步骤:
+1. 进入「采购」模块 → 点击「采购订单」→「创建」
+2. 选择供应商(必填)
+3. 添加产品行:选择产品、输入数量和价格
+4. 确认订单:点击「确认订单」
+5. 收货:供应商发货后点击「收货」
+6. 开票:收货后创建供应商账单
+
+注意事项:
+- 采购价格默认取供应商价格表
+- 可设置最低采购数量提醒
+- 收货支持分批收货
+
+
+
+
+
diff --git a/yuthon_ai_agent/data/demo_report_template.xml b/yuthon_ai_agent/data/demo_report_template.xml
new file mode 100644
index 000000000..15afb38c1
--- /dev/null
+++ b/yuthon_ai_agent/data/demo_report_template.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ 销售订单报表
+ sale.order
+ ["name","partner_id","date_order","amount_total","state","user_id"]
+ {"name":"订单号","partner_id":"客户","date_order":"下单日期","amount_total":"金额","state":"状态","user_id":"销售员"}
+ [["state","!=","cancel"]]
+ date_order desc
+ {"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"]}]}
+ [{"name":"date_from","label":"开始日期","type":"date","domain_field":"date_order","op":">=","description":"查询起始日期"},{"name":"date_to","label":"结束日期","type":"date","domain_field":"date_order","op":"<=","description":"查询截止日期"}]
+ 当用户查询销售订单列表或销售业绩时使用此模板。支持按日期范围筛选。
+ 10
+
+
+
+
+ 销售线索报表
+ crm.lead
+ ["name","partner_name","expected_revenue","probability","stage_id","create_date","user_id"]
+ {"name":"线索名称","partner_name":"客户","expected_revenue":"预期收入","probability":"赢单率","stage_id":"阶段","create_date":"创建时间","user_id":"负责人"}
+ [["active","=",True]]
+ create_date desc
+ {"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":"最低赢单率"}]}
+ 当用户查询销售线索列表或销售管道时使用此模板。
+ 20
+
+
+
+
diff --git a/yuthon_ai_agent/models/ai_report.py b/yuthon_ai_agent/models/ai_report.py
index 23aea2ef2..2e0e8c7ec 100644
--- a/yuthon_ai_agent/models/ai_report.py
+++ b/yuthon_ai_agent/models/ai_report.py
@@ -17,6 +17,8 @@ class AiReport(models.Model):
name = fields.Char('报表名称', required=True)
model_name = fields.Char('数据模型', required=True,
help='Odoo 模型技术名称,如 sale.order')
+ model_id = fields.Many2one('ir.model', string='数据模型', ondelete='cascade',
+ help='选择数据模型,选择后自动填充"数据模型"字段')
domain = fields.Text('查询条件', default='[]',
help='JSON 格式的 Odoo domain')
field_list = fields.Text('显示字段', required=True,
@@ -40,6 +42,10 @@ class AiReport(models.Model):
if not vals.get('expire_date'):
# 默认 7 天后过期
vals['expire_date'] = fields.Datetime.now() + timedelta(days=7)
+ # 新增:Auto-sync model_name from model_id
+ 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
return super().create(vals_list)
@staticmethod
@@ -53,6 +59,17 @@ class AiReport(models.Model):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
return f"{base_url}/ai/report/{self.access_token}"
+ @api.onchange('model_id')
+ def _onchange_model_id(self):
+ if self.model_id:
+ self.model_name = self.model_id.model
+
+ def write(self, vals):
+ 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
+ return super().write(vals)
+
def fetch_data(self):
"""执行查询并返回数据(增强版:支持模板参数合并、字段回退)"""
self.ensure_one()
diff --git a/yuthon_ai_agent/models/ai_report_template.py b/yuthon_ai_agent/models/ai_report_template.py
index e0b92cfb7..5660e9913 100644
--- a/yuthon_ai_agent/models/ai_report_template.py
+++ b/yuthon_ai_agent/models/ai_report_template.py
@@ -3,6 +3,7 @@ import json
import logging
from odoo import models, fields, api
+from datetime import timedelta
_logger = logging.getLogger(__name__)
@@ -15,10 +16,16 @@ class AiReportTemplate(models.Model):
name = fields.Char('模板名称', required=True)
model_name = fields.Char('数据模型', required=True,
help='Odoo 模型技术名称,如 sale.order')
+ model_id = fields.Many2one('ir.model', string='数据模型', ondelete='cascade',
+ 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='选择要显示的字段。选择后自动填充"字段列表"和"字段标签"')
default_domain = fields.Text('默认条件', default='[]',
help='JSON 格式的默认查询 domain')
default_order = fields.Char('默认排序', default='id desc')
@@ -31,6 +38,27 @@ class AiReportTemplate(models.Model):
help='JSON数组,定义模板可接受的参数。每项包含name(参数名)、label(显示名)、type(参数类型:date/char/select/float)、domain_field(映射模型字段)、op(操作符)、description(说明)、options(select选项)')
active = fields.Boolean(default=True)
+ def action_preview(self):
+ """预览报表:使用模板配置生成一个临时报表并打开 URL"""
+ self.ensure_one()
+ import json as _json
+ report = self.env['ai.report'].sudo().create({
+ 'name': f'[预览] {self.name}',
+ 'model_name': self.model_name,
+ 'field_list': self.field_list or '[]',
+ 'field_labels': self.field_labels or '{}',
+ 'domain': self.default_domain or '[]',
+ 'order': self.default_order or 'id desc',
+ 'template_id': self.id,
+ 'expire_date': fields.Datetime.now() + timedelta(hours=1),
+ })
+ url = report.get_report_url()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': url,
+ 'target': 'new',
+ }
+
@api.model
def get_templates_for_ai(self):
"""获取所有启用模板的摘要,供 AI system_msg 注入(增强版:包含参数信息)"""
@@ -58,3 +86,60 @@ class AiReportTemplate(models.Model):
pass
lines.append(base_info)
return '\n'.join(lines)
+
+ @api.onchange('model_id')
+ def _onchange_model_id(self):
+ if self.model_id:
+ self.model_name = self.model_id.model
+
+ @api.onchange('field_ids')
+ def _onchange_field_ids(self):
+ if self.field_ids:
+ import json
+ 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
+ )
+
+ @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 vals.get('field_ids'):
+ self._sync_field_list_from_ids(vals)
+ return super().create(vals_list)
+
+ def write(self, vals):
+ 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
+ if vals.get('field_ids'):
+ self._sync_field_list_from_ids(vals)
+ return super().write(vals)
+
+ @api.model
+ def _sync_field_list_from_ids(self, vals):
+ """从 field_ids (M2M command list) 中提取字段名并同步到 field_list 和 field_labels"""
+ import json
+ ids = []
+ field_cmds = vals['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])
+ if ids:
+ 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
+ )
diff --git a/yuthon_ai_agent/views/ai_report_views.xml b/yuthon_ai_agent/views/ai_report_views.xml
index 1d06a07b4..c360e6557 100644
--- a/yuthon_ai_agent/views/ai_report_views.xml
+++ b/yuthon_ai_agent/views/ai_report_views.xml
@@ -39,7 +39,11 @@
-
+
+
@@ -131,12 +135,18 @@
+
-
-
+ attrs="{'readonly': [('model_id','!=',False)], 'required': [('model_id','=',False)]}"
+ placeholder="如 model_id 不含所需模型时可手动输入"/>
+
+
+
+
+
+
+