diff --git a/addons/marketing_campaign/__init__.py b/addons/marketing_campaign/__init__.py
index f5831dc54bd..a5f67aebd79 100644
--- a/addons/marketing_campaign/__init__.py
+++ b/addons/marketing_campaign/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import marketing_campaign
+import models
import report
diff --git a/addons/marketing_campaign/__openerp__.py b/addons/marketing_campaign/__openerp__.py
index 1d3808fbb45..a71ce85c0b8 100644
--- a/addons/marketing_campaign/__openerp__.py
+++ b/addons/marketing_campaign/__openerp__.py
@@ -1,14 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
{
'name': 'Marketing Campaigns',
'version': '1.1',
- 'depends': ['document',
- 'mail',
- 'decimal_precision'
- ],
+ 'depends': ['document', 'mail', 'decimal_precision'],
'category': 'Marketing',
'description': """
This module provides leads automation through marketing campaigns (campaigns can in fact be defined on any resource, not just CRM Leads).
@@ -38,13 +34,10 @@ input segments, workflow.
'data': [
'security/marketing_campaign_security.xml',
'security/ir.model.access.csv',
- 'marketing_campaign_view.xml',
- 'marketing_campaign_data.xml',
- 'marketing_campaign_workflow.xml',
+ 'views/marketing_campaign_views.xml',
+ 'data/marketing_campaign_data.xml',
+ 'data/marketing_campaign_workflow.xml',
'report/campaign_analysis_view.xml',
],
- 'demo': ['marketing_campaign_demo.xml'],
- 'test': ['test/marketing_campaign.yml'],
- 'installable': True,
- 'auto_install': False,
+ 'demo': ['data/marketing_campaign_demo.xml'],
}
diff --git a/addons/marketing_campaign/marketing_campaign_data.xml b/addons/marketing_campaign/data/marketing_campaign_data.xml
similarity index 95%
rename from addons/marketing_campaign/marketing_campaign_data.xml
rename to addons/marketing_campaign/data/marketing_campaign_data.xml
index 4f337b1d60e..986b58508c9 100644
--- a/addons/marketing_campaign/marketing_campaign_data.xml
+++ b/addons/marketing_campaign/data/marketing_campaign_data.xml
@@ -1,5 +1,5 @@
-
-
+
+
@@ -27,4 +27,4 @@
-
+
diff --git a/addons/marketing_campaign/marketing_campaign_demo.xml b/addons/marketing_campaign/data/marketing_campaign_demo.xml
similarity index 98%
rename from addons/marketing_campaign/marketing_campaign_demo.xml
rename to addons/marketing_campaign/data/marketing_campaign_demo.xml
index 06fb4545360..ad808c35734 100644
--- a/addons/marketing_campaign/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign/data/marketing_campaign_demo.xml
@@ -1,5 +1,5 @@
-
-
+
+
welcome new partner
@@ -100,5 +100,4 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/addons/marketing_campaign/marketing_campaign_workflow.xml b/addons/marketing_campaign/data/marketing_campaign_workflow.xml
similarity index 99%
rename from addons/marketing_campaign/marketing_campaign_workflow.xml
rename to addons/marketing_campaign/data/marketing_campaign_workflow.xml
index ad3fb0ddcca..8f32522423b 100644
--- a/addons/marketing_campaign/marketing_campaign_workflow.xml
+++ b/addons/marketing_campaign/data/marketing_campaign_workflow.xml
@@ -1,6 +1,5 @@
-
-
+
@@ -140,5 +139,4 @@
state_draft_set
-
-
+
diff --git a/addons/marketing_campaign/marketing_campaign.py b/addons/marketing_campaign/marketing_campaign.py
deleted file mode 100644
index 9b1e9bad619..00000000000
--- a/addons/marketing_campaign/marketing_campaign.py
+++ /dev/null
@@ -1,799 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import time
-import base64
-from datetime import datetime
-from dateutil.relativedelta import relativedelta
-from traceback import format_exception
-from sys import exc_info
-from openerp.tools.safe_eval import safe_eval as eval
-import re
-import openerp.addons.decimal_precision as dp
-
-from openerp import api, SUPERUSER_ID
-from openerp.osv import fields, osv
-from openerp.report import render_report
-from openerp.tools.translate import _
-from openerp.exceptions import UserError
-
-_intervalTypes = {
- 'hours': lambda interval: relativedelta(hours=interval),
- 'days': lambda interval: relativedelta(days=interval),
- 'months': lambda interval: relativedelta(months=interval),
- 'years': lambda interval: relativedelta(years=interval),
-}
-
-DT_FMT = '%Y-%m-%d %H:%M:%S'
-
-
-class marketing_campaign(osv.osv):
- _name = "marketing.campaign"
- _description = "Marketing Campaign"
-
- def _count_segments(self, cr, uid, ids, field_name, arg, context=None):
- res = {}
- try:
- for segments in self.browse(cr, uid, ids, context=context):
- res[segments.id] = len(segments.segment_ids)
- except:
- pass
- return res
-
- _columns = {
- 'name': fields.char('Name', required=True),
- 'object_id': fields.many2one('ir.model', 'Resource', required=True,
- help="Choose the resource on which you want \
-this campaign to be run"),
- 'partner_field_id': fields.many2one('ir.model.fields', 'Partner Field',
- domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
- help="The generated workitems will be linked to the partner related to the record. "\
- "If the record is the partner itself leave this field empty. "\
- "This is useful for reporting purposes, via the Campaign Analysis or Campaign Follow-up views."),
- 'unique_field_id': fields.many2one('ir.model.fields', 'Unique Field',
- domain="[('model_id', '=', object_id), ('ttype', 'in', ['char','int','many2one','text','selection'])]",
- help='If set, this field will help segments that work in "no duplicates" mode to avoid '\
- 'selecting similar records twice. Similar records are records that have the same value for '\
- 'this unique field. For example by choosing the "email_from" field for CRM Leads you would prevent '\
- 'sending the same campaign to the same email address again. If not set, the "no duplicates" segments '\
- "will only avoid selecting the same record again if it entered the campaign previously. "\
- "Only easily comparable fields like textfields, integers, selections or single relationships may be used."),
- 'mode': fields.selection([('test', 'Test Directly'),
- ('test_realtime', 'Test in Realtime'),
- ('manual', 'With Manual Confirmation'),
- ('active', 'Normal')],
- 'Mode', required=True, help= \
-"""Test - It creates and process all the activities directly (without waiting for the delay on transitions) but does not send emails or produce reports.
-Test in Realtime - It creates and processes all the activities directly but does not send emails or produce reports.
-With Manual Confirmation - the campaigns runs normally, but the user has to validate all workitem manually.
-Normal - the campaign runs normally and automatically sends all emails and reports (be very careful with this mode, you're live!)"""),
- 'state': fields.selection([('draft', 'New'),
- ('running', 'Running'),
- ('cancelled', 'Cancelled'),
- ('done', 'Done')],
- 'Status', copy=False),
- 'activity_ids': fields.one2many('marketing.campaign.activity',
- 'campaign_id', 'Activities'),
- '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')),
- 'segment_ids': fields.one2many('marketing.campaign.segment', 'campaign_id', 'Segments', readonly=False),
- 'segments_count': fields.function(_count_segments, type='integer', string='Segments')
- }
-
- _defaults = {
- 'state': lambda *a: 'draft',
- 'mode': lambda *a: 'test',
- }
-
- def state_running_set(self, cr, uid, ids, *args):
- # TODO check that all subcampaigns are running
- campaign = self.browse(cr, uid, ids[0])
-
- if not campaign.activity_ids:
- raise UserError(_("The campaign cannot be started. There are no activities in it."))
-
- has_start = False
- has_signal_without_from = False
-
- for activity in campaign.activity_ids:
- if activity.start:
- has_start = True
- if activity.signal and len(activity.from_ids) == 0:
- has_signal_without_from = True
-
- if not has_start and not has_signal_without_from:
- raise UserError(_("The campaign cannot be started. It does not have any starting activity. Modify campaign's activities to mark one as the starting point."))
-
- return self.write(cr, uid, ids, {'state': 'running'})
-
- def state_done_set(self, cr, uid, ids, *args):
- # TODO check that this campaign is not a subcampaign in running mode.
- segment_ids = self.pool.get('marketing.campaign.segment').search(cr, uid,
- [('campaign_id', 'in', ids),
- ('state', '=', 'running')])
- if segment_ids :
- raise UserError(_("The campaign cannot be marked as done before all segments are closed."))
- self.write(cr, uid, ids, {'state': 'done'})
- return True
-
- def state_cancel_set(self, cr, uid, ids, *args):
- # TODO check that this campaign is not a subcampaign in running mode.
- self.write(cr, uid, ids, {'state': 'cancelled'})
- return True
-
- # dead code
- def signal(self, cr, uid, model, res_id, signal, run_existing=True, context=None):
- record = self.pool[model].browse(cr, uid, res_id, context)
- return self._signal(cr, uid, record, signal, run_existing, context)
-
- #dead code
- def _signal(self, cr, uid, record, signal, run_existing=True, context=None):
- if not signal:
- raise ValueError('Signal cannot be False.')
-
- Workitems = self.pool.get('marketing.campaign.workitem')
- domain = [('object_id.model', '=', record._name),
- ('state', '=', 'running')]
- campaign_ids = self.search(cr, uid, domain, context=context)
- for campaign in self.browse(cr, uid, campaign_ids, context=context):
- for activity in campaign.activity_ids:
- if activity.signal != signal:
- continue
-
- data = dict(activity_id=activity.id,
- res_id=record.id,
- state='todo')
- wi_domain = [(k, '=', v) for k, v in data.items()]
-
- wi_ids = Workitems.search(cr, uid, wi_domain, context=context)
- if wi_ids:
- if not run_existing:
- continue
- else:
- partner = self._get_partner_for(campaign, record)
- if partner:
- data['partner_id'] = partner.id
- wi_id = Workitems.create(cr, uid, data, context=context)
- wi_ids = [wi_id]
- Workitems.process(cr, uid, wi_ids, context=context)
- return True
-
- def _get_partner_for(self, campaign, record):
- partner_field = campaign.partner_field_id.name
- if partner_field:
- return record[partner_field]
- elif campaign.object_id.model == 'res.partner':
- return record
- return None
-
- # prevent duplication until the server properly duplicates several levels of nested o2m
- def copy(self, cr, uid, id, default=None, context=None):
- raise UserError(_('Duplicating campaigns is not supported.'))
-
- def _find_duplicate_workitems(self, cr, uid, record, campaign_rec, context=None):
- """Finds possible duplicates workitems for a record in this campaign, based on a uniqueness
- field.
-
- :param record: browse_record to find duplicates workitems for.
- :param campaign_rec: browse_record of campaign
- """
- Workitems = self.pool.get('marketing.campaign.workitem')
- duplicate_workitem_domain = [('res_id','=', record.id),
- ('campaign_id','=', campaign_rec.id)]
- unique_field = campaign_rec.unique_field_id
- if unique_field:
- unique_value = getattr(record, unique_field.name, None)
- if unique_value:
- if unique_field.ttype == 'many2one':
- unique_value = unique_value.id
- similar_res_ids = self.pool[campaign_rec.object_id.model].search(cr, uid,
- [(unique_field.name, '=', unique_value)], context=context)
- if similar_res_ids:
- duplicate_workitem_domain = [('res_id','in', similar_res_ids),
- ('campaign_id','=', campaign_rec.id)]
- return Workitems.search(cr, uid, duplicate_workitem_domain, context=context)
-
-
-
-class marketing_campaign_segment(osv.osv):
- _name = "marketing.campaign.segment"
- _description = "Campaign Segment"
- _order = "name"
-
- def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
- # next auto sync date is same for all segments
- sync_job = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'marketing_campaign', 'ir_cron_marketing_campaign_every_day', context=context)
- next_sync = sync_job and sync_job.nextcall or False
- return dict.fromkeys(ids, next_sync)
-
- _columns = {
- 'name': fields.char('Name', required=True),
- 'campaign_id': fields.many2one('marketing.campaign', 'Campaign', required=True, select=1, ondelete="cascade"),
- 'object_id': fields.related('campaign_id','object_id', type='many2one', relation='ir.model', string='Resource'),
- 'ir_filter_id': fields.many2one('ir.filters', 'Filter', ondelete="restrict",
- domain=lambda self: [('model_id', '=', self.object_id._name)],
- help="Filter to select the matching resource records that belong to this segment. "\
- "New filters can be created and saved using the advanced search on the list view of the Resource. "\
- "If no filter is set, all records are selected without filtering. "\
- "The synchronization mode may also add a criterion to the filter."),
- 'sync_last_date': fields.datetime('Last Synchronization', help="Date on which this segment was synchronized last time (automatically or manually)"),
- 'sync_mode': fields.selection([('create_date', 'Only records created after last sync'),
- ('write_date', 'Only records modified after last sync (no duplicates)'),
- ('all', 'All records (no duplicates)')],
- 'Synchronization mode',
- help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign. "\
- '"No duplicates" prevents selecting records which have already entered the campaign previously.'\
- 'If the campaign has a "unique field" set, "no duplicates" will also prevent selecting records which have '\
- 'the same value for the unique field as other records that already entered the campaign.'),
- 'state': fields.selection([('draft', 'New'),
- ('cancelled', 'Cancelled'),
- ('running', 'Running'),
- ('done', 'Done')],
- 'Status', copy=False),
- 'date_run': fields.datetime('Launch Date', help="Initial start date of this segment."),
- 'date_done': fields.datetime('End Date', help="Date this segment was last closed or cancelled."),
- 'date_next_sync': fields.function(_get_next_sync, string='Next Synchronization', type='datetime', help="Next time the synchronization job is scheduled to run automatically"),
- }
-
- _defaults = {
- 'state': lambda *a: 'draft',
- 'sync_mode': lambda *a: 'create_date',
- }
-
- def _check_model(self, cr, uid, ids, context=None):
- for obj in self.browse(cr, uid, ids, context=context):
- if not obj.ir_filter_id:
- return True
- if obj.campaign_id.object_id.model != obj.ir_filter_id.model_id:
- return False
- return True
-
- _constraints = [
- (_check_model, 'Model of filter must be same as resource model of Campaign ', ['ir_filter_id,campaign_id']),
- ]
-
- def onchange_campaign_id(self, cr, uid, ids, campaign_id):
- res = {'domain':{'ir_filter_id':[]}}
- campaign_pool = self.pool.get('marketing.campaign')
- if campaign_id:
- campaign = campaign_pool.browse(cr, uid, campaign_id)
- model_name = self.pool.get('ir.model').read(cr, uid, [campaign.object_id.id], ['model'])
- if model_name:
- mod_name = model_name[0]['model']
- res['domain'] = {'ir_filter_id': [('model_id', '=', mod_name)]}
- else:
- res['value'] = {'ir_filter_id': False}
- return res
-
- def state_running_set(self, cr, uid, ids, *args):
- segment = self.browse(cr, uid, ids[0])
- vals = {'state': 'running'}
- if not segment.date_run:
- vals['date_run'] = time.strftime('%Y-%m-%d %H:%M:%S')
- self.write(cr, uid, ids, vals)
- return True
-
- def state_done_set(self, cr, uid, ids, *args):
- wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
- [('state', '=', 'todo'), ('segment_id', 'in', ids)])
- self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
- self.write(cr, uid, ids, {'state': 'done','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
- return True
-
- def state_cancel_set(self, cr, uid, ids, *args):
- wi_ids = self.pool.get("marketing.campaign.workitem").search(cr, uid,
- [('state', '=', 'todo'), ('segment_id', 'in', ids)])
- self.pool.get("marketing.campaign.workitem").write(cr, uid, wi_ids, {'state':'cancelled'})
- self.write(cr, uid, ids, {'state': 'cancelled','date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
- return True
-
- def synchroniz(self, cr, uid, ids, *args):
- self.process_segment(cr, uid, ids)
- return True
-
- @api.cr_uid_ids_context
- def process_segment(self, cr, uid, segment_ids=None, context=None):
- Workitems = self.pool.get('marketing.campaign.workitem')
- Campaigns = self.pool.get('marketing.campaign')
- if not segment_ids:
- segment_ids = self.search(cr, uid, [('state', '=', 'running')], context=context)
-
- action_date = time.strftime('%Y-%m-%d %H:%M:%S')
- campaigns = set()
- for segment in self.browse(cr, uid, segment_ids, context=context):
- if segment.campaign_id.state != 'running':
- continue
-
- campaigns.add(segment.campaign_id.id)
- act_ids = self.pool.get('marketing.campaign.activity').search(cr,
- uid, [('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)], context=context)
-
- model_obj = self.pool[segment.object_id.model]
- criteria = []
- if segment.sync_last_date and segment.sync_mode != 'all':
- criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
- if segment.ir_filter_id:
- criteria += eval(segment.ir_filter_id.domain)
- object_ids = model_obj.search(cr, uid, criteria, context=context)
-
- # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
- for record in model_obj.browse(cr, uid, object_ids, context=context):
- # avoid duplicate workitem for the same resource
- if segment.sync_mode in ('write_date','all'):
- if Campaigns._find_duplicate_workitems(cr, uid, record, segment.campaign_id, context=context):
- continue
-
- wi_vals = {
- 'segment_id': segment.id,
- 'date': action_date,
- 'state': 'todo',
- 'res_id': record.id
- }
-
- partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, record)
- if partner:
- wi_vals['partner_id'] = partner.id
-
- for act_id in act_ids:
- wi_vals['activity_id'] = act_id
- Workitems.create(cr, uid, wi_vals, context=context)
-
- self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
- Workitems.process_all(cr, uid, list(campaigns), context=context)
- return True
-
-
-class marketing_campaign_activity(osv.osv):
- _name = "marketing.campaign.activity"
- _order = "name"
- _description = "Campaign Activity"
-
- _action_types = [
- ('email', 'Email'),
- ('report', 'Report'),
- ('action', 'Custom Action'),
- # TODO implement the subcampaigns.
- # TODO implement the subcampaign out. disallow out transitions from
- # subcampaign activities ?
- #('subcampaign', 'Sub-Campaign'),
- ]
-
- _columns = {
- 'name': fields.char('Name', required=True),
- 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
- required = True, ondelete='cascade', select=1),
- 'object_id': fields.related('campaign_id','object_id',
- type='many2one', relation='ir.model',
- string='Object', readonly=True),
- 'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
- 'condition': fields.text('Condition', size=256, required=True,
- help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
- "The expression may use the following [browsable] variables:\n"
- " - activity: the campaign activity\n"
- " - workitem: the campaign workitem\n"
- " - resource: the resource object this campaign item represents\n"
- " - transitions: list of campaign transitions outgoing from this activity\n"
- "...- re: Python regular expression module"),
- 'type': fields.selection(_action_types, 'Type', required=True,
- help="""The type of action to execute when an item enters this activity, such as:
- - Email: send an email using a predefined email template
- - Report: print an existing Report defined on the resource item and save it into a specific directory
- - Custom Action: execute a predefined action, e.g. to modify the fields of the resource record
- """),
- 'email_template_id': fields.many2one('mail.template', "Email Template", help='The email to send when this activity is activated'),
- 'report_id': fields.many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated', ),
- 'server_action_id': fields.many2one('ir.actions.server', string='Action',
- help= "The action to perform when this activity is activated"),
- 'to_ids': fields.one2many('marketing.campaign.transition',
- 'activity_from_id',
- 'Next Activities'),
- 'from_ids': fields.one2many('marketing.campaign.transition',
- 'activity_to_id',
- 'Previous Activities'),
- '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')),
- '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=0),
- 'signal': fields.char('Signal',
- help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
- 'keep_if_condition_not_met': fields.boolean("Don't Delete Workitems",
- help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
- }
-
- _defaults = {
- 'type': lambda *a: 'email',
- 'condition': lambda *a: 'True',
- }
-
- def search(self, cr, uid, args, offset=0, limit=None, order=None,
- context=None, count=False):
- if context == None:
- context = {}
- if 'segment_id' in context and context['segment_id']:
- segment_obj = self.pool.get('marketing.campaign.segment').browse(cr,
- uid, context['segment_id'])
- act_ids = []
- for activity in segment_obj.campaign_id.activity_ids:
- act_ids.append(activity.id)
- return act_ids
- return super(marketing_campaign_activity, self).search(cr, uid, args,
- offset, limit, order, context, count)
-
- #dead code
- def _process_wi_report(self, cr, uid, activity, workitem, context=None):
- report_data, format = render_report(cr, uid, [], activity.report_id.report_name, {}, context=context)
- attach_vals = {
- 'name': '%s_%s_%s'%(activity.report_id.report_name,
- activity.name,workitem.partner_id.name),
- 'datas_fname': '%s.%s'%(activity.report_id.report_name,
- activity.report_id.report_type),
- 'datas': base64.encodestring(report_data),
- }
- self.pool.get('ir.attachment').create(cr, uid, attach_vals)
- return True
-
- def _process_wi_email(self, cr, uid, activity, workitem, context=None):
- return self.pool.get('mail.template').send_mail(cr, uid,
- activity.email_template_id.id,
- workitem.res_id, context=context)
-
- #dead code
- def _process_wi_action(self, cr, uid, activity, workitem, context=None):
- if context is None:
- context = {}
- server_obj = self.pool.get('ir.actions.server')
-
- action_context = dict(context,
- active_id=workitem.res_id,
- active_ids=[workitem.res_id],
- active_model=workitem.object_id.model,
- workitem=workitem)
- server_obj.run(cr, uid, [activity.server_action_id.id],
- context=action_context)
- return True
-
- def process(self, cr, uid, act_id, wi_id, context=None):
- activity = self.browse(cr, uid, act_id, context=context)
- method = '_process_wi_%s' % (activity.type,)
- action = getattr(self, method, None)
- if not action:
- raise NotImplementedError('Method %r is not implemented on %r object.' % (method, self))
-
- workitem_obj = self.pool.get('marketing.campaign.workitem')
- workitem = workitem_obj.browse(cr, uid, wi_id, context=context)
- return action(cr, uid, activity, workitem, context=context)
-
-
-class marketing_campaign_transition(osv.osv):
- _name = "marketing.campaign.transition"
- _description = "Campaign Transition"
-
- _interval_units = [
- ('hours', 'Hour(s)'),
- ('days', 'Day(s)'),
- ('months', 'Month(s)'),
- ('years', 'Year(s)'),
- ]
-
- def _get_name(self, cr, uid, ids, fn, args, context=None):
- # name formatters that depend on trigger
- formatters = {
- 'auto': _('Automatic transition'),
- 'time': _('After %(interval_nbr)d %(interval_type)s'),
- 'cosmetic': _('Cosmetic'),
- }
- # get the translations of the values of selection field 'interval_type'
- fields = self.fields_get(cr, uid, ['interval_type'], context=context)
- interval_type_selection = dict(fields['interval_type']['selection'])
-
- result = dict.fromkeys(ids, False)
- for trans in self.browse(cr, uid, ids, context=context):
- values = {
- 'interval_nbr': trans.interval_nbr,
- 'interval_type': interval_type_selection.get(trans.interval_type, ''),
- }
- result[trans.id] = formatters[trans.trigger] % values
- return result
-
-
- def _delta(self, cr, uid, ids, context=None):
- assert len(ids) == 1
- transition = self.browse(cr, uid, ids[0], context=context)
- if transition.trigger != 'time':
- raise ValueError('Delta is only relevant for timed transition.')
- return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
-
-
- _columns = {
- 'name': fields.function(_get_name, string='Name',
- type='char', size=128),
- 'activity_from_id': fields.many2one('marketing.campaign.activity',
- 'Previous Activity', select=1,
- required=True, ondelete="cascade"),
- 'activity_to_id': fields.many2one('marketing.campaign.activity',
- 'Next Activity',
- required=True, ondelete="cascade"),
- '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'),
- ('cosmetic', 'Cosmetic'), # fake plastic transition
- ],
- 'Trigger', required=True,
- help="How is the destination workitem triggered"),
- }
-
- _defaults = {
- 'interval_nbr': 1,
- 'interval_type': 'days',
- 'trigger': 'time',
- }
- def _check_campaign(self, cr, uid, ids, context=None):
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.activity_from_id.campaign_id != obj.activity_to_id.campaign_id:
- return False
- return True
-
- _constraints = [
- (_check_campaign, 'The To/From Activity of transition must be of the same Campaign ', ['activity_from_id,activity_to_id']),
- ]
-
- _sql_constraints = [
- ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
- ]
-
-
-class marketing_campaign_workitem(osv.osv):
- _name = "marketing.campaign.workitem"
- _description = "Campaign Workitem"
-
- def _res_name_get(self, cr, uid, ids, field_name, arg, context=None):
- res = dict.fromkeys(ids, '/')
- for wi in self.browse(cr, uid, ids, context=context):
- if not wi.res_id:
- continue
-
- proxy = self.pool[wi.object_id.model]
- if not proxy.exists(cr, uid, [wi.res_id]):
- continue
- ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
- if ng:
- res[wi.id] = ng[0][1]
- return res
-
- def _resource_search(self, cr, uid, obj, name, args, domain=None, context=None):
- """Returns id of workitem whose resource_name matches with the given name"""
- if not len(args):
- return []
-
- condition_name = None
- for domain_item in args:
- # we only use the first domain criterion and ignore all the rest including operators
- if isinstance(domain_item, (list,tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_name':
- condition_name = [None, domain_item[1], domain_item[2]]
- break
-
- assert condition_name, "Invalid search domain for marketing_campaign_workitem.res_name. It should use 'res_name'"
-
- cr.execute("""select w.id, w.res_id, m.model \
- from marketing_campaign_workitem w \
- left join marketing_campaign_activity a on (a.id=w.activity_id)\
- left join marketing_campaign c on (c.id=a.campaign_id)\
- left join ir_model m on (m.id=c.object_id)
- """)
- res = cr.fetchall()
- workitem_map = {}
- matching_workitems = []
- for id, res_id, model in res:
- workitem_map.setdefault(model,{}).setdefault(res_id,set()).add(id)
- for model, id_map in workitem_map.iteritems():
- model_pool = self.pool[model]
- condition_name[0] = model_pool._rec_name
- condition = [('id', 'in', id_map.keys()), condition_name]
- for res_id in model_pool.search(cr, uid, condition, context=context):
- matching_workitems.extend(id_map[res_id])
- return [('id', 'in', list(set(matching_workitems)))]
-
- _columns = {
- 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment', readonly=True),
- 'activity_id': fields.many2one('marketing.campaign.activity','Activity',
- required=True, readonly=True),
- 'campaign_id': fields.related('activity_id', 'campaign_id',
- type='many2one', relation='marketing.campaign', string='Campaign', readonly=True, store=True),
- 'object_id': fields.related('activity_id', 'campaign_id', 'object_id',
- type='many2one', relation='ir.model', string='Resource', select=1, readonly=True, store=True),
- 'res_id': fields.integer('Resource ID', select=1, readonly=True),
- 'res_name': fields.function(_res_name_get, string='Resource Name', fnct_search=_resource_search, type="char", size=64),
- 'date': fields.datetime('Execution Date', help='If date is not set, this workitem has to be run manually', readonly=True),
- 'partner_id': fields.many2one('res.partner', 'Partner', select=1, readonly=True),
- 'state': fields.selection([ ('todo', 'To Do'),
- ('cancelled', 'Cancelled'),
- ('exception', 'Exception'),
- ('done', 'Done'),
- ], 'Status', readonly=True, copy=False),
- 'error_msg' : fields.text('Error Message', readonly=True)
- }
- _defaults = {
- 'state': lambda *a: 'todo',
- 'date': False,
- }
-
- @api.cr_uid_ids_context
- def button_draft(self, cr, uid, workitem_ids, context=None):
- for wi in self.browse(cr, uid, workitem_ids, context=context):
- if wi.state in ('exception', 'cancelled'):
- self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
- return True
-
- @api.cr_uid_ids_context
- def button_cancel(self, cr, uid, workitem_ids, context=None):
- for wi in self.browse(cr, uid, workitem_ids, context=context):
- if wi.state in ('todo','exception'):
- self.write(cr, uid, [wi.id], {'state':'cancelled'}, context=context)
- return True
-
- def _process_one(self, cr, uid, workitem, context=None):
- if workitem.state != 'todo':
- return False
-
- activity = workitem.activity_id
- proxy = self.pool[workitem.object_id.model]
- object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
-
- eval_context = {
- 'activity': activity,
- 'workitem': workitem,
- 'object': object_id,
- 'resource': object_id,
- 'transitions': activity.to_ids,
- 're': re,
- }
- try:
- condition = activity.condition
- campaign_mode = workitem.campaign_id.mode
- if condition:
- if not eval(condition, eval_context):
- if activity.keep_if_condition_not_met:
- workitem.write({'state': 'cancelled'})
- else:
- workitem.unlink()
- return
- result = True
- if campaign_mode in ('manual', 'active'):
- Activities = self.pool.get('marketing.campaign.activity')
- result = Activities.process(cr, uid, activity.id, workitem.id,
- context=context)
-
- values = dict(state='done')
- if not workitem.date:
- values['date'] = datetime.now().strftime(DT_FMT)
- workitem.write(values)
-
- if result:
- # process _chain
- workitem.refresh() # reload
- date = datetime.strptime(workitem.date, DT_FMT)
-
- for transition in activity.to_ids:
- if transition.trigger == 'cosmetic':
- continue
- launch_date = False
- if transition.trigger == 'auto':
- launch_date = date
- elif transition.trigger == 'time':
- launch_date = date + 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 on the trigger and the campaign mode
- # we know whether we must run the newly created workitem.
- #
- # rows = transition trigger \ colums = campaign mode
- #
- # test test_realtime manual normal (active)
- # time Y N N N
- # cosmetic N N N N
- # auto Y Y N Y
- #
-
- run = (transition.trigger == 'auto' \
- and campaign_mode != 'manual') \
- or (transition.trigger == 'time' \
- and campaign_mode == 'test')
- if run:
- new_wi = self.browse(cr, uid, wi_id, context)
- self._process_one(cr, uid, new_wi, context)
-
- except Exception:
- tb = "".join(format_exception(*exc_info()))
- workitem.write({'state': 'exception', 'error_msg': tb})
-
- @api.cr_uid_ids_context
- def process(self, cr, uid, workitem_ids, context=None):
- for wi in self.browse(cr, uid, workitem_ids, context=context):
- self._process_one(cr, uid, wi, context=context)
- return True
-
- def process_all(self, cr, uid, camp_ids=None, context=None):
- camp_obj = self.pool.get('marketing.campaign')
- 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:
- domain = [('campaign_id', '=', camp.id), ('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=context)
- return True
-
- def preview(self, cr, uid, ids, context=None):
- res = {}
- wi_obj = self.browse(cr, uid, ids[0], context=context)
- if wi_obj.activity_id.type == 'email':
- view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'email_template_preview_form')
- res = {
- 'name': _('Email Preview'),
- 'view_type': 'form',
- 'view_mode': 'form,tree',
- 'res_model': 'email_template.preview',
- 'view_id': False,
- 'context': context,
- 'views': [(view_id and view_id[1] or 0, 'form')],
- 'type': 'ir.actions.act_window',
- 'target': 'new',
- 'context': "{'template_id':%d,'default_res_id':%d}"%
- (wi_obj.activity_id.email_template_id.id,
- wi_obj.res_id)
- }
-
- elif wi_obj.activity_id.type == 'report':
- datas = {
- 'ids': [wi_obj.res_id],
- 'model': wi_obj.object_id.model
- }
- res = {
- 'type' : 'ir.actions.report.xml',
- 'report_name': wi_obj.activity_id.report_id.report_name,
- 'datas' : datas,
- }
- else:
- raise UserError(_('The current step for this item has no email or report to preview.'))
- return res
-
-
-class mail_template(osv.osv):
- _inherit = "mail.template"
- _defaults = {
- 'model_id': lambda obj, cr, uid, context: context.get('object_id',False),
- }
-
- # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
-
-
-class report_xml(osv.osv):
- _inherit = 'ir.actions.report.xml'
- def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
- if context is None:
- context = {}
- object_id = context.get('object_id')
- if object_id:
- model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model
- args.append(('model', '=', model))
- return super(report_xml, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
diff --git a/addons/marketing_campaign/models/__init__.py b/addons/marketing_campaign/models/__init__.py
new file mode 100644
index 00000000000..70b017971bc
--- /dev/null
+++ b/addons/marketing_campaign/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import ir_actions
+import mail_template
+import marketing_campaign
\ No newline at end of file
diff --git a/addons/marketing_campaign/models/ir_actions.py b/addons/marketing_campaign/models/ir_actions.py
new file mode 100644
index 00000000000..cb9a55db6cc
--- /dev/null
+++ b/addons/marketing_campaign/models/ir_actions.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class IrActionsReportXml(models.Model):
+ _inherit = 'ir.actions.report.xml'
+
+ @api.model
+ def search(self, args, offset=0, limit=None, order=None, count=False):
+ model_id = self.env.context.get('object_id')
+ if model_id:
+ model = self.env['ir.model'].browse(model_id).model
+ args.append(('model', '=', model))
+ return super(IrActionsReportXml, self).search(args, offset=offset, limit=limit, order=order, count=count)
diff --git a/addons/marketing_campaign/models/mail_template.py b/addons/marketing_campaign/models/mail_template.py
new file mode 100644
index 00000000000..5b6d6be7777
--- /dev/null
+++ b/addons/marketing_campaign/models/mail_template.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class MailTemplate(models.Model):
+ _inherit = "mail.template"
+
+ model_id = fields.Many2one('ir.model', default=lambda self: self.env.context.get('object_id', False))
+
+ # TODO: add constraint to prevent disabling / disapproving an email account used in a running campaign
diff --git a/addons/marketing_campaign/models/marketing_campaign.py b/addons/marketing_campaign/models/marketing_campaign.py
new file mode 100644
index 00000000000..597f5eabc89
--- /dev/null
+++ b/addons/marketing_campaign/models/marketing_campaign.py
@@ -0,0 +1,602 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import time
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
+from traceback import format_exception
+from sys import exc_info
+import re
+import openerp.addons.decimal_precision as dp
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.safe_eval import safe_eval as eval
+
+
+_intervalTypes = {
+ 'hours': lambda interval: relativedelta(hours=interval),
+ 'days': lambda interval: relativedelta(days=interval),
+ 'months': lambda interval: relativedelta(months=interval),
+ 'years': lambda interval: relativedelta(years=interval),
+}
+
+class MarketingCampaign(models.Model):
+ _name = "marketing.campaign"
+ _description = "Marketing Campaign"
+
+ name = fields.Char('Name', required=True)
+ object_id = fields.Many2one('ir.model', 'Resource', required=True,
+ help="Choose the resource on which you want this campaign to be run")
+ partner_field_id = fields.Many2one('ir.model.fields', 'Partner Field',
+ domain="[('model_id', '=', object_id), ('ttype', '=', 'many2one'), ('relation', '=', 'res.partner')]",
+ help="The generated workitems will be linked to the partner related to the record. "
+ "If the record is the partner itself leave this field empty. "
+ "This is useful for reporting purposes, via the Campaign Analysis or Campaign Follow-up views.")
+ unique_field_id = fields.Many2one('ir.model.fields', 'Unique Field',
+ domain="[('model_id', '=', object_id), ('ttype', 'in', ['char','int','many2one','text','selection'])]",
+ help='If set, this field will help segments that work in "no duplicates" mode to avoid '
+ 'selecting similar records twice. Similar records are records that have the same value for '
+ 'this unique field. For example by choosing the "email_from" field for CRM Leads you would prevent '
+ 'sending the same campaign to the same email address again. If not set, the "no duplicates" segments '
+ "will only avoid selecting the same record again if it entered the campaign previously. "
+ "Only easily comparable fields like textfields, integers, selections or single relationships may be used.")
+ mode = fields.Selection([
+ ('test', 'Test Directly'),
+ ('test_realtime', 'Test in Realtime'),
+ ('manual', 'With Manual Confirmation'),
+ ('active', 'Normal')
+ ], 'Mode', required=True, default="test",
+ help="Test - It creates and process all the activities directly (without waiting "
+ "for the delay on transitions) but does not send emails or produce reports. \n"
+ "Test in Realtime - It creates and processes all the activities directly but does "
+ "not send emails or produce reports.\n"
+ "With Manual Confirmation - the campaigns runs normally, but the user has to \n "
+ "validate all workitem manually.\n"
+ "Normal - the campaign runs normally and automatically sends all emails and "
+ "reports (be very careful with this mode, you're live!)")
+ state = fields.Selection([
+ ('draft', 'New'),
+ ('running', 'Running'),
+ ('cancelled', 'Cancelled'),
+ ('done', 'Done')
+ ], 'Status', copy=False, default="draft")
+ activity_ids = fields.One2many('marketing.campaign.activity', 'campaign_id', 'Activities')
+ 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'))
+ segment_ids = fields.One2many('marketing.campaign.segment', 'campaign_id', 'Segments', readonly=False)
+ segments_count = fields.Integer(compute='_compute_segments_count', string='Segments')
+
+ @api.multi
+ def _compute_segments_count(self):
+ for campaign in self:
+ campaign.segments_count = len(campaign.segment_ids)
+
+ @api.multi
+ def state_running_set(self):
+ # TODO check that all subcampaigns are running
+ self.ensure_one()
+
+ if not self.activity_ids:
+ raise UserError(_("The campaign cannot be started. There are no activities in it."))
+
+ has_start = False
+ has_signal_without_from = False
+
+ for activity in self.activity_ids:
+ if activity.start:
+ has_start = True
+ if activity.signal and len(activity.from_ids) == 0:
+ has_signal_without_from = True
+
+ if not has_start and not has_signal_without_from:
+ raise UserError(_("The campaign cannot be started. It does not have any starting activity. Modify campaign's activities to mark one as the starting point."))
+
+ return self.write({'state': 'running'})
+
+ @api.multi
+ def state_done_set(self):
+ # TODO check that this campaign is not a subcampaign in running mode.
+ if self.mapped('segment_ids').filtered(lambda segment: segment.state == 'running'):
+ raise UserError(_("The campaign cannot be marked as done before all segments are closed."))
+ return self.write({'state': 'done'})
+
+ @api.multi
+ def state_cancel_set(self):
+ # TODO check that this campaign is not a subcampaign in running mode.
+ return self.write({'state': 'cancelled'})
+
+ def _get_partner_for(self, record):
+ partner_field = self.partner_field_id.name
+ if partner_field:
+ return record[partner_field]
+ elif self.object_id.model == 'res.partner':
+ return record
+ return None
+
+ # prevent duplication until the server properly duplicates several levels of nested o2m
+ @api.multi
+ def copy(self, cr, uid, id, default=None, context=None):
+ self.ensure_one()
+ raise UserError(_('Duplicating campaigns is not supported.'))
+
+ def _find_duplicate_workitems(self, record):
+ """Finds possible duplicates workitems for a record in this campaign, based on a uniqueness
+ field.
+
+ :param record: browse_record to find duplicates workitems for.
+ :param campaign_rec: browse_record of campaign
+ """
+ self.ensure_one()
+ duplicate_workitem_domain = [('res_id', '=', record.id), ('campaign_id', '=', self.id)]
+ unique_field = self.unique_field_id
+ if unique_field:
+ unique_value = getattr(record, unique_field.name, None)
+ if unique_value:
+ if unique_field.ttype == 'many2one':
+ unique_value = unique_value.id
+ similar_res_ids = self.env[self.object_id.model].search([(unique_field.name, '=', unique_value)])
+ if similar_res_ids:
+ duplicate_workitem_domain = [
+ ('res_id', 'in', similar_res_ids),
+ ('campaign_id', '=', self.id)
+ ]
+ return self.env['marketing.campaign.workitem'].search(duplicate_workitem_domain)
+
+
+class MarketingCampaignSegment(models.Model):
+ _name = "marketing.campaign.segment"
+ _description = "Campaign Segment"
+ _order = "name"
+
+ name = fields.Char('Name', required=True)
+ campaign_id = fields.Many2one('marketing.campaign', 'Campaign', required=True, index=True, ondelete="cascade")
+ object_id = fields.Many2one('ir.model', related='campaign_id.object_id', string='Resource')
+ ir_filter_id = fields.Many2one('ir.filters', 'Filter', ondelete="restrict",
+ domain=lambda self: [('model_id', '=', self.object_id._name)],
+ help="Filter to select the matching resource records that belong to this segment. "
+ "New filters can be created and saved using the advanced search on the list view of the Resource. "
+ "If no filter is set, all records are selected without filtering. "
+ "The synchronization mode may also add a criterion to the filter.")
+ sync_last_date = fields.Datetime('Last Synchronization',
+ help="Date on which this segment was synchronized last time (automatically or manually)")
+ sync_mode = fields.Selection([
+ ('create_date', 'Only records created after last sync'),
+ ('write_date', 'Only records modified after last sync (no duplicates)'),
+ ('all', 'All records (no duplicates)')],
+ 'Synchronization mode', default='create_date',
+ help="Determines an additional criterion to add to the filter when selecting new records to inject in the campaign. "
+ '"No duplicates" prevents selecting records which have already entered the campaign previously.'
+ 'If the campaign has a "unique field" set, "no duplicates" will also prevent selecting records which have '
+ 'the same value for the unique field as other records that already entered the campaign.')
+ state = fields.Selection([
+ ('draft', 'New'),
+ ('cancelled', 'Cancelled'),
+ ('running', 'Running'),
+ ('done', 'Done')],
+ 'Status', copy=False, default='draft')
+ date_run = fields.Datetime('Launch Date', help="Initial start date of this segment.")
+ date_done = fields.Datetime('End Date', help="Date this segment was last closed or cancelled.")
+ date_next_sync = fields.Datetime(compute='_compute_date_next_sync', string='Next Synchronization',
+ help="Next time the synchronization job is scheduled to run automatically")
+
+ def _compute_date_next_sync(self):
+ # next auto sync date is same for all segments
+ sync_job = self.sudo().env.ref('marketing_campaign.ir_cron_marketing_campaign_every_day')
+ self.date_next_sync = sync_job and sync_job.nextcall or False
+
+ @api.constrains('ir_filter_id', 'campaign_id')
+ def _check_model(self):
+ if self.filtered(lambda segment: segment.ir_filter_id and
+ segment.campaign_id.object_id.model != segment.ir_filter_id.model_id):
+ raise ValidationError(_('Model of filter must be same as resource model of Campaign'))
+
+ @api.onchange('campaign_id')
+ def onchange_campaign_id(self):
+ res = {'domain': {'ir_filter_id': []}}
+ model = self.campaign_id.object_id.model
+ if model:
+ res['domain']['ir_filter_id'] = [('model_id', '=', model)]
+ else:
+ res.ir_filter_id = False
+ return res
+
+ @api.multi
+ def state_running_set(self):
+ self.ensure_one()
+ vals = {'state': 'running'}
+ if not self.date_run:
+ vals['date_run'] = fields.Datetime.now()
+ return self.write(vals)
+
+ @api.multi
+ def state_done_set(self):
+ self.env["marketing.campaign.workitem"].search([
+ ('state', '=', 'todo'),
+ ('segment_id', 'in', self.ids)
+ ]).write({'state': 'cancelled'})
+ return self.write({'state': 'done', 'date_done': fields.Datetime.now()})
+
+ @api.multi
+ def state_cancel_set(self):
+ self.env["marketing.campaign.workitem"].search([
+ ('state', '=', 'todo'),
+ ('segment_id', 'in', self.ids)
+ ]).write({'state': 'cancelled'})
+ return self.write({'state': 'cancelled', 'date_done': fields.Datetime.now()})
+
+ @api.multi
+ def process_segment(self):
+ Workitems = self.env['marketing.campaign.workitem']
+ Activities = self.env['marketing.campaign.activity']
+ if not self:
+ self = self.search([('state', '=', 'running')])
+
+ action_date = fields.Datetime.now()
+ campaigns = self.env['marketing.campaign']
+ for segment in self:
+ if segment.campaign_id.state != 'running':
+ continue
+
+ campaigns |= segment.campaign_id
+ activity_ids = Activities.search([('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)]).ids
+
+ criteria = []
+ if segment.sync_last_date and segment.sync_mode != 'all':
+ criteria += [(segment.sync_mode, '>', segment.sync_last_date)]
+ if segment.ir_filter_id:
+ criteria += eval(segment.ir_filter_id.domain)
+
+ # XXX TODO: rewrite this loop more efficiently without doing 1 search per record!
+ for record in self.env[segment.object_id.model].search(criteria):
+ # avoid duplicate workitem for the same resource
+ if segment.sync_mode in ('write_date', 'all'):
+ if segment.campaign_id._find_duplicate_workitems(record):
+ continue
+
+ wi_vals = {
+ 'segment_id': segment.id,
+ 'date': action_date,
+ 'state': 'todo',
+ 'res_id': record.id
+ }
+
+ partner = segment.campaign_id._get_partner_for(record)
+ if partner:
+ wi_vals['partner_id'] = partner.id
+
+ for activity_id in activity_ids:
+ wi_vals['activity_id'] = activity_id
+ Workitems.create(wi_vals)
+
+ segment.write({'sync_last_date': action_date})
+ Workitems.process_all(campaigns.ids)
+ return True
+
+
+class MarketingCampaignActivity(models.Model):
+ _name = "marketing.campaign.activity"
+ _order = "name"
+ _description = "Campaign Activity"
+
+ name = fields.Char('Name', required=True)
+ campaign_id = fields.Many2one('marketing.campaign', 'Campaign', required=True, ondelete='cascade', index=True)
+ object_id = fields.Many2one(related='campaign_id.object_id', relation='ir.model', string='Object', readonly=True)
+ start = fields.Boolean('Start', help="This activity is launched when the campaign starts.", index=True)
+ condition = fields.Text('Condition', required=True, default="True",
+ help="Python expression to decide whether the activity can be executed, otherwise it will be deleted or cancelled."
+ "The expression may use the following [browsable] variables:\n"
+ " - activity: the campaign activity\n"
+ " - workitem: the campaign workitem\n"
+ " - resource: the resource object this campaign item represents\n"
+ " - transitions: list of campaign transitions outgoing from this activity\n"
+ "...- re: Python regular expression module")
+ action_type = fields.Selection([
+ ('email', 'Email'),
+ ('report', 'Report'),
+ ('action', 'Custom Action'),
+ ], 'Type', required=True, oldname="type", default="email",
+ help="The type of action to execute when an item enters this activity, such as:\n"
+ "- Email: send an email using a predefined email template \n"
+ "- Report: print an existing Report defined on the resource item and save it into a specific directory \n"
+ "- Custom Action: execute a predefined action, e.g. to modify the fields of the resource record")
+ email_template_id = fields.Many2one('mail.template', "Email Template", help='The email to send when this activity is activated')
+ report_id = fields.Many2one('ir.actions.report.xml', "Report", help='The report to generate when this activity is activated')
+ server_action_id = fields.Many2one('ir.actions.server', string='Action',
+ help="The action to perform when this activity is activated")
+ to_ids = fields.One2many('marketing.campaign.transition', 'activity_from_id', 'Next Activities')
+ from_ids = fields.One2many('marketing.campaign.transition', 'activity_to_id', 'Previous Activities')
+ variable_cost = fields.Float('Variable Cost', digits_compute=dp.get_precision('Product Price'),
+ 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")
+ revenue = fields.Float('Revenue', digits=0,
+ 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")
+ signal = fields.Char('Signal',
+ help="An activity with a signal can be called programmatically. Be careful, the workitem is always created when "
+ "a signal is sent")
+ keep_if_condition_not_met = fields.Boolean("Don't Delete Workitems",
+ help="By activating this option, workitems that aren't executed because the condition is not met are marked as "
+ "cancelled instead of being deleted.")
+
+ @api.model
+ def search(self, args, offset=0, limit=None, order=None, count=False):
+ if 'segment_id' in self.env.context:
+ return self.env['marketing.campaign.segment'].browse(self.env.context['segment_id']).campaign_id.activity_ids
+ return super(MarketingCampaignActivity, self).search(args, offset, limit, order, count)
+
+ @api.multi
+ def _process_wi_email(self, workitem):
+ self.ensure_one()
+ return self.email_template_id.send_mail(workitem.res_id)
+
+ @api.multi
+ def process(self, workitem):
+ self.ensure_one()
+ method = '_process_wi_%s' % (self.action_type,)
+ action = getattr(self, method, None)
+ if not action:
+ raise NotImplementedError('Method %r is not implemented on %r object.' % (method, self._model))
+ return action(workitem)
+
+
+class MarketingCampaignTransition(models.Model):
+ _name = "marketing.campaign.transition"
+ _description = "Campaign Transition"
+
+ _interval_units = [
+ ('hours', 'Hour(s)'),
+ ('days', 'Day(s)'),
+ ('months', 'Month(s)'),
+ ('years', 'Year(s)'),
+ ]
+
+ name = fields.Char(compute='_compute_name', string='Name')
+ activity_from_id = fields.Many2one('marketing.campaign.activity', 'Previous Activity', index=1, required=True, ondelete="cascade")
+ activity_to_id = fields.Many2one('marketing.campaign.activity', 'Next Activity', required=True, ondelete="cascade")
+ interval_nbr = fields.Integer('Interval Value', required=True, default=1)
+ interval_type = fields.Selection(_interval_units, 'Interval Unit', required=True, default='days')
+ trigger = fields.Selection([
+ ('auto', 'Automatic'),
+ ('time', 'Time'),
+ ('cosmetic', 'Cosmetic'), # fake plastic transition
+ ], 'Trigger', required=True, default='time',
+ help="How is the destination workitem triggered")
+
+ _sql_constraints = [
+ ('interval_positive', 'CHECK(interval_nbr >= 0)', 'The interval must be positive or zero')
+ ]
+
+ def _compute_name(self):
+ # name formatters that depend on trigger
+ formatters = {
+ 'auto': _('Automatic transition'),
+ 'time': _('After %(interval_nbr)d %(interval_type)s'),
+ 'cosmetic': _('Cosmetic'),
+ }
+ # get the translations of the values of selection field 'interval_type'
+ model_fields = self.fields_get(['interval_type'])
+ interval_type_selection = dict(model_fields['interval_type']['selection'])
+
+ for transition in self:
+ values = {
+ 'interval_nbr': transition.interval_nbr,
+ 'interval_type': interval_type_selection.get(transition.interval_type, ''),
+ }
+ transition.name = formatters[transition.trigger] % values
+
+ @api.constrains('activity_from_id', 'activity_to_id')
+ def _check_campaign(self):
+ if self.filtered(lambda transition: transition.activity_from_id.campaign_id != transition.activity_to_id.campaign_id):
+ return ValidationError(_('The To/From Activity of transition must be of the same Campaign'))
+
+ def _delta(self):
+ self.ensure_one()
+ if self.trigger != 'time':
+ raise ValueError('Delta is only relevant for timed transition.')
+ return relativedelta(**{str(self.interval_type): self.interval_nbr})
+
+
+class MarketingCampaignWorkitem(models.Model):
+ _name = "marketing.campaign.workitem"
+ _description = "Campaign Workitem"
+
+ segment_id = fields.Many2one('marketing.campaign.segment', 'Segment', readonly=True)
+ activity_id = fields.Many2one('marketing.campaign.activity', 'Activity', required=True, readonly=True)
+ campaign_id = fields.Many2one('marketing.campaign', related='activity_id.campaign_id', string='Campaign', readonly=True, store=True)
+ object_id = fields.Many2one('ir.model', related='activity_id.campaign_id.object_id', string='Resource', index=1, readonly=True, store=True)
+ res_id = fields.Integer('Resource ID', index=1, readonly=True)
+ res_name = fields.Char(compute='_compute_res_name', string='Resource Name', search='search_res_name')
+ date = fields.Datetime('Execution Date', readonly=True, default=False,
+ help='If date is not set, this workitem has to be run manually')
+ partner_id = fields.Many2one('res.partner', 'Partner', index=1, readonly=True)
+ state = fields.Selection([
+ ('todo', 'To Do'),
+ ('cancelled', 'Cancelled'),
+ ('exception', 'Exception'),
+ ('done', 'Done'),
+ ], 'Status', readonly=True, copy=False, default='todo')
+ error_msg = fields.Text('Error Message', readonly=True)
+
+ def _compute_res_name(self):
+ for workitem in self:
+ proxy = self.env[workitem.object_id.model]
+ record = proxy.browse(workitem.res_id)
+ if not workitem.res_id or not record:
+ workitem.res_name = '/'
+ continue
+ workitem.res_name = record.name_get()[0][1]
+
+ def _search_res_name(self, operator, operand):
+ """Returns a domain with ids of workitem whose `operator` matches with the given `operand`"""
+ if not operand:
+ return []
+
+ condition_name = [None, operator, operand]
+
+ self.env.cr.execute("""
+ SELECT w.id, w.res_id, m.model
+ FROM marketing_campaign_workitem w \
+ LEFT JOIN marketing_campaign_activity a ON (a.id=w.activity_id)\
+ LEFT JOIN marketing_campaign c ON (c.id=a.campaign_id)\
+ LEFT JOIN ir_model m ON (m.id=c.object_id)
+ """)
+ res = self.env.cr.fetchall()
+ workitem_map = {}
+ matching_workitems = []
+ for id, res_id, model in res:
+ workitem_map.setdefault(model, {}).setdefault(res_id, set()).add(id)
+ for model, id_map in workitem_map.iteritems():
+ Model = self.env[model]
+ condition_name[0] = Model._rec_name
+ condition = [('id', 'in', id_map.keys()), condition_name]
+ for record in Model.search(condition):
+ matching_workitems.extend(id_map[record.id])
+ return [('id', 'in', list(set(matching_workitems)))]
+
+ @api.multi
+ def button_draft(self):
+ return self.filtered(lambda workitem: workitem.state in ('exception', 'cancelled')).write({'state': 'todo'})
+
+ @api.multi
+ def button_cancel(self):
+ return self.filtered(lambda workitem: workitem.state in ('todo', 'exception')).write({'state': 'cancelled'})
+
+ @api.multi
+ def _process_one(self):
+ self.ensure_one()
+ if self.state != 'todo':
+ return False
+
+ activity = self.activity_id
+ resource = self.env[self.object_id.model].browse(self.res_id)
+
+ eval_context = {
+ 'activity': activity,
+ 'workitem': self,
+ 'object': resource,
+ 'resource': resource,
+ 'transitions': activity.to_ids,
+ 're': re,
+ }
+ try:
+ condition = activity.condition
+ campaign_mode = self.campaign_id.mode
+ if condition:
+ if not eval(condition, eval_context):
+ if activity.keep_if_condition_not_met:
+ self.write({'state': 'cancelled'})
+ else:
+ self.unlink()
+ return
+ result = True
+ if campaign_mode in ('manual', 'active'):
+ result = activity.process(self)
+
+ values = {'state': 'done'}
+ if not self.date:
+ values['date'] = fields.Datetime.now()
+ self.write(values)
+
+ if result:
+ # process _chain
+ self.refresh() # reload
+ execution_date = fields.Datetime.from_string(self.date)
+
+ for transition in activity.to_ids:
+ if transition.trigger == 'cosmetic':
+ continue
+ launch_date = False
+ if transition.trigger == 'auto':
+ launch_date = execution_date
+ elif transition.trigger == 'time':
+ launch_date = execution_date + transition._delta()
+
+ if launch_date:
+ launch_date = fields.Datetime.to_string(launch_date)
+ values = {
+ 'date': launch_date,
+ 'segment_id': self.segment_id.id,
+ 'activity_id': transition.activity_to_id.id,
+ 'partner_id': self.partner_id.id,
+ 'res_id': self.res_id,
+ 'state': 'todo',
+ }
+ workitem = self.create(values)
+
+ # Now, depending on the trigger and the campaign mode
+ # we know whether we must run the newly created workitem.
+ #
+ # rows = transition trigger \ colums = campaign mode
+ #
+ # test test_realtime manual normal (active)
+ # time Y N N N
+ # cosmetic N N N N
+ # auto Y Y N Y
+ #
+
+ run = (transition.trigger == 'auto' and campaign_mode != 'manual') or (transition.trigger == 'time' and campaign_mode == 'test')
+ if run:
+ workitem._process_one()
+
+ except Exception:
+ tb = "".join(format_exception(*exc_info()))
+ self.write({'state': 'exception', 'error_msg': tb})
+
+ @api.multi
+ def process(self):
+ for workitem in self:
+ workitem._process_one()
+ return True
+
+ @api.model
+ def process_all(self, camp_ids=None):
+ if camp_ids is None:
+ campaigns = self.env['marketing.campaign'].search([('state', '=', 'running')])
+ else:
+ campaigns = self.env['marketing.campaign'].browse(camp_ids)
+ for campaign in campaigns.filtered(lambda campaign: campaign.mode != 'manual'):
+ while True:
+ domain = [('campaign_id', '=', campaign.id), ('state', '=', 'todo'), ('date', '!=', False)]
+ if campaign.mode in ('test_realtime', 'active'):
+ domain += [('date', '<=', fields.Datetime.now())]
+ workitems = self.search(domain)
+ if not workitems:
+ break
+ workitems.process()
+ return True
+
+ @api.multi
+ def preview(self):
+ self.ensure_one()
+ res = {}
+ if self.activity_id.action_type == 'email':
+ view_ref = self.env.ref('mail.email_template_preview_form')
+ res = {
+ 'name': _('Email Preview'),
+ 'view_type': 'form',
+ 'view_mode': 'form,tree',
+ 'res_model': 'email_template.preview',
+ 'view_id': False,
+ 'context': self.env.context,
+ 'views': [(view_ref and view_ref.id or False, 'form')],
+ 'type': 'ir.actions.act_window',
+ 'target': 'new',
+ 'context': "{'template_id': %d,'default_res_id': %d}" % (self.activity_id.email_template_id.id, self.res_id)
+ }
+
+ elif self.activity_id.action_type == 'report':
+ datas = {
+ 'ids': [self.res_id],
+ 'model': self.object_id.model
+ }
+ res = {
+ 'type': 'ir.actions.report.xml',
+ 'report_name': self.activity_id.report_id.report_name,
+ 'datas': datas,
+ }
+ else:
+ raise UserError(_('The current step for this item has no email or report to preview.'))
+ return res
diff --git a/addons/marketing_campaign/report/campaign_analysis.py b/addons/marketing_campaign/report/campaign_analysis.py
index bb0a8e6510d..82d7efac3f9 100644
--- a/addons/marketing_campaign/report/campaign_analysis.py
+++ b/addons/marketing_campaign/report/campaign_analysis.py
@@ -1,80 +1,79 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import tools
-from openerp.osv import fields, osv
+from odoo import tools
+from odoo import api, fields, models
-class campaign_analysis(osv.osv):
+class CampaignAnalysis(models.Model):
_name = "campaign.analysis"
_description = "Campaign Analysis"
_auto = False
_rec_name = 'date'
- def _total_cost(self, cr, uid, ids, field_name, arg, context=None):
- """
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of case and section Data’s IDs
- @param context: A standard dictionary for contextual values
- """
- result = {}
- for ca_obj in self.browse(cr, uid, ids, context=context):
- wi_ids = self.pool.get('marketing.campaign.workitem').search(cr, uid,
- [('segment_id.campaign_id', '=', ca_obj.campaign_id.id)])
- total_cost = ca_obj.activity_id.variable_cost + \
- ((ca_obj.campaign_id.fixed_cost or 1.00) / len(wi_ids))
- result[ca_obj.id] = total_cost
- return result
- _columns = {
- 'res_id' : fields.integer('Resource', readonly=True),
- 'year': fields.char('Execution Year', size=4, readonly=True),
- 'month': fields.selection([('01','January'), ('02','February'),
- ('03','March'), ('04','April'),('05','May'), ('06','June'),
- ('07','July'), ('08','August'), ('09','September'),
- ('10','October'), ('11','November'), ('12','December')],
- 'Execution Month', readonly=True),
- 'day': fields.char('Execution Day', size=10, readonly=True),
- 'date': fields.date('Execution Date', readonly=True, select=True),
- 'campaign_id': fields.many2one('marketing.campaign', 'Campaign',
- readonly=True),
- 'activity_id': fields.many2one('marketing.campaign.activity', 'Activity',
- readonly=True),
- 'segment_id': fields.many2one('marketing.campaign.segment', 'Segment',
- readonly=True),
- 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
- 'country_id': fields.related('partner_id', 'country_id',
- type='many2one', relation='res.country',string='Country'),
- 'total_cost' : fields.function(_total_cost, string='Cost',
- type="float"),
- 'revenue': fields.float('Revenue', readonly=True, digits=0),
- 'count' : fields.integer('# of Actions', readonly=True),
- 'state': fields.selection([('todo', 'To Do'),
- ('exception', 'Exception'), ('done', 'Done'),
- ('cancelled', 'Cancelled')], 'Status', readonly=True),
- }
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'campaign_analysis')
- cr.execute("""
- create or replace view campaign_analysis as (
- select
- min(wi.id) as id,
- min(wi.res_id) as res_id,
- to_char(wi.date::date, 'YYYY') as year,
- to_char(wi.date::date, 'MM') as month,
- to_char(wi.date::date, 'YYYY-MM-DD') as day,
- wi.date::date as date,
- s.campaign_id as campaign_id,
- wi.activity_id as activity_id,
- wi.segment_id as segment_id,
- wi.partner_id as partner_id ,
- wi.state as state,
- sum(act.revenue) as revenue,
- count(*) as count
- from
+
+ res_id = fields.Integer('Resource', readonly=True)
+ year = fields.Char('Execution Year', readonly=True)
+ month = fields.Selection([
+ ('01', 'January'),
+ ('02', 'February'),
+ ('03', 'March'),
+ ('04', 'April'),
+ ('05', 'May'),
+ ('06', 'June'),
+ ('07', 'July'),
+ ('08', 'August'),
+ ('09', 'September'),
+ ('10', 'October'),
+ ('11', 'November'),
+ ('12', 'December')
+ ], 'Execution Month', readonly=True)
+ day = fields.Char('Execution Day', readonly=True)
+ date = fields.Date('Execution Date', readonly=True, index=True)
+ campaign_id = fields.Many2one('marketing.campaign', 'Campaign', readonly=True)
+ activity_id = fields.Many2one('marketing.campaign.activity', 'Activity', readonly=True)
+ segment_id = fields.Many2one('marketing.campaign.segment', 'Segment', readonly=True)
+ partner_id = fields.Many2one('res.partner', 'Partner', readonly=True)
+ country_id = fields.Many2one('res.country', related='partner_id.country_id', string='Country')
+ total_cost = fields.Float(compute='_compute_total_cost', string='Cost')
+ revenue = fields.Float('Revenue', readonly=True, digits=0)
+ count = fields.Integer('# of Actions', readonly=True)
+ state = fields.Selection([
+ ('todo', 'To Do'),
+ ('exception', 'Exception'),
+ ('done', 'Done'),
+ ('cancelled', 'Cancelled')
+ ], 'Status', readonly=True)
+
+ @api.multi
+ def _compute_total_cost(self):
+ for analysis in self:
+ wi_count = self.env['marketing.campaign.workitem'].search_count([('segment_id.campaign_id', '=', analysis.campaign_id.id)])
+ analysis.total_cost = analysis.activity.variable_cost + ((analysis.campaign_id.fixed_cost or 1.00) / wi_count)
+
+ @api.model_cr
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, 'campaign_analysis')
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW campaign_analysis AS (
+ SELECT
+ min(wi.id) AS id,
+ min(wi.res_id) AS res_id,
+ to_char(wi.date::date, 'YYYY') AS year,
+ to_char(wi.date::date, 'MM') AS month,
+ to_char(wi.date::date, 'YYYY-MM-DD') AS day,
+ wi.date::date AS date,
+ s.campaign_id AS campaign_id,
+ wi.activity_id AS activity_id,
+ wi.segment_id AS segment_id,
+ wi.partner_id AS partner_id ,
+ wi.state AS state,
+ sum(act.revenue) AS revenue,
+ count(*) AS count
+ FROM
marketing_campaign_workitem wi
- left join res_partner p on (p.id=wi.partner_id)
- left join marketing_campaign_segment s on (s.id=wi.segment_id)
- left join marketing_campaign_activity act on (act.id= wi.activity_id)
- group by
+ LEFT JOIN res_partner p ON (p.id=wi.partner_id)
+ LEFT JOIN marketing_campaign_segment s ON (s.id=wi.segment_id)
+ LEFT JOIN marketing_campaign_activity act ON (act.id= wi.activity_id)
+ GROUP BY
s.campaign_id,wi.activity_id,wi.segment_id,wi.partner_id,wi.state,
wi.date::date
)
diff --git a/addons/marketing_campaign/report/campaign_analysis_view.xml b/addons/marketing_campaign/report/campaign_analysis_view.xml
index cd241efe07f..d6daa647a50 100644
--- a/addons/marketing_campaign/report/campaign_analysis_view.xml
+++ b/addons/marketing_campaign/report/campaign_analysis_view.xml
@@ -1,7 +1,5 @@
-
-
-
-
+
+
campaign.analysis.pivot
campaign.analysis
@@ -83,6 +81,4 @@
-
-
-
+
diff --git a/addons/marketing_campaign/security/marketing_campaign_security.xml b/addons/marketing_campaign/security/marketing_campaign_security.xml
index ab0b3e303f9..7178ba0905f 100644
--- a/addons/marketing_campaign/security/marketing_campaign_security.xml
+++ b/addons/marketing_campaign/security/marketing_campaign_security.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
User
@@ -16,6 +15,4 @@
-
-
-
+
diff --git a/addons/marketing_campaign/test/marketing_campaign.yml b/addons/marketing_campaign/test/marketing_campaign.yml
deleted file mode 100644
index 3d540b4ccb7..00000000000
--- a/addons/marketing_campaign/test/marketing_campaign.yml
+++ /dev/null
@@ -1,145 +0,0 @@
--
- In order to test process of compaign, I start compaign.
--
- !workflow {model: marketing.campaign, action: state_running_set, ref: marketing_campaign_openerppartnerchannel}
--
- I check the campaign on Running mode after started.
--
- !assert {model: marketing.campaign, id: marketing_campaign_openerppartnerchannel}:
- - state == 'running'
--
- I start this segment after assinged campaign.
--
- !workflow {model: marketing.campaign.segment, action: state_running_set, ref: marketing_campaign_segment0}
--
- I check the segment on Running mode after started.
--
- !assert {model: marketing.campaign.segment, id: marketing_campaign_segment0}:
- - state == 'running'
--
- I synchronized segment manually to see all step of activity and process covered on this campaign.
--
- !python {model: marketing.campaign.segment}: |
- segment_id = self.browse(cr ,uid ,ref("marketing_campaign_segment0") ,context)
- assert segment_id.date_next_sync, 'Next Synchronization date is not calculated.'
- self.synchroniz(cr, uid, [ref("marketing_campaign_segment0")])
--
- I cancel Marketing Workitems.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel'))])
- self.button_cancel(cr, uid, ids)
- record = self.browse(cr, uid, ids[0])
- assert record.state == 'cancelled' or record.state == 'done' , 'Marketing Workitem shoud be in cancel state.'
--
- I set Marketing Workitems in draft state.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel'))])
- self.button_draft(cr, uid, ids)
- record = self.browse(cr, uid, ids[0])
- assert record.state == 'todo' or record.state == 'done' , 'Marketing Workitem shoud be in draft state.'
--
- I process follow-up of first activity.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_0'))])
- assert ids, 'Follow-up item is not created for first activity.'
- work_item_id = self.browse(cr ,uid ,ids[0] ,context)
- assert work_item_id.res_name, 'Resource Name is not defined.'
- self.process(cr, uid, ids)
- record = self.browse(cr, uid, ids)[0]
- assert record.state == "done", "Follow-up item should be closed after process."
--
- I check follow-up detail of second activity after process of first activity.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_1'))])
- assert ids, 'Follow-up item is not created for second activity.'
--
- Now I increase credit limit of customer
--
- !python {model: res.partner}: |
- self.write(cr, uid, [ref("base.res_partner_2")], {'credit_limit':41000}, context=context)
--
- I process follow-up of second activity after set draft.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_1'))])
- self.button_draft(cr, uid, ids, context=context)
- self.process(cr, uid, ids, context=context)
- record = self.browse(cr, uid, ids[0], context=context)
- assert record.state == "done", "Follow-up item should be closed after process."
--
- I check follow-up detail of third activity after process of second activity.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_2'))])
- assert ids, 'Follow-up item is not created for third activity.'
--
- Now I increase credit limit of customer
--
- !python {model: res.partner}: |
- self.write(cr, uid, [ref("base.res_partner_2")], {'credit_limit':151000}, context=context)
--
- I process follow-up of third activity after set draft.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_2'))])
- self.button_draft(cr, uid, ids, context=context)
- self.process(cr, uid, ids, context=context)
- record = self.browse(cr, uid, ids[0], context=context)
- assert record.state == "done", "Follow-up item should be closed after process."
--
- I print workitem report.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_2'))])
- self.preview(cr, uid, ids)
--
- I cancel segmentation because of some activity.
--
- !workflow {model: marketing.campaign.segment, action: state_cancel_set, ref: marketing_campaign_segment0}
--
- I check the segmentation is canceled.
--
- !assert {model: marketing.campaign.segment, id: marketing_campaign_segment0}:
- - state == 'cancelled'
--
- I reopen the segmentation.
--
- !workflow {model: marketing.campaign.segment, action: state_draft_set, ref: marketing_campaign_segment0}
--
- !workflow {model: marketing.campaign.segment, action: state_running_set, ref: marketing_campaign_segment0}
--
- I check the segment on Running mode after started.
--
- !assert {model: marketing.campaign.segment, id: marketing_campaign_segment0}:
- - state == 'running'
--
- I close segmentation After completion of all activity.
--
- !workflow {model: marketing.campaign.segment, action: state_done_set, ref: marketing_campaign_segment0}
--
- I check the segmentation is done.
--
- !assert {model: marketing.campaign.segment, id: marketing_campaign_segment0}:
- - state == 'done'
--
- I close this campaing.
--
- !workflow {model: marketing.campaign.segment, action: state_done_set, ref: marketing_campaign_openerppartnerchannel}
--
- I check the campaing is done.
--
- !assert {model: marketing.campaign.segment, id: marketing_campaign_openerppartnerchannel}:
- - state == 'done'
-
diff --git a/addons/marketing_campaign/tests/__init__.py b/addons/marketing_campaign/tests/__init__.py
new file mode 100644
index 00000000000..0f036ac5661
--- /dev/null
+++ b/addons/marketing_campaign/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import test_marketing_campaign_flow
\ No newline at end of file
diff --git a/addons/marketing_campaign/tests/test_marketing_campaign_flow.py b/addons/marketing_campaign/tests/test_marketing_campaign_flow.py
new file mode 100644
index 00000000000..e84e1ef6288
--- /dev/null
+++ b/addons/marketing_campaign/tests/test_marketing_campaign_flow.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import common
+
+
+class TestMarketingCampaignFlow(common.TransactionCase):
+
+ def test_00_marketing_campaign_flow(self):
+
+ Workitem = self.env['marketing.campaign.workitem']
+
+ # In order to test process of compaign, I start compaign.
+ partner_channel = self.env.ref('marketing_campaign.marketing_campaign_openerppartnerchannel')
+ partner_channel.signal_workflow('state_running_set')
+
+ # I check the campaign on Running mode after started.
+ self.assertEqual(partner_channel.state, 'running', 'The campaign should be on Running mode after having started.')
+
+ # I start this segment after assinged campaign.
+ segment0 = self.env.ref('marketing_campaign.marketing_campaign_segment0')
+ segment0.signal_workflow('state_running_set')
+
+ # I check the segment on Running mode after started.
+ self.assertEqual(segment0.state, 'running', 'The segment should be on Running mode after having started.')
+
+ # I synchronized segment manually to see all step of activity and process covered on this campaign.
+ self.assertTrue(segment0.date_next_sync, 'Next Synchronization date is not calculated.')
+ segment0.process_segment()
+
+ # I cancel Marketing Workitems.
+ workitems = Workitem.search([
+ ('segment_id', '=', segment0.id),
+ ('campaign_id', '=', partner_channel.id)
+ ])
+ workitems.button_cancel()
+ self.assertTrue(workitems[0].state in ('cancelled', 'done'), 'Marketing Workitem shoud be in cancel state.')
+
+ # I set Marketing Workitems in draft state.
+ workitems.button_draft()
+ self.assertTrue(workitems[0].state in ('todo', 'done'), 'Marketing Workitem shoud be in draft state.')
+
+ # I process follow-up of first activity.
+ activity0_id = self.ref('marketing_campaign.marketing_campaign_activity_0')
+ workitems = Workitem.search([
+ ('segment_id', '=', segment0.id),
+ ('campaign_id', '=', partner_channel.id),
+ ('activity_id', '=', activity0_id)
+ ])
+ self.assertTrue(workitems, 'Follow-up item is not created for first activity.')
+ self.assertTrue(workitems[0].res_name, 'Resource Name is not defined.')
+ workitems.process()
+ self.assertEqual(workitems[0].state, 'done', "Follow-up item should be closed after process.")
+
+ # I check follow-up detail of second activity after process of first activity.
+ activity1_id = self.ref('marketing_campaign.marketing_campaign_activity_1')
+ workitems = Workitem.search([
+ ('segment_id', '=', segment0.id),
+ ('campaign_id', '=', partner_channel.id),
+ ('activity_id', '=', activity1_id)
+ ])
+ self.assertTrue(workitems, 'Follow-up item is not created for second activity.')
+
+ # Now I increase credit limit of customer
+ self.env.ref("base.res_partner_2").write({'credit_limit': 41000})
+
+ # I process follow-up of second activity after set draft.
+ workitems.button_draft()
+ workitems.process()
+ self.assertEqual(workitems[0].state, 'done', "Follow-up item should be closed after process.")
+
+ # I check follow-up detail of third activity after process of second activity.
+ activity2_id = self.ref('marketing_campaign.marketing_campaign_activity_2')
+ workitems = Workitem.search([
+ ('segment_id', '=', segment0.id),
+ ('campaign_id', '=', partner_channel.id),
+ ('activity_id', '=', activity2_id)
+ ])
+ self.assertTrue(workitems, 'Follow-up item is not created for third activity.')
+
+ # Now I increase credit limit of customer
+ self.env.ref("base.res_partner_2").write({'credit_limit': 151000})
+
+ # I process follow-up of third activity after set draft.
+ workitems.button_draft()
+ workitems.process()
+ self.assertEqual(workitems[0].state, 'done', "Follow-up item should be closed after process.")
+
+ # I print workitem report
+ workitems.preview()
+
+ # I cancel segmentation because of some activity.
+ segment0.signal_workflow('state_cancel_set')
+
+ # I check the segmentation is canceled.
+ self.assertEqual(segment0.state, 'cancelled', 'Segment should be in cancelled state.')
+
+ # I reopen the segmentation.
+ segment0.signal_workflow('state_draft_set')
+ segment0.signal_workflow('state_running_set')
+
+ # I check the segment on Running mode after started.
+ self.assertEqual(segment0.state, 'running', 'Segment should be in running state.')
+
+ # I close segmentation After completion of all activity.
+ segment0.signal_workflow('state_done_set')
+
+ # I check the segmentation is done.
+ self.assertEqual(segment0.state, 'done', 'Segment should be in done state.')
+
+ # I close this campaing.
+ partner_channel.signal_workflow('state_done_set')
+
+ # I check the campaing is done.
+ self.assertEqual(partner_channel.state, 'done', 'Campaign should be in done state.')
diff --git a/addons/marketing_campaign/marketing_campaign_view.xml b/addons/marketing_campaign/views/marketing_campaign_views.xml
similarity index 97%
rename from addons/marketing_campaign/marketing_campaign_view.xml
rename to addons/marketing_campaign/views/marketing_campaign_views.xml
index 1eb6572e23a..9bd7dfc126f 100644
--- a/addons/marketing_campaign/marketing_campaign_view.xml
+++ b/addons/marketing_campaign/views/marketing_campaign_views.xml
@@ -1,7 +1,5 @@
-
-
-
-
+
+
@@ -113,7 +111,7 @@
-
+
@@ -203,7 +201,7 @@
-
+
@@ -297,11 +295,11 @@
-
-
+
-
-
+
+
@@ -349,7 +347,7 @@
-
+
@@ -453,5 +451,4 @@
-
-
+
diff --git a/addons/marketing_campaign_crm_demo/__init__.py b/addons/marketing_campaign_crm_demo/__init__.py
index 50a14119520..67dee8c60db 100644
--- a/addons/marketing_campaign_crm_demo/__init__.py
+++ b/addons/marketing_campaign_crm_demo/__init__.py
@@ -1 +1,2 @@
-# Debian packaging removes blank files, so this comment is added.
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
diff --git a/addons/marketing_campaign_crm_demo/__openerp__.py b/addons/marketing_campaign_crm_demo/__openerp__.py
index 089ff58ba0c..88ef2b3d648 100644
--- a/addons/marketing_campaign_crm_demo/__openerp__.py
+++ b/addons/marketing_campaign_crm_demo/__openerp__.py
@@ -1,13 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-
{
'name': 'Marketing Campaign - Demo',
'version': '1.0',
- 'depends': ['marketing_campaign',
- 'crm',
- ],
+ 'depends': ['marketing_campaign', 'crm'],
'category': 'Marketing',
'description': """
Demo data for the module marketing_campaign.
@@ -16,6 +12,6 @@ Demo data for the module marketing_campaign.
Creates demo data like leads, campaigns and segments for the module marketing_campaign.
""",
'website': 'https://www.odoo.com/page/lead-automation',
- 'demo': ['data/marketing_campaign_demo.xml'],
+ 'demo': ['data/marketing_campaign_crm_demo.xml'],
'installable': True,
}
diff --git a/addons/marketing_campaign_crm_demo/data/marketing_campaign_demo.xml b/addons/marketing_campaign_crm_demo/data/marketing_campaign_crm_demo.xml
similarity index 100%
rename from addons/marketing_campaign_crm_demo/data/marketing_campaign_demo.xml
rename to addons/marketing_campaign_crm_demo/data/marketing_campaign_crm_demo.xml
diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py
index c45790c7264..40f5c9a5674 100644
--- a/addons/web_diagram/controllers/main.py
+++ b/addons/web_diagram/controllers/main.py
@@ -54,7 +54,7 @@ class DiagramView(openerp.http.Controller):
'd_id': transitions[tr][1]
})
connector_tr = req.session.model(connector)
- connector_ids = connector_tr.search([('id', 'in', list_tr)], 0, 0, 0, req.session.context)
+ connector_ids = connector_tr.search([('id', 'in', list_tr)], context=req.session.context)
data_connectors =connector_tr.read(connector_ids, connector_fields, req.session.context)
@@ -73,10 +73,10 @@ class DiagramView(openerp.http.Controller):
t['options'][connector_fields_string[i]] = tr[fld]
fields = req.session.model('ir.model.fields')
- field_ids = fields.search([('model', '=', model), ('relation', '=', node)], 0, 0, 0, req.session.context)
+ field_ids = fields.search([('model', '=', model), ('relation', '=', node)], context=req.session.context)
field_data = fields.read(field_ids, ['relation_field'], req.session.context)
node_act = req.session.model(node)
- search_acts = node_act.search([(field_data[0]['relation_field'], '=', id)], 0, 0, 0, req.session.context)
+ search_acts = node_act.search([(field_data[0]['relation_field'], '=', id)], context=req.session.context)
data_acts = node_act.read(search_acts, invisible_node_fields + visible_node_fields, req.session.context)
for act in data_acts: