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 ##############################################################################
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 tools.safe_eval import safe_eval as eval
32 from decimal_precision import decimal_precision as dp
34 from osv import fields, osv
36 from 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('Purchase 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 doesn't 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.get(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 can not duplicate a campaign, it's 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.get(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)
242 class marketing_campaign_segment(osv.osv):
243 _name = "marketing.campaign.segment"
244 _description = "Campaign Segment"
247 def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
248 # next auto sync date is same for all segments
249 sync_job = self.pool.get('ir.model.data').get_object(cr, uid, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
250 next_sync = sync_job and sync_job.nextcall or False
251 return dict.fromkeys(ids, next_sync)
254 'name': fields.char('Name', size=64,required=True),
255 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
256 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
257 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
258 help="Filter to select the matching resource records that belong to this segment. "\
259 "New filters can be created and saved using the advanced search on the list view of the Resource. "\
260 "If no filter is set, all records are selected without filtering. "\
261 "The synchronization mode may also add a criterion to the filter."),
262 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
263 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
264 ('write_date', 'Only records modified after last sync (no duplicates)'),
265 ('all', 'All records (no duplicates)')],
266 'Synchronization mode',
267 help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign. "\
268 '"No duplicates" prevents selecting records which have already entered the campaign previously.'\
269 'If the campaign has a "unique field" set, "no duplicates" will also prevent selecting records which have '\
270 'the same value for the unique field as other records that already entered the campaign.'),
271 'state': fields.selection([('draft', 'New'),
272 ('cancelled', 'Cancelled'),
273 ('running', 'Running'),
276 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
277 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
278 'date_next_sync': fields.function(_get_next_sync, string='Next Synchronization', type='datetime', help="Next time the synchronization job is scheduled to run automatically"),
282 'state': lambda *a: 'draft',
283 'sync_mode': lambda *a: 'create_date',
286 def _check_model(self, cr, uid, ids, context=None):
287 for obj in self.browse(cr, uid, ids, context=context):
288 if not obj.ir_filter_id:
290 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
295 (_check_model, 'Model of filter must be same as resource model of Campaign ', ['ir_filter_id,campaign_id']),
298 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
299 res = {'domain':{'ir_filter_id':[]}}
300 campaign_pool = self.pool.get('marketing.campaign')
302 campaign = campaign_pool.browse(cr, uid, campaign_id)
303 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
305 mod_name = model_name[0]['model']
306 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
308 res['value'] = {'ir_filter_id': False}
311 def state_running_set(self, cr, uid, ids, *args):
312 segment = self.browse(cr, uid, ids[0])
313 vals = {'state': 'running'}
314 if not segment.date_run:
315 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
316 self.write(cr, uid, ids, vals)
319 def state_done_set(self, cr, uid, ids, *args):
320 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
321 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
322 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
323 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
326 def state_cancel_set(self, cr, uid, ids, *args):
327 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
328 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
329 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
330 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
333 def synchroniz(self, cr, uid, ids, *args):
334 self.process_segment(cr, uid, ids)
337 def process_segment(self, cr, uid, segment_ids=None, context=None):
338 Workitems = self.pool.get('marketing.campaign.workitem')
339 Campaigns = self.pool.get('marketing.campaign')
341 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
343 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
345 for segment in self.browse(cr, uid, segment_ids, context=context):
346 if segment.campaign_id.state != 'running':
349 campaigns.add(segment.campaign_id.id)
350 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
351 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
353 model_obj = self.pool.get(segment.object_id.model)
355 if segment.sync_last_date and segment.sync_mode != 'all':
356 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
357 if segment.ir_filter_id:
358 criteria += eval(segment.ir_filter_id.domain)
359 object_ids = model_obj.search(cr, uid, criteria, context=context)
361 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
362 for record in model_obj.browse(cr, uid, object_ids, context=context):
363 # avoid duplicate workitem for the same resource
364 if segment.sync_mode in ('write_date','all'):
365 if Campaigns._find_duplicate_workitems(cr, uid, record, segment.campaign_id, context=context):
369 'segment_id': segment.id,
375 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, record)
377 wi_vals['partner_id'] = partner.id
379 for act_id in act_ids:
380 wi_vals['activity_id'] = act_id
381 Workitems.create(cr, uid, wi_vals, context=context)
383 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
384 Workitems.process_all(cr, uid, list(campaigns), context=context)
387 marketing_campaign_segment()
389 class marketing_campaign_activity(osv.osv):
390 _name = "marketing.campaign.activity"
392 _description = "Campaign Activity"
396 ('report', 'Report'),
397 ('action', 'Custom Action'),
398 # TODO implement the subcampaigns.
399 # TODO implement the subcampaign out. disallow out transitions from
400 # subcampaign activities ?
401 #('subcampaign', 'Sub-Campaign'),
405 'name': fields.char('Name', size=128, required=True),
406 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
407 required = True, ondelete='cascade', select=1),
408 'object_id': fields.related('campaign_id','object_id',
409 type='many2one', relation='ir.model',
410 string='Object', readonly=True),
411 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
412 'condition': fields.text('Condition', size=256, required=True,
413 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
414 "The expression may use the following [browsable] variables:\n"
415 " - activity: the campaign activity\n"
416 " - workitem: the campaign workitem\n"
417 " - resource: the resource object this campaign item represents\n"
418 " - transitions: list of campaign transitions outgoing from this activity\n"
419 "...- re: Python regular expression module"),
420 'type': fields.selection(_action_types, 'Type', required=True,
421 help="""The type of action to execute when an item enters this activity, such as:
422 - Email: send an email using a predefined email template
423 - Report: print an existing Report defined on the resource item and save it into a specific directory
424 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
426 'email_template_id': fields.many2one('email.template', "Email Template", help='The email to send when this activity is activated'),
427 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
428 'report_directory_id': fields.many2one('document.directory','Directory',
429 help="This folder is used to store the generated reports"),
430 'server_action_id': fields.many2one('ir.actions.server', string='Action',
431 help= "The action to perform when this activity is activated"),
432 'to_ids': fields.one2many('marketing.campaign.transition',
435 'from_ids': fields.one2many('marketing.campaign.transition',
437 'Previous Activities'),
438 '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')),
439 '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')),
440 'signal': fields.char('Signal', size=128,
441 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
442 'keep_if_condition_not_met': fields.boolean("Don't Delete Workitems",
443 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
447 'type': lambda *a: 'email',
448 'condition': lambda *a: 'True',
451 def search(self, cr, uid, args, offset=0, limit=None, order=None,
452 context=None, count=False):
455 if 'segment_id' in context and context['segment_id']:
456 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
457 uid, context['segment_id'])
459 for activity in segment_obj.campaign_id.activity_ids:
460 act_ids.append(activity.id)
462 return super(marketing_campaign_activity, self).search(cr, uid, args,
463 offset, limit, order, context, count)
466 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
467 service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
468 (report_data, format) = service.create(cr, uid, [], {}, {})
470 'name': '%s_%s_%s'%(activity.report_id.report_name,
471 activity.name,workitem.partner_id.name),
472 'datas_fname': '%s.%s'%(activity.report_id.report_name,
473 activity.report_id.report_type),
474 'parent_id': activity.report_directory_id.id,
475 'datas': base64.encodestring(report_data),
478 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
481 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
482 return self.pool.get('email.template').send_mail(cr, uid,
483 activity.email_template_id.id,
484 workitem.res_id, context=context)
487 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
490 server_obj = self.pool.get('ir.actions.server')
492 action_context = dict(context,
493 active_id=workitem.res_id,
494 active_ids=[workitem.res_id],
495 active_model=workitem.object_id.model,
497 res = server_obj.run(cr, uid, [activity.server_action_id.id],
498 context=action_context)
499 # server action return False if the action is performed
500 # except client_action, other and python code
501 return res == False and True or res
503 def process(self, cr, uid, act_id, wi_id, context=None):
504 activity = self.browse(cr, uid, act_id, context=context)
505 method = '_process_wi_%s' % (activity.type,)
506 action = getattr(self, method, None)
508 raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
510 workitem_obj = self.pool.get('marketing.campaign.workitem')
511 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
512 return action(cr, uid, activity, workitem, context=context)
514 marketing_campaign_activity()
516 class marketing_campaign_transition(osv.osv):
517 _name = "marketing.campaign.transition"
518 _description = "Campaign Transition"
521 ('hours', 'Hour(s)'), ('days', 'Day(s)'),
522 ('months', 'Month(s)'), ('years','Year(s)')
525 def _get_name(self, cr, uid, ids, fn, args, context=None):
526 result = dict.fromkeys(ids, False)
528 'auto': _('Automatic transition'),
529 'time': _('After %(interval_nbr)d %(interval_type)s'),
530 'cosmetic': _('Cosmetic'),
532 for tr in self.browse(cr, uid, ids, context=context,
533 fields_process=translate_selections):
534 result[tr.id] = formatters[tr.trigger.value] % tr
538 def _delta(self, cr, uid, ids, context=None):
540 transition = self.browse(cr, uid, ids[0], context=context)
541 if transition.trigger != 'time':
542 raise ValueError('Delta is only relevant for timed transiton')
543 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
547 'name': fields.function(_get_name, string='Name',
548 type='char', size=128),
549 'activity_from_id': fields.many2one('marketing.campaign.activity',
550 'Previous Activity', select=1,
551 required=True, ondelete="cascade"),
552 'activity_to_id': fields.many2one('marketing.campaign.activity',
554 required=True, ondelete="cascade"),
555 'interval_nbr': fields.integer('Interval Value', required=True),
556 'interval_type': fields.selection(_interval_units, 'Interval Unit',
559 'trigger': fields.selection([('auto', 'Automatic'),
561 ('cosmetic', 'Cosmetic'), # fake plastic transition
563 'Trigger', required=True,
564 help="How is the destination workitem triggered"),
569 'interval_type': 'days',
572 def _check_campaign(self, cr, uid, ids, context=None):
573 for obj in self.browse(cr, uid, ids, context=context):
574 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
579 (_check_campaign, 'The To/From Activity of transition must be of the same Campaign ', ['activity_from_id,activity_to_id']),
583 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
586 marketing_campaign_transition()
588 class marketing_campaign_workitem(osv.osv):
589 _name = "marketing.campaign.workitem"
590 _description = "Campaign Workitem"
592 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
593 res = dict.fromkeys(ids, '/')
594 for wi in self.browse(cr, uid, ids, context=context):
598 proxy = self.pool.get(wi.object_id.model)
599 if not proxy.exists(cr, uid, [wi.res_id]):
601 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
603 res[wi.id] = ng[0][1]
606 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
607 """Returns id of workitem whose resource_name matches with the given name"""
611 condition_name = None
612 for domain_item in args:
613 # we only use the first domain criterion and ignore all the rest including operators
614 if isinstance(domain_item, (list,tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_name':
615 condition_name = [None, domain_item[1], domain_item[2]]
618 assert condition_name, "Invalid search domain for marketing_campaign_workitem.res_name. It should use 'res_name'"
620 cr.execute("""select w.id, w.res_id, m.model \
621 from marketing_campaign_workitem w \
622 left join marketing_campaign_activity a on (a.id=w.activity_id)\
623 left join marketing_campaign c on (c.id=a.campaign_id)\
624 left join ir_model m on (m.id=c.object_id)
628 matching_workitems = []
629 for id, res_id, model in res:
630 workitem_map.setdefault(model,{}).setdefault(res_id,set()).add(id)
631 for model, id_map in workitem_map.iteritems():
632 model_pool = self.pool.get(model)
633 condition_name[0] = model_pool._rec_name
634 condition = [('id', 'in', id_map.keys()), condition_name]
635 for res_id in model_pool.search(cr, uid, condition, context=context):
636 matching_workitems.extend(id_map[res_id])
637 return [('id', 'in', list(set(matching_workitems)))]
640 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
641 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
642 required=True, readonly=True),
643 'campaign_id': fields.related('activity_id', 'campaign_id',
644 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
645 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
646 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
647 'res_id': fields.integer('Resource ID', select=1, readonly=True),
648 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
649 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
650 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
651 'state': fields.selection([ ('todo', 'To Do'),
652 ('cancelled', 'Cancelled'),
653 ('exception', 'Exception'),
655 ], 'Status', readonly=True),
656 'error_msg' : fields.text('Error Message', readonly=True)
659 'state': lambda *a: 'todo',
663 def button_draft(self, cr, uid, workitem_ids, context=None):
664 for wi in self.browse(cr, uid, workitem_ids, context=context):
665 if wi.state in ('exception', 'cancelled'):
666 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
669 def button_cancel(self, cr, uid, workitem_ids, context=None):
670 for wi in self.browse(cr, uid, workitem_ids, context=context):
671 if wi.state in ('todo','exception'):
672 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
675 def _process_one(self, cr, uid, workitem, context=None):
676 if workitem.state != 'todo':
679 activity = workitem.activity_id
680 proxy = self.pool.get(workitem.object_id.model)
681 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
684 'activity': activity,
685 'workitem': workitem,
687 'resource': object_id,
688 'transitions': activity.to_ids,
692 condition = activity.condition
693 campaign_mode = workitem.campaign_id.mode
695 if not eval(condition, eval_context):
696 if activity.keep_if_condition_not_met:
697 workitem.write({'state': 'cancelled'}, context=context)
699 workitem.unlink(context=context)
702 if campaign_mode in ('manual', 'active'):
703 Activities = self.pool.get('marketing.campaign.activity')
704 result = Activities.process(cr, uid, activity.id, workitem.id,
707 values = dict(state='done')
708 if not workitem.date:
709 values['date'] = datetime.now().strftime(DT_FMT)
710 workitem.write(values, context=context)
714 workitem = workitem.browse(context=context)[0] # reload
715 date = datetime.strptime(workitem.date, DT_FMT)
717 for transition in activity.to_ids:
718 if transition.trigger == 'cosmetic':
721 if transition.trigger == 'auto':
723 elif transition.trigger == 'time':
724 launch_date = date + transition._delta()
727 launch_date = launch_date.strftime(DT_FMT)
730 'segment_id': workitem.segment_id.id,
731 'activity_id': transition.activity_to_id.id,
732 'partner_id': workitem.partner_id.id,
733 'res_id': workitem.res_id,
736 wi_id = self.create(cr, uid, values, context=context)
738 # Now, depending on the trigger and the campaign mode
739 # we know whether we must run the newly created workitem.
741 # rows = transition trigger \ colums = campaign mode
743 # test test_realtime manual normal (active)
749 run = (transition.trigger == 'auto' \
750 and campaign_mode != 'manual') \
751 or (transition.trigger == 'time' \
752 and campaign_mode == 'test')
754 new_wi = self.browse(cr, uid, wi_id, context)
755 self._process_one(cr, uid, new_wi, context)
758 tb = "".join(format_exception(*exc_info()))
759 workitem.write({'state': 'exception', 'error_msg': tb},
762 def process(self, cr, uid, workitem_ids, context=None):
763 for wi in self.browse(cr, uid, workitem_ids, context=context):
764 self._process_one(cr, uid, wi, context=context)
767 def process_all(self, cr, uid, camp_ids=None, context=None):
768 camp_obj = self.pool.get('marketing.campaign')
770 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
771 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
772 if camp.mode == 'manual':
773 # manual states are not processed automatically
776 domain = [('campaign_id', '=', camp.id), ('state', '=', 'todo'), ('date', '!=', False)]
777 if camp.mode in ('test_realtime', 'active'):
778 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
780 workitem_ids = self.search(cr, uid, domain, context=context)
784 self.process(cr, uid, workitem_ids, context=context)
787 def preview(self, cr, uid, ids, context=None):
789 wi_obj = self.browse(cr, uid, ids[0], context=context)
790 if wi_obj.activity_id.type == 'email':
791 view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'email_template', 'email_template_preview_form')
793 'name': _('Email Preview'),
795 'view_mode': 'form,tree',
796 'res_model': 'email_template.preview',
799 'views': [(view_id and view_id[1] or 0, 'form')],
800 'type': 'ir.actions.act_window',
803 'context': "{'template_id':%d,'default_res_id':%d}"%
804 (wi_obj.activity_id.email_template_id.id,
808 elif wi_obj.activity_id.type == 'report':
810 'ids': [wi_obj.res_id],
811 'model': wi_obj.object_id.model
814 'type' : 'ir.actions.report.xml',
815 'report_name': wi_obj.activity_id.report_id.report_name,
819 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
822 marketing_campaign_workitem()
824 class email_template(osv.osv):
825 _inherit = "email.template"
827 'model_id': lambda obj, cr, uid, context: context.get('object_id',False),
830 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
834 class report_xml(osv.osv):
835 _inherit = 'ir.actions.report.xml'
836 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
839 object_id = context.get('object_id')
841 model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model
842 args.append(('model', '=', model))
843 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)
848 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: