From b7bb84283da39f7b23e75193179ae8be88ace66b Mon Sep 17 00:00:00 2001 From: loris-fab Date: Wed, 5 Aug 2026 17:40:12 +0200 Subject: [PATCH 1/6] [T3305] FEAT: Track UTM campaigns on communications 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. --- partner_communication/README.rst | 79 ++++++++++++ .../models/communication_config.py | 18 ++- .../models/communication_job.py | 117 ++++++++++++++---- partner_communication/readme/USAGE.md | 47 +++++++ .../static/description/index.html | 112 +++++++++++++++-- .../views/communication_config_view.xml | 5 + .../views/communication_job_view.xml | 30 +++++ .../__manifest__.py | 2 +- .../models/partner_communication.py | 2 - 9 files changed, 370 insertions(+), 42 deletions(-) create mode 100644 partner_communication/readme/USAGE.md diff --git a/partner_communication/README.rst b/partner_communication/README.rst index ecac055bc..4aa95b5ff 100644 --- a/partner_communication/README.rst +++ b/partner_communication/README.rst @@ -32,6 +32,85 @@ efficient. .. contents:: :local: +Usage +===== + +Tracking mailings sent outside of Odoo +-------------------------------------- + +Communications carry three UTM fields (Source, Medium, Campaign) so that +mailings sent outside of Odoo — through a printing house, for instance — +can be analysed together with the digital ones. + +Each communication type carries its own **Campaign Tracking** defaults, +in the *General configuration* of its form. A communication starts with +the values of its type, and they can then be changed on the +communication itself. + +A communication **created directly in the Done state** records a mailing +that was already dispatched: Odoo generates nothing and sends nothing +for it, and it is not merged into a pending communication. Its sending +date is filled in automatically when it is not given. Such a +communication keeps no content, since what was printed did not come from +Odoo. + +Importing a recipient list +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To record a mailing that has already been dispatched, import the +recipient list sent to the printer from *Contacts → Partner +Communication → Communication Jobs*, with the standard **Import +records** button. Useful 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 | ++---------------------+-----------------------------------------------+ + +An **empty cell is not the same as a missing column**: it sets the field +to empty instead of falling back on the default of the communication +type. To rely on the defaults, leave the column out of the file +entirely. + +Prefer the partner **reference** over the name: a name is matched +through ``name_search``, which silently picks the first record when +several partners share it. UTM records given by name must exist +beforehand, and their names must be unique for the same reason. + +Without the ``state`` column, the lines are imported as regular pending +communications, ready to be sent by Odoo. As a safety net, an import +never sends anything on its own, whatever the state — importing a +recipient list is meant to record mailings, and a communication type set +to send automatically would otherwise dispatch the whole file. + +Once imported, the communication list groups by Campaign, Medium and +Source. + Bug Tracker =========== diff --git a/partner_communication/models/communication_config.py b/partner_communication/models/communication_config.py index 2e5a05e58..54d1fd0e1 100644 --- a/partner_communication/models/communication_config.py +++ b/partner_communication/models/communication_config.py @@ -136,6 +136,11 @@ class CommunicationConfig(models.Model): forbid_merging = fields.Boolean( help="If selected, disable the automatic merging of communications", ) + # Values the communications of this type start with. They are only defaults: they + # are copied on the job at creation and can be changed on it afterwards. + utm_source_id = fields.Many2one("utm.source", "Default Source") + utm_medium_id = fields.Many2one("utm.medium", "Default Medium") + utm_campaign_id = fields.Many2one("utm.campaign", "Default Campaign") active = fields.Boolean(default=True) send_from = fields.Selection( [ @@ -291,10 +296,15 @@ def build_inform_mode( """ send_priority = self._get_send_priority(partner, print_if_not_email) if communication_send_mode != "partner_preference": - partner_mode = getattr( - partner, - send_mode_pref_field or "global_communication_delivery_preference", - partner.global_communication_delivery_preference, + partner_mode = ( + getattr( + partner, + send_mode_pref_field or "global_communication_delivery_preference", + partner.global_communication_delivery_preference, + ) + # An empty partner recordset (in the creation form, the config has a + # default value but no partner is selected yet) has no preference. + or "none" ) auto_mode = self._get_auto_mode(partner_mode, communication_send_mode) if communication_send_mode == partner_mode: diff --git a/partner_communication/models/communication_job.py b/partner_communication/models/communication_job.py index 8b588ac5f..d61ddbeae 100644 --- a/partner_communication/models/communication_job.py +++ b/partner_communication/models/communication_job.py @@ -185,6 +185,14 @@ class CommunicationJob(models.Model): sms_cost = fields.Float() + # Campaign tracking. Defaults to the values of the communication type, and can then + # be changed on the communication itself. + utm_source_id = fields.Many2one("utm.source", "Source", index="btree_not_null") + utm_medium_id = fields.Many2one("utm.medium", "Medium", index="btree_not_null") + utm_campaign_id = fields.Many2one( + "utm.campaign", "Campaign", index="btree_not_null" + ) + def _compute_ir_attachments(self): for job in self: job.ir_attachment_ids = job.mapped("attachment_ids.attachment_id") @@ -345,36 +353,20 @@ def create(self, vals_list): """If a pending communication for same partner exists, add the object_ids to it. Otherwise, create a new communication. opt-out partners won't create any communication. + + A communication created as done only records a mailing that was already + dispatched, outside of Odoo for instance: it is never merged, and nothing is + generated nor sent for it. """ updated = self.browse() + # A CSV import (`import_file` is set by base_import) never sends anything, even + # for pending communications: importing a recipient list is meant to record + # mailings, and a communication type set to send automatically would otherwise + # dispatch the whole file. + no_send = bool(self.env.context.get("import_file")) for vals in vals_list.copy(): - # Object ids accept lists, integer or string values. It should contain - # a comma separated list of integers - object_ids = vals.get("object_ids") - if isinstance(object_ids, list): - vals["object_ids"] = ",".join(map(str, object_ids)) - elif object_ids: - vals["object_ids"] = str(object_ids) - else: - vals["object_ids"] = str(vals["partner_id"]) - - same_job_search = [ - ("partner_id", "=", vals.get("partner_id")), - ("config_id", "=", vals.get("config_id")), - ( - "config_id", - "!=", - self.env.ref("partner_communication.default_communication").id, - ), - ("state", "in", ["pending", "failure"]), - ] + self.env.context.get("same_job_search", []) - job = self.search(same_job_search, limit=1) - - if job and not job.config_id.forbid_merging: - job.object_ids = job.object_ids + "," + vals["object_ids"] - job.refresh_text() - if job.auto_send: - job.send() + job = self._prepare_create_vals(vals, no_send=no_send) + if job: updated += job vals_list.remove(vals) @@ -401,6 +393,11 @@ def create(self, vals_list): ): job.auto_send = send_mode[1] + if job.state == "done": + # The communication is only recorded for tracking purposes: skip + # attachments and PDF rendering, and never call nor send anything. + continue + job.set_attachments() if job.send_mode in ("both", "physical"): job.count_pdf_page() @@ -427,6 +424,69 @@ def create(self, vals_list): return updated + created + def _prepare_create_vals(self, vals, no_send=False): + """Normalise the values of a communication about to be created, and merge them + into an existing pending communication when possible. + :param vals: dict: record values, updated in place + :param no_send: never send anything while creating the communication. + :return: the job the values were merged into, empty recordset if none. + """ + # Object ids accept lists, integer or string values. It should contain + # a comma separated list of integers + object_ids = vals.get("object_ids") + if isinstance(object_ids, list): + vals["object_ids"] = ",".join(map(str, object_ids)) + elif object_ids: + vals["object_ids"] = str(object_ids) + else: + vals["object_ids"] = str(vals["partner_id"]) + + if no_send: + vals["auto_send"] = False + + if "state" in vals and not vals["state"]: + # An empty cell in a CSV sets the field to False instead of leaving it out: + # fall back on the default state rather than create a stateless job. + del vals["state"] + + if vals.get("state") == "done": + # The communication only records a mailing that was already dispatched, + # outside of Odoo for instance: it is never merged, and nothing may be + # generated nor sent for it. + if not vals.get("sent_date"): + vals["sent_date"] = fields.Datetime.now() + vals["auto_send"] = False + return self.browse() + + return self._merge_into_pending_job(vals, no_send=no_send) + + def _merge_into_pending_job(self, vals, no_send=False): + """Look for a pending communication of the same partner and type in which the + values being created can be merged, and merge them into it. + :param vals: dict: record values + :param no_send: don't send the job even if it is set to be sent automatically. + :return: the job the values were merged into, empty recordset if none was found. + """ + same_job_search = [ + ("partner_id", "=", vals.get("partner_id")), + ("config_id", "=", vals.get("config_id")), + ( + "config_id", + "!=", + self.env.ref("partner_communication.default_communication").id, + ), + ("state", "in", ["pending", "failure"]), + ] + self.env.context.get("same_job_search", []) + job = self.search(same_job_search, limit=1) + if not job or job.config_id.forbid_merging: + return self.browse() + + job.object_ids = job.object_ids + "," + vals["object_ids"] + job.refresh_text() + if job.auto_send and not no_send: + job.send() + return job + @api.model def _get_dynamic_user(self, config, object_ids_str): """ @@ -486,6 +546,9 @@ def _get_default_vals(self, vals, default_vals=None): "report_id", "need_call", "print_if_not_email", + "utm_source_id", + "utm_medium_id", + "utm_campaign_id", ] ) diff --git a/partner_communication/readme/USAGE.md b/partner_communication/readme/USAGE.md new file mode 100644 index 000000000..b850164ec --- /dev/null +++ b/partner_communication/readme/USAGE.md @@ -0,0 +1,47 @@ +## Tracking mailings sent outside of Odoo + +Communications carry three UTM fields (Source, Medium, Campaign) so that mailings sent +outside of Odoo — through a printing house, for instance — can be analysed together with +the digital ones. + +Each communication type carries its own **Campaign Tracking** defaults, in the *General +configuration* of its form. A communication starts with the values of its type, and they +can then be changed on the communication itself. + +A communication **created directly in the Done state** records a mailing that was already +dispatched: Odoo generates nothing and sends nothing for it, and it is not merged into a +pending communication. Its sending date is filled in automatically when it is not given. +Such a communication keeps no content, since what was printed did not come from Odoo. + +### Importing a recipient list + +To record a mailing that has already been dispatched, import the recipient list sent to the +printer from *Contacts → Partner Communication → Communication Jobs*, with the standard +**Import records** button. Useful 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 | + +An **empty cell is not the same as a missing column**: it sets the field to empty instead +of falling back on the default of the communication type. To rely on the defaults, leave +the column out of the file entirely. + +Prefer the partner **reference** over the name: a name is matched through `name_search`, +which silently picks the first record when several partners share it. UTM records given by +name must exist beforehand, and their names must be unique for the same reason. + +Without the `state` column, the lines are imported as regular pending communications, ready +to be sent by Odoo. As a safety net, an import never sends anything on its own, whatever +the state — importing a recipient list is meant to record mailings, and a communication type +set to send automatically would otherwise dispatch the whole file. + +Once imported, the communication list groups by Campaign, Medium and Source. diff --git a/partner_communication/static/description/index.html b/partner_communication/static/description/index.html index 38bcc5aea..458b022e8 100644 --- a/partner_communication/static/description/index.html +++ b/partner_communication/static/description/index.html @@ -377,16 +377,112 @@

