Merge lp:~openerp-dev/openobject-addons/trunk-campaign-tracking-jri into lp:openobject-addons
- trunk-campaign-tracking-jri
- Merge into trunk
Proposed by
Juan Rial (OpenERP)
Status: | Needs review |
---|---|
Proposed branch: | lp:~openerp-dev/openobject-addons/trunk-campaign-tracking-jri |
Merge into: | lp:openobject-addons |
Diff against target: |
1867 lines (+1520/-36) (has conflicts) 13 files modified
marketing_campaign/__init__.py (+3/-0) marketing_campaign/__openerp__.py (+14/-0) marketing_campaign/email_template.py (+212/-0) marketing_campaign/email_template_view.xml (+20/-0) marketing_campaign/ir_mail_server.py (+260/-0) marketing_campaign/mail_message.py (+365/-0) marketing_campaign/marketing_campaign.py (+174/-11) marketing_campaign/marketing_campaign_view.xml (+2/-0) marketing_campaign/report/__init__.py (+1/-0) marketing_campaign/report/campaign_analysis.py (+93/-14) marketing_campaign/report/campaign_analysis_view.xml (+135/-11) marketing_campaign/report/campaign_tracking.py (+188/-0) marketing_campaign/report/campaign_tracking_view.xml (+53/-0) Text conflict in marketing_campaign/__openerp__.py Text conflict in marketing_campaign/report/campaign_analysis_view.xml |
To merge this branch: | bzr merge lp:~openerp-dev/openobject-addons/trunk-campaign-tracking-jri |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenERP Core Team | Pending | ||
Review via email: mp+116298@code.launchpad.net |
Commit message
Description of the change
Tracking & improved reporting for marketing campaigns.
To post a comment you must log in.
- 7086. By Juan Rial (OpenERP)
-
Removed some print statements
- 7087. By Juan Rial (OpenERP)
-
Change one small customer-specific piece of code
- 7088. By Juan Rial (OpenERP)
-
Marketing Campaign: separate return path from sender and fix small bug when running in normal mode
Unmerged revisions
- 7088. By Juan Rial (OpenERP)
-
Marketing Campaign: separate return path from sender and fix small bug when running in normal mode
- 7087. By Juan Rial (OpenERP)
-
Change one small customer-specific piece of code
- 7086. By Juan Rial (OpenERP)
-
Removed some print statements
- 7085. By Juan Rial (OpenERP)
-
Marketing Campaign: tracking
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'marketing_campaign/__init__.py' | |||
2 | --- marketing_campaign/__init__.py 2011-01-14 00:11:01 +0000 | |||
3 | +++ marketing_campaign/__init__.py 2012-08-27 10:03:27 +0000 | |||
4 | @@ -20,6 +20,9 @@ | |||
5 | 20 | ############################################################################## | 20 | ############################################################################## |
6 | 21 | 21 | ||
7 | 22 | import marketing_campaign | 22 | import marketing_campaign |
8 | 23 | import email_template | ||
9 | 24 | import ir_mail_server | ||
10 | 25 | import mail_message | ||
11 | 23 | import res_partner | 26 | import res_partner |
12 | 24 | import report | 27 | import report |
13 | 25 | 28 | ||
14 | 26 | 29 | ||
15 | === modified file 'marketing_campaign/__openerp__.py' | |||
16 | --- marketing_campaign/__openerp__.py 2012-08-22 13:02:32 +0000 | |||
17 | +++ marketing_campaign/__openerp__.py 2012-08-27 10:03:27 +0000 | |||
18 | @@ -57,15 +57,29 @@ | |||
19 | 57 | 'website': 'http://www.openerp.com', | 57 | 'website': 'http://www.openerp.com', |
20 | 58 | 'data': [ | 58 | 'data': [ |
21 | 59 | 'marketing_campaign_view.xml', | 59 | 'marketing_campaign_view.xml', |
22 | 60 | 'email_template_view.xml', | ||
23 | 60 | 'marketing_campaign_data.xml', | 61 | 'marketing_campaign_data.xml', |
24 | 61 | 'marketing_campaign_workflow.xml', | 62 | 'marketing_campaign_workflow.xml', |
25 | 62 | 'res_partner_view.xml', | 63 | 'res_partner_view.xml', |
26 | 63 | 'report/campaign_analysis_view.xml', | 64 | 'report/campaign_analysis_view.xml', |
27 | 65 | <<<<<<< TREE | ||
28 | 64 | 'security/marketing_campaign_security.xml', | 66 | 'security/marketing_campaign_security.xml', |
29 | 65 | 'security/ir.model.access.csv' | 67 | 'security/ir.model.access.csv' |
30 | 66 | ], | 68 | ], |
31 | 67 | 'demo': ['marketing_campaign_demo.xml'], | 69 | 'demo': ['marketing_campaign_demo.xml'], |
32 | 68 | 'test': ['test/marketing_campaign.yml'], | 70 | 'test': ['test/marketing_campaign.yml'], |
33 | 71 | ======= | ||
34 | 72 | 'report/campaign_tracking_view.xml', | ||
35 | 73 | "security/marketing_campaign_security.xml", | ||
36 | 74 | "security/ir.model.access.csv" | ||
37 | 75 | ], | ||
38 | 76 | 'demo_xml': [ | ||
39 | 77 | 'marketing_campaign_demo.xml', | ||
40 | 78 | ], | ||
41 | 79 | 'test': [ | ||
42 | 80 | 'test/marketing_campaign.yml', | ||
43 | 81 | ], | ||
44 | 82 | >>>>>>> MERGE-SOURCE | ||
45 | 69 | 'installable': True, | 83 | 'installable': True, |
46 | 70 | 'auto_install': False, | 84 | 'auto_install': False, |
47 | 71 | 'certificate': '00421723279617928365', | 85 | 'certificate': '00421723279617928365', |
48 | 72 | 86 | ||
49 | === added file 'marketing_campaign/email_template.py' | |||
50 | --- marketing_campaign/email_template.py 1970-01-01 00:00:00 +0000 | |||
51 | +++ marketing_campaign/email_template.py 2012-08-27 10:03:27 +0000 | |||
52 | @@ -0,0 +1,212 @@ | |||
53 | 1 | import netsvc | ||
54 | 2 | import base64 | ||
55 | 3 | from osv import osv | ||
56 | 4 | from osv import fields | ||
57 | 5 | import re | ||
58 | 6 | import urllib | ||
59 | 7 | |||
60 | 8 | |||
61 | 9 | # Helper functions to transform template fields for campaign tracking | ||
62 | 10 | def insert_html_tracking(template, track_str, get_char): | ||
63 | 11 | retval = template['body_html'] | ||
64 | 12 | if 'tracker_url' in template: | ||
65 | 13 | re_str = ['href="([^"]*)"', '(?:img[^<>]*)src="([^"]*)"'] | ||
66 | 14 | for i in range(2): | ||
67 | 15 | m = re.compile(re_str[i]).finditer(retval) | ||
68 | 16 | startpos = 0 | ||
69 | 17 | body_html = '' | ||
70 | 18 | for x in m: | ||
71 | 19 | body_html += retval[startpos:x.start(1)] \ | ||
72 | 20 | + template['tracker_url'] + get_char + 'url=' \ | ||
73 | 21 | + urllib.quote_plus(x.group(1)) \ | ||
74 | 22 | + '&' + track_str | ||
75 | 23 | startpos = x.end(1) | ||
76 | 24 | body_html += retval[startpos:] | ||
77 | 25 | retval = body_html | ||
78 | 26 | else: | ||
79 | 27 | re_str = [ | ||
80 | 28 | 'href="([^"]*\?[^"]*)"', | ||
81 | 29 | 'href="([^"\?]*)"', | ||
82 | 30 | '(img[^<>]*)src="([^"]*\?[^"]*)"', | ||
83 | 31 | '(img[^<>]*)src="([^"\?]*)"', | ||
84 | 32 | ] | ||
85 | 33 | get_chars = ['&', '?'] * 2 | ||
86 | 34 | for i in range(0, 2): | ||
87 | 35 | expr = re.compile(re_str[i]) | ||
88 | 36 | retval = expr.sub('href="\\1' + get_chars[i] + track_str + '"', retval) | ||
89 | 37 | for i in range(2, 4): | ||
90 | 38 | expr = re.compile(re_str[i]) | ||
91 | 39 | retval = expr.sub('\\1 src="\\2' + get_chars[i] + track_str + '"', retval) | ||
92 | 40 | return retval | ||
93 | 41 | |||
94 | 42 | |||
95 | 43 | def insert_text_tracking(template, track_str, get_char): | ||
96 | 44 | retval = template['body_text'] | ||
97 | 45 | if 'tracker_url' in template: | ||
98 | 46 | expr1 = re.compile('http(s?://([^\s]*?))([,\.]?\s)') | ||
99 | 47 | m = expr1.finditer(retval) | ||
100 | 48 | startpos = 0 | ||
101 | 49 | body_text = '' | ||
102 | 50 | for x in m: | ||
103 | 51 | body_text += retval[startpos:x.start()] \ | ||
104 | 52 | + template['tracker_url'] + get_char + 'url=' \ | ||
105 | 53 | + urllib.quote_plus('http' + x.group(1)) \ | ||
106 | 54 | + '&' + track_str + x.group(3) | ||
107 | 55 | startpos = x.end() | ||
108 | 56 | body_text += retval[startpos:] | ||
109 | 57 | retval = body_text | ||
110 | 58 | else: | ||
111 | 59 | re_str = ['http(s?://([^\s]*?\?[^\s]*?))([,\.]?\s)', 'http(s?://([^\s\?]*?))([,\.]?\s)'] | ||
112 | 60 | get_chars = ['&', '?'] | ||
113 | 61 | for i in range(2): | ||
114 | 62 | expr = re.compile(re_str[i]) | ||
115 | 63 | retval = expr.sub('http\\1' + get_chars[i] + track_str + '\\3', retval) | ||
116 | 64 | return retval | ||
117 | 65 | # End helper functions for campaign tracking | ||
118 | 66 | |||
119 | 67 | |||
120 | 68 | class email_template(osv.osv): | ||
121 | 69 | _name = 'email.template' | ||
122 | 70 | _inherit = 'email.template' | ||
123 | 71 | |||
124 | 72 | def generate_email(self, cr, uid, template_id, res_id, context=None): | ||
125 | 73 | """Generates an email from the template for given (model, res_id) pair. | ||
126 | 74 | |||
127 | 75 | :param template_id: id of the template to render. | ||
128 | 76 | :param res_id: id of the record to use for rendering the template (model | ||
129 | 77 | is taken from template definition) | ||
130 | 78 | :returns: a dict containing all relevant fields for creating a new | ||
131 | 79 | mail.message entry, with the addition one additional | ||
132 | 80 | special key ``attachments`` containing a list of | ||
133 | 81 | """ | ||
134 | 82 | if context is None: | ||
135 | 83 | context = {} | ||
136 | 84 | values = { | ||
137 | 85 | 'subject': False, | ||
138 | 86 | 'body_text': False, | ||
139 | 87 | 'body_html': False, | ||
140 | 88 | 'email_from': False, | ||
141 | 89 | 'return_path': False, # MODIF | ||
142 | 90 | 'email_to': False, | ||
143 | 91 | 'email_cc': False, | ||
144 | 92 | 'email_bcc': False, | ||
145 | 93 | 'reply_to': False, | ||
146 | 94 | 'auto_delete': False, | ||
147 | 95 | 'model': False, | ||
148 | 96 | 'res_id': False, | ||
149 | 97 | 'mail_server_id': False, | ||
150 | 98 | 'attachments': False, | ||
151 | 99 | 'attachment_ids': False, | ||
152 | 100 | 'message_id': False, | ||
153 | 101 | 'state': 'outgoing', | ||
154 | 102 | 'subtype': 'plain', | ||
155 | 103 | } | ||
156 | 104 | if not template_id: | ||
157 | 105 | return values | ||
158 | 106 | |||
159 | 107 | report_xml_pool = self.pool.get('ir.actions.report.xml') | ||
160 | 108 | template = self.get_email_template(cr, uid, template_id, res_id, context) | ||
161 | 109 | |||
162 | 110 | # BEGIN MODIF | ||
163 | 111 | fieldnames = ['subject', 'body_text', 'body_html', 'email_from', | ||
164 | 112 | 'email_to', 'email_cc', 'email_bcc', 'reply_to', | ||
165 | 113 | 'return_path', 'message_id'] | ||
166 | 114 | render_src = {} | ||
167 | 115 | for field in fieldnames: | ||
168 | 116 | render_src[field] = getattr(template, field) | ||
169 | 117 | if 'campaign_track_str' in context \ | ||
170 | 118 | and 'activity_track_str' in context \ | ||
171 | 119 | and 'res_track_str' in context: | ||
172 | 120 | track_str = 'c=' + context['campaign_track_str'] \ | ||
173 | 121 | + '&a=' + context['activity_track_str'] \ | ||
174 | 122 | + '&r=' + context['res_track_str'] | ||
175 | 123 | get_char = None | ||
176 | 124 | if 'tracker_url' in template: | ||
177 | 125 | if re.search('\?', template['tracker_url']): | ||
178 | 126 | get_char = '&' | ||
179 | 127 | else: | ||
180 | 128 | get_char = '?' | ||
181 | 129 | |||
182 | 130 | # Body text | ||
183 | 131 | if 'body_text' in template and template['body_text']: | ||
184 | 132 | render_src['body_text'] = insert_text_tracking(template, track_str, get_char) | ||
185 | 133 | |||
186 | 134 | # Body HTML | ||
187 | 135 | if 'body_html' in template and template['body_html']: | ||
188 | 136 | render_src['body_html'] = insert_html_tracking(template, track_str, get_char) | ||
189 | 137 | |||
190 | 138 | for field in fieldnames: | ||
191 | 139 | values[field] = self.render_template(cr, uid, render_src.get(field), | ||
192 | 140 | template.model, res_id, context=context) \ | ||
193 | 141 | or False | ||
194 | 142 | |||
195 | 143 | # Variable Sender | ||
196 | 144 | if values['return_path']: | ||
197 | 145 | bounce_split = values['return_path'].split('@') | ||
198 | 146 | else: | ||
199 | 147 | bounce_split = values['email_from'].split('@') | ||
200 | 148 | # If we have campaign tracking info | ||
201 | 149 | if 'campaign_track_str' in context \ | ||
202 | 150 | and 'activity_track_str' in context \ | ||
203 | 151 | and 'res_track_str' in context: | ||
204 | 152 | values['return_path'] = bounce_split[0] + '+' \ | ||
205 | 153 | + context['campaign_track_str'] + '-' \ | ||
206 | 154 | + context['activity_track_str'] + '-' \ | ||
207 | 155 | + context['res_track_str'] \ | ||
208 | 156 | + '@' + bounce_split[1] | ||
209 | 157 | # If we don't have campaign tracking info | ||
210 | 158 | else: | ||
211 | 159 | values['return_path'] = bounce_split[0] + '+' \ | ||
212 | 160 | + re.sub('@', '=', values['email_to']) \ | ||
213 | 161 | + '@' + bounce_split[1] | ||
214 | 162 | # END MODIF | ||
215 | 163 | |||
216 | 164 | if values['body_html']: | ||
217 | 165 | values.update(subtype='html') | ||
218 | 166 | |||
219 | 167 | if template.user_signature: | ||
220 | 168 | signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature | ||
221 | 169 | values['body_text'] += '\n\n' + signature | ||
222 | 170 | |||
223 | 171 | values.update(mail_server_id=template.mail_server_id.id or False, | ||
224 | 172 | auto_delete=template.auto_delete, | ||
225 | 173 | model=template.model, | ||
226 | 174 | res_id=res_id or False) | ||
227 | 175 | |||
228 | 176 | attachments = {} | ||
229 | 177 | # Add report as a Document | ||
230 | 178 | if template.report_template: | ||
231 | 179 | report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context) | ||
232 | 180 | report_service = 'report.' + report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name | ||
233 | 181 | # Ensure report is rendered using template's language | ||
234 | 182 | ctx = context.copy() | ||
235 | 183 | if template.lang: | ||
236 | 184 | ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context) | ||
237 | 185 | service = netsvc.LocalService(report_service) | ||
238 | 186 | (result, format) = service.create(cr, uid, [res_id], {'model': template.model}, ctx) | ||
239 | 187 | result = base64.b64encode(result) | ||
240 | 188 | if not report_name: | ||
241 | 189 | report_name = report_service | ||
242 | 190 | ext = "." + format | ||
243 | 191 | if not report_name.endswith(ext): | ||
244 | 192 | report_name += ext | ||
245 | 193 | attachments[report_name] = result | ||
246 | 194 | |||
247 | 195 | # Add document attachments | ||
248 | 196 | for attach in template.attachment_ids: | ||
249 | 197 | # keep the bytes as fetched from the db, base64 encoded | ||
250 | 198 | attachments[attach.datas_fname] = attach.datas | ||
251 | 199 | |||
252 | 200 | values['attachments'] = attachments | ||
253 | 201 | return values | ||
254 | 202 | |||
255 | 203 | _columns = { | ||
256 | 204 | 'tracker_url': fields.char('Tracker URL', None, help='The URL of the campaign tracker'), | ||
257 | 205 | 'return_path': fields.char('Return path', None, help='Email address to send mail delivery reports to'), | ||
258 | 206 | } | ||
259 | 207 | |||
260 | 208 | _defaults = { | ||
261 | 209 | 'model_id': lambda obj, cr, uid, context: context.get('object_id',False), | ||
262 | 210 | } | ||
263 | 211 | |||
264 | 212 | email_template() | ||
265 | 0 | 213 | ||
266 | === added file 'marketing_campaign/email_template_view.xml' | |||
267 | --- marketing_campaign/email_template_view.xml 1970-01-01 00:00:00 +0000 | |||
268 | +++ marketing_campaign/email_template_view.xml 2012-08-27 10:03:27 +0000 | |||
269 | @@ -0,0 +1,20 @@ | |||
270 | 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
271 | 2 | <openerp> | ||
272 | 3 | <data> | ||
273 | 4 | |||
274 | 5 | <record model="ir.ui.view" id="email_template_form_inherit"> | ||
275 | 6 | <field name="name">email.template.form</field> | ||
276 | 7 | <field name="inherit_id" ref='email_template.email_template_form' /> | ||
277 | 8 | <field name="model">email.template</field> | ||
278 | 9 | <field name="type">form</field> | ||
279 | 10 | <field name="arch" type="xml"> | ||
280 | 11 | <field name="reply_to" position="after"> | ||
281 | 12 | <field name="return_path"/> | ||
282 | 13 | </field> | ||
283 | 14 | <field name="track_campaign_item" position="after"> | ||
284 | 15 | <field name="tracker_url" /> | ||
285 | 16 | </field> | ||
286 | 17 | </field> | ||
287 | 18 | </record> | ||
288 | 19 | </data> | ||
289 | 20 | </openerp> | ||
290 | 0 | \ No newline at end of file | 21 | \ No newline at end of file |
291 | 1 | 22 | ||
292 | === added file 'marketing_campaign/ir_mail_server.py' | |||
293 | --- marketing_campaign/ir_mail_server.py 1970-01-01 00:00:00 +0000 | |||
294 | +++ marketing_campaign/ir_mail_server.py 2012-08-27 10:03:27 +0000 | |||
295 | @@ -0,0 +1,260 @@ | |||
296 | 1 | from email.MIMEText import MIMEText | ||
297 | 2 | from email.MIMEBase import MIMEBase | ||
298 | 3 | from email.MIMEMultipart import MIMEMultipart | ||
299 | 4 | from email.Charset import Charset | ||
300 | 5 | from email.Header import Header | ||
301 | 6 | from email.Utils import formatdate, make_msgid, COMMASPACE | ||
302 | 7 | from email import Encoders | ||
303 | 8 | import logging | ||
304 | 9 | import re | ||
305 | 10 | # import smtplib | ||
306 | 11 | # import threading | ||
307 | 12 | |||
308 | 13 | # Just here to satisfy code requirements, but unmodified from original | ||
309 | 14 | from osv import osv | ||
310 | 15 | # from osv import fields | ||
311 | 16 | # from openerp.tools.translate import _ | ||
312 | 17 | from openerp.tools import html2text | ||
313 | 18 | import openerp.tools as tools | ||
314 | 19 | |||
315 | 20 | # ustr was originally from tools.misc. | ||
316 | 21 | # it is moved to loglevels until we refactor tools. | ||
317 | 22 | from openerp.loglevels import ustr | ||
318 | 23 | |||
319 | 24 | _logger = logging.getLogger(__name__) | ||
320 | 25 | name_with_email_pattern = re.compile(r'("[^<@>]+")\s*<([^ ,<@]+@[^> ,]+)>') | ||
321 | 26 | address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)') | ||
322 | 27 | |||
323 | 28 | |||
324 | 29 | def try_coerce_ascii(string_utf8): | ||
325 | 30 | """Attempts to decode the given utf8-encoded string | ||
326 | 31 | as ASCII after coercing it to UTF-8, then return | ||
327 | 32 | the confirmed 7-bit ASCII string. | ||
328 | 33 | |||
329 | 34 | If the process fails (because the string | ||
330 | 35 | contains non-ASCII characters) returns ``None``. | ||
331 | 36 | """ | ||
332 | 37 | try: | ||
333 | 38 | string_utf8.decode('ascii') | ||
334 | 39 | except UnicodeDecodeError: | ||
335 | 40 | return | ||
336 | 41 | return string_utf8 | ||
337 | 42 | |||
338 | 43 | |||
339 | 44 | def extract_rfc2822_addresses(text): | ||
340 | 45 | """Returns a list of valid RFC2822 addresses | ||
341 | 46 | that can be found in ``source``, ignoring | ||
342 | 47 | malformed ones and non-ASCII ones. | ||
343 | 48 | """ | ||
344 | 49 | if not text: | ||
345 | 50 | return [] | ||
346 | 51 | candidates = address_pattern.findall(tools.ustr(text).encode('utf-8')) | ||
347 | 52 | return filter(try_coerce_ascii, candidates) | ||
348 | 53 | |||
349 | 54 | |||
350 | 55 | def encode_header_param(param_text): | ||
351 | 56 | """Returns an appropriate RFC2047 encoded representation of the given | ||
352 | 57 | header parameter value, suitable for direct assignation as the | ||
353 | 58 | param value (e.g. via Message.set_param() or Message.add_header()) | ||
354 | 59 | RFC2822 assumes that headers contain only 7-bit characters, | ||
355 | 60 | so we ensure it is the case, using RFC2047 encoding when needed. | ||
356 | 61 | |||
357 | 62 | :param param_text: unicode or utf-8 encoded string with header value | ||
358 | 63 | :rtype: string | ||
359 | 64 | :return: if ``param_text`` represents a plain ASCII string, | ||
360 | 65 | return the same 7-bit string, otherwise returns an | ||
361 | 66 | ASCII string containing the RFC2047 encoded text. | ||
362 | 67 | """ | ||
363 | 68 | # For details see the encode_header() method that uses the same logic | ||
364 | 69 | if not param_text: | ||
365 | 70 | return "" | ||
366 | 71 | param_text_utf8 = tools.ustr(param_text).encode('utf-8') | ||
367 | 72 | param_text_ascii = try_coerce_ascii(param_text_utf8) | ||
368 | 73 | return param_text_ascii if param_text_ascii\ | ||
369 | 74 | else Charset('utf8').header_encode(param_text_utf8) | ||
370 | 75 | |||
371 | 76 | |||
372 | 77 | def encode_rfc2822_address_header(header_text): | ||
373 | 78 | """If ``header_text`` contains non-ASCII characters, | ||
374 | 79 | attempts to locate patterns of the form | ||
375 | 80 | ``"Name" <address@domain>`` and replace the | ||
376 | 81 | ``"Name"`` portion by the RFC2047-encoded | ||
377 | 82 | version, preserving the address part untouched. | ||
378 | 83 | """ | ||
379 | 84 | header_text_utf8 = tools.ustr(header_text).encode('utf-8') | ||
380 | 85 | header_text_ascii = try_coerce_ascii(header_text_utf8) | ||
381 | 86 | if header_text_ascii: | ||
382 | 87 | return header_text_ascii | ||
383 | 88 | # non-ASCII characters are present, attempt to | ||
384 | 89 | # replace all "Name" patterns with the RFC2047- | ||
385 | 90 | # encoded version | ||
386 | 91 | |||
387 | 92 | def replace(match_obj): | ||
388 | 93 | name, email = match_obj.group(1), match_obj.group(2) | ||
389 | 94 | name_encoded = str(Header(name, 'utf-8')) | ||
390 | 95 | return "%s <%s>" % (name_encoded, email) | ||
391 | 96 | header_text_utf8 = name_with_email_pattern.sub(replace, | ||
392 | 97 | header_text_utf8) | ||
393 | 98 | # try again after encoding | ||
394 | 99 | header_text_ascii = try_coerce_ascii(header_text_utf8) | ||
395 | 100 | if header_text_ascii: | ||
396 | 101 | return header_text_ascii | ||
397 | 102 | # fallback to extracting pure addresses only, which could | ||
398 | 103 | # still cause a failure downstream if the actual addresses | ||
399 | 104 | # contain non-ASCII characters | ||
400 | 105 | return COMMASPACE.join(extract_rfc2822_addresses(header_text_utf8)) | ||
401 | 106 | |||
402 | 107 | |||
403 | 108 | def encode_header(header_text): | ||
404 | 109 | """Returns an appropriate representation of the given header value, | ||
405 | 110 | suitable for direct assignment as a header value in an | ||
406 | 111 | email.message.Message. RFC2822 assumes that headers contain | ||
407 | 112 | only 7-bit characters, so we ensure it is the case, using | ||
408 | 113 | RFC2047 encoding when needed. | ||
409 | 114 | |||
410 | 115 | :param header_text: unicode or utf-8 encoded string with header value | ||
411 | 116 | :rtype: string | email.header.Header | ||
412 | 117 | :return: if ``header_text`` represents a plain ASCII string, | ||
413 | 118 | return the same 7-bit string, otherwise returns an email.header.Header | ||
414 | 119 | that will perform the appropriate RFC2047 encoding of | ||
415 | 120 | non-ASCII values. | ||
416 | 121 | """ | ||
417 | 122 | if not header_text: | ||
418 | 123 | return "" | ||
419 | 124 | # convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are | ||
420 | 125 | # encoded as ASCII in utf-8 | ||
421 | 126 | header_text_utf8 = tools.ustr(header_text).encode('utf-8') | ||
422 | 127 | header_text_ascii = try_coerce_ascii(header_text_utf8) | ||
423 | 128 | # if this header contains non-ASCII characters, | ||
424 | 129 | # we'll need to wrap it up in a message.header.Header | ||
425 | 130 | # that will take care of RFC2047-encoding it as | ||
426 | 131 | # 7-bit string. | ||
427 | 132 | return header_text_ascii if header_text_ascii\ | ||
428 | 133 | else Header(header_text_utf8, 'utf-8') | ||
429 | 134 | # End "Just here ..." | ||
430 | 135 | |||
431 | 136 | |||
432 | 137 | class ir_mail_server(osv.osv): | ||
433 | 138 | _name = "ir.mail_server" | ||
434 | 139 | _inherit = "ir.mail_server" | ||
435 | 140 | |||
436 | 141 | # _columns = { | ||
437 | 142 | # 'variable_sender': fields.boolean('Variable Sender'), | ||
438 | 143 | # } | ||
439 | 144 | |||
440 | 145 | # MODIF signature | ||
441 | 146 | def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, | ||
442 | 147 | attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, | ||
443 | 148 | body_alternative=None, subtype_alternative='plain', return_path=False): | ||
444 | 149 | """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it. | ||
445 | 150 | |||
446 | 151 | :param string email_from: sender email address | ||
447 | 152 | :param list email_to: list of recipient addresses (to be joined with commas) | ||
448 | 153 | :param string subject: email subject (no pre-encoding/quoting necessary) | ||
449 | 154 | :param string body: email body, of the type ``subtype`` (by default, plaintext). | ||
450 | 155 | If html subtype is used, the message will be automatically converted | ||
451 | 156 | to plaintext and wrapped in multipart/alternative, unless an explicit | ||
452 | 157 | ``body_alternative`` version is passed. | ||
453 | 158 | :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative`` | ||
454 | 159 | :param string reply_to: optional value of Reply-To header | ||
455 | 160 | :param string object_id: optional tracking identifier, to be included in the message-id for | ||
456 | 161 | recognizing replies. Suggested format for object-id is "res_id-model", | ||
457 | 162 | e.g. "12345-crm.lead". | ||
458 | 163 | :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), | ||
459 | 164 | must match the format of the ``body`` parameter. Default is 'plain', | ||
460 | 165 | making the content part of the mail "text/plain". | ||
461 | 166 | :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain' | ||
462 | 167 | or 'html'). Default is 'plain'. | ||
463 | 168 | :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string | ||
464 | 169 | containing the bytes of the attachment | ||
465 | 170 | :param list email_cc: optional list of string values for CC header (to be joined with commas) | ||
466 | 171 | :param list email_bcc: optional list of string values for BCC header (to be joined with commas) | ||
467 | 172 | :param dict headers: optional map of headers to set on the outgoing mail (may override the | ||
468 | 173 | other headers, including Subject, Reply-To, Message-Id, etc.) | ||
469 | 174 | :rtype: email.message.Message (usually MIMEMultipart) | ||
470 | 175 | :return: the new RFC2822 email message | ||
471 | 176 | """ | ||
472 | 177 | email_from = email_from or tools.config.get('email_from') | ||
473 | 178 | assert email_from, "You must either provide a sender address explicitly or configure "\ | ||
474 | 179 | "a global sender address in the server configuration or with the "\ | ||
475 | 180 | "--email-from startup parameter." | ||
476 | 181 | |||
477 | 182 | # Note: we must force all strings to to 8-bit utf-8 when crafting message, | ||
478 | 183 | # or use encode_header() for headers, which does it automatically. | ||
479 | 184 | |||
480 | 185 | headers = headers or {} # need valid dict later | ||
481 | 186 | |||
482 | 187 | if not email_cc: | ||
483 | 188 | email_cc = [] | ||
484 | 189 | if not email_bcc: | ||
485 | 190 | email_bcc = [] | ||
486 | 191 | if not body: | ||
487 | 192 | body = u'' | ||
488 | 193 | |||
489 | 194 | email_body_utf8 = ustr(body).encode('utf-8') | ||
490 | 195 | email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8') | ||
491 | 196 | msg = MIMEMultipart() | ||
492 | 197 | |||
493 | 198 | if not message_id: | ||
494 | 199 | if object_id: | ||
495 | 200 | message_id = tools.generate_tracking_message_id(object_id) | ||
496 | 201 | else: | ||
497 | 202 | message_id = make_msgid() | ||
498 | 203 | msg['Message-Id'] = encode_header(message_id) | ||
499 | 204 | if references: | ||
500 | 205 | msg['references'] = encode_header(references) | ||
501 | 206 | msg['Subject'] = encode_header(subject) | ||
502 | 207 | msg['From'] = encode_rfc2822_address_header(email_from) | ||
503 | 208 | # del msg['Reply-To'] # MODIF: unnecessary., given what preceeds and what follows. | ||
504 | 209 | if reply_to: | ||
505 | 210 | msg['Reply-To'] = encode_rfc2822_address_header(reply_to) | ||
506 | 211 | else: | ||
507 | 212 | msg['Reply-To'] = msg['From'] | ||
508 | 213 | # BEGIN MODIF | ||
509 | 214 | if return_path: | ||
510 | 215 | msg['Return-Path'] = encode_rfc2822_address_header(return_path) | ||
511 | 216 | else: | ||
512 | 217 | msg['Return-Path'] = msg['From'] | ||
513 | 218 | # END MODIF | ||
514 | 219 | msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to)) | ||
515 | 220 | if email_cc: | ||
516 | 221 | msg['Cc'] = encode_rfc2822_address_header(COMMASPACE.join(email_cc)) | ||
517 | 222 | if email_bcc: | ||
518 | 223 | msg['Bcc'] = encode_rfc2822_address_header(COMMASPACE.join(email_bcc)) | ||
519 | 224 | msg['Date'] = formatdate() | ||
520 | 225 | # Custom headers may override normal headers or provide additional ones | ||
521 | 226 | for key, value in headers.iteritems(): | ||
522 | 227 | msg[ustr(key).encode('utf-8')] = encode_header(value) | ||
523 | 228 | |||
524 | 229 | if subtype == 'html' and not body_alternative and html2text: | ||
525 | 230 | # Always provide alternative text body ourselves if possible. | ||
526 | 231 | text_utf8 = tools.html2text(email_body_utf8.decode('utf-8')).encode('utf-8') | ||
527 | 232 | alternative_part = MIMEMultipart(_subtype="alternative") | ||
528 | 233 | alternative_part.attach(MIMEText(text_utf8, _charset='utf-8', _subtype='plain')) | ||
529 | 234 | alternative_part.attach(email_text_part) | ||
530 | 235 | msg.attach(alternative_part) | ||
531 | 236 | elif body_alternative: | ||
532 | 237 | # Include both alternatives, as specified, within a multipart/alternative part | ||
533 | 238 | alternative_part = MIMEMultipart(_subtype="alternative") | ||
534 | 239 | body_alternative_utf8 = ustr(body_alternative).encode('utf-8') | ||
535 | 240 | alternative_body_part = MIMEText(body_alternative_utf8, _subtype=subtype_alternative, _charset='utf-8') | ||
536 | 241 | alternative_part.attach(alternative_body_part) | ||
537 | 242 | alternative_part.attach(email_text_part) | ||
538 | 243 | msg.attach(alternative_part) | ||
539 | 244 | else: | ||
540 | 245 | msg.attach(email_text_part) | ||
541 | 246 | |||
542 | 247 | if attachments: | ||
543 | 248 | for (fname, fcontent) in attachments: | ||
544 | 249 | filename_rfc2047 = encode_header_param(fname) | ||
545 | 250 | part = MIMEBase('application', "octet-stream") | ||
546 | 251 | |||
547 | 252 | # The default RFC2231 encoding of Message.add_header() works in Thunderbird but not GMail | ||
548 | 253 | # so we fix it by using RFC2047 encoding for the filename instead. | ||
549 | 254 | part.set_param('name', filename_rfc2047) | ||
550 | 255 | part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047) | ||
551 | 256 | |||
552 | 257 | part.set_payload(fcontent) | ||
553 | 258 | Encoders.encode_base64(part) | ||
554 | 259 | msg.attach(part) | ||
555 | 260 | return msg | ||
556 | 0 | 261 | ||
557 | === added file 'marketing_campaign/mail_message.py' | |||
558 | --- marketing_campaign/mail_message.py 1970-01-01 00:00:00 +0000 | |||
559 | +++ marketing_campaign/mail_message.py 2012-08-27 10:03:27 +0000 | |||
560 | @@ -0,0 +1,365 @@ | |||
561 | 1 | import ast | ||
562 | 2 | import base64 | ||
563 | 3 | import dateutil.parser | ||
564 | 4 | import email | ||
565 | 5 | import logging | ||
566 | 6 | import re | ||
567 | 7 | import time | ||
568 | 8 | from email.header import decode_header | ||
569 | 9 | from email.message import Message | ||
570 | 10 | |||
571 | 11 | import tools | ||
572 | 12 | from osv import osv | ||
573 | 13 | from osv import fields | ||
574 | 14 | # from tools.translate import _ | ||
575 | 15 | # from openerp import SUPERUSER_ID | ||
576 | 16 | |||
577 | 17 | # Just here to satisfy code requirements, but unmodified from original | ||
578 | 18 | _logger = logging.getLogger('mail') | ||
579 | 19 | |||
580 | 20 | |||
581 | 21 | def decode(text): | ||
582 | 22 | """Returns unicode() string conversion of the the given encoded smtp header text""" | ||
583 | 23 | if text: | ||
584 | 24 | text = decode_header(text.replace('\r', '')) | ||
585 | 25 | return ''.join([tools.ustr(x[0], x[1]) for x in text]) | ||
586 | 26 | |||
587 | 27 | |||
588 | 28 | def to_email(text): | ||
589 | 29 | """Return a list of the email addresses found in ``text``""" | ||
590 | 30 | if not text: | ||
591 | 31 | return [] | ||
592 | 32 | return re.findall(r'([^ ,<@]+@[^> ,]+)', text) | ||
593 | 33 | # End "Just here ..." | ||
594 | 34 | |||
595 | 35 | |||
596 | 36 | class mail_message(osv.osv): | ||
597 | 37 | _name = 'mail.message' | ||
598 | 38 | _inherit = 'mail.message' | ||
599 | 39 | |||
600 | 40 | _columns = { | ||
601 | 41 | 'return_path': fields.char('Return-Path', size=None, readonly=True), | ||
602 | 42 | 'workitem_id': fields.many2one('marketing.campaign.workitem', 'Workitem'), | ||
603 | 43 | } | ||
604 | 44 | |||
605 | 45 | # MODIF signature | ||
606 | 46 | def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None, | ||
607 | 47 | email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False, | ||
608 | 48 | res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False, | ||
609 | 49 | context=None, return_path=False): | ||
610 | 50 | """Schedule sending a new email message, to be sent the next time the mail scheduler runs, or | ||
611 | 51 | the next time :meth:`process_email_queue` is called explicitly. | ||
612 | 52 | |||
613 | 53 | :param string email_from: sender email address | ||
614 | 54 | :param string return_path: bounce return path | ||
615 | 55 | :param list email_to: list of recipient addresses (to be joined with commas) | ||
616 | 56 | :param string subject: email subject (no pre-encoding/quoting necessary) | ||
617 | 57 | :param string body: email body, according to the ``subtype`` (by default, plaintext). | ||
618 | 58 | If html subtype is used, the message will be automatically converted | ||
619 | 59 | to plaintext and wrapped in multipart/alternative. | ||
620 | 60 | :param list email_cc: optional list of string values for CC header (to be joined with commas) | ||
621 | 61 | :param list email_bcc: optional list of string values for BCC header (to be joined with commas) | ||
622 | 62 | :param string model: optional model name of the document this mail is related to (this will also | ||
623 | 63 | be used to generate a tracking id, used to match any response related to the | ||
624 | 64 | same document) | ||
625 | 65 | :param int res_id: optional resource identifier this mail is related to (this will also | ||
626 | 66 | be used to generate a tracking id, used to match any response related to the | ||
627 | 67 | same document) | ||
628 | 68 | :param string reply_to: optional value of Reply-To header | ||
629 | 69 | :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), | ||
630 | 70 | must match the format of the ``body`` parameter. Default is 'plain', | ||
631 | 71 | making the content part of the mail "text/plain". | ||
632 | 72 | :param dict attachments: map of filename to filecontents, where filecontents is a string | ||
633 | 73 | containing the bytes of the attachment | ||
634 | 74 | :param dict headers: optional map of headers to set on the outgoing mail (may override the | ||
635 | 75 | other headers, including Subject, Reply-To, Message-Id, etc.) | ||
636 | 76 | :param int mail_server_id: optional id of the preferred outgoing mail server for this mail | ||
637 | 77 | :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been | ||
638 | 78 | successfully sent (default to False) | ||
639 | 79 | |||
640 | 80 | """ | ||
641 | 81 | if context is None: | ||
642 | 82 | context = {} | ||
643 | 83 | if attachments is None: | ||
644 | 84 | attachments = {} | ||
645 | 85 | attachment_obj = self.pool.get('ir.attachment') | ||
646 | 86 | for param in (email_to, email_cc, email_bcc): | ||
647 | 87 | if param and not isinstance(param, list): | ||
648 | 88 | param = [param] | ||
649 | 89 | msg_vals = { | ||
650 | 90 | 'subject': subject, | ||
651 | 91 | 'date': time.strftime('%Y-%m-%d %H:%M:%S'), | ||
652 | 92 | 'user_id': uid, | ||
653 | 93 | 'model': model, | ||
654 | 94 | 'res_id': res_id, | ||
655 | 95 | 'body_text': body if subtype != 'html' else False, | ||
656 | 96 | 'body_html': body if subtype == 'html' else False, | ||
657 | 97 | 'email_from': email_from, | ||
658 | 98 | 'return_path': return_path or email_from, # MODIF | ||
659 | 99 | 'email_to': email_to and ','.join(email_to) or '', | ||
660 | 100 | 'email_cc': email_cc and ','.join(email_cc) or '', | ||
661 | 101 | 'email_bcc': email_bcc and ','.join(email_bcc) or '', | ||
662 | 102 | 'reply_to': reply_to, | ||
663 | 103 | 'message_id': message_id, | ||
664 | 104 | 'references': references, | ||
665 | 105 | 'subtype': subtype, | ||
666 | 106 | 'headers': headers, # serialize the dict on the fly | ||
667 | 107 | 'mail_server_id': mail_server_id, | ||
668 | 108 | 'state': 'outgoing', | ||
669 | 109 | 'auto_delete': auto_delete | ||
670 | 110 | } | ||
671 | 111 | if 'workitem_id' in context and context['workitem_id']: | ||
672 | 112 | msg_vals.update({'workitem_id': context['workitem_id']}) | ||
673 | 113 | email_msg_id = self.create(cr, uid, msg_vals, context) | ||
674 | 114 | attachment_ids = [] | ||
675 | 115 | for fname, fcontent in attachments.iteritems(): | ||
676 | 116 | attachment_data = { | ||
677 | 117 | 'name': fname, | ||
678 | 118 | 'datas_fname': fname, | ||
679 | 119 | 'datas': fcontent and fcontent.encode('base64'), | ||
680 | 120 | 'res_model': self._name, | ||
681 | 121 | 'res_id': email_msg_id, | ||
682 | 122 | } | ||
683 | 123 | if 'default_type' in context: | ||
684 | 124 | del context['default_type'] | ||
685 | 125 | attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context)) | ||
686 | 126 | if attachment_ids: | ||
687 | 127 | self.write(cr, uid, email_msg_id, {'attachment_ids': [(6, 0, attachment_ids)]}, context=context) | ||
688 | 128 | return email_msg_id | ||
689 | 129 | |||
690 | 130 | def parse_message(self, message, save_original=False): | ||
691 | 131 | """Parses a string or email.message.Message representing an | ||
692 | 132 | RFC-2822 email, and returns a generic dict holding the | ||
693 | 133 | message details. | ||
694 | 134 | |||
695 | 135 | :param message: the message to parse | ||
696 | 136 | :type message: email.message.Message | string | unicode | ||
697 | 137 | :param bool save_original: whether the returned dict | ||
698 | 138 | should include an ``original`` entry with the base64 | ||
699 | 139 | encoded source of the message. | ||
700 | 140 | :rtype: dict | ||
701 | 141 | :return: A dict with the following structure, where each | ||
702 | 142 | field may not be present if missing in original | ||
703 | 143 | message:: | ||
704 | 144 | |||
705 | 145 | { 'message-id': msg_id, | ||
706 | 146 | 'subject': subject, | ||
707 | 147 | 'from': from, | ||
708 | 148 | 'to': to, | ||
709 | 149 | 'cc': cc, | ||
710 | 150 | 'headers' : { 'X-Mailer': mailer, | ||
711 | 151 | #.. all X- headers... | ||
712 | 152 | }, | ||
713 | 153 | 'subtype': msg_mime_subtype, | ||
714 | 154 | 'body_text': plaintext_body | ||
715 | 155 | 'body_html': html_body, | ||
716 | 156 | 'attachments': [('file1', 'bytes'), | ||
717 | 157 | ('file2', 'bytes') } | ||
718 | 158 | # ... | ||
719 | 159 | 'original': source_of_email, | ||
720 | 160 | } | ||
721 | 161 | """ | ||
722 | 162 | msg_txt = message | ||
723 | 163 | if isinstance(message, str): | ||
724 | 164 | msg_txt = email.message_from_string(message) | ||
725 | 165 | |||
726 | 166 | # Warning: message_from_string doesn't always work correctly on unicode, | ||
727 | 167 | # we must use utf-8 strings here :-( | ||
728 | 168 | if isinstance(message, unicode): | ||
729 | 169 | message = message.encode('utf-8') | ||
730 | 170 | msg_txt = email.message_from_string(message) | ||
731 | 171 | |||
732 | 172 | message_id = msg_txt.get('message-id', False) | ||
733 | 173 | msg = {} | ||
734 | 174 | |||
735 | 175 | if save_original: | ||
736 | 176 | # save original, we need to be able to read the original email sometimes | ||
737 | 177 | msg['original'] = message.as_string() if isinstance(message, Message) \ | ||
738 | 178 | else message | ||
739 | 179 | msg['original'] = base64.b64encode(msg['original']) # binary fields are b64 | ||
740 | 180 | |||
741 | 181 | if not message_id: | ||
742 | 182 | # Very unusual situation, be we should be fault-tolerant here | ||
743 | 183 | message_id = time.time() | ||
744 | 184 | msg_txt['message-id'] = message_id | ||
745 | 185 | _logger.info('Parsing Message without message-id, generating a random one: %s', message_id) | ||
746 | 186 | |||
747 | 187 | fields = msg_txt.keys() | ||
748 | 188 | msg['id'] = message_id | ||
749 | 189 | msg['message-id'] = message_id | ||
750 | 190 | |||
751 | 191 | if 'Subject' in fields: | ||
752 | 192 | msg['subject'] = decode(msg_txt.get('Subject')) | ||
753 | 193 | |||
754 | 194 | if 'Content-Type' in fields: | ||
755 | 195 | msg['content-type'] = msg_txt.get('Content-Type') | ||
756 | 196 | |||
757 | 197 | if 'From' in fields: | ||
758 | 198 | msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom()) | ||
759 | 199 | |||
760 | 200 | # MODIF | ||
761 | 201 | if 'Return-Path' in fields: | ||
762 | 202 | msg['return_path'] = decode(msg_txt.get('Return-Path')) or msg['from'] | ||
763 | 203 | # END MODIF | ||
764 | 204 | |||
765 | 205 | if 'To' in fields: | ||
766 | 206 | msg['to'] = decode(msg_txt.get('To')) | ||
767 | 207 | |||
768 | 208 | if 'Delivered-To' in fields: | ||
769 | 209 | msg['to'] = decode(msg_txt.get('Delivered-To')) | ||
770 | 210 | |||
771 | 211 | if 'CC' in fields: | ||
772 | 212 | msg['cc'] = decode(msg_txt.get('CC')) | ||
773 | 213 | |||
774 | 214 | if 'Cc' in fields: | ||
775 | 215 | msg['cc'] = decode(msg_txt.get('Cc')) | ||
776 | 216 | |||
777 | 217 | if 'Reply-To' in fields: | ||
778 | 218 | msg['reply'] = decode(msg_txt.get('Reply-To')) | ||
779 | 219 | |||
780 | 220 | if 'Date' in fields: | ||
781 | 221 | date_hdr = decode(msg_txt.get('Date')) | ||
782 | 222 | msg['date'] = dateutil.parser.parse(date_hdr).strftime("%Y-%m-%d %H:%M:%S") | ||
783 | 223 | |||
784 | 224 | if 'Content-Transfer-Encoding' in fields: | ||
785 | 225 | msg['encoding'] = msg_txt.get('Content-Transfer-Encoding') | ||
786 | 226 | |||
787 | 227 | if 'References' in fields: | ||
788 | 228 | msg['references'] = msg_txt.get('References') | ||
789 | 229 | |||
790 | 230 | if 'In-Reply-To' in fields: | ||
791 | 231 | msg['in-reply-to'] = msg_txt.get('In-Reply-To') | ||
792 | 232 | |||
793 | 233 | msg['headers'] = {} | ||
794 | 234 | msg['subtype'] = 'plain' | ||
795 | 235 | for item in msg_txt.items(): | ||
796 | 236 | if item[0].startswith('X-'): | ||
797 | 237 | msg['headers'].update({item[0]: item[1]}) | ||
798 | 238 | if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''): | ||
799 | 239 | encoding = msg_txt.get_content_charset() | ||
800 | 240 | body = msg_txt.get_payload(decode=True) | ||
801 | 241 | if 'text/html' in msg.get('content-type', ''): | ||
802 | 242 | msg['body_html'] = body | ||
803 | 243 | msg['subtype'] = 'html' | ||
804 | 244 | body = tools.html2plaintext(body) | ||
805 | 245 | msg['body_text'] = tools.ustr(body, encoding) | ||
806 | 246 | |||
807 | 247 | attachments = [] | ||
808 | 248 | if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''): | ||
809 | 249 | body = "" | ||
810 | 250 | if 'multipart/alternative' in msg.get('content-type', ''): | ||
811 | 251 | msg['subtype'] = 'alternative' | ||
812 | 252 | else: | ||
813 | 253 | msg['subtype'] = 'mixed' | ||
814 | 254 | for part in msg_txt.walk(): | ||
815 | 255 | if part.get_content_maintype() == 'multipart': | ||
816 | 256 | continue | ||
817 | 257 | |||
818 | 258 | encoding = part.get_content_charset() | ||
819 | 259 | filename = part.get_filename() | ||
820 | 260 | if part.get_content_maintype()=='text': | ||
821 | 261 | content = part.get_payload(decode=True) | ||
822 | 262 | if filename: | ||
823 | 263 | attachments.append((filename, content)) | ||
824 | 264 | content = tools.ustr(content, encoding) | ||
825 | 265 | if part.get_content_subtype() == 'html': | ||
826 | 266 | msg['body_html'] = content | ||
827 | 267 | msg['subtype'] = 'html' # html version prevails | ||
828 | 268 | body = tools.ustr(tools.html2plaintext(content)) | ||
829 | 269 | body = body.replace(' ', '') | ||
830 | 270 | elif part.get_content_subtype() == 'plain': | ||
831 | 271 | body = content | ||
832 | 272 | elif part.get_content_maintype() in ('application', 'image'): | ||
833 | 273 | if filename : | ||
834 | 274 | attachments.append((filename,part.get_payload(decode=True))) | ||
835 | 275 | else: | ||
836 | 276 | res = part.get_payload(decode=True) | ||
837 | 277 | body += tools.ustr(res, encoding) | ||
838 | 278 | |||
839 | 279 | msg['body_text'] = body | ||
840 | 280 | msg['attachments'] = attachments | ||
841 | 281 | |||
842 | 282 | # for backwards compatibility: | ||
843 | 283 | msg['body'] = msg['body_text'] | ||
844 | 284 | msg['sub_type'] = msg['subtype'] or 'plain' | ||
845 | 285 | return msg | ||
846 | 286 | |||
847 | 287 | def send(self, cr, uid, ids, auto_commit=False, context=None): | ||
848 | 288 | """Sends the selected emails immediately, ignoring their current | ||
849 | 289 | state (mails that have already been sent should not be passed | ||
850 | 290 | unless they should actually be re-sent). | ||
851 | 291 | Emails successfully delivered are marked as 'sent', and those | ||
852 | 292 | that fail to be deliver are marked as 'exception', and the | ||
853 | 293 | corresponding error message is output in the server logs. | ||
854 | 294 | |||
855 | 295 | :param bool auto_commit: whether to force a commit of the message | ||
856 | 296 | status after sending each message (meant | ||
857 | 297 | only for processing by the scheduler), | ||
858 | 298 | should never be True during normal | ||
859 | 299 | transactions (default: False) | ||
860 | 300 | :return: True | ||
861 | 301 | """ | ||
862 | 302 | if context is None: | ||
863 | 303 | context = {} | ||
864 | 304 | ir_mail_server = self.pool.get('ir.mail_server') | ||
865 | 305 | self.write(cr, uid, ids, {'state': 'outgoing'}, context=context) | ||
866 | 306 | for message in self.browse(cr, uid, ids, context=context): | ||
867 | 307 | try: | ||
868 | 308 | attachments = [] | ||
869 | 309 | for attach in message.attachment_ids: | ||
870 | 310 | attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) | ||
871 | 311 | |||
872 | 312 | body = message.body_html if message.subtype == 'html' else message.body_text | ||
873 | 313 | body_alternative = None | ||
874 | 314 | subtype_alternative = None | ||
875 | 315 | if message.subtype == 'html' and message.body_text: | ||
876 | 316 | # we have a plain text alternative prepared, pass it to | ||
877 | 317 | # build_message instead of letting it build one | ||
878 | 318 | body_alternative = message.body_text | ||
879 | 319 | subtype_alternative = 'plain' | ||
880 | 320 | |||
881 | 321 | msg = ir_mail_server.build_email( | ||
882 | 322 | email_from=message.email_from, | ||
883 | 323 | return_path=message.return_path, # MODIF | ||
884 | 324 | email_to=to_email(message.email_to), | ||
885 | 325 | subject=message.subject, | ||
886 | 326 | body=body, | ||
887 | 327 | body_alternative=body_alternative, | ||
888 | 328 | email_cc=to_email(message.email_cc), | ||
889 | 329 | email_bcc=to_email(message.email_bcc), | ||
890 | 330 | reply_to=message.reply_to, | ||
891 | 331 | attachments=attachments, message_id=message.message_id, | ||
892 | 332 | references=message.references, | ||
893 | 333 | object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)), | ||
894 | 334 | subtype=message.subtype, | ||
895 | 335 | subtype_alternative=subtype_alternative, | ||
896 | 336 | headers=message.headers and ast.literal_eval(message.headers)) | ||
897 | 337 | res = ir_mail_server.send_email(cr, uid, msg, | ||
898 | 338 | mail_server_id=message.mail_server_id.id, | ||
899 | 339 | context=context) | ||
900 | 340 | if res: | ||
901 | 341 | message.write({'state':'sent', 'message_id': res}) | ||
902 | 342 | else: | ||
903 | 343 | message.write({'state':'exception'}) | ||
904 | 344 | |||
905 | 345 | # if auto_delete=True then delete that sent messages as well as attachments | ||
906 | 346 | message.refresh() | ||
907 | 347 | if message.state == 'sent' and message.auto_delete: | ||
908 | 348 | self.pool.get('ir.attachment').unlink(cr, uid, | ||
909 | 349 | [x.id for x in message.attachment_ids \ | ||
910 | 350 | if x.res_model == self._name and \ | ||
911 | 351 | x.res_id == message.id], | ||
912 | 352 | context=context) | ||
913 | 353 | message.unlink() | ||
914 | 354 | except Exception: | ||
915 | 355 | _logger.exception('failed sending mail.message %s', message.id) | ||
916 | 356 | message.write({'state':'exception'}) | ||
917 | 357 | |||
918 | 358 | if auto_commit == True: | ||
919 | 359 | cr.commit() | ||
920 | 360 | return True | ||
921 | 361 | |||
922 | 362 | def create(self, cr, uid, values, context=None): | ||
923 | 363 | if 'workitem_id' in context: | ||
924 | 364 | values.update({'workitem_id': context['workitem_id']}) | ||
925 | 365 | return super(mail_message, self).create(cr, uid, values, context) | ||
926 | 0 | 366 | ||
927 | === modified file 'marketing_campaign/marketing_campaign.py' | |||
928 | --- marketing_campaign/marketing_campaign.py 2012-08-14 14:10:40 +0000 | |||
929 | +++ marketing_campaign/marketing_campaign.py 2012-08-27 10:03:27 +0000 | |||
930 | @@ -21,7 +21,7 @@ | |||
931 | 21 | 21 | ||
932 | 22 | import time | 22 | import time |
933 | 23 | import base64 | 23 | import base64 |
935 | 24 | import itertools | 24 | import hashlib |
936 | 25 | from datetime import datetime | 25 | from datetime import datetime |
937 | 26 | from dateutil.relativedelta import relativedelta | 26 | from dateutil.relativedelta import relativedelta |
938 | 27 | from operator import itemgetter | 27 | from operator import itemgetter |
939 | @@ -44,9 +44,11 @@ | |||
940 | 44 | 44 | ||
941 | 45 | DT_FMT = '%Y-%m-%d %H:%M:%S' | 45 | DT_FMT = '%Y-%m-%d %H:%M:%S' |
942 | 46 | 46 | ||
943 | 47 | |||
944 | 47 | def dict_map(f, d): | 48 | def dict_map(f, d): |
945 | 48 | return dict((k, f(v)) for k,v in d.items()) | 49 | return dict((k, f(v)) for k,v in d.items()) |
946 | 49 | 50 | ||
947 | 51 | |||
948 | 50 | def _find_fieldname(model, field): | 52 | def _find_fieldname(model, field): |
949 | 51 | inherit_columns = dict_map(itemgetter(2), model._inherit_fields) | 53 | inherit_columns = dict_map(itemgetter(2), model._inherit_fields) |
950 | 52 | all_columns = dict(inherit_columns, **model._columns) | 54 | all_columns = dict(inherit_columns, **model._columns) |
951 | @@ -55,6 +57,14 @@ | |||
952 | 55 | return fn | 57 | return fn |
953 | 56 | raise ValueError('Field not found: %r' % (field,)) | 58 | raise ValueError('Field not found: %r' % (field,)) |
954 | 57 | 59 | ||
955 | 60 | |||
956 | 61 | def compute_track_str(cr, uid, string, context=None): | ||
957 | 62 | db_uuid = osv.pooler.get_pool(cr.dbname).get('ir.config_parameter').get_param(cr, uid, 'database.uuid') | ||
958 | 63 | m = hashlib.md5() | ||
959 | 64 | m.update(db_uuid + '_' + string) | ||
960 | 65 | return m.hexdigest() | ||
961 | 66 | |||
962 | 67 | |||
963 | 58 | class selection_converter(object): | 68 | class selection_converter(object): |
964 | 59 | """Format the selection in the browse record objects""" | 69 | """Format the selection in the browse record objects""" |
965 | 60 | def __init__(self, value): | 70 | def __init__(self, value): |
966 | @@ -88,6 +98,7 @@ | |||
967 | 88 | 98 | ||
968 | 89 | _columns = { | 99 | _columns = { |
969 | 90 | 'name': fields.char('Name', size=64, required=True), | 100 | 'name': fields.char('Name', size=64, required=True), |
970 | 101 | 'track_str': fields.char('Tracking String', 32), | ||
971 | 91 | 'object_id': fields.many2one('ir.model', 'Resource', required=True, | 102 | 'object_id': fields.many2one('ir.model', 'Resource', required=True, |
972 | 92 | help="Choose the resource on which you want \ | 103 | help="Choose the resource on which you want \ |
973 | 93 | this campaign to be run"), | 104 | this campaign to be run"), |
974 | @@ -128,6 +139,13 @@ | |||
975 | 128 | 'mode': lambda *a: 'test', | 139 | 'mode': lambda *a: 'test', |
976 | 129 | } | 140 | } |
977 | 130 | 141 | ||
978 | 142 | def create(self, cr, uid, vals, context=None): | ||
979 | 143 | res_id = super(marketing_campaign, self).create(cr, uid, vals, context) | ||
980 | 144 | track_str = compute_track_str(cr, uid, self._name + '_' + str(res_id)) | ||
981 | 145 | vals = {'track_str': track_str} | ||
982 | 146 | self.pool.get(self._name).write(cr, uid, res_id, vals) | ||
983 | 147 | return res_id | ||
984 | 148 | |||
985 | 131 | def state_running_set(self, cr, uid, ids, *args): | 149 | def state_running_set(self, cr, uid, ids, *args): |
986 | 132 | # TODO check that all subcampaigns are running | 150 | # TODO check that all subcampaigns are running |
987 | 133 | campaign = self.browse(cr, uid, ids[0]) | 151 | campaign = self.browse(cr, uid, ids[0]) |
988 | @@ -386,6 +404,7 @@ | |||
989 | 386 | 404 | ||
990 | 387 | marketing_campaign_segment() | 405 | marketing_campaign_segment() |
991 | 388 | 406 | ||
992 | 407 | |||
993 | 389 | class marketing_campaign_activity(osv.osv): | 408 | class marketing_campaign_activity(osv.osv): |
994 | 390 | _name = "marketing.campaign.activity" | 409 | _name = "marketing.campaign.activity" |
995 | 391 | _order = "name" | 410 | _order = "name" |
996 | @@ -403,6 +422,7 @@ | |||
997 | 403 | 422 | ||
998 | 404 | _columns = { | 423 | _columns = { |
999 | 405 | 'name': fields.char('Name', size=128, required=True), | 424 | 'name': fields.char('Name', size=128, required=True), |
1000 | 425 | 'track_str': fields.char('Tracking String', 32), | ||
1001 | 406 | 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', | 426 | 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', |
1002 | 407 | required = True, ondelete='cascade', select=1), | 427 | required = True, ondelete='cascade', select=1), |
1003 | 408 | 'object_id': fields.related('campaign_id','object_id', | 428 | 'object_id': fields.related('campaign_id','object_id', |
1004 | @@ -417,6 +437,8 @@ | |||
1005 | 417 | " - resource: the resource object this campaign item represents\n" | 437 | " - resource: the resource object this campaign item represents\n" |
1006 | 418 | " - transitions: list of campaign transitions outgoing from this activity\n" | 438 | " - transitions: list of campaign transitions outgoing from this activity\n" |
1007 | 419 | "...- re: Python regular expression module"), | 439 | "...- re: Python regular expression module"), |
1008 | 440 | 'parent_response_cond': fields.char('Response to previous action', None), | ||
1009 | 441 | 'parent_no_response': fields.boolean('Run only when no response to previous action', help='This does not include "mail sent", "mail opened" or "url viewed" responses.'), | ||
1010 | 420 | 'type': fields.selection(_action_types, 'Type', required=True, | 442 | 'type': fields.selection(_action_types, 'Type', required=True, |
1011 | 421 | help="""The type of action to execute when an item enters this activity, such as: | 443 | help="""The type of action to execute when an item enters this activity, such as: |
1012 | 422 | - Email: send an email using a predefined email template | 444 | - Email: send an email using a predefined email template |
1013 | @@ -446,6 +468,7 @@ | |||
1014 | 446 | _defaults = { | 468 | _defaults = { |
1015 | 447 | 'type': lambda *a: 'email', | 469 | 'type': lambda *a: 'email', |
1016 | 448 | 'condition': lambda *a: 'True', | 470 | 'condition': lambda *a: 'True', |
1017 | 471 | 'parent_no_response': False, | ||
1018 | 449 | } | 472 | } |
1019 | 450 | 473 | ||
1020 | 451 | def search(self, cr, uid, args, offset=0, limit=None, order=None, | 474 | def search(self, cr, uid, args, offset=0, limit=None, order=None, |
1021 | @@ -479,10 +502,40 @@ | |||
1022 | 479 | return True | 502 | return True |
1023 | 480 | 503 | ||
1024 | 481 | def _process_wi_email(self, cr, uid, activity, workitem, context=None): | 504 | def _process_wi_email(self, cr, uid, activity, workitem, context=None): |
1026 | 482 | return self.pool.get('email.template').send_mail(cr, uid, | 505 | retval = None |
1027 | 506 | send_mail = True | ||
1028 | 507 | |||
1029 | 508 | target_model = self.pool.get(activity.object_id['model']) | ||
1030 | 509 | target = target_model.browse(cr, uid, workitem.res_id, context) | ||
1031 | 510 | # TODO: make sure opt_out is called opt_out in all possible mail-receiving models | ||
1032 | 511 | send_mail &= 'opt_out' in target and not target.opt_out | ||
1033 | 512 | |||
1034 | 513 | if activity.campaign_id.partner_field_id: | ||
1035 | 514 | partner_id_field = activity.campaign_id.partner_field_id.name | ||
1036 | 515 | partner = target.__getattr__(partner_id_field) | ||
1037 | 516 | send_mail &= not partner.opt_out | ||
1038 | 517 | |||
1039 | 518 | if send_mail: | ||
1040 | 519 | retval = self.pool.get('email.template').send_mail(cr, uid, | ||
1041 | 483 | activity.email_template_id.id, | 520 | activity.email_template_id.id, |
1042 | 484 | workitem.res_id, context=context) | 521 | workitem.res_id, context=context) |
1043 | 485 | 522 | ||
1044 | 523 | if retval: | ||
1045 | 524 | self.pool.get('marketing.campaign.workitem').write(cr, uid, workitem.id, {'mail_sent': True}) | ||
1046 | 525 | tracklog_vals = { | ||
1047 | 526 | 'workitem_id': workitem.id, | ||
1048 | 527 | 'activity_id': workitem.activity_id.id, | ||
1049 | 528 | 'res_track_id': workitem.trackitem_id.id, | ||
1050 | 529 | 'mail_message_id': retval, | ||
1051 | 530 | 'log_date': datetime.now().strftime(DT_FMT), | ||
1052 | 531 | 'log_ip': '0.0.0.0', | ||
1053 | 532 | 'log_type': 'mail_sent', | ||
1054 | 533 | 'revenue': 0.0, | ||
1055 | 534 | 'log_url': 'N/A', | ||
1056 | 535 | } | ||
1057 | 536 | self.pool.get('marketing.campaign.tracklog').create(cr, uid, tracklog_vals) | ||
1058 | 537 | return retval | ||
1059 | 538 | |||
1060 | 486 | #dead code | 539 | #dead code |
1061 | 487 | def _process_wi_action(self, cr, uid, activity, workitem, context=None): | 540 | def _process_wi_action(self, cr, uid, activity, workitem, context=None): |
1062 | 488 | if context is None: | 541 | if context is None: |
1063 | @@ -511,6 +564,26 @@ | |||
1064 | 511 | workitem = workitem_obj.browse(cr, uid, wi_id, context=context) | 564 | workitem = workitem_obj.browse(cr, uid, wi_id, context=context) |
1065 | 512 | return action(cr, uid, activity, workitem, context=context) | 565 | return action(cr, uid, activity, workitem, context=context) |
1066 | 513 | 566 | ||
1067 | 567 | def onchange_parent_no_response(self, cr, uid, ids, value, context=None): | ||
1068 | 568 | parent_response_cond = None | ||
1069 | 569 | if not value: | ||
1070 | 570 | activities = self.pool.get(self._name).browse(cr, uid, ids, context) | ||
1071 | 571 | if len(activities): | ||
1072 | 572 | parent_response_cond = activities[0].parent_response_cond | ||
1073 | 573 | return {'value': {'parent_response_cond': parent_response_cond}} | ||
1074 | 574 | |||
1075 | 575 | def write(self, cr, uid, ids, vals, context=None): | ||
1076 | 576 | if 'parent_no_response' in vals and vals['parent_no_response']: | ||
1077 | 577 | vals['parent_response_cond'] = None | ||
1078 | 578 | return super(marketing_campaign_activity, self).write(cr, uid, ids, vals, context=context) | ||
1079 | 579 | |||
1080 | 580 | def create(self, cr, uid, vals, context=None): | ||
1081 | 581 | res_id = super(marketing_campaign_activity, self).create(cr, uid, vals, context) | ||
1082 | 582 | track_str = compute_track_str(cr, uid, self._name + '_' + str(res_id)) | ||
1083 | 583 | vals = {'track_str': track_str} | ||
1084 | 584 | self.pool.get(self._name).write(cr, uid, res_id, vals, context=context) | ||
1085 | 585 | return res_id | ||
1086 | 586 | |||
1087 | 514 | marketing_campaign_activity() | 587 | marketing_campaign_activity() |
1088 | 515 | 588 | ||
1089 | 516 | class marketing_campaign_transition(osv.osv): | 589 | class marketing_campaign_transition(osv.osv): |
1090 | @@ -637,6 +710,7 @@ | |||
1091 | 637 | return [('id', 'in', list(set(matching_workitems)))] | 710 | return [('id', 'in', list(set(matching_workitems)))] |
1092 | 638 | 711 | ||
1093 | 639 | _columns = { | 712 | _columns = { |
1094 | 713 | 'parent_id': fields.many2one('marketing.campaign.workitem', 'Parent'), | ||
1095 | 640 | 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True), | 714 | 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True), |
1096 | 641 | 'activity_id': fields.many2one('marketing.campaign.activity','Activity', | 715 | 'activity_id': fields.many2one('marketing.campaign.activity','Activity', |
1097 | 642 | required=True, readonly=True), | 716 | required=True, readonly=True), |
1098 | @@ -646,6 +720,8 @@ | |||
1099 | 646 | type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True), | 720 | type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True), |
1100 | 647 | 'res_id': fields.integer('Resource ID', select=1, readonly=True), | 721 | 'res_id': fields.integer('Resource ID', select=1, readonly=True), |
1101 | 648 | 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64), | 722 | 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64), |
1102 | 723 | 'trackitem_id': fields.many2one('marketing.campaign.trackitem', 'Tracking Tag'), | ||
1103 | 724 | 'tracklog_ids': fields.one2many('marketing.campaign.tracklog', 'workitem_id', 'Tracking Logs'), | ||
1104 | 649 | 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True), | 725 | 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True), |
1105 | 650 | 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True), | 726 | 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True), |
1106 | 651 | 'state': fields.selection([ ('todo', 'To Do'), | 727 | 'state': fields.selection([ ('todo', 'To Do'), |
1107 | @@ -653,7 +729,15 @@ | |||
1108 | 653 | ('exception', 'Exception'), | 729 | ('exception', 'Exception'), |
1109 | 654 | ('done', 'Done'), | 730 | ('done', 'Done'), |
1110 | 655 | ], 'Status', readonly=True), | 731 | ], 'Status', readonly=True), |
1112 | 656 | 'error_msg' : fields.text('Error Message', readonly=True) | 732 | 'error_msg' : fields.text('Error Message', readonly=True), |
1113 | 733 | # Come from the tracklogs, but here for ease of reporting.. | ||
1114 | 734 | 'mail_sent': fields.boolean('Mail Sent'), | ||
1115 | 735 | 'mail_opened': fields.boolean('Mail Opened'), | ||
1116 | 736 | 'mail_forwarded': fields.boolean('Mail Forwarded'), | ||
1117 | 737 | 'mail_bounced': fields.boolean('Mail Bounced'), | ||
1118 | 738 | 'subscribers': fields.integer('Subscribers'), | ||
1119 | 739 | 'unsubscribed': fields.boolean('Unsubscribed'), | ||
1120 | 740 | 'clicks': fields.integer('Page Views'), | ||
1121 | 657 | } | 741 | } |
1122 | 658 | _defaults = { | 742 | _defaults = { |
1123 | 659 | 'state': lambda *a: 'todo', | 743 | 'state': lambda *a: 'todo', |
1124 | @@ -698,6 +782,35 @@ | |||
1125 | 698 | else: | 782 | else: |
1126 | 699 | workitem.unlink(context=context) | 783 | workitem.unlink(context=context) |
1127 | 700 | return | 784 | return |
1128 | 785 | # Make it easy for the user to express conditions | ||
1129 | 786 | # First: absense of response | ||
1130 | 787 | if activity.parent_no_response: | ||
1131 | 788 | searchvals = [ | ||
1132 | 789 | ('workitem_id', '=', workitem.parent_id.id), | ||
1133 | 790 | ('log_type', 'not in', ['mail_sent', 'mail_opened', 'url_viewed']), | ||
1134 | 791 | ] | ||
1135 | 792 | parent_responses = self.pool.get('marketing.campaign.tracklog').search(cr, uid, searchvals, context=context) | ||
1136 | 793 | if len(parent_responses): | ||
1137 | 794 | if activity.keep_if_condition_not_met: | ||
1138 | 795 | workitem.write({'state': 'cancelled'}, context=context) | ||
1139 | 796 | else: | ||
1140 | 797 | workitem.unlink(context=context) | ||
1141 | 798 | return | ||
1142 | 799 | # Next, specific response | ||
1143 | 800 | parent_response_cond = activity.parent_response_cond | ||
1144 | 801 | if parent_response_cond and workitem.parent_id: | ||
1145 | 802 | searchvals = [ | ||
1146 | 803 | ('workitem_id', '=', workitem.parent_id.id), | ||
1147 | 804 | ('log_type', '=', parent_response_cond), | ||
1148 | 805 | ] | ||
1149 | 806 | parent_responses = self.pool.get('marketing.campaign.tracklog').search(cr, uid, searchvals, context=context) | ||
1150 | 807 | if not len(parent_responses): | ||
1151 | 808 | if activity.keep_if_condition_not_met: | ||
1152 | 809 | workitem.write({'state': 'cancelled'}, context=context) | ||
1153 | 810 | else: | ||
1154 | 811 | workitem.unlink(context=context) | ||
1155 | 812 | return | ||
1156 | 813 | # End modif | ||
1157 | 701 | result = True | 814 | result = True |
1158 | 702 | if campaign_mode in ('manual', 'active'): | 815 | if campaign_mode in ('manual', 'active'): |
1159 | 703 | Activities = self.pool.get('marketing.campaign.activity') | 816 | Activities = self.pool.get('marketing.campaign.activity') |
1160 | @@ -711,7 +824,7 @@ | |||
1161 | 711 | 824 | ||
1162 | 712 | if result: | 825 | if result: |
1163 | 713 | # process _chain | 826 | # process _chain |
1165 | 714 | workitem = workitem.browse(context=context)[0] # reload | 827 | workitem = workitem.browse(context=context)[0] # reload |
1166 | 715 | date = datetime.strptime(workitem.date, DT_FMT) | 828 | date = datetime.strptime(workitem.date, DT_FMT) |
1167 | 716 | 829 | ||
1168 | 717 | for transition in activity.to_ids: | 830 | for transition in activity.to_ids: |
1169 | @@ -732,6 +845,7 @@ | |||
1170 | 732 | 'partner_id': workitem.partner_id.id, | 845 | 'partner_id': workitem.partner_id.id, |
1171 | 733 | 'res_id': workitem.res_id, | 846 | 'res_id': workitem.res_id, |
1172 | 734 | 'state': 'todo', | 847 | 'state': 'todo', |
1173 | 848 | 'parent_id': workitem.id, # Keep track of parent workitem so we can use that for condition lookups | ||
1174 | 735 | } | 849 | } |
1175 | 736 | wi_id = self.create(cr, uid, values, context=context) | 850 | wi_id = self.create(cr, uid, values, context=context) |
1176 | 737 | 851 | ||
1177 | @@ -760,7 +874,18 @@ | |||
1178 | 760 | context=context) | 874 | context=context) |
1179 | 761 | 875 | ||
1180 | 762 | def process(self, cr, uid, workitem_ids, context=None): | 876 | def process(self, cr, uid, workitem_ids, context=None): |
1181 | 877 | if context is None: | ||
1182 | 878 | context = {} | ||
1183 | 763 | for wi in self.browse(cr, uid, workitem_ids, context=context): | 879 | for wi in self.browse(cr, uid, workitem_ids, context=context): |
1184 | 880 | campaign_track_str = wi.campaign_id.track_str | ||
1185 | 881 | activity_track_str = wi.activity_id.track_str | ||
1186 | 882 | res_track_str = wi.trackitem_id.track_str | ||
1187 | 883 | context.update({ | ||
1188 | 884 | 'campaign_track_str': campaign_track_str, | ||
1189 | 885 | 'activity_track_str': activity_track_str, | ||
1190 | 886 | 'res_track_str': res_track_str, | ||
1191 | 887 | 'workitem_id': wi.id, | ||
1192 | 888 | }) | ||
1193 | 764 | self._process_one(cr, uid, wi, context=context) | 889 | self._process_one(cr, uid, wi, context=context) |
1194 | 765 | return True | 890 | return True |
1195 | 766 | 891 | ||
1196 | @@ -819,17 +944,55 @@ | |||
1197 | 819 | raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.')) | 944 | raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.')) |
1198 | 820 | return res | 945 | return res |
1199 | 821 | 946 | ||
1200 | 947 | def create(self, cr, uid, vals, context=None): | ||
1201 | 948 | # Get model we're working on via our associated activity | ||
1202 | 949 | activity_obj = self.pool.get('marketing.campaign.activity').browse(cr, uid, vals['activity_id']) | ||
1203 | 950 | search_domain = [ | ||
1204 | 951 | ('model_id', '=', activity_obj.object_id.id), | ||
1205 | 952 | ('res_id', '=', vals['res_id']), | ||
1206 | 953 | ] | ||
1207 | 954 | trackitem_obj = self.pool.get('marketing.campaign.trackitem') | ||
1208 | 955 | found_trackitems = trackitem_obj.search(cr, uid, search_domain) | ||
1209 | 956 | # And if no trackitem is fond, corresponding the criteria, create one | ||
1210 | 957 | if len(found_trackitems): | ||
1211 | 958 | trackitem_id = found_trackitems[0] | ||
1212 | 959 | else: | ||
1213 | 960 | wvals = { | ||
1214 | 961 | 'model_id': activity_obj.object_id.id, | ||
1215 | 962 | 'res_id': vals['res_id'], | ||
1216 | 963 | } | ||
1217 | 964 | trackitem_id = trackitem_obj.create(cr, uid, wvals, context) | ||
1218 | 965 | vals.update({'trackitem_id': trackitem_id}) | ||
1219 | 966 | return super(marketing_campaign_workitem, self).create(cr, uid, vals, context) | ||
1220 | 967 | |||
1221 | 822 | marketing_campaign_workitem() | 968 | marketing_campaign_workitem() |
1222 | 823 | 969 | ||
1227 | 824 | class email_template(osv.osv): | 970 | |
1228 | 825 | _inherit = "email.template" | 971 | class marketing_campaign_trackitem(osv.osv): |
1229 | 826 | _defaults = { | 972 | _name = 'marketing.campaign.trackitem' |
1230 | 827 | 'model_id': lambda obj, cr, uid, context: context.get('object_id',False), | 973 | _rec_name = 'track_str' |
1231 | 974 | _description = 'Tracking Tags' | ||
1232 | 975 | |||
1233 | 976 | _columns = { | ||
1234 | 977 | 'model_id': fields.many2one('ir.model', 'Resource', required=True), | ||
1235 | 978 | 'res_id': fields.integer('Resource ID', required=True), | ||
1236 | 979 | 'track_str': fields.char('Resource Code', 32), | ||
1237 | 828 | } | 980 | } |
1238 | 829 | 981 | ||
1242 | 830 | # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign | 982 | def create(self, cr, uid, vals, context=None): |
1243 | 831 | 983 | res_id = super(marketing_campaign_trackitem, self).create(cr, uid, vals, context) | |
1244 | 832 | email_template() | 984 | ir_model = self.pool.get('ir.model').browse(cr, uid, vals['model_id'], context=context) |
1245 | 985 | model_name = ir_model.model | ||
1246 | 986 | if 'track_str' not in vals: | ||
1247 | 987 | res_track_str = compute_track_str(cr, uid, model_name + '_' + str(vals['res_id'])) | ||
1248 | 988 | wvals = { | ||
1249 | 989 | 'track_str': res_track_str, | ||
1250 | 990 | } | ||
1251 | 991 | self.pool.get('marketing.campaign.trackitem').write(cr, uid, res_id, wvals, context) | ||
1252 | 992 | return res_id | ||
1253 | 993 | |||
1254 | 994 | marketing_campaign_trackitem() | ||
1255 | 995 | |||
1256 | 833 | 996 | ||
1257 | 834 | class report_xml(osv.osv): | 997 | class report_xml(osv.osv): |
1258 | 835 | _inherit = 'ir.actions.report.xml' | 998 | _inherit = 'ir.actions.report.xml' |
1259 | 836 | 999 | ||
1260 | === modified file 'marketing_campaign/marketing_campaign_view.xml' | |||
1261 | --- marketing_campaign/marketing_campaign_view.xml 2012-08-08 13:06:14 +0000 | |||
1262 | +++ marketing_campaign/marketing_campaign_view.xml 2012-08-27 10:03:27 +0000 | |||
1263 | @@ -261,6 +261,8 @@ | |||
1264 | 261 | </group> | 261 | </group> |
1265 | 262 | <group > | 262 | <group > |
1266 | 263 | <group> | 263 | <group> |
1267 | 264 | <field name="parent_response_cond" attrs="{'readonly': [('parent_no_response', '=', True)]}"/> | ||
1268 | 265 | <field name="parent_no_response" on_change="onchange_parent_no_response(parent_no_response)"/> | ||
1269 | 264 | <field name="condition" widget="char"/> | 266 | <field name="condition" widget="char"/> |
1270 | 265 | <field name="keep_if_condition_not_met"/> | 267 | <field name="keep_if_condition_not_met"/> |
1271 | 266 | </group> | 268 | </group> |
1272 | 267 | 269 | ||
1273 | === modified file 'marketing_campaign/report/__init__.py' | |||
1274 | --- marketing_campaign/report/__init__.py 2011-01-14 00:11:01 +0000 | |||
1275 | +++ marketing_campaign/report/__init__.py 2012-08-27 10:03:27 +0000 | |||
1276 | @@ -20,5 +20,6 @@ | |||
1277 | 20 | ############################################################################## | 20 | ############################################################################## |
1278 | 21 | 21 | ||
1279 | 22 | import campaign_analysis | 22 | import campaign_analysis |
1280 | 23 | import campaign_tracking | ||
1281 | 23 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: | 24 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
1282 | 24 | 25 | ||
1283 | 25 | 26 | ||
1284 | === modified file 'marketing_campaign/report/campaign_analysis.py' | |||
1285 | --- marketing_campaign/report/campaign_analysis.py 2012-07-10 12:34:00 +0000 | |||
1286 | +++ marketing_campaign/report/campaign_analysis.py 2012-08-27 10:03:27 +0000 | |||
1287 | @@ -43,6 +43,7 @@ | |||
1288 | 43 | ((ca_obj.campaign_id.fixed_cost or 1.00) / len(wi_ids)) | 43 | ((ca_obj.campaign_id.fixed_cost or 1.00) / len(wi_ids)) |
1289 | 44 | result[ca_obj.id] = total_cost | 44 | result[ca_obj.id] = total_cost |
1290 | 45 | return result | 45 | return result |
1291 | 46 | |||
1292 | 46 | _columns = { | 47 | _columns = { |
1293 | 47 | 'res_id' : fields.integer('Resource', readonly=True), | 48 | 'res_id' : fields.integer('Resource', readonly=True), |
1294 | 48 | 'year': fields.char('Year', size=4, readonly=True), | 49 | 'year': fields.char('Year', size=4, readonly=True), |
1295 | @@ -66,17 +67,32 @@ | |||
1296 | 66 | type="float", digits_compute=dp.get_precision('Account')), | 67 | type="float", digits_compute=dp.get_precision('Account')), |
1297 | 67 | 'revenue': fields.float('Revenue', readonly=True, digits_compute=dp.get_precision('Account')), | 68 | 'revenue': fields.float('Revenue', readonly=True, digits_compute=dp.get_precision('Account')), |
1298 | 68 | 'count' : fields.integer('# of Actions', readonly=True), | 69 | 'count' : fields.integer('# of Actions', readonly=True), |
1302 | 69 | 'state': fields.selection([('todo', 'To Do'), | 70 | # 'state': fields.selection([('todo', 'To Do'), |
1303 | 70 | ('exception', 'Exception'), ('done', 'Done'), | 71 | # ('exception', 'Exception'), ('done', 'Done'), |
1304 | 71 | ('cancelled', 'Cancelled')], 'Status', readonly=True), | 72 | # ('cancelled', 'Cancelled')], 'Status', readonly=True), |
1305 | 73 | # TODO: clean these up; don't think we still use them all | ||
1306 | 74 | 'mail_sent': fields.integer('Mail Sent'), | ||
1307 | 75 | 'mail_sent_bool': fields.boolean('Mail Sent B'), | ||
1308 | 76 | 'mail_opened': fields.integer('Mail Opened'), | ||
1309 | 77 | 'mail_opened_bool': fields.boolean('Mail Opened B'), | ||
1310 | 78 | 'mail_forwarded': fields.integer('Mail Forwarded'), | ||
1311 | 79 | 'mail_forwarded_bool': fields.boolean('Mail Forwarded B'), | ||
1312 | 80 | 'mail_bounced': fields.integer('Mail Bounced'), | ||
1313 | 81 | 'mail_bounced_bool': fields.boolean('Mail Bounced B'), | ||
1314 | 82 | # 'subscribers': fields.integer('Subscribers'), | ||
1315 | 83 | 'unsubscribed': fields.integer('Unsubscribed'), | ||
1316 | 84 | 'unsubscribed_bool': fields.boolean('Unsubscribed B'), | ||
1317 | 85 | 'clicks': fields.integer('Page Views'), | ||
1318 | 72 | } | 86 | } |
1319 | 87 | |||
1320 | 73 | def init(self, cr): | 88 | def init(self, cr): |
1324 | 74 | tools.drop_view_if_exists(cr, 'campaign_analysis') | 89 | tools.drop_view_if_exists(cr, self._table) |
1325 | 75 | cr.execute(""" | 90 | args = (self._table) |
1326 | 76 | create or replace view campaign_analysis as ( | 91 | query = """ |
1327 | 92 | create or replace view %s as ( | ||
1328 | 77 | select | 93 | select |
1331 | 78 | min(wi.id) as id, | 94 | wi.id as id, |
1332 | 79 | min(wi.res_id) as res_id, | 95 | wi.res_id as res_id, |
1333 | 80 | to_char(wi.date::date, 'YYYY') as year, | 96 | to_char(wi.date::date, 'YYYY') as year, |
1334 | 81 | to_char(wi.date::date, 'MM') as month, | 97 | to_char(wi.date::date, 'MM') as month, |
1335 | 82 | to_char(wi.date::date, 'YYYY-MM-DD') as day, | 98 | to_char(wi.date::date, 'YYYY-MM-DD') as day, |
1336 | @@ -85,19 +101,82 @@ | |||
1337 | 85 | wi.activity_id as activity_id, | 101 | wi.activity_id as activity_id, |
1338 | 86 | wi.segment_id as segment_id, | 102 | wi.segment_id as segment_id, |
1339 | 87 | wi.partner_id as partner_id , | 103 | wi.partner_id as partner_id , |
1343 | 88 | wi.state as state, | 104 | wi.mail_sent as mail_sent_bool, |
1344 | 89 | sum(act.revenue) as revenue, | 105 | wi.mail_sent::int as mail_sent, |
1345 | 90 | count(*) as count | 106 | wi.mail_opened as mail_opened_bool, |
1346 | 107 | wi.mail_opened::int as mail_opened, | ||
1347 | 108 | wi.mail_forwarded as mail_forwarded_bool, | ||
1348 | 109 | wi.mail_forwarded::int as mail_forwarded, | ||
1349 | 110 | wi.mail_bounced as mail_bounced_bool, | ||
1350 | 111 | wi.mail_bounced::int as mail_bounced, | ||
1351 | 112 | wi.unsubscribed as unsubscribed_bool, | ||
1352 | 113 | wi.unsubscribed::int as unsubscribed, | ||
1353 | 114 | (select count(*) from marketing_campaign_tracklog tl | ||
1354 | 115 | where tl.log_type = 'url_viewed' | ||
1355 | 116 | and tl.workitem_id = wi.id) | ||
1356 | 117 | as clicks, | ||
1357 | 118 | coalesce(act.revenue + | ||
1358 | 119 | (select sum(revenue) from marketing_campaign_tracklog tl | ||
1359 | 120 | where tl.workitem_id = wi.id), | ||
1360 | 121 | 0.0) as revenue | ||
1361 | 91 | from | 122 | from |
1362 | 92 | marketing_campaign_workitem wi | 123 | marketing_campaign_workitem wi |
1363 | 93 | left join res_partner p on (p.id=wi.partner_id) | 124 | left join res_partner p on (p.id=wi.partner_id) |
1364 | 94 | left join marketing_campaign_segment s on (s.id=wi.segment_id) | 125 | left join marketing_campaign_segment s on (s.id=wi.segment_id) |
1365 | 95 | left join marketing_campaign_activity act on (act.id= wi.activity_id) | 126 | left join marketing_campaign_activity act on (act.id= wi.activity_id) |
1366 | 96 | group by | 127 | group by |
1369 | 97 | s.campaign_id,wi.activity_id,wi.segment_id,wi.partner_id,wi.state, | 128 | wi.id,wi.res_id,wi.date,wi.activity_id,wi.segment_id,wi.partner_id,wi.mail_sent,wi.mail_opened,wi.mail_forwarded,wi.mail_bounced,wi.unsubscribed,act.revenue,s.campaign_id,act.id |
1368 | 98 | wi.date::date | ||
1370 | 99 | ) | 129 | ) |
1372 | 100 | """) | 130 | """ % args |
1373 | 131 | cr.execute(query) | ||
1374 | 132 | |||
1375 | 101 | campaign_analysis() | 133 | campaign_analysis() |
1376 | 102 | 134 | ||
1377 | 135 | class campaign_click_analysis(osv.osv): | ||
1378 | 136 | _name = "campaign.click_analysis" | ||
1379 | 137 | _inherit = "campaign.analysis" | ||
1380 | 138 | _description = 'Click Analysis' | ||
1381 | 139 | _auto = False | ||
1382 | 140 | |||
1383 | 141 | _columns = { | ||
1384 | 142 | # 'subscribers': fields.integer('Subscribers'), | ||
1385 | 143 | 'log_url': fields.char('URL', None), | ||
1386 | 144 | 'clicks': fields.integer('Page Views'), | ||
1387 | 145 | } | ||
1388 | 146 | |||
1389 | 147 | def init(self, cr): | ||
1390 | 148 | tools.drop_view_if_exists(cr, self._table) | ||
1391 | 149 | args = (self._table) | ||
1392 | 150 | query = """ | ||
1393 | 151 | create or replace view %s as ( | ||
1394 | 152 | select | ||
1395 | 153 | tl.id as id, | ||
1396 | 154 | wi.id as wi_id, | ||
1397 | 155 | tl.res_id as res_id, | ||
1398 | 156 | to_char(tl.log_date::date, 'YYYY') as year, | ||
1399 | 157 | to_char(tl.log_date::date, 'MM') as month, | ||
1400 | 158 | to_char(tl.log_date::date, 'YYYY-MM-DD') as day, | ||
1401 | 159 | tl.log_date::date as date, | ||
1402 | 160 | s.campaign_id as campaign_id, | ||
1403 | 161 | wi.activity_id as activity_id, | ||
1404 | 162 | wi.segment_id as segment_id, | ||
1405 | 163 | wi.partner_id as partner_id , | ||
1406 | 164 | 1 as clicks, | ||
1407 | 165 | coalesce(tl.log_url, 'N/A') as log_url, | ||
1408 | 166 | coalesce(act.revenue + tl.revenue, 0.0) as revenue | ||
1409 | 167 | from | ||
1410 | 168 | marketing_campaign_tracklog tl | ||
1411 | 169 | left join marketing_campaign_workitem wi on (tl.workitem_id = wi.id) | ||
1412 | 170 | left join res_partner p on (p.id=wi.partner_id) | ||
1413 | 171 | left join marketing_campaign_segment s on (s.id=wi.segment_id) | ||
1414 | 172 | left join marketing_campaign_activity act on (act.id= wi.activity_id) | ||
1415 | 173 | where tl.log_type = 'url_viewed' | ||
1416 | 174 | group by | ||
1417 | 175 | tl.id,wi.id,tl.res_id,tl.log_date,s.campaign_id,wi.activity_id,wi.segment_id,wi.partner_id,tl.log_url,act.revenue,tl.revenue,act.id | ||
1418 | 176 | ) | ||
1419 | 177 | """ % args | ||
1420 | 178 | cr.execute(query) | ||
1421 | 179 | |||
1422 | 180 | campaign_click_analysis() | ||
1423 | 181 | |||
1424 | 103 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: | 182 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
1425 | 104 | 183 | ||
1426 | === modified file 'marketing_campaign/report/campaign_analysis_view.xml' | |||
1427 | --- marketing_campaign/report/campaign_analysis_view.xml 2012-08-08 13:06:14 +0000 | |||
1428 | +++ marketing_campaign/report/campaign_analysis_view.xml 2012-08-27 10:03:27 +0000 | |||
1429 | @@ -10,14 +10,24 @@ | |||
1430 | 10 | <field name="month" invisible="1"/> | 10 | <field name="month" invisible="1"/> |
1431 | 11 | <field name="day" invisible="1"/> | 11 | <field name="day" invisible="1"/> |
1432 | 12 | <field name="date" invisible="1"/> | 12 | <field name="date" invisible="1"/> |
1434 | 13 | <field name="state" invisible="1"/> | 13 | <!-- <field name="state" invisible="1"/> --> |
1435 | 14 | <field name="campaign_id" invisible="1"/> | 14 | <field name="campaign_id" invisible="1"/> |
1436 | 15 | <field name="activity_id" invisible="1"/> | 15 | <field name="activity_id" invisible="1"/> |
1437 | 16 | <field name="segment_id" invisible="1"/> | 16 | <field name="segment_id" invisible="1"/> |
1438 | 17 | <field name="partner_id" invisible="1"/> | 17 | <field name="partner_id" invisible="1"/> |
1439 | 18 | <field name="country_id" invisible="1"/> | 18 | <field name="country_id" invisible="1"/> |
1440 | 19 | <field name="res_id" invisible="1"/> | 19 | <field name="res_id" invisible="1"/> |
1442 | 20 | <field name="count"/> | 20 | <field name="mail_sent"/> |
1443 | 21 | <field name="mail_sent_bool" invisible="1"/> | ||
1444 | 22 | <field name="mail_opened"/> | ||
1445 | 23 | <field name="mail_opened_bool" invisible="1"/> | ||
1446 | 24 | <field name="mail_forwarded"/> | ||
1447 | 25 | <field name="mail_forwarded_bool" invisible="1"/> | ||
1448 | 26 | <field name="mail_bounced"/> | ||
1449 | 27 | <field name="mail_bounced_bool" invisible="1"/> | ||
1450 | 28 | <field name="unsubscribed"/> | ||
1451 | 29 | <field name="unsubscribed_bool" invisible="1"/> | ||
1452 | 30 | <field name="clicks"/> | ||
1453 | 21 | <field name="total_cost" string="Cost"/><!-- sum="Cost"/--> | 31 | <field name="total_cost" string="Cost"/><!-- sum="Cost"/--> |
1454 | 22 | <field name="revenue"/> | 32 | <field name="revenue"/> |
1455 | 23 | </tree> | 33 | </tree> |
1456 | @@ -29,6 +39,7 @@ | |||
1457 | 29 | <field name="model">campaign.analysis</field> | 39 | <field name="model">campaign.analysis</field> |
1458 | 30 | <field name="arch" type="xml"> | 40 | <field name="arch" type="xml"> |
1459 | 31 | <search string="Campaign Analysis"> | 41 | <search string="Campaign Analysis"> |
1460 | 42 | <<<<<<< TREE | ||
1461 | 32 | <field name="date"/> | 43 | <field name="date"/> |
1462 | 33 | <filter icon="terp-gtk-go-back-rtl" string="To Do" domain="[('state','=','todo')]"/> | 44 | <filter icon="terp-gtk-go-back-rtl" string="To Do" domain="[('state','=','todo')]"/> |
1463 | 34 | <filter icon="terp-dialog-close" string="Done" domain="[('state','=','done')]"/> | 45 | <filter icon="terp-dialog-close" string="Done" domain="[('state','=','done')]"/> |
1464 | @@ -38,12 +49,45 @@ | |||
1465 | 38 | <field name="segment_id"/> | 49 | <field name="segment_id"/> |
1466 | 39 | <field name="partner_id"/> | 50 | <field name="partner_id"/> |
1467 | 40 | <field name="country_id"/> | 51 | <field name="country_id"/> |
1468 | 52 | ======= | ||
1469 | 53 | <group> | ||
1470 | 54 | <field name="date"/> | ||
1471 | 55 | <!-- <separator orientation="vertical"/> | ||
1472 | 56 | <filter icon="terp-gtk-go-back-rtl" | ||
1473 | 57 | string="To Do" | ||
1474 | 58 | domain="[('state','=','todo')]"/> | ||
1475 | 59 | <filter icon="terp-dialog-close" | ||
1476 | 60 | string="Done" | ||
1477 | 61 | domain="[('state','=','done')]"/> | ||
1478 | 62 | <filter icon="terp-emblem-important" | ||
1479 | 63 | string="Exceptions" | ||
1480 | 64 | domain="[('state','=','exception')]"/> | ||
1481 | 65 | --> <separator orientation="vertical"/> | ||
1482 | 66 | <field name="campaign_id"/> | ||
1483 | 67 | <field name="activity_id"/> | ||
1484 | 68 | <field name="segment_id"/> | ||
1485 | 69 | <field name="partner_id"/> | ||
1486 | 70 | <field name="country_id"/> | ||
1487 | 71 | </group> | ||
1488 | 72 | <newline/> | ||
1489 | 73 | >>>>>>> MERGE-SOURCE | ||
1490 | 41 | <group expand="0" string="Group By..."> | 74 | <group expand="0" string="Group By..."> |
1491 | 42 | <filter string="Campaign" name="Campaign" icon="terp-gtk-jump-to-rtl" context="{'group_by':'campaign_id'}" /> | 75 | <filter string="Campaign" name="Campaign" icon="terp-gtk-jump-to-rtl" context="{'group_by':'campaign_id'}" /> |
1492 | 43 | <filter string="Segment" name="Segment" icon="terp-stock_symbol-selection" context="{'group_by':'segment_id'}"/> | 76 | <filter string="Segment" name="Segment" icon="terp-stock_symbol-selection" context="{'group_by':'segment_id'}"/> |
1493 | 44 | <filter string="Activity" name="activity" icon="terp-stock_align_left_24" context="{'group_by':'activity_id'}"/> | 77 | <filter string="Activity" name="activity" icon="terp-stock_align_left_24" context="{'group_by':'activity_id'}"/> |
1494 | 45 | <filter string="Resource" icon="terp-accessories-archiver" context="{'group_by':'res_id'}"/> | 78 | <filter string="Resource" icon="terp-accessories-archiver" context="{'group_by':'res_id'}"/> |
1495 | 79 | <<<<<<< TREE | ||
1496 | 46 | <filter string="Status" icon="terp-stock_effects-object-colorize" context="{'group_by':'state'}"/> | 80 | <filter string="Status" icon="terp-stock_effects-object-colorize" context="{'group_by':'state'}"/> |
1497 | 81 | ======= | ||
1498 | 82 | <separator orientation="vertical"/> | ||
1499 | 83 | <filter string="Mail Sent" icon="terp-mail-" context="{'group_by':'mail_sent_bool'}"/> | ||
1500 | 84 | <filter string="Mail Opened" icon="terp-mail-message-new" context="{'group_by':'mail_opened_bool'}"/> | ||
1501 | 85 | <filter string="Mail Forwarded" icon="terp-mail-forward" context="{'group_by':'mail_forwarded_bool'}"/> | ||
1502 | 86 | <filter string="Mail Bounced" icon="terp-mail_delete" context="{'group_by':'mail_bounced_bool'}"/> | ||
1503 | 87 | <filter string="Unsubscribed" icon="terp-mail_delete" context="{'group_by':'unsubscribed_bool'}"/> | ||
1504 | 88 | |||
1505 | 89 | <separator orientation="vertical"/> | ||
1506 | 90 | >>>>>>> MERGE-SOURCE | ||
1507 | 47 | <filter string="Partner" icon="terp-partner" context="{'group_by':'partner_id'}"/> | 91 | <filter string="Partner" icon="terp-partner" context="{'group_by':'partner_id'}"/> |
1508 | 48 | <filter string="Day" icon="terp-go-today" context="{'group_by':'day'}"/> | 92 | <filter string="Day" icon="terp-go-today" context="{'group_by':'day'}"/> |
1509 | 49 | <filter string="Month" icon="terp-go-month" context="{'group_by':'month'}"/> | 93 | <filter string="Month" icon="terp-go-month" context="{'group_by':'month'}"/> |
1510 | @@ -53,17 +97,97 @@ | |||
1511 | 53 | </field> | 97 | </field> |
1512 | 54 | </record> | 98 | </record> |
1513 | 55 | 99 | ||
1522 | 56 | <record id="action_campaign_analysis_all" model="ir.actions.act_window"> | 100 | |
1523 | 57 | <field name="name">Campaign Analysis</field> | 101 | <record id="view_campaign_click_analysis_tree" model="ir.ui.view"> |
1524 | 58 | <field name="res_model">campaign.analysis</field> | 102 | <field name="name">campaign.click_analysis.tree</field> |
1525 | 59 | <field name="view_type">form</field> | 103 | <field name="model">campaign.click_analysis</field> |
1526 | 60 | <field name="view_mode">tree</field> | 104 | <field name="type">tree</field> |
1527 | 61 | <field name="context">{'search_default_year': 1,"search_default_This Month":1,'group_by': [], 'search_default_Campaign': 1, 'search_default_Segment': 1, 'group_by_no_leaf':1}</field> | 105 | <field name="arch" type="xml"> |
1528 | 62 | <field name="search_view_id" ref="view_campaign_analysis_search"/> | 106 | <tree string="Marketing Reports"> |
1529 | 63 | </record> | 107 | <field name="year" invisible="1"/> |
1530 | 108 | <field name="month" invisible="1"/> | ||
1531 | 109 | <field name="day" invisible="1"/> | ||
1532 | 110 | <field name="date" invisible="1"/> | ||
1533 | 111 | <!-- <field name="state" invisible="1"/> --> | ||
1534 | 112 | <field name="campaign_id" invisible="1"/> | ||
1535 | 113 | <field name="activity_id" invisible="1"/> | ||
1536 | 114 | <field name="segment_id" invisible="1"/> | ||
1537 | 115 | <field name="partner_id" invisible="1"/> | ||
1538 | 116 | <!-- <field name="country_id" invisible="1"/> --> | ||
1539 | 117 | <field name="res_id" invisible="1"/> | ||
1540 | 118 | <field name="log_url" invisible="1"/> | ||
1541 | 119 | <field name="total_cost" string="Cost"/><!-- sum="Cost"/--> | ||
1542 | 120 | <field name="revenue"/> | ||
1543 | 121 | </tree> | ||
1544 | 122 | </field> | ||
1545 | 123 | </record> | ||
1546 | 124 | |||
1547 | 125 | <record id="view_campaign_click_analysis_search" model="ir.ui.view"> | ||
1548 | 126 | <field name="name">campaign.click_analysis.search</field> | ||
1549 | 127 | <field name="model">campaign.click_analysis</field> | ||
1550 | 128 | <field name="type">search</field> | ||
1551 | 129 | <field name="arch" type="xml"> | ||
1552 | 130 | <search string="Click Analysis"> | ||
1553 | 131 | <group> | ||
1554 | 132 | <filter icon="terp-go-year" name="year" | ||
1555 | 133 | string="Year" | ||
1556 | 134 | domain="[('year','=',time.strftime('%%Y'))]"/> | ||
1557 | 135 | <separator orientation="vertical"/> | ||
1558 | 136 | |||
1559 | 137 | <filter icon="terp-go-month" | ||
1560 | 138 | string="Month" name="This Month" | ||
1561 | 139 | domain="[('month','=',time.strftime('%%m'))]"/> | ||
1562 | 140 | <filter icon="terp-go-month" string=" Month-1 " | ||
1563 | 141 | domain="[('create_date','<=', (datetime.date.today() - relativedelta(day=31, months=1)).strftime('%%Y-%%m-%%d')),('create_date','>=',(datetime.date.today() - relativedelta(day=1,months=1)).strftime('%%Y-%%m-%%d'))]"/> | ||
1564 | 142 | |||
1565 | 143 | <filter icon="terp-go-today" | ||
1566 | 144 | string="Today" | ||
1567 | 145 | domain="[('date','=',time.strftime('%%Y/%%m/%%d'))]"/> | ||
1568 | 146 | <separator orientation="vertical"/> | ||
1569 | 147 | <field name="campaign_id"/> | ||
1570 | 148 | <field name="activity_id"/> | ||
1571 | 149 | <field name="segment_id"/> | ||
1572 | 150 | <field name="partner_id"/> | ||
1573 | 151 | <field name="country_id"/> | ||
1574 | 152 | </group> | ||
1575 | 153 | <newline/> | ||
1576 | 154 | <group expand="0" string="Group By..."> | ||
1577 | 155 | <filter string="Campaign" name="Campaign" icon="terp-gtk-jump-to-rtl" context="{'group_by':'campaign_id'}" /> | ||
1578 | 156 | <filter string="Segment" name="Segment" icon="terp-stock_symbol-selection" context="{'group_by':'segment_id'}" /> | ||
1579 | 157 | <filter string="Activity" name="activity" icon="terp-stock_align_left_24" context="{'group_by':'activity_id'}" /> | ||
1580 | 158 | <filter string="Resource" icon="terp-accessories-archiver" context="{'group_by':'res_id'}"/> | ||
1581 | 159 | <filter string="url" name="URL" icon="STOCK_FILE" context="{'group_by':'log_url'}"/> | ||
1582 | 160 | <separator orientation="vertical"/> | ||
1583 | 161 | <filter string="Partner" icon="terp-partner" context="{'group_by':'partner_id'}"/> | ||
1584 | 162 | <separator orientation="vertical"/> | ||
1585 | 163 | <filter string="Day" icon="terp-go-today" context="{'group_by':'day'}"/> | ||
1586 | 164 | <filter string="Month" icon="terp-go-month" context="{'group_by':'month'}"/> | ||
1587 | 165 | <filter string="Year" icon="terp-go-year" context="{'group_by':'year'}"/> | ||
1588 | 166 | </group> | ||
1589 | 167 | </search> | ||
1590 | 168 | </field> | ||
1591 | 169 | </record> | ||
1592 | 170 | |||
1593 | 171 | <record id="action_campaign_analysis_all" model="ir.actions.act_window"> | ||
1594 | 172 | <field name="name">E-Mail Analysis</field> | ||
1595 | 173 | <field name="res_model">campaign.analysis</field> | ||
1596 | 174 | <field name="view_type">form</field> | ||
1597 | 175 | <field name="view_mode">tree</field> | ||
1598 | 176 | <field name="context">{'search_default_year': 1,"search_default_This Month":1,'group_by': [], 'search_default_Campaign': 1, 'search_default_Segment': 1, 'group_by_no_leaf':1}</field> | ||
1599 | 177 | <field name="search_view_id" ref="view_campaign_analysis_search"/> | ||
1600 | 178 | </record> | ||
1601 | 179 | |||
1602 | 180 | <record id="action_campaign_click_analysis" model="ir.actions.act_window"> | ||
1603 | 181 | <field name="name">Click Analysis</field> | ||
1604 | 182 | <field name="res_model">campaign.click_analysis</field> | ||
1605 | 183 | <field name="view_type">form</field> | ||
1606 | 184 | <field name="view_mode">tree</field> | ||
1607 | 185 | <field name="context">{'search_default_year': 1,"search_default_This Month":1,'group_by': [], 'search_default_Campaign': 1, 'search_default_Segment': 1, 'search_default_URL': 1, 'group_by_no_leaf':1}</field> | ||
1608 | 186 | <field name="search_view_id" ref="view_campaign_click_analysis_search"/> | ||
1609 | 187 | </record> | ||
1610 | 64 | 188 | ||
1611 | 65 | <menuitem name="Marketing" id="base.menu_report_marketing" parent="base.menu_reporting" sequence="45"/> | 189 | <menuitem name="Marketing" id="base.menu_report_marketing" parent="base.menu_reporting" sequence="45"/> |
1612 | 66 | <menuitem action="action_campaign_analysis_all" id="menu_action_campaign_analysis_all" parent="base.menu_report_marketing" sequence="2"/> | 190 | <menuitem action="action_campaign_analysis_all" id="menu_action_campaign_analysis_all" parent="base.menu_report_marketing" sequence="2"/> |
1614 | 67 | 191 | <menuitem action="action_campaign_click_analysis" id="menu_action_campaign_click_analysis" parent="base.menu_report_marketing" sequence="3"/> | |
1615 | 68 | </data> | 192 | </data> |
1616 | 69 | </openerp> | 193 | </openerp> |
1617 | 70 | 194 | ||
1618 | === added file 'marketing_campaign/report/campaign_tracking.py' | |||
1619 | --- marketing_campaign/report/campaign_tracking.py 1970-01-01 00:00:00 +0000 | |||
1620 | +++ marketing_campaign/report/campaign_tracking.py 2012-08-27 10:03:27 +0000 | |||
1621 | @@ -0,0 +1,188 @@ | |||
1622 | 1 | from osv import osv, fields | ||
1623 | 2 | import re | ||
1624 | 3 | from datetime import datetime | ||
1625 | 4 | |||
1626 | 5 | |||
1627 | 6 | class campaign_tracklog(osv.osv): | ||
1628 | 7 | _name = 'marketing.campaign.tracklog' | ||
1629 | 8 | _description = 'Campaign Logs' | ||
1630 | 9 | |||
1631 | 10 | # These are copy/pasted from marketing.campaign.workitem | ||
1632 | 11 | def _res_name_get(self, cr, uid, ids, field_name, arg, context=None): | ||
1633 | 12 | res = dict.fromkeys(ids, 'N/A') | ||
1634 | 13 | for tl in self.browse(cr, uid, ids, context=context): | ||
1635 | 14 | if not tl.workitem_id: | ||
1636 | 15 | continue | ||
1637 | 16 | for wi in self.pool.get('marketing.campaign.workitem').browse(cr, uid, [tl.workitem_id.id], context=context): | ||
1638 | 17 | if not wi.res_id: | ||
1639 | 18 | continue | ||
1640 | 19 | |||
1641 | 20 | proxy = self.pool.get(wi.object_id.model) | ||
1642 | 21 | if not proxy.exists(cr, uid, [wi.res_id]): | ||
1643 | 22 | continue | ||
1644 | 23 | ng = proxy.name_get(cr, uid, [wi.res_id], context=context) | ||
1645 | 24 | if ng: | ||
1646 | 25 | res[tl.id] = ng[0][1] | ||
1647 | 26 | return res | ||
1648 | 27 | |||
1649 | 28 | def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None): | ||
1650 | 29 | """Returns id of tracklogs whose resource_name matches with the given name""" | ||
1651 | 30 | if not len(args): | ||
1652 | 31 | return [] | ||
1653 | 32 | |||
1654 | 33 | condition_name = None | ||
1655 | 34 | for domain_item in args: | ||
1656 | 35 | # we only use the first domain criterion and ignore all the rest including operators | ||
1657 | 36 | if isinstance(domain_item, (list,tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_name': | ||
1658 | 37 | condition_name = [None, domain_item[1], domain_item[2]] | ||
1659 | 38 | break | ||
1660 | 39 | |||
1661 | 40 | assert condition_name, "Invalid search domain for marketing_campaign_workitem.res_name. It should use 'res_name'" | ||
1662 | 41 | |||
1663 | 42 | cr.execute("""select w.id, w.res_id, m.model \ | ||
1664 | 43 | from marketing_campaign_workitem w \ | ||
1665 | 44 | left join marketing_campaign_activity a on (a.id=w.activity_id)\ | ||
1666 | 45 | left join marketing_campaign c on (c.id=a.campaign_id)\ | ||
1667 | 46 | left join ir_model m on (m.id=c.object_id) | ||
1668 | 47 | """) | ||
1669 | 48 | res = cr.fetchall() | ||
1670 | 49 | workitem_map = {} | ||
1671 | 50 | matching_workitems = [] | ||
1672 | 51 | for id, res_id, model in res: | ||
1673 | 52 | workitem_map.setdefault(model,{}).setdefault(res_id,set()).add(id) | ||
1674 | 53 | for model, id_map in workitem_map.iteritems(): | ||
1675 | 54 | model_pool = self.pool.get(model) | ||
1676 | 55 | condition_name[0] = model_pool._rec_name | ||
1677 | 56 | condition = [('id', 'in', id_map.keys()), condition_name] | ||
1678 | 57 | for res_id in model_pool.search(cr, uid, condition, context=context): | ||
1679 | 58 | matching_workitems.extend(id_map[res_id]) | ||
1680 | 59 | return [('workitem_id', 'in', list(set(matching_workitems)))] | ||
1681 | 60 | # end copy/paste | ||
1682 | 61 | |||
1683 | 62 | _columns = { | ||
1684 | 63 | 'workitem_id': fields.many2one('marketing.campaign.workitem', 'Work Item', ondelete="cascade"), | ||
1685 | 64 | 'activity_id': fields.many2one('marketing.campaign.activity', 'Campaign Activity', ondelete="cascade"), | ||
1686 | 65 | 'campaign_id': fields.related('activity_id', 'campaign_id', type='many2one', relation='marketing.campaign', string='Campaign', store=True), | ||
1687 | 66 | 'res_track_id': fields.many2one('marketing.campaign.trackitem', 'Tracking Tag'), | ||
1688 | 67 | 'model_id': fields.related('res_track_id', 'model_id', type='many2one', relation='ir.model', string='Resource Type', store=True), | ||
1689 | 68 | 'res_id': fields.related('res_track_id', 'res_id', type='integer', string='Resource ID', store=True), | ||
1690 | 69 | 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=None), | ||
1691 | 70 | 'mail_message_id': fields.many2one('mail.message', 'Mail Message'), | ||
1692 | 71 | 'log_date': fields.datetime('Logged Date'), | ||
1693 | 72 | 'log_ip': fields.char('Logged IP', None), | ||
1694 | 73 | 'log_type': fields.char('Access Type', None), | ||
1695 | 74 | 'revenue': fields.float('Revenue', digits=(0,2)), | ||
1696 | 75 | 'log_url': fields.char('Logged URL', None), | ||
1697 | 76 | } | ||
1698 | 77 | |||
1699 | 78 | _defaults = { | ||
1700 | 79 | 'log_ip': '0.0.0.0', | ||
1701 | 80 | 'log_date': lambda *a: datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | ||
1702 | 81 | 'revenue': 0, | ||
1703 | 82 | 'log_url': 'N/A', | ||
1704 | 83 | } | ||
1705 | 84 | |||
1706 | 85 | def onchange_activity(self, cr, uid, ids, activity_id, context=None): | ||
1707 | 86 | value = {} | ||
1708 | 87 | if activity_id: | ||
1709 | 88 | activity = self.pool.get('marketing.campaign.activity').browse(cr, uid, activity_id, context) | ||
1710 | 89 | value['campaign_id'] = activity.campaign_id.id | ||
1711 | 90 | value['model_id'] = activity.object_id.id | ||
1712 | 91 | return {'value': value} | ||
1713 | 92 | |||
1714 | 93 | def onchange_trackitem(self, cr, uid, ids, res_track_id, context=None): | ||
1715 | 94 | value = {} | ||
1716 | 95 | if res_track_id: | ||
1717 | 96 | trackitem = self.pool.get('marketing.campaign.trackitem').browse(cr, uid, res_track_id, context) | ||
1718 | 97 | value['model_id'] = trackitem.model_id.id | ||
1719 | 98 | value['res_id'] = trackitem.res_id | ||
1720 | 99 | if trackitem.res_id: | ||
1721 | 100 | target_model = self.pool.get(trackitem.model_id['model']) | ||
1722 | 101 | discard, value['res_name'] = target_model.name_get(cr, uid, [trackitem.res_id], context=context)[0] | ||
1723 | 102 | return {'value': value} | ||
1724 | 103 | |||
1725 | 104 | def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): | ||
1726 | 105 | my_id = campaign_id = activity_id = trackitem_id = None | ||
1727 | 106 | to_user = msg_dict['to'].split('@')[0] | ||
1728 | 107 | if len(to_user.split('+')) > 1: | ||
1729 | 108 | # Track string encoded in return-path's address tag | ||
1730 | 109 | track_str = re.sub('=', '@', to_user.split('+')[1]) | ||
1731 | 110 | if len(track_str.split('-')) == 3: | ||
1732 | 111 | campaign_str, activity_str, trackitem_str = track_str.split('-') | ||
1733 | 112 | # Campaign | ||
1734 | 113 | campaign_ids = self.pool.get('marketing.campaign').search(cr, uid, [('track_str', '=', campaign_str)]) | ||
1735 | 114 | if len(campaign_ids): | ||
1736 | 115 | campaign_id = campaign_ids[0] | ||
1737 | 116 | # Activity | ||
1738 | 117 | activity_ids = self.pool.get('marketing.campaign.activity').search(cr, uid, [('track_str', '=', activity_str)]) | ||
1739 | 118 | if len(activity_ids): | ||
1740 | 119 | activity_id = activity_ids[0] | ||
1741 | 120 | # Trackitem | ||
1742 | 121 | trackitem_ids = self.pool.get('marketing.campaign.trackitem').search(cr, uid, [('track_str', '=', trackitem_str)]) | ||
1743 | 122 | if len(trackitem_ids): | ||
1744 | 123 | trackitem_id = trackitem_ids[0] | ||
1745 | 124 | if campaign_id is not None and activity_id is not None and trackitem_id is not None: | ||
1746 | 125 | wi_search_vals = [('campaign_id', '=', campaign_id), | ||
1747 | 126 | ('activity_id', '=', activity_id), | ||
1748 | 127 | ('trackitem_id', '=', trackitem_id)] | ||
1749 | 128 | workitem_ids = self.pool.get('marketing.campaign.workitem').search(cr, uid, wi_search_vals) | ||
1750 | 129 | if len(workitem_ids): | ||
1751 | 130 | workitem_id = workitem_ids[0] | ||
1752 | 131 | my_create_vals = { | ||
1753 | 132 | 'res_track_id': trackitem_id, | ||
1754 | 133 | 'log_type': 'mail_bounced', | ||
1755 | 134 | 'activity_id': activity_id, | ||
1756 | 135 | 'workitem_id': workitem_id, | ||
1757 | 136 | } | ||
1758 | 137 | my_id = self.create(cr, uid, my_create_vals) | ||
1759 | 138 | return my_id | ||
1760 | 139 | |||
1761 | 140 | def create(self, cr, uid, vals, context=None): | ||
1762 | 141 | # Check which workitem we belong to | ||
1763 | 142 | res_id = self.pool.get('marketing.campaign.trackitem').browse(cr, uid, vals['res_track_id']).res_id | ||
1764 | 143 | args = [ | ||
1765 | 144 | ('activity_id', '=', vals['activity_id']), | ||
1766 | 145 | ('res_id', '=', res_id), | ||
1767 | 146 | ] | ||
1768 | 147 | wi_obj = self.pool.get('marketing.campaign.workitem') | ||
1769 | 148 | wi_id = wi_obj.search(cr, uid, args, context=context)[0] | ||
1770 | 149 | vals.update({'workitem_id': wi_id}) | ||
1771 | 150 | # Check which mail message we belong to | ||
1772 | 151 | mail_msg_obj = self.pool.get('mail.message') | ||
1773 | 152 | mail_msg_ids = mail_msg_obj.search(cr, uid, [('workitem_id', '=', wi_id)]) | ||
1774 | 153 | if len(mail_msg_ids): | ||
1775 | 154 | vals.update({'mail_message_id': mail_msg_ids[0]}) | ||
1776 | 155 | retval = super(campaign_tracklog, self).create(cr, uid, vals, context=context) | ||
1777 | 156 | |||
1778 | 157 | # Update related workitem if necessary | ||
1779 | 158 | wvals = {} | ||
1780 | 159 | if vals['log_type'] in ['mail_opened', 'mail_forwarded']: | ||
1781 | 160 | wvals.update({'mail_opened': True}) | ||
1782 | 161 | # If opened from multiple IPs, consider it a forward. | ||
1783 | 162 | # We're probably never gonna see "mail_forwarded" in the wild | ||
1784 | 163 | fwd_search = [ | ||
1785 | 164 | ('workitem_id', '=', wi_id), | ||
1786 | 165 | ('log_type', '=', 'mail_opened'), | ||
1787 | 166 | ('log_ip', '!=', vals['log_ip']), | ||
1788 | 167 | ] | ||
1789 | 168 | if len(self.pool.get(self._name).search(cr, uid, fwd_search)) \ | ||
1790 | 169 | or vals['log_type'] == 'mail_forwarded': | ||
1791 | 170 | wvals.update({'mail_forwarded': True}) | ||
1792 | 171 | # We're probably never gonna see "mail_bounced" in the wild. | ||
1793 | 172 | # TODO: implement logic for bounces, unsubscribes | ||
1794 | 173 | if vals['log_type'] == 'mail_bounced': | ||
1795 | 174 | wvals.update({'mail_bounced': True}) | ||
1796 | 175 | if vals['log_type'] == 'unsubscribe': | ||
1797 | 176 | wvals.update({'unsubscribed': True}) | ||
1798 | 177 | # Check if target has optin or optout stuffi | ||
1799 | 178 | wi = wi_obj.browse(cr, uid, wi_id, context) | ||
1800 | 179 | target_model = self.pool.get(wi.object_id['model']) | ||
1801 | 180 | target = target_model.browse(cr, uid, res_id, context) | ||
1802 | 181 | # TODO: check it's opt_out everywhere | ||
1803 | 182 | if 'opt_out' in target: | ||
1804 | 183 | target_wvals = { | ||
1805 | 184 | 'opt_out': True, | ||
1806 | 185 | } | ||
1807 | 186 | target_model.write(cr, uid, res_id, target_wvals) | ||
1808 | 187 | wi_obj.write(cr, uid, [wi_id], wvals, context) | ||
1809 | 188 | return retval | ||
1810 | 0 | 189 | ||
1811 | === added file 'marketing_campaign/report/campaign_tracking_view.xml' | |||
1812 | --- marketing_campaign/report/campaign_tracking_view.xml 1970-01-01 00:00:00 +0000 | |||
1813 | +++ marketing_campaign/report/campaign_tracking_view.xml 2012-08-27 10:03:27 +0000 | |||
1814 | @@ -0,0 +1,53 @@ | |||
1815 | 1 | <?xml version="1.0" encoding="utf-8"?> | ||
1816 | 2 | <openerp> | ||
1817 | 3 | <data> | ||
1818 | 4 | <record model="ir.actions.act_window" id="action_marketing_campaign_log"> | ||
1819 | 5 | <field name="name">Campaign Logs</field> | ||
1820 | 6 | <field name="res_model">marketing.campaign.tracklog</field> | ||
1821 | 7 | <field name="view_type">form</field> | ||
1822 | 8 | <field name="view_mode">tree,form</field> | ||
1823 | 9 | <field name="help">Marketing Campaign Response Logs</field> | ||
1824 | 10 | </record> | ||
1825 | 11 | |||
1826 | 12 | <record id="view_marketing_campaign_log_form" model="ir.ui.view"> | ||
1827 | 13 | <field name="name">marketing.campaign.log.form</field> | ||
1828 | 14 | <field name="model">marketing.campaign.tracklog</field> | ||
1829 | 15 | <field name="type">form</field> | ||
1830 | 16 | <field name="arch" type="xml"> | ||
1831 | 17 | <form string="Logs"> | ||
1832 | 18 | <field name="campaign_id" readonly="True"/> | ||
1833 | 19 | <field name="model_id" readonly="True"/> | ||
1834 | 20 | <field name="activity_id" on_change="onchange_activity(activity_id)"/> | ||
1835 | 21 | <field name="res_name" readonly="True"/> | ||
1836 | 22 | <field name="res_track_id" on_change="onchange_trackitem(res_track_id)"/> | ||
1837 | 23 | <field name="res_id" readonly="True"/> | ||
1838 | 24 | <field name="log_date"/> | ||
1839 | 25 | <field name="log_type"/> | ||
1840 | 26 | <field name="log_ip"/> | ||
1841 | 27 | <field name="revenue"/> | ||
1842 | 28 | <field name="log_url"/> | ||
1843 | 29 | </form> | ||
1844 | 30 | </field> | ||
1845 | 31 | </record> | ||
1846 | 32 | |||
1847 | 33 | <record id="view_marketing_campaign_log_list" model="ir.ui.view"> | ||
1848 | 34 | <field name="name">marketing.campaign.log.list</field> | ||
1849 | 35 | <field name="model">marketing.campaign.tracklog</field> | ||
1850 | 36 | <field name="type">tree</field> | ||
1851 | 37 | <field name="arch" type="xml"> | ||
1852 | 38 | <form string="Logs"> | ||
1853 | 39 | <field name="log_date"/> | ||
1854 | 40 | <field name="campaign_id"/> | ||
1855 | 41 | <field name="activity_id"/> | ||
1856 | 42 | <field name="model_id"/> | ||
1857 | 43 | <field name="res_name"/> | ||
1858 | 44 | <field name="log_type"/> | ||
1859 | 45 | <field name="log_ip"/> | ||
1860 | 46 | <field name="revenue"/> | ||
1861 | 47 | </form> | ||
1862 | 48 | </field> | ||
1863 | 49 | </record> | ||
1864 | 50 | |||
1865 | 51 | <menuitem parent="base.menu_report_marketing" id="menu_marketing_campaign_log" action="action_marketing_campaign_log" /> | ||
1866 | 52 | </data> | ||
1867 | 53 | </openerp> |