# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models, tools from odoo.exceptions import UserError from odoo.osv import expression class MassMailingContactListRel(models.Model): """ Intermediate model between mass mailing list and mass mailing contact Indicates if a contact is opted out for a particular list """ _name = 'mailing.contact.subscription' _description = 'Mass Mailing Subscription Information' _table = 'mailing_contact_list_rel' _rec_name = 'contact_id' contact_id = fields.Many2one('mailing.contact', string='Contact', ondelete='cascade', required=True) list_id = fields.Many2one('mailing.list', string='Mailing List', ondelete='cascade', required=True) opt_out = fields.Boolean(string='Opt Out', help='The contact has chosen not to receive mails anymore from this list', default=False) unsubscription_date = fields.Datetime(string='Unsubscription Date') message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False) is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False) _sql_constraints = [ ('unique_contact_list', 'unique (contact_id, list_id)', 'A mailing contact cannot subscribe to the same mailing list multiple times.') ] @api.model_create_multi def create(self, vals_list): now = fields.Datetime.now() for vals in vals_list: if 'opt_out' in vals and not vals.get('unsubscription_date'): vals['unsubscription_date'] = now if vals['opt_out'] else False if vals.get('unsubscription_date'): vals['opt_out'] = True return super().create(vals_list) def write(self, vals): if 'opt_out' in vals and 'unsubscription_date' not in vals: vals['unsubscription_date'] = fields.Datetime.now() if vals['opt_out'] else False if vals.get('unsubscription_date'): vals['opt_out'] = True return super(MassMailingContactListRel, self).write(vals) class MassMailingContact(models.Model): """Model of a contact. This model is different from the partner model because it holds only some basic information: name, email. The purpose is to be able to deal with large contact list to email without bloating the partner base.""" _name = 'mailing.contact' _inherit = ['mail.thread.blacklist'] _description = 'Mailing Contact' _order = 'email' _mailing_enabled = True def default_get(self, fields_list): """ When coming from a mailing list we may have a default_list_ids context key. We should use it to create subscription_list_ids default value that are displayed to the user as list_ids is not displayed on form view. """ res = super(MassMailingContact, self).default_get(fields_list) if 'subscription_list_ids' in fields_list and not res.get('subscription_list_ids'): list_ids = self.env.context.get('default_list_ids') if 'default_list_ids' not in res and list_ids and isinstance(list_ids, (list, tuple)): res['subscription_list_ids'] = [ (0, 0, {'list_id': list_id}) for list_id in list_ids] return res name = fields.Char() company_name = fields.Char(string='Company Name') title_id = fields.Many2one('res.partner.title', string='Title') email = fields.Char('Email') list_ids = fields.Many2many( 'mailing.list', 'mailing_contact_list_rel', 'contact_id', 'list_id', string='Mailing Lists') subscription_list_ids = fields.One2many( 'mailing.contact.subscription', 'contact_id', string='Subscription Information') country_id = fields.Many2one('res.country', string='Country') tag_ids = fields.Many2many('res.partner.category', string='Tags') opt_out = fields.Boolean( 'Opt Out', compute='_compute_opt_out', search='_search_opt_out', help='Opt out flag for a specific mailing list. ' 'This field should not be used in a view without a unique and active mailing list context.') @api.model def _search_opt_out(self, operator, value): # Assumes operator is '=' or '!=' and value is True or False if operator != '=': if operator == '!=' and isinstance(value, bool): value = not value else: raise NotImplementedError() if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: [active_list_id] = self._context['default_list_ids'] contacts = self.env['mailing.contact.subscription'].search([('list_id', '=', active_list_id)]) return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] return expression.FALSE_DOMAIN if value else expression.TRUE_DOMAIN @api.depends('subscription_list_ids') @api.depends_context('default_list_ids') def _compute_opt_out(self): if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: [active_list_id] = self._context['default_list_ids'] for record in self: active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id) record.opt_out = active_subscription_list.opt_out else: for record in self: record.opt_out = False def get_name_email(self, name): name, email = self.env['res.partner']._parse_partner_name(name) if name and not email: email = name if email and not name: name = email return name, email @api.model_create_multi def create(self, vals_list): """ Synchronize default_list_ids (currently used notably for computed fields) default key with subscription_list_ids given by user when creating contacts. Those two values have the same purpose, adding a list to to the contact either through a direct write on m2m, either through a write on middle model subscription. This is a bit hackish but is due to default_list_ids key being used to compute oupt_out field. This should be cleaned in master but here we simply try to limit issues while keeping current behavior. """ default_list_ids = self._context.get('default_list_ids') default_list_ids = default_list_ids if isinstance(default_list_ids, (list, tuple)) else [] for vals in vals_list: if vals.get('list_ids') and vals.get('subscription_list_ids'): raise UserError(_('You should give either list_ids, either subscription_list_ids to create new contacts.')) if default_list_ids: for vals in vals_list: if vals.get('list_ids'): continue current_list_ids = [] subscription_ids = vals.get('subscription_list_ids') or [] for subscription in subscription_ids: if len(subscription) == 3: current_list_ids.append(subscription[2]['list_id']) for list_id in set(default_list_ids) - set(current_list_ids): subscription_ids.append((0, 0, {'list_id': list_id})) vals['subscription_list_ids'] = subscription_ids return super(MassMailingContact, self.with_context(default_list_ids=False)).create(vals_list) @api.returns('self', lambda value: value.id) def copy(self, default=None): """ Cleans the default_list_ids while duplicating mailing contact in context of a mailing list because we already have subscription lists copied over for newly created contact, no need to add the ones from default_list_ids again """ if self.env.context.get('default_list_ids'): self = self.with_context(default_list_ids=False) return super().copy(default) @api.model def name_create(self, name): name, email = self.get_name_email(name) contact = self.create({'name': name, 'email': email}) return contact.name_get()[0] @api.model def add_to_list(self, name, list_id): name, email = self.get_name_email(name) contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]}) return contact.name_get()[0] def _message_get_default_recipients(self): return { r.id: { 'partner_ids': [], 'email_to': ','.join(tools.email_normalize_all(r.email)) or r.email, 'email_cc': False, } for r in self } def action_add_to_mailing_list(self): ctx = dict(self.env.context, default_contact_ids=self.ids) action = self.env["ir.actions.actions"]._for_xml_id("mass_mailing.mailing_contact_to_list_action") action['view_mode'] = 'form' action['target'] = 'new' action['context'] = ctx return action @api.model def get_import_templates(self): return [{ 'label': _('Import Template for Mailing List Contacts'), 'template': '/mass_mailing/static/xls/mailing_contact.xls' }]