From 788ba5cdaabe9afd6ca6458a8338e7c2c41bf5f3 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Thu, 6 Nov 2025 10:58:04 +0100 Subject: [PATCH 1/2] [IMP] OD-942, mail_activity_team: assign plan activities to teams --- mail_activity_team/README.rst | 43 +++++----- mail_activity_team/__manifest__.py | 4 +- mail_activity_team/models/__init__.py | 5 +- .../models/mail_activity_mixin.py | 7 ++ .../models/mail_activity_plan_template.py | 66 +++++++++++++++ mail_activity_team/readme/USAGE.md | 3 + .../static/description/index.html | 30 +++---- .../tests/test_mail_activity_team.py | 57 +++++++++++++ .../mail_activity_plan_template_views.xml | 18 +++++ .../views/mail_activity_plan_views.xml | 15 ++++ .../wizard/mail_activity_schedule.py | 80 ++++++++++++++++--- 11 files changed, 277 insertions(+), 51 deletions(-) create mode 100644 mail_activity_team/models/mail_activity_plan_template.py create mode 100644 mail_activity_team/views/mail_activity_plan_template_views.xml create mode 100644 mail_activity_team/views/mail_activity_plan_views.xml diff --git a/mail_activity_team/README.rst b/mail_activity_team/README.rst index 752d566a1..234e5710a 100644 --- a/mail_activity_team/README.rst +++ b/mail_activity_team/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ================== Mail Activity Team ================== @@ -17,7 +13,7 @@ Mail Activity Team .. |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 +.. |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%2Fmail-lightgray.png?logo=github @@ -56,6 +52,9 @@ Teams. When you create a new activity the application will propose the user's assigned team. +When creating activity plans, instead of assigning an activity to a +user, there is also the option to assign it to a team instead. + You can report on the activities assigned to a team going to *Dashboards / Activities*, and then filter by a specific team or group by teams. @@ -81,35 +80,35 @@ Authors Contributors ------------ -- `ForgeFlow `__: +- `ForgeFlow `__: - - Jordi Ballester Alomar (jordi.ballester@forgeflow.com) - - Miquel Raïch (miquel.raich@forgeflow.com) - - Bernat Puig Font (bernat.puig@forgeflow.com) + - Jordi Ballester Alomar (jordi.ballester@forgeflow.com) + - Miquel Raïch (miquel.raich@forgeflow.com) + - Bernat Puig Font (bernat.puig@forgeflow.com) -- Pedro Gonzalez (pedro.gonzalez@pesol.es) -- `Tecnativa `__: +- Pedro Gonzalez (pedro.gonzalez@pesol.es) +- `Tecnativa `__: - - David Vidal + - David Vidal -- `Dynapps `__: +- `Dynapps `__: - - Raf Ven + - Raf Ven -- [Trobz] (https://trobz.com): +- [Trobz] (https://trobz.com): - - Son Ho sonhd@trobz.com + - Son Ho sonhd@trobz.com -- [Camptocamp] (https://camptocamp.com): +- [Camptocamp] (https://camptocamp.com): - - Vincent Van Rossem vincent.vanrossem@camptocamp.com - - Italo Lopes italo.lopes@camptocamp.com + - Vincent Van Rossem vincent.vanrossem@camptocamp.com + - Italo Lopes italo.lopes@camptocamp.com -- `CorporateHub `__ +- `CorporateHub `__ - - Alexey Pelykh alexey.pelykh@corphub.eu + - Alexey Pelykh alexey.pelykh@corphub.eu -- Stefan Rijnhart (stefan@opener.amsterdam) +- Stefan Rijnhart (stefan@opener.amsterdam) Other credits ------------- diff --git a/mail_activity_team/__manifest__.py b/mail_activity_team/__manifest__.py index fa7d444fd..dca3987f6 100644 --- a/mail_activity_team/__manifest__.py +++ b/mail_activity_team/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Mail Activity Team", "summary": "Add Teams to Activities", - "version": "18.0.1.0.1", + "version": "18.0.1.0.2", "development_status": "Beta", "category": "Social Network", "website": "https://github.com/OCA/mail", @@ -18,6 +18,8 @@ "security/mail_activity_team_security.xml", "wizard/mail_activity_schedule.xml", "views/ir_actions_server_views.xml", + "views/mail_activity_plan_template_views.xml", + "views/mail_activity_plan_views.xml", "views/mail_activity_type.xml", "views/mail_activity_team_views.xml", "views/mail_activity_views.xml", diff --git a/mail_activity_team/models/__init__.py b/mail_activity_team/models/__init__.py index 32109baf1..ccd0cec35 100644 --- a/mail_activity_team/models/__init__.py +++ b/mail_activity_team/models/__init__.py @@ -1,6 +1,7 @@ from . import ir_actions_server -from . import mail_activity_team +from . import mail_activity_team # Has to load early from . import mail_activity from . import mail_activity_mixin -from . import res_users +from . import mail_activity_plan_template from . import mail_activity_type +from . import res_users diff --git a/mail_activity_team/models/mail_activity_mixin.py b/mail_activity_team/models/mail_activity_mixin.py index d24c6af9c..0cc1ccb65 100644 --- a/mail_activity_team/models/mail_activity_mixin.py +++ b/mail_activity_team/models/mail_activity_mixin.py @@ -44,6 +44,13 @@ def activity_schedule( user-team missmatch. We can hook onto `act_values` dict as it's passed to the create activity method. """ + # Pick up some defaults from mail.activity.schedule + for field in ("team_id", "team_user_id", "user_id"): + if self.env.context.get(f"schedule_default_{field}") and not act_values.get( + field + ): + act_values[field] = self.env.context[f"schedule_default_{field}"] + if self.env.context.get("force_activity_team"): act_values["team_id"] = self.env.context["force_activity_team"].id if "team_id" not in act_values: diff --git a/mail_activity_team/models/mail_activity_plan_template.py b/mail_activity_team/models/mail_activity_plan_template.py new file mode 100644 index 000000000..86089c5be --- /dev/null +++ b/mail_activity_team/models/mail_activity_plan_template.py @@ -0,0 +1,66 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class MailActivityPlanTemplate(models.Model): + _inherit = "mail.activity.plan.template" + + activity_team_id = fields.Many2one( + comodel_name="mail.activity.team", + compute="_compute_activity_team_id", + ondelete="restrict", + readonly=False, + store=True, + ) + activity_team_required = fields.Boolean( + compute="_compute_activity_team_required", + help="Indicate if this plan template must have an activity team", + ) + # Add compute method to existing field + responsible_id = fields.Many2one( + compute="_compute_responsible_id", + readonly=False, + store=True, + ) + responsible_type = fields.Selection( + ondelete={"team": "set default"}, + selection_add=[("team", "Team")], + ) + + @api.depends("responsible_type") + def _compute_activity_team_required(self): + """Hook to override requiredness of activity team""" + for template in self: + template.activity_team_required = template.responsible_type == "team" + + @api.depends("activity_type_id", "responsible_type") + def _compute_activity_team_id(self): + """Assign the default team from the activity type""" + for template in self: + if template.activity_team_required: + if template.activity_type_id.default_team_id: + template.activity_team_id = ( + template.activity_type_id.default_team_id + ) + elif template.activity_team_id: + template.activity_team_id = False + + @api.depends("responsible_type") + def _compute_responsible_id(self): + """Wipe responsible if field is not visible (c.q. allowed)""" + for template in self: + if template.activity_team_required and template.responsible_id: + template.responsible_id = False + + @api.constrains("responsible_type", "activity_team_id") + def _check_activity_team(self): + for template in self: + if template.activity_team_required and not template.activity_team_id: + raise ValidationError(self.env._("Please enter an activity team.")) + + def _determine_responsible(self, on_demand_responsible, applied_on_record): + # Avoid signalling an error for a 'team' template without a user. + self.ensure_one() + if self.activity_team_required: + return {"error": False} + return super()._determine_responsible(on_demand_responsible, applied_on_record) diff --git a/mail_activity_team/readme/USAGE.md b/mail_activity_team/readme/USAGE.md index 3da67bd8e..e0729e6af 100644 --- a/mail_activity_team/readme/USAGE.md +++ b/mail_activity_team/readme/USAGE.md @@ -12,5 +12,8 @@ Teams. When you create a new activity the application will propose the user's assigned team. +When creating activity plans, instead of assigning an activity to a user, there +is also the option to assign it to a team instead. + You can report on the activities assigned to a team going to *Dashboards / Activities*, and then filter by a specific team or group by teams. diff --git a/mail_activity_team/static/description/index.html b/mail_activity_team/static/description/index.html index 07fbbce1d..6d8bc9d4a 100644 --- a/mail_activity_team/static/description/index.html +++ b/mail_activity_team/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Mail Activity Team -
+
+

Mail Activity Team

- - -Odoo Community Association - -
-

Mail Activity Team

-

Beta License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

This module adds the possibility to assign teams to activities.

Table of contents

@@ -391,7 +386,7 @@

Mail Activity Team

-

Usage

+

Usage

To set up new teams:

  1. Go to Settings / Activate developer mode
  2. @@ -404,11 +399,13 @@

    Usage

    Teams.

    When you create a new activity the application will propose the user’s assigned team.

    +

    When creating activity plans, instead of assigning an activity to a +user, there is also the option to assign it to a team instead.

    You can report on the activities assigned to a team going to Dashboards / Activities, and then filter by a specific team or group by teams.

-

Bug Tracker

+

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 @@ -416,16 +413,16 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ForgeFlow
  • Sodexis
-

Contributors

+

Contributors

-

Other credits

+

Other credits

The migration of this module from 16.0 to 17.0 was financially supported by Camptocamp

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -477,6 +474,5 @@

Maintainers

-
diff --git a/mail_activity_team/tests/test_mail_activity_team.py b/mail_activity_team/tests/test_mail_activity_team.py index 4a20b4039..3054ddeb7 100644 --- a/mail_activity_team/tests/test_mail_activity_team.py +++ b/mail_activity_team/tests/test_mail_activity_team.py @@ -447,3 +447,60 @@ def test_migration(self): mod.migrate(self.env.cr, "18.0.1.0.0") self.assertFalse(rule.perm_create) + + def test_mail_activity_plan(self): + """Activities for teams can be scheduled using an activity plan""" + plan = self.env["mail.activity.plan"].create( + { + "name": __name__, + "res_model": "res.partner", + } + ) + self.env["mail.activity.plan.template"].create( + { + "summary": __name__, + "responsible_type": "other", + "responsible_id": self.employee3.id, + "activity_type_id": self.activity1.id, + "plan_id": plan.id, + "sequence": 1, + "delay_count": 1, + } + ) + self.env["mail.activity.plan.template"].create( + { + "summary": __name__, + "responsible_type": "team", + "activity_team_id": self.team1.id, + "activity_type_id": self.activity2.id, + "plan_id": plan.id, + "sequence": 2, + "delay_count": 2, + } + ) + activities = self.partner_client.activity_ids + self.env["mail.activity.schedule"].with_context( + active_ids=self.partner_client.ids, + active_model=self.partner_client._name, + ).create( + { + "plan_id": plan.id, + "plan_date": date.today(), + } + ).action_schedule_plan() + new_activities = self.partner_client.activity_ids - activities + self.assertRecordValues( + new_activities, + [ + { + "activity_type_id": self.activity2.id, + "team_id": self.team1.id, + "user_id": False, + }, + { + "activity_type_id": self.activity1.id, + "team_id": False, + "user_id": self.employee3.id, + }, + ], + ) diff --git a/mail_activity_team/views/mail_activity_plan_template_views.xml b/mail_activity_team/views/mail_activity_plan_template_views.xml new file mode 100644 index 000000000..9cfab1338 --- /dev/null +++ b/mail_activity_team/views/mail_activity_plan_template_views.xml @@ -0,0 +1,18 @@ + + + + + mail.activity.plan.template + + + + + + + diff --git a/mail_activity_team/views/mail_activity_plan_views.xml b/mail_activity_team/views/mail_activity_plan_views.xml new file mode 100644 index 000000000..e6c95b13b --- /dev/null +++ b/mail_activity_team/views/mail_activity_plan_views.xml @@ -0,0 +1,15 @@ + + + + + mail.activity.plan + + + + + + + diff --git a/mail_activity_team/wizard/mail_activity_schedule.py b/mail_activity_team/wizard/mail_activity_schedule.py index c8a18eed1..446bee141 100644 --- a/mail_activity_team/wizard/mail_activity_schedule.py +++ b/mail_activity_team/wizard/mail_activity_schedule.py @@ -1,8 +1,13 @@ # Copyright 2024 Camptocamp SA # Copyright 2024 CorporateHub # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import inspect +import logging from odoo import api, fields, models +from odoo.tools.misc import format_date + +_logger = logging.getLogger(__name__) class MailActivitySchedule(models.TransientModel): @@ -60,13 +65,70 @@ def _onchange_activity_team_user_id(self): ) def _action_schedule_activities(self): - return self._get_applied_on_records().activity_schedule( - activity_type_id=self.activity_type_id.id, - automated=False, - summary=self.summary, - note=self.note, - user_id=self.activity_team_user_id.id, - team_user_id=self.activity_team_user_id.id, - team_id=self.activity_team_id.id, - date_deadline=self.date_deadline, + # Insert default team data which is picked up for activities that are + # created without a team already. + self = self.with_context( + schedule_default_team_id=self.activity_team_id.id, + schedule_default_team_user_id=self.activity_team_user_id.id, + schedule_default_user_id=self.activity_team_user_id.id, + ) + return super()._action_schedule_activities() + + def action_schedule_plan(self): + # Triggering scheduled team activities in + # _plan_filter_activity_templates_to_schedule which is called from the + # super method to fetch the activities that need to be scheduled. + # This is because activity parameters are determined inline in the + # super method, and the activity team cannot be inserted there in a + # clean override. + self = self.with_context(fire_team_activities=True) + return super().action_schedule_plan() + + @staticmethod + def _get_activity_schedule_plan_data(): + """Fetch some variables defined in action_schedule_plan""" + frame = inspect.currentframe() + while frame.f_back: + frame = frame.f_back + f_locals = frame.f_locals + if "activity_descriptions" in f_locals and "record" in f_locals: + return f_locals["record"], f_locals["activity_descriptions"] + _logger.warning( + "Could not find 'activity_descriptions' list in inspected frames" ) + return None, None + + def _plan_filter_activity_templates_to_schedule(self): + # Instead of returning all templates, including those with a team, + # go ahead and schedule only those with a team and only return + # the remaining activity templates. + res = super()._plan_filter_activity_templates_to_schedule() + if self.env.context.get("fire_team_activities"): + # Immediately schedule team activities + record, activity_descriptions = self._get_activity_schedule_plan_data() + if record is None: + return res + templates = res.filtered("activity_team_required") + others = res - templates + for template in templates: + date_deadline = template._get_date_deadline(self.plan_date) + record.activity_schedule( + activity_type_id=template.activity_type_id.id, + automated=False, + summary=template.summary, + note=template.note, + user_id=template.activity_team_id.user_id.id, + team_id=template.activity_team_id.id, + date_deadline=date_deadline, + ) + activity_descriptions.append( + self.env._( + "%(activity)s, assigned to team %(name)s, " + "due on the %(deadline)s", + activity=template.summary or template.activity_type_id.name, + name=template.activity_team_id.name, + deadline=format_date(self.env, date_deadline), + ) + ) + return others + return res From 5ab0483fd2c7ee26e026a9e58f75d2e5f6838703 Mon Sep 17 00:00:00 2001 From: brunodecoen-quatra Date: Thu, 11 Dec 2025 14:40:41 +0100 Subject: [PATCH 2/2] [IMP] OD-1159, oca-mail: Activity counter voor Team activities --- mail_activity_team/models/mail_activity.py | 16 +++++++++ .../activity_menu_view/activity_menu_view.xml | 12 ++++++- .../static/src/models/activity_menu.esm.js | 33 +++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/mail_activity_team/models/mail_activity.py b/mail_activity_team/models/mail_activity.py index f70019e01..67f124cba 100644 --- a/mail_activity_team/models/mail_activity.py +++ b/mail_activity_team/models/mail_activity.py @@ -105,3 +105,19 @@ def _onchange_activity_type_id(self): if self.user_id not in members and members: self.user_id = members[:1] return res + + @api.model + def get_team_activity_count(self): + """Get count of activities assigned to user's teams.""" + user_team_ids = self.env.user.activity_team_ids.ids + if not user_team_ids: + return {"team_count": 0} + + team_activities = self.search_count( + [ + ("team_id", "in", user_team_ids), + ("date_deadline", ">=", fields.Date.today()), + ] + ) + + return {"team_count": team_activities} diff --git a/mail_activity_team/static/src/components/activity_menu_view/activity_menu_view.xml b/mail_activity_team/static/src/components/activity_menu_view/activity_menu_view.xml index a8a938d07..d3b99e98b 100644 --- a/mail_activity_team/static/src/components/activity_menu_view/activity_menu_view.xml +++ b/mail_activity_team/static/src/components/activity_menu_view/activity_menu_view.xml @@ -1,7 +1,7 @@ @@ -32,5 +32,15 @@
+ + + + + / + + diff --git a/mail_activity_team/static/src/models/activity_menu.esm.js b/mail_activity_team/static/src/models/activity_menu.esm.js index 142417287..e2a480793 100644 --- a/mail_activity_team/static/src/models/activity_menu.esm.js +++ b/mail_activity_team/static/src/models/activity_menu.esm.js @@ -1,22 +1,33 @@ +import {onWillStart, useRef} from "@odoo/owl"; +import {user} from "@web/core/user"; import {ActivityMenu} from "@mail/core/web/activity_menu"; import {patch} from "@web/core/utils/patch"; -import {useRef} from "@odoo/owl"; -import {user} from "@web/core/user"; patch(ActivityMenu.prototype, { setup() { super.setup(); this.currentFilter = "my"; this.rootRef = useRef("mail_activity_team_dropdown"); + this.teamActivityCount = 0; + + onWillStart(async () => { + await this._loadActivities(); + }); }, + + get teamCounter() { + return this.teamActivityCount; + }, + activateFilter(filter_el) { this.deactivateButtons(); - filter_el.classList.add("active"); this.currentFilter = filter_el.dataset.filter; this.updateTeamActivitiesContext(); this.store.fetchData({systray_get_activities: true}); + this._loadActivities(); }, + updateTeamActivitiesContext() { var active = false; if (this.currentFilter === "team") { @@ -24,6 +35,7 @@ patch(ActivityMenu.prototype, { } user.updateContext({team_activities: active}); }, + onBeforeOpen() { user.updateContext({team_activities: false}); super.onBeforeOpen(); @@ -32,7 +44,22 @@ patch(ActivityMenu.prototype, { deactivateButtons() { this.rootRef.el.querySelector(".o_filter_nav_item").classList.remove("active"); }, + onClickActivityFilter(filter) { this.activateFilter(this.rootRef.el.querySelector("." + filter)); }, + + async _loadActivities() { + try { + // Load team activity count + const resultTeam = await this.orm.call( + "mail.activity", + "get_team_activity_count", + [] + ); + this.teamActivityCount = resultTeam.team_count || 0; + } catch (error) { + console.error("Error loading activities:", error); + } + }, });