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_paper(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',
531 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
534 marketing_campaign_transition()
536 class marketing_campaign_workitem(osv.osv):
537 _name = "marketing.campaign.workitem"
538 _description = "Campaign Workitem"
540 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
541 res = dict.fromkeys(ids, '/')
542 for wi in self.browse(cr, uid, ids, context=context):
546 proxy = self.pool.get(wi.object_id.model)
547 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
549 res[wi.id] = ng[0][1]
552 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
553 """Returns id of workitem whose resource_name matches with the given name"""
562 cr.execute("""select w.id, w.res_id, m.model \
563 from marketing_campaign_workitem w \
564 left join marketing_campaign_activity a on (a.id=w.activity_id)\
565 left join marketing_campaign c on (c.id=a.campaign_id)\
566 left join ir_model m on (m.id=c.object_id)
569 for id, res_id, model in res:
570 model_pool = self.pool.get(model)
572 if arg[1] == 'ilike':
573 condition.append((model_pool._rec_name, 'ilike', arg[2]))
574 res_ids = model_pool.search(cr, uid, condition, context=context)
575 if res_id in res_ids:
577 return [('id', 'in', final_ids)]
580 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
581 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
582 required=True, readonly=True),
583 'campaign_id': fields.related('activity_id', 'campaign_id',
584 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True),
585 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
586 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True),
587 'res_id': fields.integer('Resource ID', select=1, readonly=True),
588 'res_name': fields.function(_res_name_get, method=True, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
589 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
590 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
591 'state': fields.selection([('todo', 'To Do'),
592 ('exception', 'Exception'), ('done', 'Done'),
593 ('cancelled', 'Cancelled')], 'State', readonly=True),
595 'error_msg' : fields.text('Error Message', readonly=True)
598 'state': lambda *a: 'todo',
602 def button_draft(self, cr, uid, workitem_ids, context={}):
603 for wi in self.browse(cr, uid, workitem_ids, context=context):
604 if wi.state in ('exception', 'cancelled'):
605 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
608 def button_cancel(self, cr, uid, workitem_ids, context={}):
609 for wi in self.browse(cr, uid, workitem_ids, context=context):
610 if wi.state in ('todo','exception'):
611 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
614 def _process_one(self, cr, uid, workitem, context=None):
615 if workitem.state != 'todo':
618 activity = workitem.activity_id
619 proxy = self.pool.get(workitem.object_id.model)
620 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
623 'activity': activity,
624 'workitem': workitem,
626 'resource': object_id,
627 'transitions': activity.to_ids,
631 condition = activity.condition
632 campaign_mode = workitem.campaign_id.mode
634 if not eval(condition, eval_context):
635 if activity.keep_if_condition_not_met:
636 workitem.write({'state': 'cancelled'}, context=context)
638 workitem.unlink(context=context)
641 if campaign_mode in ('manual', 'active'):
642 Activities = self.pool.get('marketing.campaign.activity')
643 result = Activities.process(cr, uid, activity.id, workitem.id,
646 values = dict(state='done')
647 if not workitem.date:
648 values['date'] = datetime.now().strftime(DT_FMT)
649 workitem.write(values, context=context)
653 workitem = workitem.browse(context)[0] # reload
654 date = datetime.strptime(workitem.date, DT_FMT)
656 for transition in activity.to_ids:
657 if transition.trigger == 'cosmetic':
660 if transition.trigger == 'auto':
662 elif transition.trigger == 'time':
663 launch_date = date + transition._delta()
666 launch_date = launch_date.strftime(DT_FMT)
669 'segment_id': workitem.segment_id.id,
670 'activity_id': transition.activity_to_id.id,
671 'partner_id': workitem.partner_id.id,
672 'res_id': workitem.res_id,
675 wi_id = self.create(cr, uid, values, context=context)
677 # Now, depending on the trigger and the campaign mode
678 # we know whether we must run the newly created workitem.
680 # rows = transition trigger \ colums = campaign mode
682 # test test_realtime manual normal (active)
688 run = (transition.trigger == 'auto' \
689 and campaign_mode != 'manual') \
690 or (transition.trigger == 'time' \
691 and campaign_mode == 'test')
693 new_wi = self.browse(cr, uid, wi_id, context)
694 self._process_one(cr, uid, new_wi, context)
697 tb = "".join(format_exception(*exc_info()))
698 workitem.write({'state': 'exception', 'error_msg': tb},
701 def process(self, cr, uid, workitem_ids, context=None):
702 for wi in self.browse(cr, uid, workitem_ids, context):
703 self._process_one(cr, uid, wi, context)
706 def process_all(self, cr, uid, camp_ids=None, context=None):
707 camp_obj = self.pool.get('marketing.campaign')
709 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
710 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
711 if camp.mode == 'manual':
712 # manual states are not processed automatically
715 domain = [('state', '=', 'todo'), ('date', '!=', False)]
716 if camp.mode in ('test_realtime', 'active'):
717 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
719 workitem_ids = self.search(cr, uid, domain, context=context)
723 self.process(cr, uid, workitem_ids, context)
726 def preview(self, cr, uid, ids, context):
728 wi_obj = self.browse(cr, uid, ids[0], context)
729 if wi_obj.activity_id.type == 'email':
730 data_obj = self.pool.get('ir.model.data')
731 data_id = data_obj._get_id(cr, uid, 'email_template', 'email_template_preview_form')
734 view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
736 'name': _('Email Preview'),
738 'view_mode': 'form,tree',
739 'res_model': 'email_template.preview',
742 'views': [(view_id, 'form')],
743 'type': 'ir.actions.act_window',
746 'context': "{'template_id':%d,'default_rel_model_ref':%d}"%
747 (wi_obj.activity_id.email_template_id.id,
751 elif wi_obj.activity_id.type == 'report':
753 'ids': [wi_obj.res_id],
754 'model': wi_obj.object_id.model
757 'type' : 'ir.actions.report.xml',
758 'report_name': wi_obj.activity_id.report_id.report_name,
762 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
765 marketing_campaign_workitem()
767 class email_template(osv.osv):
768 _inherit = "email.template"
770 'object_name': lambda obj, cr, uid, context: context.get('object_id',False),
773 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
777 class report_xml(osv.osv):
778 _inherit = 'ir.actions.report.xml'
779 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
782 object_id = context.get('object_id')
784 model = self.pool.get('ir.model').browse(cr, uid, object_id).model
785 args.append(('model', '=', model))
786 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)