From 497ba7dab96a15c965e1e2824d9111772a98210b Mon Sep 17 00:00:00 2001 From: "Mansi Kariya (OpenERP Trainee)" Date: Tue, 30 Jul 2013 17:17:45 +0530 Subject: [PATCH 01/44] [IMP] purchases Configuration menus location and name changed bzr revid: mansi.mk.179@gmail.com-20130730114745-o2xzb0h63ze29stq --- addons/purchase/purchase_view.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index dffe1ed54ff..0463527461e 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -14,9 +14,6 @@ - Pricelist Versions @@ -36,14 +33,18 @@

+ + + - From d1e7ad9be6f01644567df0fb8d0e68e6c40aff5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 6 Aug 2013 17:10:18 +0200 Subject: [PATCH 02/44] [IMP] tools: added a regex for bounce email addresses bzr revid: tde@openerp.com-20130806151018-0uom07dbr8b7ycb3 --- openerp/tools/mail.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 5970ce47040..36a4c1610c4 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -297,6 +297,11 @@ command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) # group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE) +# Bounce regex +# Typical form of bounce is bounce-128-crm.lead-34@domain +# group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID +bounce_re = re.compile("[\w]+-(\d+)-?([\w.]+)?-?(\d+)?", re.UNICODE) + def generate_tracking_message_id(res_id): """Returns a string that can be used in the Message-ID RFC822 header field From 38a534dee0bf15a56a97874a01172cb67cd6b94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 6 Aug 2013 17:11:43 +0200 Subject: [PATCH 03/44] [IMP] mail: first implementation of tracking and bounce management. Added 2 fields on mail_mail to count read/bounce. Added bounce alias bounce-mail_id-model-res_id. Added message_receive_bounce method that try to incremetn message_bounce field. bzr revid: tde@openerp.com-20130806151143-7dw6xlj8n7mh0nqe --- addons/crm/crm_lead.py | 2 + addons/crm/crm_lead_view.xml | 1 + addons/mail/controllers/main.py | 21 ++++++--- addons/mail/data/mail_data.xml | 6 +++ addons/mail/mail_mail.py | 83 ++++++++++++++++++++++++--------- addons/mail/mail_mail_view.xml | 40 ++++++++++------ addons/mail/mail_thread.py | 28 +++++++++++ 7 files changed, 136 insertions(+), 45 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index c0f003f9792..52987de00e1 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -273,6 +273,8 @@ class crm_lead(format_address, osv.osv): selection=crm.AVAILABLE_STATES, string="Status", readonly=True, help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'), + # Messaging and marketing + 'message_bounce': fields.integer('Bounce'), # Only used for type opportunity 'probability': fields.float('Success Rate (%)',group_operator="avg"), 'planned_revenue': fields.float('Expected Revenue', track_visibility='always'), diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index aa5aac76b9d..113355eea83 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -182,6 +182,7 @@ + diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index ef3a60987d6..27373c34add 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -1,17 +1,17 @@ import base64 +import psycopg2 import openerp from openerp import SUPERUSER_ID -import openerp.addons.web.http as oeweb +import openerp.addons.web.http as http from openerp.addons.web.controllers.main import content_disposition +from openerp.addons.web.http import request -#---------------------------------------------------------- -# Controller -#---------------------------------------------------------- -class MailController(oeweb.Controller): + +class MailController(http.Controller): _cp_path = '/mail' - @oeweb.httprequest + @http.httprequest def download_attachment(self, req, model, id, method, attachment_id, **kw): Model = req.session.model(model) res = getattr(Model, method)(int(id), int(attachment_id)) @@ -24,7 +24,7 @@ class MailController(oeweb.Controller): ('Content-Disposition', content_disposition(filename, req))]) return req.not_found() - @oeweb.jsonrequest + @http.jsonrequest def receive(self, req): """ End-point to receive mail from an external SMTP server. """ dbs = req.jsonrequest.get('databases') @@ -38,3 +38,10 @@ class MailController(oeweb.Controller): except psycopg2.Error: pass return True + + @http.route('/mail/track//blank.gif', type='http', auth='admin') + def track_read_email(self, mail_id): + """ Email tracking. """ + mail_mail = request.registry.get('mail.mail') + mail_mail.set_opened(request.cr, request.uid, [mail_id]) + return False diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index 4e935e4aa88..8ba92a8205c 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -46,6 +46,12 @@ catchall + + + mail.bounce.alias + bounce + + Discussions diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index bf76216e7e4..5548608aeeb 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -61,15 +61,16 @@ class mail_mail(osv.Model): # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification # and during unlink() we will not cascade delete the parent and its attachments 'notification': fields.boolean('Is Notification', - help='Mail has been created to notify people of an existing mail.message') + help='Mail has been created to notify people of an existing mail.message'), + # Bounce and tracking + 'opened': fields.integer( + 'Opened', + help='Number of times this email has been seen, using the OpenERP tracking.'), + 'replied': fields.integer( + 'Reply Received', + help='If checked, a reply to this email has been received.'), } - def _get_default_from(self, cr, uid, context=None): - """ Kept for compatibility - TDE TODO: remove me in 8.0 - """ - return self.pool['mail.message']._get_default_from(cr, uid, context=context) - _defaults = { 'state': 'outgoing', } @@ -158,6 +159,18 @@ class mail_mail(osv.Model): def cancel(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) + def set_opened(self, cr, uid, ids, context=None): + """ Increment opened counter """ + for mail in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [mail.id], {'opened': (mail.opened + 1)}, context=context) + return True + + def set_replied(self, cr, uid, ids, context=None): + """ Increment replied counter """ + for mail in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [mail.id], {'replied': (mail.replied + 1)}, context=context) + return True + def process_email_queue(self, cr, uid, ids=None, context=None): """Send immediately queued messages, committing after each message is sent - this is not transactional and should @@ -226,13 +239,22 @@ class mail_mail(osv.Model): } if mail.notification: fragment.update({ - 'message_id': mail.mail_message_id.id, - }) + 'message_id': mail.mail_message_id.id, + }) url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment))) return _("""Access your messages and documents in OpenERP""") % url else: return None + def _get_tracking_url(self, cr, uid, mail, partner=None, context=None): + if not mail.auto_delete: + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') + track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id) + print base_url, track_url + return '' % track_url + else: + return '' + def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): """ If subject is void and record_name defined: ' posted on ' @@ -257,8 +279,11 @@ class mail_mail(osv.Model): # generate footer link = self._get_partner_access_link(cr, uid, mail, partner, context=context) + tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context) if link: body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div') + if tracking_url: + body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div') return body def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): @@ -319,25 +344,37 @@ class mail_mail(osv.Model): email_list.append(self.send_get_email_dict(cr, uid, mail, context=context)) for partner in mail.recipient_ids: email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context)) + # headers + headers = {} + bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context) + catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context) + if bounce_alias and catchall_domain: + if mail.model and mail.res_id: + headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain) + else: + headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain) # build an RFC2822 email.message.Message object and send it without queuing + res = None for email in email_list: msg = ir_mail_server.build_email( - email_from = mail.email_from, - email_to = email.get('email_to'), - subject = email.get('subject'), - body = email.get('body'), - body_alternative = email.get('body_alternative'), - email_cc = tools.email_split(mail.email_cc), - reply_to = mail.reply_to, - attachments = attachments, - message_id = mail.message_id, - references = mail.references, - object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), - subtype = 'html', - subtype_alternative = 'plain') + email_from=mail.email_from, + email_to=email.get('email_to'), + subject=email.get('subject'), + body=email.get('body'), + body_alternative=email.get('body_alternative'), + email_cc=tools.email_split(mail.email_cc), + reply_to=mail.reply_to, + attachments=attachments, + message_id=mail.message_id, + references=mail.references, + object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), + subtype='html', + subtype_alternative='plain', + headers=headers) res = ir_mail_server.send_email(cr, uid, msg, - mail_server_id=mail.mail_server_id.id, context=context) + mail_server_id=mail.mail_server_id.id, + context=context) if res: mail.write({'state': 'sent', 'message_id': res}) mail_sent = True diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml index f3728d4653a..3901a192225 100644 --- a/addons/mail/mail_mail_view.xml +++ b/addons/mail/mail_mail_view.xml @@ -14,7 +14,7 @@ - - - mail.mass_mailing.segment.tree - mail.mass_mailing.segment + + + mail.mass_mailing.tree + mail.mass_mailing 10 - + @@ -128,11 +128,11 @@ - - mail.mass_mailing.segment.form - mail.mass_mailing.segment + + mail.mass_mailing.form + mail.mass_mailing -
+ @@ -150,12 +150,12 @@ - - mail.mass_mailing.segment.form - mail.mass_mailing.segment + + mail.mass_mailing.form + mail.mass_mailing 18 - + @@ -173,9 +173,9 @@ - - mail.mass_mailing.segment.kanban - mail.mass_mailing.segment + + mail.mass_mailing.kanban + mail.mass_mailing @@ -229,9 +229,9 @@ - - Mass Mailing Segments - mail.mass_mailing.segment + + Mass Mailings + mail.mass_mailing form kanban,tree,form @@ -245,9 +245,9 @@ - + action="action_view_mass_mailings"/> diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index 54f3413e9f1..6931ab0265b 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,1,1,1,0 access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1 -access_mass_mailing_segment,mail.mass_mailing.segment,model_mail_mass_mailing_segment,,1,1,1,0 -access_mass_mailing_segment_system,mail.mass_mailing.segment.system,model_mail_mass_mailing_segment,base.group_system,1,1,1,1 \ No newline at end of file +access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,,1,1,1,0 +access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1 +access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,,1,1,1,1 \ No newline at end of file diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 4fe858734fe..7b22bc8f4aa 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -34,8 +34,8 @@ class MailComposeMessage(osv.TransientModel): 'mass_mailing_campaign_id': fields.many2one( 'mail.mass_mailing.campaign', 'Mass mailing campaign', ), - 'mass_mailing_segment_id': fields.many2one( - 'mail.mass_mailing.segment', 'Mass mailing segment', + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass mailing', domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]", ), } @@ -44,18 +44,18 @@ class MailComposeMessage(osv.TransientModel): 'use_mass_mailing_campaign': False, } - def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mail_segment_id, context=None): - if mass_mail_segment_id: - segment = self.pool['mail.mass_mailing.segment'].browse(cr, uid, mass_mail_segment_id, context=context) - if segment.mass_mailing_campaign_id.id == mass_mailing_campaign_id: + def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mailing_id, context=None): + if mass_mailing_id: + mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context) + if mass_mailing.mass_mailing_campaign_id.id == mass_mailing_campaign_id: return {} - return {'value': {'mass_mailing_segment_id': False}} + return {'value': {'mass_mailing_id': False}} def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """ Override method that generated the mail content by adding the mass mailing campaign, when doing pure email mass mailing. """ res = super(MailComposeMessage, self).render_message_batch(cr, uid, wizard, res_ids, context=context) - if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_segment_id: # TODO: which kind of mass mailing ? + if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_id: # TODO: which kind of mass mailing ? for res_id in res_ids: - res[res_id]['mass_mailing_segment_id'] = wizard.mass_mailing_segment_id.id + res[res_id]['mass_mailing_id'] = wizard.mass_mailing_id.id return res diff --git a/addons/mass_mailing/wizard/mail_compose_message_view.xml b/addons/mass_mailing/wizard/mail_compose_message_view.xml index 9dcf57682f6..bdd4431a71d 100644 --- a/addons/mass_mailing/wizard/mail_compose_message_view.xml +++ b/addons/mass_mailing/wizard/mail_compose_message_view.xml @@ -15,16 +15,16 @@
- + 'form_view_ref': 'mass_mailing.view_mail_mass_mailing_form_readonly'}"/>
diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py index ffb7c004415..927c34ab5fd 100644 --- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py +++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py @@ -24,11 +24,11 @@ from openerp.osv import osv, fields from openerp.tools.translate import _ -class MailMassMailingSegmentCreate(osv.TransientModel): - """Wizard to help creating mass mailing segments for a campaign. """ +class MailMassMailingCreate(osv.TransientModel): + """Wizard to help creating mass mailing waves for a campaign. """ - _name = 'mail.mass_mailing.segment.create' - _description = 'Mass mailing segment creation' + _name = 'mail.mass_mailing.create' + _description = 'Mass mailing creation' _columns = { 'mass_mailing_campaign_id': fields.many2one( @@ -55,11 +55,11 @@ class MailMassMailingSegmentCreate(osv.TransientModel): 'email.template', 'Template', required=True, domain="[('model_id', '=', model_id)]", ), - 'segment_name': fields.char( - 'Segment name', required=True, + 'name': fields.char( + 'Name', required=True, ), - 'mass_mailing_segment_id': fields.many2one( - 'mail.mass_mailing.segment', 'Mass Mailing Segment', + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass Mailing', ), } @@ -80,23 +80,23 @@ class MailMassMailingSegmentCreate(osv.TransientModel): domain = False return {'value': {'domain': domain}} - def create_segment(self, cr, uid, ids, context=None): - """ Create a segment based on wizard data, and update the wizard """ + def create_mass_mailing(self, cr, uid, ids, context=None): + """ Create a mass mailing based on wizard data, and update the wizard """ for wizard in self.browse(cr, uid, ids, context=context): - segment_values = { - 'name': wizard.segment_name, + mass_mailing_values = { + 'name': wizard.name, 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, 'domain': wizard.domain, 'template_id': wizard.template_id.id, } - segment_id = self.pool['mail.mass_mailing.segment'].create(cr, uid, segment_values, context=context) - self.write(cr, uid, [wizard.id], {'mass_mailing_segment_id': segment_id}, context=context) + mass_mailing_id = self.pool['mail.mass_mailing'].create(cr, uid, mass_mailing_values, context=context) + self.write(cr, uid, [wizard.id], {'mass_mailing_id': mass_mailing_id}, context=context) return True def launch_composer(self, cr, uid, ids, context=None): - """ Main wizard action: create a new segment and launch the mail.compose.message + """ Main wizard action: create a new mailing and launch the mail.compose.message email composer with wizard data. """ - self.create_segment(cr, uid, ids, context=context) + self.create_mass_mailing(cr, uid, ids, context=context) wizard = self.browse(cr, uid, ids[0], context=context) ctx = dict(context) @@ -107,7 +107,7 @@ class MailMassMailingSegmentCreate(osv.TransientModel): 'default_use_active_domain': True, 'default_active_domain': wizard.domain, 'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, - 'default_mass_mailing_segment_id': wizard.mass_mailing_segment_id.id, + 'default_mass_mailing_id': wizard.mass_mailing_id.id, }) return { 'name': _('Compose Email for Mass Mailing'), diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml index 5d211bb9eb4..a10101e9446 100644 --- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml +++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml @@ -3,17 +3,17 @@ - - mail.mass_mailing.segment.create.form - mail.mass_mailing.segment.create + + mail.mass_mailing.create.form + mail.mass_mailing.create - +

- Please choose a mass mailing campaign that will hold the new segment. + Please choose a mass mailing campaign that will hold the new mailing.

@@ -43,23 +43,23 @@ attrs="{'invisible': [('filter_id', '=', False)]}"/>

- Please choose the name of the campaign segment. + attrs="{'invisible': ['|', ('name', '!=', False), ('template_id', '=', False)]}"> + Please choose the name of the mailing.

-