Skip to content

T3305-Tracking-UTM - #2129

Open
loris-fab wants to merge 3 commits into
18.0from
T3305-Tracking-UTM
Open

T3305-Tracking-UTM#2129
loris-fab wants to merge 3 commits into
18.0from
T3305-Tracking-UTM

Conversation

@loris-fab

@loris-fab loris-fab commented Aug 5, 2026

Copy link
Copy Markdown

Goal

Our campaign analysis only covers digital interactions, while physical mailings
(white mail, magazines) appear to be more efficient. To measure those offline
campaigns, Odoo needs to record what was sent, to whom, and when it was
dispatched even though the dispatch itself happens outside of Odoo, at a
printing house.

Communications already are the history of everything a partner receives, so they
are what gets extended, rather than adding a parallel tracking model.

Technical aspect

  • UTM fields.

    • Source, Medium and Campaign are added on
      partner.communication.job and on partner.communication.config. A
      communication starts with the values configured on its type, through the
      existing _get_default_vals mechanism . The same one that already copies
      report_id and need_call. That also gives the pre-filling on the type's
      onchange for free.
    • They are exposed on the form, as optional list columns,
    • And as three group by in the search view.
  • utm_campaign_id moved down from partner_communication_compassion, where
    it was declared but referenced by no view and no code, into the base module
    alongside the two new ones. The column already exists, so no data migration.
    artner_communication,partner_communication_compassion

  • CSV import: recording a mailing that was dispatched outside of Odoo at a
    printing house, typically is done by importing the recipient list with the
    standard Import records button on Contacts → Partner Communication →
    Communication Jobs
    . No dedicated wizard was added: an import is an ordinary
    creation, driven entirely by the columns of the file.

    How it works

    • state = done is what marks an external mailing. A communication created
      in that state records something already dispatched: it is never merged into a
      pending communication, its sending date is filled in when missing, and Odoo
      skips attachment generation, PDF rendering, call scheduling and sending. The
      convention already existed in the module .
    • An import never sends anything on its own, whatever the state of the
      lines. Without that safety net, importing a list against a communication type
      configured to send automatically would dispatch the whole file for real.
    • UTM columns are optional. A communication starts with the Source, Medium
      and Campaign configured on its type(partner.communication.config), so the columns only need to be present
      when a line departs from that default.

    Columns

    Column Content
    partner_id Partner reference (the ref field), or the partner name
    config_id Name of the communication type
    state Done for a mailing that was already dispatched
    send_mode Print report for a letter (or the technical value physical)
    subject Optional — a readable label, otherwise the lines show no subject
    utm_source_id Optional — defaults to the source of the communication type
    utm_medium_id Optional — defaults to the medium of the communication type
    utm_campaign_id Optional — defaults to the campaign of the communication type
    sent_date Optional — dispatch date, defaults to the date of the import

    Pitfalls

    • An empty cell is not a missing column. The converter turns an empty value
      into False, so the key is present and no default applies: the UTM fields
      stay empty instead of inheriting from the communication type. To rely on the
      defaults, leave the column out of the file entirely.
    • Imported communications keep no content. _compute_email_template_id
      skips communications that are already done, so no template is attached and
      both subject and body stay empty. This is intended (what was printed did not
      come from Odoo ) but since the subject is the display name, the lines appear
      blank in lists.
  • Bug fix. Opening the communication creation form raised
    TypeError: argument of type 'bool' is not iterable. The form has a default
    communication type but no partner yet, and build_inform_mode was iterating
    over the delivery preference of an empty partner recordset.

  • create() is split into _prepare_create_vals and _merge_into_pending_job;
    it was over the complexity limit enforced by ruff otherwise.

Misc

  • Both modules (partner_communication,partner_communication_compassion) must be updated together. utm_campaign_id changes owning
    module: updating only partner_communication_compassion would leave the field
    declared nowhere.
  • Communications imported as done during the import keep no subject or body, since
    _compute_email_template_id skips communications that are already done. This
    is intended (what was printed did not come from Odoo).

Campaign analysis only covered digital interactions, while physical mailings
appear to be more efficient. Communications can now carry the UTM parameters
of the campaign they belong to, including the ones dispatched outside of Odoo.

- Add Source, Medium and Campaign on the communication and on its type, the
  communication starting with the values configured on its type.
- Record mailings sent by a printing house by importing the recipient list as
  a CSV: a communication created as done is never merged into a pending one,
  and nothing is generated nor sent for it. As a safety net, an import never
  sends anything on its own, whatever the state of the imported lines.
- Move utm_campaign_id down from partner_communication_compassion, where it
  was declared but referenced nowhere, so the base module carries all three.
- Fix a crash when opening the communication creation form: the type has a
  default value but no partner is selected yet, and build_inform_mode was
  iterating over the delivery preference of an empty partner.
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for the posted P1 finding.
  • The contract-validation review explains that PR T3305-Tracking-UTM #2129's guard depends on no_send and job.auto_send, that no repository code was changed, and that the described state transition could bypass the guard but delivery was not runtime-confirmed.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Queued auto-send can be bypassed by clearing the editable auto_send flag

    • Bug
      • A job created with auto_send queues a delayed send action while it remains pending. Before the action runs, a user can clear auto_send in the pending job form. A later CSV import has no_send=True; PR T3305-Tracking-UTM #2129 then sees job.auto_send == False, merges the imported object IDs into the queued job, and does not cancel the pre-existing queued action. The queued action can subsequently process the merged job. This exact delivery outcome could not be executed in Odoo here.
    • Cause
      • The merge safety condition treats the mutable auto_send field as proof that no delayed send has been queued. The job’s write() implementation accepts an auto_send change without cancelling or invalidating delayed queue work.
    • Fix
      • Make the import merge decision depend on durable queued-work state rather than the editable auto_send flag, or cancel/invalidate the queued send when auto_send is cleared. Add an Odoo regression test that queues auto-send, clears auto_send while pending, imports a matching no_send row, executes queued work, and asserts the imported recipient is not delivered.

    T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "[T3305] FIX: Never merge an import into ..." | Re-trigger Greptile

Comment thread partner_communication/models/communication_job.py Outdated
A job set to send automatically already has its send task in the queue, with a
10 seconds eta. Merging imported values into it made that task deliver them too
when it ran, which the no_send guard did not catch: it only prevented the
immediate, synchronous send. Imported values now stay in their own job, which is
created without auto_send.
Comment thread partner_communication/models/communication_job.py Outdated
Guarding on auto_send was not enough: the field is editable, so clearing it on a
communication whose send task is already queued would let an import merge into
it, and the queued task would then deliver the imported records.

An import now never merges at all, which removes the whole race rather than
guessing whether a send is pending. Every imported line keeps its own record.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on August 13. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.

@loris-fab
loris-fab requested a review from ecino August 6, 2026 07:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant