[MOD] event module should be in marketing application IF Association is not installed...
[odoo/odoo.git] / addons / marketing_campaign / marketing_campaign.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 OpenERP SA (<http://openerp.com>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import time
23 import base64
24 from datetime import datetime
25 from dateutil.relativedelta import relativedelta
26 from operator import itemgetter
27 from traceback import format_exception
28 from sys import exc_info
29 from tools.safe_eval import safe_eval as eval
30
31 from osv import fields, osv
32 import netsvc
33 from tools.translate import _
34
35 _intervalTypes = {
36     'hours': lambda interval: relativedelta(hours=interval),
37     'days': lambda interval: relativedelta(days=interval),
38     'months': lambda interval: relativedelta(months=interval),
39     'years': lambda interval: relativedelta(years=interval),
40 }
41
42 DT_FMT = '%Y-%m-%d %H:%M:%S'
43
44 def dict_map(f, d):
45     return dict((k, f(v)) for k,v in d.items())
46
47 def _find_fieldname(model, field):
48     inherit_columns = dict_map(itemgetter(2), model._inherit_fields)
49     all_columns = dict(inherit_columns, **model._columns)
50     for fn in all_columns:
51         if all_columns[fn] is field:
52             return fn
53     raise ValueError('field not found: %r' % (field,))
54
55 class selection_converter(object):
56     """Format the selection in the browse record objects"""
57     def __init__(self, value):
58         self._value = value
59         self._str = value
60
61     def set_value(self, cr, uid, _self_again, record, field, lang):
62         # this design is terrible
63         # search fieldname from the field
64         fieldname = _find_fieldname(record._table, field)
65         context = dict(lang=lang.code)
66         fg = record._table.fields_get(cr, uid, [fieldname], context=context)
67         selection = dict(fg[fieldname]['selection'])
68         self._str = selection[self.value]
69
70     @property
71     def value(self):
72         return self._value
73
74     def __str__(self):
75         return self._str
76
77 translate_selections = {
78     'selection': selection_converter,
79 }
80
81
82 class marketing_campaign(osv.osv):
83     _name = "marketing.campaign"
84     _description = "Marketing Campaign"
85
86     _columns = {
87         'name': fields.char('Name', size=64, required=True),
88         'object_id': fields.many2one('ir.model', 'Model', required=True,
89                                       help="Choose the model on which you want \
90 this campaign to be run"),
91         'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
92                                             domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
93                                             help="The generated workitems will be linked to the partner related to the record. If the record is the partner itself left this field empty."),
94         'mode': fields.selection([('test', 'Test Directly'),
95                                 ('test_realtime', 'Test in Realtime'),
96                                 ('manual', 'With Manual Confirmation'),
97                                 ('active', 'Normal')],
98                                  'Mode', required=True, help= \
99 """Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
100 Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
101 With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
102 Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
103         'state': fields.selection([('draft', 'Draft'),
104                                    ('running', 'Running'),
105                                    ('done', 'Done'),
106                                    ('cancelled', 'Cancelled'),],
107                                    'State',),
108         'activity_ids': fields.one2many('marketing.campaign.activity',
109                                        'campaign_id', 'Activities'),
110         'fixed_cost': fields.float('Fixed Cost', help="Fixed cost for the campaign (used for campaign analysis), see also variable cost on activities"),
111     }
112     
113     _defaults = {
114         'state': lambda *a: 'draft',
115         'mode': lambda *a: 'test',
116     }
117
118     def state_running_set(self, cr, uid, ids, *args):
119         # TODO check that all subcampaigns are running
120         campaign = self.browse(cr, uid, ids[0])
121
122         if not campaign.activity_ids:
123             raise osv.except_osv(_("Error"), _("The campaign cannot be started: there are no activities in it"))
124
125         has_start = False
126         has_signal_without_from = False
127
128         for activity in campaign.activity_ids:
129             if activity.start:
130                 has_start = True
131             if activity.signal and len(activity.from_ids) == 0:
132                 has_signal_without_from = True
133
134             if activity.type != 'email':
135                 continue
136             if not activity.email_template_id.enforce_from_account:
137                 raise osv.except_osv(_("Error"), _("The campaign cannot be started: an email account is missing in the email activity '%s'")%activity.name)
138             if activity.email_template_id.enforce_from_account.state != 'approved':
139                 raise osv.except_osv(_("Error"), _("The campaign cannot be started: the email account is not approved in the email activity '%s'")%activity.name)
140
141         if not has_start and not has_signal_without_from:
142             raise osv.except_osv(_("Error"), _("The campaign hasn't any starting activity nor any activity with a signal and no previous activity."))
143
144         return self.write(cr, uid, ids, {'state': 'running'})
145
146     def state_done_set(self, cr, uid, ids, *args):
147         # TODO check that this campaign is not a subcampaign in running mode.
148         segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
149                                             [('campaign_id', 'in', ids),
150                                             ('state', '=', 'running')])
151         if segment_ids :
152             raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are done"))
153         self.write(cr, uid, ids, {'state': 'done'})
154         return True
155
156     def state_cancel_set(self, cr, uid, ids, *args):
157         # TODO check that this campaign is not a subcampaign in running mode.
158         self.write(cr, uid, ids, {'state': 'cancelled'})
159         return True
160
161
162     def signal(self, cr, uid, model, res_id, signal, context=None):
163         record = self.pool.get(model).browse(cr, uid, res_id, context)
164         return self._signal(cr, uid, record, signal, context)
165
166     def _signal(self, cr, uid, record, signal, context=None):
167         if not signal:
168             raise ValueError('signal cannot be False')
169
170         Workitems = self.pool.get('marketing.campaign.workitem')
171         domain = [('object_id.model', '=', record._table._name),
172                   ('state', '=', 'running')]
173         campaign_ids = self.search(cr, uid, domain, context=context)
174         for campaign in self.browse(cr, uid, campaign_ids, context):
175             for activity in campaign.activity_ids:
176                 if activity.signal != signal:
177                     continue
178
179                 data = dict(activity_id=activity.id,
180                             res_id=record.id,
181                             state='todo')
182                 wi_domain = [(k, '=', v) for k, v in data.items()]
183
184                 wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
185                 if not wi_ids:
186                     partner = self._get_partner_for(campaign, record)
187                     if partner:
188                         data['partner_id'] = partner.id
189                     wi_id = Workitems.create(cr, uid, data, context=context)
190                     wi_ids = [wi_id]
191                 Workitems.process(cr, uid, wi_ids, context=context)
192         return True
193
194     def _get_partner_for(self, campaign, record):
195         partner_field = campaign.partner_field_id.name
196         if partner_field:
197             return getattr(record, partner_field)
198         elif campaign.object_id.model == 'res.partner':
199             return record
200         return None
201
202 marketing_campaign()
203
204 class marketing_campaign_segment(osv.osv):
205     _name = "marketing.campaign.segment"
206     _description = "Campaign Segment"
207
208     _columns = {
209         'name': fields.char('Name', size=64,required=True),
210         'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
211              required=True, select=1),
212         'object_id': fields.related('campaign_id','object_id',
213                                       type='many2one', relation='ir.model',
214                                       string='Object'),
215         'ir_filter_id': fields.many2one('ir.filters', 'Filter', help=""),
216         'sync_last_date': fields.datetime('Latest Synchronization'),
217         'sync_mode': fields.selection([('create_date', 'If record created after last sync'),
218                                       ('write_date', 'If record modified after last sync (no duplicates)')],
219                                       'Workitem creation mode',
220                                       help="Determines when new workitems should be created for records matching a segment."),
221         'state': fields.selection([('draft', 'Draft'),
222                                    ('running', 'Running'),
223                                    ('done', 'Done'),
224                                    ('cancelled', 'Cancelled')],
225                                    'State',),
226         'date_run': fields.datetime('Launching Date'),
227         'date_done': fields.datetime('End Date'),
228     }
229
230     _defaults = {
231         'state': lambda *a: 'draft',
232         'sync_mode': lambda *a: 'create_date',
233     }
234
235     def state_running_set(self, cr, uid, ids, *args):
236         segment = self.browse(cr, uid, ids[0])
237         vals = {'state': 'running'}
238         if not segment.date_run:
239             vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
240         self.write(cr, uid, ids, vals)
241         return True
242
243     def state_done_set(self, cr, uid, ids, *args):
244         wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
245                                 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
246         self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
247         self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
248         return True
249
250     def state_cancel_set(self, cr, uid, ids, *args):
251         wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
252                                 [('state', '=', 'todo'), ('segment_id', 'in', ids)])
253         self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
254         self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
255         return True
256
257     def synchroniz(self, cr, uid, ids, *args):
258         self.process_segment(cr, uid, ids)
259         return True
260
261     def process_segment(self, cr, uid, segment_ids=None, context=None):
262         Workitems = self.pool.get('marketing.campaign.workitem')
263         if not segment_ids:
264             segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
265
266         action_date = time.strftime('%Y-%m-%d %H:%M:%S')
267         campaigns = set()
268         for segment in self.browse(cr, uid, segment_ids, context=context):
269             campaigns.add(segment.campaign_id.id)
270             act_ids = self.pool.get('marketing.campaign.activity').search(cr,
271                   uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
272
273             model_obj = self.pool.get(segment.object_id.model)
274             criteria = []
275             if segment.sync_last_date:
276                 criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
277             if segment.ir_filter_id:
278                 criteria += eval(segment.ir_filter_id.domain)
279             object_ids = model_obj.search(cr, uid, criteria, context=context)
280
281             # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
282             for o_ids in model_obj.browse(cr, uid, object_ids, context=context):
283                 # avoid duplicated workitem for the same resource
284                 if segment.sync_mode == 'write_date':
285                     wi_ids = Workitems.search(cr, uid, [('res_id','=',o_ids.id),('segment_id','=',segment.id)], context=context)
286                     if wi_ids:
287                         continue
288
289                 wi_vals = {
290                     'segment_id': segment.id,
291                     'date': action_date,
292                     'state': 'todo',
293                     'res_id': o_ids.id
294                 }
295
296                 partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, o_ids)
297                 if partner:
298                     wi_vals['partner_id'] = partner.id
299
300                 for act_id in act_ids:
301                     wi_vals['activity_id'] = act_id
302                     Workitems.create(cr, uid, wi_vals, context=context)
303
304             self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
305         Workitems.process_all(cr, uid, list(campaigns), context=context)
306         return True
307
308 marketing_campaign_segment()
309
310 class marketing_campaign_activity(osv.osv):
311     _name = "marketing.campaign.activity"
312     _description = "Campaign Activity"
313
314     _action_types = [
315         ('email', 'E-mail'),
316         ('paper', 'Paper'),
317         ('action', 'Action'),
318         # TODO implement the subcampaigns.
319         # TODO implement the subcampaign out. disallow out transitions from
320         # subcampaign activities ?
321         #('subcampaign', 'Sub-Campaign'),
322     ]
323
324     _columns = {
325         'name': fields.char('Name', size=128, required=True),
326         'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
327                                             required = True, ondelete='cascade', select=1),
328         'object_id': fields.related('campaign_id','object_id',
329                                       type='many2one', relation='ir.model',
330                                       string='Object', readonly=True),
331         'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
332         'condition': fields.char('Condition', size=256, required=True,
333                                  help="Python condition to know if the activity can be executed, otherwise it will be deleted or cancelled."),
334         'type': fields.selection(_action_types, 'Type', required=True,
335                                   help="Describe type of action to be performed on the Activity.Eg : Send email,Send paper.."),
336         'email_template_id': fields.many2one('email.template','Email Template'),
337         'report_id': fields.many2one('ir.actions.report.xml', 'Reports', ),
338         'report_directory_id': fields.many2one('document.directory','Directory',
339                                 help="Folder is used to store the generated reports"),
340         'server_action_id': fields.many2one('ir.actions.server', string='Action',
341                                 help= "Describes the action name.\n"
342                                 "eg:On which object which action to be taken on basis of which condition"),
343         'to_ids': fields.one2many('marketing.campaign.transition',
344                                             'activity_from_id',
345                                             'Next Activities'),
346         'from_ids': fields.one2many('marketing.campaign.transition',
347                                             'activity_to_id',
348                                             'Previous Activities'),
349         #'subcampaign_id': fields.many2one('marketing.campaign', 'Sub-Campaign',
350         #                                  domain="[('object_id', '=', object_id)]"),
351         'variable_cost': fields.float('Variable Cost'),
352         'revenue': fields.float('Revenue'),
353         'signal': fields.char('Signal', size=128,
354                               help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
355         'keep_if_condition_not_met': fields.boolean('Keep as cancelled when condition not met',
356                                                     help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
357     }
358
359     _defaults = {
360         'type': lambda *a: 'email',
361         'condition': lambda *a: 'True',
362     }
363
364     def search(self, cr, uid, args, offset=0, limit=None, order=None,
365                                         context=None, count=False):
366         if context == None:
367             context = {}
368         if 'segment_id' in context  and context['segment_id']:
369             segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
370                                                     uid, context['segment_id'])
371             act_ids = []
372             for activity in segment_obj.campaign_id.activity_ids:
373                 act_ids.append(activity.id)
374             return act_ids
375         return super(marketing_campaign_activity, self).search(cr, uid, args,
376                                            offset, limit, order, context, count)
377
378     def _process_wi_paper(self, cr, uid, activity, workitem, context=None):
379         service = netsvc.LocalService('report.%s'%activity.report_id.report_name)
380         (report_data, format) = service.create(cr, uid, [], {}, {})
381         attach_vals = {
382                 'name': '%s_%s_%s'%(activity.report_id.report_name,
383                                     activity.name,workitem.partner_id.name),
384                 'datas_fname': '%s.%s'%(activity.report_id.report_name,
385                                             activity.report_id.report_type),
386                 'parent_id': activity.report_directory_id.id,
387                 'datas': base64.encodestring(report_data),
388                 'file_type': format
389                 }
390         self.pool.get('ir.attachment').create(cr, uid, attach_vals)
391         return True
392
393     def _process_wi_email(self, cr, uid, activity, workitem, context=None):
394         return self.pool.get('email.template').generate_mail(cr, uid,
395                                             activity.email_template_id.id,
396                                             [workitem.res_id], context=context)
397
398     def _process_wi_action(self, cr, uid, activity, workitem, context=None):
399         if context is None:
400             context = {}
401         server_obj = self.pool.get('ir.actions.server')
402
403         action_context = dict(context,
404                               active_id=workitem.res_id,
405                               active_ids=[workitem.res_id],
406                               active_model=workitem.object_id.model)
407         res = server_obj.run(cr, uid, [activity.server_action_id.id],
408                              context=action_context)
409         # server action return False if the action is perfomed
410         # except client_action, other and python code
411         return res == False and True or res
412
413     def process(self, cr, uid, act_id, wi_id, context=None):
414         activity = self.browse(cr, uid, act_id, context=context)
415         method = '_process_wi_%s' % (activity.type,)
416         action = getattr(self, method, None)
417         if not action:
418             raise NotImplementedError('method %r in not implemented on %r object' % (method, self))
419
420         workitem_obj = self.pool.get('marketing.campaign.workitem')
421         workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
422         return action(cr, uid, activity, workitem, context)
423
424 marketing_campaign_activity()
425
426 class marketing_campaign_transition(osv.osv):
427     _name = "marketing.campaign.transition"
428     _description = "Campaign Transition"
429
430     _interval_units = [('hours', 'Hour(s)'), ('days', 'Day(s)'),
431                        ('months', 'Month(s)'), ('years','Year(s)')]
432
433
434     def _get_name(self, cr, uid, ids, fn, args, context=None):
435         result = dict.fromkeys(ids, False)
436         formatters = {
437             'auto': _('Automatic transition'),
438             'time': _('After %(interval_nbr)d %(interval_type)s'),
439             'cosmetic': _('Cosmetic'),
440         }
441         for tr in self.browse(cr, uid, ids, context=context,
442                               fields_process=translate_selections):
443             result[tr.id] = formatters[tr.trigger.value] % tr
444         return result
445
446
447     def _delta(self, cr, uid, ids, context=None):
448         assert len(ids) == 1
449         transition = self.browse(cr, uid, ids[0], context)
450         if transition.trigger != 'time':
451             raise ValueError('Delta is only relevant for timed transiton')
452         return relativedelta(**{transition.interval_type: transition.interval_nbr})
453
454
455     _columns = {
456         'name': fields.function(_get_name, method=True, string='Name',
457                                 type='char', size=128),
458         'activity_from_id': fields.many2one('marketing.campaign.activity',
459                                             'Source Activity', select=1,
460                                             required=True),
461         'activity_to_id': fields.many2one('marketing.campaign.activity',
462                                           'Destination Activity',
463                                           required=True),
464         'interval_nbr': fields.integer('Interval Value', required=True),
465         'interval_type': fields.selection(_interval_units, 'Interval Unit',
466                                           required=True),
467
468         'trigger': fields.selection([('auto', 'Automatic'),
469                                      ('time', 'Time'),
470                                      ('cosmetic', 'Cosmetic'),  # fake plastic transition
471                                     ],
472                                     'Trigger', required=True,
473                                     help="How is the destination workitem triggered"),
474     }
475
476     _defaults = {
477         'interval_nbr': 1,
478         'interval_type': 'days',
479         'trigger': 'time',
480     }
481
482     _sql_constraints = [
483         ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
484     ]
485
486 marketing_campaign_transition()
487
488 class marketing_campaign_workitem(osv.osv):
489     _name = "marketing.campaign.workitem"
490     _description = "Campaign Workitem"
491
492     def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
493         res = dict.fromkeys(ids, '/')
494         for wi in self.browse(cr, uid, ids, context=context):
495             if not wi.res_id:
496                 continue
497
498             proxy = self.pool.get(wi.object_id.model)
499             ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
500             if ng:
501                 res[wi.id] = ng[0][1]
502         return res
503
504     _columns = {
505         'segment_id': fields.many2one('marketing.campaign.segment', 'Segment'),
506         'activity_id': fields.many2one('marketing.campaign.activity','Activity',
507              required=True),
508         'campaign_id': fields.related('activity_id', 'campaign_id',
509              type='many2one', relation='marketing.campaign', string='Campaign', readonly=True),
510         'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
511              type='many2one', relation='ir.model', string='Object', select=1),
512         'res_id': fields.integer('Resource ID', select=1, readonly=1),
513         'res_name': fields.function(_res_name_get, method=True, string='Resource Name', type="char", size=64),
514         'date': fields.datetime('Execution Date', help='If date is not set, this workitem have to be run manually'),
515         'partner_id': fields.many2one('res.partner', 'Partner', select=1),
516         'state': fields.selection([('todo', 'To Do'),
517                                    ('exception', 'Exception'), ('done', 'Done'),
518                                    ('cancelled', 'Cancelled')], 'State'),
519
520         'error_msg' : fields.text('Error Message')
521     }
522     _defaults = {
523         'state': lambda *a: 'todo',
524         'date': False,
525     }
526
527     def button_draft(self, cr, uid, workitem_ids, context={}):
528         for wi in self.browse(cr, uid, workitem_ids, context=context):
529             if wi.state in ('exception', 'cancelled'):
530                 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
531         return True
532
533     def button_cancel(self, cr, uid, workitem_ids, context={}):
534         for wi in self.browse(cr, uid, workitem_ids, context=context):
535             if wi.state in ('todo','exception'):
536                 self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
537         return True
538
539     def _process_one(self, cr, uid, workitem, context=None):
540         if workitem.state != 'todo':
541             return
542
543         activity = workitem.activity_id
544         proxy = self.pool.get(workitem.object_id.model)
545         object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
546
547         eval_context = {
548             'activity': activity,
549             'workitem': workitem,
550             'object': object_id,
551             'transition': activity.to_ids
552         }
553         try:
554             condition = activity.condition
555             campaign_mode = workitem.campaign_id.mode
556             if condition:
557                 if not eval(condition, eval_context):
558                     if activity.keep_if_condition_not_met:
559                         workitem.write({'state': 'cancelled'}, context=context)
560                     else:
561                         workitem.unlink(context=context)
562                     return
563             result = True
564             if campaign_mode in ('manual', 'active'):
565                 Activities = self.pool.get('marketing.campaign.activity')
566                 result = Activities.process(cr, uid, activity.id, workitem.id,
567                                             context=context)
568
569             values = dict(state='done')
570             if not workitem.date:
571                 values['date'] = datetime.now().strftime(DT_FMT)
572             workitem.write(values, context=context)
573
574             if result:
575                 # process _chain
576                 for transition in activity.to_ids:
577                     if transition.trigger == 'cosmetic':
578                         continue
579                     launch_date = False
580                     if transition.trigger == 'auto':
581                         launch_date = datetime.now()
582                     elif transition.trigger == 'time':
583                         launch_date = datetime.now() + transition._delta()
584
585                     if launch_date:
586                         launch_date = launch_date.strftime(DT_FMT)
587                     values = {
588                         'date': launch_date,
589                         'segment_id': workitem.segment_id.id,
590                         'activity_id': transition.activity_to_id.id,
591                         'partner_id': workitem.partner_id.id,
592                         'res_id': workitem.res_id,
593                         'state': 'todo',
594                     }
595                     wi_id = self.create(cr, uid, values, context=context)
596
597                     # Now, depending of the trigger and the campaign mode 
598                     # we know if must run the newly created workitem.
599                     #
600                     # rows = transition trigger \ colums = campaign mode
601                     #
602                     #           test    test_realtime     manual      normal (active)
603                     # time       Y            N             N           N
604                     # cosmetic   N            N             N           N
605                     # auto       Y            Y             Y           Y
606                     # 
607
608                     run = transition.trigger == 'auto' \
609                           or (transition.trigger == 'time' \
610                               and campaign_mode == 'test')
611                     if run:
612                         new_wi = self.browse(cr, uid, wi_id, context)
613                         self._process_one(cr, uid, new_wi, context)
614
615         except Exception:
616             tb = "".join(format_exception(*exc_info()))
617             workitem.write({'state': 'exception', 'error_msg': tb},
618                      context=context)
619
620     def process(self, cr, uid, workitem_ids, context=None):
621         for wi in self.browse(cr, uid, workitem_ids, context):
622             self._process_one(cr, uid, wi, context)
623         return True
624
625     def process_all(self, cr, uid, camp_ids=None, context=None):
626         camp_obj = self.pool.get('marketing.campaign')
627         if camp_ids is None:
628             camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
629         for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
630             if camp.mode == 'manual':
631                 # manual states are not processed automatically
632                 continue
633             while True:
634                 domain = [('state', '=', 'todo'), ('date', '!=', False)]
635                 if camp.mode in ('test_realtime', 'active'):
636                     domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
637
638                 workitem_ids = self.search(cr, uid, domain, context=context)
639                 if not workitem_ids:
640                     break
641
642                 self.process(cr, uid, workitem_ids, context)
643         return True
644
645     def preview(self, cr, uid, ids, context):
646         res = {}
647         wi_obj = self.browse(cr, uid, ids[0], context)
648         if wi_obj.activity_id.type == 'email':
649             data_obj = self.pool.get('ir.model.data')
650             data_id = data_obj._get_id(cr, uid, 'email_template', 'email_template_preview_form')
651             view_id = 0
652             if data_id:
653                 view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
654             res = {
655                 'name': _('Email Preview'),
656                 'view_type': 'form',
657                 'view_mode': 'form,tree',
658                 'res_model': 'email_template.preview',
659                 'view_id': False,
660                 'context': context,
661                 'views': [(view_id, 'form')],
662                 'type': 'ir.actions.act_window',
663                 'target': 'new',
664                 'nodestroy':True,
665                 'context': "{'template_id':%d,'default_rel_model_ref':%d}"%
666                                 (wi_obj.activity_id.email_template_id.id,
667                                  wi_obj.res_id)
668             }
669
670         elif wi_obj.activity_id.type == 'paper':
671             datas = {'ids': [wi_obj.res_id],
672                      'model': wi_obj.object_id.model}
673             res = {
674                 'type' : 'ir.actions.report.xml',
675                 'report_name': wi_obj.activity_id.report_id.report_name,
676                 'datas' : datas,
677                 'nodestroy': True,
678             }
679         else:
680             raise osv.except_osv(_('No preview'),_('The current step for this item has no email or report to preview.'))
681         return res
682
683 marketing_campaign_workitem()
684
685 class email_template(osv.osv):
686     _inherit = "email.template"
687     _defaults = {
688         'object_name': lambda obj, cr, uid, context: context.get('object_id',False),
689     }
690     
691     # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
692      
693 email_template()
694
695 class report_xml(osv.osv):
696     _inherit = 'ir.actions.report.xml'
697     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
698         if context is None:
699             context = {}
700         object_id = context.get('object_id')
701         if object_id:
702             model = self.pool.get('ir.model').browse(cr, uid, object_id).model
703             args.append(('model', '=', model))
704         return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count)
705
706 report_xml()
707