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
175 changes: 175 additions & 0 deletions mail_reply_stage/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

================
Mail Reply Stage
================

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

.. |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%2Fsocial-lightgray.png?logo=github
:target: https://github.com/OCA/social/tree/19.0/mail_reply_stage
:alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/social-19-0/social-19-0-mail_reply_stage
: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/social&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module provides a feature that automatically updates the stage of a
record when a non-internal user sends a mail message to that record.

**Table of contents**

.. contents::
:local:

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

Go to **Settings > Technical > Email > Mail Reply Configurations** and
create records according to your needs.

For each record:

- **Model**: Choose a model (required).
- **Parent Field**: Select the many2one field that links to the parent
model. For example, if you select the model project.task, choose
project_id as the parent field.
- **Parent Stage Field**: Choose the many2many field from the model of
Parent Field that defines the allowed stages.The system will check
whether the selected Reply Stage is included in the value of this
field. If the Reply Stage is not present in the Parent Stage Field,
it will not be assigned to the record.
- **domain**: Set a domain to filter which records this config applies
to. Example: ``[('project_id.name', '=', 'My Project')]``
- **Reply Stage Field**: Choose the field (e.g., stage_id) to be
updated when a non-internal user replies. (required)
- **Reply Stage**: Set the name of the stage to apply on reply.
(required)

Examples
--------

Example 1 – For "Office Design" Project
---------------------------------------

This rule applies to tasks under the **"Office Design"** project.

================== ===============================================
**Field** **Value**
================== ===============================================
Model Task (``project.task``)
Parent Field Project (``project_id``)
Parent Stage Field Task Stages (``project.task.type_ids``)
Domain ``[('project_id.name', '=', 'Office Design')]``
Reply Stage Field Stage (``stage_id``)
Reply Stage Reply to Customer
================== ===============================================

Example 2 – Fallback for All Other Projects
-------------------------------------------

This rule applies to all tasks that do **not** belong to the "Office
Design" project.

================== =======================================
**Field** **Value**
================== =======================================
Model Task (``project.task``)
Parent Field Project (``project_id``)
Parent Stage Field Task Stages (``project.task.type_ids``)
Domain
Reply Stage Field Stage (``stage_id``)
Reply Stage Need Discussion
================== =======================================

Use the up/down arrows to prioritize the rules. The system evaluates
rules from top to bottom and applies only the first matching one. Place
more specific rules (with a domain) above general ones (e.g., fallback
rules with an empty domain).

Based on the two example configurations: For a task under the "Office
Design" project, both rules match. However, the first rule at the top
will be used.

Note: Make sure the selected reply stage exists in the parent record’s
allowed stages, as defined by the **Parent Stage Field**.

Known issues / Roadmap
======================

Due to a technical limitation, if you create a new stage after the reply
stage configuration record has already been created and want to use this
new stage as the Reply Stage, you must clear and reselect the Reply
Stage Field to trigger the onchange. This will allow the newly created
stage to appear in the Reply Stage selection.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/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/social/issues/new?body=module:%20mail_reply_stage%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
-------

* Quartile

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

- `Quartile <https://www.quartile.co>`__

- Aung Ko Ko Lin

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-yostashiro| image:: https://github.com/yostashiro.png?size=40px
:target: https://github.com/yostashiro
:alt: yostashiro
.. |maintainer-aungkokolin1997| image:: https://github.com/aungkokolin1997.png?size=40px
:target: https://github.com/aungkokolin1997
:alt: aungkokolin1997

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

|maintainer-yostashiro| |maintainer-aungkokolin1997|

This module is part of the `OCA/social <https://github.com/OCA/social/tree/19.0/mail_reply_stage>`_ 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 mail_reply_stage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions mail_reply_stage/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Mail Reply Stage",
"category": "Mail",
"version": "19.0.1.0.0",
"author": "Quartile, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
"license": "AGPL-3",
"depends": ["mail"],
"data": [
"security/ir.model.access.csv",
"views/mail_reply_config_views.xml",
],
"maintainers": ["yostashiro", "aungkokolin1997"],
"installable": True,
}
3 changes: 3 additions & 0 deletions mail_reply_stage/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import ir_model_data
from . import mail_message
from . import mail_reply_config
33 changes: 33 additions & 0 deletions mail_reply_stage/models/ir_model_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import api, models
from odoo.fields import Domain


class IrModelData(models.Model):
_inherit = "ir.model.data"

@api.model
@api.readonly
def name_search(
self,
name: str = "",
domain=None,
operator: str = "ilike",
limit: int = 100,
):
stage_model = self.env.context.get("mail_reply_stage_model")
if name and stage_model:
stage_ids = self.env[stage_model]._search(
[("name", operator, name)], limit=limit
)
domain = [
("model", "=", stage_model),
("res_id", "in", stage_ids),
]
records = self.search_fetch(Domain(domain), ["display_name"], limit=limit)
return [(rec.id, rec.display_name) for rec in records]
return super().name_search(
name=name, domain=domain, operator=operator, limit=limit
)
72 changes: 72 additions & 0 deletions mail_reply_stage/models/mail_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging

from odoo import api, models
from odoo.tools import safe_eval

_logger = logging.getLogger(__name__)


class MailMessage(models.Model):
_inherit = "mail.message"

def _get_reply_stage(self, res, config):
self.ensure_one()
reply_stage = self.env[config.reply_stage_model_name].search(
[("id", "=", config.reply_stage_id)]
)
if config.parent_stage_field_id:
parent_field_rec = getattr(res, config.parent_field_id.name, None)
allowed_stages = getattr(
parent_field_rec,
config.parent_stage_field_id.name,
self.env[config.parent_stage_field_id.relation],
)
reply_stage = reply_stage.filtered(lambda stage: stage in allowed_stages)
return reply_stage

def _get_mail_reply_config(self, res, res_model):
self.ensure_one()
configs = self.env["mail.reply.config"].search(
[("model_id", "=", res_model.id)], order="sequence ASC"
)
for config in configs:
reply_stage = self._get_reply_stage(res, config)
if not reply_stage:
continue
domain = []
if config.domain:
try:
domain = safe_eval.safe_eval(config.domain)
except Exception as e:
_logger.warning("Invalid domain: %s (%s)", config.domain, e)
continue
if not domain or res.filtered_domain(domain):
return config, reply_stage
return None, None

@api.model_create_multi
def create(self, values_list):
messages = super().create(values_list)
for message in messages:
user = message.author_id.user_ids[:1]
if user and user.has_group("base.group_user"):
continue
if message.subtype_id and message.subtype_id.internal:
continue
res_model = (
self.env["ir.model"]
.sudo()
.search([("model", "=", message.model)], limit=1)
)
if not res_model:
continue
res = self.env[message.model].browse(message.res_id)
config, reply_stage = message._get_mail_reply_config(res, res_model)
if not config:
continue
if reply_stage:
res.sudo().write({config.reply_stage_field_id.name: reply_stage.id})
return messages
Loading