From ec5fca119407e25c92b5c8f275e0b68829fd7288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Fri, 29 May 2026 09:26:05 +0800 Subject: [PATCH] =?UTF-8?q?ai=E5=8A=A8=E6=80=81=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_ai_agent/__manifest__.py | 2 + yuthon_ai_agent/data/demo_manual.xml | 50 +++++++++++ yuthon_ai_agent/data/demo_report_template.xml | 35 ++++++++ yuthon_ai_agent/models/ai_report.py | 17 ++++ yuthon_ai_agent/models/ai_report_template.py | 85 +++++++++++++++++++ yuthon_ai_agent/views/ai_report_views.xml | 28 ++++-- 6 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 yuthon_ai_agent/data/demo_manual.xml create mode 100644 yuthon_ai_agent/data/demo_report_template.xml 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 不含所需模型时可手动输入"/> + + + + +