1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 OpenERP SA (<http://openerp.com>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
25 from datetime import datetime
26 from dateutil.relativedelta import relativedelta
27 from operator import itemgetter
28 from traceback import format_exception
29 from sys import exc_info
30 from tools.safe_eval import safe_eval as eval
32 from decimal_precision import decimal_precision as dp
34 from osv import fields, osv
36 from tools.translate import _
39 'hours': lambda interval: relativedelta(hours=interval),
40 'days': lambda interval: relativedelta(days=interval),
41 'months': lambda interval: relativedelta(months=interval),
42 'years': lambda interval: relativedelta(years=interval),
45 DT_FMT = '%Y-%m-%d %H:%M:%S'
48 return dict((k, f(v)) for k,v in d.items())
50 def _find_fieldname(model, field):
51 inherit_columns = dict_map(itemgetter(2), model._inherit_fields)
52 all_columns = dict(inherit_columns, **model._columns)
53 for fn in all_columns:
54 if all_columns[fn] is field:
56 raise ValueError('field not found: %r' % (field,))
58 class selection_converter(object):
59 """Format the selection in the browse record objects"""
60 def __init__(self, value):
64 def set_value(self, cr, uid, _self_again, record, field, lang):
65 # this design is terrible
66 # search fieldname from the field
67 fieldname = _find_fieldname(record._table, field)
68 context = dict(lang=lang.code)
69 fg = record._table.fields_get(cr, uid, [fieldname], context=context)
70 selection = dict(fg[fieldname]['selection'])
71 self._str = selection[self.value]
80 translate_selections = {
81 'selection': selection_converter,
85 class marketing_campaign(osv.osv):
86 _name = "marketing.campaign"
87 _description = "Marketing Campaign"
90 'name': fields.char('Name', size=64, required=True),
91 'object_id': fields.many2one('ir.model', 'Resource', required=True,
92 help="Choose the resource on which you want \
93 this campaign to be run"),
94 'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
95 domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
96 help="The generated workitems will be linked to the partner related to the record. "\
97 "If the record is the partner itself leave this field empty. "\
98 "This is useful for reporting purposes, via the Campaign Analysis or Campaign Follow-up views."),
99 'unique_field_id': fields.many2one('ir.model.fields', 'Unique Field',
100 domain="[('model_id', '=', object_id), ('ttype', 'in', ['char','int','many2one','text','selection'])]",
101 help='If set, this field will help segments that work in "no duplicates" mode to avoid '\
102 'selecting similar records twice. Similar records are records that have the same value for '\
103 'this unique field. For example by choosing the "email_from" field for CRM Leads you would prevent '\
104 'sending the same campaign to the same email address again. If not set, the "no duplicates" segments '\
105 "will only avoid selecting the same record again if it entered the campaign previously. "\
106 "Only easily comparable fields like textfields, integers, selections or single relationships may be used."),
107 'mode': fields.selection([('test', 'Test Directly'),
108 ('test_realtime', 'Test in Realtime'),
109 ('manual', 'With Manual Confirmation'),
110 ('active', 'Normal')],
111 'Mode', required=True, help= \
112 """Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
113 Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
114 With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
115 Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
116 'state': fields.selection([('draft', 'New'),
117 ('running', 'Running'),
119 ('cancelled', 'Cancelled'),],
121 'activity_ids': fields.one2many('marketing.campaign.activity',
122 'campaign_id', 'Activities'),
123 'fixed_cost': fields.float('Fixed Cost', help="Fixed cost for running this campaign. You may also specify variable cost and revenue on each campaign activity. Cost and Revenue statistics are included in Campaign Reporting.", digits_compute=dp.get_precision('Purchase Price')),
127 'state': lambda *a: 'draft',
128 'mode': lambda *a: 'test',
131 def state_running_set(self, cr, uid, ids, *args):
132 # TODO check that all subcampaigns are running
133 campaign = self.browse(cr, uid, ids[0])
135 if not campaign.activity_ids:
136 raise osv.except_osv(_("Error"), _("The campaign cannot be started: there are no activities in it"))
139 has_signal_without_from = False
141 for activity in campaign.activity_ids:
144 if activity.signal and len(activity.from_ids) == 0:
145 has_signal_without_from = True
147 if not has_start and not has_signal_without_from:
148 raise osv.except_osv(_("Error"), _("The campaign cannot be started: it doesn't have any starting activity (or any activity with a signal and no previous activity)"))
150 return self.write(cr, uid, ids, {'state': 'running'})
152 def state_done_set(self, cr, uid, ids, *args):
153 # TODO check that this campaign is not a subcampaign in running mode.
154 segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
155 [('campaign_id', 'in', ids),
156 ('state', '=', 'running')])
158 raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are done"))
159 self.write(cr, uid, ids, {'state': 'done'})
162 def state_cancel_set(self, cr, uid, ids, *args):
163 # TODO check that this campaign is not a subcampaign in running mode.
164 self.write(cr, uid, ids, {'state': 'cancelled'})
168 def signal(self, cr, uid, model, res_id, signal, run_existing=True, context=None):
169 record = self.pool.get(model).browse(cr, uid, res_id, context)
170 return self._signal(cr, uid, record, signal, run_existing, context)
172 def _signal(self, cr, uid, record, signal, run_existing=True, context=None):
174 raise ValueError('signal cannot be False')
176 Workitems = self.pool.get('marketing.campaign.workitem')
177 domain = [('object_id.model', '=', record._table._name),
178 ('state', '=', 'running')]
179 campaign_ids = self.search(cr, uid, domain, context=context)
180 for campaign in self.browse(cr, uid, campaign_ids, context=context):
181 for activity in campaign.activity_ids:
182 if activity.signal != signal:
185 data = dict(activity_id=activity.id,
188 wi_domain = [(k, '=', v) for k, v in data.items()]
190 wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
195 partner = self._get_partner_for(campaign, record)
197 data['partner_id'] = partner.id
198 wi_id = Workitems.create(cr, uid, data, context=context)
200 Workitems.process(cr, uid, wi_ids, context=context)
203 def _get_partner_for(self, campaign, record):
204 partner_field = campaign.partner_field_id.name
206 return getattr(record, partner_field)
207 elif campaign.object_id.model == 'res.partner':
211 # prevent duplication until the server properly duplicates several levels of nested o2m
212 def copy(self, cr, uid, id, default=None, context=None):
213 raise osv.except_osv(_("Operation not supported"), _("Sorry, campaign duplication is not supported at the moment."))
215 def _find_duplicate_workitems(self, cr, uid, record, campaign_rec, context=None):
216 """Finds possible duplicates workitems for a record in this campaign, based on a uniqueness
219 :param record: browse_record to find duplicates workitems for.
220 :param campaign_rec: browse_record of campaign
222 Workitems = self.pool.get('marketing.campaign.workitem')
223 duplicate_workitem_domain = [('res_id','=', record.id),
224 ('campaign_id','=', campaign_rec.id)]
225 unique_field = campaign_rec.unique_field_id
227 unique_value = getattr(record, unique_field.name, None)
229 if unique_field.ttype == 'many2one':
230 unique_value = unique_value.id
231 similar_res_ids = self.pool.get(campaign_rec.object_id.model).search(cr, uid,
232 [(unique_field.name, '=', unique_value)], context=context)
234 duplicate_workitem_domain = [('res_id','in', similar_res_ids),
235 ('campaign_id','=', campaign_rec.id)]
236 return Workitems.search(cr, uid, duplicate_workitem_domain, context=context)
241 class marketing_campaign_segment(osv.osv):
242 _name = "marketing.campaign.segment"
243 _description = "Campaign Segment"
246 def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
247 # next auto sync date is same for all segments
248 sync_job = self.pool.get('ir.model.data').get_object(cr, uid, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
249 next_sync = sync_job and sync_job.nextcall or False
250 return dict.fromkeys(ids, next_sync)
253 'name': fields.char('Name', size=64,required=True),
254 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
255 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
256 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
257 help="Filter to select the matching resource records that belong to this segment. "\
258 "New filters can be created and saved using the advanced search on the list view of the Resource. "\
259 "If no filter is set, all records are selected without filtering. "\
260 "The synchronization mode may also add a criterion to the filter."),
261 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
262 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
263 ('write_date', 'Only records modified after last sync (no duplicates)'),
264 ('all', 'All records (no duplicates)')],
265 'Synchronization mode',
266 help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign. "\
267 '"No duplicates" prevents selecting records which have already entered the campaign previously.'\
268 'If the campaign has a "unique field" set, "no duplicates" will also prevent selecting records which have '\
269 'the same value for the unique field as other records that already entered the campaign.'),
270 'state': fields.selection([('draft', 'New'),
271 ('running', 'Running'),
273 ('cancelled', 'Cancelled')],
275 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
276 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
277 'date_next_sync': fields.function(_get_next_sync, string='Next Synchronization', type='datetime', help="Next time the synchronization job is scheduled to run automatically"),
281 'state': lambda *a: 'draft',
282 'sync_mode': lambda *a: 'create_date',
285 def _check_model(self, cr, uid, ids, context=None):
286 for obj in self.browse(cr, uid, ids, context=context):
287 if not obj.ir_filter_id:
289 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
294 (_check_model, 'Model of filter must be same as resource model of Campaign ', ['ir_filter_id,campaign_id']),
297 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
298 res = {'domain':{'ir_filter_id':[]}}
299 campaign_pool = self.pool.get('marketing.campaign')
301 campaign = campaign_pool.browse(cr, uid, campaign_id)
302 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
304 mod_name = model_name[0]['model']
305 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
307 res['value'] = {'ir_filter_id': False}
310 def state_running_set(self, cr, uid, ids, *args):
311 segment = self.browse(cr, uid, ids[0])
312 vals = {'state': 'running'}
313 if not segment.date_run:
314 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
315 self.write(cr, uid, ids, vals)
318 def state_done_set(self, cr, uid, ids, *args):
319 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
320 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
321 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
322 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
325 def state_cancel_set(self, cr, uid, ids, *args):
326 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
327 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
328 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
329 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
332 def synchroniz(self, cr, uid, ids, *args):
333 self.process_segment(cr, uid, ids)
336 def process_segment(self, cr, uid, segment_ids=None, context=None):
337 Workitems = self.pool.get('marketing.campaign.workitem')
338 Campaigns = self.pool.get('marketing.campaign')
340 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
342 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
344 for segment in self.browse(cr, uid, segment_ids, context=context):
345 if segment.campaign_id.state != 'running':
348 campaigns.add(segment.campaign_id.id)
349 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
350 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
352 model_obj = self.pool.get(segment.object_id.model)
354 if segment.sync_last_date and segment.sync_mode != 'all':
355 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
356 if segment.ir_filter_id:
357 criteria += eval(segment.ir_filter_id.domain)
358 object_ids = model_obj.search(cr, uid, criteria, context=context)
360 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
361 for record in model_obj.browse(cr, uid, object_ids, context=context):
362 # avoid duplicate workitem for the same resource
363 if segment.sync_mode in ('write_date','all'):
364 if Campaigns._find_duplicate_workitems(cr, uid, record, segment.campaign_id, context=context):
368 'segment_id': segment.id,
374 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, record)
376 wi_vals['partner_id'] = partner.id
378 for act_id in act_ids:
379 wi_vals['activity_id'] = act_id
380 Workitems.create(cr, uid, wi_vals, context=context)
382 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
383 Workitems.process_all(cr, uid, list(campaigns), context=context)
386 marketing_campaign_segment()
388 class marketing_campaign_activity(osv.osv):
389 _name = "marketing.campaign.activity"
391 _description = "Campaign Activity"
395 ('report', 'Report'),
396 ('action', 'Custom Action'),
397 # TODO implement the subcampaigns.
398 # TODO implement the subcampaign out. disallow out transitions from
399 # subcampaign activities ?
400 #('subcampaign', 'Sub-Campaign'),
404 'name': fields.char('Name', size=128, required=True),
405 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
406 required = True, ondelete='cascade', select=1),
407 'object_id': fields.related('campaign_id','object_id',
408 type='many2one', relation='ir.model',
409 string='Object', readonly=True),
410 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
411 'condition': fields.text('Condition', size=256, required=True,
412 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
413 "The expression may use the following [browsable] variables:\n"
414 " - activity: the campaign activity\n"
415 " - workitem: the campaign workitem\n"
416 " - resource: the resource object this campaign item represents\n"
417 " - transitions: list of campaign transitions outgoing from this activity\n"
418 "...- re: Python regular expression module"),
419 'type': fields.selection(_action_types, 'Type', required=True,
420 help="""The type of action to execute when an item enters this activity, such as:
421 - Email: send an email using a predefined email template
422 - Report: print an existing Report defined on the resource item and save it into a specific directory
423 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
425 'email_template_id': fields.many2one('email.template', "Email Template", help='The e-mail to send when this activity is activated'),
426 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
427 'report_directory_id': fields.many2one('document.directory','Directory',
428 help="This folder is used to store the generated reports"),
429 'server_action_id': fields.many2one('ir.actions.server', string='Action',
430 help= "The action to perform when this activity is activated"),
431 'to_ids': fields.one2many('marketing.campaign.transition',
434 'from_ids': fields.one2many('marketing.campaign.transition',
436 'Previous Activities'),
437 'variable_cost': fields.float('Variable Cost', help="Set a variable cost if you consider that every campaign item that has reached this point has entailed a certain cost. You can get cost statistics in the Reporting section", digits_compute=dp.get_precision('Purchase Price')),
438 'revenue': fields.float('Revenue', help="Set an expected revenue if you consider that every campaign item that has reached this point has generated a certain revenue. You can get revenue statistics in the Reporting section", digits_compute=dp.get_precision('Sale Price')),
439 'signal': fields.char('Signal', size=128,
440 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
441 'keep_if_condition_not_met': fields.boolean("Don't delete workitems",
442 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
446 'type': lambda *a: 'email',
447 'condition': lambda *a: 'True',
450 def search(self, cr, uid, args, offset=0, limit=None, order=None,
451 context=None, count=False):
454 if 'segment_id' in context and context['segment_id']:
455 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
456 uid, context['segment_id'])
458 for activity in segment_obj.campaign_id.activity_ids:
459 act_ids.append(activity.id)
461 return super(marketing_campaign_activity, self).search(cr, uid, args,
462 offset, limit, order, context, count)
464 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
465 service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
466 (report_data, format) = service.create(cr, uid, [], {}, {})
468 'name': '%s_%s_%s'%(activity.report_id.report_name,
469 activity.name,workitem.partner_id.name),
470 'datas_fname': '%s.%s'%(activity.report_id.report_name,
471 activity.report_id.report_type),
472 'parent_id': activity.report_directory_id.id,
473 'datas': base64.encodestring(report_data),
476 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
479 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
480 return self.pool.get('email.template').send_mail(cr, uid,
481 activity.email_template_id.id,
482 workitem.res_id, context=context)
484 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
487 server_obj = self.pool.get('ir.actions.server')
489 action_context = dict(context,
490 active_id=workitem.res_id,
491 active_ids=[workitem.res_id],
492 active_model=workitem.object_id.model,
494 res = server_obj.run(cr, uid, [activity.server_action_id.id],
495 context=action_context)
496 # server action return False if the action is performed
497 # except client_action, other and python code
498 return res == False and True or res
500 def process(self, cr, uid, act_id, wi_id, context=None):
501 activity = self.browse(cr, uid, act_id, context=context)
502 method = '_process_wi_%s' % (activity.type,)
503 action = getattr(self, method, None)
505 raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
507 workitem_obj = self.pool.get('marketing.campaign.workitem')
508 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
509 return action(cr, uid, activity, workitem, context=context)
511 marketing_campaign_activity()
513 class marketing_campaign_transition(osv.osv):
514 _name = "marketing.campaign.transition"
515 _description = "Campaign Transition"
518 ('hours', 'Hour(s)'), ('days', 'Day(s)'),
519 ('months', 'Month(s)'), ('years','Year(s)')
522 def _get_name(self, cr, uid, ids, fn, args, context=None):
523 result = dict.fromkeys(ids, False)
525 'auto': _('Automatic transition'),
526 'time': _('After %(interval_nbr)d %(interval_type)s'),
527 'cosmetic': _('Cosmetic'),
529 for tr in self.browse(cr, uid, ids, context=context,
530 fields_process=translate_selections):
531 result[tr.id] = formatters[tr.trigger.value] % tr
535 def _delta(self, cr, uid, ids, context=None):
537 transition = self.browse(cr, uid, ids[0], context=context)
538 if transition.trigger != 'time':
539 raise ValueError('Delta is only relevant for timed transiton')
540 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
544 'name': fields.function(_get_name, string='Name',
545 type='char', size=128),
546 'activity_from_id': fields.many2one('marketing.campaign.activity',
547 'Previous Activity', select=1,
548 required=True, ondelete="cascade"),
549 'activity_to_id': fields.many2one('marketing.campaign.activity',
551 required=True, ondelete="cascade"),
552 'interval_nbr': fields.integer('Interval Value', required=True),
553 'interval_type': fields.selection(_interval_units, 'Interval Unit',
556 'trigger': fields.selection([('auto', 'Automatic'),
558 ('cosmetic', 'Cosmetic'), # fake plastic transition
560 'Trigger', required=True,
561 help="How is the destination workitem triggered"),
566 'interval_type': 'days',
569 def _check_campaign(self, cr, uid, ids, context=None):
570 for obj in self.browse(cr, uid, ids, context=context):
571 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
576 (_check_campaign, 'The To/From Activity of transition must be of the same Campaign ', ['activity_from_id,activity_to_id']),
580 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
583 marketing_campaign_transition()
585 class marketing_campaign_workitem(osv.osv):
586 _name = "marketing.campaign.workitem"
587 _description = "Campaign Workitem"
589 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
590 res = dict.fromkeys(ids, '/')
591 for wi in self.browse(cr, uid, ids, context=context):
595 proxy = self.pool.get(wi.object_id.model)
596 if not proxy.exists(cr, uid, [wi.res_id]):
598 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
600 res[wi.id] = ng[0][1]
603 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
604 """Returns id of workitem whose resource_name matches with the given name"""
608 condition_name = None
609 for domain_item in args:
610 # we only use the first domain criterion and ignore all the rest including operators
611 if isinstance(domain_item, (list,tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_name':
612 condition_name = [None, domain_item[1], domain_item[2]]
615 assert condition_name, "Invalid search domain for marketing_campaign_workitem.res_name. It should use 'res_name'"
617 cr.execute("""select w.id, w.res_id, m.model \
618 from marketing_campaign_workitem w \
619 left join marketing_campaign_activity a on (a.id=w.activity_id)\
620 left join marketing_campaign c on (c.id=a.campaign_id)\
621 left join ir_model m on (m.id=c.object_id)
625 matching_workitems = []
626 for id, res_id, model in res:
627 workitem_map.setdefault(model,{}).setdefault(res_id,set()).add(id)
628 for model, id_map in workitem_map.iteritems():
629 model_pool = self.pool.get(model)
630 condition_name[0] = model_pool._rec_name
631 condition = [('id', 'in', id_map.keys()), condition_name]
632 for res_id in model_pool.search(cr, uid, condition, context=context):
633 matching_workitems.extend(id_map[res_id])
634 return [('id', 'in', list(set(matching_workitems)))]
637 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
638 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
639 required=True, readonly=True),
640 'campaign_id': fields.related('activity_id', 'campaign_id',
641 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
642 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
643 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
644 'res_id': fields.integer('Resource ID', select=1, readonly=True),
645 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
646 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
647 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
648 'state': fields.selection([('todo', 'To Do'),
649 ('exception', 'Exception'), ('done', 'Done'),
650 ('cancelled', 'Cancelled')], 'State', readonly=True),
652 'error_msg' : fields.text('Error Message', readonly=True)
655 'state': lambda *a: 'todo',
659 def button_draft(self, cr, uid, workitem_ids, context=None):
660 for wi in self.browse(cr, uid, workitem_ids, context=context):
661 if wi.state in ('exception', 'cancelled'):
662 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
665 def button_cancel(self, cr, uid, workitem_ids, context=None):
666 for wi in self.browse(cr, uid, workitem_ids, context=context):
667 if wi.state in ('todo','exception'):
668 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
671 def _process_one(self, cr, uid, workitem, context=None):
672 if workitem.state != 'todo':
675 activity = workitem.activity_id
676 proxy = self.pool.get(workitem.object_id.model)
677 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
680 'activity': activity,
681 'workitem': workitem,
683 'resource': object_id,
684 'transitions': activity.to_ids,
688 condition = activity.condition
689 campaign_mode = workitem.campaign_id.mode
691 if not eval(condition, eval_context):
692 if activity.keep_if_condition_not_met:
693 workitem.write({'state': 'cancelled'}, context=context)
695 workitem.unlink(context=context)
698 if campaign_mode in ('manual', 'active'):
699 Activities = self.pool.get('marketing.campaign.activity')
700 result = Activities.process(cr, uid, activity.id, workitem.id,
703 values = dict(state='done')
704 if not workitem.date:
705 values['date'] = datetime.now().strftime(DT_FMT)
706 workitem.write(values, context=context)
710 workitem = workitem.browse(context=context)[0] # reload
711 date = datetime.strptime(workitem.date, DT_FMT)
713 for transition in activity.to_ids:
714 if transition.trigger == 'cosmetic':
717 if transition.trigger == 'auto':
719 elif transition.trigger == 'time':
720 launch_date = date + transition._delta()
723 launch_date = launch_date.strftime(DT_FMT)
726 'segment_id': workitem.segment_id.id,
727 'activity_id': transition.activity_to_id.id,
728 'partner_id': workitem.partner_id.id,
729 'res_id': workitem.res_id,
732 wi_id = self.create(cr, uid, values, context=context)
734 # Now, depending on the trigger and the campaign mode
735 # we know whether we must run the newly created workitem.
737 # rows = transition trigger \ colums = campaign mode
739 # test test_realtime manual normal (active)
745 run = (transition.trigger == 'auto' \
746 and campaign_mode != 'manual') \
747 or (transition.trigger == 'time' \
748 and campaign_mode == 'test')
750 new_wi = self.browse(cr, uid, wi_id, context)
751 self._process_one(cr, uid, new_wi, context)
754 tb = "".join(format_exception(*exc_info()))
755 workitem.write({'state': 'exception', 'error_msg': tb},
758 def process(self, cr, uid, workitem_ids, context=None):
759 for wi in self.browse(cr, uid, workitem_ids, context=context):
760 self._process_one(cr, uid, wi, context=context)
763 def process_all(self, cr, uid, camp_ids=None, context=None):
764 camp_obj = self.pool.get('marketing.campaign')
766 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
767 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
768 if camp.mode == 'manual':
769 # manual states are not processed automatically
772 domain = [('campaign_id', '=', camp.id), ('state', '=', 'todo'), ('date', '!=', False)]
773 if camp.mode in ('test_realtime', 'active'):
774 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
776 workitem_ids = self.search(cr, uid, domain, context=context)
780 self.process(cr, uid, workitem_ids, context=context)
783 def preview(self, cr, uid, ids, context=None):
785 wi_obj = self.browse(cr, uid, ids[0], context=context)
786 if wi_obj.activity_id.type == 'email':
787 view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'email_template', 'email_template_preview_form')
789 'name': _('Email Preview'),
791 'view_mode': 'form,tree',
792 'res_model': 'email_template.preview',
795 'views': [(view_id and view_id[1] or 0, 'form')],
796 'type': 'ir.actions.act_window',
799 'context': "{'template_id':%d,'default_res_id':%d}"%
800 (wi_obj.activity_id.email_template_id.id,
804 elif wi_obj.activity_id.type == 'report':
806 'ids': [wi_obj.res_id],
807 'model': wi_obj.object_id.model
810 'type' : 'ir.actions.report.xml',
811 'report_name': wi_obj.activity_id.report_id.report_name,
815 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
818 marketing_campaign_workitem()
820 class email_template(osv.osv):
821 _inherit = "email.template"
823 'model_id': lambda obj, cr, uid, context: context.get('object_id',False),
826 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
830 class report_xml(osv.osv):
831 _inherit = 'ir.actions.report.xml'
832 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
835 object_id = context.get('object_id')
837 model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model
838 args.append(('model', '=', model))
839 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)
844 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: