diff --git a/srm/README.rst b/srm/README.rst new file mode 100644 index 00000000000..18494ea65ab --- /dev/null +++ b/srm/README.rst @@ -0,0 +1,89 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=== +SRM +=== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6e6380a8b3a6c8aa36be3cb6861a190da89e603afaa94e6c982b505f29c5e56f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcrm-lightgray.png?logo=github + :target: https://github.com/OCA/crm/tree/19.0/srm + :alt: OCA/crm +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/crm-19-0/crm-19-0-srm + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/crm&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows the usage of crm module to manage leads coming from +suppliers. The flow is similar to CRM. The main change is that leads be +generated from customer or supplier request type. For supplier requests +leads can be converted in purchases. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Telmo Santos +- Vincent Van Rossem + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/crm `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/srm/__init__.py b/srm/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/srm/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/srm/__manifest__.py b/srm/__manifest__.py new file mode 100644 index 00000000000..b6c2db2beb1 --- /dev/null +++ b/srm/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "SRM", + "summary": "Use CRM model for suppliers", + "version": "19.0.1.0.0", + "development_status": "Alpha", + "category": "CRM", + "website": "https://github.com/OCA/crm", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "account", + "crm", + "sale_crm", + "purchase", + ], + "data": [ + "security/ir.model.access.csv", + "views/srm_menu.xml", + "views/srm_lead.xml", + "views/crm_lead.xml", + "views/purchase_order.xml", + "wizard/srm_opportunity_to_rfq.xml", + ], + "installable": True, +} diff --git a/srm/models/__init__.py b/srm/models/__init__.py new file mode 100644 index 00000000000..313fab586fb --- /dev/null +++ b/srm/models/__init__.py @@ -0,0 +1,3 @@ +from . import crm_lead +from . import crm_team +from . import purchase_order diff --git a/srm/models/crm_lead.py b/srm/models/crm_lead.py new file mode 100644 index 00000000000..c6965da64a6 --- /dev/null +++ b/srm/models/crm_lead.py @@ -0,0 +1,147 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from collections import defaultdict + +from odoo import api, fields, models +from odoo.fields import Domain + + +class CrmLead(models.Model): + _inherit = "crm.lead" + + user_id = fields.Many2one(string="Responsible") + team_id = fields.Many2one(string="Team") + + request_type = fields.Selection( + selection=[ + ("customer", "Customer Lead"), + ("supplier", "Supplier Lead"), + ], + ) + purchase_amount_total = fields.Monetary( + compute="_compute_purchase_amount_total", + string="Sum of Purchase Orders", + help="Untaxed Total of Confirmed Purchase Orders", + currency_field="company_currency", + ) + request_for_quotation_count = fields.Integer( + compute="_compute_request_for_quotation_count", + string="Number of Request for Quotations", + ) + purchase_order_count = fields.Integer( + compute="_compute_purchase_order_count", string="Number of Purchase Orders" + ) + purchase_order_ids = fields.One2many( + comodel_name="purchase.order", + inverse_name="opportunity_id", + string="Purchase Orders", + ) + + def _get_lead_purchase_order_domain(self): + return Domain("state", "not in", ("draft", "sent", "cancel")) + + def _get_lead_request_for_quotation_domain(self): + return Domain("state", "in", ("draft", "sent")) + + def _get_purchase_order_lead_domain(self): + return Domain("opportunity_id", "in", self.ids) + + @api.depends("purchase_order_ids.state") + def _compute_purchase_order_count(self): + purchase_order_per_lead = { + lead.id: count + for lead, count in self.env["purchase.order"]._read_group( + domain=Domain.AND( + [ + self._get_purchase_order_lead_domain(), + self._get_lead_purchase_order_domain(), + ] + ), + groupby=["opportunity_id"], + aggregates=["__count"], + ) + } + for lead in self: + lead.purchase_order_count = purchase_order_per_lead.get(lead.id, 0) + + @api.depends("purchase_order_ids.state") + def _compute_request_for_quotation_count(self): + rfq_per_lead = { + lead.id: count + for lead, count in self.env["purchase.order"]._read_group( + domain=Domain.AND( + [ + self._get_purchase_order_lead_domain(), + self._get_lead_request_for_quotation_domain(), + ] + ), + groupby=["opportunity_id"], + aggregates=["__count"], + ) + } + for lead in self: + lead.request_for_quotation_count = rfq_per_lead.get(lead.id, 0) + + @api.depends( + "purchase_order_ids.state", + "purchase_order_ids.currency_id", + "purchase_order_ids.amount_untaxed", + "purchase_order_ids.date_order", + "purchase_order_ids.company_id", + ) + def _compute_purchase_amount_total(self): + amount_per_lead = defaultdict(float) + + for lead, currency, company, date_order, amount in self.env[ + "purchase.order" + ]._read_group( + domain=Domain.AND( + [ + self._get_purchase_order_lead_domain(), + self._get_lead_purchase_order_domain(), + ] + ), + groupby=["opportunity_id", "currency_id", "company_id", "date_order:day"], + aggregates=["amount_untaxed:sum"], + ): + company_currency = lead.company_currency or self.env.company.currency_id + amount_per_lead[lead.id] += currency._convert( + amount, + company_currency, + company, + date_order or fields.Date.context_today(self), + ) + + for lead in self: + lead.purchase_amount_total = amount_per_lead.get(lead.id, 0.0) + + def _create_customer(self, with_parent=None): + """It can be a customer or supplier depending on lead request type""" + self.ensure_one() + self = self.with_context(res_partner_search_mode=self.request_type) + return super()._create_customer(with_parent=with_parent) + + def action_lead_rfq_new(self): + self.ensure_one() + if not self.partner_id: + return self.env["ir.actions.actions"]._for_xml_id( + "srm.srm_rfq_partner_action" + ) + else: + return self.action_rfq_new() + + def action_rfq_new(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id("srm.action_lead_rfq_new") + action["context"] = self._prepare_rfq_context() + return action + + def _prepare_rfq_context(self): + self.ensure_one() + rfq_context = { + "default_partner_id": self.partner_id.id, + "default_opportunity_id": self.id, + } + if self.user_id: + rfq_context["default_user_id"] = self.user_id.id + return rfq_context diff --git a/srm/models/crm_team.py b/srm/models/crm_team.py new file mode 100644 index 00000000000..5bcd0045b62 --- /dev/null +++ b/srm/models/crm_team.py @@ -0,0 +1,18 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class Team(models.Model): + _inherit = "crm.team" + + @api.model + def action_your_pipeline(self): + action = super().action_your_pipeline() + if request_type := self.env.context.get("request_type", False): + action["domain"] = ( + f"[('type','=','opportunity'), ('request_type', '=', '{request_type}')]" + ) + action["context"]["default_request_type"] = request_type + return action diff --git a/srm/models/purchase_order.py b/srm/models/purchase_order.py new file mode 100644 index 00000000000..9304c3a4a80 --- /dev/null +++ b/srm/models/purchase_order.py @@ -0,0 +1,23 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + opportunity_id = fields.Many2one( + comodel_name="crm.lead", + string="Opportunity", + check_company=True, + domain=""" + [ + ('type', '=', 'opportunity'), + ('request_type', '=', 'supplier'), + '|', + ('company_id', '=', False), + ('company_id', '=', company_id), + ] + """, + ) diff --git a/srm/pyproject.toml b/srm/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/srm/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/srm/readme/CONTRIBUTORS.md b/srm/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..4c486a0a610 --- /dev/null +++ b/srm/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Telmo Santos \<\> +- Vincent Van Rossem \<\> diff --git a/srm/readme/DESCRIPTION.md b/srm/readme/DESCRIPTION.md new file mode 100644 index 00000000000..d210882c47d --- /dev/null +++ b/srm/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows the usage of crm module to manage leads coming from suppliers. +The flow is similar to CRM. The main change is that leads be generated from customer or supplier request type. +For supplier requests leads can be converted in purchases. diff --git a/srm/security/ir.model.access.csv b/srm/security/ir.model.access.csv new file mode 100644 index 00000000000..f7e0abb7e62 --- /dev/null +++ b/srm/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_srm_rfq_partner,access.srm.rfq.partner,model_srm_rfq_partner,purchase.group_purchase_user,1,1,1,0 diff --git a/srm/static/description/index.html b/srm/static/description/index.html new file mode 100644 index 00000000000..a1c270c0b65 --- /dev/null +++ b/srm/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

