[ADD] marketing_campaign: 2 new transition triggers: auto and signal. auto transition mean the destination activity is immediatly executed. signal transition mean the destination activity is executed only when it receive the signal.

[IMP] marketing_campaign: better activity view

bzr revid: chs@openerp.com-20100701115355-nx7of0ju225idwej
This commit is contained in:
Christophe Simonis 2010-07-01 13:53:55 +02:00
parent a658eb33f0
commit f2bcf8d808
2 changed files with 229 additions and 110 deletions

View File

@ -36,6 +36,8 @@ _intervalTypes = {
'years': lambda interval: relativedelta(years=interval),
}
DT_FMT = '%Y-%m-%d %H:%M:%S'
def dict_map(f, d):
return dict((k, f(v)) for k,v in d.items())
@ -142,6 +144,32 @@ you required for the campaign"),
def state_cancel_set(self, cr, uid, ids, *args):
self.write(cr, uid, ids, {'state': 'cancelled'})
return True
def signal(self, cr, uid, model, res_id, signal, context=None):
if not signal:
raise ValueError('signal cannot be False')
Workitems = self.pool.get('marketing.campaign.workitem')
domain = [('object_id.name', '=', model),
('state', '=', 'running')]
campaign_ids = self.search(cr, uid, domain, context=context)
for campaign in self.browse(cr, uid, campaign_ids, context):
for activity in campaign.activity_ids:
if activity.signal != signal:
continue
wi_domain = [('activity_id', '=', activity.id),
('res_id', '=', res_id),
('state', '=', 'todo'),
('date', '=', False),
]
wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
Workitems.process(cr, uid, wi_ids, context=context)
def _signal(self, cr, uid, record, signal, context=None):
return self.signal(cr, uid, record._table._name,
record.id, signal, context)
marketing_campaign()
class marketing_campaign_segment(osv.osv):
@ -252,6 +280,7 @@ marketing_campaign_segment()
class marketing_campaign_activity(osv.osv):
_name = "marketing.campaign.activity"
_description = "Campaign Activity"
_actions_type = [('email', 'E-mail'), ('paper', 'Paper'), ('action', 'Action'),
('subcampaign', 'Sub-Campaign')]
_columns = {
@ -284,7 +313,10 @@ class marketing_campaign_activity(osv.osv):
'subcampaign_segment_id': fields.many2one('marketing.campaign.segment',
'Sub Campaign Segment'),
'variable_cost': fields.float('Variable Cost'),
'revenue': fields.float('Revenue')
'revenue': fields.float('Revenue'),
'signal': fields.char('Signal', size=128,
help='An activity with a signal can be called \
programmatically'),
}
_defaults = {
@ -292,7 +324,23 @@ class marketing_campaign_activity(osv.osv):
'condition': lambda *a: 'True',
'object_id' : lambda obj, cr, uid, context : context.get('object_id',False),
}
def _check_signal(self, cr, uid, ids, context=None):
return all(activity.signal
for activity in self.browse(cr, uid, ids, context)
for transition in activity.from_ids
if transition.trigger == 'signal')
_constraints = [
(_check_signal,
"An incoming transition is triggered by a signal but this transition \
doesn't have one",
['signal', 'from_ids']
),
]
def __init__(self, *args):
# FIXME use self._process_wi_<type>
self._actions = {'paper' : self.process_wi_report,
'email' : self.process_wi_email,
'action' : self.process_wi_action,
@ -359,32 +407,70 @@ class marketing_campaign_transition(osv.osv):
def _get_name(self, cr, uid, ids, fn, args, context=None):
result = dict.fromkeys(ids, False)
for tr in self.browse(cr, uid, ids, context=context, fields_process=translate_selections):
result[tr.id] = _('After %(interval_nbr)d %(interval_type)s') % tr
formatters = {
'auto': _('Automatic transition'),
'time': _('After %(interval_nbr)d %(interval_type)s'),
'signal': _('On signal'),
}
for tr in self.browse(cr, uid, ids, context=context,
fields_process=translate_selections):
result[tr.id] = formatters[tr.trigger.value] % tr
return result
def _delta(self, cr, uid, ids, context=None):
assert len(ids) == 1
transition = self.browse(cr, uid, ids[0], context)
if transition.trigger != 'time':
raise ValueError('Delta is only relevant for timed transiton')
return relativedelta(**{transition.interval_type: transition.interval_nbr})
_columns = {
'name': fields.function(_get_name, method=True, string='Name',
type='char', size=128),
'activity_from_id': fields.many2one('marketing.campaign.activity',
'Source Activity', select=1),
'Source Activity', select=1,
required=True),
'activity_to_id': fields.many2one('marketing.campaign.activity',
'Destination Activity'),
'Destination Activity',
required=True),
'interval_nbr': fields.integer('Interval Value', required=True),
'interval_type': fields.selection(_interval_units, 'Interval Unit',
required=True),
'trigger': fields.selection([('auto', 'Automatic'),
('time', 'Time'),
('signal','Signal')],
'Trigger', required=True,
help="How is trigger the destination workitem"),
}
_defaults = {
'interval_nbr': 1,
'interval_type': 'days',
'trigger': 'time',
}
_sql_constraints = [
('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
]
def _check_signal(self, cr, uid, ids, context=None):
return all(tr.activity_to_id.signal
for tr in self.browse(cr, uid, ids, context)
if tr.trigger == 'signal')
_constraints = [
(_check_signal,
"The transition is triggered by a signal but destination activity \
doesn't have one",
['trigger', 'activity_to_ids']
),
]
def default_get(self, cr, uid, fields, context=None):
# TODO remove type_id and use directly the fieldname as key (use default_<field> ??)
value = super(marketing_campaign_transition, self).default_get(cr, uid,
fields, context)
if context and 'type_id' in context:
@ -399,6 +485,7 @@ class marketing_campaign_workitem(osv.osv):
_description = "Campaign Workitem"
def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
# FIXME better code
res = {}
for obj in self.browse(cr, uid, ids, context=context):
if obj.res_id:
@ -421,9 +508,9 @@ class marketing_campaign_workitem(osv.osv):
type='many2one', relation='ir.model', string='Object', select=1),
'res_id': fields.integer('Resource ID', select=1, readonly=1),
'res_name': fields.function(_res_name_get, method=True, string='Resource Name', type="char", size=64),
'date': fields.datetime('Execution Date'),
'date': fields.datetime('Execution Date', help='If date is not set, this workitem have to be run manually'),
'partner_id': fields.many2one('res.partner', 'Partner', select=1),
'state': fields.selection([('todo', 'To Do'), ('inprogress', 'In Progress'),
'state': fields.selection([('todo', 'To Do'),
('exception', 'Exception'), ('done', 'Done'),
('cancelled', 'Cancelled')], 'State'),
@ -431,27 +518,9 @@ class marketing_campaign_workitem(osv.osv):
}
_defaults = {
'state': lambda *a: 'todo',
'date': False,
}
def process_chain(self, cr, uid, workitem_id, context={}):
workitem = self.browse(cr, uid, workitem_id)
for mct_id in workitem.activity_id.to_ids:
launch_date = time.strftime('%Y-%m-%d %H:%M:%S')
if mct_id.interval_type and mct_id.interval_nbr :
launch_date = (datetime.now() + _intervalTypes[ \
mct_id.interval_type](mct_id.interval_nbr) \
).strftime('%Y-%m-%d %H:%M:%S')
workitem_vals = {
'segment_id': workitem.segment_id.id,
'activity_id': mct_id.activity_to_id.id,
'date': launch_date,
'partner_id': workitem.partner_id.id,
'res_id': workitem.res_id,
'state': 'todo',
}
self.create(cr, uid, workitem_vals)
return True
def button_draft(self, cr, uid, workitem_ids, context={}):
for wi in self.browse(cr, uid, workitem_ids, context=context):
if wi.state=='exception':
@ -464,57 +533,106 @@ class marketing_campaign_workitem(osv.osv):
self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
return True
def process(self, cr, uid, workitem_ids, context={}):
for wi in self.browse(cr, uid, workitem_ids):
if wi.state == 'todo':
eval_context = {
'pool': self.pool,
'cr': cr,
'uid': uid,
'wi': wi,
'object': wi.activity_id,
'transition': wi.activity_id.to_ids
}
try:
expr = eval(str(wi.activity_id.condition), eval_context)
if expr:
result = True
if wi.campaign_id.mode in ('manual','active'):
result = self.pool.get('marketing.campaign.activity').process(
cr, uid, wi.activity_id.id, wi.id, context)
if result:
self.write(cr, uid, wi.id, {'state': 'done'})
self.process_chain(cr, uid, wi.id, context)
else:
vals = {'state': 'exception'}
if type(result) == type({}) and 'error_msg' in result:
vals['error_msg'] = result['error_msg']
self.write(cr, uid, wi.id, vals)
else:
self.write(cr, uid, wi.id, {'state': 'cancelled'})
except Exception,e:
self.write(cr, uid, wi.id, {'state': 'exception', 'error_msg': str(e)})
def _process_one(self, cr, uid, workitem, context=None):
if workitem.state != 'todo':
return
activity = workitem.activity_id
eval_context = {
'pool': self.pool,
'cr': cr,
'uid': uid,
'wi': workitem,
'object': activity,
'transition': activity.to_ids
}
try:
condition = activity.condition
campaign_mode = workitem.campaign_id.mode
if condition:
if not eval(condition, eval_context):
workitem.write({'state': 'cancelled'}, context=context)
return
result = True
if campaign_mode in ('manual', 'active'):
Activities = self.pool.get('marketing.campaign.activity')
result = Activities.process(activity.id, workitem.id,
context=context)
values = dict(state='done')
if not workitem.date:
values['date'] = datetime.now().strftime(DT_FMT)
workitem.write(values, context=context)
if result:
# process _chain
for transition in activity.to_ids:
launch_date = False
if transition.trigger == 'auto':
launch_date = datetime.now()
elif transition.trigger == 'time':
launch_date = datetime.now() + transition._delta()
if launch_date:
launch_date = launch_date.strftime(DT_FMT)
values = {
'date': launch_date,
'segment_id': workitem.segment_id.id,
'activity_id': transition.activity_to_id.id,
'partner_id': workitem.partner_id.id,
'res_id': workitem.res_id,
'state': 'todo',
}
wi_id = self.create(cr, uid, values, context=context)
# Now, depending of the trigger and the campaign mode
# we now if must run the newly created workitem.
#
# rows = transition trigger \ colums = campaign mode
#
# test test_realtime manual normal (active)
# time Y N N N
# signal N N N N
# auto Y Y N Y
#
run = False
if transition.trigger != 'signal' and campaign_mode != 'manual':
if transition.trigger == 'auto' or campaign_mode == 'test':
run = True
if run:
new_wi = self.browse(cr, uid, wi_id, context)
self._process_one(cr, uid, new_wi, context)
except Exception, e:
workitem.write({'state': 'exception', 'error_msg': str(e)},
context=context)
def process(self, cr, uid, workitem_ids, context=None):
for wi in self.browse(cr, uid, workitem_ids, context):
self._process_one(cr, uid, wi, context)
return True
def process_all(self, cr, uid, camp_ids=None, context={}):
def process_all(self, cr, uid, camp_ids=None, context=None):
camp_obj = self.pool.get('marketing.campaign')
if not camp_ids:
if camp_ids is None:
camp_ids = camp_obj.search(cr, uid, [('state','=','running')], context=context)
for camp in camp_obj.browse(cr, uid, camp_ids, context=context):
if camp.mode == 'manual':
# manual states are not processed automatically
continue
while True:
if camp.mode in ('test_realtime','active'):
workitem_ids = self.search(cr, uid, [('state', '=', 'todo'),
('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))])
elif camp.mode == 'test':
workitem_ids = self.search(cr, uid, [('state', '=', 'todo')])
else:
# manual states are not processed automatically
workitem_ids = []
if workitem_ids:
self.process(cr, uid, workitem_ids, context)
else:
domain = [('state', '=', 'todo'), ('date', '!=', False)]
if camp.mode in ('test_realtime', 'active'):
domain += [('date','<=', time.strftime('%Y-%m-%d %H:%M:%S'))]
workitem_ids = self.search(cr, uid, domain, context=context)
if not workitem_ids:
break
self.process(cr, uid, workitem_ids, context)
def preview(self, cr, uid, ids, context):
res = {}
wi_obj = self.browse(cr, uid, ids)[0]

View File

@ -218,52 +218,54 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Activities">
<group colspan="4" col="6">
<separator string="Activity Definition" colspan="4"/>
<separator string="Cost Control" colspan="2"/>
<field name="name" select="1" />
<field name="start"/>
<field name="variable_cost" select="1"/>
<field name="object_id" invisible="1"/>
</group>
<group colspan="2" col="2">
<separator string="Condition" colspan="2"/>
<field name="condition"/>
</group>
<group colspan="2" col="2">
<separator string="Action" colspan="2"/>
<field name="type" colspan="4"/>
<group colspan="4" attrs="{'invisible':[('type','!=','email')]}" >
<field name="email_template_id" attrs="{'required':[('type','=','email')]}" />
</group>
<group colspan="4" attrs="{'invisible':[('type','!=','paper')]}" >
<field name="report_id" attrs="{'required':[('type','=','paper')]}" context="{'object_id':object_id}"/>
<field name="report_directory_id" attrs="{'required':[('type','=','paper')]}" />
</group>
<group colspan="4" attrs="{'invisible':[('type','!=','action')]}" >
<field name="server_action_id" attrs="{'required':[('type','=','action')]}" domain="[('model_id','=',object_id)]" />
</group>
<group colspan="4" attrs="{'invisible':[('type','!=','subcampaign')]}" >
<field name="subcampaign_id" attrs="{'required':[('type','=','subcampaign')]}" />
<field name="subcampaign_segment_id" attrs="{'required':[('type','=','subcampaign')]}" />
<field name="object_id" invisible="1"/>
<separator string="Activity Definition" colspan="4"/>
<field name="name" select="1" colspan='4' />
<field name="signal"/>
<field name="start"/>
<field name="variable_cost" select="1"/>
<newline/>
<separator string="Action" colspan="4"/>
<group colspan='4' col='4'>
<field name="condition" colspan='4'/>
<field name="type" width='100'/>
<group colspan='2' col='1'>
<group attrs="{'invisible':[('type','!=','email')]}" >
<field name="email_template_id" attrs="{'required':[('type','=','email')]}" />
</group>
<group attrs="{'invisible':[('type','!=','paper')]}" >
<field name="report_id" attrs="{'required':[('type','=','paper')]}" context="{'object_id':object_id}"/>
<field name="report_directory_id" attrs="{'required':[('type','=','paper')]}" />
</group>
<group attrs="{'invisible':[('type','!=','action')]}" >
<field name="server_action_id" attrs="{'required':[('type','=','action')]}" domain="[('model_id','=',object_id)]" />
</group>
<group attrs="{'invisible':[('type','!=','subcampaign')]}" >
<field name="subcampaign_id" attrs="{'required':[('type','=','subcampaign')]}" />
<field name="subcampaign_segment_id" attrs="{'required':[('type','=','subcampaign')]}" />
</group>
</group>
</group>
<newline/>
<group colspan="4" col="2" expand="1">
<separator string="Transitions" colspan="2"/>
<field name="from_ids" nolabel="1" mode="tree" default_get="{'type_id':'activity_to_id','activity_id':active_id or False}">
<tree string="Incoming Transitions" editable="bottom">
<field name="activity_from_id"/>
<field name='trigger'/>
<field name="interval_nbr"/>
<field name="interval_type"/>
</tree>
</field>
<field name="to_ids" nolabel="1" mode="tree" default_get="{'type_id':'activity_from_id','activity_id':active_id or False}">
<tree string="Outgoing Transitions" editable="bottom">
<field name="activity_to_id"/>
<field name="interval_nbr"/>
<field name="interval_type"/>
</tree>
</field>
<field name="from_ids" nolabel="1" mode="tree" default_get="{'type_id':'activity_to_id','activity_id':active_id or False}">
<tree string="Incoming Transitions" editable="bottom">
<field name="activity_from_id"/>
<field name="interval_nbr"/>
<field name="interval_type"/>
</tree>
<tree string="Outgoing Transitions" editable="bottom">
<field name="activity_to_id"/>
<field name='trigger'/>
<field name="interval_nbr"/>
<field name="interval_type"/>
</tree>
</field>
</group>
</form>
@ -357,7 +359,6 @@
<field name="arch" type="xml">
<search string="Marketing Campaign Activities">
<filter icon="terp-gtk-go-back-rtl" string="To Do" name = "todo" domain="[('state','=','todo')]"/>
<filter icon="terp-camera_test" string="In Progress" domain="[('state','=','inprogress')]"/>
<filter icon="terp-emblem-important" string="Exception" domain="[('state','=','exception')]"/>
<separator orientation="vertical"/>
<field name="segment_id" select="1"/>