diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 1962f8a..f78e057 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -2,16 +2,17 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { - 'name': 'Main Module', - 'version': '1.0.0', - 'author': 'Numigi', - 'maintainer': 'Numigi', - 'website': 'https://www.numigi.com', - 'license': 'LGPL-3', - 'category': 'Other', - 'summary': 'Install all addons required for testing.', - 'depends': [ - 'fetchmail_outlook_by_company', + "name": "Main Module", + "version": "1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://www.numigi.com", + "license": "LGPL-3", + "category": "Other", + "summary": "Install all addons required for testing.", + "depends": [ + "fetchmail_outlook_by_company", + "mail_activity_not_deleted", ], - 'installable': True, + "installable": True, } diff --git a/Dockerfile b/Dockerfile index aa384b1..b368025 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN mkdir -p "${THIRD_PARTY_ADDONS}" && chown -R odoo "${THIRD_PARTY_ADDONS}" USER odoo COPY fetchmail_outlook_by_company /mnt/extra-addons/fetchmail_outlook_by_company +COPY mail_activity_not_deleted /mnt/extra-addons/mail_activity_not_deleted COPY .docker_files/main /mnt/extra-addons/main COPY .docker_files/odoo.conf /etc/odoo diff --git a/mail_activity_not_deleted/README.rst b/mail_activity_not_deleted/README.rst new file mode 100644 index 0000000..a59e3f5 --- /dev/null +++ b/mail_activity_not_deleted/README.rst @@ -0,0 +1,18 @@ +Mail Activity Not Deleted +========================= +Since version 11.0, Odoo adds the concept of activities. +One issue with this feature is that when an activity is completed, the activity record is deleted from the database. + +This module deactivates terminated activities instead of deleting them from the mail_activity table. + +New State +--------- +The state Done is added to activities. Any completed activity is automatically set to Done. + +New Field +--------- +The field Date Done is added to activities. When completing the activity, this field is filled with the current time. + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/mail_activity_not_deleted/__init__.py b/mail_activity_not_deleted/__init__.py new file mode 100644 index 0000000..fd8d229 --- /dev/null +++ b/mail_activity_not_deleted/__init__.py @@ -0,0 +1,4 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/mail_activity_not_deleted/__manifest__.py b/mail_activity_not_deleted/__manifest__.py new file mode 100644 index 0000000..e2d35c8 --- /dev/null +++ b/mail_activity_not_deleted/__manifest__.py @@ -0,0 +1,14 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + 'name': 'Mail Activity Not Deleted', + 'version': '14.0.1.0.1', + 'author': 'Numigi', + 'maintainer': 'Numigi', + 'license': 'LGPL-3', + 'category': 'Other', + 'summary': 'Deactivate terminated activities instead of deleting.', + 'depends': ['mail'], + 'installable': True, +} diff --git a/mail_activity_not_deleted/i18n/fr.po b/mail_activity_not_deleted/i18n/fr.po new file mode 100644 index 0000000..5b64120 --- /dev/null +++ b/mail_activity_not_deleted/i18n/fr.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_activity_not_deleted +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-05-29 15:13-0400\n" +"PO-Revision-Date: 2018-05-29 19:13+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 1.8.7.1\n" + +#. module: mail_activity_not_deleted +#: model:ir.model.fields,field_description:mail_activity_not_deleted.field_mail_activity_active +msgid "Active" +msgstr "Actif" + +#. module: mail_activity_not_deleted +#: model:ir.model,name:mail_activity_not_deleted.model_mail_activity +msgid "Activity" +msgstr "Activité" + +#. module: mail_activity_not_deleted +#: model:ir.model.fields,field_description:mail_activity_not_deleted.field_mail_activity_date_done +msgid "Date Done" +msgstr "Terminé le" diff --git a/mail_activity_not_deleted/models/__init__.py b/mail_activity_not_deleted/models/__init__.py new file mode 100644 index 0000000..d970ab5 --- /dev/null +++ b/mail_activity_not_deleted/models/__init__.py @@ -0,0 +1,4 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import mail_activity, user, mail_activity_mixin diff --git a/mail_activity_not_deleted/models/mail_activity.py b/mail_activity_not_deleted/models/mail_activity.py new file mode 100644 index 0000000..173cad9 --- /dev/null +++ b/mail_activity_not_deleted/models/mail_activity.py @@ -0,0 +1,39 @@ +# © 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime + +from odoo import api, fields, models + + +class MailActivityInactivatedInsteadOfDeleted(models.Model): + + _inherit = "mail.activity" + + active = fields.Boolean(default=True) + date_done = fields.Datetime() + + state = fields.Selection(selection_add=[("done", "Done")]) + + @api.depends("date_deadline") + def _compute_state(self): + super()._compute_state() + + done_activities = self.filtered(lambda a: a.date_done) + for activity in done_activities: + activity.state = "done" + + def unlink(self): + self.write( + {"active": False, "date_done": datetime.now()} + ) + for activity in self: + activity._update_record_date_deadline() + + return True + + def _update_record_date_deadline(self): + """Update the stored fields that depend on activity_ids on the related record.""" + record = self.env[self.res_model].browse(self.res_id) + record.modified(["activity_ids"]) + record.recompute() diff --git a/mail_activity_not_deleted/models/mail_activity_mixin.py b/mail_activity_not_deleted/models/mail_activity_mixin.py new file mode 100644 index 0000000..5f16303 --- /dev/null +++ b/mail_activity_not_deleted/models/mail_activity_mixin.py @@ -0,0 +1,12 @@ +# © 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class MailActivityMixin(models.AbstractModel): + + _inherit = 'mail.activity.mixin' + + # auto_join prevents the active filter from being automatically applied. + activity_ids = fields.One2many(auto_join=False) diff --git a/mail_activity_not_deleted/models/user.py b/mail_activity_not_deleted/models/user.py new file mode 100644 index 0000000..1d72d7a --- /dev/null +++ b/mail_activity_not_deleted/models/user.py @@ -0,0 +1,83 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from collections import defaultdict + +from odoo import api, fields, models, modules + + +class User(models.Model): + + _inherit = 'res.users' + + @api.model + def systray_get_activities(self): + """Prevent inactive activities from appearing in the main Odoo navbar. + + This method is a copy from the method defined at: + odoo/addons/mail/models/res_users.py + + Only the active filter was added in the sql query. + """ + query = """SELECT array_agg(res_id) as res_ids, m.id, count(*), + CASE + WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today' + WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue' + WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned' + END AS states + FROM mail_activity AS act + JOIN ir_model AS m ON act.res_model_id = m.id + WHERE user_id = %(user_id)s + AND act.active + GROUP BY m.id, states; + """ + self.env.cr.execute( + query, + { + "today": fields.Date.context_today(self), + "user_id": self.env.uid, + }, + ) + activity_data = self.env.cr.dictfetchall() + records_by_state_by_model = defaultdict( + lambda: {"today": set(), "overdue": set(), "planned": set(), "all": set()} + ) + for data in activity_data: + records_by_state_by_model[data["id"]][data["states"]] = set(data["res_ids"]) + records_by_state_by_model[data["id"]]["all"] = records_by_state_by_model[ + data["id"] + ]["all"] | set(data["res_ids"]) + user_activities = {} + for model_id in records_by_state_by_model: + model_dic = records_by_state_by_model[model_id] + model = ( + self.env["ir.model"] + .browse(model_id) + .with_prefetch(tuple(records_by_state_by_model.keys())) + ) + allowed_records = self.env[model.model].search( + [("id", "in", tuple(model_dic["all"]))] + ) + if not allowed_records: + continue + module = self.env[model.model]._original_module + icon = module and modules.module.get_module_icon(module) + today = len(model_dic["today"] & set(allowed_records.ids)) + overdue = len(model_dic["overdue"] & set(allowed_records.ids)) + user_activities[model.model] = { + "name": model.name, + "model": model.model, + "type": "activity", + "icon": icon, + "total_count": today + overdue, + "today_count": today, + "overdue_count": overdue, + "planned_count": len(model_dic["planned"] & set(allowed_records.ids)), + "actions": [ + { + "icon": "fa-clock-o", + "name": "Summary", + } + ], + } + return list(user_activities.values()) diff --git a/mail_activity_not_deleted/static/description/icon.png b/mail_activity_not_deleted/static/description/icon.png new file mode 100644 index 0000000..92a86b1 Binary files /dev/null and b/mail_activity_not_deleted/static/description/icon.png differ diff --git a/mail_activity_not_deleted/tests/__init__.py b/mail_activity_not_deleted/tests/__init__.py new file mode 100644 index 0000000..98a1f55 --- /dev/null +++ b/mail_activity_not_deleted/tests/__init__.py @@ -0,0 +1,2 @@ + +from . import test_mail_activity diff --git a/mail_activity_not_deleted/tests/test_mail_activity.py b/mail_activity_not_deleted/tests/test_mail_activity.py new file mode 100644 index 0000000..bb858ab --- /dev/null +++ b/mail_activity_not_deleted/tests/test_mail_activity.py @@ -0,0 +1,70 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime +from odoo.tests import common + + +class TestMailActivity(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env['res.partner'].create({'name': 'Test'}) + cls.partner.write({ + 'activity_ids': [(0, 0, { + 'res_id': cls.partner.id, + 'res_model_id': cls.env.ref('base.model_res_partner').id, + 'date_deadline': datetime.now().date(), + 'user_id': cls.env.user.id, + })] + }) + cls.activity = cls.partner.activity_ids + + def test_when_activity_is_completed_then_it_is_inactive_instead_of_deleted(self): + self.assertTrue(self.activity.active) + + self.activity.action_done() + self.assertTrue(self.activity.exists()) + self.assertFalse(self.activity.active) + + def test_when_activity_is_rescheduled_then_it_is_inactive_instead_of_deleted(self): + self.assertTrue(self.activity.active) + + self.activity.action_done_schedule_next() + self.assertTrue(self.activity.exists()) + self.assertFalse(self.activity.active) + + def test_when_record_is_deactivated_then_the_activity_is_inactive_instead_of_deleted(self): + self.assertTrue(self.activity.active) + + self.partner.active = False + self.activity.refresh() + self.assertTrue(self.activity.exists()) + self.assertFalse(self.activity.active) + + def test_the_date_done_is_computed_when_the_activity_is_completed(self): + self.assertFalse(self.activity.date_done) + + time_before = datetime.now() + self.activity.action_done() + time_after = datetime.now() + + self.assertLessEqual(time_before, self.activity.date_done) + self.assertLessEqual(self.activity.date_done, time_after) + + def test_the_state_is_done_after_the_activity_is_completed(self): + self.assertNotEqual(self.activity.state, 'done') + self.activity.action_done() + self.activity.refresh() + self.assertEqual(self.activity.state, 'done') + + def test_when_the_activity_is_archived_then_it_is_not_due_today(self): + self.assertEqual(self.partner.activity_state, 'today') + self.activity.action_done() + self.assertFalse(self.partner.activity_state) + + def test_when_the_activity_is_archived_then_partner_has_no_activity_deadline(self): + self.assertTrue(self.partner.activity_date_deadline) + self.activity.action_done() + self.assertFalse(self.partner.activity_date_deadline)