diff --git a/mass_mailing_custom_unsubscribe/README.rst b/mass_mailing_custom_unsubscribe/README.rst
index 14efcbcd81..568d653014 100644
--- a/mass_mailing_custom_unsubscribe/README.rst
+++ b/mass_mailing_custom_unsubscribe/README.rst
@@ -10,7 +10,10 @@ This addon extends the unsubscription form to let you:
- Choose which mailing lists are not cross-unsubscriptable when unsubscribing
from a different one.
-- Know why and when a contact has been unsubscribed from a mass mailing.
+- Know why and when a contact has been subscribed or unsubscribed from a
+ mass mailing.
+- Provide proof on why you are sending mass mailings to a given contact, as
+ required by the GDPR in Europe.
Configuration
=============
diff --git a/mass_mailing_custom_unsubscribe/__manifest__.py b/mass_mailing_custom_unsubscribe/__manifest__.py
index c533f2c173..d8bbe90f9a 100644
--- a/mass_mailing_custom_unsubscribe/__manifest__.py
+++ b/mass_mailing_custom_unsubscribe/__manifest__.py
@@ -3,9 +3,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': "Customizable unsubscription process on mass mailing emails",
- "summary": "Know unsubscription reasons, track them",
+ "summary": "Know and track (un)subscription reasons, GDPR compliant",
'category': 'Marketing',
- 'version': '10.0.1.0.0',
+ 'version': '10.0.2.0.0',
'depends': [
'website_mass_mailing',
],
diff --git a/mass_mailing_custom_unsubscribe/controllers/main.py b/mass_mailing_custom_unsubscribe/controllers/main.py
index 403efeb9af..3b2157227f 100644
--- a/mass_mailing_custom_unsubscribe/controllers/main.py
+++ b/mass_mailing_custom_unsubscribe/controllers/main.py
@@ -45,6 +45,8 @@ def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
_logger.debug(
"Called `mailing()` with: %r",
(mailing_id, email, res_id, token, post))
+ if res_id:
+ res_id = int(res_id)
mailing = request.env["mail.mass_mailing"].sudo().browse(mailing_id)
mailing._unsubscribe_token(res_id, token)
# Mass mailing list contacts are a special case because they have a
@@ -90,12 +92,21 @@ def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id,
token, reason_id=None, details=None):
"""Store unsubscription reasons when unsubscribing from RPC."""
# Update request context and reset environment
+ environ = request.httprequest.headers.environ
+ extra_context = {
+ "default_metadata": "\n".join(
+ "%s: %s" % (val, environ.get(val)) for val in (
+ "REMOTE_ADDR",
+ "HTTP_USER_AGENT",
+ "HTTP_ACCEPT_LANGUAGE",
+ )
+ ),
+ }
if reason_id:
- request.context = dict(
- request.context,
- default_reason_id=int(reason_id),
- default_details=details or False,
- )
+ extra_context["default_reason_id"] = int(reason_id)
+ if details:
+ extra_context["default_details"] = details
+ request.context = dict(request.context, **extra_context)
# FIXME Remove token check in version where this is merged:
# https://github.com/odoo/odoo/pull/14385
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
diff --git a/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml b/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml
index 4335f9b77f..24a6eda30e 100644
--- a/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml
+++ b/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml
@@ -2,7 +2,6 @@
-
-
diff --git a/mass_mailing_custom_unsubscribe/exceptions.py b/mass_mailing_custom_unsubscribe/exceptions.py
index 195b619874..bc9bbcc441 100644
--- a/mass_mailing_custom_unsubscribe/exceptions.py
+++ b/mass_mailing_custom_unsubscribe/exceptions.py
@@ -7,3 +7,7 @@
class DetailsRequiredError(exceptions.ValidationError):
pass
+
+
+class ReasonRequiredError(exceptions.ValidationError):
+ pass
diff --git a/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
index 62fcf04daf..2eec3d4a8c 100644
--- a/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
+++ b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
@@ -40,14 +40,24 @@ def _unsubscribe_token(self, res_id, compare=None):
def update_opt_out(self, email, res_ids, value):
"""Save unsubscription reason when opting out from mailing."""
self.ensure_one()
- if value and self.env.context.get("default_reason_id"):
- for res_id in res_ids:
+ action = "unsubscription" if value else "subscription"
+ records = self.env[self.mailing_model].browse(res_ids)
+ previous = self.env["mail.unsubscription"].search(limit=1, args=[
+ ("mass_mailing_id", "=", self.id),
+ ("email", "=", email),
+ ("action", "=", action),
+ ])
+ for one in records:
+ # Store action only when something changed, or there was no
+ # previous subscription record
+ if one.opt_out != value or (action == "subscription" and
+ not previous):
# reason_id and details are expected from the context
self.env["mail.unsubscription"].create({
"email": email,
"mass_mailing_id": self.id,
- "unsubscriber_id": "%s,%d" % (
- self.mailing_model, int(res_id)),
+ "unsubscriber_id": "%s,%d" % (one._name, one.id),
+ "action": action,
})
return super(MailMassMailing, self).update_opt_out(
email, res_ids, value)
diff --git a/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py b/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
index 83999bcb18..148ae9da8a 100644
--- a/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
+++ b/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
@@ -2,7 +2,7 @@
# Copyright 2016 Jairo Llopis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from openerp import _, api, fields, models
+from odoo import _, api, fields, models
from .. import exceptions
@@ -10,12 +10,22 @@ class MailUnsubscription(models.Model):
_name = "mail.unsubscription"
_inherit = "mail.thread"
_rec_name = "date"
+ _order = "date DESC"
date = fields.Datetime(
default=lambda self: self._default_date(),
required=True)
email = fields.Char(
required=True)
+ action = fields.Selection(
+ selection=[
+ ("subscription", "Subscription"),
+ ("unsubscription", "Unsubscription"),
+ ],
+ required=True,
+ default="unsubscription",
+ help="What did the (un)subscriber choose to do.",
+ )
mass_mailing_id = fields.Many2one(
"mail.mass_mailing",
"Mass mailing",
@@ -23,19 +33,29 @@ class MailUnsubscription(models.Model):
help="Mass mailing from which he was unsubscribed.")
unsubscriber_id = fields.Reference(
lambda self: self._selection_unsubscriber_id(),
- "Unsubscriber",
- required=True,
- help="Who was unsubscribed.")
+ "(Un)subscriber",
+ help="Who was subscribed or unsubscribed.")
+ mailing_list_id = fields.Many2one(
+ "mail.mass_mailing.list",
+ "Mailing list",
+ ondelete="set null",
+ compute="_compute_mailing_list_id",
+ store=True,
+ help="(Un)subscribed mass mailing list, if any.",
+ )
reason_id = fields.Many2one(
"mail.unsubscription.reason",
"Reason",
ondelete="restrict",
- required=True,
help="Why the unsubscription was made.")
details = fields.Char(
help="More details on why the unsubscription was made.")
details_required = fields.Boolean(
related="reason_id.details_required")
+ metadata = fields.Text(
+ readonly=True,
+ help="HTTP request metadata used when creating this record.",
+ )
@api.model
def _default_date(self):
@@ -46,6 +66,15 @@ def _selection_unsubscriber_id(self):
"""Models that can be linked to a ``mail.mass_mailing``."""
return self.env["mail.mass_mailing"]._get_mailing_model()
+ @api.multi
+ @api.constrains("action", "reason_id")
+ def _check_reason_needed(self):
+ """Ensure reason is given for unsubscriptions."""
+ for one in self:
+ if one.action == "unsubscription" and not one.reason_id:
+ raise exceptions.ReasonRequiredError(
+ _("Please indicate why are you unsubscribing."))
+
@api.multi
@api.constrains("details", "reason_id")
def _check_details_needed(self):
@@ -55,6 +84,24 @@ def _check_details_needed(self):
raise exceptions.DetailsRequiredError(
_("Please provide details on why you are unsubscribing."))
+ @api.multi
+ @api.depends("unsubscriber_id")
+ def _compute_mailing_list_id(self):
+ """Get the mass mailing list, if it is possible."""
+ for one in self:
+ try:
+ one.mailing_list_id = one.unsubscriber_id.list_id
+ except AttributeError:
+ # Possibly model != mail.mass_mailing.contact; no problem
+ pass
+
+ @api.model
+ def create(self, vals):
+ # No reasons for subscriptions
+ if vals.get("action") == "subscription":
+ vals = dict(vals, reason_id=False, details=False)
+ return super(MailUnsubscription, self).create(vals)
+
class MailUnsubscriptionReason(models.Model):
_name = "mail.unsubscription.reason"
diff --git a/mass_mailing_custom_unsubscribe/static/src/js/require_details.js b/mass_mailing_custom_unsubscribe/static/src/js/require_details.js
index 6df78b7566..6c778c68a8 100644
--- a/mass_mailing_custom_unsubscribe/static/src/js/require_details.js
+++ b/mass_mailing_custom_unsubscribe/static/src/js/require_details.js
@@ -5,7 +5,7 @@ odoo.define("mass_mailing_custom_unsubscribe.require_details",
"use strict";
var animation = require("web_editor.snippets.animation");
- return animation.registry.mass_mailing_custom_unsubscribe_require_details =
+ animation.registry.mass_mailing_custom_unsubscribe_require_details =
animation.Class.extend({
selector: ".js_unsubscription_reason",
@@ -19,7 +19,10 @@ odoo.define("mass_mailing_custom_unsubscribe.require_details",
toggle: function (event) {
this.$details.prop(
"required",
- $(event.target).is("[data-details-required]"));
+ $(event.target).is("[data-details-required]") &&
+ $(event.target).is(":visible"));
},
});
+
+ return animation.registry.mass_mailing_custom_unsubscribe_require_details;
});
diff --git a/mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js b/mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
index 49f92b6dc1..12ec3a05ab 100644
--- a/mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
+++ b/mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
@@ -7,15 +7,15 @@
* that when it gets merged, and remove most of this file. */
odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
"use strict";
- var core = require("web.core"),
- ajax = require("web.ajax"),
- animation = require("web_editor.snippets.animation"),
- _t = core._t;
+ var core = require("web.core");
+ var ajax = require("web.ajax");
+ var animation = require("web_editor.snippets.animation");
+ var _t = core._t;
- return animation.registry.mass_mailing_unsubscribe =
+ animation.registry.mass_mailing_unsubscribe =
animation.Class.extend({
selector: "#unsubscribe_form",
- start: function (editable_mode) {
+ start: function () {
this.controller = '/mail/mailing/unsubscribe';
this.$alert = this.$(".alert");
this.$email = this.$("input[name='email']");
@@ -32,7 +32,7 @@ odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
// Helper to get list ids, to use in this.$contacts.map()
int_val: function (index, element) {
- return parseInt($(element).val());
+ return parseInt($(element).val(), 10);
},
// Get a filtered array of integer IDs of matching lists
@@ -50,11 +50,17 @@ odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
});
// Hide reasons form if you are only subscribing
this.$reasons.toggleClass("hidden", !$disabled.length);
+ var $radios = this.$reasons.find(":radio");
if (this.$reasons.is(":hidden")) {
// Uncheck chosen reason
- this.$reasons.find(":radio").prop("checked", false)
+ $radios.prop("checked", false)
+ // Unrequire specifying a reason
+ .prop("required", false)
// Remove possible constraints for details
.trigger("change");
+ } else {
+ // Require specifying a reason
+ $radios.prop("required", true);
}
},
@@ -62,16 +68,18 @@ odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
values: function () {
var result = {
email: this.$email.val(),
- mailing_id: parseInt(this.$mailing_id.val()),
+ mailing_id: parseInt(this.$mailing_id.val(), 10),
opt_in_ids: this.contact_ids(true),
opt_out_ids: this.contact_ids(false),
- res_id: parseInt(this.$res_id.val()),
+ res_id: parseInt(this.$res_id.val(), 10),
token: this.$token.val(),
};
// Only send reason and details if an unsubscription was found
if (this.$reasons.is(":visible")) {
result.reason_id = parseInt(
- this.$reasons.find("[name='reason_id']:checked").val());
+ this.$reasons.find("[name='reason_id']:checked").val(),
+ 10
+ );
result.details = this.$details.val();
}
return result;
@@ -108,4 +116,6 @@ odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
.addClass("alert-warning");
},
});
+
+ return animation.registry.mass_mailing_unsubscribe;
});
diff --git a/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py b/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py
index 06c4750769..b6d7043ac9 100644
--- a/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py
+++ b/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py
@@ -2,11 +2,11 @@
# Copyright 2016 Jairo Llopis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from openerp.tests.common import TransactionCase
+from openerp.tests.common import SavepointCase
from .. import exceptions
-class UnsubscriptionCase(TransactionCase):
+class UnsubscriptionCase(SavepointCase):
def test_details_required(self):
"""Cannot create unsubscription without details when required."""
with self.assertRaises(exceptions.DetailsRequiredError):
@@ -19,3 +19,13 @@ def test_details_required(self):
self.env.ref(
"mass_mailing_custom_unsubscribe.reason_other").id,
})
+
+ def test_reason_required(self):
+ """Cannot create unsubscription without reason when required."""
+ with self.assertRaises(exceptions.ReasonRequiredError):
+ self.env["mail.unsubscription"].create({
+ "email": "axelor@yourcompany.example.com",
+ "mass_mailing_id": self.env.ref("mass_mailing.mass_mail_1").id,
+ "unsubscriber_id":
+ "res.partner,%d" % self.env.ref("base.res_partner_2").id,
+ })
diff --git a/mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml b/mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
index da8f23ca69..177b5598cd 100644
--- a/mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
+++ b/mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
@@ -14,11 +14,15 @@
+
-
+
+
+
@@ -36,11 +40,13 @@
Mail Unsubscription Tree
mail.unsubscription
-
+
+
+
@@ -54,6 +60,7 @@
+
@@ -63,6 +70,10 @@
context="{'group_by': 'date:month'}"/>
+
+
+
+ Mail Unsubscription Pivot
+ mail.unsubscription
+
+
+
+
+
+
+
+
+
+
+ Mail Unsubscription Graph
+ mail.unsubscription
+
+
+
+
+
+
+
+