Skip to content

[20.0][MIG] mail_tracking: Migration to 20.0 and rename to mail_recipient_tracking - #310

Open
CarlosRoca13 wants to merge 116 commits into
OCA:20.0from
Tecnativa:20.0-MIG-mail_tracking
Open

CarlosRoca13 wants to merge 116 commits into
OCA:20.0from
Tecnativa:20.0-MIG-mail_tracking

Conversation

@CarlosRoca13

Copy link
Copy Markdown
Contributor

cc @Tecnativa TT65067

This PR migrates mail_tracking, which needs to be renamed because Odoo now uses the same name.

ping @carlos-lopez-tecnativa @victoralmau

@OCA-git-bot OCA-git-bot added series:20.0 mod:mail_recipient_tracking Module mail_recipient_tracking labels Oct 22, 2026
@CarlosRoca13
CarlosRoca13 force-pushed the 20.0-MIG-mail_tracking branch from 264d75a to c72225a Compare October 22, 2026 12:07
@pedrobaeza

Copy link
Copy Markdown
Member

/ocabot migration mail_tracking

@OCA-git-bot OCA-git-bot added this to the 20.0 milestone Oct 22, 2026
@OCA-git-bot

Copy link
Copy Markdown
Contributor

There's no issue in this repo with the title 'Migration to version 20.0' and the milestone 20.0, so not possible to add the comment.

@pedrobaeza pedrobaeza changed the title [20.0][MIG] mail_recipient_tracking: Migration to 20.0 [20.0][MIG] mail_tracking: Migration to 20.0 and rename to mail_recipient_tracking Oct 22, 2026
@pedrobaeza

Copy link
Copy Markdown
Member

The renaming doesn't seem to be done for the whole commit history. Please check in https://github.com/OCA/maintainer-tools/wiki/Migration-to-version-20.0#technical-method-to-migrate-a-module-from-190-to-200-branch how to do it

@pedrobaeza

Copy link
Copy Markdown
Member

There's no issue in this repo with the title 'Migration to version 20.0' and the milestone 20.0, so not possible to add the comment.

@sbidoul @simahawk do you know why this is not linked?

@rodrigoluizgenz

Copy link
Copy Markdown

Hello guys, can you check this ?
Tested on 20.0 localhost database with hr installed: test_access_tracking_email fails (AccessError not raised).
In 20.0, BaseModel.check_access()/has_access() rely only on _access_domain() + the access cache and no longer call _check_access(), so the override never runs on read() (searches are still filtered by _search).

The rule probably needs to move to an _access_domain override or an ir.rule. CI doesn't catch it because the test is skipped without hr.

antespi and others added 20 commits October 23, 2026 07:51
* [ADD] mail_tracking addon

* Add description icon

* Fixes remarked

* Fix Travis error

* Remarks fixed
…ent events (OCA#82)

[IMP] mail_tracking: Speed installation time, discard concurrent events and other fixes
* Improve tests
* Show trackings even if partner removed
* Disable CSRF protection to webhooks controllers
As regular users can't access this object.
* [FIX] Only one data-odoo-tracking-email tag in each email
* [FIX] Get status even in multicompany instances
On our server,
queries based on "mail_tracking_event"."tracking_email_id" improved from 501,924 ms to 1,840 ms
queries based on "mail_tracking_email"."mail_message_id" improved from 167,436 ms to 3,223 ms

The last ones are run several times when a thread has many messages
CarlosRoca13 and others added 10 commits October 23, 2026 07:51
…s reviewed

Before these changes, when the bus event was received in tabs that did not have the message open, an error appeared because the system was not able to find the message.

After these changes, if the message is not available on the screen, the process continues because synchronization will not be necessary, and therefore the error will no longer occur.
Create a dedicated company alias domain during the test setup to avoid
temporary alias domains becoming referenced by aliases created by other
installed modules.

TT63167
Add optional mail headers expected by inherited `mail.thread`
implementations, avoiding failures when modules such as
`mass_mailing` are installed.

TT63167
Add the optional mail headers to the second payload as well to keep the
test compatible with inherited mail.thread implementations such as
mass_mailing.

TT63167
Currently translated at 100.0% (126 of 126 strings)

Translation: mail-19.0/mail-19.0-mail_tracking
Translate-URL: https://translation.odoo-community.org/projects/mail-19-0/mail-19-0-mail_tracking/sl/
`MailMail._send()` tells a successful delivery from a failed one by the
exception raised by `send_email()`, not by its return value. `mail_tracking`
catches every exception and, when a `mail.tracking.email` record exists, records
the error on the tracking record and returns normally, so core believes the
mail was delivered: the recipient is marked as reached and its notification is
written `sent` for a mail that never left, core's `except AssertionError` never
runs so a NO_VALID_RECIPIENT is never classified, and the mail keeps the
placeholder failure reason set before the sending loop.

A tracking record is created for every outgoing email, so with `mail_tracking`
installed core's whole failure-handling path is disabled for the database.

This is not a duplicate of OCA#215/OCA#212, which added `else: raise`, i.e. it only
re-raises when there is NO tracking record -- the branch that is dead in
practice. The live case is the other one.

Re-raise unconditionally. The error is still recorded on the tracking record by
the `smtp_error()` call just above, so nothing is lost; only core's contract is
restored.

Note for adopters: delivery failures that used to be silent now surface as
`exception` states on `mail.mail`/`mail.notification`, so a backlog of
previously hidden failures may become visible at once on upgrade.

Fixes OCA#253
@CarlosRoca13
CarlosRoca13 force-pushed the 20.0-MIG-mail_tracking branch from c72225a to f9be1c7 Compare October 23, 2026 06:02
@CarlosRoca13

Copy link
Copy Markdown
Contributor Author

I think it's all solved 😄

:return: number of emails sent again
"""
self.check_access("read")
messages = self.filtered("mail_tracking_needs_action")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
messages = self.filtered("mail_tracking_needs_action")
messages = self.filtered("is_failed_message")
Image Image Image Image Image Image Image Image

action_retry_failed_mails only checks read access and then mail_tracking_needs_action. After that, _retry_failed_mails runs with sudo(). Any internal user who can read a message can call the method over RPC and resend its failed emails to the original recipients, even if they are neither the author nor a notified partner. The UI hides the Retry button from them, but the server does not enforce the same rule.

My suggestion is to filter by is_failed_message. It is computed for the current user and is true only when the user is the author or a notified partner, the message needs action and it has a failed tracking. The server now enforces the rule the UI already promises. For an uninvolved user the recordset is empty, so the call returns 0 and sends nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! Applied: action_retry_failed_mails now filters by is_failed_message. The server applies the same rule as the web client, so only the author or a notified partner can resend the failed emails.

Comment on lines +88 to +101
domain = Domain.AND(
[
[
(
"mail_tracking_ids.state",
"in" if operand else "not in",
list(self.get_failed_states()),
),
("mail_tracking_needs_action", "=", True),
],
is_involve,
]
)
return domain

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
domain = Domain.AND(
[
[
(
"mail_tracking_ids.state",
"in" if operand else "not in",
list(self.get_failed_states()),
),
("mail_tracking_needs_action", "=", True),
],
is_involve,
]
)
return domain
domain = Domain.AND(
[
[
("mail_tracking_ids.state", "in", list(self.get_failed_states())),
("mail_tracking_needs_action", "=", True),
],
is_involve,
]
)
# The ORM normalizes "= v" / "!= v" into "in" / "not in" over a set of values
wants_failed = operand if isinstance(operand, bool) else any(operand)
return domain if (operator in ("in", "=")) == wants_failed else ~domain
Image Image Image Image Image

The ORM calls this search method with a normalized condition, never with a plain boolean: is_failed_message = False arrives as ('not in', {True}). The old code ignored operator and tested operand for truthiness. A non-empty set is always truthy, so it always built the positive domain. As a result, = True, = False, != True and != False all returned the failed messages, and the negative searches were wrong.

The fix always builds the positive domain (failed state, needs action, user involved) and negates the whole domain with ~ when the search asks for non-failed messages. This matches the computed field, which is true only when all three conditions hold together.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks! I checked it in _optimize_boolean_in: since 20.0 the search method always receives ("in" | "not in", [True]), so the operand was always truthy and negative searches returned the failed messages. Applied, slightly simplified since the value is always normalized.

return domain if operator == "in" else ~domain

@CarlosRoca13
CarlosRoca13 force-pushed the 20.0-MIG-mail_tracking branch from f9be1c7 to e65cd52 Compare October 23, 2026 11:19
@CarlosRoca13

Copy link
Copy Markdown
Contributor Author

@cristina-hidalgo-tecnativa Can you check it again?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Retry and Set as Reviewed actions of the messaging menu "Failed" tab are <span role="button"> elements with only a click handler. They have no tabindex, so keyboard users cannot focus them with Tab, and they do not react to Enter or Space. role="button" only changes what screen readers announce; it adds no behavior, so assistive technology announces a button that cannot be activated.

A native <button> cannot be used because the core mail.NotificationItem row is itself a <button>, and nested buttons are invalid HTML. So the pseudo-buttons are kept, with tabindex="0" and a keydown handler for Enter and Space that runs the same action as the click. preventDefault stops Space from scrolling the page, and stopPropagation keeps the key event from reaching the parent row.

@CarlosRoca13
CarlosRoca13 force-pushed the 20.0-MIG-mail_tracking branch from e65cd52 to 2d756aa Compare October 23, 2026 13:39
@CarlosRoca13

Copy link
Copy Markdown
Contributor Author

Solved! 😄

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reviewed and tested locally and on runboat. LGTM!

Small doubt: wouldn't it be reasonable to add an else with self.skipTest(...) for when hr is not installed, so the skip is at least visible?

@pedrobaeza

Copy link
Copy Markdown
Member

Please include #298 and #297 before migration commit.

moylop260 and others added 3 commits October 28, 2026 08:54
mail_id has an ON DELETE SET NULL foreign key to mail_mail, so every
deleted mail.mail scans the whole mail_tracking_email table. Odoo 19.0
deletes up to 10,000 cancelled mails per autovacuum run. With 7M
trackings that is about 1.5 s per mail without the index and 6.5 s for
the whole batch with it.

Use a partial index: mail_id is set to NULL every time its mail.mail is
deleted, so most rows hold NULL, and the foreign key trigger only runs
"WHERE mail_id = $1" with a non-NULL value. The index stays much
smaller, as core does for mailing.trace.mail_mail_id.
mail.message.mail_tracking_ids sets bypass_search_access, so the ORM
searches mail.tracking.email with bypass_access=True. _search ignored
it and filtered every matching tracking in Python. With 7M trackings,
get_failed_count went from 5-10 s to under 1 s.
Odoo 20.0 core provides a `mail_tracking` module (technical tracking of field values), so this addon is renamed from `mail_tracking` to `mail_recipient_tracking`.
@CarlosRoca13
CarlosRoca13 force-pushed the 20.0-MIG-mail_tracking branch from 2d756aa to 5936326 Compare October 28, 2026 07:57
@CarlosRoca13

Copy link
Copy Markdown
Contributor Author

Done @pedrobaeza

@rodrigoluizgenz

Copy link
Copy Markdown

Thanks for the migration! A question about the upgrade path for databases that already have the OCA mail_tracking installed.

The PR doesn't include a migrations/ folder or a module-rename step. If a 19.0 database arrives at 20.0 with mail_tracking still in ir_module_module, that row is taken over by the new core mail_tracking ("Discuss Tracking"). When the core module updates, ir.model.data._process_end unlinks every mail_tracking.* xmlid it didn't load, which includes all of the OCA metadata:

  • mail.tracking.email / mail.tracking.event: the ir.model rows are removed, but the tables survive because _drop_table only acts on models in the registry. They are orphaned until mail_recipient_tracking is installed and picks them up again (same _name).
  • Stored fields the OCA module adds to core models: ir.model.fields.unlink() → _drop_column drops the column, because those models are in the registry. That means res_partner.email_bounced, mail_message.email_to / email_cc / mail_tracking_needs_action and res_company.mail_tracking_show_aliases would be lost.

Renaming the module before the upgrade (e.g. openupgradelib.update_module_names(cr, [("mail_tracking", "mail_recipient_tracking")], merge_modules=False)) avoids this. Is the plan to handle it through an OpenUpgrade apriori.py renamed_modules entry? A note in the README (INSTALL/ROADMAP) for users migrating with the Odoo upgrade service would also help, since they'd need to run the rename themselves.

@CarlosRoca13

CarlosRoca13 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the migration! A question about the upgrade path for databases that already have the OCA mail_tracking installed.

The PR doesn't include a migrations/ folder or a module-rename step. If a 19.0 database arrives at 20.0 with mail_tracking still in ir_module_module, that row is taken over by the new core mail_tracking ("Discuss Tracking"). When the core module updates, ir.model.data._process_end unlinks every mail_tracking.* xmlid it didn't load, which includes all of the OCA metadata:

  • mail.tracking.email / mail.tracking.event: the ir.model rows are removed, but the tables survive because _drop_table only acts on models in the registry. They are orphaned until mail_recipient_tracking is installed and picks them up again (same _name).
  • Stored fields the OCA module adds to core models: ir.model.fields.unlink() → _drop_column drops the column, because those models are in the registry. That means res_partner.email_bounced, mail_message.email_to / email_cc / mail_tracking_needs_action and res_company.mail_tracking_show_aliases would be lost.

Renaming the module before the upgrade (e.g. openupgradelib.update_module_names(cr, [("mail_tracking", "mail_recipient_tracking")], merge_modules=False)) avoids this. Is the plan to handle it through an OpenUpgrade apriori.py renamed_modules entry? A note in the README (INSTALL/ROADMAP) for users migrating with the Odoo upgrade service would also help, since they'd need to run the rename themselves.

I'm waiting to set the rename to openupgrade when this PR has being merge OCA/OpenUpgrade#6045

@pedrobaeza

Copy link
Copy Markdown
Member

@CarlosRoca13 OpenUpgrade initialization merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:mail_recipient_tracking Module mail_recipient_tracking series:20.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.