[IMP] marketing_campaign, email_template: first series of improvements during review...
authorOlivier Dony <odo@openerp.com>
Fri, 16 Jul 2010 09:05:13 +0000 (11:05 +0200)
committerOlivier Dony <odo@openerp.com>
Fri, 16 Jul 2010 09:05:13 +0000 (11:05 +0200)
bzr revid: odo@openerp.com-20100716090513-uvavs7wply3bpsfv

addons/email_template/email_template.py
addons/email_template/email_template_account.py
addons/email_template/email_template_account_view.xml
addons/marketing_campaign/marketing_campaign.py
addons/marketing_campaign/marketing_campaign_view.xml

index 07dce40..5aedea7 100644 (file)
@@ -133,11 +133,13 @@ class email_template(osv.osv):
                    string="Enforce From Account",
                    help="Emails will be sent only from this account(which are approved)."),
         'from_email' : fields.related('enforce_from_account', 'email_id',
-                                                type='char', string='From',),        
+                                                type='char', string='From',
+                                                help='From Email (select mail account)',
+                                                readonly=True),        
         'def_to':fields.char(
-                 'Recepient (To)',
+                 'Recipient (To)',
                  size=250,
-                 help="The default recepient of email." 
+                 help="The default recipient of email." 
                  "Placeholders can be used here."),
         'def_cc':fields.char(
                  'Default CC',
@@ -172,7 +174,7 @@ class email_template(osv.osv):
         'use_sign':fields.boolean(
                   'Signature',
                   help="the signature from the User details" 
-                  "will be appened to the mail"),
+                  " will be appended to the mail"),
         'file_name':fields.char(
                 'File Name Pattern',
                 size=200,
index e5bbe0f..8c71ce0 100644 (file)
@@ -59,21 +59,23 @@ class email_template_account(osv.osv):
         'user':fields.many2one('res.users',
                         'Related User', required=True,
                         readonly=True, states={'draft':[('readonly', False)]}),
-        'email_id': fields.char('Email ID',
+        'email_id': fields.char('From Email',
                         size=120, required=True,
                         readonly=True, states={'draft':[('readonly', False)]} ,
-                        help=" eg:yourname@yourdomain.com "),
+                        help="eg: yourname@yourdomain.com "),
         'smtpserver': fields.char('Server',
                         size=120, required=True,
                         readonly=True, states={'draft':[('readonly', False)]},
-                        help="Enter name of outgoing server,eg:smtp.gmail.com "),
+                        help="Enter name of outgoing server, eg:smtp.gmail.com "),
         'smtpport': fields.integer('SMTP Port ',
                         size=64, required=True,
                         readonly=True, states={'draft':[('readonly', False)]},
                         help="Enter port number,eg:SMTP-587 "),
         'smtpuname': fields.char('User Name',
                         size=120, required=False,
-                        readonly=True, states={'draft':[('readonly', False)]}),
+                        readonly=True, states={'draft':[('readonly', False)]},
+                        help="Specify the username if your SMTP server requires authentication, "
+                        "otherwise leave it empty."),
         'smtppass': fields.char('Password',
                         size=120, invisible=True,
                         required=False, readonly=True,
@@ -91,11 +93,11 @@ class email_template_account(osv.osv):
         'company':fields.selection([
                         ('yes', 'Yes'),
                         ('no', 'No')
-                        ], 'Company Mail A/c',
+                        ], 'Corporate',
                         readonly=True,
-                        help="Select if this mail account does not belong" \
-                        "to specific user but the organisation as a whole." \
-                        "eg:info@somedomain.com",
+                        help="Select if this mail account does not belong " \
+                        "to specific user but to the organization as a whole. " \
+                        "eg: info@companydomain.com",
                         required=True, states={
                                            'draft':[('readonly', False)]
                                            }),
