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. +

+
+
+ +
+
+ +
+
+ +
+
+
+ MailChimp Tutorial +
+
+
+ +
+
+ +
+ +
+
+ +
+
+ + +
+
+
+

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. +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
You can drop mail at info@teqstars.com +
+
+
+
+ +
+
Yes, We provide 90 days free support for bugs. +
+
+
+ +
+
+
+
+
+
+
+
+ + + + +
+ +
+
+
+
+ + + +
+
+
+
+ +
+
+

Need Any Help?

+

Contact us for any problem or request!

+
+
+
+ Want Demo or Need Support? +
+ +
diff --git a/ext/custom-addons/mailchimp/static/description/mailchimp_odoo.png b/ext/custom-addons/mailchimp/static/description/mailchimp_odoo.png new file mode 100644 index 00000000..f3fb4d31 Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/mailchimp_odoo.png differ diff --git a/ext/custom-addons/mailchimp/static/description/mailchimp_video.jpg b/ext/custom-addons/mailchimp/static/description/mailchimp_video.jpg new file mode 100644 index 00000000..b793b4d4 Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/mailchimp_video.jpg differ diff --git a/ext/custom-addons/mailchimp/static/description/nearby_warehouse.png b/ext/custom-addons/mailchimp/static/description/nearby_warehouse.png new file mode 100644 index 00000000..1ad2f9d4 Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/nearby_warehouse.png differ diff --git a/ext/custom-addons/mailchimp/static/description/pos_discount.png b/ext/custom-addons/mailchimp/static/description/pos_discount.png new file mode 100644 index 00000000..a7ccafdd Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/pos_discount.png differ diff --git a/ext/custom-addons/mailchimp/static/description/pos_user_control.png b/ext/custom-addons/mailchimp/static/description/pos_user_control.png new file mode 100644 index 00000000..9e609d68 Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/pos_user_control.png differ diff --git a/ext/custom-addons/mailchimp/static/description/product_alias.png b/ext/custom-addons/mailchimp/static/description/product_alias.png new file mode 100644 index 00000000..8d1dfabe Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/product_alias.png differ diff --git a/ext/custom-addons/mailchimp/static/description/shipstation_odoo.png b/ext/custom-addons/mailchimp/static/description/shipstation_odoo.png new file mode 100644 index 00000000..e309ce37 Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/shipstation_odoo.png differ diff --git a/ext/custom-addons/mailchimp/static/description/sticky_header.jpg b/ext/custom-addons/mailchimp/static/description/sticky_header.jpg new file mode 100644 index 00000000..6bcdd43c Binary files /dev/null and b/ext/custom-addons/mailchimp/static/description/sticky_header.jpg differ diff --git a/ext/custom-addons/mailchimp/static/src/scss/mailchimp.scss b/ext/custom-addons/mailchimp/static/src/scss/mailchimp.scss new file mode 100644 index 00000000..a9b529bc --- /dev/null +++ b/ext/custom-addons/mailchimp/static/src/scss/mailchimp.scss @@ -0,0 +1,20 @@ +.o_kanban_view { + .oe_kanban_mailchimp { + .o_title { + margin-bottom: 16px; + } + .o_kanban_primary_bottom { + margin-top: 16px; + } + .oe_margin_top_8 { + margin-top: 8px; + } + .oe_margin_bottom_8 { + margin-bottom: 8px; + } + } +} + +.o_white_body { + background-color: white; +} diff --git a/ext/custom-addons/mailchimp/views/assets.xml b/ext/custom-addons/mailchimp/views/assets.xml new file mode 100644 index 00000000..17b6383b --- /dev/null +++ b/ext/custom-addons/mailchimp/views/assets.xml @@ -0,0 +1,8 @@ + + + + diff --git a/ext/custom-addons/mailchimp/views/mailchimp_accounts_view.xml b/ext/custom-addons/mailchimp/views/mailchimp_accounts_view.xml new file mode 100644 index 00000000..40863de8 --- /dev/null +++ b/ext/custom-addons/mailchimp/views/mailchimp_accounts_view.xml @@ -0,0 +1,145 @@ + + + + + mailchimp.accounts.form + mailchimp.accounts + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +

Publicity settings

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+

Form settings

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+

Campaign defaults

+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

New subscriber notifications

+

One by one

+
+ Get quick email alerts when subscribers join or leave this audience. +
+
+
+
+
+
+
+
+
+
+
+
+

Required Email Footer Content

+
+
+
+
+
+
+
+
+
+
+
+

Email Beamer

+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + mailchimp.lists.tree + mailchimp.lists + + + + + + + + + + + + + + + + + mail.mass_mailing.kanban + mail.mass_mailing + + + + + + + + + MailChimp + + + + + +
+
diff --git a/ext/custom-addons/mailchimp/views/res_partner_views.xml b/ext/custom-addons/mailchimp/views/res_partner_views.xml new file mode 100644 index 00000000..ce93aab8 --- /dev/null +++ b/ext/custom-addons/mailchimp/views/res_partner_views.xml @@ -0,0 +1,20 @@ + + + + res.partner.mailchimp.inherit + res.partner + + + + + + + + + + + + + + + diff --git a/ext/custom-addons/mailchimp/wizard/__init__.py b/ext/custom-addons/mailchimp/wizard/__init__.py new file mode 100644 index 00000000..4fb6113a --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/__init__.py @@ -0,0 +1,4 @@ +from . import import_export_operation_wizard +from . import test_mailing +from . import mass_mailing_schedule_date +from . import partner_export_update_wizard diff --git a/ext/custom-addons/mailchimp/wizard/import_export_operation_view.xml b/ext/custom-addons/mailchimp/wizard/import_export_operation_view.xml new file mode 100644 index 00000000..51571876 --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/import_export_operation_view.xml @@ -0,0 +1,40 @@ + + + + + Import/Export Operation + mailchimp.import.export.operation + +
+ + + + + + + + + + +
+
+
+
+
+ + + Import/Export Operations + ir.actions.act_window + mailchimp.import.export.operation + form + form + new + + + +
+
\ No newline at end of file diff --git a/ext/custom-addons/mailchimp/wizard/import_export_operation_wizard.py b/ext/custom-addons/mailchimp/wizard/import_export_operation_wizard.py new file mode 100644 index 00000000..4f07282b --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/import_export_operation_wizard.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class ImportExportOperation(models.TransientModel): + _name = "mailchimp.import.export.operation" + _description = "Import/Export Operation" + + account_ids = fields.Many2many('mailchimp.accounts', required=True, + help="Select Account from which you want to perform import/export operation") + + get_lists = fields.Boolean("Lists/Audiences", help="Obtains available lists from MailChimp") + get_templates = fields.Boolean("Templates", help="Get a list of an account's available templates.") + get_campaigns = fields.Boolean("Campaigns", help="Get a list of campaigns.") + + @api.model + def default_get(self, fields): + res = super(ImportExportOperation, self).default_get(fields) + accounts = self.env['mailchimp.accounts'].search([]) + res.update({'account_ids': [(6, 0, accounts.ids)]}) + return res + + @api.multi + def process_operation(self): + for account in self.account_ids: + if self.get_lists: + account.import_lists() + if self.get_templates: + account.import_templates() + if self.get_campaigns: + account.import_campaigns() + return True diff --git a/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date.py b/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date.py new file mode 100644 index 00000000..c2eaa88e --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class MassMailingScheduleDate(models.TransientModel): + _name = 'mass.mailing.schedule.date' + _description = 'Mass Mailing Scheduling' + + schedule_date = fields.Datetime(string='Schedule in the Future') + mass_mailing_id = fields.Many2one('mail.mass_mailing', required=True) + + @api.constrains('schedule_date') + def _check_schedule_date(self): + for scheduler in self: + if scheduler.schedule_date < fields.Datetime.now(): + raise ValidationError(_('Please select a date equal/or greater than the current date.')) + + def set_schedule_date(self): + self.ensure_one() + mailing = self.mass_mailing_id + if mailing.mailchimp_template_id: + schedule_date = datetime.strptime(self.schedule_date, DEFAULT_SERVER_DATETIME_FORMAT) + mailing.schedule_mailchimp_champaign(schedule_date) + self.mass_mailing_id.write({'schedule_date': self.schedule_date, 'state': 'in_queue'}) diff --git a/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date_view.xml b/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date_view.xml new file mode 100644 index 00000000..19df5c84 --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/mass_mailing_schedule_date_view.xml @@ -0,0 +1,32 @@ + + + + mass.mailing.schedule.date.view.form + mass.mailing.schedule.date + +
+ + + + + +

+ If you are using MailChimp Campaigns then Campaigns may only be scheduled to send on the + quarter-hour (:00, :15, :30, :45) +

