diff --git a/addons/marketing_campaign/__openerp__.py b/addons/marketing_campaign/__openerp__.py
index c60db8c24e2..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).
@@ -44,7 +40,4 @@ input segments, workflow.
'report/campaign_analysis_view.xml',
],
'demo': ['data/marketing_campaign_demo.xml'],
- 'test': ['test/marketing_campaign.yml'],
- 'installable': True,
- 'auto_install': False,
}
diff --git a/addons/marketing_campaign/data/marketing_campaign_data.xml b/addons/marketing_campaign/data/marketing_campaign_data.xml
index 4f337b1d60e..986b58508c9 100644
--- a/addons/marketing_campaign/data/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/data/marketing_campaign_demo.xml b/addons/marketing_campaign/data/marketing_campaign_demo.xml
index 06fb4545360..ad808c35734 100644
--- a/addons/marketing_campaign/data/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/data/marketing_campaign_workflow.xml b/addons/marketing_campaign/data/marketing_campaign_workflow.xml
index ad3fb0ddcca..8f32522423b 100644
--- a/addons/marketing_campaign/data/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/models/ir_actions.py b/addons/marketing_campaign/models/ir_actions.py
index 5883400f7d0..cb9a55db6cc 100644
--- a/addons/marketing_campaign/models/ir_actions.py
+++ b/addons/marketing_campaign/models/ir_actions.py
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv
+from odoo import api, models
-class report_xml(osv.osv):
+
+class IrActionsReportXml(models.Model):
_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
+
+ @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(report_xml, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
+ 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
index 6a8d1a47d2c..5b6d6be7777 100644
--- a/addons/marketing_campaign/models/mail_template.py
+++ b/addons/marketing_campaign/models/mail_template.py
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv
+from odoo import fields, models
-class mail_template(osv.osv):
+
+class MailTemplate(models.Model):
_inherit = "mail.template"
- _defaults = {
- 'model_id': lambda obj, cr, uid, context: context.get('object_id',False),
- }
+
+ 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
index e4eb585d3e2..597f5eabc89 100644
--- a/addons/marketing_campaign/models/marketing_campaign.py
+++ b/addons/marketing_campaign/models/marketing_campaign.py
@@ -2,20 +2,17 @@
# 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
+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),
@@ -24,77 +21,71 @@ _intervalTypes = {
'years': lambda interval: relativedelta(years=interval),
}
-DT_FMT = '%Y-%m-%d %H:%M:%S'
-
-
-class marketing_campaign(osv.osv):
+class MarketingCampaign(models.Model):
_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
+ 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')
- _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')
- }
+ @api.multi
+ def _compute_segments_count(self):
+ for campaign in self:
+ campaign.segments_count = len(campaign.segment_ids)
- _defaults = {
- 'state': lambda *a: 'draft',
- 'mode': lambda *a: 'test',
- }
-
- def state_running_set(self, cr, uid, ids, *args):
+ @api.multi
+ def state_running_set(self):
# TODO check that all subcampaigns are running
- campaign = self.browse(cr, uid, ids[0])
+ self.ensure_one()
- if not campaign.activity_ids:
+ 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 campaign.activity_ids:
+ for activity in self.activity_ids:
if activity.start:
has_start = True
if activity.signal and len(activity.from_ids) == 0:
@@ -103,186 +94,166 @@ Normal - the campaign runs normally and automatically sends all emails and repor
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'})
+ return self.write({'state': 'running'})
- def state_done_set(self, cr, uid, ids, *args):
+ @api.multi
+ def state_done_set(self):
# 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 :
+ 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."))
- self.write(cr, uid, ids, {'state': 'done'})
- return True
+ return self.write({'state': 'done'})
- def state_cancel_set(self, cr, uid, ids, *args):
+ @api.multi
+ def state_cancel_set(self):
# TODO check that this campaign is not a subcampaign in running mode.
- self.write(cr, uid, ids, {'state': 'cancelled'})
- return True
+ return self.write({'state': 'cancelled'})
- def _get_partner_for(self, campaign, record):
- partner_field = campaign.partner_field_id.name
+ def _get_partner_for(self, record):
+ partner_field = self.partner_field_id.name
if partner_field:
return record[partner_field]
- elif campaign.object_id.model == 'res.partner':
+ 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, cr, uid, record, campaign_rec, context=None):
+ 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
"""
- 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
+ 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.pool[campaign_rec.object_id.model].search(cr, uid,
- [(unique_field.name, '=', unique_value)], context=context)
+ 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','=', campaign_rec.id)]
- return Workitems.search(cr, uid, duplicate_workitem_domain, context=context)
+ duplicate_workitem_domain = [
+ ('res_id', 'in', similar_res_ids),
+ ('campaign_id', '=', self.id)
+ ]
+ return self.env['marketing.campaign.workitem'].search(duplicate_workitem_domain)
-
-class marketing_campaign_segment(osv.osv):
+class MarketingCampaignSegment(models.Model):
_name = "marketing.campaign.segment"
_description = "Campaign Segment"
_order = "name"
- def _get_next_sync(self, cr, uid, ids, fn, args, context=None):
+ 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.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)
+ 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
- _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"),
- }
+ @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'))
- _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)]}
+ @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['value'] = {'ir_filter_id': False}
+ res.ir_filter_id = False
return res
- def state_running_set(self, cr, uid, ids, *args):
- segment = self.browse(cr, uid, ids[0])
+ @api.multi
+ def state_running_set(self):
+ self.ensure_one()
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
+ if not self.date_run:
+ vals['date_run'] = fields.Datetime.now()
+ return self.write(vals)
- 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
+ @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()})
- 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
+ @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()})
- def synchroniz(self, cr, uid, ids, *args):
- self.process_segment(cr, uid, ids)
- return True
+ @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')])
- @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):
+ action_date = fields.Datetime.now()
+ campaigns = self.env['marketing.campaign']
+ for segment in self:
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)
+ campaigns |= segment.campaign_id
+ activity_ids = Activities.search([('start', '=', True), ('campaign_id', '=', segment.campaign_id.id)]).ids
- 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):
+ 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 Campaigns._find_duplicate_workitems(cr, uid, record, segment.campaign_id, context=context):
+ if segment.sync_mode in ('write_date', 'all'):
+ if segment.campaign_id._find_duplicate_workitems(record):
continue
wi_vals = {
@@ -292,106 +263,86 @@ class marketing_campaign_segment(osv.osv):
'res_id': record.id
}
- partner = self.pool.get('marketing.campaign')._get_partner_for(segment.campaign_id, record)
+ partner = segment.campaign_id._get_partner_for(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)
+ for activity_id in activity_ids:
+ wi_vals['activity_id'] = activity_id
+ Workitems.create(wi_vals)
- self.write(cr, uid, segment.id, {'sync_last_date':action_date}, context=context)
- Workitems.process_all(cr, uid, list(campaigns), context=context)
+ segment.write({'sync_last_date': action_date})
+ Workitems.process_all(campaigns.ids)
return True
-class marketing_campaign_activity(osv.osv):
+class MarketingCampaignActivity(models.Model):
_name = "marketing.campaign.activity"
_order = "name"
_description = "Campaign Activity"
- _action_types = [
+ 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'),
- # TODO implement the subcampaigns.
- # TODO implement the subcampaign out. disallow out transitions from
- # subcampaign activities ?
- #('subcampaign', 'Sub-Campaign'),
- ]
+ ], '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.")
- _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.")
- }
+ @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)
- _defaults = {
- 'type': lambda *a: 'email',
- 'condition': lambda *a: 'True',
- }
+ @api.multi
+ def _process_wi_email(self, workitem):
+ self.ensure_one()
+ return self.email_template_id.send_mail(workitem.res_id)
- 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)
-
- 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,)
+ @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))
-
- 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)
+ raise NotImplementedError('Method %r is not implemented on %r object.' % (method, self._model))
+ return action(workitem)
-class marketing_campaign_transition(osv.osv):
+class MarketingCampaignTransition(models.Model):
_name = "marketing.campaign.transition"
_description = "Campaign Transition"
@@ -402,7 +353,23 @@ class marketing_campaign_transition(osv.osv):
('years', 'Year(s)'),
]
- def _get_name(self, cr, uid, ids, fn, args, context=None):
+ 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'),
@@ -410,219 +377,154 @@ class marketing_campaign_transition(osv.osv):
'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'])
+ model_fields = self.fields_get(['interval_type'])
+ interval_type_selection = dict(model_fields['interval_type']['selection'])
- result = dict.fromkeys(ids, False)
- for trans in self.browse(cr, uid, ids, context=context):
+ for transition in self:
values = {
- 'interval_nbr': trans.interval_nbr,
- 'interval_type': interval_type_selection.get(trans.interval_type, ''),
+ 'interval_nbr': transition.interval_nbr,
+ 'interval_type': interval_type_selection.get(transition.interval_type, ''),
}
- result[trans.id] = formatters[trans.trigger] % values
- return result
+ 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, cr, uid, ids, context=None):
- assert len(ids) == 1
- transition = self.browse(cr, uid, ids[0], context=context)
- if transition.trigger != 'time':
+ def _delta(self):
+ self.ensure_one()
+ if self.trigger != 'time':
raise ValueError('Delta is only relevant for timed transition.')
- return relativedelta(**{str(transition.interval_type): transition.interval_nbr})
+ return relativedelta(**{str(self.interval_type): self.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):
+class MarketingCampaignWorkitem(models.Model):
_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
+ 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)
- proxy = self.pool[wi.object_id.model]
- if not proxy.exists(cr, uid, [wi.res_id]):
+ 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
- ng = proxy.name_get(cr, uid, [wi.res_id], context=context)
- if ng:
- res[wi.id] = ng[0][1]
- return res
+ workitem.res_name = record.name_get()[0][1]
- 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):
+ 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
- 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
+ condition_name = [None, operator, operand]
- 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()
+ 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)
+ 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
+ Model = self.env[model]
+ condition_name[0] = Model._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])
+ for record in Model.search(condition):
+ matching_workitems.extend(id_map[record.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.multi
+ def button_draft(self):
+ return self.filtered(lambda workitem: workitem.state in ('exception', 'cancelled')).write({'state': 'todo'})
- @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.multi
+ def button_cancel(self):
+ return self.filtered(lambda workitem: workitem.state in ('todo', 'exception')).write({'state': 'cancelled'})
- @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':
+ @api.multi
+ def _process_one(self):
+ self.ensure_one()
+ if self.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)
+ activity = self.activity_id
+ resource = self.env[self.object_id.model].browse(self.res_id)
eval_context = {
'activity': activity,
- 'workitem': workitem,
- 'object': object_id,
- 'resource': object_id,
+ 'workitem': self,
+ 'object': resource,
+ 'resource': resource,
'transitions': activity.to_ids,
're': re,
}
try:
condition = activity.condition
- campaign_mode = workitem.campaign_id.mode
+ campaign_mode = self.campaign_id.mode
if condition:
if not eval(condition, eval_context):
if activity.keep_if_condition_not_met:
- workitem.write({'state': 'cancelled'})
+ self.write({'state': 'cancelled'})
else:
- workitem.unlink()
+ self.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)
+ result = activity.process(self)
- values = dict(state='done')
- if not workitem.date:
- values['date'] = datetime.now().strftime(DT_FMT)
- workitem.write(values)
+ values = {'state': 'done'}
+ if not self.date:
+ values['date'] = fields.Datetime.now()
+ self.write(values)
if result:
# process _chain
- workitem.refresh() # reload
- date = datetime.strptime(workitem.date, DT_FMT)
+ 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 = date
+ launch_date = execution_date
elif transition.trigger == 'time':
- launch_date = date + transition._delta()
+ launch_date = execution_date + transition._delta()
if launch_date:
- launch_date = launch_date.strftime(DT_FMT)
+ launch_date = fields.Datetime.to_string(launch_date)
values = {
'date': launch_date,
- 'segment_id': workitem.segment_id.id,
+ 'segment_id': self.segment_id.id,
'activity_id': transition.activity_to_id.id,
- 'partner_id': workitem.partner_id.id,
- 'res_id': workitem.res_id,
+ 'partner_id': self.partner_id.id,
+ 'res_id': self.res_id,
'state': 'todo',
}
- wi_id = self.create(cr, uid, values, context=context)
+ workitem = self.create(values)
# Now, depending on the trigger and the campaign mode
# we know whether we must run the newly created workitem.
@@ -635,75 +537,66 @@ class marketing_campaign_workitem(osv.osv):
# auto Y Y N Y
#
- run = (transition.trigger == 'auto' \
- and campaign_mode != 'manual') \
- or (transition.trigger == 'time' \
- and campaign_mode == 'test')
+ 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)
+ workitem._process_one()
except Exception:
tb = "".join(format_exception(*exc_info()))
- workitem.write({'state': 'exception', 'error_msg': tb})
+ self.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)
+ @api.multi
+ def process(self):
+ for workitem in self:
+ workitem._process_one()
return True
- def process_all(self, cr, uid, camp_ids=None, context=None):
- camp_obj = self.pool.get('marketing.campaign')
+ @api.model
+ def process_all(self, camp_ids=None):
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
+ 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', '=', 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:
+ 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
-
- self.process(cr, uid, workitem_ids, context=context)
+ workitems.process()
return True
- def preview(self, cr, uid, ids, context=None):
+ @api.multi
+ def preview(self):
+ self.ensure_one()
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')
+ 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': context,
- 'views': [(view_id and view_id[1] or 0, 'form')],
+ '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}"%
- (wi_obj.activity_id.email_template_id.id,
- wi_obj.res_id)
+ 'context': "{'template_id': %d,'default_res_id': %d}" % (self.activity_id.email_template_id.id, self.res_id)
}
- elif wi_obj.activity_id.type == 'report':
+ elif self.activity_id.action_type == 'report':
datas = {
- 'ids': [wi_obj.res_id],
- 'model': wi_obj.object_id.model
+ 'ids': [self.res_id],
+ 'model': self.object_id.model
}
res = {
- 'type' : 'ir.actions.report.xml',
- 'report_name': wi_obj.activity_id.report_id.report_name,
- 'datas' : datas,
+ '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.pivotcampaign.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/views/marketing_campaign_views.xml b/addons/marketing_campaign/views/marketing_campaign_views.xml
index 1eb6572e23a..9bd7dfc126f 100644
--- a/addons/marketing_campaign/views/marketing_campaign_views.xml
+++ b/addons/marketing_campaign/views/marketing_campaign_views.xml
@@ -1,7 +1,5 @@
-
-
-
-
+
+
@@ -113,7 +111,7 @@
-
+
@@ -203,7 +201,7 @@