mirror of https://github.com/OCA/social.git
[IMP] mass_mailing_custom_unsubscribe: GDPR compliance (#267)
* [IMP] mass_mailing_custom_unsubscribe: GDPR compliance - Record resubscriptions too. - Record action metadata. - Make ESLint happy. - Quick color-based action distinction in tree view. - Add useful quick groupings. - Display (un)subscription metadata. - Pivot & graph views.pull/595/head
parent
db13aecd8b
commit
59028ea0f3
|
@ -10,7 +10,10 @@ This addon extends the unsubscription form to let you:
|
||||||
|
|
||||||
- Choose which mailing lists are not cross-unsubscriptable when unsubscribing
|
- Choose which mailing lists are not cross-unsubscriptable when unsubscribing
|
||||||
from a different one.
|
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
|
Configuration
|
||||||
=============
|
=============
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{
|
{
|
||||||
'name': "Customizable unsubscription process on mass mailing emails",
|
'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',
|
'category': 'Marketing',
|
||||||
'version': '10.0.1.0.0',
|
'version': '10.0.2.0.0',
|
||||||
'depends': [
|
'depends': [
|
||||||
'website_mass_mailing',
|
'website_mass_mailing',
|
||||||
],
|
],
|
||||||
|
|
|
@ -45,6 +45,8 @@ class CustomUnsubscribe(MassMailController):
|
||||||
_logger.debug(
|
_logger.debug(
|
||||||
"Called `mailing()` with: %r",
|
"Called `mailing()` with: %r",
|
||||||
(mailing_id, email, res_id, token, post))
|
(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 = request.env["mail.mass_mailing"].sudo().browse(mailing_id)
|
||||||
mailing._unsubscribe_token(res_id, token)
|
mailing._unsubscribe_token(res_id, token)
|
||||||
# Mass mailing list contacts are a special case because they have a
|
# Mass mailing list contacts are a special case because they have a
|
||||||
|
@ -90,12 +92,21 @@ class CustomUnsubscribe(MassMailController):
|
||||||
token, reason_id=None, details=None):
|
token, reason_id=None, details=None):
|
||||||
"""Store unsubscription reasons when unsubscribing from RPC."""
|
"""Store unsubscription reasons when unsubscribing from RPC."""
|
||||||
# Update request context and reset environment
|
# Update request context and reset environment
|
||||||
if reason_id:
|
environ = request.httprequest.headers.environ
|
||||||
request.context = dict(
|
extra_context = {
|
||||||
request.context,
|
"default_metadata": "\n".join(
|
||||||
default_reason_id=int(reason_id),
|
"%s: %s" % (val, environ.get(val)) for val in (
|
||||||
default_details=details or False,
|
"REMOTE_ADDR",
|
||||||
|
"HTTP_USER_AGENT",
|
||||||
|
"HTTP_ACCEPT_LANGUAGE",
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if reason_id:
|
||||||
|
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:
|
# FIXME Remove token check in version where this is merged:
|
||||||
# https://github.com/odoo/odoo/pull/14385
|
# https://github.com/odoo/odoo/pull/14385
|
||||||
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
|
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
<openerp>
|
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
|
|
||||||
<record id="reason_not_interested"
|
<record id="reason_not_interested"
|
||||||
|
@ -38,4 +37,3 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</openerp>
|
|
||||||
|
|
|
@ -7,3 +7,7 @@ from openerp import exceptions
|
||||||
|
|
||||||
class DetailsRequiredError(exceptions.ValidationError):
|
class DetailsRequiredError(exceptions.ValidationError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReasonRequiredError(exceptions.ValidationError):
|
||||||
|
pass
|
||||||
|
|
|
@ -40,14 +40,24 @@ class MailMassMailing(models.Model):
|
||||||
def update_opt_out(self, email, res_ids, value):
|
def update_opt_out(self, email, res_ids, value):
|
||||||
"""Save unsubscription reason when opting out from mailing."""
|
"""Save unsubscription reason when opting out from mailing."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if value and self.env.context.get("default_reason_id"):
|
action = "unsubscription" if value else "subscription"
|
||||||
for res_id in res_ids:
|
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
|
# reason_id and details are expected from the context
|
||||||
self.env["mail.unsubscription"].create({
|
self.env["mail.unsubscription"].create({
|
||||||
"email": email,
|
"email": email,
|
||||||
"mass_mailing_id": self.id,
|
"mass_mailing_id": self.id,
|
||||||
"unsubscriber_id": "%s,%d" % (
|
"unsubscriber_id": "%s,%d" % (one._name, one.id),
|
||||||
self.mailing_model, int(res_id)),
|
"action": action,
|
||||||
})
|
})
|
||||||
return super(MailMassMailing, self).update_opt_out(
|
return super(MailMassMailing, self).update_opt_out(
|
||||||
email, res_ids, value)
|
email, res_ids, value)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# 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
|
from .. import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,12 +10,22 @@ class MailUnsubscription(models.Model):
|
||||||
_name = "mail.unsubscription"
|
_name = "mail.unsubscription"
|
||||||
_inherit = "mail.thread"
|
_inherit = "mail.thread"
|
||||||
_rec_name = "date"
|
_rec_name = "date"
|
||||||
|
_order = "date DESC"
|
||||||
|
|
||||||
date = fields.Datetime(
|
date = fields.Datetime(
|
||||||
default=lambda self: self._default_date(),
|
default=lambda self: self._default_date(),
|
||||||
required=True)
|
required=True)
|
||||||
email = fields.Char(
|
email = fields.Char(
|
||||||
required=True)
|
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(
|
mass_mailing_id = fields.Many2one(
|
||||||
"mail.mass_mailing",
|
"mail.mass_mailing",
|
||||||
"Mass mailing",
|
"Mass mailing",
|
||||||
|
@ -23,19 +33,29 @@ class MailUnsubscription(models.Model):
|
||||||
help="Mass mailing from which he was unsubscribed.")
|
help="Mass mailing from which he was unsubscribed.")
|
||||||
unsubscriber_id = fields.Reference(
|
unsubscriber_id = fields.Reference(
|
||||||
lambda self: self._selection_unsubscriber_id(),
|
lambda self: self._selection_unsubscriber_id(),
|
||||||
"Unsubscriber",
|
"(Un)subscriber",
|
||||||
required=True,
|
help="Who was subscribed or unsubscribed.")
|
||||||
help="Who was 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(
|
reason_id = fields.Many2one(
|
||||||
"mail.unsubscription.reason",
|
"mail.unsubscription.reason",
|
||||||
"Reason",
|
"Reason",
|
||||||
ondelete="restrict",
|
ondelete="restrict",
|
||||||
required=True,
|
|
||||||
help="Why the unsubscription was made.")
|
help="Why the unsubscription was made.")
|
||||||
details = fields.Char(
|
details = fields.Char(
|
||||||
help="More details on why the unsubscription was made.")
|
help="More details on why the unsubscription was made.")
|
||||||
details_required = fields.Boolean(
|
details_required = fields.Boolean(
|
||||||
related="reason_id.details_required")
|
related="reason_id.details_required")
|
||||||
|
metadata = fields.Text(
|
||||||
|
readonly=True,
|
||||||
|
help="HTTP request metadata used when creating this record.",
|
||||||
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_date(self):
|
def _default_date(self):
|
||||||
|
@ -46,6 +66,15 @@ class MailUnsubscription(models.Model):
|
||||||
"""Models that can be linked to a ``mail.mass_mailing``."""
|
"""Models that can be linked to a ``mail.mass_mailing``."""
|
||||||
return self.env["mail.mass_mailing"]._get_mailing_model()
|
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.multi
|
||||||
@api.constrains("details", "reason_id")
|
@api.constrains("details", "reason_id")
|
||||||
def _check_details_needed(self):
|
def _check_details_needed(self):
|
||||||
|
@ -55,6 +84,24 @@ class MailUnsubscription(models.Model):
|
||||||
raise exceptions.DetailsRequiredError(
|
raise exceptions.DetailsRequiredError(
|
||||||
_("Please provide details on why you are unsubscribing."))
|
_("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):
|
class MailUnsubscriptionReason(models.Model):
|
||||||
_name = "mail.unsubscription.reason"
|
_name = "mail.unsubscription.reason"
|
||||||
|
|
|
@ -5,7 +5,7 @@ odoo.define("mass_mailing_custom_unsubscribe.require_details",
|
||||||
"use strict";
|
"use strict";
|
||||||
var animation = require("web_editor.snippets.animation");
|
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({
|
animation.Class.extend({
|
||||||
selector: ".js_unsubscription_reason",
|
selector: ".js_unsubscription_reason",
|
||||||
|
|
||||||
|
@ -19,7 +19,10 @@ odoo.define("mass_mailing_custom_unsubscribe.require_details",
|
||||||
toggle: function (event) {
|
toggle: function (event) {
|
||||||
this.$details.prop(
|
this.$details.prop(
|
||||||
"required",
|
"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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
* that when it gets merged, and remove most of this file. */
|
* that when it gets merged, and remove most of this file. */
|
||||||
odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
|
odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var core = require("web.core"),
|
var core = require("web.core");
|
||||||
ajax = require("web.ajax"),
|
var ajax = require("web.ajax");
|
||||||
animation = require("web_editor.snippets.animation"),
|
var animation = require("web_editor.snippets.animation");
|
||||||
_t = core._t;
|
var _t = core._t;
|
||||||
|
|
||||||
return animation.registry.mass_mailing_unsubscribe =
|
animation.registry.mass_mailing_unsubscribe =
|
||||||
animation.Class.extend({
|
animation.Class.extend({
|
||||||
selector: "#unsubscribe_form",
|
selector: "#unsubscribe_form",
|
||||||
start: function (editable_mode) {
|
start: function () {
|
||||||
this.controller = '/mail/mailing/unsubscribe';
|
this.controller = '/mail/mailing/unsubscribe';
|
||||||
this.$alert = this.$(".alert");
|
this.$alert = this.$(".alert");
|
||||||
this.$email = this.$("input[name='email']");
|
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()
|
// Helper to get list ids, to use in this.$contacts.map()
|
||||||
int_val: function (index, element) {
|
int_val: function (index, element) {
|
||||||
return parseInt($(element).val());
|
return parseInt($(element).val(), 10);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get a filtered array of integer IDs of matching lists
|
// 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
|
// Hide reasons form if you are only subscribing
|
||||||
this.$reasons.toggleClass("hidden", !$disabled.length);
|
this.$reasons.toggleClass("hidden", !$disabled.length);
|
||||||
|
var $radios = this.$reasons.find(":radio");
|
||||||
if (this.$reasons.is(":hidden")) {
|
if (this.$reasons.is(":hidden")) {
|
||||||
// Uncheck chosen reason
|
// 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
|
// Remove possible constraints for details
|
||||||
.trigger("change");
|
.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 () {
|
values: function () {
|
||||||
var result = {
|
var result = {
|
||||||
email: this.$email.val(),
|
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_in_ids: this.contact_ids(true),
|
||||||
opt_out_ids: this.contact_ids(false),
|
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(),
|
token: this.$token.val(),
|
||||||
};
|
};
|
||||||
// Only send reason and details if an unsubscription was found
|
// Only send reason and details if an unsubscription was found
|
||||||
if (this.$reasons.is(":visible")) {
|
if (this.$reasons.is(":visible")) {
|
||||||
result.reason_id = parseInt(
|
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();
|
result.details = this.$details.val();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -108,4 +116,6 @@ odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
|
||||||
.addClass("alert-warning");
|
.addClass("alert-warning");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return animation.registry.mass_mailing_unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# 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
|
from .. import exceptions
|
||||||
|
|
||||||
|
|
||||||
class UnsubscriptionCase(TransactionCase):
|
class UnsubscriptionCase(SavepointCase):
|
||||||
def test_details_required(self):
|
def test_details_required(self):
|
||||||
"""Cannot create unsubscription without details when required."""
|
"""Cannot create unsubscription without details when required."""
|
||||||
with self.assertRaises(exceptions.DetailsRequiredError):
|
with self.assertRaises(exceptions.DetailsRequiredError):
|
||||||
|
@ -19,3 +19,13 @@ class UnsubscriptionCase(TransactionCase):
|
||||||
self.env.ref(
|
self.env.ref(
|
||||||
"mass_mailing_custom_unsubscribe.reason_other").id,
|
"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,
|
||||||
|
})
|
||||||
|
|
|
@ -14,11 +14,15 @@
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="mass_mailing_id"/>
|
<field name="mass_mailing_id"/>
|
||||||
<field name="unsubscriber_id"/>
|
<field name="unsubscriber_id"/>
|
||||||
|
<field name="mailing_list_id"/>
|
||||||
<field name="email"/>
|
<field name="email"/>
|
||||||
<field name="reason_id"/>
|
<field name="action"/>
|
||||||
|
<field name="reason_id"
|
||||||
|
attrs="{'required': [('action', '=', 'unsubscription')]}"/>
|
||||||
<field name="details"
|
<field name="details"
|
||||||
attrs="{'required': [('details_required', '=', True)]}"/>
|
attrs="{'required': [('details_required', '=', True)]}"/>
|
||||||
<field name="details_required" invisible="True"/>
|
<field name="details_required" invisible="True"/>
|
||||||
|
<field name="metadata"/>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
<div class="oe_chatter">
|
<div class="oe_chatter">
|
||||||
|
@ -36,11 +40,13 @@
|
||||||
<field name="name">Mail Unsubscription Tree</field>
|
<field name="name">Mail Unsubscription Tree</field>
|
||||||
<field name="model">mail.unsubscription</field>
|
<field name="model">mail.unsubscription</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree>
|
<tree decoration-warning="action == 'unsubscription'">
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="mass_mailing_id"/>
|
<field name="mass_mailing_id"/>
|
||||||
<field name="unsubscriber_id"/>
|
<field name="unsubscriber_id"/>
|
||||||
|
<field name="mailing_list_id"/>
|
||||||
<field name="email" invisible="True"/>
|
<field name="email" invisible="True"/>
|
||||||
|
<field name="action"/>
|
||||||
<field name="reason_id"/>
|
<field name="reason_id"/>
|
||||||
<field name="details" invisible="True"/>
|
<field name="details" invisible="True"/>
|
||||||
</tree>
|
</tree>
|
||||||
|
@ -54,6 +60,7 @@
|
||||||
<search>
|
<search>
|
||||||
<field name="mass_mailing_id"/>
|
<field name="mass_mailing_id"/>
|
||||||
<field name="unsubscriber_id"/>
|
<field name="unsubscriber_id"/>
|
||||||
|
<field name="mailing_list_id"/>
|
||||||
<field name="email"/>
|
<field name="email"/>
|
||||||
<field name="reason_id"/>
|
<field name="reason_id"/>
|
||||||
<field name="details"/>
|
<field name="details"/>
|
||||||
|
@ -63,6 +70,10 @@
|
||||||
context="{'group_by': 'date:month'}"/>
|
context="{'group_by': 'date:month'}"/>
|
||||||
<filter string="Year"
|
<filter string="Year"
|
||||||
context="{'group_by': 'date:year'}"/>
|
context="{'group_by': 'date:year'}"/>
|
||||||
|
<filter string="Action"
|
||||||
|
context="{'group_by': 'action'}"/>
|
||||||
|
<filter string="Email"
|
||||||
|
context="{'group_by': 'email'}"/>
|
||||||
<filter string="Reason"
|
<filter string="Reason"
|
||||||
context="{'group_by': 'reason_id'}"/>
|
context="{'group_by': 'reason_id'}"/>
|
||||||
<filter string="Mass mailing"
|
<filter string="Mass mailing"
|
||||||
|
@ -72,8 +83,32 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="mail_unsubscription_view_pivot">
|
||||||
|
<field name="name">Mail Unsubscription Pivot</field>
|
||||||
|
<field name="model">mail.unsubscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="(Un)subscriptions">
|
||||||
|
<field name="reason_id" type="row"/>
|
||||||
|
<field name="mailing_list_id" type="row"/>
|
||||||
|
<field name="action" type="col"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="mail_unsubscription_view_graph">
|
||||||
|
<field name="name">Mail Unsubscription Graph</field>
|
||||||
|
<field name="model">mail.unsubscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="(Un)subscriptions">
|
||||||
|
<field name="date" type="row"/>
|
||||||
|
<field name="action" type="col"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<act_window id="mail_unsubscription_action"
|
<act_window id="mail_unsubscription_action"
|
||||||
name="Unsubscriptions"
|
name="(Un)subscriptions"
|
||||||
|
view_mode="tree,form,pivot,graph"
|
||||||
res_model="mail.unsubscription"/>
|
res_model="mail.unsubscription"/>
|
||||||
|
|
||||||
<menuitem id="mail_unsubscription_menu"
|
<menuitem id="mail_unsubscription_menu"
|
||||||
|
|
Loading…
Reference in New Issue