import logging import hashlib from datetime import datetime _logger = logging.getLogger(__name__) from odoo import api, fields, models, _ from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT EMAIL_PATTERN = '([^ ,;<@]+@[^> ,;]+)' unwanted_data = ['_links', 'modules'] replacement_of_key = [('id', 'list_id')] DATE_CONVERSION = ['date_created', 'last_sub_date', 'last_unsub_date', 'campaign_last_sent'] NOT_REQUIRED_ON_UPDATE = ['color', 'list_id', 'web_id', 'from_name', 'from_email', 'subject', 'partner_id', 'account_id', 'date_created', 'list_rating', 'subscribe_url_short', 'subscribe_url_long', 'beamer_address', 'id', 'display_name', 'create_uid', 'create_date', 'write_uid', 'write_date', '__last_update', '__last_update', 'statistics_ids', 'stats_overview_ids', 'stats_audience_perf_ids', 'stats_campaign_perf_ids', 'stats_since_last_campaign_ids', 'lang_id', 'odoo_list_id', 'last_create_update_date', 'is_update_required', 'contact_ids', 'subscription_contact_ids', 'mailchimp_list_id'] class MassMailingList(models.Model): _inherit = "mail.mass_mailing.list" def _compute_contact_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) where c.opt_out <> true AND c.is_email_valid = TRUE AND c.email IS NOT NULL group by list_id ''') data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_nbr = data.get(mailing_list.id, 0) contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') class MailChimpLists(models.Model): _name = "mailchimp.lists" _inherits = {'mail.mass_mailing.list': 'odoo_list_id'} _description = "MailChimp Audience" def _compute_contact_unsub_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) where COALESCE(c.opt_out,TRUE) = TRUE AND COALESCE(c.is_email_valid,TRUE) = TRUE AND c.email IS NOT NULL group by list_id ''') data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_unsub_nbr = data.get(mailing_list.odoo_list_id.id, 0) def _compute_contact_cleaned_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) where COALESCE(c.is_email_valid,FALSE) = FALSE AND c.email IS NOT NULL group by list_id ''') data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_cleaned_nbr = data.get(mailing_list.odoo_list_id.id, 0) def _compute_contact_total_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) where c.email IS NOT NULL group by list_id ''') data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_total_nbr = data.get(mailing_list.odoo_list_id.id, 0) def _is_update_required(self): for record in self: if record.write_date and record.last_create_update_date and record.write_date > record.last_create_update_date: record.is_update_required = True else: record.is_update_required = False # name = fields.Char("Name", required=True, help="This is how the list will be named in MailChimp.") color = fields.Integer('Color Index', default=0) list_id = fields.Char("Audience ID", copy=False, readonly=True) web_id = fields.Char("Website Identification", readonly=True) partner_id = fields.Many2one('res.partner', string="Contact", ondelete='restrict') permission_reminder = fields.Text("Permission Reminder", help="Remind recipients how they signed up to your list.") use_archive_bar = fields.Boolean("Use Archive Bar", help="Whether campaigns for this list use the Archive Bar in archives by default.") notify_on_subscribe = fields.Char("Email Subscribe Notifications To", help="The email address to send subscribe notifications to. \n Additional email addresses must be separated by a comma.") notify_on_unsubscribe = fields.Char("Email Unsubscribe Notifications To", help="The email address to send unsubscribe notifications to. \n Additional email addresses must be separated by a comma.") date_created = fields.Datetime("Creation Date", readonly=True) list_rating = fields.Selection([('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5')], "List Rating") email_type_option = fields.Boolean("Let users pick plain-text or HTML emails.", help="Whether the list suports multiple formats for emails. When set to true, subscribers can choose whether they want to receive HTML or plain-text emails. When set to false, subscribers will receive HTML emails, with a plain-text alternative backup.") subscribe_url_short = fields.Char("Subscribe URL Short", readonly=True) subscribe_url_long = fields.Char("Subscribe URL Long", readonly=True, help="The full version of this list’s subscribe form (host will vary).") beamer_address = fields.Char("Beamer Address", readonly=True) visibility = fields.Selection([('pub', 'Yes. My campaigns are public, and I want them to be discovered.'), ('prv', 'No, my campaigns for this list are not public.')], help="Whether this list is public or private.") double_optin = fields.Boolean("Enable double opt-in", help="Whether or not to require the subscriber to confirm subscription via email.") has_welcome = fields.Boolean("Send a Final Welcome Email", help="Whether or not this list has a welcome automation connected. Welcome Automations: welcomeSeries, singleWelcome, emailFollowup.") marketing_permissions = fields.Boolean("Enable GDPR fields", help="Whether or not the list has marketing permissions (eg. GDPR) enabled.") from_name = fields.Char("Default From Name", required=True) from_email = fields.Char("Default From Email Address", required=True) subject = fields.Char("Default Email Subject") lang_id = fields.Many2one('res.lang', string="Language") odoo_list_id = fields.Many2one('mail.mass_mailing.list', string='Odoo Mailing List', required=True, ondelete="cascade") contact_unsub_nbr = fields.Integer(compute="_compute_contact_unsub_nbr", string='Number of Unsubscribed Contacts') contact_cleaned_nbr = fields.Integer(compute="_compute_contact_cleaned_nbr", string='Number of Cleaned Contacts') contact_total_nbr = fields.Integer(compute="_compute_contact_total_nbr", string='Number of Total Contacts') statistics_ids = fields.One2many('mailchimp.lists.stats', 'list_id', string="Statistics", help="Stats for the list. Many of these are cached for at least five minutes.") stats_overview_ids = fields.One2many('mailchimp.lists.stats', 'list_id', string="Statistics", help="Stats for the list. Many of these are cached for at least five minutes.") stats_audience_perf_ids = fields.One2many('mailchimp.lists.stats', 'list_id', string="Statistics", help="Stats for the list. Many of these are cached for at least five minutes.") stats_campaign_perf_ids = fields.One2many('mailchimp.lists.stats', 'list_id', string="Statistics", help="Stats for the list. Many of these are cached for at least five minutes.") stats_since_last_campaign_ids = fields.One2many('mailchimp.lists.stats', 'list_id', string="Statistics", help="Stats for the list. Many of these are cached for at least five minutes.") account_id = fields.Many2one("mailchimp.accounts", string="Account", required=True) last_create_update_date = fields.Datetime("Last Create Update") write_date = fields.Datetime('Update on', index=True, readonly=True) is_update_required = fields.Boolean("Update Required?", compute="_is_update_required") member_since_last_changed = fields.Datetime("Fetch Member Since Last Change", copy=False) segment_ids = fields.One2many("mailchimp.segments", 'list_id', string="Segments", copy=False) merge_field_ids = fields.One2many("mailchimp.merge.fields", 'list_id', string="Merge Fields", copy=False) @api.multi def unlink(self): odoo_lists = self.mapped('odoo_list_id') super(MailChimpLists, self).unlink() return odoo_lists.unlink() def action_view_recipients(self): action = self.env.ref('mass_mailing.action_view_mass_mailing_contacts_from_list').read()[0] action['domain'] = [('list_ids', 'in', self.odoo_list_id.ids)] ctx = {'default_list_ids': [self.odoo_list_id.id]} if self.env.context.get('show_total', False): action['context'] = ctx if self.env.context.get('show_sub', False): ctx.update({'search_default_not_opt_out': 1}) action['context'] = ctx if self.env.context.get('show_unsub', False): ctx.update({'search_default_unsub_contact': 1}) action['context'] = ctx if self.env.context.get('show_cleaned', False): ctx.update({'search_default_cleaned_contact': 1}) action['context'] = ctx return action @api.model def _prepare_vals_for_update(self): self.ensure_one() prepared_vals = {} for field_name in self.fields_get_keys(): if hasattr(self, field_name) and field_name not in NOT_REQUIRED_ON_UPDATE: prepared_vals.update({field_name: getattr(self, field_name)}) partner_id = self.partner_id prepared_vals['contact'] = {'company': partner_id.name, 'address1': partner_id.street, 'address2': partner_id.street2, 'city': partner_id.city, 'state': partner_id.state_id.name or '', 'zip': partner_id.zip, 'country': partner_id.country_id.code, 'phone': partner_id.phone} prepared_vals['campaign_defaults'] = {'from_name': self.from_name, 'from_email': self.from_email, 'subject': self.subject or '', 'language': self.lang_id.iso_code or ''} return prepared_vals @api.multi def export_in_mailchimp(self): for list in self: prepared_vals = list._prepare_vals_for_update() response = list.account_id._send_request('lists', prepared_vals, method='POST') return True @api.multi def update_in_mailchimp(self): for list in self: prepared_vals = list._prepare_vals_for_update() response = list.account_id._send_request('lists/%s' % list.list_id, prepared_vals, method='PATCH') list.write({'last_create_update_date': fields.Datetime.now()}) return True @api.model def _find_partner(self, location): partners = self.env['res.partner'] state = self.env['res.country.state'] domain = [] if 'address1' in location and 'city' in location and 'company' in location: domain.append(('name', '=', location['company'])) domain.append(('street', '=', location['address1'])) domain.append(('city', '=', location['city'])) if location.get('state'): domain.append(('state_id.name', '=', location['state'])) if location.get('zip'): domain.append(('zip', '=', location['zip'])) partners = partners.search(domain, limit=1) if not partners: country_id = self.env['res.country'].search([('code', '=', location['country'])], limit=1) if country_id and location['state']: state = self.env['res.country.state'].search( ['|', ('name', '=', location['state']), ('code', '=', location['state']), ('country_id', '=', country_id.id)], limit=1) elif location['state']: state = self.env['res.country.state'].search( ['|', ('name', '=', location['state']), ('code', '=', location['state'])], limit=1) location.update({'name': location.pop('company'), 'street': location.pop('address1'), 'street2': location.pop('address2'), 'state_id': state.id, 'country_id': country_id.id}) partners = partners.create(location) return partners @api.multi def create_or_update_list(self, values_dict, account=False): list_id = values_dict.get('id') existing_list = self.search([('list_id', '=', list_id)]) stats = values_dict.pop('stats', {}) values_dict.update(values_dict.pop('campaign_defaults')) lang_id = self.env['res.lang'].search([('iso_code', '=', values_dict.get('language', 'en'))]) for item in unwanted_data: values_dict.pop(item) for old_key, new_key in replacement_of_key: values_dict[new_key] = values_dict.pop(old_key) for item in DATE_CONVERSION: if values_dict.get(item, False) == '': values_dict[item] = False if values_dict.get(item, False): values_dict[item] = account.covert_date(values_dict.get(item)) values_dict.update({'account_id': account.id}) partner = self._find_partner(values_dict.pop('contact')) values_dict.update( {'partner_id': partner.id, 'lang_id': lang_id.id, 'list_rating': str(values_dict.pop('list_rating', '0'))}) if not existing_list: existing_list = self.create(values_dict) else: existing_list.write(values_dict) existing_list.create_or_update_statistics(stats) # existing_list.fetch_members() existing_list.fetch_segments() existing_list.fetch_merge_fields() existing_list.write({'last_create_update_date': fields.Datetime.now()}) return True @api.multi def import_lists(self, account=False): if not account: raise Warning("MailChimp Account not defined to import lists") response = account._send_request('lists', {}, method='GET') for list in response.get('lists'): self.create_or_update_list(list, account=account) return True @api.one def refresh_list(self): if not self.account_id: raise Warning("MailChimp Account not defined to Refresh list") response = self.account_id._send_request('lists/%s' % self.list_id, {}) self.create_or_update_list(response, account=self.account_id) return True @api.multi def create_or_update_statistics(self, stats): self.ensure_one() self.statistics_ids.unlink() for item in DATE_CONVERSION: if stats.get(item, False): stats[item] = self.account_id.covert_date(stats.get(item)) else: stats[item] = False self.write({'statistics_ids': [(0, 0, stats)]}) return True @api.one def fetch_merge_fields(self): mailchimp_merge_field_obj = self.env['mailchimp.merge.fields'] if not self.account_id: raise Warning("MailChimp Account not defined to Fetch Merge Field list") count = 1000 offset = 0 merge_field_list = [] prepared_vals = {} while True: prepared_vals.update({'count': count, 'offset': offset, 'fields': 'merge_fields.merge_id,merge_fields.tag,merge_fields.name,merge_fields.type,merge_fields.required,merge_fields.default_value,merge_fields.public,merge_fields.display_order,merge_fields.list_id,merge_fields.options'}) response = self.account_id._send_request('lists/%s/merge-fields' % self.list_id, {}, params=prepared_vals) if len(response.get('merge_fields')) == 0: break if isinstance(response.get('merge_fields'), dict): merge_field_list = [response.get('merge_fields')] else: merge_field_list += response.get('merge_fields') offset = offset + 1000 for merge_field in merge_field_list: if not merge_field.get('merge_id', False): continue merge_field_id = mailchimp_merge_field_obj.search([('merge_id', '=', merge_field.get('merge_id')), ('list_id', '=', self.id)]) merge_field.update({'list_id': self.id}) if merge_field.get('options', {}).get('date_format'): date_format = merge_field.pop('options', {}).get('date_format') date_format = date_format.replace("MM", '%m') date_format = date_format.replace("DD", '%d') date_format = date_format.replace("YYYY", '%Y') merge_field.update({'date_format': date_format}) if not merge_field_id: mailchimp_merge_field_obj.create(merge_field) if merge_field_id: merge_field_id.write(merge_field) return merge_field_list @api.one def fetch_segments(self): mailchimp_segments_obj = self.env['mailchimp.segments'] if not self.account_id: raise Warning("MailChimp Account not defined to Fetch Segments list") count = 1000 offset = 0 segments_list = [] prepared_vals = {} while True: prepared_vals.update({'count': count, 'offset': offset}) response = self.account_id._send_request('lists/%s/segments' % self.list_id, {}, params=prepared_vals) if len(response.get('segments')) == 0: break if isinstance(response.get('segments'), dict): segments_list = [response.get('segments')] else: segments_list += response.get('segments') offset = offset + 1000 for segment in segments_list: if not segment.get('id', False): continue segment_id = mailchimp_segments_obj.search([('mailchimp_id', '=', segment.get('id'))]) name = segment.get('name') if segment.get('type') == 'static': name = "Tags : %s" % name vals = {'mailchimp_id': segment.get('id'), 'name': name, 'list_id': self.id} if not segment_id: mailchimp_segments_obj.create(vals) if segment_id: segment_id.write(vals) return segments_list def _prepare_vals_for_to_create_partner(self, merge_field_vals): prepared_vals = {} for custom_field in self.merge_field_ids: if custom_field.type == 'address': address_dict = merge_field_vals.get(custom_field.tag) if not address_dict: continue state_id = False country = self.env['res.country'].search([('code', '=', address_dict.get('country', ''))], limit=1) if country: state_id = self.env['res.country.state'].search( ['|', ('name', '=', address_dict['state']), ('code', '=', address_dict['state']), ('country_id', '=', country.id)], limit=1) prepared_vals.update({ 'street': address_dict.get('addr1', ''), 'street2': address_dict.get('addr2', ''), 'city': address_dict.get('city', ''), 'zip': address_dict.get('zip', ''), 'state_id': state_id.id if state_id else False, 'country_id': country.id, }) elif custom_field.tag in ['FNAME', 'LNAME'] and not prepared_vals.get('name', False): prepared_vals.update({'name': "%s %s" % (merge_field_vals.get('FNAME'), merge_field_vals.get('LNAME'))}) elif custom_field.field_id and custom_field.type in ['date', 'birthday']: value = merge_field_vals.get(custom_field.tag) if value and custom_field.field_id.ttype in ['date']: if len(value.split('/')) > 1: value = datetime.strptime(value, custom_field.date_format).strftime(DEFAULT_SERVER_DATE_FORMAT) prepared_vals.update({custom_field.field_id.name: value or False}) elif custom_field.field_id: prepared_vals.update({custom_field.field_id.name: merge_field_vals.get(custom_field.tag)}) return prepared_vals @api.one def fetch_members(self): mailing_contact_obj = self.env['mail.mass_mailing.contact'] state = self.env['res.country.state'] country = self.env['res.country'] if not self.account_id: raise Warning("MailChimp Account not defined to Fetch Member list") if not self.merge_field_ids: return True count = 1000 offset = 0 members_list = [] members_list_to_create = [] prepared_vals = {} if self.member_since_last_changed: prepared_vals.update({'since_last_changed': datetime.strptime(self.member_since_last_changed, DEFAULT_SERVER_DATETIME_FORMAT).strftime("%Y-%m-%dT%H:%M:%S+00:00")}) while True: prepared_vals.update({'count': count, 'offset': offset, 'fields': 'members.email_address,members.merge_fields,members.tags,members.web_id,members.status'}) response = self.account_id._send_request('lists/%s/members' % self.list_id, {}, params=prepared_vals) if len(response.get('members')) == 0: break # if isinstance(response.get('members'), dict): # members_list += [response.get('members')] if isinstance(response.get('members'), dict): members_data = [response.get('members')] else: members_data = response.get('members') offset = offset + 1000 for member in members_data: if not member.get('email_address', False): continue update_partner_required = True contact_id = mailing_contact_obj.search([('email', '=', member.get('email_address'))]) create_vals = member.get('merge_fields') prepared_vals_for_create_partner = self._prepare_vals_for_to_create_partner(create_vals) name = "%s %s" % (create_vals.pop('FNAME'), create_vals.pop('LNAME')) tag_ids = self.env['res.partner.category'] tag_list = member.get('tags') if tag_list: tag_ids = self.env['res.partner.category'].create_or_update_tags(tag_list) prepared_vals_for_create_partner.update({'category_id': [(6, 0, tag_ids.ids)]}) if not contact_id: if not self.account_id.auto_create_member: continue self.update_partner_detail(name, member.get('email_address'), prepared_vals_for_create_partner) update_partner_required = False contact_id = mailing_contact_obj.create( {'name': name, 'email': member.get('email_address'), 'country_id': prepared_vals_for_create_partner.get('country_id', False) or False}) if contact_id: md5_email = hashlib.md5(member.get('email_address').encode('utf-8')).hexdigest() vals = {'list_id': self.odoo_list_id.id, 'contact_id': contact_id.id, 'mailchimp_id': member.get('web_id'), 'md5_email': md5_email} status = member.get('status', '') if update_partner_required: self.update_partner_detail(name, member.get('email_address'), prepared_vals_for_create_partner) contact_vals = {'opt_out': True} if status == 'unsubscribed' else {'opt_out': False} if status == 'cleaned': contact_vals.update({'is_email_valid': False, 'opt_out': True}) contact_vals.update({'tag_ids': [(6, 0, tag_ids.ids)]}) contact_id.write(contact_vals) existing_define_list = contact_id.subscription_list_ids.filtered( lambda x: x.list_id.id == self.odoo_list_id.id) if existing_define_list: existing_define_list.write(vals) else: contact_id.subscription_list_ids.create(vals) self._cr.commit() self.write({'member_since_last_changed': fields.Datetime.now()}) return members_list @api.multi def update_partner_detail(self, name, email, partner_detail): query = """ SELECT id FROM res_partner WHERE LOWER(substring(email, '([^ ,;<@]+@[^> ,;]+)')) = LOWER(substring('{}', '([^ ,;<@]+@[^> ,;]+)'))""".format(email) self._cr.execute(query) partner_id = self._cr.fetchone() partner_id = partner_id[0] if partner_id else False if partner_id: partner_id = self.env['res.partner'].browse(partner_id) if partner_detail: partner_id.write(partner_detail) else: if self.account_id.auto_create_member and self.account_id.auto_create_partner: partner_detail.update({ 'email': email, 'is_company': False, 'type': 'contact', }) self.env['res.partner'].create(partner_detail) return True @api.model def fetch_member_cron(self): account_obj = self.env['mailchimp.accounts'] for record in account_obj.search([('auto_refresh_member', '=', True)]): list_ids = self.search([('account_id', '=', record.id)]) for list in list_ids: list.fetch_members() return True class MailChimpListsStats(models.Model): _name = "mailchimp.lists.stats" _description = "MailChimp Statistics" list_id = fields.Many2one("mailchimp.lists", string="MailChimp List", required=True, ondelete='cascade') member_count = fields.Integer("Subscribed Count") unsubscribe_count = fields.Integer("Unsubscribe Count") cleaned_count = fields.Integer("Cleaned Count") member_count_since_send = fields.Integer("Subscribed Count") unsubscribe_count_since_send = fields.Integer("Unsubscribe Count") cleaned_count_since_send = fields.Integer("Cleaned Count") campaign_count = fields.Integer("Campaign Count") campaign_last_sent = fields.Datetime("Campaign Last Sent") merge_field_count = fields.Integer("Merge Count") avg_sub_rate = fields.Float("Average Subscription Rate") avg_unsub_rate = fields.Float("Average Unsubscription Rate") target_sub_rate = fields.Float("Average Subscription Rate") open_rate = fields.Float("Open Rate") click_rate = fields.Float("Click Rate") last_sub_date = fields.Datetime("Date of Last Subscribe") last_unsub_date = fields.Datetime("Date of Last Unsubscribe")