SRM

+ +

Alpha License: AGPL-3 OCA/crm Translate me on Weblate Try me on Runboat

+

This module allows the usage of crm module to manage leads coming from +suppliers. The flow is similar to CRM. The main change is that leads be +generated from customer or supplier request type. For supplier requests +leads can be converted in purchases.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/crm project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/srm/tests/__init__.py b/srm/tests/__init__.py new file mode 100644 index 00000000000..a330a068a83 --- /dev/null +++ b/srm/tests/__init__.py @@ -0,0 +1 @@ +from . import test_srm diff --git a/srm/tests/test_srm.py b/srm/tests/test_srm.py new file mode 100644 index 00000000000..c87cab59acc --- /dev/null +++ b/srm/tests/test_srm.py @@ -0,0 +1,262 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import Command, fields +from odoo.tests.common import tagged, users + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT +from odoo.addons.crm.tests import common as crm_common + + +@tagged("lead_manage") +class TestSupplierRelationshipManagement(crm_common.TestCrmCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + cls.user_sales_salesman.group_ids |= cls.env.ref("purchase.group_purchase_user") + cls.lead_1.write( + { + "user_id": cls.user_sales_salesman.id, + } + ) + cls.lead_2 = cls.env["crm.lead"].create( + { + "name": "Jimmy Choo Request", + "type": "lead", + "user_id": cls.user_sales_leads.id, + "team_id": cls.sales_team_1.id, + "partner_id": False, + "contact_name": "Jimmy Choo", + "email_from": "jimmy.choo@test.example.com", + "lang_id": cls.lang_fr.id, + "phone": "+1 202 555 9999", + "country_id": cls.env.ref("base.us").id, + "probability": 20, + } + ) + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.product = cls.env["product.product"].create( + { + "name": "Product", + "list_price": 30.0, + "standard_price": 30.0, + "type": "consu", + "uom_id": cls.uom_unit.id, + "purchase_ok": True, + } + ) + + cls._create_crm_purchase_orders(cls.lead_1, cls.contact_company) + cls._create_crm_purchase_orders( + cls.lead_2, + cls.contact_company_1, + prefix="OTHER", + ) + + @classmethod + def _create_purchase_order( + cls, lead, partner, qty, *, state="draft", name_suffix="" + ): + po = cls.env["purchase.order"].create( + { + "partner_id": partner.id, + "opportunity_id": lead.id, + "order_line": [ + Command.create( + { + "name": f"{cls.product.name} {name_suffix}".strip(), + "product_id": cls.product.id, + "product_qty": qty, + "product_uom_id": cls.product.uom_id.id, + "price_unit": cls.product.list_price, + "date_planned": fields.Datetime.now(), + } + ), + ], + } + ) + + if state == "sent": + po.write({"state": "sent"}) + elif state == "purchase": + po.button_confirm() + elif state == "cancel": + po.button_cancel() + + return po + + @classmethod + def _create_crm_purchase_orders(cls, lead, partner, prefix="MAIN"): + cls.rfq_draft = cls._create_purchase_order( + lead, partner, 1.0, state="draft", name_suffix=f"{prefix} RFQ DRAFT" + ) + cls.rfq_sent = cls._create_purchase_order( + lead, partner, 2.0, state="sent", name_suffix=f"{prefix} RFQ SENT" + ) + cls.po_1 = cls._create_purchase_order( + lead, partner, 3.0, state="purchase", name_suffix=f"{prefix} PO 1" + ) + cls.po_2 = cls._create_purchase_order( + lead, partner, 4.0, state="purchase", name_suffix=f"{prefix} PO 2" + ) + cls.po_cancel = cls._create_purchase_order( + lead, partner, 5.0, state="cancel", name_suffix=f"{prefix} PO CANCEL" + ) + + def test_00_lead_purchase_data(self): + """Test that the lead's purchase data is correctly computed.""" + # RFQs: draft + sent + self.assertEqual(self.lead_1.request_for_quotation_count, 2) + + # Purchase Orders: confirmed only (draft/sent/cancel excluded) + self.assertEqual(self.lead_1.purchase_order_count, 2) + + expected_amount = self.po_1.amount_untaxed + self.po_2.amount_untaxed + self.assertEqual(self.lead_1.purchase_amount_total, expected_amount) + + def test_01_lead_create_request_type_partner(self): + """Test that a customer is created with specified type.""" + lead_customer = self.lead_1.with_user(self.env.user).copy( + { + "request_type": "customer", + } + ) + lead_supplier = self.lead_1.with_user(self.env.user).copy( + { + "request_type": "supplier", + } + ) + customer = lead_customer._create_customer() + supplier = lead_supplier._create_customer() + self.assertEqual(customer.supplier_rank, 0) + self.assertEqual(customer.customer_rank, 1) + self.assertEqual(supplier.supplier_rank, 1) + self.assertEqual(supplier.customer_rank, 0) + + # N.B.: the following tests are adapted from the standard analogues in ``sale_crm`` + @users("user_sales_salesman") + def test_02_lead_convert_to_rfq_create(self): + """Test that a RFQ can be created from a lead.""" + # Perform initial tests, do not repeat them at each test + lead = self.lead_1.with_user(self.env.user) + self.assertEqual(lead.partner_id, self.env["res.partner"]) + new_partner = self.env["res.partner"].search( + [("email_normalized", "=", "amy.wong@test.example.com")] + ) + self.assertEqual(new_partner, self.env["res.partner"]) + + # invoke wizard and apply it + convert = ( + self.env["srm.rfq.partner"] + .with_context(**{"active_model": "crm.lead", "active_id": lead.id}) + .create({}) + ) + + self.assertEqual(convert.action, "create") + self.assertEqual(convert.partner_id, self.env["res.partner"]) + + action = convert.action_apply() + + # test lead update + new_partner = self.env["res.partner"].search( + [("email_normalized", "=", "amy.wong@test.example.com")] + ) + self.assertEqual(lead.partner_id, new_partner) + # test wizard action (does not create anything, just returns action) + self.assertEqual(action["res_model"], "purchase.order") + self.assertEqual(action["context"]["default_partner_id"], new_partner.id) + self.assertEqual(action["context"]["default_opportunity_id"], lead.id) + self.assertEqual(action["context"]["default_user_id"], lead.user_id.id) + + @users("user_sales_salesman") + def test_03_lead_convert_to_rfq_exist(self): + """Test taking only existing customer while converting.""" + lead = self.lead_1.with_user(self.env.user) + # invoke wizard and apply it + convert = ( + self.env["srm.rfq.partner"] + .with_context(**{"active_model": "crm.lead", "active_id": lead.id}) + .create({"action": "exist"}) + ) + + self.assertEqual(convert.action, "exist") + self.assertEqual(convert.partner_id, self.env["res.partner"]) + + action = convert.action_apply() + + # test lead update + new_partner = self.env["res.partner"].search( + [("email_normalized", "=", "amy.wong@test.example.com")] + ) + self.assertEqual(new_partner, self.env["res.partner"]) + + convert.write({"partner_id": self.contact_2.id}) + action = convert.action_apply() + + # test lead update + new_partner = self.env["res.partner"].search( + [("email_normalized", "=", "amy.wong@test.example.com")] + ) + self.assertEqual(new_partner, self.env["res.partner"]) + self.assertEqual(lead.partner_id, self.contact_2) + self.assertEqual(lead.email_from, self.contact_2.email) + self.assertEqual(action["context"]["default_partner_id"], self.contact_2.id) + self.assertEqual(action["context"]["default_opportunity_id"], lead.id) + + @users("user_sales_salesman") + def test_04_lead_convert_to_rfq_false_match_create(self): + lead = self.lead_1.with_user(self.env.user) + + # invoke wizard and apply it + convert = ( + self.env["srm.rfq.partner"] + .with_context( + **{ + "active_model": "crm.lead", + "active_id": lead.id, + } + ) + .create({"action": "create"}) + ) + + convert.write({"partner_id": self.contact_2.id}) + + self.assertEqual(convert.action, "create") + + # ignore matching partner and create a new one + convert.action_apply() + + self.assertTrue(bool(lead.partner_id.id)) + self.assertNotEqual(lead.partner_id, self.contact_2) + + @users("user_sales_salesman") + def test_05_lead_convert_to_rfq_nothing(self): + """Test doing nothing about customer while converting""" + lead = self.lead_1.with_user(self.env.user) + + # invoke wizard and apply it + convert = ( + self.env["srm.rfq.partner"] + .with_context( + **{ + "active_model": "crm.lead", + "active_id": lead.id, + "default_action": "nothing", + } + ) + .create({}) + ) + + self.assertEqual(convert.action, "nothing") + self.assertEqual(convert.partner_id, self.env["res.partner"]) + + action = convert.action_apply() + + # test lead update + new_partner = self.env["res.partner"].search( + [("email_normalized", "=", "amy.wong@test.example.com")] + ) + self.assertEqual(new_partner, self.env["res.partner"]) + self.assertEqual(lead.partner_id, self.env["res.partner"]) + self.assertEqual(action["context"]["default_partner_id"], False) + self.assertEqual(action["context"]["default_opportunity_id"], lead.id) diff --git a/srm/views/crm_lead.xml b/srm/views/crm_lead.xml new file mode 100644 index 00000000000..65387d3fec0 --- /dev/null +++ b/srm/views/crm_lead.xml @@ -0,0 +1,20 @@ + + + + action = model.with_context(request_type='customer').action_your_pipeline() + + + + [('type', '=', 'opportunity'), ('request_type', '=', 'customer')] + { + 'default_type': 'opportunity', + 'search_default_assigned_to_me': 1, + 'show_user_team_stages': 1, + 'default_request_type': 'customer', + } + + diff --git a/srm/views/purchase_order.xml b/srm/views/purchase_order.xml new file mode 100644 index 00000000000..a340b23886b --- /dev/null +++ b/srm/views/purchase_order.xml @@ -0,0 +1,22 @@ + + + + purchase.order.form + purchase.order + + + + + + + + + + Lead RFQ new + purchase.order + form + {'search_default_partner_id': active_id, 'default_partner_id': active_id} + + diff --git a/srm/views/srm_lead.xml b/srm/views/srm_lead.xml new file mode 100644 index 00000000000..6b74dbbc724 --- /dev/null +++ b/srm/views/srm_lead.xml @@ -0,0 +1,225 @@ + + + + crm.lead.form + crm.lead + + + + 1 + + + + + + + + + + + crm.lead.oppor.inherited.crm + crm.lead + + + + + + + + + + + + + + srm.lead.form.quick_create + crm.lead + + + + + + + + + + + Srm: Pipeline + crm.lead + kanban,list,graph,pivot,form,calendar,activity + [('type','=','opportunity'), ('request_type','=', 'supplier')] + + { + 'default_type': 'opportunity', + 'search_default_assigned_to_me': 1, + 'default_request_type': 'supplier', + } + + + + + + Srm: My Pipeline + + code + action = model.with_context(request_type='supplier').action_your_pipeline() + + + + + Leads + crm.lead + list,kanban,graph,pivot,calendar,form,activity + ['&','|', ('type','=','lead'), ('type','=',False), ('request_type', '=', 'supplier')] + + + { + 'default_type':'lead', + 'default_request_type': 'supplier', + 'search_default_type': 'lead', + 'search_default_to_process':1, + } + + + + + + Srm Leads Analysis + crm.lead + pivot,graph,list + ['&', ('request_type','=', 'supplier'), '|', ('active','=',True), ('active','=',False)] + + + + + + Pipeline Analysis + crm.lead + pivot,graph,list,form,cohort + [('request_type','=', 'supplier')] + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/srm/views/srm_menu.xml b/srm/views/srm_menu.xml new file mode 100644 index 00000000000..860a6aeb3f2 --- /dev/null +++ b/srm/views/srm_menu.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/srm/wizard/__init__.py b/srm/wizard/__init__.py new file mode 100644 index 00000000000..14fe9893e82 --- /dev/null +++ b/srm/wizard/__init__.py @@ -0,0 +1 @@ +from . import srm_opportunity_to_rfq diff --git a/srm/wizard/srm_opportunity_to_rfq.py b/srm/wizard/srm_opportunity_to_rfq.py new file mode 100644 index 00000000000..ff8adaedef9 --- /dev/null +++ b/srm/wizard/srm_opportunity_to_rfq.py @@ -0,0 +1,60 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class Opportunity2Rfq(models.TransientModel): + _name = "srm.rfq.partner" + _description = "Create new or use existing Supplier on new RFQ" + + @api.model + def default_get(self, field_list): + result = super().default_get(field_list) + + if self.env.context.get("active_model") != "crm.lead": + raise UserError(self.env._("You can only apply this action from a lead.")) + + lead = self.env["crm.lead"] + lead_id = result.get("lead_id") or ( + self.env.context.get("active_id") if "lead_id" in field_list else False + ) + if lead_id: + lead = self.env["crm.lead"].browse(lead_id) + result["lead_id"] = lead.id + + partner_id = result.get("partner_id") or lead._find_matching_partner().id + + if "action" in field_list and not result.get("action"): + result["action"] = "exist" if partner_id else "create" + if "partner_id" in field_list and not result.get("partner_id"): + result["partner_id"] = partner_id + + return result + + action = fields.Selection( + selection=[ + ("create", "Create a new vendor"), + ("exist", "Link to an existing vendor"), + ("nothing", "Do not link to a vendor"), + ], + string="RFQ Vendor", + required=True, + ) + lead_id = fields.Many2one( + comodel_name="crm.lead", string="Associated Lead", required=True + ) + partner_id = fields.Many2one(comodel_name="res.partner", string="Vendor") + + def action_apply(self): + """Create/link vendor if requested, then open the RFQ form.""" + self.ensure_one() + if self.action == "create": + self.lead_id._handle_partner_assignment(create_missing=True) + elif self.action == "exist": + self.lead_id._handle_partner_assignment( + force_partner_id=self.partner_id.id, + create_missing=False, + ) + return self.lead_id.action_rfq_new() diff --git a/srm/wizard/srm_opportunity_to_rfq.xml b/srm/wizard/srm_opportunity_to_rfq.xml new file mode 100644 index 00000000000..41196dc3ac4 --- /dev/null +++ b/srm/wizard/srm_opportunity_to_rfq.xml @@ -0,0 +1,45 @@ + + + + srm.rfq.partner.view.form + srm.rfq.partner + +
+ + + + + + + + + + + +
+
+
+
+
+ + + New RFQ + ir.actions.act_window + srm.rfq.partner + form + + new + +