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
82 changes: 82 additions & 0 deletions partner_communication/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========

Expand Down
2 changes: 1 addition & 1 deletion partner_communication/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 14 additions & 4 deletions partner_communication/models/communication_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -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:
Expand Down
122 changes: 95 additions & 27 deletions partner_communication/models/communication_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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",
]
)

Expand Down
49 changes: 49 additions & 0 deletions partner_communication/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading