Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions crm_lead_escalation/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

===================
CRM Lead Escalation
===================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:4339b52bc69a03106fdcd37224108166ea3ce150fd82ea48c7367a7813c34497
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |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/crm_lead_escalation
: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-crm_lead_escalation
: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|

Leads sometimes stop moving: nobody calls the customer back, and nobody
marks the lead as won or lost either. This module lets you define
**escalation rules** that watch for those leads and warn a manager
automatically.

A rule answers three questions:

- **When?** Either the lead got no follow-up for a given delay, or it is
neither won nor lost a given delay after its creation. The delay is
expressed in minutes, hours or days.
- **Who?** The sales team leader of the lead, or one specific user.
- **What?** A to-do activity is scheduled on the lead for that person,
an email is sent and logged in the chatter of the lead with its
recipient, and the lead can optionally be reassigned to them.

Anything a salesperson does on the lead - a message, a planned or
completed activity such as a call or a meeting, a stage change or a
change of salesperson - counts as a follow-up and restarts the clock, so
nobody gets notified twice for the same standstill.

Several rules can run side by side, with their own delays, sales teams
and recipients.

Relation to the native rotting feature
--------------------------------------

Odoo 19 can already highlight stale records:
``crm.stage.rotting_threshold_days`` together with ``is_rotting`` on the
mail tracking duration mixin. That mechanism measures the time since the
last **stage change**, per stage, with a granularity of one day, and it
only colours the record in the interface.

This module answers a different question: it measures the time since the
last **follow-up** of any kind, with a granularity of one minute, per
rule rather than per stage, and it notifies somebody about it.

**Table of contents**

.. contents::
:local:

Configuration
=============

Go to **Settings > CRM > Lead Escalation > Manage Escalation Rules**, or
to **CRM > Configuration > Lead Escalation Rules**, and create a rule:

1. Choose the trigger: *No follow-up* or *Not won nor lost*.
2. Set the delay, in minutes, hours or days.
3. Choose who to notify: the *Sales Team Leader* of the lead, or a
specific user. Other modules may add more recipients to that list.
4. Tick **Assign Lead to Recipient** to also make that person the
salesperson of the lead.
5. Optionally restrict the rule to some sales teams, to leads or to
opportunities only.

Rules are checked by the scheduled action **CRM: Escalate Leads**, which
runs every 5 minutes. That interval is the precision of the shortest
delays: raise it if you only use delays counted in days.

The system parameter ``crm_lead_escalation.batch_size``, 1000 by
default, sets how many leads each rule escalates per run.

Usage
=====

Escalated leads are listed with the **Escalated** filter of the pipeline
search view. On the lead itself, the *Last Follow-up* field shows when
somebody last worked on it.

Once the recipient works on the lead, the escalation is cleared and the
rule can fire again if the lead stalls a second time.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/crm/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 <https://github.com/OCA/crm/issues/new?body=module:%20crm_lead_escalation%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* ForgeFlow

Contributors
------------

- Jasmin Solanki <jasmin.solanki@forgeflow.com>

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.

.. |maintainer-JasminSForgeFlow| image:: https://github.com/JasminSForgeFlow.png?size=40px
:target: https://github.com/JasminSForgeFlow
:alt: JasminSForgeFlow

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-JasminSForgeFlow|

