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: the email account is missing in 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 email activity '%s'")%activity.name)
142 if not has_start and not has_signal_without_from:
143 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)"))
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"
216 def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
217 # next auto sync date is same for all segments
218 sync_job = self.pool.get('ir.model.data').get_object(cr, uid, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
219 next_sync = sync_job and sync_job.nextcall or False
220 return dict.fromkeys(ids, next_sync)
223 'name': fields.char('Name', size=64,required=True),
224 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
225 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
226 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
227 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. If no filter is set, all records are selected without filtering. The synchronization mode may also add a criterion to the filter."),
228 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
229 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
230 ('write_date', 'Only records modified after last sync (no duplicates)'),
231 ('all', 'All records (no duplicates)')],
232 'Synchronization mode',
233 help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign."),
234 'state': fields.selection([('draft', 'Draft'),
235 ('running', 'Running'),
237 ('cancelled', 'Cancelled')],
239 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
240 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
241 'date_next_sync': fields.function(_get_next_sync, method=True, string='Next Synchronization', type='datetime', help="Next time the synchronization job is scheduled to run automatically"),
245 'state': lambda *a: 'draft',
246 'sync_mode': lambda *a: 'create_date',
249 def _check_model(self, cr, uid, ids, context=None):
252 for obj in self.browse(cr, uid, ids, context=context):
253 if not obj.ir_filter_id:
255 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
260 (_check_model, _('Model of filter must be same as resource model of Campaign '), ['ir_filter_id,campaign_id']),
263 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
264 res = {'domain':{'ir_filter_id':[]}}
265 campaign_pool = self.pool.get('marketing.campaign')
267 campaign = campaign_pool.browse(cr, uid, campaign_id)
268 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
270 mod_name = model_name[0]['model']
271 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
273 res['value'] = {'ir_filter_id': False}
276 def state_running_set(self, cr, uid, ids, *args):
277 segment = self.browse(cr, uid, ids[0])
278 vals = {'state': 'running'}
279 if not segment.date_run:
280 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
281 self.write(cr, uid, ids, vals)
284 def state_done_set(self, cr, uid, ids, *args):
285 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
286 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
287 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
288 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
291 def state_cancel_set(self, cr, uid, ids, *args):
292 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
293 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
294 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
295 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
298 def synchroniz(self, cr, uid, ids, *args):
299 self.process_segment(cr, uid, ids)
302 def process_segment(self, cr, uid, segment_ids=None, context=None):
303 Workitems = self.pool.get('marketing.campaign.workitem')
305 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
307 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
309 for segment in self.browse(cr, uid, segment_ids, context=context):
310 if segment.campaign_id.state != 'running':
313 campaigns.add(segment.campaign_id.id)
314 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
315 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
317 model_obj = self.pool.get(segment.object_id.model)
319 if segment.sync_last_date and segment.sync_mode != 'all':
320 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
321 if segment.ir_filter_id:
322 criteria += eval(segment.ir_filter_id.domain)
323 object_ids = model_obj.search(cr, uid, criteria, context=context)
325 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
326 for o_ids in model_obj.browse(cr, uid, object_ids, context=context):
327 # avoid duplicated workitem for the same resource
328 if segment.sync_mode in ('write_date','all'):
329 wi_ids = Workitems.search(cr, uid, [('res_id','=',o_ids.id),('segment_id','=',segment.id)], context=context)
334 'segment_id': segment.id,
340 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, o_ids)
342 wi_vals['partner_id'] = partner.id
344 for act_id in act_ids:
345 wi_vals['activity_id'] = act_id
346 Workitems.create(cr, uid, wi_vals, context=context)
348 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
349 Workitems.process_all(cr, uid, list(campaigns), context=context)
352 marketing_campaign_segment()
354 class marketing_campaign_activity(osv.osv):
355 _name = "marketing.campaign.activity"
356 _description = "Campaign Activity"
360 ('report', 'Report'),
361 ('action', 'Custom Action'),
362 # TODO implement the subcampaigns.
363 # TODO implement the subcampaign out. disallow out transitions from
364 # subcampaign activities ?
365 #('subcampaign', 'Sub-Campaign'),
369 'name': fields.char('Name', size=128, required=True),
370 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
371 required = True, ondelete='cascade', select=1),
372 'object_id': fields.related('campaign_id','object_id',
373 type='many2one', relation='ir.model',
374 string='Object', readonly=True),
375 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
376 'condition': fields.text('Condition', size=256, required=True,
377 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
378 "The expression may use the following [browsable] variables:\n"
379 " - activity: the campaign activity\n"
380 " - workitem: the campaign workitem\n"
381 " - resource: the resource object this campaign item represents\n"
382 " - transitions: list of campaign transitions outgoing from this activity\n"
383 "...- re: Python regular expression module"),
384 'type': fields.selection(_action_types, 'Type', required=True,
385 help="""The type of action to execute when an item enters this activity, such as:
386 - Email: send an email using a predefined email template
387 - Report: print an existing Report defined on the resource item and save it into a specific directory
388 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
390 'email_template_id': fields.many2one('email.template', "Email Template", help='The e-mail to send when this activity is activated'),
391 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
392 'report_directory_id': fields.many2one('document.directory','Directory',
393 help="This folder is used to store the generated reports"),
394 'server_action_id': fields.many2one('ir.actions.server', string='Action',
395 help= "The action to perform when this activity is activated"),
396 'to_ids': fields.one2many('marketing.campaign.transition',
399 'from_ids': fields.one2many('marketing.campaign.transition',
401 'Previous Activities'),
402 '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"),
403 '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"),
404 'signal': fields.char('Signal', size=128,
405 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
406 'keep_if_condition_not_met': fields.boolean("Don't delete workitems",
407 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
411 'type': lambda *a: 'email',
412 'condition': lambda *a: 'True',
415 def search(self, cr, uid, args, offset=0, limit=None, order=None,
416 context=None, count=False):
419 if 'segment_id' in context and context['segment_id']:
420 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
421 uid, context['segment_id'])
423 for activity in segment_obj.campaign_id.activity_ids:
424 act_ids.append(activity.id)
426 return super(marketing_campaign_activity, self).search(cr, uid, args,
427 offset, limit, order, context, count)
429 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
430 service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
431 (report_data, format) = service.create(cr, uid, [], {}, {})
433 'name': '%s_%s_%s'%(activity.report_id.report_name,
434 activity.name,workitem.partner_id.name),
435 'datas_fname': '%s.%s'%(activity.report_id.report_name,
436 activity.report_id.report_type),
437 'parent_id': activity.report_directory_id.id,
438 'datas': base64.encodestring(report_data),
441 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
444 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
445 return self.pool.get('email.template').generate_mail(cr, uid,
446 activity.email_template_id.id,
447 [workitem.res_id], context=context)
449 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
452 server_obj = self.pool.get('ir.actions.server')
454 action_context = dict(context,
455 active_id=workitem.res_id,
456 active_ids=[workitem.res_id],
457 active_model=workitem.object_id.model)
458 res = server_obj.run(cr, uid, [activity.server_action_id.id],
459 context=action_context)
460 # server action return False if the action is perfomed
461 # except client_action, other and python code
462 return res == False and True or res
464 def process(self, cr, uid, act_id, wi_id, context=None):
465 activity = self.browse(cr, uid, act_id, context=context)
466 method = '_process_wi_%s' % (activity.type,)
467 action = getattr(self, method, None)
469 raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
471 workitem_obj = self.pool.get('marketing.campaign.workitem')
472 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
473 return action(cr, uid, activity, workitem, context)
475 marketing_campaign_activity()
477 class marketing_campaign_transition(osv.osv):
478 _name = "marketing.campaign.transition"
479 _description = "Campaign Transition"
482 ('hours', 'Hour(s)'), ('days', 'Day(s)'),
483 ('months', 'Month(s)'), ('years','Year(s)')
486 def _get_name(self, cr, uid, ids, fn, args, context=None):
487 result = dict.fromkeys(ids, False)
489 'auto': _('Automatic transition'),
490 'time': _('After %(interval_nbr)d %(interval_type)s'),
491 'cosmetic': _('Cosmetic'),
493 for tr in self.browse(cr, uid, ids, context=context,
494 fields_process=translate_selections):
495 result[tr.id] = formatters[tr.trigger.value] % tr
499 def _delta(self, cr, uid, ids, context=None):
501 transition = self.browse(cr, uid, ids[0], context)
502 if transition.trigger != 'time':
503 raise ValueError('Delta is only relevant for timed transiton')
504 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
508 'name': fields.function(_get_name, method=True, string='Name',
509 type='char', size=128),
510 'activity_from_id': fields.many2one('marketing.campaign.activity',
511 'Previous Activity', select=1,
512 required=True, ondelete="cascade"),
513 'activity_to_id': fields.many2one('marketing.campaign.activity',
515 required=True, ondelete="cascade"),
516 'interval_nbr': fields.integer('Interval Value', required=True),
517 'interval_type': fields.selection(_interval_units, 'Interval Unit',
520 'trigger': fields.selection([('auto', 'Automatic'),
522 ('cosmetic', 'Cosmetic'), # fake plastic transition
524 'Trigger', required=True,
525 help="How is the destination workitem triggered"),
530 'interval_type': 'days',
533 def _check_campaign(self, cr, uid, ids, context=None):
536 for obj in self.browse(cr, uid, ids, context=context):
537 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
542 (_check_campaign, _('The To/From Activity of transition must be of the same Campaign '), ['activity_from_id,activity_to_id']),
546 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
549 marketing_campaign_transition()
551 class marketing_campaign_workitem(osv.osv):
552 _name = "marketing.campaign.workitem"
553 _description = "Campaign Workitem"
555 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
556 res = dict.fromkeys(ids, '/')
557 for wi in self.browse(cr, uid, ids, context=context):
561 proxy = self.pool.get(wi.object_id.model)
562 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
564 res[wi.id] = ng[0][1]
567 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
568 """Returns id of workitem whose resource_name matches with the given name"""
577 cr.execute("""select w.id, w.res_id, m.model \
578 from marketing_campaign_workitem w \
579 left join marketing_campaign_activity a on (a.id=w.activity_id)\
580 left join marketing_campaign c on (c.id=a.campaign_id)\
581 left join ir_model m on (m.id=c.object_id)
584 for id, res_id, model in res:
585 model_pool = self.pool.get(model)
587 if arg[1] == 'ilike':
588 condition.append((model_pool._rec_name, 'ilike', arg[2]))
589 res_ids = model_pool.search(cr, uid, condition, context=context)
590 if res_id in res_ids:
592 return [('id', 'in', final_ids)]
595 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
596 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
597 required=True, readonly=True),
598 'campaign_id': fields.related('activity_id', 'campaign_id',
599 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
600 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
601 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
602 'res_id': fields.integer('Resource ID', select=1, readonly=True),
603 'res_name': fields.function(_res_name_get, method=True, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
604 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
605 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
606 'state': fields.selection([('todo', 'To Do'),
607 ('exception', 'Exception'), ('done', 'Done'),
608 ('cancelled', 'Cancelled')], 'State', readonly=True),
610 'error_msg' : fields.text('Error Message', readonly=True)
613 'state': lambda *a: 'todo',
617 def button_draft(self, cr, uid, workitem_ids, context={}):
618 for wi in self.browse(cr, uid, workitem_ids, context=context):
619 if wi.state in ('exception', 'cancelled'):
620 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
623 def button_cancel(self, cr, uid, workitem_ids, context={}):
624 for wi in self.browse(cr, uid, workitem_ids, context=context):
625 if wi.state in ('todo','exception'):
626 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
629 def _process_one(self, cr, uid, workitem, context=None):
630 if workitem.state != 'todo':
633 activity = workitem.activity_id
634 proxy = self.pool.get(workitem.object_id.model)
635 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
638 'activity': activity,
639 'workitem': workitem,
641 'resource': object_id,
642 'transitions': activity.to_ids,
646 condition = activity.condition
647 campaign_mode = workitem.campaign_id.mode
649 if not eval(condition, eval_context):
650 if activity.keep_if_condition_not_met:
651 workitem.write({'state': 'cancelled'}, context=context)
653 workitem.unlink(context=context)
656 if campaign_mode in ('manual', 'active'):
657 Activities = self.pool.get('marketing.campaign.activity')
658 result = Activities.process(cr, uid, activity.id, workitem.id,
661 values = dict(state='done')
662 if not workitem.date:
663 values['date'] = datetime.now().strftime(DT_FMT)
664 workitem.write(values, context=context)
668 workitem = workitem.browse(context)[0] # reload
669 date = datetime.strptime(workitem.date, DT_FMT)
671 for transition in activity.to_ids:
672 if transition.trigger == 'cosmetic':
675 if transition.trigger == 'auto':
677 elif transition.trigger == 'time':
678 launch_date = date + transition._delta()
681 launch_date = launch_date.strftime(DT_FMT)
684 'segment_id': workitem.segment_id.id,
685 'activity_id': transition.activity_to_id.id,
686 'partner_id': workitem.partner_id.id,
687 'res_id': workitem.res_id,
690 wi_id = self.create(cr, uid, values, context=context)
692 # Now, depending on the trigger and the campaign mode
693 # we know whether we must run the newly created workitem.
695 # rows = transition trigger \ colums = campaign mode
697 # test test_realtime manual normal (active)
703 run = (transition.trigger == 'auto' \
704 and campaign_mode != 'manual') \
705 or (transition.trigger == 'time' \
706 and campaign_mode == 'test')
708 new_wi = self.browse(cr, uid, wi_id, context)
709 self._process_one(cr, uid, new_wi, context)
712 tb = "".join(format_exception(*exc_info()))
713 workitem.write({'state': 'exception', 'error_msg': tb},
716 def process(self, cr, uid, workitem_ids, context=None):
717 for wi in self.browse(cr, uid, workitem_ids, context):
718 self._process_one(cr, uid, wi, context)
721 def process_all(self, cr, uid, camp_ids=None, context=None):
722 camp_obj = self.pool.get('marketing.campaign')
724 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
725 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
726 if camp.mode == 'manual':
727 # manual states are not processed automatically
730 domain = [('state', '=', 'todo'), ('date', '!=', False)]
731 if camp.mode in ('test_realtime', 'active'):
732 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
734 workitem_ids = self.search(cr, uid, domain, context=context)
738 self.process(cr, uid, workitem_ids, context)
741 def preview(self, cr, uid, ids, context):
743 wi_obj = self.browse(cr, uid, ids[0], context)
744 if wi_obj.activity_id.type == 'email':
745 data_obj = self.pool.get('ir.model.data')
746 data_id = data_obj._get_id(cr, uid, 'email_template', 'email_template_preview_form')
749 view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
751 'name': _('Email Preview'),
753 'view_mode': 'form,tree',
754 'res_model': 'email_template.preview',
757 'views': [(view_id, 'form')],
758 'type': 'ir.actions.act_window',
761 'context': "{'template_id':%d,'default_rel_model_ref':%d}"%
762 (wi_obj.activity_id.email_template_id.id,
766 elif wi_obj.activity_id.type == 'report':
768 'ids': [wi_obj.res_id],
769 'model': wi_obj.object_id.model
772 'type' : 'ir.actions.report.xml',
773 'report_name': wi_obj.activity_id.report_id.report_name,
777 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
780 marketing_campaign_workitem()
782 class email_template(osv.osv):
783 _inherit = "email.template"
785 'object_name': lambda obj, cr, uid, context: context.get('object_id',False),
788 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
792 class report_xml(osv.osv):
793 _inherit = 'ir.actions.report.xml'
794 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
797 object_id = context.get('object_id')
799 model = self.pool.get('ir.model').browse(cr, uid, object_id).model
800 args.append(('model', '=', model))
801 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)