diff --git a/fetchmail_mail_activity_team_activity/README.rst b/fetchmail_mail_activity_team_activity/README.rst new file mode 100644 index 000000000..fb45115d2 --- /dev/null +++ b/fetchmail_mail_activity_team_activity/README.rst @@ -0,0 +1,10 @@ +========================================== +Mail Activity Fetchmail with Team Activity +========================================== + +Usage +===== + +Uses mail.activity.team to configure automatic activities when mails arrive for the specified models. + +The configuration to add RMA and PO models (Settings --> Technical --> Activity Teams menu). diff --git a/fetchmail_mail_activity_team_activity/__init__.py b/fetchmail_mail_activity_team_activity/__init__.py new file mode 100644 index 000000000..69f7babdf --- /dev/null +++ b/fetchmail_mail_activity_team_activity/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/fetchmail_mail_activity_team_activity/__manifest__.py b/fetchmail_mail_activity_team_activity/__manifest__.py new file mode 100644 index 000000000..5869a917a --- /dev/null +++ b/fetchmail_mail_activity_team_activity/__manifest__.py @@ -0,0 +1,18 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Mail Activity on Fetchmail with Team Activity", + "version": "18.0.1.0.0", + "category": "Social Network", + "author": "Nitrokey GmbH, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/mail", + "license": "AGPL-3", + "summary": """ + * Uses mail.activity.team to configure automatic activities when mails + arrive for the specified models. + * The configuration to add RMA and PO models + (Settings --> Technical --> Activity Teams menu) + """, + "depends": ["mail_activity_team", "calendar"], + "installable": True, +} diff --git a/fetchmail_mail_activity_team_activity/i18n/fetchmail_mail_activity_team_activity.pot b/fetchmail_mail_activity_team_activity/i18n/fetchmail_mail_activity_team_activity.pot new file mode 100644 index 000000000..837e187e5 --- /dev/null +++ b/fetchmail_mail_activity_team_activity/i18n/fetchmail_mail_activity_team_activity.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fetchmail_mail_activity_team_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: fetchmail_mail_activity_team_activity +#: model:ir.model,name:fetchmail_mail_activity_team_activity.model_mail_thread +msgid "Email Thread" +msgstr "" diff --git a/fetchmail_mail_activity_team_activity/models/__init__.py b/fetchmail_mail_activity_team_activity/models/__init__.py new file mode 100644 index 000000000..f2f13374b --- /dev/null +++ b/fetchmail_mail_activity_team_activity/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import mail_thread diff --git a/fetchmail_mail_activity_team_activity/models/mail_thread.py b/fetchmail_mail_activity_team_activity/models/mail_thread.py new file mode 100644 index 000000000..edded8690 --- /dev/null +++ b/fetchmail_mail_activity_team_activity/models/mail_thread.py @@ -0,0 +1,58 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + @api.model + def message_route( + self, message, message_dict, model=None, thread_id=None, custom_values=None + ): + # Called by fetchmail module on mail.threads with received emails + routes = super().message_route( + message, + message_dict, + model=model, + thread_id=thread_id, + custom_values=custom_values, + ) + + for route in routes: + # routes: list of routes [(model, thread_id, custom_values, user_id, alias)] + model, thread_id = route[0], route[1] + mdl = self.env[model] + # Model needs mail.activity.mixin + if not all(hasattr(mdl, x) for x in ("activity_ids", "activity_schedule")): + continue + + # Skip if there is no team + domain = [("res_model_ids.model", "=", model)] + team = self.env["mail.activity.team"].search(domain, limit=1) + user = team.user_id or team.member_ids + if not thread_id or not team or not user: + continue + + thread = mdl.browse(thread_id) + + # Only schedule if there is no email open email activity + activity_type = self.env.ref("mail.mail_activity_data_email") + domain = [ + ("res_model", "=", thread._name), + ("res_id", "=", thread.id), + ("automated", "=", True), + ("activity_type_id", "=", activity_type.id), + ] + if thread.activity_ids.search(domain): + continue + + # Schedule the new email activity + thread.activity_schedule( + "mail.mail_activity_data_email", + user_id=user[0].id, + team_id=team.id, + automated=True, + ) + + return routes diff --git a/fetchmail_mail_activity_team_activity/pyproject.toml b/fetchmail_mail_activity_team_activity/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/fetchmail_mail_activity_team_activity/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fetchmail_mail_activity_team_activity/static/description/icon.png b/fetchmail_mail_activity_team_activity/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/fetchmail_mail_activity_team_activity/static/description/icon.png differ diff --git a/fetchmail_mail_activity_team_activity/tests/__init__.py b/fetchmail_mail_activity_team_activity/tests/__init__.py new file mode 100644 index 000000000..68d825162 --- /dev/null +++ b/fetchmail_mail_activity_team_activity/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_activity diff --git a/fetchmail_mail_activity_team_activity/tests/test_activity.py b/fetchmail_mail_activity_team_activity/tests/test_activity.py new file mode 100644 index 000000000..e14809d34 --- /dev/null +++ b/fetchmail_mail_activity_team_activity/tests/test_activity.py @@ -0,0 +1,114 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import uuid + +from odoo.fields import Command +from odoo.tests import TransactionCase + +_logger = logging.getLogger(__name__) + + +class TestActivity(TransactionCase): + def setUp(self): + super().setUp() + # Clean up any existing teams before each test + self.env["mail.activity.team"].search([]).unlink() + + def generate_reply(self, message): + return ( + "From: ext.partner@example.org\r\n" + "To: odoo.test@local\r\n" + f"Message-ID: {uuid.uuid4()}\r\n" + f"In-Reply-To: {message['message_id']}\r\n" + "Subject: Test Email Reply\r\n\r\n" + ) + + def test_no_team(self): + """Test that no activity is created when no team exists""" + # Ensure no teams exist + teams = self.env["mail.activity.team"].search([]) + self.assertEqual(len(teams), 0, "Should have no teams before test") + + thread = self.env.user.partner_id + msg = thread.message_notify( + partner_ids=self.env.user.partner_id.ids, + body="Testing", + ) + + # Record initial state + initial_activities = thread.activity_ids + initial_message_count = len(thread.message_ids) + + # Process email reply + reply = self.generate_reply(msg) + thread_id = thread.message_process(thread._name, reply) + + # Assertions + self.assertEqual(thread_id, thread.id) + self.assertTrue(len(thread.message_ids) > initial_message_count) + + # CRITICAL: No new activity should be created + self.assertEqual( + thread.activity_ids.ids, + initial_activities.ids, + "No activity should be created when no team exists", + ) + + def test_with_team(self): + thread = self.env.user.partner_id + model = self.env.ref("base.model_res_partner") + + self.env["mail.activity.team"].create( + { + "name": "Testing Team", + "res_model_ids": [Command.set(model.ids)], + "user_id": self.env.user.id, + } + ) + msg = thread.message_notify( + partner_ids=self.env.user.partner_id.ids, + body="Testing", + ) + + messages = thread.message_ids + activities = thread.activity_ids + reply = self.generate_reply(msg) + + thread_id = thread.message_process(thread._name, reply) + self.assertTrue(len(messages) < len(thread.message_ids)) + self.assertEqual(thread_id, thread.id) + + activity = thread.activity_ids - activities + self.assertTrue(activity) + self.assertEqual( + activity.activity_type_id, + self.env.ref("mail.mail_activity_data_email"), + ) + + # Schedule it only once + activities = thread.activity_ids + reply = self.generate_reply(msg) + thread.message_process(thread._name, reply) + self.assertEqual(activities, thread.activity_ids) + + def test_with_team_no_activity_mixin(self): + thread = self.env["calendar.event"].create({"name": "Testing"}) + model = self.env.ref("calendar.model_calendar_event") + self.env["mail.activity.team"].create( + { + "name": "Testing Team", + "res_model_ids": [Command.set(model.ids)], + "user_id": self.env.user.id, + } + ) + msg = thread.message_notify( + partner_ids=self.env.user.partner_id.ids, + body="Testing", + ) + + activities = thread.activity_ids + reply = self.generate_reply(msg) + thread_id = thread.message_process(thread._name, reply) + self.assertEqual(activities, thread.activity_ids) + self.assertEqual(thread_id, thread.id)