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