[REF] mass_mailing: move mailing capability on model
PURPOSE Remove hardcoded list of models on which mailing is possible. Indeed it is not modular and not really smart with enterprise code not being reachable in community. SPECIFICATIONS Replace hardcoded list of models on which mailing (both mail or sms) is possible by a computed searchable field on ``ir.model`` based on a class attribute. It allows to cleanly define models having mass mailing capabilities and add this attribute in bridge modules (when existing) or directly on base model definition to avoid bridge modules. In this commit we introduce a basic ``_mailing_enabled`` class attribute activating mailing on model. Mailing models may also have a ``_mailing_get_default_domain`` method allowing to define a custom default domain when sending a marketing mailing on records on this class. Mailing models can now define a ``_mailing_get_opt_out_list(_sms)`` method allowing to define custom behavior to fetch opt-outed records. Instead of defining a model-based behavior on Mailing itself, it now calls the model defined one. We still have two methods, one for mailing and one for SMS opt out computation as it relies on different underlying models and fields. LINKS Task ID-2431217 COM PR odoo/odoo#67322 ENT PR odoo/enterprise#16876 UPG PR odoo/upgrade#2236
This commit is contained in:
@@ -95,6 +95,7 @@ class Applicant(models.Model):
|
||||
_description = "Applicant"
|
||||
_order = "priority desc, id desc"
|
||||
_inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin']
|
||||
_mailing_enabled = True
|
||||
|
||||
name = fields.Char("Subject / Application Name", required=True, help="Email subject for applications sent via email")
|
||||
active = fields.Boolean("Active", default=True, help="If the active field is set to false, it will allow you to hide the case without removing it.")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_model
|
||||
from . import link_tracker
|
||||
from . import mailing_contact
|
||||
from . import mailing_list
|
||||
@@ -10,6 +11,7 @@ from . import mail_mail
|
||||
from . import mail_render_mixin
|
||||
from . import mail_thread
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import utm
|
||||
from . import res_company
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = 'ir.model'
|
||||
|
||||
is_mailing_enabled = fields.Boolean(
|
||||
string="Mailing Enabled",
|
||||
compute='_compute_is_mailing_enabled', search='_search_is_mailing_enabled',
|
||||
help="Whether this model supports marketing mailing capabilities (notably email and SMS).",
|
||||
)
|
||||
|
||||
def _compute_is_mailing_enabled(self):
|
||||
for model in self:
|
||||
model.is_mailing_enabled = getattr(self.env[model.model], '_mailing_enabled', False)
|
||||
|
||||
def _search_is_mailing_enabled(self, operator, value):
|
||||
if operator not in ('=', '!='):
|
||||
raise ValueError(_("Searching Mailing Enabled models supports only direct search using '='' or '!='."))
|
||||
|
||||
valid_models = self.env['ir.model']
|
||||
for model in self.search([]):
|
||||
if model.model not in self.env or model.is_transient():
|
||||
continue
|
||||
if getattr(self.env[model.model], '_mailing_enabled', False):
|
||||
valid_models |= model
|
||||
|
||||
search_is_mailing_enabled = (operator == '=' and value) or (operator == '!=' and not value)
|
||||
if search_is_mailing_enabled:
|
||||
return [('id', 'in', valid_models.ids)]
|
||||
return [('id', 'not in', valid_models.ids)]
|
||||
@@ -10,7 +10,6 @@ import re
|
||||
import threading
|
||||
import werkzeug.urls
|
||||
from ast import literal_eval
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from werkzeug.urls import url_join
|
||||
|
||||
@@ -20,17 +19,6 @@ from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
MASS_MAILING_BUSINESS_MODELS = [
|
||||
'crm.lead',
|
||||
'event.registration',
|
||||
'hr.applicant',
|
||||
'res.partner',
|
||||
'event.track',
|
||||
'sale.order',
|
||||
'mailing.list',
|
||||
'mailing.contact'
|
||||
]
|
||||
|
||||
# Syntax of the data URL Scheme: https://tools.ietf.org/html/rfc2397#section-3
|
||||
# Used to find inline images
|
||||
image_re = re.compile(r"data:(image/[A-Za-z]+);base64,(.*)")
|
||||
@@ -128,7 +116,7 @@ class MassMailing(models.Model):
|
||||
mailing_model_real = fields.Char(string='Recipients Real Model', compute='_compute_model')
|
||||
mailing_model_id = fields.Many2one(
|
||||
'ir.model', string='Recipients Model', ondelete='cascade', required=True,
|
||||
domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
|
||||
domain=[('is_mailing_enabled', '=', True)],
|
||||
default=lambda self: self.env.ref('mass_mailing.model_mailing_list').id)
|
||||
mailing_model_name = fields.Char(
|
||||
string='Recipients Model Name', related='mailing_model_id.model',
|
||||
@@ -485,20 +473,16 @@ class MassMailing(models.Model):
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _get_opt_out_list(self):
|
||||
"""Returns a set of emails opted-out in target model"""
|
||||
""" Give list of opt-outed emails, depending on specific model-based
|
||||
computation if available.
|
||||
|
||||
:return list: opt-outed emails, preferably normalized (aka not records)
|
||||
"""
|
||||
self.ensure_one()
|
||||
opt_out = {}
|
||||
target = self.env[self.mailing_model_real]
|
||||
if self.mailing_model_real == "mailing.contact":
|
||||
# if user is opt_out on One list but not on another
|
||||
# or if two user with same email address, one opted in and the other one opted out, send the mail anyway
|
||||
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
|
||||
target_list_contacts = self.env['mailing.contact.subscription'].search(
|
||||
[('list_id', 'in', self.contact_list_ids.ids)])
|
||||
opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized')
|
||||
opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized')
|
||||
opt_out = set(c for c in opt_out_contacts if c not in opt_in_contacts)
|
||||
|
||||
if hasattr(self.env[self.mailing_model_name], '_mailing_get_opt_out_list'):
|
||||
opt_out = self.env[self.mailing_model_name]._mailing_get_opt_out_list(self)
|
||||
_logger.info(
|
||||
"Mass-mailing %s targets %s, blacklist: %s emails",
|
||||
self, target._name, len(opt_out))
|
||||
@@ -848,8 +832,8 @@ class MassMailing(models.Model):
|
||||
|
||||
def _get_default_mailing_domain(self):
|
||||
mailing_domain = []
|
||||
if self.mailing_model_name == 'mailing.list' and self.contact_list_ids:
|
||||
mailing_domain = [('list_ids', 'in', self.contact_list_ids.ids)]
|
||||
if hasattr(self.env[self.mailing_model_name], '_mailing_get_default_domain'):
|
||||
mailing_domain = self.env[self.mailing_model_name]._mailing_get_default_domain(self)
|
||||
|
||||
if self.mailing_type == 'mail' and 'is_blacklisted' in self.env[self.mailing_model_name]._fields:
|
||||
mailing_domain = expression.AND([[('is_blacklisted', '=', False)], mailing_domain])
|
||||
|
||||
@@ -54,6 +54,7 @@ class MassMailingContact(models.Model):
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_description = 'Mailing Contact'
|
||||
_order = 'email'
|
||||
_mailing_enabled = True
|
||||
|
||||
def default_get(self, fields):
|
||||
""" When coming from a mailing list we may have a default_list_ids context
|
||||
|
||||
@@ -10,6 +10,7 @@ class MassMailingList(models.Model):
|
||||
_name = 'mailing.list'
|
||||
_order = 'name'
|
||||
_description = 'Mailing List'
|
||||
_mailing_enabled = True
|
||||
|
||||
name = fields.Char(string='Mailing List', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
@@ -224,6 +225,24 @@ class MassMailingList(models.Model):
|
||||
def close_dialog(self):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
# ------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _mailing_get_default_domain(self, mailing):
|
||||
return [('list_ids', 'in', mailing.contact_list_ids.ids)]
|
||||
|
||||
def _mailing_get_opt_out_list(self, mailing):
|
||||
""" Check subscription on all involved mailing lists. If user is opt_out
|
||||
on one list but not on another if two users with same email address, one
|
||||
opted in and the other one opted out, send the mail anyway. """
|
||||
# TODO DBE Fixme : Optimize the following to get real opt_out and opt_in
|
||||
subscriptions = self.subscription_ids if self else mailing.contact_list_ids.subscription_ids
|
||||
opt_out_contacts = subscriptions.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized')
|
||||
opt_in_contacts = subscriptions.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized')
|
||||
opt_out = set(c for c in opt_out_contacts if c not in opt_in_contacts)
|
||||
return opt_out
|
||||
|
||||
# ------------------------------------------------------
|
||||
# UTILITY
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_mailing_enabled = True
|
||||
@@ -92,7 +92,7 @@ class TestMassMailValues(MassMailCommon):
|
||||
self.assertEqual(mailing.reply_to_mode, 'new')
|
||||
self.assertEqual(mailing.reply_to, self.email_reply_to)
|
||||
# default for mailing list: depends upon contact_list_ids
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [])
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('list_ids', 'in', [])])
|
||||
mailing.write({
|
||||
'contact_list_ids': [(4, self.mailing_list_1.id), (4, self.mailing_list_2.id)]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import crm_lead
|
||||
from . import mailing_mailing
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class CrmLead(models.Model):
|
||||
_inherit = 'crm.lead'
|
||||
_mailing_enabled = True
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event
|
||||
from . import event_event
|
||||
from . import event_registration
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class EventRegistration(models.Model):
|
||||
_inherit = 'event.registration'
|
||||
_mailing_enabled = True
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import event
|
||||
from . import event_event
|
||||
from . import event_track
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class EventTrack(models.Model):
|
||||
_inherit = 'event.track'
|
||||
_mailing_enabled = True
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mailing_mailing
|
||||
from . import sale_order
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
_mailing_enabled = True
|
||||
@@ -51,3 +51,15 @@ class MailingList(models.Model):
|
||||
return super(MailingList, self)._get_contact_statistics_joins() + '''
|
||||
LEFT JOIN phone_blacklist bl_sms ON c.phone_sanitized = bl_sms.number and bl_sms.active
|
||||
'''
|
||||
|
||||
def _mailing_get_opt_out_list_sms(self, mailing):
|
||||
""" Check subscription on all involved mailing lists. If user is opt_out
|
||||
on one list but not on another, one opted in and the other one opted out,
|
||||
send mailing anyway.
|
||||
|
||||
:return list: opt-outed record IDs
|
||||
"""
|
||||
subscriptions = self.subscription_ids if self else mailing.contact_list_ids.subscription_ids
|
||||
opt_out_contacts = subscriptions.filtered(lambda sub: sub.opt_out).mapped('contact_id')
|
||||
opt_in_contacts = subscriptions.filtered(lambda sub: not sub.opt_out).mapped('contact_id')
|
||||
return list(set(c.id for c in opt_out_contacts if c not in opt_in_contacts))
|
||||
|
||||
@@ -145,20 +145,16 @@ class Mailing(models.Model):
|
||||
# --------------------------------------------------
|
||||
|
||||
def _get_opt_out_list_sms(self):
|
||||
"""Returns a set of emails opted-out in target model"""
|
||||
""" Give list of opt-outed records, depending on specific model-based
|
||||
computation if available.
|
||||
|
||||
:return list: opt-outed record IDs
|
||||
"""
|
||||
self.ensure_one()
|
||||
opt_out = []
|
||||
target = self.env[self.mailing_model_real]
|
||||
if self.mailing_model_real == 'mailing.contact':
|
||||
# if user is opt_out on One list but not on another
|
||||
# or if two user with same email address, one opted in and the other one opted out, send the mail anyway
|
||||
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
|
||||
subscriptions = self.env['mailing.contact.subscription'].sudo().search(
|
||||
[('list_id', 'in', self.contact_list_ids.ids)])
|
||||
opt_out_contacts = subscriptions.filtered(lambda sub: sub.opt_out).mapped('contact_id')
|
||||
opt_in_contacts = subscriptions.filtered(lambda sub: not sub.opt_out).mapped('contact_id')
|
||||
opt_out = list(set(c.id for c in opt_out_contacts if c not in opt_in_contacts))
|
||||
|
||||
if hasattr(self.env[self.mailing_model_name], '_mailing_get_opt_out_list_sms'):
|
||||
opt_out = self.env[self.mailing_model_name]._mailing_get_opt_out_list_sms(self)
|
||||
_logger.info("Mass SMS %s targets %s: optout: %s contacts", self, target._name, len(opt_out))
|
||||
else:
|
||||
_logger.info("Mass SMS %s targets %s: no opt out list available", self, target._name)
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestMassMailValues(MassSMSCommon):
|
||||
self.assertEqual(mailing.mailing_model_name, 'mailing.list')
|
||||
self.assertEqual(mailing.mailing_model_real, 'mailing.contact')
|
||||
# default for mailing list: depends upon contact_list_ids
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [])
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('list_ids', 'in', [])])
|
||||
mailing.write({
|
||||
'contact_list_ids': [(4, self.mailing_list_1.id), (4, self.mailing_list_2.id)]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_mail_models
|
||||
from . import mailing_mailing
|
||||
|
||||
@@ -10,6 +10,7 @@ class MailTestSMS(models.Model):
|
||||
_description = 'Chatter Model for SMS Gateway'
|
||||
_name = 'mail.test.sms'
|
||||
_inherit = ['mail.thread']
|
||||
_mailing_enabled = True
|
||||
_order = 'name asc, id asc'
|
||||
|
||||
name = fields.Char()
|
||||
@@ -32,6 +33,7 @@ class MailTestSMSBL(models.Model):
|
||||
_description = 'SMS Mailing Blacklist Enabled'
|
||||
_name = 'mail.test.sms.bl'
|
||||
_inherit = ['mail.thread.phone']
|
||||
_mailing_enabled = True
|
||||
_order = 'name asc, id asc'
|
||||
|
||||
name = fields.Char()
|
||||
@@ -54,6 +56,7 @@ class MailTestSMSOptout(models.Model):
|
||||
_description = 'SMS Mailing Blacklist / Optout Enabled'
|
||||
_name = 'mail.test.sms.bl.optout'
|
||||
_inherit = ['mail.thread.phone']
|
||||
_mailing_enabled = True
|
||||
_order = 'name asc, id asc'
|
||||
|
||||
name = fields.Char()
|
||||
@@ -64,6 +67,13 @@ class MailTestSMSOptout(models.Model):
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
opt_out = fields.Boolean()
|
||||
|
||||
def _mailing_get_opt_out_list_sms(self, mailing):
|
||||
res_ids = mailing._get_recipients()
|
||||
return self.search([
|
||||
('id', 'in', res_ids),
|
||||
('opt_out', '=', True)
|
||||
]).ids
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
@@ -78,11 +88,19 @@ class MailTestSMSPartner(models.Model):
|
||||
_description = 'Chatter Model for SMS Gateway (Partner only)'
|
||||
_name = 'mail.test.sms.partner'
|
||||
_inherit = ['mail.thread']
|
||||
_mailing_enabled = True
|
||||
|
||||
name = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
opt_out = fields.Boolean()
|
||||
|
||||
def _mailing_get_opt_out_list_sms(self, mailing):
|
||||
res_ids = mailing._get_recipients()
|
||||
return self.search([
|
||||
('id', 'in', res_ids),
|
||||
('opt_out', '=', True)
|
||||
]).ids
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
return ['customer_id']
|
||||
|
||||
@@ -96,11 +114,19 @@ class MailTestSMSPartner2Many(models.Model):
|
||||
_description = 'Chatter Model for SMS Gateway (M2M Partners only)'
|
||||
_name = 'mail.test.sms.partner.2many'
|
||||
_inherit = ['mail.thread']
|
||||
_mailing_enabled = True
|
||||
|
||||
name = fields.Char()
|
||||
customer_ids = fields.Many2many('res.partner', string='Customers')
|
||||
opt_out = fields.Boolean()
|
||||
|
||||
def _mailing_get_opt_out_list_sms(self, mailing):
|
||||
res_ids = mailing._get_recipients()
|
||||
return self.search([
|
||||
('id', 'in', res_ids),
|
||||
('opt_out', '=', True)
|
||||
]).ids
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
return ['customer_ids']
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mailing_mailing
|
||||
from . import mailing_models
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailing(models.Model):
|
||||
_inherit = 'mailing.mailing'
|
||||
|
||||
def _get_opt_out_list(self):
|
||||
"""Returns a set of emails opted-out in target model"""
|
||||
self.ensure_one()
|
||||
if self.mailing_model_real == 'mailing.test.optout':
|
||||
res_ids = self._get_recipients()
|
||||
opt_out_contacts = set(self.env['mailing.test.optout'].search([
|
||||
('id', 'in', res_ids),
|
||||
('opt_out', '=', True)
|
||||
]).mapped('email_normalized'))
|
||||
_logger.info(
|
||||
"Mass-mailing %s targets %s, optout: %s emails",
|
||||
self, self.mailing_model_real, len(opt_out_contacts))
|
||||
return opt_out_contacts
|
||||
return super(Mailing, self)._get_opt_out_list()
|
||||
@@ -51,6 +51,14 @@ class MailingOptOut(models.Model):
|
||||
customer_id = fields.Many2one('res.partner', 'Customer', tracking=True)
|
||||
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
|
||||
|
||||
def _mailing_get_opt_out_list(self, mailing):
|
||||
res_ids = mailing._get_recipients()
|
||||
opt_out_contacts = set(self.search([
|
||||
('id', 'in', res_ids),
|
||||
('opt_out', '=', True)
|
||||
]).mapped('email_normalized'))
|
||||
return opt_out_contacts
|
||||
|
||||
|
||||
class MailingPerformance(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread to test pure mass
|
||||
|
||||
@@ -303,10 +303,13 @@ class TestMassMailing(TestMassMailCommon):
|
||||
# contact_1 is optout but same email is not optout from the same list
|
||||
# contact 3 is optout in list 1 but not in list 2
|
||||
# contact 5 is optout
|
||||
Sub = self.env['mailing.contact.subscription']
|
||||
Sub.search([('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id)]).write({'opt_out': True})
|
||||
Sub.search([('contact_id', '=', mailing_contact_3.id), ('list_id', '=', mailing_list_1.id)]).write({'opt_out': True})
|
||||
Sub.search([('contact_id', '=', mailing_contact_5.id), ('list_id', '=', mailing_list_1.id)]).write({'opt_out': True})
|
||||
subs = self.env['mailing.contact.subscription'].search([
|
||||
'|', '|',
|
||||
'&', ('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id),
|
||||
'&', ('contact_id', '=', mailing_contact_3.id), ('list_id', '=', mailing_list_1.id),
|
||||
'&', ('contact_id', '=', mailing_contact_5.id), ('list_id', '=', mailing_list_1.id)
|
||||
])
|
||||
subs.write({'opt_out': True})
|
||||
|
||||
# create mass mailing record
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
|
||||
Reference in New Issue
Block a user