@@ -316,8 +318,6 @@ class email_template_account(osv.osv):
                         msg['To'] = u','.join(addresses_l['To'])
                     if addresses_l['CC']:
                         msg['CC'] = u','.join(addresses_l['CC'])
-#                    if addresses_l['BCC']:
-#                        msg['BCC'] = u','.join(addresses_l['BCC'])
                     if body.get('text', False):
                         temp_body_text = body.get('text', '')
                         l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
index 4a35134..d86b418 100644 (file)
@@ -24,7 +24,7 @@
                                 <field name="smtpssl" select="2" colspan="2" />
                                 <field name="smtptls" select="2" colspan="2" />
                             </group>
-                            <button name="check_outgoing_connection" type="object" string="Check Outgoing Connection" />
+                            <button name="check_outgoing_connection" type="object" string="Test Outgoing Connection" />
                             <separator string="User Information" colspan="4" />
                             <group col="2" colspan="2">
                                 <field name="email_id" select="1" on_change="on_change_emailid(name,email_id)" colspan="2" />
index 83e5ed4..b6d2bc7 100644 (file)
@@ -26,6 +26,7 @@ from dateutil.relativedelta import relativedelta
 from operator import itemgetter
 from traceback import format_exception
 from sys import exc_info
+from tools.safe_eval import safe_eval as eval
 
 from osv import fields, osv
 import netsvc
@@ -82,6 +83,13 @@ class marketing_campaign(osv.osv):
     _name = "marketing.campaign"
     _description = "Marketing Campaign"
 
+
+    def _check_has_start(self, cr, uid, ids, context=None):
+        for campaign in self.browse(cr, uid, ids, context=context):
+            if not any(a.start for a in campaign.activity_ids):
+                return False
+        return True
+
     _columns = {
         'name': fields.char('Name', size=64, required=True),
         'object_id': fields.many2one('ir.model', 'Model', required=True,
@@ -90,15 +98,15 @@ 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 left this field empty."),
-        'mode':fields.selection([('test', 'Test Directly'),
+        '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 do not send emails or produce reports.
-Test in Realtime - It creates and process all the activities directly but do not send emails or produce reports.
+"""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"""),
+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', 'Draft'),
                                    ('running', 'Running'),
                                    ('done', 'Done'),
@@ -106,32 +114,34 @@ Normal - the campaign runs normally and automatically sends all emails and repor
                                    'State',),
         'activity_ids': fields.one2many('marketing.campaign.activity',
                                        'campaign_id', 'Activities'),
-        'fixed_cost': fields.float('Fixed Cost', help="The fixed cost is cost\
-you required for the campaign"),
+        'fixed_cost': fields.float('Fixed Cost', help="Fixed cost for the campaign (used for campaign analysis), see also variable cost on activities"),
     }
+    
     _defaults = {
         'state': lambda *a: 'draft',
         'mode': lambda *a: 'test',
     }
+           
+    _constraints = [(_check_has_start, 'Please mark at least one activity as a start activity', ['Activities'])]
 
     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 osv.except_osv("Error", "There is no activitity in the campaign")
-        actvity_ids = [ act_id.id for act_id in campaign.activity_ids]
+        activity_ids = [ act_id.id for act_id in campaign.activity_ids]
+        
+        if not activity_ids:
+            raise osv.except_osv(_("Error"), _("The campaign cannot be started : there are no activities in it"))
+        
         act_obj = self.pool.get('marketing.campaign.activity')
-        act_ids  = act_obj.search(cr, uid, [('id', 'in', actvity_ids),
-                                                    ('start', '=', True)])
-        if not act_ids :
-            raise osv.except_osv("Error", "There is no starting activitity in the campaign")
-        act_ids = act_obj.search(cr, uid, [('id', 'in', actvity_ids),
+        act_ids = act_obj.search(cr, uid, [('id', 'in', activity_ids),
                                             ('type', '=', 'email')])
         for activity in act_obj.browse(cr, uid, act_ids):
             if not activity.email_template_id.enforce_from_account :
-                raise osv.except_osv("Error", "Campaign cannot be start : Email Account is missing in email activity")
+                raise osv.except_osv(_("Error"), _("The campaign cannot be started: an email account is missing in the email activity '%s'")%activity.name)
             if activity.email_template_id.enforce_from_account.state != 'approved' :
-                raise osv.except_osv("Error", "Campaign cannot be start : Email Account is not approved for email activity")
+                raise osv.except_osv(_("Error"), _("The campaign cannot be started: the email account is not approved in the email activity '%s'")%activity.name)
         self.write(cr, uid, ids, {'state': 'running'})
         return True
 
@@ -141,7 +151,7 @@ you required for the campaign"),
                                             [('campaign_id', 'in', ids),
                                             ('state', '=', 'running')])
         if segment_ids :
-            raise osv.except_osv("Error", "Campaign cannot be marked as done before all segments are done")
+            raise osv.except_osv(_("Error"), _("The campaign cannot be marked as done before all segments are done"))
         self.write(cr, uid, ids, {'state': 'done'})
         return True
 
@@ -187,7 +197,7 @@ you required for the campaign"),
         partner_field = campaign.partner_field_id.name
         if partner_field:
             return getattr(record, partner_field)
-        elif campaign.model_id.model == 'res.partner':
+        elif campaign.object_id.model == 'res.partner':
             return record
         return None
 
@@ -206,9 +216,10 @@ class marketing_campaign_segment(osv.osv):
                                       string='Object'),
         'ir_filter_id': fields.many2one('ir.filters', 'Filter', help=""),
         'sync_last_date': fields.datetime('Latest Synchronization'),
-        'sync_mode': fields.selection([('create_date', 'Sync only on creation'),
-                                      ('write_date', 'Sync at each modification')],
-                                      'Synchronization Mode'),
+        'sync_mode': fields.selection([('create_date', 'If record created after last sync'),
+                                      ('write_date', 'If record modified after last sync (no duplicates)')],
+                                      'Workitem creation mode',
+                                      help="Determines when new workitems should be created for records matching a segment."),
         'state': fields.selection([('draft', 'Draft'),
                                    ('running', 'Running'),
                                    ('done', 'Done'),
@@ -269,6 +280,7 @@ class marketing_campaign_segment(osv.osv):
                 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 o_ids in model_obj.browse(cr, uid, object_ids, context=context):
                 # avoid duplicated workitem for the same resource
                 if segment.sync_mode == 'write_date':
@@ -318,9 +330,9 @@ class marketing_campaign_activity(osv.osv):
         '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."),
+        'start': fields.boolean('Start', help= "This activity is launched when the campaign starts.", select=True),
         'condition': fields.char('Condition', size=256, required=True,
-                                 help="Python condition to know if the activity can be launched"),
+                                 help="Python condition to know if the activity can be executed, otherwise it will be deleted or cancelled."),
         'type': fields.selection(_action_types, 'Type', required=True,
                                   help="Describe type of action to be performed on the Activity.Eg : Send email,Send paper.."),
         'email_template_id': fields.many2one('email.template','Email Template'),
@@ -341,9 +353,9 @@ class marketing_campaign_activity(osv.osv):
         'variable_cost': fields.float('Variable Cost'),
         'revenue': fields.float('Revenue'),
         'signal': fields.char('Signal', size=128,
-                              help='An activity with a signal can be called programmatically. Attention, the workitem is always created when the signal is send'),
-        'keep_if_condition_not_met': fields.boolean('Keep if condition not met',
-                                                    help="By activating this option, the workitems that aren't processed because the condition is not met are marked as cancelled instead of being deleted.")
+                              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('Keep as cancelled when condition not met',
+                                                    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 = {
@@ -460,7 +472,7 @@ class marketing_campaign_transition(osv.osv):
                                      ('cosmetic', 'Cosmetic'),  # fake plastic transition
                                     ],
                                     'Trigger', required=True,
-                                    help="How is triggered the destination workitem"),
+                                    help="How is the destination workitem triggered"),
     }
 
     _defaults = {
@@ -516,7 +528,7 @@ class marketing_campaign_workitem(osv.osv):
 
     def button_draft(self, cr, uid, workitem_ids, context={}):
         for wi in self.browse(cr, uid, workitem_ids, context=context):
-            if wi.state=='exception':
+            if wi.state in ('exception', 'cancelled'):
                 self.write(cr, uid, [wi.id], {'state':'todo'}, context=context)
         return True
 
@@ -531,13 +543,13 @@ class marketing_campaign_workitem(osv.osv):
             return
 
         activity = workitem.activity_id
+        proxy = self.pool.get(workitem.object_id.model)
+        object_id = proxy.browse(cr, uid, workitem.res_id, context=context)
 
         eval_context = {
-            'pool': self.pool,
-            'cr': cr,
-            'uid': uid,
-            'wi': workitem,
-            'object': activity,
+            'activity': activity,
+            'workitem': workitem,
+            'object': object_id,
             'transition': activity.to_ids
         }
         try:
@@ -585,7 +597,7 @@ class marketing_campaign_workitem(osv.osv):
                     wi_id = self.create(cr, uid, values, context=context)
 
                     # Now, depending of the trigger and the campaign mode 
-                    # we now if must run the newly created workitem.
+                    # we know if must run the newly created workitem.
                     #
                     # rows = transition trigger \ colums = campaign mode
                     #
@@ -675,6 +687,9 @@ class email_template(osv.osv):
     _defaults = {
         'object_name': 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
+     
 email_template()
 
 class report_xml(osv.osv):
index 1d07fc1..289730a 100644 (file)
                     </group>
                     <field name="type" width='100'/>
                     <group colspan='2' col='1'>
-                        <group attrs="{'invisible':[('type','!=','email')]}" >
-                            <field name="email_template_id" attrs="{'required':[('type','=','email')]}" />
-                        </group>
+                        <field name="email_template_id" attrs="{'required':[('type','=','email')], 'invisible':[('type','!=','email')]}"
+                               context="{'default_object_name':object_id}" />
                         <group attrs="{'invisible':[('type','!=','paper')]}" >
                             <field name="report_id" attrs="{'required':[('type','=','paper')]}" context="{'object_id':object_id}"/>
                             <field name="report_directory_id" attrs="{'required':[('type','=','paper')]}" />
                         </group>
-                        <group attrs="{'invisible':[('type','!=','action')]}" >
-                            <field name="server_action_id" attrs="{'required':[('type','=','action')]}" domain="[('model_id','=',object_id)]" />
-                        </group>
+                        <field name="server_action_id" attrs="{'required':[('type','=','action')],'invisible':[('type','!=','action')]}" domain="[('model_id','=',object_id)]" />
                         <!--
-                        <group attrs="{'invisible':[('type','!=','subcampaign')]}" >
-                            <field name="subcampaign_id" attrs="{'required':[('type','=','subcampaign')]}" />
-                        </group>
+                         <field name="subcampaign_id" attrs="{'required':[('type','=','subcampaign')], 'invisible':[('type','!=','subcampaign')]}" />
                         -->
                     </group>
                 </group>
                 <separator string="Status" colspan="4"/>
                 <group colspan="4" col="11">
                     <field name="state" nolabel="1" readonly="True" select="1"/>
-                    <button string="Retry" states="exception" name="button_draft" type="object" icon="gtk-ok"/>
+                    <button string="Reset" states="exception,cancelled" name="button_draft" type="object" icon="gtk-ok"/>
                     <button string="Process" states="todo" name="process" type="object" icon="gtk-ok"/>
                     <button string="Cancel" states="todo,exception" name="button_cancel" type="object" icon="gtk-cancel"/>
                 </group>