Maintainers
+Maintainers
This module is part of the CompassionCH/compassion-modules project on GitHub.
You are welcome to contribute.
diff --git a/partner_communication/README.rst b/partner_communication/README.rst index ecac055bc..2f04c0927 100644 --- a/partner_communication/README.rst +++ b/partner_communication/README.rst @@ -32,6 +32,88 @@ 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. For the +same reason an imported line is never merged into an existing +communication, which may already be waiting to be sent: every line keeps +its own record. + +Once imported, the communication list groups by Campaign, Medium and +Source. + Bug Tracker =========== diff --git a/partner_communication/__manifest__.py b/partner_communication/__manifest__.py index b0d020061..44b156191 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.1", + "version": "18.0.1.0.2", "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 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 ab5da275e..dfea071c5 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") @@ -343,36 +351,21 @@ 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. It never merges either, so that it cannot touch a + # communication that already has a send task queued. + 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) @@ -399,6 +392,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() @@ -425,6 +423,73 @@ 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: nothing may be sent because of this creation, so never merge: + an existing communication may already have a send task queued, + which would then deliver these values along with it. + :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() + + if no_send: + return self.browse() + + return self._merge_into_pending_job(vals) + + def _merge_into_pending_job(self, vals): + """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 + :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: + job.send() + return job + @api.model def _get_dynamic_user(self, config, object_ids_str): """ @@ -484,6 +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", ] ) diff --git a/partner_communication/readme/USAGE.md b/partner_communication/readme/USAGE.md new file mode 100644 index 000000000..1d851effc --- /dev/null +++ b/partner_communication/readme/USAGE.md @@ -0,0 +1,49 @@ +## 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. For the same reason an +imported line is never merged into an existing communication, which may already be waiting +to be sent: every line keeps its own record. + +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..9ef327dc9 100644 --- a/partner_communication/static/description/index.html +++ b/partner_communication/static/description/index.html @@ -377,16 +377,115 @@
Table of contents
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.
+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. For the +same reason an imported line is never merged into an existing +communication, which may already be waiting to be sent: every line keeps +its own record.
+Once imported, the communication list groups by Campaign, Medium and +Source.
+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 +493,15 @@
Do not contact contributors directly about support or help with technical issues.
This module is part of the CompassionCH/compassion-modules project on GitHub.
You are welcome to contribute.