Partner Communication

Table of contents

+
+

Usage

+
+

Tracking mailings sent outside of Odoo

+

Communications carry three UTM fields (Source, Medium, Campaign) so that +mailings sent outside of Odoo — through a printing house, for instance — +can be analysed together with the digital ones.

+

Each communication type carries its own Campaign Tracking defaults, +in the General configuration of its form. A communication starts with +the values of its type, and they can then be changed on the +communication itself.

+

A communication created directly in the Done state records a mailing +that was already dispatched: Odoo generates nothing and sends nothing +for it, and it is not merged into a pending communication. Its sending +date is filled in automatically when it is not given. Such a +communication keeps no content, since what was printed did not come from +Odoo.

+
+

Importing a recipient list

+

To record a mailing that has already been dispatched, import the +recipient list sent to the printer from Contacts → Partner +Communication → Communication Jobs, with the standard Import +records button. Useful columns:

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnContent
partner_idPartner reference (the ref field), or the +partner name
config_idName of the communication type
stateDone for a mailing that was already +dispatched
send_modePrint report for a letter (or the +technical value physical)
subjectOptional — a readable label, otherwise the +lines show no subject
utm_source_idOptional — defaults to the source of the +communication type
utm_medium_idOptional — defaults to the medium of the +communication type
utm_campaign_idOptional — defaults to the campaign of the +communication type
sent_dateOptional — dispatch date, defaults to the +date of the import
+

