port of durpro_helpdek_sale to helpdesk sale order + base for AI
This commit is contained in:
parent
2164e2be54
commit
aa16543a86
23 changed files with 639 additions and 0 deletions
6
helpdesk_ai_sale_order/__init__.py
Normal file
6
helpdesk_ai_sale_order/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
32
helpdesk_ai_sale_order/__manifest__.py
Normal file
32
helpdesk_ai_sale_order/__manifest__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Helpdesk Sale Order',
|
||||
'license': 'LGPL-3',
|
||||
'version': '18.0.0.1',
|
||||
'category' : 'Sales/Sales',
|
||||
'summary': """Allows your helpdesk team to create sales orders from helpdesk tickets.""",
|
||||
'description': """
|
||||
Convert helpdesk tickets into sales orders.
|
||||
|
||||
This feature is useful for companies that sell products or services to their customers
|
||||
and might receive orders through their helpdesk tickets or simply used orders@theirodoo.com
|
||||
as a triage helpdesk ticket.
|
||||
""",
|
||||
'author': 'Bemade',
|
||||
'maintainer': 'it@bemade.org',
|
||||
'depends': [
|
||||
'sale_management',
|
||||
'helpdesk',
|
||||
'helpdesk_sale',
|
||||
'sale_project'
|
||||
],
|
||||
'data': [
|
||||
'views/account_move_view.xml',
|
||||
'views/helpdesk_ticket_views.xml',
|
||||
'views/sale_view.xml',
|
||||
'views/helpdesk_team_views.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
22
helpdesk_ai_sale_order/migration18.md
Normal file
22
helpdesk_ai_sale_order/migration18.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Migration de durpro_helpdesk_sale vers Odoo 18.0
|
||||
|
||||
## Description
|
||||
Module d'intégration entre le helpdesk et les ventes pour Durpro.
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
|
||||
### Création de commandes de vente depuis les tickets
|
||||
- Vérification Odoo 18.0 : À vérifier
|
||||
- Différences avec la version native : À documenter
|
||||
- Alternatives disponibles : À identifier
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
|
||||
### Modèle HelpdeskTicket
|
||||
- Ajouts/Modifications : À documenter
|
||||
- Recherche dans le projet : À effectuer
|
||||
- Existence dans Odoo standard/enterprise : À vérifier
|
||||
- Recommandations de migration : À formuler
|
||||
|
||||
## Vues à Modifier
|
||||
- Liste des vues tree à convertir en list : À identifier
|
||||
8
helpdesk_ai_sale_order/models/__init__.py
Normal file
8
helpdesk_ai_sale_order/models/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sale_order
|
||||
from . import account_move
|
||||
from . import helpdesk_team
|
||||
from . import helpdesk_ticket
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
12
helpdesk_ai_sale_order/models/account_move.py
Normal file
12
helpdesk_ai_sale_order/models/account_move.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
helpdesk_ticket_id = fields.Many2one(
|
||||
'helpdesk.ticket',
|
||||
string='Helpdesk Ticket',
|
||||
copy=False,
|
||||
)
|
||||
7
helpdesk_ai_sale_order/models/helpdesk_team.py
Normal file
7
helpdesk_ai_sale_order/models/helpdesk_team.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class HelpdeskTeam(models.Model):
|
||||
_inherit = 'helpdesk.team'
|
||||
|
||||
use_sale_orders = fields.Boolean('Sales', help="Create quotes from tickets")
|
||||
90
helpdesk_ai_sale_order/models/helpdesk_ticket.py
Normal file
90
helpdesk_ai_sale_order/models/helpdesk_ticket.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from odoo import models, fields, api, _, Command
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = "helpdesk.ticket"
|
||||
|
||||
sale_order_ids = fields.One2many(
|
||||
"sale.order",
|
||||
"helpdesk_ticket_id",
|
||||
string="Sale Orders",
|
||||
help="Sale orders associated to this ticket.",
|
||||
copy=False,
|
||||
)
|
||||
sale_order_count = fields.Integer(compute="_compute_sale_order_count")
|
||||
team_use_sale_orders = fields.Boolean(related="team_id.use_sale_orders", string="Team Uses Sales Orders", readonly=True)
|
||||
|
||||
@api.depends("sale_order_ids")
|
||||
def _compute_sale_order_count(self):
|
||||
self.sale_order_count = len(self.sale_order_ids)
|
||||
|
||||
def action_view_sale_order(self):
|
||||
self.ensure_one()
|
||||
sale_order_form_view = self.env.ref("sale.view_order_form")
|
||||
sale_order_tree_view = self.env.ref("sale.view_order_tree")
|
||||
|
||||
action = {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "sale.order",
|
||||
"context": {"create": False},
|
||||
}
|
||||
|
||||
if self.sale_order_count == 1:
|
||||
action.update(
|
||||
res_id=self.sale_order_ids[0].id,
|
||||
views=[(sale_order_form_view.id, "form")],
|
||||
)
|
||||
else:
|
||||
action.update(
|
||||
domain=[("id", "in", self.sale_order_ids.ids)],
|
||||
views=[
|
||||
(sale_order_tree_view.id, "tree"),
|
||||
(sale_order_form_view.id, "form"),
|
||||
],
|
||||
name=_("Purchase Orders from Ticket"),
|
||||
)
|
||||
return action
|
||||
|
||||
def action_convert_to_sale_order(self):
|
||||
self.ensure_one()
|
||||
if not self.team_use_sale_orders:
|
||||
raise UserError(_("Creating quotes from tickets is not enabled for this helpdesk team."))
|
||||
|
||||
so_values = self._generate_so_values()
|
||||
so = self.env["sale.order"].create([so_values])
|
||||
self.message_change_thread(so)
|
||||
attachments = self.env["ir.attachment"].search(
|
||||
[("res_model", "=", "helpdesk.ticket"), ("res_id", "=", self.id)]
|
||||
)
|
||||
attachments.sudo().write({"res_model": "sale.order", "res_id": so.id})
|
||||
activities = self.activity_ids
|
||||
activities.sudo().write(
|
||||
{
|
||||
"res_model_id": self.env.ref("sale.model_sale_order").id,
|
||||
"res_id": so.id,
|
||||
"res_model": "sale.order",
|
||||
}
|
||||
)
|
||||
# The activities will be linked to the SO through the res_model and res_id fields
|
||||
self.action_archive()
|
||||
return self.action_view_sale_order()
|
||||
|
||||
def _generate_so_values(self):
|
||||
team = self.user_id.sale_team_id if self.user_id else self.env.user.sale_team_id
|
||||
if not team:
|
||||
raise UserError(
|
||||
_(
|
||||
"Creating sale orders is reserved to sales users. Assign the user to sale team first."
|
||||
)
|
||||
)
|
||||
team_id = team.id
|
||||
return {
|
||||
"partner_id": self.partner_id.id,
|
||||
"helpdesk_ticket_id": self.id,
|
||||
"company_id": self.company_id.id,
|
||||
"team_id": team_id,
|
||||
}
|
||||
37
helpdesk_ai_sale_order/models/sale_order.py
Normal file
37
helpdesk_ai_sale_order/models/sale_order.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
helpdesk_ticket_id = fields.Many2one(
|
||||
'helpdesk.ticket',
|
||||
string='Helpdesk Ticket',
|
||||
copy=False,
|
||||
)
|
||||
|
||||
def action_view_ticket(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'helpdesk.ticket',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.helpdesk_ticket_id.id,
|
||||
}
|
||||
|
||||
def _prepare_invoice(self):
|
||||
result = super(SaleOrder, self)._prepare_invoice()
|
||||
result.update({'helpdesk_ticket_id': self.helpdesk_ticket_id.id})
|
||||
return result
|
||||
|
||||
def create(self, vals_list):
|
||||
sos = super().create(vals_list)
|
||||
for so in sos.filtered('helpdesk_ticket_id'):
|
||||
so.message_post_with_source(
|
||||
'helpdesk.ticket_creation',
|
||||
render_values={
|
||||
'self': so,
|
||||
'ticket': so.helpdesk_ticket_id
|
||||
}, subtype_id=self.env.ref('mail.mt_note').id
|
||||
)
|
||||
return sos
|
||||
BIN
helpdesk_ai_sale_order/static/description/icon.png
Normal file
BIN
helpdesk_ai_sale_order/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
1
helpdesk_ai_sale_order/tests/__init__.py
Normal file
1
helpdesk_ai_sale_order/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_helpdesk_sale
|
||||
65
helpdesk_ai_sale_order/tests/test_helpdesk_sale.py
Normal file
65
helpdesk_ai_sale_order/tests/test_helpdesk_sale.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestHelpdeskSale(TransactionCase):
|
||||
def test_convert_ticket_to_sale_order_moves_messages(self):
|
||||
# User needs to be part of a sales team, so set that up first
|
||||
sale_team = self.env.ref("sales_team.crm_team_1")
|
||||
helpdesk_team = self.env.ref("helpdesk.helpdesk_team1")
|
||||
self.env.user.sale_team_id = sale_team
|
||||
|
||||
ticket = self.env["helpdesk.ticket"].create(
|
||||
{
|
||||
"name": "Test Ticket",
|
||||
"description": "Test description",
|
||||
"partner_id": self.env.ref("base.res_partner_2").id,
|
||||
"user_id": self.env.user.id,
|
||||
"team_id": helpdesk_team.id,
|
||||
}
|
||||
)
|
||||
ticket.message_post(body="This is a test email message")
|
||||
ticket.message_post(body="This is another message")
|
||||
ticket.message_post(body="This is a comment.", message_type="comment")
|
||||
|
||||
num_messages = len(ticket.message_ids)
|
||||
self.assertGreaterEqual(num_messages, 3)
|
||||
messages = ticket.message_ids
|
||||
|
||||
action = ticket.action_convert_to_sale_order()
|
||||
so = self.env["sale.order"].browse(action["res_id"])
|
||||
|
||||
for message in messages:
|
||||
self.assertIn(message.body, so.message_ids.mapped("body"))
|
||||
self.assertFalse(ticket.active)
|
||||
|
||||
def test_convert_ticket_to_sale_order_moves_activities(self):
|
||||
# User needs to be part of a sales team, so set that up first
|
||||
sale_team = self.env.ref("sales_team.crm_team_1")
|
||||
helpdesk_team = self.env.ref("helpdesk.helpdesk_team1")
|
||||
self.env.user.sale_team_id = sale_team
|
||||
|
||||
ticket = self.env["helpdesk.ticket"].create(
|
||||
{
|
||||
"name": "Test Ticket",
|
||||
"description": "Test description",
|
||||
"partner_id": self.env.ref("base.res_partner_2").id,
|
||||
"user_id": self.env.user.id,
|
||||
"team_id": helpdesk_team.id,
|
||||
}
|
||||
)
|
||||
activity = self.env["mail.activity"].create(
|
||||
{
|
||||
"activity_type_id": self.env.ref("mail.mail_activity_data_call").id,
|
||||
"res_model_id": self.env.ref("helpdesk.model_helpdesk_ticket").id,
|
||||
"res_id": ticket.id,
|
||||
"user_id": self.env.user.id,
|
||||
}
|
||||
)
|
||||
|
||||
action = ticket.action_convert_to_sale_order()
|
||||
self.env.invalidate_all(flush=True)
|
||||
so = self.env["sale.order"].browse(action["res_id"])
|
||||
|
||||
self.assertIn(activity, so.activity_ids)
|
||||
18
helpdesk_ai_sale_order/views/account_move_view.xml
Normal file
18
helpdesk_ai_sale_order/views/account_move_view.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_move_form_inherit_helpdesk_custom" model="ir.ui.view">
|
||||
<field name="name">account.move.from.inherited.helpdesk</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='invoice_origin']" position="after">
|
||||
<field name="helpdesk_ticket_id" readonly="state not in ['draft', 'sent']"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
15
helpdesk_ai_sale_order/views/helpdesk_team_views.xml
Normal file
15
helpdesk_ai_sale_order/views/helpdesk_team_views.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_team_view_form_inherit_sale_order" model="ir.ui.view">
|
||||
<field name="name">helpdesk.team.form.inherit.sale.order</field>
|
||||
<field name="model">helpdesk.team</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_team_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='after-sales']" position="inside">
|
||||
<setting help="Create quotes from tickets">
|
||||
<field name="use_sale_orders"/>
|
||||
</setting>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
32
helpdesk_ai_sale_order/views/helpdesk_ticket_views.xml
Normal file
32
helpdesk_ai_sale_order/views/helpdesk_ticket_views.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="helpdesk_ticket_form_view_inherit_saleorder" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.form.view.inherit.saleorder</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_convert_to_sale_order"
|
||||
string="Convert to Quotation"
|
||||
groups="helpdesk.group_helpdesk_manager,helpdesk.group_helpdesk_user"
|
||||
type="object"
|
||||
class="btn btn-secondary"
|
||||
invisible="not team_use_sale_orders"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
name="action_view_sale_order"
|
||||
icon="fa-usd"
|
||||
type="object"
|
||||
invisible="sale_order_count == 0">
|
||||
<field string="Sale Orders" name="sale_order_count" widget="statinfo" />
|
||||
</button>
|
||||
</xpath>
|
||||
<field name="team_id" position="after">
|
||||
<field name="team_use_sale_orders" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
26
helpdesk_ai_sale_order/views/sale_view.xml
Normal file
26
helpdesk_ai_sale_order/views/sale_view.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_order_form_inherit_sales" model="ir.ui.view">
|
||||
<field name="name">sale.order.from.inherited.saleorder</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field string="Ticket" name="helpdesk_ticket_id" invisible="1" />
|
||||
<button
|
||||
string="Ticket"
|
||||
class="oe_stat_button"
|
||||
name="action_view_ticket"
|
||||
type="object"
|
||||
icon="fa-life-ring"
|
||||
help="Tickets for this order"
|
||||
invisible="helpdesk_ticket_id == False"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
4
helpdesk_ai_sale_order/wizard/__init__.py
Normal file
4
helpdesk_ai_sale_order/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sale_advance_payment
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
10
helpdesk_ai_sale_order/wizard/sale_advance_payment.py
Normal file
10
helpdesk_ai_sale_order/wizard/sale_advance_payment.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class SaleAdvancePaymentInv(models.TransientModel):
|
||||
_inherit = "sale.advance.payment.inv"
|
||||
|
||||
def _create_invoice(self, order, so_line, amount):
|
||||
res = super(SaleAdvancePaymentInv, self)._create_invoice(order, so_line, amount)
|
||||
res.helpdesk_ticket_id = order.helpdesk_ticket_id
|
||||
return res
|
||||
3
helpdesk_sale_order_ai/__init__.py
Normal file
3
helpdesk_sale_order_ai/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
27
helpdesk_sale_order_ai/__manifest__.py
Normal file
27
helpdesk_sale_order_ai/__manifest__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Helpdesk Sale Order AI',
|
||||
'license': 'LGPL-3',
|
||||
'version': '18.0.0.1',
|
||||
'category': 'Sales/Sales',
|
||||
'summary': """Automatically create sales orders from helpdesk tickets using AI.""",
|
||||
'description': """
|
||||
Extends the Helpdesk Sale Order module to automatically create sales orders from helpdesk tickets using AI.
|
||||
|
||||
This module adds AI capabilities to analyze ticket content and automatically generate appropriate sales orders
|
||||
with relevant products and services based on the ticket description.
|
||||
""",
|
||||
'author': 'Bemade',
|
||||
'maintainer': 'it@bemade.org',
|
||||
'depends': [
|
||||
'helpdesk_sale_order',
|
||||
'openai_connector', # Supposant qu'un module de connexion OpenAI existe
|
||||
],
|
||||
'data': [
|
||||
'views/helpdesk_team_views.xml',
|
||||
'views/helpdesk_ticket_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
4
helpdesk_sale_order_ai/models/__init__.py
Normal file
4
helpdesk_sale_order_ai/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import helpdesk_ticket
|
||||
from . import helpdesk_team
|
||||
25
helpdesk_sale_order_ai/models/helpdesk_team.py
Normal file
25
helpdesk_sale_order_ai/models/helpdesk_team.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class HelpdeskTeam(models.Model):
|
||||
_inherit = 'helpdesk.team'
|
||||
|
||||
use_ai_sale_orders = fields.Boolean(
|
||||
string='Use AI for Sale Orders',
|
||||
help='If checked, the system will use AI to automatically generate sale orders from ticket descriptions.',
|
||||
default=False,
|
||||
)
|
||||
|
||||
ai_prompt_template = fields.Text(
|
||||
string='AI Prompt Template',
|
||||
help='Template for the prompt sent to the AI. Use placeholders like {description}, {customer}, etc.',
|
||||
default="""Based on the following helpdesk ticket description, identify products and services that should be included in a sales order:
|
||||
|
||||
Ticket Description: {description}
|
||||
Customer: {customer}
|
||||
|
||||
Please provide a list of products/services with quantities and descriptions in the following format:
|
||||
Product/Service Name | Quantity | Description
|
||||
"""
|
||||
)
|
||||
181
helpdesk_sale_order_ai/models/helpdesk_ticket.py
Normal file
181
helpdesk_sale_order_ai/models/helpdesk_ticket.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
import json
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = 'helpdesk.ticket'
|
||||
|
||||
# Utiliser un champ calculé au lieu d'un champ simple avec onchange
|
||||
team_use_ai_sale_orders = fields.Boolean(
|
||||
string='Team Uses AI for Sale Orders',
|
||||
compute='_compute_team_use_ai_sale_orders',
|
||||
)
|
||||
|
||||
@api.depends('team_id')
|
||||
def _compute_team_use_ai_sale_orders(self):
|
||||
for ticket in self:
|
||||
ticket.team_use_ai_sale_orders = False
|
||||
if ticket.team_id:
|
||||
# Vérifier si le champ existe sur l'équipe
|
||||
team = self.env['helpdesk.team'].sudo().browse(ticket.team_id.id)
|
||||
if hasattr(team, 'use_ai_sale_orders'):
|
||||
ticket.team_use_ai_sale_orders = team.use_ai_sale_orders
|
||||
|
||||
ai_generated_products = fields.Text(
|
||||
string='AI Generated Products',
|
||||
readonly=True,
|
||||
help='Products suggested by AI based on ticket description',
|
||||
)
|
||||
|
||||
def action_convert_to_sale_order(self):
|
||||
"""Override to use AI for generating sale order if enabled"""
|
||||
self.ensure_one()
|
||||
|
||||
# Check if the team allows sale orders
|
||||
if not self.team_use_sale_orders:
|
||||
raise UserError(_("You cannot create a sale order from this ticket because your team does not allow it."))
|
||||
|
||||
# Vérifier directement sur l'équipe si l'IA est activée
|
||||
use_ai = False
|
||||
if self.team_id and hasattr(self.team_id, 'use_ai_sale_orders'):
|
||||
use_ai = self.team_id.use_ai_sale_orders
|
||||
|
||||
# If AI is enabled for this team, use it to generate the sale order
|
||||
if use_ai:
|
||||
return self._ai_convert_to_sale_order()
|
||||
|
||||
# Otherwise, use the standard method from the parent module
|
||||
return super(HelpdeskTicket, self).action_convert_to_sale_order()
|
||||
|
||||
def _ai_convert_to_sale_order(self):
|
||||
"""Create a sale order using AI to suggest products based on ticket description"""
|
||||
self.ensure_one()
|
||||
|
||||
# Generate AI suggestions if not already done
|
||||
if not self.ai_generated_products:
|
||||
self._generate_ai_product_suggestions()
|
||||
|
||||
# Create the sale order with AI-suggested products
|
||||
so_vals = self._generate_ai_so_values()
|
||||
sale_order = self.env['sale.order'].create([so_vals])
|
||||
|
||||
# Link the sale order to the ticket
|
||||
self.write({
|
||||
'sale_order_id': sale_order.id,
|
||||
})
|
||||
|
||||
# Return the action to view the created sale order
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Sale Order'),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': sale_order.id,
|
||||
'view_mode': 'form',
|
||||
'context': {'create': False},
|
||||
}
|
||||
|
||||
def _generate_ai_product_suggestions(self):
|
||||
"""Use AI to generate product suggestions based on ticket description"""
|
||||
self.ensure_one()
|
||||
|
||||
# Skip if no description
|
||||
if not self.description:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Prepare the prompt using the template from the team
|
||||
prompt = self.team_id.ai_prompt_template.format(
|
||||
description=self.description,
|
||||
customer=self.partner_id.name or 'Unknown',
|
||||
)
|
||||
|
||||
# Call the AI service (assuming an OpenAI connector module exists)
|
||||
ai_service = self.env['openai.service'].sudo()
|
||||
response = ai_service.generate_completion(prompt)
|
||||
|
||||
# Store the AI response
|
||||
self.ai_generated_products = response
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error("Error generating AI product suggestions: %s", str(e))
|
||||
return False
|
||||
|
||||
def _generate_ai_so_values(self):
|
||||
"""Generate sale order values with AI-suggested products"""
|
||||
# Start with the base SO values from the parent method
|
||||
so_vals = self._generate_so_values()
|
||||
|
||||
# Parse AI suggestions and add as order lines
|
||||
if self.ai_generated_products:
|
||||
order_lines = self._parse_ai_product_suggestions()
|
||||
if order_lines:
|
||||
so_vals['order_line'] = order_lines
|
||||
|
||||
return so_vals
|
||||
|
||||
def _parse_ai_product_suggestions(self):
|
||||
"""Parse the AI-generated product suggestions into sale order lines"""
|
||||
order_lines = []
|
||||
|
||||
if not self.ai_generated_products:
|
||||
return order_lines
|
||||
|
||||
# Simple parsing of the AI response
|
||||
# Format expected: Product/Service Name | Quantity | Description
|
||||
lines = self.ai_generated_products.strip().split('\n')
|
||||
|
||||
for line in lines:
|
||||
if '|' not in line:
|
||||
continue
|
||||
|
||||
parts = [part.strip() for part in line.split('|')]
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
product_name = parts[0]
|
||||
quantity = 1.0
|
||||
description = ''
|
||||
|
||||
# Try to parse quantity
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
quantity = float(parts[1])
|
||||
except ValueError:
|
||||
quantity = 1.0
|
||||
|
||||
# Get description if available
|
||||
if len(parts) > 2:
|
||||
description = parts[2]
|
||||
|
||||
# Search for matching product
|
||||
product = self.env['product.product'].search([
|
||||
('name', 'ilike', product_name),
|
||||
('sale_ok', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not product:
|
||||
# If no product found, create a service product
|
||||
product = self.env['product.product'].create({
|
||||
'name': product_name,
|
||||
'type': 'service',
|
||||
'sale_ok': True,
|
||||
'purchase_ok': False,
|
||||
'list_price': 0.0,
|
||||
})
|
||||
|
||||
# Create order line
|
||||
order_line = (0, 0, {
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': quantity,
|
||||
'name': description or product.name,
|
||||
})
|
||||
|
||||
order_lines.append(order_line)
|
||||
|
||||
return order_lines
|
||||
14
helpdesk_sale_order_ai/views/helpdesk_team_views.xml
Normal file
14
helpdesk_sale_order_ai/views/helpdesk_team_views.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_team_view_form_inherit_helpdesk_sale_order_ai" model="ir.ui.view">
|
||||
<field name="name">helpdesk.team.form.inherit.sale.order.ai</field>
|
||||
<field name="model">helpdesk.team</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_team_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_use_sale_orders']" position="after">
|
||||
<field name="use_ai_sale_orders" attrs="{'invisible': [('team_use_sale_orders', '=', False)]}"/>
|
||||
<field name="ai_prompt_template" attrs="{'invisible': [('use_ai_sale_orders', '=', False)]}" widget="text_field"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue