diff --git a/srm/README.rst b/srm/README.rst new file mode 100644 index 00000000000..e28fbd3d32c --- /dev/null +++ b/srm/README.rst @@ -0,0 +1,85 @@ +=== +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/licence-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/18.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-18-0/crm-18-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=18.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..0123ab3cfa9 --- /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": "18.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..82dd669bf67 --- /dev/null +++ b/srm/models/crm_lead.py @@ -0,0 +1,138 @@ +# 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 + + +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 [("state", "not in", ("draft", "sent", "cancel"))] + + def _get_lead_request_for_quotation_domain(self): + return [("state", "in", ("draft", "sent"))] + + @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=[ + ("opportunity_id", "in", self.ids), + *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=[ + ("opportunity_id", "in", self.ids), + *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=[ + ("opportunity_id", "in", self.ids), + *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): + """It can be a customer or supplier depending on lead request type""" + self = self.with_context(res_partner_search_mode=self.request_type) + return super()._create_customer() + + def action_lead_rfq_new(self): + 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): + 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..65beadaba1a --- /dev/null +++ b/srm/models/crm_team.py @@ -0,0 +1,19 @@ +# 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() + request_type = self.env.context.get("request_type") + if request_type: + 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..6eafd244ebb --- /dev/null +++ b/srm/models/purchase_order.py @@ -0,0 +1,16 @@ +# 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..bc1a392f913 --- /dev/null +++ b/srm/static/description/index.html @@ -0,0 +1,433 @@ + + + + + +SRM + + + +
+

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/views/crm_lead.xml b/srm/views/crm_lead.xml new file mode 100644 index 00000000000..5fc597bbb2e --- /dev/null +++ b/srm/views/crm_lead.xml @@ -0,0 +1,17 @@ + + + + action = model.with_context(request_type='customer').action_your_pipeline() + + + + [('type','=','opportunity'),('request_type','in',(False,'customer'))] + {'default_type': 'opportunity', 'default_request_type': 'customer', 'search_default_assigned_to_me': 1} + + 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..4426e42fe23 --- /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..7058f3623bd --- /dev/null +++ b/srm/wizard/srm_opportunity_to_rfq.py @@ -0,0 +1,58 @@ +# 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, fields): + result = super().default_get(fields) + + active_model = self.env.context.get("active_model") + if active_model != "crm.lead": + raise UserError(self.env._("You can only apply this action from a lead.")) + + lead = False + if result.get("lead_id"): + lead = self.env["crm.lead"].browse(result["lead_id"]) + elif "lead_id" in fields and self.env.context.get("active_id"): + lead = self.env["crm.lead"].browse(self.env.context["active_id"]) + if lead: + result["lead_id"] = lead.id + partner_id = result.get("partner_id") or lead._find_matching_partner().id + if "action" in fields and not result.get("action"): + result["action"] = "exist" if partner_id else "create" + if "partner_id" in fields and not result.get("partner_id"): + result["partner_id"] = partner_id + + return result + + action = fields.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("crm.lead", "Associated Lead", required=True) + partner_id = fields.Many2one("res.partner", "Vendor") + + def action_apply(self): + """Convert lead to opportunity or merge lead and opportunity and open + the freshly created opportunity view. + """ + 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 + +