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
37 changes: 20 additions & 17 deletions mail_activity_team/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,35 +84,38 @@ Authors
Contributors
------------

- `ForgeFlow <https://www.forgeflow.com>`__:
- `ForgeFlow <https://www.forgeflow.com>`__:

- 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 <https://www.tecnativa.com>`__:
- Pedro Gonzalez (pedro.gonzalez@pesol.es)
- `Tecnativa <https://www.tecnativa.com>`__:

- David Vidal
- David Vidal

- `Dynapps <https://www.dynapps.eu>`__:
- `Dynapps <https://www.dynapps.eu>`__:

- 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 <https://corporatehub.eu/>`__
- `CorporateHub <https://corporatehub.eu/>`__

- Alexey Pelykh alexey.pelykh@corphub.eu
- Alexey Pelykh alexey.pelykh@corphub.eu

- Stefan Rijnhart (stefan@opener.amsterdam)
- Stefan Rijnhart (stefan@opener.amsterdam)
- `glueckkanja AG <https://glueckkanja.com/>`__

- Christopher Rogos (crogos@gmail.com)

Other credits
-------------
Expand Down
3 changes: 3 additions & 0 deletions mail_activity_team/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"views/mail_activity_views.xml",
"views/res_users_views.xml",
],
"demo": [
"demo/mail_activity_team.xml",
],
"assets": {
"web.assets_backend": [
"mail_activity_team/static/src/components/*/*",
Expand Down
79 changes: 79 additions & 0 deletions mail_activity_team/demo/mail_activity_team.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="0">
<!-- Mail Activity Teams -->
<record id="mail_activity_team_administrator" model="mail.activity.team">
<field name="name">Administrator Team</field>
<field name="active">True</field>
<field name="user_id" ref="base.user_admin" />
<field name="member_ids" eval="[Command.set([ref('base.user_admin')])]" />
<field
name="res_model_ids"
eval="[Command.set([ref('base.model_res_partner')])]"
/>
</record>

<record id="mail_activity_team_support" model="mail.activity.team">
<field name="name">Support Team</field>
<field name="active">True</field>
<field name="user_id" ref="base.user_demo" />
<field name="member_ids" eval="[Command.set([ref('base.user_demo')])]" />
<field
name="res_model_ids"
eval="[Command.set([ref('base.model_res_partner')])]"
/>
</record>

<!-- Mail Activities for Sales Team -->
<record id="mail_activity_sales_1" model="mail.activity">
<field name="activity_type_id" ref="mail.mail_activity_data_call" />
<field name="team_id" ref="mail_activity_team_administrator" />
<field name="user_id" ref="base.user_admin" />
<field name="res_model_id" ref="base.model_res_partner" />
<field name="res_id" ref="base.res_partner_1" />
<field name="summary">Schedule follow-up call</field>
<field
name="date_deadline"
eval="(datetime.now() + timedelta(days=7)).date()"
/>
</record>

<record id="mail_activity_sales_2" model="mail.activity">
<field name="activity_type_id" ref="mail.mail_activity_data_email" />
<field name="team_id" ref="mail_activity_team_administrator" />
<field name="user_id" ref="base.user_admin" />
<field name="res_model_id" ref="base.model_res_partner" />
<field name="res_id" ref="base.res_partner_2" />
<field name="summary">Send quotation</field>
<field
name="date_deadline"
eval="(datetime.now() + timedelta(days=3)).date()"
/>
</record>

<!-- Mail Activities for Support Team -->
<record id="mail_activity_support_1" model="mail.activity">
<field name="activity_type_id" ref="mail.mail_activity_data_todo" />
<field name="team_id" ref="mail_activity_team_support" />
<field name="user_id" ref="base.user_demo" />
<field name="res_model_id" ref="base.model_res_partner" />
<field name="res_id" ref="base.res_partner_1" />
<field name="summary">Create support ticket</field>
<field
name="date_deadline"
eval="(datetime.now() + timedelta(days=1)).date()"
/>
</record>

<record id="mail_activity_support_2" model="mail.activity">
<field name="activity_type_id" ref="mail.mail_activity_data_call" />
<field name="team_id" ref="mail_activity_team_support" />
<field name="user_id" ref="base.user_demo" />
<field name="res_model_id" ref="base.model_res_partner" />
<field name="res_id" ref="base.res_partner_3" />
<field name="summary">Technical support call</field>
<field
name="date_deadline"
eval="(datetime.now() + timedelta(days=2)).date()"
/>
</record>
</odoo>
147 changes: 143 additions & 4 deletions mail_activity_team/models/mail_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

from odoo import SUPERUSER_ID, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.misc import get_lang

from odoo.addons.mail.tools.discuss import Store


class MailActivity(models.Model):
Expand Down Expand Up @@ -74,15 +77,70 @@ def create(self, vals_list):
# odoo import api, fields, models the default one linked to the activity type.
# We don't want this behavior because using the team_id, we want to assign the
# activity to the whole team.
new_vals_list = []
for vals in vals_list:
new_vals = vals.copy()
# we need to be sure that we are in a context where the team_id is set,
# and we don't want to use user_id
if vals.get("team_id"):
if new_vals.get("team_id"):
# using team, we have user_id = team_user_id,
# so if we don't have a user_team_id we don't want user_id too
if "user_id" in vals and not vals.get("team_user_id"):
del vals["user_id"]
return super().create(vals_list)
if "user_id" in new_vals and not new_vals.get("team_user_id"):
del new_vals["user_id"]
# team_user_id is a related field pointing to user_id (readonly=False).
# If left in vals, the ORM places it in the 'inversed' bucket and
# calls _inverse_related *after* the initial INSERT while user_id is
# still NULL. That triggers Model.write({'user_id': …}) which in turn
# fires action_notify() → action_notify_team() a first time, and then
# core mail.activity.create()'s post-hook fires action_notify() a
# second time — causing duplicate notifications for team members.
# Eagerly resolving team_user_id to user_id here ensures user_id is
# stored in the INSERT so no inverse write occurs.
if "team_user_id" in new_vals:
team_user_id_val = new_vals.pop("team_user_id")
new_vals.setdefault("user_id", team_user_id_val)
new_vals_list.append(new_vals)
activities = super().create(new_vals_list)
if not self.env.context.get("mail_activity_quick_update"):
# Core create() triggers action_notify() only for activities assigned to
# users different from the current one. Notify team members here only for
# activities not covered by that path to avoid duplicate notifications.
activities_without_core_notify = activities.filtered(
lambda activity: activity.user_id == self.env.user
)
activities_without_core_notify.action_notify_team()
return activities

def write(self, values):
# Notify the new team, but prevent duplicate notifications by excluding
# activities that will be notified by core write()->action_notify()
# when the user changes.
team_notify_activities = self.env["mail.activity"]
core_notified_activities = self.env["mail.activity"]
if not self.env.context.get("mail_activity_quick_update", False):
new_team_id = values.get("team_id", False)
team_notify_activities = self.filtered(
lambda activity, new_team_id=new_team_id: new_team_id
and activity.team_id.id != new_team_id
)
new_user_id = values.get("user_id", False)
user_changed_activities = self.filtered(
lambda activity, new_user_id=new_user_id: new_user_id
and activity.user_id.id != new_user_id
)
team_notify_activities |= user_changed_activities

# Core write() calls action_notify() for user changes except when
# assigning to the current user; avoid re-sending team notifications.
if new_user_id != self.env.uid:
core_notified_activities = user_changed_activities

res = super().write(values)
# notify new responsibles

if not self.env.context.get("mail_activity_quick_update", False):
(team_notify_activities - core_notified_activities).action_notify_team()
return res

@api.onchange("team_id")
def _onchange_team_id(self):
Expand Down Expand Up @@ -130,3 +188,84 @@ def _onchange_activity_type_id(self):
if self.user_id not in members and members:
self.user_id = members[:1]
return res

def _to_store_defaults(self, target):
values = super()._to_store_defaults(target)
values.append(Store.One("team_id", "name"))
return values

def action_notify_team(self):
# Like action_notify(), but for team members.
classified = self._classify_by_model()
for model, activity_data in classified.items():
records_sudo = self.env[model].sudo().browse(activity_data["record_ids"])
# in case record was cascade-deleted in DB, skipping unlink override
activity_data["record_ids"] = records_sudo.exists().ids

for activity in self:
if activity.res_id not in classified[activity.res_model]["record_ids"]:
continue
if not activity.team_id.notify_members:
continue

record = activity.env[activity.res_model].browse(activity.res_id)
# Notify each team member except the assigned user and the current user
members = activity.team_id.member_ids.filtered(
lambda member, assigned_user_id=activity.user_id: self.env.uid
not in member.user_ids.ids
and (
not assigned_user_id
or (assigned_user_id and assigned_user_id not in member.user_ids)
)
)
for member in members:
activity_ctx = (
activity.with_context(lang=member.lang) if member.lang else activity
)
model_description = (
activity_ctx.env["ir.model"]
._get(activity_ctx.res_model)
.display_name
)
body = activity_ctx.env["ir.qweb"]._render(
"mail.message_activity_assigned",
{
"activity": activity_ctx,
"model_description": model_description,
"is_html_empty": lambda value: not value
or value == "<p><br></p>",
},
minimal_qcontext=True,
)
record.message_notify(
partner_ids=member.sudo().partner_id.ids,
body=body,
model_description=model_description,
email_layout_xmlid="mail.mail_notification_layout",
subject=self.env._(
"%(activity_name)s: %(summary)s (Team Activity)",
activity_name=activity_ctx.res_name,
summary=activity_ctx.summary
or activity_ctx.activity_type_id.name,
),
subtitles=[
self.env._("Activity: %s", activity_ctx.activity_type_id.name),
self.env._("Team: %s", activity_ctx.team_id.name),
self.env._(
"Deadline: %s",
(
activity_ctx.date_deadline.strftime(
get_lang(activity_ctx.env).date_format
)
if hasattr(activity_ctx.date_deadline, "strftime")
else str(activity_ctx.date_deadline)
),
),
],
)

def action_notify(self):
"""Override to notify team members when notify_members is enabled."""
result = super().action_notify()
self.action_notify_team()
return result
5 changes: 5 additions & 0 deletions mail_activity_team/models/mail_activity_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def _compute_missing_activities(self):
string="Team Members",
)
user_id = fields.Many2one(comodel_name="res.users", string="Team Leader")
notify_members = fields.Boolean(
default=False,
help="When enabled, all team members will be notified "
"when an activity is assigned to this team.",
)
count_missing_activities = fields.Integer(
string="Missing Activities", compute="_compute_missing_activities", default=0
)
Expand Down
2 changes: 2 additions & 0 deletions mail_activity_team/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
- [CorporateHub](https://corporatehub.eu/)
- Alexey Pelykh <alexey.pelykh@corphub.eu>
- Stefan Rijnhart (<stefan@opener.amsterdam>)
- [glueckkanja AG](https://glueckkanja.com/)
- Christopher Rogos (<crogos@gmail.com>)
4 changes: 4 additions & 0 deletions mail_activity_team/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,10 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
</ul>
</li>
<li>Stefan Rijnhart (<a class="reference external" href="mailto:stefan&#64;opener.amsterdam">stefan&#64;opener.amsterdam</a>)</li>
<li><a class="reference external" href="https://glueckkanja.com/">glueckkanja AG</a><ul>
<li>Christopher Rogos (<a class="reference external" href="mailto:crogos&#64;gmail.com">crogos&#64;gmail.com</a>)</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="other-credits">
Expand Down
27 changes: 7 additions & 20 deletions mail_activity_team/static/src/core/web/activity.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,33 @@
t-inherit-mode="extension"
>
<!-- Prevent error rendering the avatar in case of missing user -->
<xpath
expr="//div/div[hasclass('o-mail-Activity-sidebar')]/a/img"
position="attributes"
>
<attribute name="t-if">props.activity.user_id</attribute>
</xpath>
<!-- Prevent error rendering the assigned user in case of missing user -->
<xpath
expr="//div/div/div[hasclass('o-mail-Activity-info')]/span[hasclass('o-mail-Activity-user')]"
position="attributes"
>
<xpath expr="//img[hasclass('o-mail-Activity-avatar')]" position="attributes">
<attribute name="t-if">props.activity.user_id</attribute>
</xpath>
<!-- Show team in the condensed view -->
<xpath
expr="//div/div/div[hasclass('o-mail-Activity-info')]/span[hasclass('o-mail-Activity-user')]"
position="after"
>
<xpath expr="//span[hasclass('o-mail-Activity-user')]" position="after">
<span
class="o-mail-Activity-user px-1"
class="o-mail-Activity-team px-1"
t-if="props.activity.team_id"
>(Team <t t-esc="props.activity.team_id[1]" />)
>(Team <t t-esc="props.activity.team_id.displayName" />)
</span>
</xpath>
<!-- Prevent error rendering the assigned user in case of missing user in the expanded view -->
<xpath
expr="//t[@t-esc='props.activity.persona.name']/parent::td/parent::tr"
expr="//t[@t-esc='props.activity.user_id.name']/parent::td/parent::tr"
position="attributes"
>
<attribute name="t-if">props.activity.user_id</attribute>
</xpath>
<!-- Show team in the expanded view -->
<xpath
expr="//t[@t-esc='props.activity.persona.name']/parent::td/parent::tr"
expr="//table[hasclass('o-mail-Activity-details')]/tbody/tr[@t-if='props.activity.user_id']"
position="after"
>
<tr t-if="props.activity.team_id">
<td class="text-end fw-bolder">Team</td>
<td>
<t t-esc="props.activity.team_id[1]" />
<t t-esc="props.activity.team_id.displayName" />
</td>
</tr>
</xpath>
Expand Down
Loading
Loading