diff --git a/commercial_invoice/models/commercial_invoice.py b/commercial_invoice/models/commercial_invoice.py
index f9316f3..365cf16 100644
--- a/commercial_invoice/models/commercial_invoice.py
+++ b/commercial_invoice/models/commercial_invoice.py
@@ -35,7 +35,7 @@ class CommercialInvoice(models.Model):
"res.currency",
string="Currency",
required=True,
- default=lambda self: self.env.company.currency_id,
+ default=lambda self: self.env.ref("base.USD")
)
related_parties = fields.Boolean(string="Related Parties", default=False)
diff --git a/delivery_carrier_partner_account/__manifest__.py b/delivery_carrier_partner_account/__manifest__.py
index 08c316f..a504df0 100644
--- a/delivery_carrier_partner_account/__manifest__.py
+++ b/delivery_carrier_partner_account/__manifest__.py
@@ -19,7 +19,7 @@
#
{
"name": "Carrier Accounts by Partner",
- "version": "18.0.0.1.3",
+ "version": "18.0.0.1.4",
"summary": "Add one or many carrier accounts per partner",
"category": "Delivery",
"author": "Bemade Inc.",
diff --git a/delivery_carrier_partner_account/models/delivery_carrier_account.py b/delivery_carrier_partner_account/models/delivery_carrier_account.py
index 136fe82..abcf440 100644
--- a/delivery_carrier_partner_account/models/delivery_carrier_account.py
+++ b/delivery_carrier_partner_account/models/delivery_carrier_account.py
@@ -38,19 +38,3 @@ class DeliveryCarrierAccount(models.Model):
def _compute_display_name(self):
for record in self:
record.display_name = record.account_number
-
- def write(self, vals):
- res = super().write(vals)
- for partner in self.partner_id.filtered(
- lambda partner: not partner.default_carrier_account_id
- ):
- partner.default_carrier_account_id = partner.carrier_account_ids[0]
- return res
-
- @api.model_create_multi
- def create(self, vals_list):
- res = super().create(vals_list)
- for rec in res:
- if not rec.partner_id.default_carrier_account_id:
- rec.partner_id.default_carrier_account_id = rec
- return res
diff --git a/delivery_carrier_partner_account/models/res_partner.py b/delivery_carrier_partner_account/models/res_partner.py
index 92459ff..495f9cb 100644
--- a/delivery_carrier_partner_account/models/res_partner.py
+++ b/delivery_carrier_partner_account/models/res_partner.py
@@ -13,29 +13,77 @@ class Partner(models.Model):
default_carrier_account_id = fields.Many2one(
comodel_name="delivery.carrier.account",
+ compute="_compute_logistic_defaults",
+ inverse="_inverse_default_carrier_account_id",
+ store=True,
tracking=1,
ondelete="restrict",
)
- def write(self, vals):
- update_default_carrier = (
- "carrier_account_ids" in vals and not self.default_carrier_account_id
- )
- res = super().write(vals)
- if update_default_carrier and self.carrier_account_ids:
- self.default_carrier_account_id = self.carrier_account_ids[0]
- self.property_delivery_carrier_id = (
- self.default_carrier_account_id.delivery_carrier_id
- )
- return res
+ property_delivery_carrier_id = fields.Many2one(
+ compute="_compute_logistic_defaults",
+ inverse="_inverse_property_delivery_carrier_id",
+ store=True,
+ )
- @api.model_create_multi
- def create(self, vals_list):
- res = super().create(vals_list)
- for rec in res:
- if not rec.default_carrier_account_id and rec.carrier_account_ids:
- rec.default_carrier_account_id = rec.carrier_account_ids[0]
- return res
+ def _inverse_default_carrier_account_id(self):
+ pass
+
+ def _inverse_property_delivery_carrier_id(self):
+ pass
+
+ @api.depends(
+ "carrier_account_ids",
+ "carrier_account_ids.active",
+ "carrier_account_ids.delivery_carrier_id",
+ )
+ def _compute_logistic_defaults(self):
+ # Unset default carrier account if it is archived
+ for partner in self.filtered(
+ lambda partner: partner.default_carrier_account_id
+ and not partner.default_carrier_account_id.active
+ ):
+ partner.default_carrier_account_id = False
+
+ # Unset the default carrier if no accounts are available
+ for partner in self.filtered(
+ lambda partner: partner.property_delivery_carrier_id
+ ).filtered(
+ lambda partner: not partner.carrier_account_ids.filtered(
+ lambda account: account.delivery_carrier_id
+ == partner.property_delivery_carrier_id
+ and account.active
+ )
+ ):
+ partner.property_delivery_carrier_id = False
+
+ # Set default carrier account if not set and accounts available
+ for partner in self.filtered(
+ lambda partner: not partner.default_carrier_account_id
+ and partner.carrier_account_ids.filtered("active")
+ ):
+ partner.default_carrier_account_id = partner.carrier_account_ids.filtered(
+ "active"
+ )[0]
+
+ # Set default carrier if not set and default account is set
+ for partner in self.filtered(
+ lambda partner: not partner.property_delivery_carrier_id
+ and partner.default_carrier_account_id
+ ):
+ partner.property_delivery_carrier_id = (
+ partner.default_carrier_account_id.delivery_carrier_id
+ )
+
+ # Reset default carrier if account is set and doesn't match
+ for partner in self.filtered(
+ lambda partner: partner.default_carrier_account_id
+ and partner.default_carrier_account_id.delivery_carrier_id
+ != partner.property_delivery_carrier_id
+ ):
+ partner.property_delivery_carrier_id = (
+ partner.default_carrier_account_id.delivery_carrier_id
+ )
def get_carrier_account(self, carrier):
self.ensure_one()
@@ -52,3 +100,22 @@ class Partner(models.Model):
if commercial_patner_accounts:
return commercial_patner_accounts[0]
return self.env["delivery.carrier.account"]
+
+ def process_carrier_account_archiving(self):
+ for partner in self:
+ # Unset default carrier account if it is archived
+ if (
+ partner.default_carrier_account_id
+ and not partner.default_carrier_account_id.active
+ ):
+ partner.default_carrier_account_id = False
+ # Unset default carrier if not more accounts available
+ if (
+ partner.property_delivery_carrier_id
+ and not partner.carrier_account_ids.filtered(
+ lambda account: account.delivery_carrier_id
+ == partner.property_delivery_carrier_id
+ and account.active
+ )
+ ):
+ partner.property_delivery_carrier_id = False
diff --git a/delivery_carrier_partner_account/tests/test_res_partner.py b/delivery_carrier_partner_account/tests/test_res_partner.py
index d2c1e46..f1d2ccd 100644
--- a/delivery_carrier_partner_account/tests/test_res_partner.py
+++ b/delivery_carrier_partner_account/tests/test_res_partner.py
@@ -1,4 +1,4 @@
-from odoo.tests import TransactionCase, tagged
+from odoo.tests import TransactionCase, tagged, Form
from odoo import Command
@@ -12,16 +12,15 @@ class TestResPartner(TransactionCase):
partner = self.env["res.partner"].create(
{
"name": "Test Partner",
- "carrier_account_ids": [
- Command.create(
- {
- "delivery_carrier_id": self.env.ref(
- "delivery.free_delivery_carrier"
- ).id,
- "account_number": "1234567890",
- }
- )
- ],
+ }
+ )
+ account = self.env["delivery.carrier.account"].create(
+ {
+ "partner_id": partner.id,
+ "delivery_carrier_id": self.env.ref(
+ "delivery.free_delivery_carrier"
+ ).id,
+ "account_number": "1234567890",
}
)
@@ -97,3 +96,53 @@ class TestResPartner(TransactionCase):
)
self.assertEqual(partner.default_carrier_account_id, new_account)
+
+ def test_no_archived_default_carrier_account(self):
+ partner = self.env["res.partner"].create(
+ {
+ "name": "Test Partner",
+ }
+ )
+ account = self.env["delivery.carrier.account"].create(
+ {
+ "partner_id": partner.id,
+ "delivery_carrier_id": self.env.ref(
+ "delivery.free_delivery_carrier"
+ ).id,
+ "account_number": "1234567890",
+ }
+ )
+
+ with Form(account) as account_form:
+ account_form.active = False
+
+ self.assertFalse(partner.default_carrier_account_id)
+
+ def test_multiple_carrier_accounts_reset_default_on_archive(self):
+ partner = self.env["res.partner"].create(
+ {
+ "name": "Test Partner",
+ }
+ )
+ account1 = self.env["delivery.carrier.account"].create(
+ {
+ "partner_id": partner.id,
+ "delivery_carrier_id": self.env.ref(
+ "delivery.free_delivery_carrier"
+ ).id,
+ "account_number": "1234567890",
+ }
+ )
+ account2 = self.env["delivery.carrier.account"].create(
+ {
+ "partner_id": partner.id,
+ "delivery_carrier_id": self.env.ref(
+ "delivery.free_delivery_carrier"
+ ).id,
+ "account_number": "1234567891",
+ }
+ )
+ self.assertEqual(partner.default_carrier_account_id, account1)
+ with Form(account1) as account_form:
+ account_form.active = False
+ self.assertEqual(partner.default_carrier_account_id, account2)
diff --git a/helpdesk_sale_order/__init__.py b/helpdesk_sale_order/__init__.py
new file mode 100644
index 0000000..c6d481c
--- /dev/null
+++ b/helpdesk_sale_order/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import wizard
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/helpdesk_sale_order/__manifest__.py b/helpdesk_sale_order/__manifest__.py
new file mode 100644
index 0000000..1ea1e8e
--- /dev/null
+++ b/helpdesk_sale_order/__manifest__.py
@@ -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,
+}
diff --git a/helpdesk_sale_order/migration18.md b/helpdesk_sale_order/migration18.md
new file mode 100644
index 0000000..dc98f30
--- /dev/null
+++ b/helpdesk_sale_order/migration18.md
@@ -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
\ No newline at end of file
diff --git a/helpdesk_sale_order/models/__init__.py b/helpdesk_sale_order/models/__init__.py
new file mode 100644
index 0000000..d1aeada
--- /dev/null
+++ b/helpdesk_sale_order/models/__init__.py
@@ -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:
diff --git a/helpdesk_sale_order/models/account_move.py b/helpdesk_sale_order/models/account_move.py
new file mode 100644
index 0000000..447b66d
--- /dev/null
+++ b/helpdesk_sale_order/models/account_move.py
@@ -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,
+ )
diff --git a/helpdesk_sale_order/models/helpdesk_team.py b/helpdesk_sale_order/models/helpdesk_team.py
new file mode 100644
index 0000000..1a8790d
--- /dev/null
+++ b/helpdesk_sale_order/models/helpdesk_team.py
@@ -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")
\ No newline at end of file
diff --git a/helpdesk_sale_order/models/helpdesk_ticket.py b/helpdesk_sale_order/models/helpdesk_ticket.py
new file mode 100644
index 0000000..9d93f7b
--- /dev/null
+++ b/helpdesk_sale_order/models/helpdesk_ticket.py
@@ -0,0 +1,94 @@
+# -*- 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()
+
+ action = {
+ "type": "ir.actions.act_window",
+ "res_model": "sale.order",
+ "context": {"create": False},
+ "name": _("Sale Orders"),
+ "view_mode": "list,form",
+ }
+
+ if self.sale_order_count == 1:
+ action.update({
+ "res_id": self.sale_order_ids[0].id,
+ "view_mode": "form",
+ })
+ else:
+ action.update({
+ "domain": [("id", "in", self.sale_order_ids.ids)],
+ })
+ 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",
+ }
+ )
+
+ # Archive the ticket
+ self.action_archive()
+
+ # Retourner une action directe vers le devis créé
+ return {
+ "type": "ir.actions.act_window",
+ "res_model": "sale.order",
+ "res_id": so.id,
+ "view_mode": "form",
+ "context": {"create": False},
+ }
+
+ 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,
+ }
diff --git a/helpdesk_sale_order/models/sale_order.py b/helpdesk_sale_order/models/sale_order.py
new file mode 100644
index 0000000..9378b4a
--- /dev/null
+++ b/helpdesk_sale_order/models/sale_order.py
@@ -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
diff --git a/helpdesk_sale_order/static/description/icon.png b/helpdesk_sale_order/static/description/icon.png
new file mode 100644
index 0000000..1a0c0fb
Binary files /dev/null and b/helpdesk_sale_order/static/description/icon.png differ
diff --git a/helpdesk_sale_order/tests/__init__.py b/helpdesk_sale_order/tests/__init__.py
new file mode 100644
index 0000000..2c7eba5
--- /dev/null
+++ b/helpdesk_sale_order/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_helpdesk_sale
diff --git a/helpdesk_sale_order/tests/test_helpdesk_sale.py b/helpdesk_sale_order/tests/test_helpdesk_sale.py
new file mode 100644
index 0000000..c57151b
--- /dev/null
+++ b/helpdesk_sale_order/tests/test_helpdesk_sale.py
@@ -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)
diff --git a/helpdesk_sale_order/views/account_move_view.xml b/helpdesk_sale_order/views/account_move_view.xml
new file mode 100644
index 0000000..2686d33
--- /dev/null
+++ b/helpdesk_sale_order/views/account_move_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ account.move.from.inherited.helpdesk
+ account.move
+
+
+
+
+
+
+
+
+
+
+
diff --git a/helpdesk_sale_order/views/helpdesk_team_views.xml b/helpdesk_sale_order/views/helpdesk_team_views.xml
new file mode 100644
index 0000000..60498e1
--- /dev/null
+++ b/helpdesk_sale_order/views/helpdesk_team_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ helpdesk.team.form.inherit.sale.order
+ helpdesk.team
+
+
+
+
+
+
+
+
+
+
diff --git a/helpdesk_sale_order/views/helpdesk_ticket_views.xml b/helpdesk_sale_order/views/helpdesk_ticket_views.xml
new file mode 100644
index 0000000..e9a3578
--- /dev/null
+++ b/helpdesk_sale_order/views/helpdesk_ticket_views.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ helpdesk.ticket.form.view.inherit.saleorder
+ helpdesk.ticket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/helpdesk_sale_order/views/sale_view.xml b/helpdesk_sale_order/views/sale_view.xml
new file mode 100644
index 0000000..b575981
--- /dev/null
+++ b/helpdesk_sale_order/views/sale_view.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ sale.order.from.inherited.saleorder
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/helpdesk_sale_order/wizard/__init__.py b/helpdesk_sale_order/wizard/__init__.py
new file mode 100644
index 0000000..713605c
--- /dev/null
+++ b/helpdesk_sale_order/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import sale_advance_payment
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/helpdesk_sale_order/wizard/sale_advance_payment.py b/helpdesk_sale_order/wizard/sale_advance_payment.py
new file mode 100644
index 0000000..40d8975
--- /dev/null
+++ b/helpdesk_sale_order/wizard/sale_advance_payment.py
@@ -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
\ No newline at end of file
diff --git a/helpdesk_sale_order_ai/__init__.py b/helpdesk_sale_order_ai/__init__.py
new file mode 100644
index 0000000..cde864b
--- /dev/null
+++ b/helpdesk_sale_order_ai/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/helpdesk_sale_order_ai/__manifest__.py b/helpdesk_sale_order_ai/__manifest__.py
new file mode 100644
index 0000000..5be0551
--- /dev/null
+++ b/helpdesk_sale_order_ai/__manifest__.py
@@ -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,
+}
diff --git a/helpdesk_sale_order_ai/models/__init__.py b/helpdesk_sale_order_ai/models/__init__.py
new file mode 100644
index 0000000..1be4296
--- /dev/null
+++ b/helpdesk_sale_order_ai/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import helpdesk_ticket
+from . import helpdesk_team
diff --git a/helpdesk_sale_order_ai/models/helpdesk_team.py b/helpdesk_sale_order_ai/models/helpdesk_team.py
new file mode 100644
index 0000000..7cc6df0
--- /dev/null
+++ b/helpdesk_sale_order_ai/models/helpdesk_team.py
@@ -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
+"""
+ )
diff --git a/helpdesk_sale_order_ai/models/helpdesk_ticket.py b/helpdesk_sale_order_ai/models/helpdesk_ticket.py
new file mode 100644
index 0000000..7757810
--- /dev/null
+++ b/helpdesk_sale_order_ai/models/helpdesk_ticket.py
@@ -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
diff --git a/helpdesk_sale_order_ai/views/helpdesk_team_views.xml b/helpdesk_sale_order_ai/views/helpdesk_team_views.xml
new file mode 100644
index 0000000..23708a2
--- /dev/null
+++ b/helpdesk_sale_order_ai/views/helpdesk_team_views.xml
@@ -0,0 +1,14 @@
+
+
+
+ helpdesk.team.form.inherit.sale.order.ai
+ helpdesk.team
+
+
+
+
+
+
+
+
+