[MERGE] marketing_campaign, marketing_campaign_crm_demo: Migrate to new API
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Cron -->
|
||||
@@ -27,4 +27,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
<openerp>
|
||||
<data>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Email tempalte -->
|
||||
<record id="email_template_1" model="mail.template">
|
||||
<field name="name">welcome new partner</field>
|
||||
@@ -100,5 +100,4 @@
|
||||
<field name="campaign_id" ref="marketing_campaign_openerppartnerchannel"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<!-- Marketing Campaigns -->
|
||||
<record id="wkf_marketing_campaign" model="workflow">
|
||||
@@ -140,5 +139,4 @@
|
||||
<field name="signal">state_draft_set</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_report_campaign_analysis_pivot" model="ir.ui.view">
|
||||
<field name="name">campaign.analysis.pivot</field>
|
||||
<field name="model">campaign.analysis</field>
|
||||
@@ -83,6 +81,4 @@
|
||||
</record>
|
||||
|
||||
<menuitem name="Campaign" action="action_campaign_analysis_all" id="menu_action_campaign_analysis_all" parent="report_marketing_campaign" sequence="20"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="marketing_campaign.group_lead_automation_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="category_id" ref="base.module_category_lead_automation"/>
|
||||
@@ -16,6 +15,4 @@
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('marketing_campaign.group_lead_automation_manager'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import test_marketing_campaign_flow
|
||||
@@ -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.')
|
||||
+10
-13
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Marketing Campaign -->
|
||||
|
||||
<record id="act_marketing_campaing_segment_opened" model="ir.actions.act_window">
|
||||
@@ -113,7 +111,7 @@
|
||||
<diagram string="Campaign Editor">
|
||||
<node object="marketing.campaign.activity" shape="rectangle:type=='subcampaign'" bgcolor="gray:start==True">
|
||||
<field name="name"/>
|
||||
<field name="type"/>
|
||||
<field name="action_type"/>
|
||||
<field name="start" invisible="1"/>
|
||||
<field name="condition" widget="char"/>
|
||||
</node>
|
||||
@@ -203,7 +201,7 @@
|
||||
<label for="sync_last_date"/>
|
||||
<div>
|
||||
<field name="sync_last_date" class="oe_inline"/>
|
||||
<button string="Synchronize Manually" states="running" name="synchroniz" type="object"/>
|
||||
<button string="Synchronize Manually" states="running" name="process_segment" type="object"/>
|
||||
</div>
|
||||
<field name="date_next_sync"/>
|
||||
</group>
|
||||
@@ -297,11 +295,11 @@
|
||||
<field name="keep_if_condition_not_met"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="type"/>
|
||||
<field name="email_template_id" attrs="{'required':[('type','=','email')], 'invisible':[('type','!=','email')]}"
|
||||
<field name="action_type"/>
|
||||
<field name="email_template_id" attrs="{'required':[('action_type','=','email')], 'invisible':[('action_type','!=','email')]}"
|
||||
context="{'default_model_id':object_id}"/>
|
||||
<field name="server_action_id" attrs="{'required':[('type','=','action')],'invisible':[('type','!=','action')]}" domain="[('model_id','=',object_id)]"/>
|
||||
<field name="report_id" attrs="{'required':[('type','=','report')],'invisible':[('type','!=','report')]}" context="{'object_id':object_id}"/>
|
||||
<field name="server_action_id" attrs="{'required':[('action_type','=','action')],'invisible':[('action_type','!=','action')]}" domain="[('model_id','=',object_id)]"/>
|
||||
<field name="report_id" attrs="{'required':[('action_type','=','report')],'invisible':[('action_type','!=','report')]}" context="{'object_id':object_id}"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Previous Activities"/>
|
||||
@@ -349,7 +347,7 @@
|
||||
<tree string="Activities">
|
||||
<field name="name"/>
|
||||
<field name="condition"/>
|
||||
<field name="type"/>
|
||||
<field name="action_type"/>
|
||||
<field name="start"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -453,5 +451,4 @@
|
||||
|
||||
<menuitem id="menu_action_marketing_campaign_workitem" parent="report_marketing_campaign" action="action_marketing_campaign_workitem" sequence="3"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user