+
+
+
+
+
+ + + When do you want to send your mailing? + mass.mailing.schedule.date + ir.actions.act_window + form + new + +
\ No newline at end of file diff --git a/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.py b/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.py new file mode 100644 index 00000000..ad6ce054 --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class ParterExportMailchimp(models.TransientModel): + _name = 'partner.export.mailchimp' + + odoo_list_ids = fields.Many2many('mailchimp.lists', string='MailChimp Lists', domain=[('odoo_list_id', '!=', False)]) + + @api.multi + def get_mailing_contact_id(self, partner_id, force_create=False): + mailing_contact_obj = self.env['mail.mass_mailing.contact'] + if not partner_id.email: + return False + query = """ + SELECT id + FROM mail_mass_mailing_contact + WHERE LOWER(substring(email, '([^ ,;<@]+@[^> ,;]+)')) = LOWER(substring('{}', '([^ ,;<@]+@[^> ,;]+)'))""".format( + partner_id.email) + self._cr.execute(query) + contact_id = self._cr.fetchone() + contact_id = contact_id[0] if contact_id else False + prepared_vals = {'name': partner_id.name, 'email': partner_id.email, 'tag_ids': [(6, 0, partner_id.category_id.ids)], 'country_id': partner_id.country_id.id} + if contact_id: + contact_id = mailing_contact_obj.browse(contact_id) + contact_id.write(prepared_vals) + if not contact_id and force_create: + contact_id = mailing_contact_obj.create(prepared_vals) + return contact_id.id + + @api.multi + def action_export_partner_mailchimp(self): + mailing_contact_obj = self.env['mail.mass_mailing.contact'] + partner_ids = self.env['res.partner'].search([('id', 'in', self._context.get('active_ids', []))]) + for partner_id in partner_ids: + for odoo_list_id in self.odoo_list_ids: + contact_id = self.get_mailing_contact_id(partner_id, force_create=True) + if contact_id: + contact_id = mailing_contact_obj.browse(contact_id) + if odoo_list_id.id not in contact_id.subscription_list_ids.mapped('list_id').mapped('mailchimp_list_id').ids: + vals = {'list_id': odoo_list_id.odoo_list_id.id, 'contact_id': contact_id.id} + contact_id.subscription_list_ids.create(vals) + contact_id.action_export_to_mailchimp() + return True + + @api.multi + def action_update_partner_mailchimp(self): + mailing_contact_obj = self.env['mail.mass_mailing.contact'] + partner_ids = self.env['res.partner'].search([('id', 'in', self._context.get('active_ids', []))]) + for partner_id in partner_ids: + contact_id = self.get_mailing_contact_id(partner_id) + if contact_id: + contact_id = mailing_contact_obj.browse(contact_id) + contact_id.action_update_to_mailchimp() + return True diff --git a/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.xml b/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.xml new file mode 100644 index 00000000..5582bccd --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/partner_export_update_wizard.xml @@ -0,0 +1,55 @@ + + + + + partner.export.mailchimp.form + partner.export.mailchimp + +
+ + + + + +
+ + +
+
+
+
+ + + Export to MailChimp + partner.export.mailchimp + form + form + new + + + + + + + +
diff --git a/ext/custom-addons/mailchimp/wizard/test_mailing.py b/ext/custom-addons/mailchimp/wizard/test_mailing.py new file mode 100644 index 00000000..8968e142 --- /dev/null +++ b/ext/custom-addons/mailchimp/wizard/test_mailing.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, tools + + +class TestMassMailing(models.TransientModel): + _inherit = 'mail.mass_mailing.test' + + @api.multi + def send_mail_test(self): + self.ensure_one() + mails = self.env['mail.mail'] + mailing = self.mass_mailing_id + test_emails = tools.email_split(self.email_to) + if mailing.mailchimp_template_id: + return mailing.send_test_mail_mailchimp(test_emails) + mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout') + for test_mail in test_emails: + # Convert links in absolute URLs before the application of the shortener + mailing.write({'body_html': self.env['mail.thread']._replace_local_links(mailing.body_html)}) + body = tools.html_sanitize(mailing.body_html, sanitize_attributes=True, sanitize_style=True) + mail_values = { + 'email_from': mailing.email_from, + 'reply_to': mailing.reply_to, + 'email_to': test_mail, + 'subject': mailing.name, + 'body_html': mass_mail_layout.render({'body': body}, engine='ir.qweb', minimal_qcontext=True), + 'notification': True, + 'mailing_id': mailing.id, + 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], + 'auto_delete': True, + } + mail = self.env['mail.mail'].create(mail_values) + mails |= mail + mails.send() + return True