621 lines
28 KiB
Python
621 lines
28 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from dateutil.relativedelta import relativedelta
|
||
|
from traceback import format_exception
|
||
|
from sys import exc_info
|
||
|
|
||
|
import re
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.exceptions import UserError, ValidationError
|
||
|
from odoo.tools.safe_eval import safe_eval
|
||
|
|
||
|
import odoo.addons.decimal_precision as dp
|
||
|
|
||
|
_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=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_draft_set(self):
|
||
|
return self.write({'state': 'draft'})
|
||
|
|
||
|
@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, default=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.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:
|
||
|
self.ir_filter_id = False
|
||
|
return res
|
||
|
|
||
|
@api.multi
|
||
|
def state_draft_set(self):
|
||
|
return self.write({'state': 'draft'})
|
||
|
|
||
|
@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 += safe_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=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_wi_report(self, workitem):
|
||
|
self.ensure_one()
|
||
|
return self.report_id.render_report(workitem.res_id, self.report_id.report_name, None)
|
||
|
|
||
|
@api.multi
|
||
|
def _process_wi_action(self, workitem):
|
||
|
self.ensure_one()
|
||
|
return self.server_action_id.run()
|
||
|
|
||
|
@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._name))
|
||
|
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.exists():
|
||
|
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 safe_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
|