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 ##############################################################################
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from operator import itemgetter
27 from traceback import format_exception
28 from sys import exc_info
29 from tools.safe_eval import safe_eval as eval
32 from osv import fields, osv
34 from tools.translate import _
37 'hours': lambda interval: relativedelta(hours=interval),
38 'days': lambda interval: relativedelta(days=interval),
39 'months': lambda interval: relativedelta(months=interval),
40 'years': lambda interval: relativedelta(years=interval),
43 DT_FMT = '%Y-%m-%d %H:%M:%S'
46 return dict((k, f(v)) for k,v in d.items())
48 def _find_fieldname(model, field):
49 inherit_columns = dict_map(itemgetter(2), model._inherit_fields)
50 all_columns = dict(inherit_columns, **model._columns)
51 for fn in all_columns:
52 if all_columns[fn] is field:
54 raise ValueError('field not found: %r' % (field,))
56 class selection_converter(object):
57 """Format the selection in the browse record objects"""
58 def __init__(self, value):
62 def set_value(self, cr, uid, _self_again, record, field, lang):
63 # this design is terrible
64 # search fieldname from the field
65 fieldname = _find_fieldname(record._table, field)
66 context = dict(lang=lang.code)
67 fg = record._table.fields_get(cr, uid, [fieldname], context=context)
68 selection = dict(fg[fieldname]['selection'])
69 self._str = selection[self.value]
78 translate_selections = {
79 'selection': selection_converter,
83 class marketing_campaign(osv.osv):
84 _name = "marketing.campaign"
85 _description = "Marketing Campaign"
88 'name': fields.char('Name', size=64, required=True),
89 'object_id': fields.many2one('ir.model', 'Resource', required=True,
90 help="Choose the model on which you want \
91 this campaign to be run"),
92 'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
93 domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
94 help="The generated workitems will be linked to the partner related to the record. If the record is the partner itself leave this field empty."),
95 'mode': fields.selection([('test', 'Test Directly'),
96 ('test_realtime', 'Test in Realtime'),
97 ('manual', 'With Manual Confirmation'),
98 ('active', 'Normal')],
99 'Mode', required=True, help= \
100 """Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
101 Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
102 With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
103 Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
104 'state': fields.selection([('draft', 'Draft'),
105 ('running', 'Running'),
107 ('cancelled', 'Cancelled'),],
109 'activity_ids': fields.one2many('marketing.campaign.activity',
110 'campaign_id', 'Activities'),
111 '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."),
115 'state': lambda *a: 'draft',
116 'mode': lambda *a: 'test',
119 def state_running_set(self, cr, uid, ids, *args):
120 # TODO check that all subcampaigns are running
121 campaign = self.browse(cr, uid, ids[0])
123 if not campaign.activity_ids:
124 raise osv.except_osv(_("Error"), _("The campaign cannot be started: there are no activities in it"))
127 has_signal_without_from = False
129 for activity in campaign.activity_ids:
132 if activity.signal and len(activity.from_ids) == 0:
133 has_signal_without_from = True
135 if activity.type != 'email':
137 if not activity.email_template_id.from_account:
138 raise osv.except_osv(_("Error"), _("The campaign cannot be started: an email account is missing in the email activity '%s'")%activity.name)
139 if activity.email_template_id.from_account.state != 'approved':
140 raise osv.except_osv(_("Error"), _("The campaign cannot be started: the email account is not approved in the email activity '%s'")%activity.name)
142 if not has_start and not has_signal_without_from:
143 raise osv.except_osv(_("Error"), _("The campaign hasn't any starting activity nor any activity with a signal and no previous activity."))
145 return self.write(cr, uid, ids, {'state': 'running'})
147 def state_done_set(self, cr, uid, ids, *args):
148 # TODO check that this campaign is not a subcampaign in running mode.
149 segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
150 [('campaign_id', 'in', ids),
151 ('state', '=', 'running')])
153 raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are done"))
154 self.write(cr, uid, ids, {'state': 'done'})
157 def state_cancel_set(self, cr, uid, ids, *args):
158 # TODO check that this campaign is not a subcampaign in running mode.
159 self.write(cr, uid, ids, {'state': 'cancelled'})
163 def signal(self, cr, uid, model, res_id, signal, run_existing=True, context=None):
164 record = self.pool.get(model).browse(cr, uid, res_id, context)
165 return self._signal(cr, uid, record, signal, run_existing, context)
167 def _signal(self, cr, uid, record, signal, run_existing=True, context=None):
169 raise ValueError('signal cannot be False')
171 Workitems = self.pool.get('marketing.campaign.workitem')
172 domain = [('object_id.model', '=', record._table._name),
173 ('state', '=', 'running')]
174 campaign_ids = self.search(cr, uid, domain, context=context)
175 for campaign in self.browse(cr, uid, campaign_ids, context):
176 for activity in campaign.activity_ids:
177 if activity.signal != signal:
180 data = dict(activity_id=activity.id,
183 wi_domain = [(k, '=', v) for k, v in data.items()]
185 wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
190 partner = self._get_partner_for(campaign, record)
192 data['partner_id'] = partner.id
193 wi_id = Workitems.create(cr, uid, data, context=context)
195 Workitems.process(cr, uid, wi_ids, context=context)
198 def _get_partner_for(self, campaign, record):
199 partner_field = campaign.partner_field_id.name
201 return getattr(record, partner_field)
202 elif campaign.object_id.model == 'res.partner':
206 # prevent duplication until the server properly duplicates several levels of nested o2m
207 def copy(self, cr, uid, id, default=None, context=None):
208 raise osv.except_osv("Operation not supported", "Sorry, campaign duplication is not supported at the moment.")
212 class marketing_campaign_segment(osv.osv):
213 _name = "marketing.campaign.segment"
214 _description = "Campaign Segment"
217 'name': fields.char('Name', size=64,required=True),
218 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
219 required=True, select=1, ondelete="cascade"),
220 'object_id': fields.related('campaign_id','object_id',
221 type='many2one', relation='ir.model',
223 'ir_filter_id': fields.many2one('ir.filters', 'Filter', help="Filter to select the matching resource records that belong to this segment. New filters can be created and saved using the advanced search on the list view of the Resource"),
224 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
225 'sync_mode': fields.selection([('create_date', 'If record created after last sync'),
226 ('write_date', 'If record modified after last sync (no duplicates)'),
227 ('all', 'All records (no duplicates)')],
228 'Workitem creation mode',
229 help="Determines how new campaign workitems are created for resource records matching this segment. This is used when segments are synchronized manually, or automatically via the scheduled job."),
230 'state': fields.selection([('draft', 'Draft'),
231 ('running', 'Running'),
233 ('cancelled', 'Cancelled')],
235 'date_run': fields.datetime('Launching Date', help="Initial start date of this segment."),
236 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
240 'state': lambda *a: 'draft',
241 'sync_mode': lambda *a: 'create_date',
244 def _check_model(self, cr, uid, ids, context=None):
247 for obj in self.browse(cr, uid, ids, context=context):
248 if not obj.ir_filter_id:
250 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
255 (_check_model, _('Model of filter must be same as resource model of Campaign '), ['ir_filter_id,campaign_id']),
258 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
259 res = {'domain':{'ir_filter_id':[]}}
260 campaign_pool = self.pool.get('marketing.campaign')
262 campaign = campaign_pool.browse(cr, uid, campaign_id)
263 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
265 mod_name = model_name[0]['model']
266 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
267 res['context'] = {'default_model_id': model_name[0]['model']}
269 res['value'] = {'ir_filter_id': False}
272 def state_running_set(self, cr, uid, ids, *args):
273 segment = self.browse(cr, uid, ids[0])
274 vals = {'state': 'running'}
275 if not segment.date_run:
276 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
277 self.write(cr, uid, ids, vals)
280 def state_done_set(self, cr, uid, ids, *args):
281 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
282 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
283 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
284 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
287 def state_cancel_set(self, cr, uid, ids, *args):
288 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
289 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
290 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
291 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
294 def synchroniz(self, cr, uid, ids, *args):
295 self.process_segment(cr, uid, ids)
298 def process_segment(self, cr, uid, segment_ids=None, context=None):
299 Workitems = self.pool.get('marketing.campaign.workitem')
301 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
303 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
305 for segment in self.browse(cr, uid, segment_ids, context=context):
306 if segment.campaign_id.state != 'running':
309 campaigns.add(segment.campaign_id.id)
310 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
311 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
313 model_obj = self.pool.get(segment.object_id.model)
315 if segment.sync_last_date and segment.sync_mode != 'all':
316 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
317 if segment.ir_filter_id:
318 criteria += eval(segment.ir_filter_id.domain)
319 object_ids = model_obj.search(cr, uid, criteria, context=context)
321 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
322 for o_ids in model_obj.browse(cr, uid, object_ids, context=context):
323 # avoid duplicated workitem for the same resource
324 if segment.sync_mode in ('write_date','all'):
325 wi_ids = Workitems.search(cr, uid, [('res_id','=',o_ids.id),('segment_id','=',segment.id)], context=context)
330 'segment_id': segment.id,
336 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, o_ids)
338 wi_vals['partner_id'] = partner.id
340 for act_id in act_ids:
341 wi_vals['activity_id'] = act_id
342 Workitems.create(cr, uid, wi_vals, context=context)
344 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
345 Workitems.process_all(cr, uid, list(campaigns), context=context)
348 marketing_campaign_segment()
350 class marketing_campaign_activity(osv.osv):
351 _name = "marketing.campaign.activity"
352 _description = "Campaign Activity"
356 ('report', 'Report'),
357 ('action', 'Custom Action'),
358 # TODO implement the subcampaigns.
359 # TODO implement the subcampaign out. disallow out transitions from
360 # subcampaign activities ?
361 #('subcampaign', 'Sub-Campaign'),
365 'name': fields.char('Name', size=128, required=True),
366 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
367 required = True, ondelete='cascade', select=1),
368 'object_id': fields.related('campaign_id','object_id',
369 type='many2one', relation='ir.model',
370 string='Object', readonly=True),
371 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
372 'condition': fields.text('Condition', size=256, required=True,
373 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
374 "The expression may use the following [browsable] variables:\n"
375 " - activity: the campaign activity\n"
376 " - workitem: the campaign workitem\n"
377 " - resource: the resource object this campaign item represents\n"
378 " - transitions: list of campaign transitions outgoing from this activity\n"
379 "...- re: Python regular expression module"),
380 'type': fields.selection(_action_types, 'Type', required=True,
381 help="""The type of action to execute when an item enters this activity, such as:
382 - Email: send an email using a predefined email template
383 - Report: print an existing Report defined on the resource item and save it into a specific directory
384 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
386 'email_template_id': fields.many2one('email.template', "Email Template", help='The e-mail to send when this activity is activated'),
387 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
388 'report_directory_id': fields.many2one('document.directory','Directory',
389 help="This folder is used to store the generated reports"),
390 'server_action_id': fields.many2one('ir.actions.server', string='Action',
391 help= "The action to perform when this activity is activated"),
392 'to_ids': fields.one2many('marketing.campaign.transition',
395 'from_ids': fields.one2many('marketing.campaign.transition',
397 'Previous Activities'),
398 '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"),
399 '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"),
400 'signal': fields.char('Signal', size=128,
401 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
402 'keep_if_condition_not_met': fields.boolean("Don't delete workitems",
403 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
407 'type': lambda *a: 'email',
408 'condition': lambda *a: 'True',
411 def search(self, cr, uid, args, offset=0, limit=None, order=None,
412 context=None, count=False):
415 if 'segment_id' in context and context['segment_id']:
416 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
417 uid, context['segment_id'])
419 for activity in segment_obj.campaign_id.activity_ids:
420 act_ids.append(activity.id)
422 return super(marketing_campaign_activity, self).search(cr, uid, args,
423 offset, limit, order, context, count)
425 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
426 service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
427 (report_data, format) = service.create(cr, uid, [], {}, {})
429 'name': '%s_%s_%s'%(activity.report_id.report_name,
430 activity.name,workitem.partner_id.name),
431 'datas_fname': '%s.%s'%(activity.report_id.report_name,
432 activity.report_id.report_type),
433 'parent_id': activity.report_directory_id.id,
434 'datas': base64.encodestring(report_data),
437 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
440 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
441 return self.pool.get('email.template').generate_mail(cr, uid,
442 activity.email_template_id.id,
443 [workitem.res_id], context=context)
445 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
448 server_obj = self.pool.get('ir.actions.server')
450 action_context = dict(context,
451 active_id=workitem.res_id,
452 active_ids=[workitem.res_id],
453 active_model=workitem.object_id.model)
454 res = server_obj.run(cr, uid, [activity.server_action_id.id],
455 context=action_context)
456 # server action return False if the action is perfomed
457 # except client_action, other and python code
458 return res == False and True or res
460 def process(self, cr, uid, act_id, wi_id, context=None):
461 activity = self.browse(cr, uid, act_id, context=context)
462 method = '_process_wi_%s' % (activity.type,)
463 action = getattr(self, method, None)
465 raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
467 workitem_obj = self.pool.get('marketing.campaign.workitem')
468 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
469 return action(cr, uid, activity, workitem, context)
471 marketing_campaign_activity()
473 class marketing_campaign_transition(osv.osv):
474 _name = "marketing.campaign.transition"
475 _description = "Campaign Transition"
478 ('hours', 'Hour(s)'), ('days', 'Day(s)'),
479 ('months', 'Month(s)'), ('years','Year(s)')
482 def _get_name(self, cr, uid, ids, fn, args, context=None):
483 result = dict.fromkeys(ids, False)
485 'auto': _('Automatic transition'),
486 'time': _('After %(interval_nbr)d %(interval_type)s'),
487 'cosmetic': _('Cosmetic'),
489 for tr in self.browse(cr, uid, ids, context=context,
490 fields_process=translate_selections):
491 result[tr.id] = formatters[tr.trigger.value] % tr
495 def _delta(self, cr, uid, ids, context=None):
497 transition = self.browse(cr, uid, ids[0], context)
498 if transition.trigger != 'time':
499 raise ValueError('Delta is only relevant for timed transiton')
500 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
504 'name': fields.function(_get_name, method=True, string='Name',
505 type='char', size=128),
506 'activity_from_id': fields.many2one('marketing.campaign.activity',
507 'Previous Activity', select=1,
508 required=True, ondelete="cascade"),
509 'activity_to_id': fields.many2one('marketing.campaign.activity',
511 required=True, ondelete="cascade"),
512 'interval_nbr': fields.integer('Interval Value', required=True),
513 'interval_type': fields.selection(_interval_units, 'Interval Unit',
516 'trigger': fields.selection([('auto', 'Automatic'),
518 ('cosmetic', 'Cosmetic'), # fake plastic transition
520 'Trigger', required=True,
521 help="How is the destination workitem triggered"),
526 'interval_type': 'days',
529 def _check_campaign(self, cr, uid, ids, context=None):
532 for obj in self.browse(cr, uid, ids, context=context):
533 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
538 (_check_campaign, _('The To/From Activity of transition must be of the same Campaign '), ['activity_from_id,activity_to_id']),
542 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
545 marketing_campaign_transition()
547 class marketing_campaign_workitem(osv.osv):
548 _name = "marketing.campaign.workitem"
549 _description = "Campaign Workitem"
551 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
552 res = dict.fromkeys(ids, '/')
553 for wi in self.browse(cr, uid, ids, context=context):
557 proxy = self.pool.get(wi.object_id.model)
558 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
560 res[wi.id] = ng[0][1]
563 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
564 """Returns id of workitem whose resource_name matches with the given name"""
573 cr.execute("""select w.id, w.res_id, m.model \
574 from marketing_campaign_workitem w \
575 left join marketing_campaign_activity a on (a.id=w.activity_id)\
576 left join marketing_campaign c on (c.id=a.campaign_id)\
577 left join ir_model m on (m.id=c.object_id)
580 for id, res_id, model in res:
581 model_pool = self.pool.get(model)
583 if arg[1] == 'ilike':
584 condition.append((model_pool._rec_name, 'ilike', arg[2]))
585 res_ids = model_pool.search(cr, uid, condition, context=context)
586 if res_id in res_ids:
588 return [('id', 'in', final_ids)]
591 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
592 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
593 required=True, readonly=True),
594 'campaign_id': fields.related('activity_id', 'campaign_id',
595 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
596 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
597 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True),
598 'res_id': fields.integer('Resource ID', select=1, readonly=True),
599 'res_name': fields.function(_res_name_get, method=True, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
600 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
601 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
602 'state': fields.selection([('todo', 'To Do'),
603 ('exception', 'Exception'), ('done', 'Done'),
604 ('cancelled', 'Cancelled')], 'State', readonly=True),
606 'error_msg' : fields.text('Error Message', readonly=True)
609 'state': lambda *a: 'todo',
613 def button_draft(self, cr, uid, workitem_ids, context={}):
614 for wi in self.browse(cr, uid, workitem_ids, context=context):
615 if wi.state in ('exception', 'cancelled'):
616 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
619 def button_cancel(self, cr, uid, workitem_ids, context={}):
620 for wi in self.browse(cr, uid, workitem_ids, context=context):
621 if wi.state in ('todo','exception'):
622 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
625 def _process_one(self, cr, uid, workitem, context=None):
626 if workitem.state != 'todo':
629 activity = workitem.activity_id
630 proxy = self.pool.get(workitem.object_id.model)
631 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
634 'activity': activity,
635 'workitem': workitem,
637 'resource': object_id,
638 'transitions': activity.to_ids,
642 condition = activity.condition
643 campaign_mode = workitem.campaign_id.mode
645 if not eval(condition, eval_context):
646 if activity.keep_if_condition_not_met:
647 workitem.write({'state': 'cancelled'}, context=context)
649 workitem.unlink(context=context)
652 if campaign_mode in ('manual', 'active'):
653 Activities = self.pool.get('marketing.campaign.activity')
654 result = Activities.process(cr, uid, activity.id, workitem.id,
657 values = dict(state='done')
658 if not workitem.date:
659 values['date'] = datetime.now().strftime(DT_FMT)
660 workitem.write(values, context=context)
664 workitem = workitem.browse(context)[0] # reload
665 date = datetime.strptime(workitem.date, DT_FMT)
667 for transition in activity.to_ids:
668 if transition.trigger == 'cosmetic':
671 if transition.trigger == 'auto':
673 elif transition.trigger == 'time':
674 launch_date = date + transition._delta()
677 launch_date = launch_date.strftime(DT_FMT)
680 'segment_id': workitem.segment_id.id,
681 'activity_id': transition.activity_to_id.id,
682 'partner_id': workitem.partner_id.id,
683 'res_id': workitem.res_id,
686 wi_id = self.create(cr, uid, values, context=context)
688 # Now, depending on the trigger and the campaign mode
689 # we know whether we must run the newly created workitem.
691 # rows = transition trigger \ colums = campaign mode
693 # test test_realtime manual normal (active)
699 run = (transition.trigger == 'auto' \
700 and campaign_mode != 'manual') \
701 or (transition.trigger == 'time' \
702 and campaign_mode == 'test')
704 new_wi = self.browse(cr, uid, wi_id, context)
705 self._process_one(cr, uid, new_wi, context)
708 tb = "".join(format_exception(*exc_info()))
709 workitem.write({'state': 'exception', 'error_msg': tb},
712 def process(self, cr, uid, workitem_ids, context=None):
713 for wi in self.browse(cr, uid, workitem_ids, context):
714 self._process_one(cr, uid, wi, context)
717 def process_all(self, cr, uid, camp_ids=None, context=None):
718 camp_obj = self.pool.get('marketing.campaign')
720 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
721 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
722 if camp.mode == 'manual':
723 # manual states are not processed automatically
726 domain = [('state', '=', 'todo'), ('date', '!=', False)]
727 if camp.mode in ('test_realtime', 'active'):
728 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
730 workitem_ids = self.search(cr, uid, domain, context=context)
734 self.process(cr, uid, workitem_ids, context)
737 def preview(self, cr, uid, ids, context):
739 wi_obj = self.browse(cr, uid, ids[0], context)
740 if wi_obj.activity_id.type == 'email':
741 data_obj = self.pool.get('ir.model.data')
742 data_id = data_obj._get_id(cr, uid, 'email_template', 'email_template_preview_form')
745 view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
747 'name': _('Email Preview'),
749 'view_mode': 'form,tree',
750 'res_model': 'email_template.preview',
753 'views': [(view_id, 'form')],
754 'type': 'ir.actions.act_window',
757 'context': "{'template_id':%d,'default_rel_model_ref':%d}"%
758 (wi_obj.activity_id.email_template_id.id,
762 elif wi_obj.activity_id.type == 'report':
764 'ids': [wi_obj.res_id],
765 'model': wi_obj.object_id.model
768 'type' : 'ir.actions.report.xml',
769 'report_name': wi_obj.activity_id.report_id.report_name,
773 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
776 marketing_campaign_workitem()
778 class email_template(osv.osv):
779 _inherit = "email.template"
781 'object_name': lambda obj, cr, uid, context: context.get('object_id',False),
784 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
788 class report_xml(osv.osv):
789 _inherit = 'ir.actions.report.xml'
790 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
793 object_id = context.get('object_id')
795 model = self.pool.get('ir.model').browse(cr, uid, object_id).model
796 args.append(('model', '=', model))
797 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)