An empty cell is not the same as a missing column: it sets the field +to empty instead of falling back on the default of the communication +type. To rely on the defaults, leave the column out of the file +entirely.

+

Prefer the partner reference over the name: a name is matched +through name_search, which silently picks the first record when +several partners share it. UTM records given by name must exist +beforehand, and their names must be unique for the same reason.

+

Without the state column, the lines are imported as regular pending +communications, ready to be sent by Odoo. As a safety net, an import +never sends anything on its own, whatever the state — importing a +recipient list is meant to record mailings, and a communication type set +to send automatically would otherwise dispatch the whole file.

+

Once imported, the communication list groups by Campaign, Medium and +Source.

+
+
-

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 @@ -394,15 +490,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Compassion Switzerland
-

Maintainers

+

Maintainers

This module is part of the CompassionCH/compassion-modules project on GitHub.

You are welcome to contribute.

diff --git a/partner_communication/views/communication_config_view.xml b/partner_communication/views/communication_config_view.xml index 5fdafe0bd..92b89de37 100644 --- a/partner_communication/views/communication_config_view.xml +++ b/partner_communication/views/communication_config_view.xml @@ -41,6 +41,11 @@ + + + + + + + + + + @@ -225,6 +230,9 @@ + + +
From 852872f263303e685aa0fcca41262689c78e64e5 Mon Sep 17 00:00:00 2001 From: loris-fab Date: Thu, 3 Sep 2026 14:02:28 +0200 Subject: [PATCH 4/6] [T3305] REF: Use utm.mixin for campaign tracking on communications partner.communication.defaults now inherits utm.mixin, so the config, the default config and the job share source_id, medium_id and campaign_id instead of three hand-declared utm_*_id fields. The config's own source_id is the source of its communications, which removes the redundant "Default Source". The job keeps its btree_not_null indexes. The unused utm_campaign_id column on the job is dropped by Odoo on update. Co-Authored-By: Claude Fable 5.1 --- partner_communication/README.rst | 68 +++++++++-------- partner_communication/__manifest__.py | 2 +- .../models/communication_config.py | 16 ++-- .../models/communication_job.py | 18 ++--- partner_communication/readme/USAGE.md | 30 ++++---- .../static/description/index.html | 75 ++++++++++--------- .../views/communication_config_view.xml | 12 ++- .../views/communication_job_view.xml | 30 ++++---- 8 files changed, 132 insertions(+), 119 deletions(-) diff --git a/partner_communication/README.rst b/partner_communication/README.rst index 2f04c0927..cb6328e65 100644 --- a/partner_communication/README.rst +++ b/partner_communication/README.rst @@ -7,7 +7,7 @@ Partner Communication !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:4ab6ac3d709e050b5b05223900a63985bf3602dd9f6d83da92ff0df0feab456b + !! source digest: sha256:b1d1e08b27519fb349288f424fa7810de667b6877f31bef440b323ef3d708be3 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -42,9 +42,11 @@ Communications carry three UTM fields (Source, Medium, Campaign) so that mailings sent outside of Odoo — through a printing house, for instance — can be analysed together with the digital ones. -Each communication type carries its own **Campaign Tracking** defaults, -in the *General configuration* of its form. A communication starts with -the values of its type, and they can then be changed on the +A communication type is itself the UTM **source** of its communications. +Its default **medium** and **campaign** are set under *Campaign +Tracking* in the *General configuration* of its form, and can be refined +per language or per user in its *Custom configuration*. A communication +starts with the values of its type, and they can then be changed on the communication itself. A communication **created directly in the Done state** records a mailing @@ -62,35 +64,35 @@ recipient list sent to the printer from *Contacts → Partner Communication → Communication Jobs*, with the standard **Import records** button. Useful 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 | -+---------------------+-----------------------------------------------+ ++-----------------+---------------------------------------------------+ +| 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 | ++-----------------+---------------------------------------------------+ +| ``source_id`` | Optional — defaults to the communication type, | +| | which is a source | ++-----------------+---------------------------------------------------+ +| ``medium_id`` | Optional — defaults to the medium of the | +| | communication type | ++-----------------+---------------------------------------------------+ +| ``campaign_id`` | Optional — defaults to the campaign of the | +| | communication type | ++-----------------+---------------------------------------------------+ +| ``sent_date`` | Optional — dispatch date, defaults to the date of | +| | the import | ++-----------------+---------------------------------------------------+ An **empty cell is not the same as a missing column**: it sets the field to empty instead of falling back on the default of the communication diff --git a/partner_communication/__manifest__.py b/partner_communication/__manifest__.py index 44b156191..1749105d3 100644 --- a/partner_communication/__manifest__.py +++ b/partner_communication/__manifest__.py @@ -30,7 +30,7 @@ # pylint: disable=C8101 { "name": "Partner Communication", - "version": "18.0.1.0.2", + "version": "18.0.1.0.3", "category": "Other", "author": "Compassion Switzerland", "license": "AGPL-3", diff --git a/partner_communication/models/communication_config.py b/partner_communication/models/communication_config.py index 54d1fd0e1..cbc540bb9 100644 --- a/partner_communication/models/communication_config.py +++ b/partner_communication/models/communication_config.py @@ -17,9 +17,15 @@ class CommunicationDefaults(models.AbstractModel): """Abstract class to share config settings between communication config - and communication job.""" + and communication job. + + It carries the UTM fields (source_id, medium_id, campaign_id) of utm.mixin: a + communication starts with the values of its type, or of the default config that + applies, and they can then be changed on the communication itself. + """ _name = "partner.communication.defaults" + _inherit = "utm.mixin" _description = "Communication Defaults" user_id = fields.Many2one("res.users", "From", domain=[("share", "=", False)]) @@ -90,11 +96,14 @@ class CommunicationConfig(models.Model): ########################################################################## # FIELDS # ########################################################################## + # Also the source_id of utm.mixin: the communication type is itself the UTM + # source of its communications. source_id = fields.Many2one( "utm.source", "UTM Source", required=True, ondelete="restrict", + help="The communications of this type are tracked with this source.", ) model_id = fields.Many2one( "ir.model", @@ -136,11 +145,6 @@ class CommunicationConfig(models.Model): forbid_merging = fields.Boolean( help="If selected, disable the automatic merging of communications", ) - # Values the communications of this type start with. They are only defaults: they - # are copied on the job at creation and can be changed on it afterwards. - utm_source_id = fields.Many2one("utm.source", "Default Source") - utm_medium_id = fields.Many2one("utm.medium", "Default Medium") - utm_campaign_id = fields.Many2one("utm.campaign", "Default Campaign") active = fields.Boolean(default=True) send_from = fields.Selection( [ diff --git a/partner_communication/models/communication_job.py b/partner_communication/models/communication_job.py index 9aad5ed04..72ad875ea 100644 --- a/partner_communication/models/communication_job.py +++ b/partner_communication/models/communication_job.py @@ -185,13 +185,11 @@ class CommunicationJob(models.Model): sms_cost = fields.Float() - # Campaign tracking. Defaults to the values of the communication type, and can then - # be changed on the communication itself. - utm_source_id = fields.Many2one("utm.source", "Source", index="btree_not_null") - utm_medium_id = fields.Many2one("utm.medium", "Medium", index="btree_not_null") - utm_campaign_id = fields.Many2one( - "utm.campaign", "Campaign", index="btree_not_null" - ) + # Campaign tracking, from utm.mixin through partner.communication.defaults. + # Indexed here only: communications are filtered and grouped by campaign. + source_id = fields.Many2one(index="btree_not_null") + medium_id = fields.Many2one(index="btree_not_null") + campaign_id = fields.Many2one(index="btree_not_null") def _compute_ir_attachments(self): for job in self: @@ -551,9 +549,9 @@ def _get_default_vals(self, vals, default_vals=None): "report_id", "need_call", "print_if_not_email", - "utm_source_id", - "utm_medium_id", - "utm_campaign_id", + "source_id", + "medium_id", + "campaign_id", ] ) diff --git a/partner_communication/readme/USAGE.md b/partner_communication/readme/USAGE.md index 1d851effc..45a8bfea2 100644 --- a/partner_communication/readme/USAGE.md +++ b/partner_communication/readme/USAGE.md @@ -4,9 +4,11 @@ Communications carry three UTM fields (Source, Medium, Campaign) so that mailing outside of Odoo — through a printing house, for instance — can be analysed together with the digital ones. -Each communication type carries its own **Campaign Tracking** defaults, in the *General -configuration* of its form. A communication starts with the values of its type, and they -can then be changed on the communication itself. +A communication type is itself the UTM **source** of its communications. Its default +**medium** and **campaign** are set under *Campaign Tracking* in the *General configuration* +of its form, and can be refined per language or per user in its *Custom configuration*. A +communication starts with the values of its type, and they can then be changed on the +communication itself. A communication **created directly in the Done state** records a mailing that was already dispatched: Odoo generates nothing and sends nothing for it, and it is not merged into a @@ -19,17 +21,17 @@ To record a mailing that has already been dispatched, import the recipient list printer from *Contacts → Partner Communication → Communication Jobs*, with the standard **Import records** button. Useful 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 | +| 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 | +| `source_id` | Optional — defaults to the communication type, which is a source | +| `medium_id` | Optional — defaults to the medium of the communication type | +| `campaign_id` | Optional — defaults to the campaign of the communication type | +| `sent_date` | Optional — dispatch date, defaults to the date of the import | An **empty cell is not the same as a missing column**: it sets the field to empty instead of falling back on the default of the communication type. To rely on the defaults, leave diff --git a/partner_communication/static/description/index.html b/partner_communication/static/description/index.html index 9ef327dc9..5e5a81f49 100644 --- a/partner_communication/static/description/index.html +++ b/partner_communication/static/description/index.html @@ -2,19 +2,18 @@ - + Partner Communication