This module is part of the `OCA/crm <https://github.com/OCA/crm/tree/19.0/crm_lead_escalation>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions crm_lead_escalation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
26 changes: 26 additions & 0 deletions crm_lead_escalation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "CRM Lead Escalation",
"summary": "Notify a manager when a lead is not followed up or not closed in time",
"version": "19.0.1.0.0",
"category": "Sales/CRM",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/crm",
"license": "AGPL-3",
"depends": ["crm"],
"data": [
"security/ir.model.access.csv",
"security/crm_lead_escalation_rule_rules.xml",
"views/crm_lead_escalation_rule_views.xml",
"views/crm_lead_views.xml",
"views/res_config_settings_views.xml",
"data/mail_template_data.xml",
"data/ir_cron_data.xml",
"data/ir_config_parameter_data.xml",
],
"development_status": "Beta",
"maintainers": ["JasminSForgeFlow"],
"installable": True,
}
9 changes: 9 additions & 0 deletions crm_lead_escalation/data/ir_config_parameter_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ForgeFlow S.L.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="config_parameter_batch_size" model="ir.config_parameter">
<field name="key">crm_lead_escalation.batch_size</field>
<field name="value">1000</field>
</record>
</odoo>
14 changes: 14 additions & 0 deletions crm_lead_escalation/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ForgeFlow S.L.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record forcecreate="True" id="ir_cron_crm_lead_escalation" model="ir.cron">
<field name="name">CRM: Escalate Leads</field>
<field name="model_id" ref="model_crm_lead_escalation_rule" />
<field name="state">code</field>
<field name="code">model._cron_escalate_leads()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
</record>
</odoo>
43 changes: 43 additions & 0 deletions crm_lead_escalation/data/mail_template_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ForgeFlow S.L.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="mail_template_crm_lead_escalation" model="mail.template">
<field name="name">CRM: Lead Escalation</field>
<field name="model_id" ref="crm.model_crm_lead" />
<field name="use_default_to" eval="False" />
<field name="subject">Lead needs attention: {{ object.name }}</field>
<field
name="description"
>Sent to the escalation recipient when a lead is not followed up in time.</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p>Hello,</p>
<p>
The lead
<t t-out="object.name or ''" />
has not moved forward and needs your attention.
</p>
<ul>
<li t-if="object.partner_id">Customer: <t
t-out="object.partner_id.display_name"
/></li>
<li t-if="object.user_id">Salesperson: <t
t-out="object.user_id.display_name"
/></li>
<li t-if="object.team_id">Sales Team: <t
t-out="object.team_id.display_name"
/></li>
<li t-if="object.stage_id">Stage: <t
t-out="object.stage_id.display_name"
/></li>
<li t-if="object.last_followup_date">Last follow-up: <t
t-out="format_datetime(object.last_followup_date, tz=ctx.get('tz'), dt_format='short')"
/></li>
</ul>
<p>A to-do activity has been scheduled on the lead for you.</p>
</div>
</field>
<field name="auto_delete" eval="True" />
</record>
</odoo>
3 changes: 3 additions & 0 deletions crm_lead_escalation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import crm_lead_escalation_rule
from . import crm_lead
from . import mail_activity
61 changes: 61 additions & 0 deletions crm_lead_escalation/models/crm_lead.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import Command, fields, models

FOLLOWUP_RESET_FIELDS = {"stage_id", "user_id"}


class CrmLead(models.Model):
_inherit = "crm.lead"

last_followup_date = fields.Datetime(
string="Last Follow-up",
default=lambda self: self.env.cr.now(),
readonly=True,
copy=False,
index=True,
help="Last time somebody worked on this lead: a message, a planned or "
"completed activity, a stage change or a new salesperson.",
)
escalation_rule_ids = fields.Many2many(
"crm.lead.escalation.rule",
string="Applied Escalation Rules",
readonly=True,
copy=False,
)

def write(self, vals):
changed_fields = FOLLOWUP_RESET_FIELDS & vals.keys()
# Rewriting a field with the value it already holds is not a follow-up.
to_reset = self.filtered(
lambda lead: any(
lead[name].id != (vals[name] or False) for name in changed_fields
)
)
res = super().write(vals)
if to_reset:
to_reset._reset_followup()
return res

def message_post(self, **kwargs):
message = super().message_post(**kwargs)
if kwargs.get("message_type", "notification") in ("comment", "email"):
self._reset_followup()
return message

def _reset_followup(self):
"""Restart the escalation clock and allow the rules to fire again."""
if self.env.context.get("crm_lead_escalation"):
return
leads = self.filtered(lambda lead: lead.won_status == "pending")
if not leads:
return
# Technical fields: whoever may post on the lead may reset its clock,
# even without write access on the lead itself.
leads.sudo().write(
{
"last_followup_date": self.env.cr.now(),
"escalation_rule_ids": [Command.clear()],
}
)
Loading
Loading