From 52b4c30e711dd3a26fd5d16aa2011780b4c6c2ca Mon Sep 17 00:00:00 2001 From: Franco Leyes Date: Thu, 21 May 2026 15:12:50 +0000 Subject: [PATCH] [IMP] crm_sale_ux: Implement auto mark opportunities as Won feature on sales order confirmation --- crm_sale_ux/README.rst | 4 +- crm_sale_ux/__init__.py | 2 + crm_sale_ux/__manifest__.py | 3 +- crm_sale_ux/models/__init__.py | 5 ++ crm_sale_ux/models/res_config_settings.py | 14 ++++ crm_sale_ux/models/sale_order.py | 27 +++++++ crm_sale_ux/tests/__init__.py | 4 + crm_sale_ux/tests/test_auto_set_won.py | 77 +++++++++++++++++++ .../views/res_config_settings_views.xml | 19 +++++ 9 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 crm_sale_ux/models/__init__.py create mode 100644 crm_sale_ux/models/res_config_settings.py create mode 100644 crm_sale_ux/models/sale_order.py create mode 100644 crm_sale_ux/tests/__init__.py create mode 100644 crm_sale_ux/tests/test_auto_set_won.py create mode 100644 crm_sale_ux/views/res_config_settings_views.xml diff --git a/crm_sale_ux/README.rst b/crm_sale_ux/README.rst index 37a5461da..00ad0d4ef 100644 --- a/crm_sale_ux/README.rst +++ b/crm_sale_ux/README.rst @@ -17,6 +17,7 @@ CRM Sale UX This module: #. If the option "Allow any user as salesman" is enable, it would be possible to assign any user as salesperson in leads, opportunities and teams. +#. Adds a CRM setting to automatically mark opportunities as Won when their quotation is confirmed. Installation ============ @@ -30,7 +31,8 @@ Configuration To configure this module, you need to: -#. Nothing to configure +#. Go to CRM -> Configuration -> Settings. +#. Enable "Auto mark opportunities as Won". Usage ===== diff --git a/crm_sale_ux/__init__.py b/crm_sale_ux/__init__.py index b05ab175f..b0bff36b0 100644 --- a/crm_sale_ux/__init__.py +++ b/crm_sale_ux/__init__.py @@ -1,2 +1,4 @@ # © 2016 ADHOC SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/crm_sale_ux/__manifest__.py b/crm_sale_ux/__manifest__.py index 22ab3f32a..e4a1e856a 100644 --- a/crm_sale_ux/__manifest__.py +++ b/crm_sale_ux/__manifest__.py @@ -9,9 +9,10 @@ "website": "www.adhoc.com.ar", "license": "AGPL-3", "images": [], - "depends": ["crm", "sale_ux"], + "depends": ["sale_crm", "sale_ux"], "data": [ "views/crm_lead_views.xml", + "views/res_config_settings_views.xml", ], "demo": [], "installable": True, diff --git a/crm_sale_ux/models/__init__.py b/crm_sale_ux/models/__init__.py new file mode 100644 index 000000000..c9a1373f0 --- /dev/null +++ b/crm_sale_ux/models/__init__.py @@ -0,0 +1,5 @@ +# © 2016 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import res_config_settings +from . import sale_order diff --git a/crm_sale_ux/models/res_config_settings.py b/crm_sale_ux/models/res_config_settings.py new file mode 100644 index 000000000..0d068629f --- /dev/null +++ b/crm_sale_ux/models/res_config_settings.py @@ -0,0 +1,14 @@ +# © 2026 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + crm_auto_won_on_sale_confirm = fields.Boolean( + string="Mark opportunity as won on sales order confirmation", + config_parameter="crm_sale_ux.auto_won_on_sale_confirm", + help="When enabled, confirming a quotation linked to an opportunity marks that opportunity as Won.", + ) diff --git a/crm_sale_ux/models/sale_order.py b/crm_sale_ux/models/sale_order.py new file mode 100644 index 000000000..462f9b369 --- /dev/null +++ b/crm_sale_ux/models/sale_order.py @@ -0,0 +1,27 @@ +# © 2026 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.tools import str2bool + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_confirm(self): + res = super().action_confirm() + + enabled = str2bool( + self.env["ir.config_parameter"].sudo().get_param("crm_sale_ux.auto_won_on_sale_confirm", "False"), + False, + ) + if not enabled: + return res + + opportunities = self.mapped("opportunity_id").filtered( + lambda lead: lead.type == "opportunity" and not lead.stage_id.is_won + ) + if opportunities: + opportunities.action_set_won() + + return res diff --git a/crm_sale_ux/tests/__init__.py b/crm_sale_ux/tests/__init__.py new file mode 100644 index 000000000..955f1a1ef --- /dev/null +++ b/crm_sale_ux/tests/__init__.py @@ -0,0 +1,4 @@ +# © 2026 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_auto_set_won diff --git a/crm_sale_ux/tests/test_auto_set_won.py b/crm_sale_ux/tests/test_auto_set_won.py new file mode 100644 index 000000000..25b9295dd --- /dev/null +++ b/crm_sale_ux/tests/test_auto_set_won.py @@ -0,0 +1,77 @@ +# © 2026 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.crm.tests.common import TestCrmCommon +from odoo.fields import Command +from odoo.tests import tagged + + +@tagged("crm_sale_ux", "post_install", "-at_install") +class TestCrmSaleUxAutoSetWon(TestCrmCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env.user.partner_id + cls.product = cls.env["product.product"].create( + { + "name": "CRM Sale UX product", + "type": "service", + "list_price": 100.0, + } + ) + + def _create_opportunity(self, name): + stage = self.env["crm.stage"].search([("is_won", "=", False)], order="sequence, id", limit=1) + return self.env["crm.lead"].create( + { + "name": name, + "type": "opportunity", + "partner_id": self.partner.id, + "stage_id": stage.id, + "user_id": self.env.user.id, + } + ) + + def _create_quotation(self, opportunity): + return self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "opportunity_id": opportunity.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + "product_uom_qty": 1.0, + "price_unit": self.product.list_price, + "name": self.product.name, + } + ) + ], + } + ) + + def test_confirm_quotation_does_not_set_won_when_disabled(self): + self.env["ir.config_parameter"].sudo().set_param("crm_sale_ux.auto_won_on_sale_confirm", False) + opportunity = self._create_opportunity("Won setting disabled") + stage_before = opportunity.stage_id + + quotation = self._create_quotation(opportunity) + quotation.action_confirm() + + self.assertEqual(opportunity.stage_id, stage_before) + self.assertFalse(opportunity.stage_id.is_won) + + def test_confirm_quotation_sets_won_when_enabled(self): + self.env["ir.config_parameter"].sudo().set_param("crm_sale_ux.auto_won_on_sale_confirm", True) + opportunity = self._create_opportunity("Won setting enabled") + + quotation = self._create_quotation(opportunity) + + sale_exception_installed = self.env["sale.order"]._fields.get("ignore_exception") + if sale_exception_installed: + self.env["exception.rule"].search([("active", "=", True)]).write({"active": False}) + + quotation.action_confirm() + + self.assertTrue(opportunity.stage_id.is_won) + self.assertEqual(opportunity.probability, 100) diff --git a/crm_sale_ux/views/res_config_settings_views.xml b/crm_sale_ux/views/res_config_settings_views.xml new file mode 100644 index 000000000..93769a1a2 --- /dev/null +++ b/crm_sale_ux/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ + + + + res.config.settings.view.form.inherit.crm.sale.ux + res.config.settings + + + + + + + + + +