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 import api
35 from openerp.osv import fields, osv
36 from openerp.report import render_report
37 from openerp.tools.translate import _
40 'hours': lambda interval: relativedelta(hours=interval),
41 'days': lambda interval: relativedelta(days=interval),
42 'months': lambda interval: relativedelta(months=interval),
43 'years': lambda interval: relativedelta(years=interval),
46 DT_FMT = '%Y-%m-%d %H:%M:%S'
49 class marketing_campaign(osv.osv):
50 _name = "marketing.campaign"
51 _description = "Marketing Campaign"
53 def _count_segments(self, cr, uid, ids, field_name, arg, context=None):
56 for segments in self.browse(cr, uid, ids, context=context):
57 res[segments.id] = len(segments.segment_ids)
63 'name': fields.char('Name', required=True),
64 'object_id': fields.many2one('ir.model', 'Resource', required=True,
65 help="Choose the resource on which you want \
66 this campaign to be run"),
67 'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
68 domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
69 help="The generated workitems will be linked to the partner related to the record. "\
70 "If the record is the partner itself leave this field empty. "\
71 "This is useful for reporting purposes, via the Campaign Analysis or Campaign Follow-up views."),
72 'unique_field_id': fields.many2one('ir.model.fields', 'Unique Field',
73 domain="[('model_id', '=', object_id), ('ttype', 'in', ['char','int','many2one','text','selection'])]",
74 help='If set, this field will help segments that work in "no duplicates" mode to avoid '\
75 'selecting similar records twice. Similar records are records that have the same value for '\
76 'this unique field. For example by choosing the "email_from" field for CRM Leads you would prevent '\
77 'sending the same campaign to the same email address again. If not set, the "no duplicates" segments '\
78 "will only avoid selecting the same record again if it entered the campaign previously. "\
79 "Only easily comparable fields like textfields, integers, selections or single relationships may be used."),
80 'mode': fields.selection([('test', 'Test Directly'),
81 ('test_realtime', 'Test in Realtime'),
82 ('manual', 'With Manual Confirmation'),
83 ('active', 'Normal')],
84 'Mode', required=True, help= \
85 """Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
86 Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
87 With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
88 Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
89 'state': fields.selection([('draft', 'New'),
90 ('running', 'Running'),
91 ('cancelled', 'Cancelled'),
93 'Status', copy=False),
94 'activity_ids': fields.one2many('marketing.campaign.activity',
95 'campaign_id', 'Activities'),
96 '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')),
97 'segment_ids': fields.one2many('marketing.campaign.segment', 'campaign_id', 'Segments', readonly=False),
98 'segments_count': fields.function(_count_segments, type='integer', string='Segments')
102 'state': lambda *a: 'draft',
103 'mode': lambda *a: 'test',
106 def state_running_set(self, cr, uid, ids, *args):
107 # TODO check that all subcampaigns are running
108 campaign = self.browse(cr, uid, ids[0])
110 if not campaign.activity_ids:
111 raise osv.except_osv(_("Error"), _("The campaign cannot be started. There are no activities in it."))
114 has_signal_without_from = False
116 for activity in campaign.activity_ids:
119 if activity.signal and len(activity.from_ids) == 0:
120 has_signal_without_from = True
122 if not has_start and not has_signal_without_from:
123 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."))
125 return self.write(cr, uid, ids, {'state': 'running'})
127 def state_done_set(self, cr, uid, ids, *args):
128 # TODO check that this campaign is not a subcampaign in running mode.
129 segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
130 [('campaign_id', 'in', ids),
131 ('state', '=', 'running')])
133 raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are closed."))
134 self.write(cr, uid, ids, {'state': 'done'})
137 def state_cancel_set(self, cr, uid, ids, *args):
138 # TODO check that this campaign is not a subcampaign in running mode.
139 self.write(cr, uid, ids, {'state': 'cancelled'})
143 def signal(self, cr, uid, model, res_id, signal, run_existing=True, context=None):
144 record = self.pool[model].browse(cr, uid, res_id, context)
145 return self._signal(cr, uid, record, signal, run_existing, context)
148 def _signal(self, cr, uid, record, signal, run_existing=True, context=None):
150 raise ValueError('Signal cannot be False.')
152 Workitems = self.pool.get('marketing.campaign.workitem')
153 domain = [('object_id.model', '=', record._name),
154 ('state', '=', 'running')]
155 campaign_ids = self.search(cr, uid, domain, context=context)
156 for campaign in self.browse(cr, uid, campaign_ids, context=context):
157 for activity in campaign.activity_ids:
158 if activity.signal != signal:
161 data = dict(activity_id=activity.id,
164 wi_domain = [(k, '=', v) for k, v in data.items()]
166 wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
171 partner = self._get_partner_for(campaign, record)
173 data['partner_id'] = partner.id
174 wi_id = Workitems.create(cr, uid, data, context=context)
176 Workitems.process(cr, uid, wi_ids, context=context)
179 def _get_partner_for(self, campaign, record):
180 partner_field = campaign.partner_field_id.name
182 return getattr(record, partner_field)
183 elif campaign.object_id.model == 'res.partner':
187 # prevent duplication until the server properly duplicates several levels of nested o2m
188 def copy(self, cr, uid, id, default=None, context=None):
189 raise osv.except_osv(_("Operation not supported"), _("You cannot duplicate a campaign, Not supported yet."))
191 def _find_duplicate_workitems(self, cr, uid, record, campaign_rec, context=None):
192 """Finds possible duplicates workitems for a record in this campaign, based on a uniqueness
195 :param record: browse_record to find duplicates workitems for.
196 :param campaign_rec: browse_record of campaign
198 Workitems = self.pool.get('marketing.campaign.workitem')
199 duplicate_workitem_domain = [('res_id','=', record.id),
200 ('campaign_id','=', campaign_rec.id)]
201 unique_field = campaign_rec.unique_field_id
203 unique_value = getattr(record, unique_field.name, None)
205 if unique_field.ttype == 'many2one':
206 unique_value = unique_value.id
207 similar_res_ids = self.pool[campaign_rec.object_id.model].search(cr, uid,
208 [(unique_field.name, '=', unique_value)], context=context)
210 duplicate_workitem_domain = [('res_id','in', similar_res_ids),
211 ('campaign_id','=', campaign_rec.id)]
212 return Workitems.search(cr, uid, duplicate_workitem_domain, context=context)
216 class marketing_campaign_segment(osv.osv):
217 _name = "marketing.campaign.segment"
218 _description = "Campaign Segment"
221 def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
222 # next auto sync date is same for all segments
223 sync_job = self.pool.get('ir.model.data').get_object(cr, uid, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
224 next_sync = sync_job and sync_job.nextcall or False
225 return dict.fromkeys(ids, next_sync)
228 'name': fields.char('Name', required=True),
229 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
230 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
231 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
232 help="Filter to select the matching resource records that belong to this segment. "\
233 "New filters can be created and saved using the advanced search on the list view of the Resource. "\
234 "If no filter is set, all records are selected without filtering. "\
235 "The synchronization mode may also add a criterion to the filter."),
236 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
237 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
238 ('write_date', 'Only records modified after last sync (no duplicates)'),
239 ('all', 'All records (no duplicates)')],
240 'Synchronization mode',
241 help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign. "\
242 '"No duplicates" prevents selecting records which have already entered the campaign previously.'\
243 'If the campaign has a "unique field" set, "no duplicates" will also prevent selecting records which have '\
244 'the same value for the unique field as other records that already entered the campaign.'),
245 'state': fields.selection([('draft', 'New'),
246 ('cancelled', 'Cancelled'),
247 ('running', 'Running'),
249 'Status', copy=False),
250 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
251 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
252 'date_next_sync': fields.function(_get_next_sync, string='Next Synchronization', type='datetime', help="Next time the synchronization job is scheduled to run automatically"),
256 'state': lambda *a: 'draft',
257 'sync_mode': lambda *a: 'create_date',
260 def _check_model(self, cr, uid, ids, context=None):
261 for obj in self.browse(cr, uid, ids, context=context):
262 if not obj.ir_filter_id:
264 if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
269 (_check_model, 'Model of filter must be same as resource model of Campaign ', ['ir_filter_id,campaign_id']),
272 def onchange_campaign_id(self, cr, uid, ids, campaign_id):
273 res = {'domain':{'ir_filter_id':[]}}
274 campaign_pool = self.pool.get('marketing.campaign')
276 campaign = campaign_pool.browse(cr, uid, campaign_id)
277 model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
279 mod_name = model_name[0]['model']
280 res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
282 res['value'] = {'ir_filter_id': False}
285 def state_running_set(self, cr, uid, ids, *args):
286 segment = self.browse(cr, uid, ids[0])
287 vals = {'state': 'running'}
288 if not segment.date_run:
289 vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
290 self.write(cr, uid, ids, vals)
293 def state_done_set(self, cr, uid, ids, *args):
294 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
295 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
296 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
297 self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
300 def state_cancel_set(self, cr, uid, ids, *args):
301 wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
302 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
303 self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
304 self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
307 def synchroniz(self, cr, uid, ids, *args):
308 self.process_segment(cr, uid, ids)
311 @api.cr_uid_ids_context
312 def process_segment(self, cr, uid, segment_ids=None, context=None):
313 Workitems = self.pool.get('marketing.campaign.workitem')
314 Campaigns = self.pool.get('marketing.campaign')
316 segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
318 action_date = time.strftime('%Y-%m-%d %H:%M:%S')
320 for segment in self.browse(cr, uid, segment_ids, context=context):
321 if segment.campaign_id.state != 'running':
324 campaigns.add(segment.campaign_id.id)
325 act_ids = self.pool.get('marketing.campaign.activity').search(cr,
326 uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
328 model_obj = self.pool[segment.object_id.model]
330 if segment.sync_last_date and segment.sync_mode != 'all':
331 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
332 if segment.ir_filter_id:
333 criteria += eval(segment.ir_filter_id.domain)
334 object_ids = model_obj.search(cr, uid, criteria, context=context)
336 # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
337 for record in model_obj.browse(cr, uid, object_ids, context=context):
338 # avoid duplicate workitem for the same resource
339 if segment.sync_mode in ('write_date','all'):
340 if Campaigns._find_duplicate_workitems(cr, uid, record, segment.campaign_id, context=context):
344 'segment_id': segment.id,
350 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, record)
352 wi_vals['partner_id'] = partner.id
354 for act_id in act_ids:
355 wi_vals['activity_id'] = act_id
356 Workitems.create(cr, uid, wi_vals, context=context)
358 self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
359 Workitems.process_all(cr, uid, list(campaigns), context=context)
363 class marketing_campaign_activity(osv.osv):
364 _name = "marketing.campaign.activity"
366 _description = "Campaign Activity"
370 ('report', 'Report'),
371 ('action', 'Custom Action'),
372 # TODO implement the subcampaigns.
373 # TODO implement the subcampaign out. disallow out transitions from
374 # subcampaign activities ?
375 #('subcampaign', 'Sub-Campaign'),
379 'name': fields.char('Name', required=True),
380 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
381 required = True, ondelete='cascade', select=1),
382 'object_id': fields.related('campaign_id','object_id',
383 type='many2one', relation='ir.model',
384 string='Object', readonly=True),
385 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
386 'condition': fields.text('Condition', size=256, required=True,
387 help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
388 "The expression may use the following [browsable] variables:\n"
389 " - activity: the campaign activity\n"
390 " - workitem: the campaign workitem\n"
391 " - resource: the resource object this campaign item represents\n"
392 " - transitions: list of campaign transitions outgoing from this activity\n"
393 "...- re: Python regular expression module"),
394 'type': fields.selection(_action_types, 'Type', required=True,
395 help="""The type of action to execute when an item enters this activity, such as:
396 - Email: send an email using a predefined email template
397 - Report: print an existing Report defined on the resource item and save it into a specific directory
398 - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
400 'email_template_id': fields.many2one('email.template', "Email Template", help='The email to send when this activity is activated'),
401 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
402 'report_directory_id': fields.many2one('document.directory','Directory',
403 help="This folder is used to store the generated reports"),
404 'server_action_id': fields.many2one('ir.actions.server', string='Action',
405 help= "The action to perform when this activity is activated"),
406 'to_ids': fields.one2many('marketing.campaign.transition',
409 'from_ids': fields.one2many('marketing.campaign.transition',
411 'Previous Activities'),
412 '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')),
413 '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')),
414 'signal': fields.char('Signal',
415 help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
416 'keep_if_condition_not_met': fields.boolean("Don't Delete Workitems",
417 help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
421 'type': lambda *a: 'email',
422 'condition': lambda *a: 'True',
425 def search(self, cr, uid, args, offset=0, limit=None, order=None,
426 context=None, count=False):
429 if 'segment_id' in context and context['segment_id']:
430 segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
431 uid, context['segment_id'])
433 for activity in segment_obj.campaign_id.activity_ids:
434 act_ids.append(activity.id)
436 return super(marketing_campaign_activity, self).search(cr, uid, args,
437 offset, limit, order, context, count)
440 def _process_wi_report(self, cr, uid, activity, workitem, context=None):
441 report_data, format = render_report(cr, uid, [], activity.report_id.report_name, {}, context=context)
443 'name': '%s_%s_%s'%(activity.report_id.report_name,
444 activity.name,workitem.partner_id.name),
445 'datas_fname': '%s.%s'%(activity.report_id.report_name,
446 activity.report_id.report_type),
447 'parent_id': activity.report_directory_id.id,
448 'datas': base64.encodestring(report_data),
451 self.pool.get('ir.attachment').create(cr, uid, attach_vals)
454 def _process_wi_email(self, cr, uid, activity, workitem, context=None):
455 return self.pool.get('email.template').send_mail(cr, uid,
456 activity.email_template_id.id,
457 workitem.res_id, context=context)
460 def _process_wi_action(self, cr, uid, activity, workitem, context=None):
463 server_obj = self.pool.get('ir.actions.server')
465 action_context = dict(context,
466 active_id=workitem.res_id,
467 active_ids=[workitem.res_id],
468 active_model=workitem.object_id.model,
470 server_obj.run(cr, uid, [activity.server_action_id.id],
471 context=action_context)
474 def process(self, cr, uid, act_id, wi_id, context=None):
475 activity = self.browse(cr, uid, act_id, context=context)
476 method = '_process_wi_%s' % (activity.type,)
477 action = getattr(self, method, None)
479 raise NotImplementedError('Method %r is not implemented on %r object.' % (method, self))
481 workitem_obj = self.pool.get('marketing.campaign.workitem')
482 workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
483 return action(cr, uid, activity, workitem, context=context)
486 class marketing_campaign_transition(osv.osv):
487 _name = "marketing.campaign.transition"
488 _description = "Campaign Transition"
491 ('hours', 'Hour(s)'),
493 ('months', 'Month(s)'),
494 ('years', 'Year(s)'),
497 def _get_name(self, cr, uid, ids, fn, args, context=None):
498 # name formatters that depend on trigger
500 'auto': _('Automatic transition'),
501 'time': _('After %(interval_nbr)d %(interval_type)s'),
502 'cosmetic': _('Cosmetic'),
504 # get the translations of the values of selection field 'interval_type'
505 fields = self.fields_get(cr, uid, ['interval_type'], context=context)
506 interval_type_selection = dict(fields['interval_type']['selection'])
508 result = dict.fromkeys(ids, False)
509 for trans in self.browse(cr, uid, ids, context=context):
511 'interval_nbr': trans.interval_nbr,
512 'interval_type': interval_type_selection.get(trans.interval_type, ''),
514 result[trans.id] = formatters[trans.trigger] % values
518 def _delta(self, cr, uid, ids, context=None):
520 transition = self.browse(cr, uid, ids[0], context=context)
521 if transition.trigger != 'time':
522 raise ValueError('Delta is only relevant for timed transition.')
523 return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
527 'name': fields.function(_get_name, string='Name',
528 type='char', size=128),
529 'activity_from_id': fields.many2one('marketing.campaign.activity',
530 'Previous Activity', select=1,
531 required=True, ondelete="cascade"),
532 'activity_to_id': fields.many2one('marketing.campaign.activity',
534 required=True, ondelete="cascade"),
535 'interval_nbr': fields.integer('Interval Value', required=True),
536 'interval_type': fields.selection(_interval_units, 'Interval Unit',
539 'trigger': fields.selection([('auto', 'Automatic'),
541 ('cosmetic', 'Cosmetic'), # fake plastic transition
543 'Trigger', required=True,
544 help="How is the destination workitem triggered"),
549 'interval_type': 'days',
552 def _check_campaign(self, cr, uid, ids, context=None):
553 for obj in self.browse(cr, uid, ids, context=context):
554 if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
559 (_check_campaign, 'The To/From Activity of transition must be of the same Campaign ', ['activity_from_id,activity_to_id']),
563 ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
567 class marketing_campaign_workitem(osv.osv):
568 _name = "marketing.campaign.workitem"
569 _description = "Campaign Workitem"
571 def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
572 res = dict.fromkeys(ids, '/')
573 for wi in self.browse(cr, uid, ids, context=context):
577 proxy = self.pool[wi.object_id.model]
578 if not proxy.exists(cr, uid, [wi.res_id]):
580 ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
582 res[wi.id] = ng[0][1]
585 def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
586 """Returns id of workitem whose resource_name matches with the given name"""
590 condition_name = None
591 for domain_item in args:
592 # we only use the first domain criterion and ignore all the rest including operators
593 if isinstance(domain_item, (list,tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_name':
594 condition_name = [None, domain_item[1], domain_item[2]]
597 assert condition_name, "Invalid search domain for marketing_campaign_workitem.res_name. It should use 'res_name'"
599 cr.execute("""select w.id, w.res_id, m.model \
600 from marketing_campaign_workitem w \
601 left join marketing_campaign_activity a on (a.id=w.activity_id)\
602 left join marketing_campaign c on (c.id=a.campaign_id)\
603 left join ir_model m on (m.id=c.object_id)
607 matching_workitems = []
608 for id, res_id, model in res:
609 workitem_map.setdefault(model,{}).setdefault(res_id,set()).add(id)
610 for model, id_map in workitem_map.iteritems():
611 model_pool = self.pool[model]
612 condition_name[0] = model_pool._rec_name
613 condition = [('id', 'in', id_map.keys()), condition_name]
614 for res_id in model_pool.search(cr, uid, condition, context=context):
615 matching_workitems.extend(id_map[res_id])
616 return [('id', 'in', list(set(matching_workitems)))]
619 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
620 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
621 required=True, readonly=True),
622 'campaign_id': fields.related('activity_id', 'campaign_id',
623 type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
624 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
625 type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
626 'res_id': fields.integer('Resource ID', select=1, readonly=True),
627 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
628 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
629 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
630 'state': fields.selection([ ('todo', 'To Do'),
631 ('cancelled', 'Cancelled'),
632 ('exception', 'Exception'),
634 ], 'Status', readonly=True, copy=False),
635 'error_msg' : fields.text('Error Message', readonly=True)
638 'state': lambda *a: 'todo',
642 @api.cr_uid_ids_context
643 def button_draft(self, cr, uid, workitem_ids, context=None):
644 for wi in self.browse(cr, uid, workitem_ids, context=context):
645 if wi.state in ('exception', 'cancelled'):
646 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
649 @api.cr_uid_ids_context
650 def button_cancel(self, cr, uid, workitem_ids, context=None):
651 for wi in self.browse(cr, uid, workitem_ids, context=context):
652 if wi.state in ('todo','exception'):
653 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
656 def _process_one(self, cr, uid, workitem, context=None):
657 if workitem.state != 'todo':
660 activity = workitem.activity_id
661 proxy = self.pool[workitem.object_id.model]
662 object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
665 'activity': activity,
666 'workitem': workitem,
668 'resource': object_id,
669 'transitions': activity.to_ids,
673 condition = activity.condition
674 campaign_mode = workitem.campaign_id.mode
676 if not eval(condition, eval_context):
677 if activity.keep_if_condition_not_met:
678 workitem.write({'state': 'cancelled'})
683 if campaign_mode in ('manual', 'active'):
684 Activities = self.pool.get('marketing.campaign.activity')
685 result = Activities.process(cr, uid, activity.id, workitem.id,
688 values = dict(state='done')
689 if not workitem.date:
690 values['date'] = datetime.now().strftime(DT_FMT)
691 workitem.write(values)
695 workitem.refresh() # reload
696 date = datetime.strptime(workitem.date, DT_FMT)
698 for transition in activity.to_ids:
699 if transition.trigger == 'cosmetic':
702 if transition.trigger == 'auto':
704 elif transition.trigger == 'time':
705 launch_date = date + transition._delta()
708 launch_date = launch_date.strftime(DT_FMT)
711 'segment_id': workitem.segment_id.id,
712 'activity_id': transition.activity_to_id.id,
713 'partner_id': workitem.partner_id.id,
714 'res_id': workitem.res_id,
717 wi_id = self.create(cr, uid, values, context=context)
719 # Now, depending on the trigger and the campaign mode
720 # we know whether we must run the newly created workitem.
722 # rows = transition trigger \ colums = campaign mode
724 # test test_realtime manual normal (active)
730 run = (transition.trigger == 'auto' \
731 and campaign_mode != 'manual') \
732 or (transition.trigger == 'time' \
733 and campaign_mode == 'test')
735 new_wi = self.browse(cr, uid, wi_id, context)
736 self._process_one(cr, uid, new_wi, context)
739 tb = "".join(format_exception(*exc_info()))
740 workitem.write({'state': 'exception', 'error_msg': tb})
742 @api.cr_uid_ids_context
743 def process(self, cr, uid, workitem_ids, context=None):
744 for wi in self.browse(cr, uid, workitem_ids, context=context):
745 self._process_one(cr, uid, wi, context=context)
748 def process_all(self, cr, uid, camp_ids=None, context=None):
749 camp_obj = self.pool.get('marketing.campaign')
751 camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
752 for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
753 if camp.mode == 'manual':
754 # manual states are not processed automatically
757 domain = [('campaign_id', '=', camp.id), ('state', '=', 'todo'), ('date', '!=', False)]
758 if camp.mode in ('test_realtime', 'active'):
759 domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
761 workitem_ids = self.search(cr, uid, domain, context=context)
765 self.process(cr, uid, workitem_ids, context=context)
768 def preview(self, cr, uid, ids, context=None):
770 wi_obj = self.browse(cr, uid, ids[0], context=context)
771 if wi_obj.activity_id.type == 'email':
772 view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'email_template', 'email_template_preview_form')
774 'name': _('Email Preview'),
776 'view_mode': 'form,tree',
777 'res_model': 'email_template.preview',
780 'views': [(view_id and view_id[1] or 0, 'form')],
781 'type': 'ir.actions.act_window',
784 'context': "{'template_id':%d,'default_res_id':%d}"%
785 (wi_obj.activity_id.email_template_id.id,
789 elif wi_obj.activity_id.type == 'report':
791 'ids': [wi_obj.res_id],
792 'model': wi_obj.object_id.model
795 'type' : 'ir.actions.report.xml',
796 'report_name': wi_obj.activity_id.report_id.report_name,
800 raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
804 class email_template(osv.osv):
805 _inherit = "email.template"
807 'model_id': lambda obj, cr, uid, context: context.get('object_id',False),
810 # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
813 class report_xml(osv.osv):
814 _inherit = 'ir.actions.report.xml'
815 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
818 object_id = context.get('object_id')
820 model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model
821 args.append(('model', '=', model))
822 return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)
826 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: