odoo/ext/custom-addons/mailchimp/models/mailchimp_lists.py

542 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 lists 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")