diff --git a/helpdesk_ai_sale_order/__init__.py b/helpdesk_ai_sale_order/__init__.py new file mode 100644 index 0000000..c6d481c --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/__manifest__.py b/helpdesk_ai_sale_order/__manifest__.py new file mode 100644 index 0000000..1ea1e8e --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/migration18.md b/helpdesk_ai_sale_order/migration18.md new file mode 100644 index 0000000..dc98f30 --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/models/__init__.py b/helpdesk_ai_sale_order/models/__init__.py new file mode 100644 index 0000000..d1aeada --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/models/account_move.py b/helpdesk_ai_sale_order/models/account_move.py new file mode 100644 index 0000000..447b66d --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/models/helpdesk_team.py b/helpdesk_ai_sale_order/models/helpdesk_team.py new file mode 100644 index 0000000..1a8790d --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/models/helpdesk_ticket.py b/helpdesk_ai_sale_order/models/helpdesk_ticket.py new file mode 100644 index 0000000..1498eeb --- /dev/null +++ b/helpdesk_ai_sale_order/models/helpdesk_ticket.py @@ -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, + } diff --git a/helpdesk_ai_sale_order/models/sale_order.py b/helpdesk_ai_sale_order/models/sale_order.py new file mode 100644 index 0000000..9378b4a --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/static/description/icon.png b/helpdesk_ai_sale_order/static/description/icon.png new file mode 100644 index 0000000..1a0c0fb Binary files /dev/null and b/helpdesk_ai_sale_order/static/description/icon.png differ diff --git a/helpdesk_ai_sale_order/tests/__init__.py b/helpdesk_ai_sale_order/tests/__init__.py new file mode 100644 index 0000000..2c7eba5 --- /dev/null +++ b/helpdesk_ai_sale_order/tests/__init__.py @@ -0,0 +1 @@ +from . import test_helpdesk_sale diff --git a/helpdesk_ai_sale_order/tests/test_helpdesk_sale.py b/helpdesk_ai_sale_order/tests/test_helpdesk_sale.py new file mode 100644 index 0000000..c57151b --- /dev/null +++ b/helpdesk_ai_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_ai_sale_order/views/account_move_view.xml b/helpdesk_ai_sale_order/views/account_move_view.xml new file mode 100644 index 0000000..2686d33 --- /dev/null +++ b/helpdesk_ai_sale_order/views/account_move_view.xml @@ -0,0 +1,18 @@ + + + + + + account.move.from.inherited.helpdesk + account.move + + + + + + + + + + + diff --git a/helpdesk_ai_sale_order/views/helpdesk_team_views.xml b/helpdesk_ai_sale_order/views/helpdesk_team_views.xml new file mode 100644 index 0000000..60498e1 --- /dev/null +++ b/helpdesk_ai_sale_order/views/helpdesk_team_views.xml @@ -0,0 +1,15 @@ + + + + helpdesk.team.form.inherit.sale.order + helpdesk.team + + + + + + + + + + diff --git a/helpdesk_ai_sale_order/views/helpdesk_ticket_views.xml b/helpdesk_ai_sale_order/views/helpdesk_ticket_views.xml new file mode 100644 index 0000000..e9a3578 --- /dev/null +++ b/helpdesk_ai_sale_order/views/helpdesk_ticket_views.xml @@ -0,0 +1,32 @@ + + + + + helpdesk.ticket.form.view.inherit.saleorder + helpdesk.ticket + + + + + + + + + + + + diff --git a/helpdesk_ai_sale_order/views/sale_view.xml b/helpdesk_ai_sale_order/views/sale_view.xml new file mode 100644 index 0000000..b575981 --- /dev/null +++ b/helpdesk_ai_sale_order/views/sale_view.xml @@ -0,0 +1,26 @@ + + + + + + sale.order.from.inherited.saleorder + sale.order + + + + +