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
31 from decimal_precision import decimal_precision as dp
33 from osv import fields, osv
35 from tools.translate import _
38 'hours': lambda interval: relativedelta(hours=interval),
39 'days': lambda interval: relativedelta(days=interval),
40 'months': lambda interval: relativedelta(months=interval),
41 'years': lambda interval: relativedelta(years=interval),
44 DT_FMT = '%Y-%m-%d %H:%M:%S'
47 return dict((k, f(v)) for k,v in d.items())
49 def _find_fieldname(model, field):
50 inherit_columns = dict_map(itemgetter(2), model._inherit_fields)
51 all_columns = dict(inherit_columns, **model._columns)
52 for fn in all_columns:
53 if all_columns[fn] is field:
55 raise ValueError('field not found: %r' % (field,))
57 class selection_converter(object):
58 """Format the selection in the browse record objects"""
59 def __init__(self, value):
63 def set_value(self, cr, uid, _self_again, record, field, lang):
64 # this design is terrible
65 # search fieldname from the field
66 fieldname = _find_fieldname(record._table, field)
67 context = dict(lang=lang.code)
68 fg = record._table.fields_get(cr, uid, [fieldname], context=context)
69 selection = dict(fg[fieldname]['selection'])
70 self._str = selection[self.value]
79 translate_selections = {
80 'selection': selection_converter,
84 class marketing_campaign(osv.osv):
85 _name = "marketing.campaign"
86 _description = "Marketing Campaign"
89 'name': fields.char('Name', size=64, required=True),
90 'object_id': fields.many2one('ir.model', 'Resource', required=True,
91 help="Choose the resource on which you want \
92 this campaign to be run"),
93 'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
94 domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
95 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."),
96 'mode': fields.selection([('test', 'Test Directly'),
97 ('test_realtime', 'Test in Realtime'),
98 ('manual', 'With Manual Confirmation'),
99 ('active', 'Normal')],
100 'Mode', required=True, help= \
101 """Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
102 Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
103 With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
104 Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
105 'state': fields.selection([('draft', 'Draft'),
106 ('running', 'Running'),
108 ('cancelled', 'Cancelled'),],
110 'activity_ids': fields.one2many('marketing.campaign.activity',
111 'campaign_id', 'Activities'),
112 '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')),
116 'state': lambda *a: 'draft',
117 'mode': lambda *a: 'test',
120 def state_running_set(self, cr, uid, ids, *args):
121 # TODO check that all subcampaigns are running
122 campaign = self.browse(cr, uid, ids[0])
124 if not campaign.activity_ids:
125 raise osv.except_osv(_("Error"), _("The campaign cannot be started: there are no activities in it"))
128 has_signal_without_from = False
130 for activity in campaign.activity_ids:
133 if activity.signal and len(activity.from_ids) == 0:
134 has_signal_without_from = True
136 if activity.type != 'email':
138 if not activity.email_template_id.from_account:
139 raise osv.except_osv(_("Error"), _("The campaign cannot be started: the email account is missing in email activity '%s'")%activity.name)
140 if activity.email_template_id.from_account.state != 'approved':
141 raise osv.except_osv(_("Error"), _("The campaign cannot be started: the email account is not approved in email activity '%s'")%activity.name)
143 if not has_start and not has_signal_without_from:
144 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)"))
146 return self.write(cr, uid, ids, {'state': 'running'})
148 def state_done_set(self, cr, uid, ids, *args):
149 # TODO check that this campaign is not a subcampaign in running mode.
150 segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
151 [('campaign_id', 'in', ids),
152 ('state', '=', 'running')])
154 raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are done"))
155 self.write(cr, uid, ids, {'state': 'done'})
158 def state_cancel_set(self, cr, uid, ids, *args):
159 # TODO check that this campaign is not a subcampaign in running mode.
160 self.write(cr, uid, ids, {'state': 'cancelled'})
164 def signal(self, cr, uid, model, res_id, signal, run_existing=True, context=None):
165 record = self.pool.get(model).browse(cr, uid, res_id, context)
166 return self._signal(cr, uid, record, signal, run_existing, context)
168 def _signal(self, cr, uid, record, signal, run_existing=True, context=None):
170 raise ValueError('signal cannot be False')
172 Workitems = self.pool.get('marketing.campaign.workitem')
173 domain = [('object_id.model', '=', record._table._name),
174 ('state', '=', 'running')]
175 campaign_ids = self.search(cr, uid, domain, context=context)
176 for campaign in self.browse(cr, uid, campaign_ids, context=context):
177 for activity in campaign.activity_ids:
178 if activity.signal != signal:
181 data = dict(activity_id=activity.id,
184 wi_domain = [(k, '=', v) for k, v in data.items()]
186 wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
191 partner = self._get_partner_for(campaign, record)
193 data['partner_id'] = partner.id
194 wi_id = Workitems.create(cr, uid, data, context=context)
196 Workitems.process(cr, uid, wi_ids, context=context)
199 def _get_partner_for(self, campaign, record):
200 partner_field = campaign.partner_field_id.name
202 return getattr(record, partner_field)
203 elif campaign.object_id.model == 'res.partner':
207 # prevent duplication until the server properly duplicates several levels of nested o2m
208 def copy(self, cr, uid, id, default=None, context=None):
209 raise osv.except_osv(_("Operation not supported"), _("Sorry, campaign duplication is not supported at the moment."))
213 class marketing_campaign_segment(osv.osv):
214 _name = "marketing.campaign.segment"
215 _description = "Campaign Segment"
217 def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
218 # next auto sync date is same for all segments
219 sync_job = self.pool.get('ir.model.data').get_object(cr, uid, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
220 next_sync = sync_job and sync_job.nextcall or False
221 return dict.fromkeys(ids, next_sync)
224 'name': fields.char('Name', size=64,required=True),
225 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
226 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
227 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
228 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."),
229 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
230 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
231 ('write_date', 'Only records modified after last sync (no duplicates)'),
232 ('all', 'All records (no duplicates)')],
233 'Synchronization mode',
234 help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign."),
235 'state': fields.selection([('draft', 'Draft'),
236 ('running', 'Running'),
238 ('cancelled', 'Cancelled')],
240 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
241 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
242 '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"),
246 'state': lambda *a: 'draft',
247 'sync_mode': lambda *a: 'create_date',
250 def _check_model(self, cr, uid, ids, context=None):
251 for obj in self.browse(cr, uid, ids, context=context):
252 if not obj.ir_filter_id:
254 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
259 (_check_model, 'Model of filter must be same as resource model of Campaign ', ['ir_filter_id,campaign_id']),
262 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
263 res = {'domain':{'ir_filter_id':[]}}
264 campaign_pool = self.pool.get('marketing.campaign')
266 campaign = campaign_pool.browse(cr, uid, campaign_id)
267 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
269 mod_name = model_name[0]['model']
270 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
272 res['value'] = {'ir_filter_id': False}
275 def state_running_set(self, cr, uid, ids, *args):
276 segment = self.browse(cr, uid, ids[0])
277 vals = {'state': 'running'}
278 if not segment.date_run:
279 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
280 self.write(cr, uid, ids, vals)
283 def state_done_set(self, cr, uid, ids, *args):
284 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
285 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
286 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
287 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
290 def state_cancel_set(self, cr, uid, ids, *args):
291 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
292 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
293 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
294 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
297 def synchroniz(self, cr, uid, ids, *args):
298 self.process_segment(cr, uid, ids)
301 def process_segment(self, cr, uid, segment_ids=None, context=None):
302 Workitems = self.pool.get('marketing.campaign.workitem')
304 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
306 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
308 for segment in self.browse(cr, uid, segment_ids, context=context):
309 if segment.campaign_id.state != 'running':
312 campaigns.add(segment.campaign_id.id)
313 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
314 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
316 model_obj = self.pool.get(segment.object_id.model)
318 if segment.sync_last_date and segment.sync_mode != 'all':
319 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
320 if segment.ir_filter_id:
321 criteria += eval(segment.ir_filter_id.domain)
322 object_ids = model_obj.search(cr, uid, criteria, context=context)
324 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
325 for o_ids in model_obj.browse(cr, uid, object_ids, context=context):
326 # avoid duplicated workitem for the same resource
327 if segment.sync_mode in ('write_date','all'):
328 wi_ids = Workitems.search(cr, uid, [('res_id','=',o_ids.id),('segment_id','=',segment.id)], context=context)
333 'segment_id': segment.id,
339 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, o_ids)
341 wi_vals['partner_id'] = partner.id
343 for act_id in act_ids:
344 wi_vals['activity_id'] = act_id
345 Workitems.create(cr, uid, wi_vals, context=context)
347 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
348 Workitems.process_all(cr, uid, list(campaigns), context=context)
351 marketing_campaign_segment()
353 class marketing_campaign_activity(osv.osv):
354 _name = "marketing.campaign.activity"
355 _description = "Campaign Activity"
359 ('report', 'Report'),
360 ('action', 'Custom Action'),
361 # TODO implement the subcampaigns.
362 # TODO implement the subcampaign out. disallow out transitions from
363 # subcampaign activities ?
364 #('subcampaign', 'Sub-Campaign'),
368 'name': fields.char('Name', size=128, required=True),
369 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
370 required = True, ondelete='cascade', select=1),
371 'object_id': fields.related('campaign_id','object_id',
372 type='many2one', relation='ir.model',
373 string='Object', readonly=True),
374 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
375 'condition': fields.text('Condition', size=256, required=True,
376 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
377 "The expression may use the following [browsable] variables:\n"
378 " - activity: the campaign activity\n"
379 " - workitem: the campaign workitem\n"
380 " - resource: the resource object this campaign item represents\n"
381 " - transitions: list of campaign transitions outgoing from this activity\n"
382 "...- re: Python regular expression module"),
383 'type': fields.selection(_action_types, 'Type', required=True,
384 help="""The type of action to execute when an item enters this activity, such as:
385 - Email: send an email using a predefined email template
386 - Report: print an existing Report defined on the resource item and save it into a specific directory
387 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
389 'email_template_id': fields.many2one('email.template', "Email Template", help='The e-mail to send when this activity is activated'),
390 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
391 'report_directory_id': fields.many2one('document.directory','Directory',
392 help="This folder is used to store the generated reports"),
393 'server_action_id': fields.many2one('ir.actions.server', string='Action',
394 help= "The action to perform when this activity is activated"),
395 'to_ids': fields.one2many('marketing.campaign.transition',
398 'from_ids': fields.one2many('marketing.campaign.transition',
400 'Previous Activities'),
401 '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')),
402 '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')),
403 'signal': fields.char('Signal', size=128,
404 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
405 'keep_if_condition_not_met': fields.boolean("Don't delete workitems",
406 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
410 'type': lambda *a: 'email',
411 'condition': lambda *a: 'True',
414 def search(self, cr, uid, args, offset=0, limit=None, order=None,
415 context=None, count=False):
418 if 'segment_id' in context and context['segment_id']:
419 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
420 uid, context['segment_id'])
422 for activity in segment_obj.campaign_id.activity_ids:
423 act_ids.append(activity.id)
425 return super(marketing_campaign_activity, self).search(cr, uid, args,
426 offset, limit, order, context, count)
428 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
429 service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
430 (report_data, format) = service.create(cr, uid, [], {}, {})
432 'name': '%s_%s_%s'%(activity.report_id.report_name,
433 activity.name,workitem.partner_id.name),
434 'datas_fname': '%s.%s'%(activity.report_id.report_name,
435 activity.report_id.report_type),
436 'parent_id': activity.report_directory_id.id,
437 'datas': base64.encodestring(report_data),
440 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
443 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
444 return self.pool.get('email.template').generate_mail(cr, uid,
445 activity.email_template_id.id,
446 [workitem.res_id], context=context)
448 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
451 server_obj = self.pool.get('ir.actions.server')
453 action_context = dict(context,
454 active_id=workitem.res_id,
455 active_ids=[workitem.res_id],
456 active_model=workitem.object_id.model)
457 res = server_obj.run(cr, uid, [activity.server_action_id.id],
458 context=action_context)
459 # server action return False if the action is perfomed
460 # except client_action, other and python code
461 return res == False and True or res
463 def process(self, cr, uid, act_id, wi_id, context=None):
464 activity = self.browse(cr, uid, act_id, context=context)
465 method = '_process_wi_%s' % (activity.type,)
466 action = getattr(self, method, None)
468 raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
470 workitem_obj = self.pool.get('marketing.campaign.workitem')
471 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
472 return action(cr, uid, activity, workitem, context=context)
474 marketing_campaign_activity()
476 class marketing_campaign_transition(osv.osv):
477 _name = "marketing.campaign.transition"
478 _description = "Campaign Transition"
481 ('hours', 'Hour(s)'), ('days', 'Day(s)'),
482 ('months', 'Month(s)'), ('years','Year(s)')
485 def _get_name(self, cr, uid, ids, fn, args, context=None):
486 result = dict.fromkeys(ids, False)
488 'auto': _('Automatic transition'),
489 'time': _('After %(interval_nbr)d %(interval_type)s'),
490 'cosmetic': _('Cosmetic'),
492 for tr in self.browse(cr, uid, ids, context=context,
493 fields_process=translate_selections):
494 result[tr.id] = formatters[tr.trigger.value] % tr
498 def _delta(self, cr, uid, ids, context=None):
500 transition = self.browse(cr, uid, ids[0], context=context)
501 if transition.trigger != 'time':
502 raise ValueError('Delta is only relevant for timed transiton')
503 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
507 'name': fields.function(_get_name, method=True, string='Name',
508 type='char', size=128),
509 'activity_from_id': fields.many2one('marketing.campaign.activity',
510 'Previous Activity', select=1,
511 required=True, ondelete="cascade"),
512 'activity_to_id': fields.many2one('marketing.campaign.activity',
514 required=True, ondelete="cascade"),
515 'interval_nbr': fields.integer('Interval Value', required=True),
516 'interval_type': fields.selection(_interval_units, 'Interval Unit',
519 'trigger': fields.selection([('auto', 'Automatic'),
521 ('cosmetic', 'Cosmetic'), # fake plastic transition
523 'Trigger', required=True,
524 help="How is the destination workitem triggered"),
529 'interval_type': 'days',
532 def _check_campaign(self, cr, uid, ids, context=None):
533 for obj in self.browse(cr, uid, ids, context=context):
534 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
539 (_check_campaign, 'The To/From Activity of transition must be of the same Campaign ', ['activity_from_id,activity_to_id']),
543 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
546 marketing_campaign_transition()
548 class marketing_campaign_workitem(osv.osv):
549 _name = "marketing.campaign.workitem"
550 _description = "Campaign Workitem"
552 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
553 res = dict.fromkeys(ids, '/')
554 for wi in self.browse(cr, uid, ids, context=context):
558 proxy = self.pool.get(wi.object_id.model)
559 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
561 res[wi.id] = ng[0][1]
564 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
565 """Returns id of workitem whose resource_name matches with the given name"""
572 cr.execute("""select w.id, w.res_id, m.model \
573 from marketing_campaign_workitem w \
574 left join marketing_campaign_activity a on (a.id=w.activity_id)\
575 left join marketing_campaign c on (c.id=a.campaign_id)\
576 left join ir_model m on (m.id=c.object_id)
579 for id, res_id, model in res:
580 model_pool = self.pool.get(model)
582 if arg[1] == 'ilike':
583 condition.append((model_pool._rec_name, 'ilike', arg[2]))
584 res_ids = model_pool.search(cr, uid, condition, context=context)
585 if res_id in res_ids:
587 return [('id', 'in', final_ids)]
590 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
591 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
592 required=True, readonly=True),
593 'campaign_id': fields.related('activity_id', 'campaign_id',
594 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
595 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
596 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
597 'res_id': fields.integer('Resource ID', select=1, readonly=True),
598 'res_name': fields.function(_res_name_get, method=True, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
599 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
600 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
601 'state': fields.selection([('todo', 'To Do'),
602 ('exception', 'Exception'), ('done', 'Done'),
603 ('cancelled', 'Cancelled')], 'State', readonly=True),
605 'error_msg' : fields.text('Error Message', readonly=True)
608 'state': lambda *a: 'todo',
612 def button_draft(self, cr, uid, workitem_ids, context=None):
613 for wi in self.browse(cr, uid, workitem_ids, context=context):
614 if wi.state in ('exception', 'cancelled'):
615 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
618 def button_cancel(self, cr, uid, workitem_ids, context=None):
619 for wi in self.browse(cr, uid, workitem_ids, context=context):
620 if wi.state in ('todo','exception'):
621 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
624 def _process_one(self, cr, uid, workitem, context=None):
625 if workitem.state != 'todo':
628 activity = workitem.activity_id
629 proxy = self.pool.get(workitem.object_id.model)
630 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
633 'activity': activity,
634 'workitem': workitem,
636 'resource': object_id,
637 'transitions': activity.to_ids,
641 condition = activity.condition
642 campaign_mode = workitem.campaign_id.mode
644 if not eval(condition, eval_context):
645 if activity.keep_if_condition_not_met:
646 workitem.write({'state': 'cancelled'}, context=context)
648 workitem.unlink(context=context)
651 if campaign_mode in ('manual', 'active'):
652 Activities = self.pool.get('marketing.campaign.activity')
653 result = Activities.process(cr, uid, activity.id, workitem.id,
656 values = dict(state='done')
657 if not workitem.date:
658 values['date'] = datetime.now().strftime(DT_FMT)
659 workitem.write(values, context=context)
663 workitem = workitem.browse(context)[0] # reload
664 date = datetime.strptime(workitem.date, DT_FMT)
666 for transition in activity.to_ids:
667 if transition.trigger == 'cosmetic':
670 if transition.trigger == 'auto':
672 elif transition.trigger == 'time':
673 launch_date = date + transition._delta()
676 launch_date = launch_date.strftime(DT_FMT)
679 'segment_id': workitem.segment_id.id,
680 'activity_id': transition.activity_to_id.id,
681 'partner_id': workitem.partner_id.id,
682 'res_id': workitem.res_id,
685 wi_id = self.create(cr, uid, values, context=context)
687 # Now, depending on the trigger and the campaign mode
688 # we know whether we must run the newly created workitem.
690 # rows = transition trigger \ colums = campaign mode
692 # test test_realtime manual normal (active)
698 run = (transition.trigger == 'auto' \
699 and campaign_mode != 'manual') \
700 or (transition.trigger == 'time' \
701 and campaign_mode == 'test')
703 new_wi = self.browse(cr, uid, wi_id, context)
704 self._process_one(cr, uid, new_wi, context)
707 tb = "".join(format_exception(*exc_info()))
708 workitem.write({'state': 'exception', 'error_msg': tb},
711 def process(self, cr, uid, workitem_ids, context=None):
712 for wi in self.browse(cr, uid, workitem_ids, context=context):
713 self._process_one(cr, uid, wi, context=context)
716 def process_all(self, cr, uid, camp_ids=None, context=None):
717 camp_obj = self.pool.get('marketing.campaign')
719 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
720 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
721 if camp.mode == 'manual':
722 # manual states are not processed automatically
725 domain = [('state', '=', 'todo'), ('date', '!=', False)]
726 if camp.mode in ('test_realtime', 'active'):
727 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
729 workitem_ids = self.search(cr, uid, domain, context=context)
733 self.process(cr, uid, workitem_ids, context=context)
736 def preview(self, cr, uid, ids, context=None):
738 wi_obj = self.browse(cr, uid, ids[0], context=context)
739 if wi_obj.activity_id.type == 'email':
740 data_obj = self.pool.get('ir.model.data')
741 data_id = data_obj._get_id(cr, uid, 'email_template', 'email_template_preview_form')
744 view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
746 'name': _('Email Preview'),
748 'view_mode': 'form,tree',
749 'res_model': 'email_template.preview',
752 'views': [(view_id, 'form')],
753 'type': 'ir.actions.act_window',
756 'context': "{'template_id':%d,'default_rel_model_ref':%d}"%
757 (wi_obj.activity_id.email_template_id.id,
761 elif wi_obj.activity_id.type == 'report':
763 'ids': [wi_obj.res_id],
764 'model': wi_obj.object_id.model
767 'type' : 'ir.actions.report.xml',
768 'report_name': wi_obj.activity_id.report_id.report_name,
772 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
775 marketing_campaign_workitem()
777 class email_template(osv.osv):
778 _inherit = "email.template"
780 'object_name': lambda obj, cr, uid, context: context.get('object_id',False),
783 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
787 class report_xml(osv.osv):
788 _inherit = 'ir.actions.report.xml'
789 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
792 object_id = context.get('object_id')
794 model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model
795 args.append(('model', '=', model))
796 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)