diff --git a/ext/custom-addons/mailchimp/__init__.py b/ext/custom-addons/mailchimp/__init__.py
new file mode 100644
index 00000000..fab5aef6
--- /dev/null
+++ b/ext/custom-addons/mailchimp/__init__.py
@@ -0,0 +1,3 @@
+from . import models
+from . import wizard
+from . import controllers
\ No newline at end of file
diff --git a/ext/custom-addons/mailchimp/__manifest__.py b/ext/custom-addons/mailchimp/__manifest__.py
new file mode 100644
index 00000000..7a2bf8ed
--- /dev/null
+++ b/ext/custom-addons/mailchimp/__manifest__.py
@@ -0,0 +1,60 @@
+{
+ "name": "Odoo MailChimp Integration",
+ "version": "11.0.3",
+ "category": "Marketing",
+ 'summary': 'Integrate & Manage MailChimp Operations from Odoo',
+
+ "depends": ["mass_mailing"],
+
+ 'data': [
+ 'data/ir_cron.xml',
+ 'views/assets.xml',
+ 'views/mailchimp_accounts_view.xml',
+ 'views/mailchimp_lists_view.xml',
+ 'wizard/import_export_operation_view.xml',
+ 'views/mass_mailing_contact_view.xml',
+ 'views/mass_mailing_list_view.xml',
+ 'views/mailchimp_template_view.xml',
+ 'views/mass_mailing_view.xml',
+ 'wizard/mass_mailing_schedule_date_view.xml',
+ 'views/res_partner_views.xml',
+ 'wizard/partner_export_update_wizard.xml',
+ 'security/ir.model.access.csv'
+ ],
+
+ 'images': ['static/description/mailchimp_odoo.png'],
+
+ "author": "Teqstars",
+ "website": "https://teqstars.com",
+ 'support': 'info@teqstars.com',
+ 'maintainer': 'Teqstars',
+ "description": """
+ - Manage your MailChimp operation from Odoo
+ - Integration mailchimp
+ - Connector mailchimp
+ - mailchimp Connector
+ - Odoo mailchimp Connector
+ - mailchimp integration
+ - mailchimp odoo connector
+ - mailchimp odoo integration
+ - odoo mailchimp integration
+ - odoo integration with mailchimp
+ - odoo teqstars apps
+ - teqstars odoo apps
+ - manage audience
+ - manage champaign
+ - email Marketing
+ - mailchimp marketing
+ - odoo and mailchimp
+ """,
+
+ 'demo': [],
+ 'license': 'OPL-1',
+ 'live_test_url':'http://bit.ly/2n7ExKX',
+ 'auto_install': False,
+ "installable": True,
+ 'application': True,
+ 'qweb': ['static/src/xml/shipstation_dashboard_template.xml'],
+ "price": "149.00",
+ "currency": "EUR",
+}
diff --git a/ext/custom-addons/mailchimp/controllers/__init__.py b/ext/custom-addons/mailchimp/controllers/__init__.py
new file mode 100644
index 00000000..e43086e1
--- /dev/null
+++ b/ext/custom-addons/mailchimp/controllers/__init__.py
@@ -0,0 +1 @@
+from . import mailchimp
\ No newline at end of file
diff --git a/ext/custom-addons/mailchimp/controllers/mailchimp.py b/ext/custom-addons/mailchimp/controllers/mailchimp.py
new file mode 100644
index 00000000..0a69f708
--- /dev/null
+++ b/ext/custom-addons/mailchimp/controllers/mailchimp.py
@@ -0,0 +1,75 @@
+from odoo.http import request
+from odoo import http
+import logging
+import hashlib
+
+_logger = logging.getLogger(__name__)
+
+
+class MailChimp(http.Controller):
+
+ @http.route('/mailchimp/webhook/notification', type='http', auth="public", csrf=False)
+ def mailchimp_api(self, **kwargs):
+ # TODO : Work with multi databases.
+ contact_obj = request.env['mail.mass_mailing.contact'].sudo()
+ mass_mailling_obj = request.env['mail.mass_mailing'].sudo()
+ mailchimp_list_obj = request.env['mailchimp.lists'].sudo()
+ if kwargs.get('data[merges][EMAIL]', False):
+ email_address = kwargs['data[merges][EMAIL]']
+ mailchimp_id = kwargs['data[web_id]']
+ event = kwargs.get('type', False)
+ contact_id = contact_obj.search([('email', '=', email_address)])
+ mailchimp_list_id = mailchimp_list_obj.search([('list_id', '=', kwargs['data[list_id]'])])
+ name = "%s %s" % (kwargs.get('data[merges][FNAME]'), kwargs.get('data[merges][LNAME]'))
+ md5_email = hashlib.md5(email_address.encode('utf-8')).hexdigest()
+ merge_field_dict = {}
+ update_partner_required = True
+ for custom_field in mailchimp_list_id.merge_field_ids:
+ tag = custom_field.tag
+ if custom_field.type == 'address':
+ address_dict = {
+ 'addr1': kwargs.get('data[merges][{}][addr1]'.format(tag), ''),
+ 'addr2': kwargs.get('data[merges][{}][addr2]'.format(tag), ''),
+ 'city': kwargs.get('data[merges][{}][city]'.format(tag), ''),
+ 'state': kwargs.get('data[merges][{}][state]'.format(tag), ''),
+ 'zip': kwargs.get('data[merges][{}][zip]'.format(tag), ''),
+ 'country': kwargs.get('data[merges][{}][country]'.format(tag), ''),
+ }
+ merge_field_dict.update({tag: address_dict})
+ else:
+ merge_field_dict.update({tag: kwargs.get('data[merges][{}]'.format(tag), '')})
+ tag_ids = contact_id.fetch_specific_member_data(mailchimp_list_id, md5_email)
+ prepared_vals_for_create_partner = mailchimp_list_id._prepare_vals_for_to_create_partner(merge_field_dict)
+ prepared_vals_for_create_partner.update({'category_id': [(6, 0, tag_ids.ids)]})
+ if not contact_id:
+ if prepared_vals_for_create_partner:
+ mailchimp_list_id.update_partner_detail(name, email_address, prepared_vals_for_create_partner)
+ update_partner_required = False
+ contact_id = contact_id.create({'name': name, 'email': email_address, 'country_id': prepared_vals_for_create_partner.get('country_id', False) or False})
+ if contact_id and kwargs.get('data[action]', '') != 'delete':
+ if tag_ids or not tag_ids and contact_id.tag_ids:
+ contact_id.write({'tag_ids': [(6, 0, tag_ids.ids)]})
+ if update_partner_required:
+ mailchimp_list_id.update_partner_detail(name, email_address, prepared_vals_for_create_partner)
+ vals = {'list_id': mailchimp_list_id.odoo_list_id.id, 'contact_id': contact_id.id, 'mailchimp_id': mailchimp_id, 'md5_email': md5_email}
+ existing_define_list = contact_id.subscription_list_ids.filtered(
+ lambda x: x.list_id.id == mailchimp_list_id.odoo_list_id.id)
+ if existing_define_list:
+ existing_define_list.write(vals)
+ else:
+ contact_id.subscription_list_ids.create(vals)
+ if event == 'unsubscribe':
+ mass_mailling_obj.update_opt_out_ts(contact_id.email, mailchimp_list_id.odoo_list_id.ids, True)
+ elif event == 'subscribe':
+ mass_mailling_obj.update_opt_out_ts(contact_id.email, mailchimp_list_id.odoo_list_id.ids, False)
+ elif event == 'profile':
+ name = "%s %s" % (kwargs.get('data[merges][FNAME]'), kwargs.get('data[merges][LNAME]'))
+ contact_id.write({'name': name, 'email': kwargs.get('data[merges][EMAIL]')})
+ if kwargs.get('data[action]', '') == 'delete':
+ if len(contact_id.list_ids) > 1:
+ contact_id.write({'list_ids': [(3, mailchimp_list_id.odoo_list_id.id)]})
+ else:
+ contact_id.unlink()
+ request._cr.commit()
+ return 'SUCCESS'
+ return 'FAILURE'
diff --git a/ext/custom-addons/mailchimp/data/ir_cron.xml b/ext/custom-addons/mailchimp/data/ir_cron.xml
new file mode 100644
index 00000000..8b44408f
--- /dev/null
+++ b/ext/custom-addons/mailchimp/data/ir_cron.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ Do Not Delete : Fetch Campaigns Reports From MailChimp
+
+ code
+ model.fetch_email_activity()
+ 1
+ hours
+ -1
+ 1
+
+
+
+ Do Not Delete : Fetch MailChimp Audience
+
+ code
+ model.fetch_member_cron()
+ 1
+ hours
+ -1
+ 1
+
+
+
\ No newline at end of file
diff --git a/ext/custom-addons/mailchimp/models/__init__.py b/ext/custom-addons/mailchimp/models/__init__.py
new file mode 100644
index 00000000..3cf06fb7
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/__init__.py
@@ -0,0 +1,13 @@
+from . import mailchimp_lists
+from . import mailchimp_accounts
+from . import mass_mailing_contact
+from . import mass_mailing_list
+from . import mailchimp_template
+from . import mass_mailing
+from . import mail_mail_statistics
+from . import mass_mailing_list_contact_rel
+from . import res_partner
+from . import ir_model
+from . import res_partner_category
+from . import mailchimp_segments
+from . import mailchimp_merge_fields
diff --git a/ext/custom-addons/mailchimp/models/ir_model.py b/ext/custom-addons/mailchimp/models/ir_model.py
new file mode 100644
index 00000000..70ebebbc
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/ir_model.py
@@ -0,0 +1,96 @@
+import logging
+
+from odoo import api, fields, models, SUPERUSER_ID, tools, _
+from odoo.exceptions import AccessError, UserError, ValidationError
+
+_logger = logging.getLogger(__name__)
+
+MODULE_UNINSTALL_FLAG = '_force_unlink'
+
+
+class IrModelData(models.Model):
+ _inherit = 'ir.model.data'
+
+ @api.model
+ def _module_data_uninstall(self, modules_to_remove):
+ """Deletes all the records referenced by the ir.model.data entries
+ ``ids`` along with their corresponding database backed (including
+ dropping tables, columns, FKs, etc, as long as there is no other
+ ir.model.data entry holding a reference to them (which indicates that
+ they are still owned by another module).
+ Attempts to perform the deletion in an appropriate order to maximize
+ the chance of gracefully deleting all records.
+ This step is performed as part of the full uninstallation of a module.
+ """
+ if not (self._uid == SUPERUSER_ID or self.env.user.has_group('base.group_system')):
+ raise AccessError(_('Administrator access is required to uninstall a module'))
+
+ # enable model/field deletion
+ self = self.with_context(**{MODULE_UNINSTALL_FLAG: True})
+
+ datas = self.search([('module', 'in', modules_to_remove)])
+ to_unlink = tools.OrderedSet()
+ undeletable = self.browse([])
+
+ for data in datas.sorted(key='id', reverse=True):
+ model = data.model
+ res_id = data.res_id
+ if data.model == 'ir.model' and 'mailchimp' in modules_to_remove:
+ model_id = self.env[model].browse(res_id).with_context(prefetch_fields=False)
+ if model_id.model == 'mail.mass_mailing.list_contact_rel':
+ continue
+ if data.model == 'ir.model.fields' and 'mailchimp' in modules_to_remove:
+ field_id = self.env[model].browse(res_id).with_context(prefetch_fields=False)
+ if field_id.name in ['contact_id', 'list_id']:
+ continue
+ to_unlink.add((model, res_id))
+
+ def unlink_if_refcount(to_unlink):
+ undeletable = self.browse()
+ for model, res_id in to_unlink:
+ external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)])
+ if external_ids - datas:
+ # if other modules have defined this record, we must not delete it
+ continue
+ if model == 'ir.model.fields':
+ # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
+ # has been turned off on the model.
+ field = self.env[model].browse(res_id).with_context(
+ prefetch_fields=False,
+ )
+ if not field.exists():
+ _logger.info('Deleting orphan external_ids %s', external_ids)
+ external_ids.unlink()
+ continue
+ if field.name in models.LOG_ACCESS_COLUMNS and field.model in self.env and self.env[field.model]._log_access:
+ continue
+ if field.name == 'id':
+ continue
+ _logger.info('Deleting %s@%s', res_id, model)
+ try:
+ self._cr.execute('SAVEPOINT record_unlink_save')
+ self.env[model].browse(res_id).unlink()
+ except Exception:
+ _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
+ undeletable += external_ids
+ self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
+ else:
+ self._cr.execute('RELEASE SAVEPOINT record_unlink_save')
+ return undeletable
+
+ # Remove non-model records first, then model fields, and finish with models
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
+
+ modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
+ constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
+ constraints._module_data_uninstall()
+
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
+
+ relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
+ relations._module_data_uninstall()
+
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
+
+ (datas - undeletable).unlink()
diff --git a/ext/custom-addons/mailchimp/models/mail_mail_statistics.py b/ext/custom-addons/mailchimp/models/mail_mail_statistics.py
new file mode 100644
index 00000000..1616cdbc
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mail_mail_statistics.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+
+
+class MailMailStats(models.Model):
+ _inherit = 'mail.mail.statistics'
+
+ email = fields.Char(string="Recipient email address")
diff --git a/ext/custom-addons/mailchimp/models/mailchimp_accounts.py b/ext/custom-addons/mailchimp/models/mailchimp_accounts.py
new file mode 100644
index 00000000..9903e3db
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mailchimp_accounts.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+import json
+import time
+import requests
+from email.utils import formataddr
+from odoo import api, fields, models, _
+from odoo.tools.safe_eval import safe_eval
+from odoo.exceptions import ValidationError, Warning
+
+
+class MailChimpAccounts(models.Model):
+ _name = "mailchimp.accounts"
+
+ name = fields.Char("Name", required=True, copy=False, help="Name of your MailChimp account")
+
+ # Authentication
+ api_key = fields.Char('API Key', required=True, copy=False)
+ auto_refresh_member = fields.Boolean("Auto Sync Member?", copy=False, default=True)
+ auto_create_member = fields.Boolean("Auto Create Member?", copy=False, default=True)
+ auto_create_partner = fields.Boolean("Auto Create Customer?", copy=False, default=False)
+ list_ids = fields.One2many('mailchimp.lists', 'account_id', string="Lists/Audience", copy=False)
+ campaign_ids = fields.One2many('mail.mass_mailing', 'mailchimp_account_id', string="Campaigns", copy=False)
+
+ _sql_constraints = [
+ ('api_keys_uniq', 'unique(api_key)', 'API keys must be unique per MailChimp Account!'),
+ ]
+
+ @api.model
+ def _send_request(self, request_url, request_data, params=False, method='GET'):
+ if '-' not in self.api_key:
+ raise ValidationError(_("MailChimp API key is invalid!"))
+ if len(self.api_key.split('-')) > 2:
+ raise ValidationError(_("MailChimp API key is invalid!"))
+
+ api_key, dc = self.api_key.split('-')
+ headers = {
+ 'Content-Type': 'application/json'
+ }
+ data = json.dumps(request_data)
+ api_url = "https://{dc}.api.mailchimp.com/3.0/{url}".format(dc=dc, url=request_url)
+ try:
+ req = requests.request(method, api_url, auth=('apikey', api_key), headers=headers, params=params, data=data)
+ req.raise_for_status()
+ response_text = req.text
+ except requests.HTTPError as e:
+ raise Warning("%s" % req.text)
+ response = json.loads(response_text) if response_text else {}
+ return response
+
+ @api.multi
+ def get_refresh_member_action(self):
+ action = self.env.ref('base.ir_cron_act').read()[0]
+ refresh_member_cron = self.env.ref('mailchimp.fetch_member')
+ if refresh_member_cron:
+ action['views'] = [(False, 'form')]
+ action['res_id'] = refresh_member_cron.id
+ else:
+ raise ValidationError(_("Scheduled action isn't found! Please upgrade app to get it back!"))
+ return action
+
+ def covert_date(self, value):
+ before_date = value[:19]
+ coverted_date = time.strptime(before_date, "%Y-%m-%dT%H:%M:%S")
+ final_date = time.strftime("%Y-%m-%d %H:%M:%S", coverted_date)
+ return final_date
+
+ @api.multi
+ def import_lists(self):
+ mailchimp_lists = self.env['mailchimp.lists']
+ for account in self:
+ mailchimp_lists.import_lists(account)
+ return True
+
+ @api.multi
+ def import_templates(self):
+ mailchimp_templates = self.env['mailchimp.templates']
+ for account in self:
+ mailchimp_templates.import_templates(account)
+ return True
+
+ @api.multi
+ def import_campaigns(self):
+ mass_mailing_obj = self.env['mail.mass_mailing']
+ for account in self:
+ mass_mailing_obj.import_campaigns(account)
+ return True
+
+ @api.one
+ def test_connection(self):
+ response = self._send_request('lists', {})
+ if response:
+ raise Warning("Test Connection Succeeded")
+ return True
diff --git a/ext/custom-addons/mailchimp/models/mailchimp_lists.py b/ext/custom-addons/mailchimp/models/mailchimp_lists.py
new file mode 100644
index 00000000..ee5503b3
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mailchimp_lists.py
@@ -0,0 +1,541 @@
+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")
diff --git a/ext/custom-addons/mailchimp/models/mailchimp_merge_fields.py b/ext/custom-addons/mailchimp/models/mailchimp_merge_fields.py
new file mode 100644
index 00000000..062396a6
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mailchimp_merge_fields.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+
+
+class MailChimpMergeFields(models.Model):
+ _name = "mailchimp.merge.fields"
+
+ name = fields.Char("Name", required=True, copy=False, help="Name of your MailChimp Merge Fields")
+ merge_id = fields.Char("Merge ID", readonly=True, copy=False)
+ tag = fields.Char("Merge Field Tag", help="The tag used in Mailchimp campaigns and for the /members endpoint.")
+ type = fields.Selection(
+ [('text', 'Text'), ('number', 'Number'), ('address', 'Address'), ('phone', 'Phone'), ('date', 'Date'), ('radio', 'Radio'), ('dropdown', 'Dropdown'), ('birthday', 'Birthday'), ('zip', 'Zip'), ('imageurl', 'ImageURL'), ('url', 'URL')])
+ date_format = fields.Char('Date Format', copy=False)
+ required = fields.Boolean("Required?", copy=False, help="Merge field is required or not.")
+ public = fields.Boolean("Visible?", copy=False, help="Whether the merge field is displayed on the signup form.")
+ default_value = fields.Char("Default Value", help="The default value for the merge field if null.")
+ display_order = fields.Char("Display Order", help="The order that the merge field displays on the list signup form.")
+ list_id = fields.Many2one("mailchimp.lists", string="Associated MailChimp List", ondelete='cascade', required=True, copy=False)
+ field_id = fields.Many2one('ir.model.fields', string='Odoo Field', help="""Odoo will fill value of selected field while contact is going to export or update""", domain="[('model_id.model', '=', 'res.partner')]")
+
+ _sql_constraints = [
+ ('merge_id_list_id_uniq', 'unique(merge_id, list_id)', 'Merge ID must be unique per MailChimp Lists!'),
+ ]
diff --git a/ext/custom-addons/mailchimp/models/mailchimp_segments.py b/ext/custom-addons/mailchimp/models/mailchimp_segments.py
new file mode 100644
index 00000000..2ba7a324
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mailchimp_segments.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+
+
+class MailChimpSegments(models.Model):
+ _name = "mailchimp.segments"
+
+ name = fields.Char("Name", required=True, copy=False, help="Name of your MailChimp Segments")
+ mailchimp_id = fields.Char("MailChimp ID", readonly=True, copy=False)
+ list_id = fields.Many2one("mailchimp.lists", string="Associated MailChimp List", ondelete='cascade', required=True, copy=False)
+
+ _sql_constraints = [
+ ('mailchimp_id_uniq', 'unique(mailchimp_id)', 'MailChimp ID must be unique per MailChimp Segments!'),
+ ]
diff --git a/ext/custom-addons/mailchimp/models/mailchimp_template.py b/ext/custom-addons/mailchimp/models/mailchimp_template.py
new file mode 100644
index 00000000..c6a29802
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mailchimp_template.py
@@ -0,0 +1,64 @@
+from odoo import api, fields, models, _
+
+REPLACEMENT_OF_KEY = [('id', 'template_id')]
+DATE_CONVERSION = ['date_created', 'date_edited']
+UNWANTED_DATA = ['_links', 'created_by', 'edited_by', 'thumbnail']
+
+
+class MailChimpTemplates(models.Model):
+ _name = "mailchimp.templates"
+ _description = "Templates"
+
+ name = fields.Char("Name", required=True, help="The name of the template.")
+ template_id = fields.Char("Template ID", copy=False)
+ type = fields.Selection([('user', 'User'), ('gallery', 'Gallery'), ('base', 'Base')], default='user', copy=False,
+ help="The type of template (user, base, or gallery).")
+ drag_and_drop = fields.Boolean("Drag and Drop", help="Whether the template uses the drag and drop editor.")
+ responsive = fields.Boolean("Responsive", help="Whether the template contains media queries to make it responsive.")
+ category = fields.Char("Template Category", help="If available, the category the template is listed in.")
+ date_created = fields.Datetime("Created On")
+ date_edited = fields.Datetime("Edited On")
+ active = fields.Boolean("Active", default=True)
+ folder_id = fields.Char("Folder ID", help="The id of the folder the template is currently in.")
+ share_url = fields.Char("Share URL", help="The URL used for template sharing")
+ account_id = fields.Many2one("mailchimp.accounts", string="Account", required=True, ondelete='cascade')
+
+ @api.multi
+ def create_or_update_template(self, values_dict, account=False):
+ template_id = values_dict.get('id')
+ existing_list = self.search([('template_id', '=', template_id)])
+ 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})
+ if not existing_list:
+ existing_list = self.create(values_dict)
+ else:
+ existing_list.write(values_dict)
+ return True
+
+ @api.multi
+ def import_templates(self, account=False):
+ if not account:
+ raise Warning("MailChimp Account not defined to import templates")
+ count = 1000
+ offset = 0
+ template_list = []
+ while True:
+ prepared_vals = {'count': count, 'offset': offset}
+ response = account._send_request('templates', {}, params=prepared_vals)
+ if len(response.get('templates')) == 0:
+ break
+ if isinstance(response.get('templates'), dict):
+ template_list += [response.get('templates')]
+ template_list += response.get('templates')
+ offset = offset + 1000
+ for template_dict in template_list:
+ self.create_or_update_template(template_dict, account=account)
+ return True
diff --git a/ext/custom-addons/mailchimp/models/mass_mailing.py b/ext/custom-addons/mailchimp/models/mass_mailing.py
new file mode 100644
index 00000000..d07a1c47
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mass_mailing.py
@@ -0,0 +1,348 @@
+from datetime import datetime, timedelta
+from odoo.tools.safe_eval import safe_eval
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import Warning, ValidationError
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+
+from email.utils import formataddr, parseaddr
+
+REPLACEMENT_OF_KEY = [('id', 'mailchimp_id'), ('create_time', 'create_date'), ('send_time', 'sent_date'),
+ ('type', 'mailchimp_champ_type')]
+DATE_CONVERSION = ['create_date', 'sent_date']
+UNWANTED_DATA = ['_links', 'created_by', 'edited_by', 'thumbnail']
+
+
+class MassMailing(models.Model):
+ _inherit = "mail.mass_mailing"
+
+ create_date = fields.Datetime("Created on", readonly=True, index=True)
+ mailchimp_template_id = fields.Many2one('mailchimp.templates', "MailChimp Template", copy=False)
+ mailchimp_account_id = fields.Many2one('mailchimp.accounts', string="MailChimp Account",
+ related="mailchimp_template_id.account_id", store=True)
+ mailchimp_list_id = fields.Many2one("mailchimp.lists", string="MailChimp List")
+ mailchimp_id = fields.Char("MailChimp ID", copy=False)
+ mailchimp_segment_id = fields.Many2one('mailchimp.segments', string="MailChimp Segments", copy=False)
+ mailchimp_champ_type = fields.Selection(
+ [('regular', 'Regular'), ('plaintext', 'Plain Text'), ('absplit', 'AB Split'), ('rss', 'RSS'),
+ ('variate', 'Variate')],
+ default='regular', string="Type")
+
+ def update_opt_out_ts(self, email, list_ids, value):
+ if len(list_ids) > 0:
+ model = self.env['mail.mass_mailing.contact'].with_context(active_test=False)
+ records = model.search([('email', '=ilike', email), ('list_ids', 'in', list_ids)])
+ records.write({'opt_out': value})
+
+ @api.multi
+ def action_schedule_date(self):
+ self.ensure_one()
+ action = self.env.ref('mailchimp.mass_mailing_schedule_date_action').read()[0]
+ action['context'] = dict(self.env.context, default_mass_mailing_id=self.id)
+ return action
+
+ @api.model
+ def fetch_send_to_activity(self):
+ self.ensure_one()
+ account = self.mailchimp_template_id.account_id
+ if not account:
+ return True
+ count = 1000
+ offset = 0
+ sent_to_lists = []
+ while True:
+ prepared_vals = {'count': count, 'offset': offset,
+ 'fields': 'sent_to.status,sent_to.email_address'}
+ response = account._send_request(
+ 'reports/%s/sent-to' % self.mailchimp_id, {}, params=prepared_vals)
+ if len(response.get('sent_to')) == 0:
+ break
+ if isinstance(response.get('sent_to'), dict):
+ sent_to_lists += [response.get('sent_to')]
+ sent_to_lists += response.get('sent_to')
+ offset = offset + 1000
+ return sent_to_lists
+
+ @api.multi
+ def process_send_to_activity_report(self):
+ self.ensure_one()
+ stat_obj = self.env['mail.mail.statistics']
+ sent_to_lists = self.fetch_send_to_activity()
+ if not sent_to_lists:
+ return True
+ domain = safe_eval(self.mailing_domain)
+ contact_ids = self.env[self.mailing_model_real].search(domain)
+ for record_dict in sent_to_lists:
+ prepared_vals = {}
+ email_address = record_dict.get('email_address')
+ status = record_dict.get('status')
+ if status == 'sent':
+ prepared_vals.update({'sent': self.sent_date, 'scheduled': self.sent_date, 'bounced': False})
+ elif status in ['hard', 'soft']:
+ prepared_vals.update({'bounced': self.sent_date, 'sent': self.sent_date, 'scheduled': self.sent_date})
+ existing = self.statistics_ids.filtered(lambda x: x.email == email_address)
+ if existing:
+ existing.write(prepared_vals)
+ else:
+ res_id = contact_ids.filtered(lambda x: x.email == email_address)
+ prepared_vals.update({
+ 'model': self.mailing_model_real,
+ 'res_id': res_id.id,
+ 'mass_mailing_id': self.id,
+ 'email': email_address,
+ })
+ self.statistics_ids.create(prepared_vals)
+ return sent_to_lists
+
+ @api.multi
+ def process_email_activity_report(self):
+ self.ensure_one()
+ self.process_send_to_activity_report()
+ account = self.mailchimp_template_id.account_id
+ count = 1000
+ offset = 0
+ email_lists = []
+ while True:
+ prepared_vals = {'count': count, 'offset': offset, 'fields': 'emails.email_address,emails.activity'}
+ response = account._send_request(
+ 'reports/%s/email-activity' % self.mailchimp_id, {}, params=prepared_vals)
+ if len(response.get('emails')) == 0:
+ break
+ if isinstance(response.get('emails'), dict):
+ email_lists += [response.get('emails')]
+ email_lists += response.get('emails')
+ offset = offset + 1000
+
+ for email in email_lists:
+ email_address = email.get('email_address')
+ activities = email.get('activity')
+ if not activities:
+ continue
+ prepared_vals = {}
+ for acitvity in activities:
+ action = acitvity.get('action')
+ if action == 'open':
+ prepared_vals.update({'opened': account.covert_date(acitvity.get('timestamp'))})
+ elif action == 'click':
+ prepared_vals.update({'clicked': account.covert_date(acitvity.get('timestamp'))})
+ elif action == 'bounce':
+ prepared_vals.update({'bounced': account.covert_date(acitvity.get('timestamp'))})
+ existing = self.statistics_ids.filtered(lambda x: x.email == email_address)
+ if existing:
+ existing.write(prepared_vals)
+
+ @api.model
+ def fetch_email_activity(self):
+ stat_obj = self.env['mail.mail.statistics']
+ sent_date = datetime.today() - timedelta(days=30)
+ sent_date = sent_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ for record in self.search([('state', 'not in', ['draft', 'in_queue']), ('mailchimp_id', '!=', False), ('sent_date', '>=', sent_date)]):
+ record.fetch_campaign()
+ # record.process_email_activity_report()
+ return True
+
+ @api.multi
+ def create_or_update_campaigns(self, values_dict, account=False):
+ fetch_needed = False
+ list_obj = self.env['mailchimp.lists']
+ template_obj = self.env['mailchimp.templates']
+ mailchimp_id = values_dict.get('id')
+ settings_dict = values_dict.get('settings')
+ recipients_dict = values_dict.get('recipients')
+ list_id = recipients_dict.get('list_id')
+ template_id = settings_dict.get('template_id')
+ if list_id:
+ list_obj = list_obj.search([('list_id', '=', list_id)]).odoo_list_id
+ if template_id:
+ template_obj = template_obj.search([('template_id', '=', template_id), ('account_id', '=', account.id)])
+ status = values_dict.get('status')
+ subject_line = settings_dict.get('subject_line') or settings_dict.get('title')
+ try:
+ email_from = formataddr((settings_dict.get('from_name'), settings_dict.get('reply_to')))
+ except Exception as e:
+ email_from = self.env['mail.message']._get_default_from()
+ prepared_vals = {
+ 'create_date': values_dict.get('create_time'),
+ 'sent_date': values_dict.get('send_time'),
+ 'name': subject_line,
+ 'mailchimp_id': mailchimp_id,
+ 'mailing_model_id': self.env.ref('mass_mailing.model_mail_mass_mailing_list').id,
+ 'contact_list_ids': [(6, 0, list_obj.ids)],
+ 'mailchimp_template_id': template_obj.id,
+ 'mailchimp_champ_type': values_dict.get('type'),
+ 'email_from': email_from,
+ 'reply_to': email_from,
+ }
+ if status in ['save', 'paused']:
+ prepared_vals.update({'state': 'draft'})
+ elif status == 'schedule':
+ prepared_vals.update({'state': 'in_queue'})
+ elif status == 'sending':
+ prepared_vals.update({'state': 'sending'})
+ elif status == 'sent':
+ fetch_needed = True
+ prepared_vals.update({'state': 'done'})
+ for item in DATE_CONVERSION:
+ if prepared_vals.get(item, False) == '':
+ prepared_vals[item] = False
+ if prepared_vals.get(item, False):
+ prepared_vals[item] = account.covert_date(prepared_vals.get(item))
+ existing_list = self.search([('mailchimp_id', '=', mailchimp_id)])
+ if not existing_list:
+ existing_list = self.create(prepared_vals)
+ self.env.cr.execute("""
+ UPDATE
+ mail_mass_mailing
+ SET create_date = '%s'
+ WHERE id = %s
+ """ % (prepared_vals.get('create_date'), existing_list.id))
+ else:
+ existing_list.write(prepared_vals)
+ existing_list._onchange_model_and_list()
+ existing_list.body_html = False
+ if fetch_needed:
+ existing_list.process_email_activity_report()
+ return True
+
+ @api.multi
+ def fetch_campaign(self):
+ self.ensure_one()
+ if not self.mailchimp_id:
+ return True
+ account = self.mailchimp_template_id.account_id
+ params_vals = {
+ 'fields': 'id,type,status,create_time,send_time,settings.template_id,settings.subject_line,settings.title,settings.from_name,settings.reply_to,recipients.list_id'}
+ response = account._send_request('campaigns/%s' % self.mailchimp_id, {}, params=params_vals)
+ self.create_or_update_campaigns(response, account=account)
+ return True
+
+ @api.multi
+ def import_campaigns(self, account=False):
+ if not account:
+ raise Warning("MailChimp Account not defined to import Campaigns")
+ count = 1000
+ offset = 0
+ campaigns_list = []
+ while True:
+ prepared_vals = {'count': count, 'offset': offset}
+ response = account._send_request('campaigns', {}, params=prepared_vals)
+ if len(response.get('campaigns')) == 0:
+ break
+ if isinstance(response.get('campaigns'), dict):
+ campaigns_list += [response.get('campaigns')]
+ campaigns_list += response.get('campaigns')
+ offset = offset + 1000
+ for campaigns_dict in campaigns_list:
+ self.create_or_update_campaigns(campaigns_dict, account=account)
+ return True
+
+ @api.model
+ def _prepare_vals_for_export(self):
+ self.ensure_one()
+ from_name, from_email = parseaddr(self.email_from)
+ reply_to_name, reply_to_email = parseaddr(self.reply_to)
+ settings_dict = {'subject_line': self.name, 'title': self.name, 'from_name': from_name,
+ 'reply_to': reply_to_email, 'template_id': int(self.mailchimp_template_id.template_id)}
+ prepared_vals = {'type': 'regular',
+ 'recipients': {'list_id': self.contact_list_ids.mailchimp_list_id.list_id, },
+ 'settings': settings_dict}
+ if self.mailchimp_segment_id.mailchimp_id:
+ prepared_vals['recipients'].update({'segment_opts': {'saved_segment_id': int(self.mailchimp_segment_id.mailchimp_id)}})
+ return prepared_vals
+
+ @api.one
+ def export_to_mailchimp(self, account=False):
+ if self.mailchimp_id:
+ return True
+ if not account:
+ raise Warning("MailChimp Account not defined in selected Template.")
+ prepared_vals = self._prepare_vals_for_export()
+ response = account._send_request('campaigns', prepared_vals, method='POST')
+ if response.get('id', False):
+ self.write({'mailchimp_id': response['id']})
+ else:
+ ValidationError(_("MailChimp Identification wasn't received. Please try again!"))
+ self._cr.commit()
+ return True
+
+ @api.one
+ def send_now_mailchimp(self, account=False):
+ if not account:
+ raise Warning("MailChimp Account not defined in selected Template.")
+ response = account._send_request('campaigns/%s/actions/send' % self.mailchimp_id, {}, method='POST')
+ return True
+
+ @api.multi
+ def send_test_mail_mailchimp(self, test_emails):
+ self.ensure_one()
+ self.export_to_mailchimp(self.mailchimp_template_id.account_id)
+ prepared_vals = {'test_emails': test_emails, 'send_type': 'html'}
+ response = self.mailchimp_template_id.account_id._send_request('campaigns/%s/actions/test' % self.mailchimp_id,
+ prepared_vals, method='POST')
+ return True
+
+ @api.multi
+ def schedule_mailchimp_champaign(self, schedule_date):
+ self.ensure_one()
+ self.export_to_mailchimp(self.mailchimp_template_id.account_id)
+ prepared_vals = {'schedule_time': schedule_date.isoformat()}
+ response = self.mailchimp_template_id.account_id._send_request(
+ 'campaigns/%s/actions/schedule' % self.mailchimp_id,
+ prepared_vals, method='POST')
+ return True
+
+ @api.multi
+ def cancel_mass_mailing(self):
+ res = super(MassMailing, self).cancel_mass_mailing()
+ if self.mailchimp_id and self.mailchimp_template_id:
+ self.mailchimp_template_id.account_id._send_request('campaigns/%s/actions/cancel-send' % self.mailchimp_id,
+ {}, method='POST')
+ if self.schedule_date:
+ self.mailchimp_template_id.account_id._send_request(
+ 'campaigns/%s/actions/unschedule' % self.mailchimp_id,
+ {}, method='POST')
+ return res
+
+ @api.multi
+ def put_in_queue(self):
+ res = super(MassMailing, self).put_in_queue()
+ for record in self.filtered(lambda x: x.mailchimp_template_id):
+ if len(record.contact_list_ids) > 1:
+ raise ValidationError(_("Multiple list is not allowed while going with MailChimp!"))
+ if record.contact_list_ids.filtered(lambda x: not x.mailchimp_list_id):
+ raise ValidationError(_("Please provide MailChimp list as you selected MailChimp Template!"))
+ record.export_to_mailchimp(record.mailchimp_template_id.account_id)
+ if record.mailchimp_id:
+ record.send_now_mailchimp(record.mailchimp_template_id.account_id)
+ record.process_send_to_activity_report()
+ record.fetch_campaign()
+ return res
+
+ @api.model
+ def _process_mass_mailing_queue(self):
+ mass_mailings = self.search(
+ [('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()),
+ ('schedule_date', '=', False)])
+ for mass_mailing in mass_mailings:
+ user = mass_mailing.write_uid or self.env.user
+ mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get())
+ if mass_mailing.mailchimp_id:
+ mass_mailing.fetch_campaign()
+ continue
+ if len(mass_mailing.get_remaining_recipients()) > 0:
+ mass_mailing.state = 'sending'
+ mass_mailing.send_mail()
+ else:
+ mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
+
+ @api.onchange('mailing_model_id', 'contact_list_ids')
+ def _onchange_model_and_list(self):
+ res = super(MassMailing, self)._onchange_model_and_list()
+ mailing_domain = []
+ list_obj = self.env['mailchimp.lists']
+ list_ids = list_obj.search([('odoo_list_id', 'in', self.contact_list_ids.ids)])
+
+ self.mailchimp_list_id = list_ids and list_ids[0] or False
+ if self.mailchimp_list_id:
+ self.email_from = formataddr((self.mailchimp_list_id.from_name, self.mailchimp_list_id.from_email))
+ self.reply_to = formataddr((self.mailchimp_list_id.from_name, self.mailchimp_list_id.from_email))
+ return res
diff --git a/ext/custom-addons/mailchimp/models/mass_mailing_contact.py b/ext/custom-addons/mailchimp/models/mass_mailing_contact.py
new file mode 100644
index 00000000..a1df2587
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mass_mailing_contact.py
@@ -0,0 +1,192 @@
+import re
+import hashlib
+from datetime import datetime
+from odoo import api, fields, models, _
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+
+EMAIL_PATTERN = '([^ ,;<@]+@[^> ,;]+)'
+
+
+def _partner_split_name(partner_name):
+ return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])]
+
+
+class massMailingContact(models.Model):
+ _inherit = "mail.mass_mailing.contact"
+
+ @api.multi
+ def get_partner(self, email):
+ query = """
+ SELECT id
+ FROM res_partner
+ WHERE LOWER(substring(email, '([^ ,;<@]+@[^> ,;]+)')) = LOWER(substring('{}', '([^ ,;<@]+@[^> ,;]+)'))""".format(email)
+ self._cr.execute(query)
+ return self._cr.fetchone() or False
+
+ @api.model
+ def create(self, vals):
+ res = super(massMailingContact, self).create(vals)
+ if vals.get('email', False):
+ partner_record = self.get_partner(vals.get('email'))
+ res.related_partner_id = partner_record
+ return res
+
+ @api.multi
+ def write(self, vals):
+ if vals.get('email', False) and not self._context.get('do_not_update', False):
+ partner_record = self.get_partner(vals.get('email'))
+ vals.update({'related_partner_id': partner_record})
+ res = super(massMailingContact, self).write(vals)
+ return res
+
+ @api.depends('subscription_list_ids', 'subscription_list_ids.mailchimp_id', 'subscription_list_ids.list_id')
+ def _get_pending_for_export(self):
+ available_mailchimp_lists = self.env['mailchimp.lists'].search([])
+ lists = available_mailchimp_lists.mapped('odoo_list_id').ids
+ for record in self:
+ if record.subscription_list_ids.filtered(lambda x: x.list_id.id in lists and not x.mailchimp_id):
+ record.pending_for_export = True
+ else:
+ record.pending_for_export = False
+
+ @api.depends('email')
+ def _compute_is_email_valid(self):
+ for record in self:
+ record.is_email_valid = re.match(EMAIL_PATTERN, record.email)
+
+ @api.depends('email')
+ def _compute_related_partner_id(self):
+ for record in self:
+ query = """
+ SELECT id
+ FROM res_partner
+ WHERE LOWER(substring(email, '([^ ,;<@]+@[^> ,;]+)')) = LOWER(substring('{}', '([^ ,;<@]+@[^> ,;]+)'))""".format(record.email)
+ self._cr.execute(query)
+ partner_record = self._cr.fetchone()
+ if partner_record:
+ record.related_partner_id = partner_record[0]
+ else:
+ record.related_partner_id = False
+
+ is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True)
+ pending_for_export = fields.Boolean(compute="_get_pending_for_export", string="Pending For Export", store=True)
+ related_partner_id = fields.Many2one('res.partner', 'Related Customer', compute="_compute_related_partner_id", help='Display related customer by matching Email address.', store=True)
+ subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'contact_id', string='Subscription Information')
+
+ def _prepare_vals_for_merge_fields(self, mailchimp_list_id):
+ self.ensure_one()
+ merge_fields_vals = {}
+ partner_id = self.related_partner_id
+ for custom_field in mailchimp_list_id.merge_field_ids:
+ if custom_field.type == 'address' and partner_id:
+ address = {'addr1': partner_id.street or '',
+ 'addr2': partner_id.street2 or '',
+ 'city': partner_id.city or '',
+ 'state': partner_id.state_id.name if partner_id.state_id else '',
+ 'zip': partner_id.zip,
+ 'country': partner_id.country_id.code if partner_id.country_id else ''}
+ merge_fields_vals.update({custom_field.tag: address})
+ elif custom_field.tag == 'FNAME':
+ merge_fields_vals.update({custom_field.tag: _partner_split_name(self.name)[0] if _partner_split_name(self.name)[0] else _partner_split_name(self.name)[1]})
+ elif custom_field.tag == 'LNAME':
+ merge_fields_vals.update({custom_field.tag: _partner_split_name(self.name)[1] if _partner_split_name(self.name)[0] else _partner_split_name(self.name)[0]})
+ elif custom_field.type in ['date', 'birthday']:
+ value = getattr(partner_id or self, custom_field.field_id.name) if custom_field.field_id and hasattr(partner_id or self, custom_field.field_id.name) else ''
+ if value:
+ value = datetime.strptime(value, DEFAULT_SERVER_DATETIME_FORMAT).strftime(custom_field.date_format)
+ # value = value.strftime(custom_field.date_format)
+ merge_fields_vals.update({custom_field.tag: value or ''})
+ else:
+ value = getattr(partner_id or self, custom_field.field_id.name) if custom_field.field_id and hasattr(partner_id or self, custom_field.field_id.name) else ''
+ merge_fields_vals.update({custom_field.tag: value or ''})
+ return merge_fields_vals
+
+ @api.multi
+ def action_export_to_mailchimp(self):
+ available_mailchimp_lists = self.env['mailchimp.lists'].search([])
+ lists = available_mailchimp_lists.mapped('odoo_list_id').ids
+ for record in self:
+ lists_to_export = record.subscription_list_ids.filtered(
+ lambda x: x.list_id.id in lists and not x.mailchimp_id)
+ for list in lists_to_export:
+ mailchimp_list_id = list.list_id.mailchimp_list_id
+ merge_fields_vals = record._prepare_vals_for_merge_fields(mailchimp_list_id)
+ # address = ''
+ # phone = ''
+ # partner_id = record.related_partner_id
+ # if partner_id:
+ # address = {'addr1': partner_id.street or '',
+ # 'addr2': partner_id.street2 or '',
+ # 'city': partner_id.city or '',
+ # 'state': partner_id.state_id.name if partner_id.state_id else '',
+ # 'zip': partner_id.zip,
+ # 'country': partner_id.country_id.code if partner_id.country_id else ''}
+ # phone = partner_id.phone or partner_id.mobile
+ prepared_vals = {"email_address": record.email.lower(),
+ "status": "unsubscribed" if record.opt_out else "subscribed",
+ "merge_fields": merge_fields_vals,
+ "tags": [tag.name for tag in record.tag_ids]}
+ response = mailchimp_list_id.account_id._send_request('lists/%s/members' % mailchimp_list_id.list_id,
+ prepared_vals, method='POST')
+ if response.get('web_id', False):
+ email_address = response.get('email_address')
+ md5_email = hashlib.md5(email_address.encode('utf-8')).hexdigest()
+ list.write({'mailchimp_id': response.get('web_id', False), 'md5_email': md5_email})
+ return True
+
+ @api.multi
+ def action_update_to_mailchimp(self):
+ available_mailchimp_lists = self.env['mailchimp.lists'].search([])
+ lists = available_mailchimp_lists.mapped('odoo_list_id').ids
+ for record in self:
+ lists_to_export = record.subscription_list_ids.filtered(
+ lambda x: x.list_id.id in lists and x.mailchimp_id)
+ for list in lists_to_export:
+ mailchimp_list_id = list.list_id.mailchimp_list_id
+ merge_fields_vals = record._prepare_vals_for_merge_fields(mailchimp_list_id)
+ # partner_id = record.related_partner_id
+ # address = ''
+ # phone = ''
+ # if partner_id:
+ # address = {'addr1': partner_id.street or '',
+ # 'addr2': partner_id.street2 or '',
+ # 'city': partner_id.city or '',
+ # 'state': partner_id.state_id.name if partner_id.state_id else '',
+ # 'zip': partner_id.zip,
+ # 'country': partner_id.country_id.code if partner_id.country_id else ''}
+ # phone = partner_id.phone or partner_id.mobile
+ prepared_vals = {"email_address": record.email.lower(),
+ "status": "unsubscribed" if record.opt_out else "subscribed",
+ 'merge_fields': merge_fields_vals, }
+ response = mailchimp_list_id.account_id._send_request(
+ 'lists/%s/members/%s' % (mailchimp_list_id.list_id, list.md5_email),
+ prepared_vals, method='PATCH')
+ tag_res = record.update_tag_on_mailchimp(response, mailchimp_list_id, list.md5_email)
+ if response.get('web_id', False):
+ email_address = response.get('email_address')
+ md5_email = hashlib.md5(email_address.encode('utf-8')).hexdigest()
+ list.write({'mailchimp_id': response.get('web_id', False), 'md5_email': md5_email})
+ return True
+
+ def update_tag_on_mailchimp(self, response, mailchimp_list_id, md5_email):
+ tag_list = []
+ tags = response.get('tags', []) and [tag['name'] for tag in response.get('tags', [])] or []
+ tag_name_list = self.tag_ids.mapped('name')
+ unique_tags = list(set(tags + tag_name_list))
+ for tag in unique_tags:
+ if tag in tag_name_list:
+ tag_dict = {'name': tag, 'status': 'active'}
+ else:
+ tag_dict = {'name': tag, 'status': 'inactive'}
+ tag_list.append(tag_dict)
+ tag_vals = {'tags': tag_list}
+ tag_res = mailchimp_list_id.account_id._send_request('lists/%s/members/%s/tags' % (mailchimp_list_id.list_id, md5_email), tag_vals, method='POST')
+ return tag_res
+
+ def fetch_specific_member_data(self, mailchimp_list_id, md5_email):
+ member_response = mailchimp_list_id.account_id._send_request('lists/%s/members/%s' % (mailchimp_list_id.list_id, md5_email), {}, method='GET')
+ tag_list = member_response.get('tags', [])
+ tag_ids = self.env['res.partner.category']
+ if tag_list:
+ tag_ids = self.env['res.partner.category'].create_or_update_tags(tag_list)
+ return tag_ids
diff --git a/ext/custom-addons/mailchimp/models/mass_mailing_list.py b/ext/custom-addons/mailchimp/models/mass_mailing_list.py
new file mode 100644
index 00000000..b2e5d60b
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mass_mailing_list.py
@@ -0,0 +1,17 @@
+from odoo import api, fields, models, _
+
+
+class MassMailingList(models.Model):
+ _inherit = "mail.mass_mailing.list"
+
+ @api.one
+ def _compute_mailchimp_list_id(self):
+ mailchimp_list_obj = self.env['mailchimp.lists']
+ list_id = mailchimp_list_obj.search([('odoo_list_id', '=', self.id)])
+ if list_id:
+ self.mailchimp_list_id = list_id.id
+ else:
+ self.mailchimp_list_id = False
+
+ mailchimp_list_id = fields.Many2one('mailchimp.lists', compute='_compute_mailchimp_list_id',
+ string="Associated MailChimp List")
diff --git a/ext/custom-addons/mailchimp/models/mass_mailing_list_contact_rel.py b/ext/custom-addons/mailchimp/models/mass_mailing_list_contact_rel.py
new file mode 100644
index 00000000..3efa002c
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/mass_mailing_list_contact_rel.py
@@ -0,0 +1,37 @@
+from odoo import api, fields, models, _
+
+
+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 = 'mail.mass_mailing.list_contact_rel'
+ _description = 'Mass Mailing Subscription Information'
+ _table = 'mail_mass_mailing_contact_list_rel'
+ _rec_name = 'contact_id'
+
+ @api.depends('list_id')
+ def _compute_mailchimp_list_id(self):
+ mailchimp_list_obj = self.env['mailchimp.lists']
+ for record in self:
+ list_id = mailchimp_list_obj.search([('odoo_list_id', '=', record.list_id.id)], limit=1)
+ record.mailchimp_list_id = list_id.id
+
+ contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True)
+ list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True)
+ mailchimp_id = fields.Char("MailChimp ID", readonly=1, copy=False)
+ mailchimp_list_id = fields.Many2one(compute="_compute_mailchimp_list_id", string="MailChimp List", store=True)
+ md5_email = fields.Char("MD5 Email", readonly=1, copy=False)
+ related_partner_id = fields.Many2one('res.partner', related='contact_id.related_partner_id', string='Related Customer', readonly=True, store=True)
+ opt_out = fields.Boolean(related="contact_id.opt_out", string='Opt Out', help='The contact has chosen not to receive mails anymore from this list')
+
+ _sql_constraints = [
+ ('unique_contact_list', 'unique (contact_id, list_id)',
+ 'A contact cannot be subscribed multiple times to the same list!')
+ ]
+
+ @api.model_cr
+ def init(self):
+ self._cr.execute("""select * from information_schema.columns where column_name = 'id' and table_name = 'mail_mass_mailing_contact_list_rel'""")
+ if not self._cr.fetchone():
+ self._cr.execute("""ALTER TABLE mail_mass_mailing_contact_list_rel ADD COLUMN id SERIAL PRIMARY KEY""")
diff --git a/ext/custom-addons/mailchimp/models/res_partner.py b/ext/custom-addons/mailchimp/models/res_partner.py
new file mode 100644
index 00000000..8ea65ae1
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/res_partner.py
@@ -0,0 +1,40 @@
+from odoo import fields, models, api
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel',
+ 'related_partner_id', string='Subscription Information', domain=[('mailchimp_list_id','!=',False)])
+
+ @api.multi
+ def get_mailing_partner(self, email):
+ query = """
+ SELECT id
+ FROM mail_mass_mailing_contact
+ WHERE LOWER(substring(email, '([^ ,;<@]+@[^> ,;]+)')) = LOWER(substring('{}', '([^ ,;<@]+@[^> ,;]+)'))""".format(email)
+ self._cr.execute(query)
+ return self._cr.fetchone()
+
+ @api.model
+ def create(self, vals):
+ res = super(ResPartner, self).create(vals)
+ if vals.get('email', False):
+ mailing_contact = self.env['mail.mass_mailing.contact']
+ partner_record = self.get_mailing_partner(vals.get('email'))
+ if partner_record:
+ mailing_contact.browse(partner_record[0]).related_partner_id = res.id
+ return res
+
+ @api.multi
+ def write(self, vals):
+ if vals.get('email', False):
+ mailing_contact = self.env['mail.mass_mailing.contact']
+ partner_record = self.get_mailing_partner(vals.get('email'))
+ if partner_record:
+ mailing_contact.browse(partner_record[0]).related_partner_id = self.id
+ else:
+ partner_record = self.get_mailing_partner(self.email)
+ if partner_record:
+ mailing_contact.browse(partner_record[0]).write({'related_partner_id':False})
+ res = super(ResPartner, self).write(vals)
+ return res
diff --git a/ext/custom-addons/mailchimp/models/res_partner_category.py b/ext/custom-addons/mailchimp/models/res_partner_category.py
new file mode 100644
index 00000000..b61c52ce
--- /dev/null
+++ b/ext/custom-addons/mailchimp/models/res_partner_category.py
@@ -0,0 +1,24 @@
+from odoo import fields,api,models
+
+class ResPartnerCategory(models.Model):
+ _inherit = 'res.partner.category'
+
+ mailchimp_id = fields.Char('Mailchimp Id')
+
+ @api.multi
+ def create_or_update_tags(self, values_dict, account=False):
+ tag_ids = self
+ for val in values_dict:
+ tag_id = val.get('id')
+ existing_list = self.search([('mailchimp_id', '=', tag_id)])
+ val.update({'mailchimp_id':val.pop('id')})
+ if not existing_list:
+ existing_list =self.search([('name', '=', val.get('name'))])
+ if not existing_list:
+ existing_list = self.create(val)
+ else:
+ existing_list.write(val)
+ else:
+ existing_list.write(val)
+ tag_ids += existing_list
+ return tag_ids
\ No newline at end of file
diff --git a/ext/custom-addons/mailchimp/security/ir.model.access.csv b/ext/custom-addons/mailchimp/security/ir.model.access.csv
new file mode 100644
index 00000000..a5b18d1f
--- /dev/null
+++ b/ext/custom-addons/mailchimp/security/ir.model.access.csv
@@ -0,0 +1,13 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_mailchimp_accounts_public,mailchimp.accounts,mailchimp.model_mailchimp_accounts,,1,0,0,0
+access_mailchimp_lists_public,mailchimp.lists,mailchimp.model_mailchimp_lists,,1,0,0,0
+access_mailchimp_lists_stats_public,mailchimp.lists.stats,mailchimp.model_mailchimp_lists_stats,,1,0,0,0
+access_mailchimp_templates_public,mailchimp.templates,mailchimp.model_mailchimp_templates,,1,0,0,0
+access_mailchimp_accounts_user,mailchimp.accounts.user,mailchimp.model_mailchimp_accounts,mass_mailing.group_mass_mailing_user,1,1,1,1
+access_mailchimp_lists_user,mailchimp.lists.user,mailchimp.model_mailchimp_lists,mass_mailing.group_mass_mailing_user,1,1,1,1
+access_mailchimp_lists_stats_user,mailchimp.lists.stats.user,mailchimp.model_mailchimp_lists_stats,mass_mailing.group_mass_mailing_user,1,1,1,1
+access_mailchimp_templates_user,mailchimp.templates.user,mailchimp.model_mailchimp_templates,mass_mailing.group_mass_mailing_user,1,1,1,1
+access_mailchimp_segments_public,mailchimp.segments,mailchimp.model_mailchimp_segments,,1,0,0,0
+access_mailchimp_segments_user,mailchimp.segments.user,mailchimp.model_mailchimp_segments,mass_mailing.group_mass_mailing_user,1,1,1,1
+access_mailchimp_merge_fields_public,mailchimp.merge.fields,mailchimp.model_mailchimp_merge_fields,,1,0,0,0
+access_mailchimp_merge_fields_user,mailchimp.merge.fields.user,mailchimp.model_mailchimp_merge_fields,mass_mailing.group_mass_mailing_user,1,1,1,1
diff --git a/ext/custom-addons/mailchimp/static/description/all_in_one_dashboard.png b/ext/custom-addons/mailchimp/static/description/all_in_one_dashboard.png
new file mode 100644
index 00000000..6fb4bc0a
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/all_in_one_dashboard.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/fedex_odoo.png b/ext/custom-addons/mailchimp/static/description/fedex_odoo.png
new file mode 100644
index 00000000..bb9e2806
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/fedex_odoo.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/icon.png b/ext/custom-addons/mailchimp/static/description/icon.png
new file mode 100644
index 00000000..6ce9b40b
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/icon.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/1.png b/ext/custom-addons/mailchimp/static/description/img/1.png
new file mode 100644
index 00000000..f46be129
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/1.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/10.png b/ext/custom-addons/mailchimp/static/description/img/10.png
new file mode 100644
index 00000000..f6768cc0
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/10.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/11.png b/ext/custom-addons/mailchimp/static/description/img/11.png
new file mode 100644
index 00000000..c3e6c322
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/11.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/12.png b/ext/custom-addons/mailchimp/static/description/img/12.png
new file mode 100644
index 00000000..05d135b8
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/12.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/13.png b/ext/custom-addons/mailchimp/static/description/img/13.png
new file mode 100644
index 00000000..67a7563f
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/13.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/14.png b/ext/custom-addons/mailchimp/static/description/img/14.png
new file mode 100644
index 00000000..a95ce10b
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/14.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/15.1.png b/ext/custom-addons/mailchimp/static/description/img/15.1.png
new file mode 100644
index 00000000..849f49c2
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/15.1.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/15.png b/ext/custom-addons/mailchimp/static/description/img/15.png
new file mode 100644
index 00000000..afb1a8d2
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/15.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/16.png b/ext/custom-addons/mailchimp/static/description/img/16.png
new file mode 100644
index 00000000..a11e4675
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/16.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/17.gif b/ext/custom-addons/mailchimp/static/description/img/17.gif
new file mode 100644
index 00000000..31949ef7
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/17.gif differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/2.png b/ext/custom-addons/mailchimp/static/description/img/2.png
new file mode 100644
index 00000000..1d7ececd
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/2.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/3.png b/ext/custom-addons/mailchimp/static/description/img/3.png
new file mode 100644
index 00000000..fbb3d75d
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/3.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/4.gif b/ext/custom-addons/mailchimp/static/description/img/4.gif
new file mode 100644
index 00000000..2e162cf1
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/4.gif differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/4.png b/ext/custom-addons/mailchimp/static/description/img/4.png
new file mode 100644
index 00000000..1642c242
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/4.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/5.gif b/ext/custom-addons/mailchimp/static/description/img/5.gif
new file mode 100644
index 00000000..56c99d12
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/5.gif differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/5.png b/ext/custom-addons/mailchimp/static/description/img/5.png
new file mode 100644
index 00000000..240042e2
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/5.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/6.png b/ext/custom-addons/mailchimp/static/description/img/6.png
new file mode 100644
index 00000000..69673e3f
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/6.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/7.png b/ext/custom-addons/mailchimp/static/description/img/7.png
new file mode 100644
index 00000000..2de2a4af
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/7.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/8.png b/ext/custom-addons/mailchimp/static/description/img/8.png
new file mode 100644
index 00000000..9e93e989
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/8.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/9.png b/ext/custom-addons/mailchimp/static/description/img/9.png
new file mode 100644
index 00000000..2da58615
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/9.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/img/TeqStars.png b/ext/custom-addons/mailchimp/static/description/img/TeqStars.png
new file mode 100644
index 00000000..bf59fd02
Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/img/TeqStars.png differ
diff --git a/ext/custom-addons/mailchimp/static/description/index.html b/ext/custom-addons/mailchimp/static/description/index.html
new file mode 100644
index 00000000..300745a2
--- /dev/null
+++ b/ext/custom-addons/mailchimp/static/description/index.html
@@ -0,0 +1,451 @@
+
+
+
+ MailChimp Odoo Integration
+
+ Seamless one click integration to synchronize your contact list, campaign, templates between Odoo and
+ MailChimp. Level up your email marketing, get more sales and happy customers.
+
+ Import Lists/Audiences from MailChimp to Odoo.
+
+
+ Import Member/contacts from MailChimp to Odoo.
+
+
+ Import Templates from MailChimp to Odoo.
+
+
+ Import Campaigns from MailChimp to Odoo.
+
+
+ Import Campaign Reports from MailChimp to Odoo.
+
+
+ Import Merge Fields from MailChimp to Odoo and map it with Odoo fields.
+
+
+ Import Segments from MailChimp to Odoo and that will use to filter recipients while sending Campaign
+
+
+ Export/Update Lists/Audiences from Odoo to MailChimp
+
+
+ Export/Update Member/contacts from Odoo to MailChimp
+
+
+ Create Campaign by using MailChimp Template applying recipients filter using Segments and send it to MailChimp
+
+
+ Update record on the fly by setting up Webhook
+
+
+ Easy to get Statistics of Campaigns and Audiences.
+
+
+ Use Multiple Accounts as well.
+
+
+
+
+
+
+
+
+
+
+
Setup MailChimp account
+
+
+
+
+
+
+
Configure auto member synchronization
+
+
+
+
+
+
+
View available Lists and Campaigns from
+ Account
+
+
+
+
+
+
+
MailChimp Lists/Audiences
+
+
+
+
+
+
+
Setting up or change any setting directly in Odoo
+ and Update it to MailChimp.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
MailChimp Template
+
+
+
+
+
+
+
Do multiple operation on one click
+
+
+
+
+
+
+
Easy to identify MailChimp list from other
+ lists
+
+
+
+
+
+
+
Send Campaign with filtering recipients by selecting segments.
+
+
+
+
+
+
+
Send Campaign by creating mass mailing record and
+ get statistics back to Odoo
+
+
+
+
+
+
+
+
+
+
Quick shoot of export and update process
+
+
+
+
+
+
+
Setup Webhook to get real-time information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v11.0.3.0
Released on 17 October 2019 (UTC).
+
+
+
+
+ ADD
+ Allow to Import Merge field (Custom fields) and map it with Odoo field. So, user can configure n no of custom fields to export or update to MailChimp.
+
+
+
+
+
+
+ v11.0.2.0
Released on 16 October 2019 (UTC).
+
+
+
+
+ FIX
+ Minor bugs and increase speed of import/fetch member process.
+
+
+
+
+ ADD
+ Introduced Segments to filter recipients while sending campaign.
+