[FIX]crm : fixed problems regardig email_from/email_to
[odoo/odoo.git] / addons / crm / crm.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 import re
24 import os
25 import base64
26 import tools
27 import mx.DateTime
28 import datetime
29
30 from datetime import datetime
31 from datetime import timedelta
32 from osv import fields
33 from osv import orm
34 from osv import osv
35 from osv.orm import except_orm
36 from tools.translate import _
37
38
39
40 MAX_LEVEL = 15
41 AVAILABLE_STATES = [
42     ('draft','Draft'),
43     ('open','Open'),
44     ('cancel', 'Cancelled'),
45     ('done', 'Closed'),
46     ('pending','Pending')
47 ]
48
49 AVAILABLE_PRIORITIES = [
50     ('5','Lowest'),
51     ('4','Low'),
52     ('3','Normal'),
53     ('2','High'),
54     ('1','Highest')
55 ]
56 class crm_case_section(osv.osv):
57     _name = "crm.case.section"
58     _description = "Sales Teams"
59     _order = "name"
60     _columns = {
61         'name': fields.char('Sales Team',size=64, required=True, translate=True),
62         'code': fields.char('Code',size=8),
63         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the sales team without removing it."),
64         'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
65         'user_id': fields.many2one('res.users', 'Responsible User'),
66         'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by Open ERP about cases in this sales team"),
67         'parent_id': fields.many2one('crm.case.section', 'Parent Section'),
68         'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Sections'),
69     }
70     _defaults = {
71         'active': lambda *a: 1,
72         'allow_unlink': lambda *a: 1,
73     }
74     _sql_constraints = [
75         ('code_uniq', 'unique (code)', 'The code of the section must be unique !')
76     ]
77     def _check_recursion(self, cr, uid, ids):
78         level = 100
79         while len(ids):
80             cr.execute('select distinct parent_id from crm_case_section where id =ANY(%s)',(ids,))
81             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
82             if not level:
83                 return False
84             level -= 1
85         return True
86     _constraints = [
87         (_check_recursion, 'Error ! You cannot create recursive sections.', ['parent_id'])
88     ]
89     def name_get(self, cr, uid, ids, context={}):
90         if not len(ids):
91             return []
92         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
93         res = []
94         for record in reads:
95             name = record['name']
96             if record['parent_id']:
97                 name = record['parent_id'][1]+' / '+name
98             res.append((record['id'], name))
99         return res
100 crm_case_section()
101
102 class crm_case_categ(osv.osv):
103     _name = "crm.case.categ"
104     _description = "Category of case"
105
106     _columns = {
107         'name': fields.char('Case Category Name', size=64, required=True, translate=True),        
108         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
109         'object_id': fields.many2one('ir.model','Object Name'),        
110     }
111     def _find_object_id(self, cr, uid, context=None):
112         object_id = context and context.get('object_id', False) or False
113         ids =self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
114         return ids and ids[0] 
115     _defaults = {        
116         'object_id' : _find_object_id
117     }
118 #               
119 crm_case_categ()
120
121 class crm_case_resource_type(osv.osv):
122     _name = "crm.case.resource.type"
123     _description = "Resource Type of case"
124     _rec_name = "name"
125     _columns = {
126         'name': fields.char('Case Resource Type', size=64, required=True, translate=True),
127         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
128         'object_id': fields.many2one('ir.model','Object Name'),        
129     }
130     def _find_object_id(self, cr, uid, context=None):
131         object_id = context and context.get('object_id', False) or False
132         ids =self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
133         return ids and ids[0] 
134     _defaults = {
135         'object_id' : _find_object_id
136     }    
137 crm_case_resource_type()
138
139
140 class crm_case_stage(osv.osv):
141     _name = "crm.case.stage"
142     _description = "Stage of case"
143     _rec_name = 'name'
144     _order = "sequence"
145     _columns = {
146         'name': fields.char('Stage Name', size=64, required=True, translate=True),
147         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
148         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of case stages."),
149         'object_id': fields.many2one('ir.model','Object Name'),
150         'probability': fields.float('Probability (%)', required=True),
151         'on_change': fields.boolean('Change Probability Automatically',help="Change Probability on next and previous stages."),
152         'requirements': fields.text('Requirements')
153     }
154     def _find_object_id(self, cr, uid, context=None):
155         object_id = context and context.get('object_id', False) or False
156         ids =self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
157         return ids and ids[0]     
158     _defaults = {
159         'sequence': lambda *args: 1,
160         'probability': lambda *args: 0.0,
161         'object_id' : _find_object_id
162     }
163     
164 crm_case_stage()
165
166 def _links_get(self, cr, uid, context={}):
167     obj = self.pool.get('res.request.link')
168     ids = obj.search(cr, uid, [])
169     res = obj.read(cr, uid, ids, ['object', 'name'], context)
170     return [(r['object'], r['name']) for r in res]
171
172
173 class crm_case(osv.osv):
174     _name = "crm.case"
175     _description = "Case"
176
177     def _email_last(self, cursor, user, ids, name, arg, context=None):
178         res = {}
179         for case in self.browse(cursor, user, ids):
180             if case.history_line:
181                 res[case.id] = case.history_line[0].description
182             else:
183                 res[case.id] = False
184         return res
185
186     def copy(self, cr, uid, id, default=None, context={}):
187         if not default: default = {}
188         default.update( {'state':'draft', 'id':False})
189         return super(crm_case, self).copy(cr, uid, id, default, context)
190
191     def _get_log_ids(self, cr, uid, ids, field_names, arg, context={}):
192         result = {}
193         history_obj = False
194         model_obj = self.pool.get('ir.model')
195         if 'history_line' in field_names:
196             history_obj = self.pool.get('crm.case.history')
197             name = 'history_line'
198         if 'log_ids' in field_names:
199             history_obj = self.pool.get('crm.case.log')
200             name = 'log_ids'
201         if not history_obj:
202             return result
203         for case in self.browse(cr, uid, ids, context):
204             model_ids = model_obj.search(cr, uid, [('model','=',case._name)])
205             history_ids = history_obj.search(cr, uid, [('model_id','=',model_ids[0]),('res_id','=',case.id)])             
206             if history_ids:
207                 result[case.id] = {name:history_ids}
208             else:
209                 result[case.id] = {name:[]}         
210         return result
211
212     _columns = {
213         'id': fields.integer('ID', readonly=True),
214         'name': fields.char('Description', size=1024, required=True),
215         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the case without removing it."),
216         'description': fields.text('Description'),
217         'section_id': fields.many2one('crm.case.section', 'Sales Team', select=True, help='Sales team to which Case belongs to. Define Responsible user and Email account for mail gateway.'),
218         'email_from': fields.char('Email', size=128, help="These people will receive email."),
219         'email_cc': fields.text('Watchers Emails', size=252 , help="These people will receive a copy of the future" \
220                                                                     " communication between partner and users by email"),
221         'probability': fields.float('Probability'),
222         'email_last': fields.function(_email_last, method=True,
223             string='Latest E-Mail', type='text'),
224         'partner_id': fields.many2one('res.partner', 'Partner'),
225         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
226         'create_date': fields.datetime('Creation Date' ,readonly=True),
227         'write_date': fields.datetime('Update Date' ,readonly=True),
228         'date_deadline': fields.date('Deadline'),
229         'user_id': fields.many2one('res.users', 'Responsible'),
230         'history_line': fields.function(_get_log_ids, method=True, type='one2many', multi="history_line", relation="crm.case.history", string="Communication"),
231         'log_ids': fields.function(_get_log_ids, method=True, type='one2many', multi="log_ids", relation="crm.case.log", string="Logs History"),
232         'stage_id': fields.many2one ('crm.case.stage', 'Stage', domain="[('section_id','=',section_id),('object_id.model', '=', 'crm.opportunity')]"),
233         'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True,
234                                   help='The state is set to \'Draft\', when a case is created.\
235                                   \nIf the case is in progress the state is set to \'Open\'.\
236                                   \nWhen the case is over, the state is set to \'Done\'.\
237                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
238         'company_id': fields.many2one('res.company','Company'),
239     }
240     def _get_default_partner_address(self, cr, uid, context):
241         if not context.get('portal',False):
242             return False
243         return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id
244     def _get_default_partner(self, cr, uid, context):
245         if not context.get('portal',False):
246             return False
247         user = self.pool.get('res.users').browse(cr, uid, uid, context)
248         if not user.address_id:
249             return False
250         return user.address_id.partner_id.id
251     def _get_default_email(self, cr, uid, context):
252         if not context.get('portal',False):
253             return False
254         user = self.pool.get('res.users').browse(cr, uid, uid, context)
255         if not user.address_id:
256             return False
257         return user.address_id.email
258     def _get_default_user(self, cr, uid, context):
259         if context.get('portal', False):
260             return False
261         return uid
262
263     def _get_section(self, cr, uid, context):
264        user = self.pool.get('res.users').browse(cr, uid, uid,context=context)
265        return user.context_section_id.id or False
266
267     _defaults = {
268         'active': lambda *a: 1,
269         'user_id': _get_default_user,
270         'partner_id': _get_default_partner,
271         'partner_address_id': _get_default_partner_address,
272         'email_from': _get_default_email,
273         'state': lambda *a: 'draft',
274         'date_deadline': lambda *a:(datetime.today() + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S'),
275         'section_id': _get_section,
276         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c),
277     }
278     _order = 'date_deadline desc, create_date desc,id desc'
279
280     def unlink(self, cr, uid, ids, context={}):
281         for case in self.browse(cr, uid, ids, context):
282             if (not case.section_id.allow_unlink) and (case.state <> 'draft'):
283                 raise osv.except_osv(_('Warning !'),
284                     _('You can not delete this case. You should better cancel it.'))
285         return super(crm_case, self).unlink(cr, uid, ids, context)
286
287     def stage_next(self, cr, uid, ids, context={}):
288         s = self.get_stage_dict(cr, uid, ids, context=context)
289         for case in self.browse(cr, uid, ids, context):
290             section = (case.section_id.id or False)
291             if section in s:
292                 st = case.stage_id.id  or False
293                 if st in s[section]:                    
294                     self.write(cr, uid, [case.id], {'stage_id': s[section][st]})
295         return True
296     
297     def get_stage_dict(self, cr, uid, ids, context={}):
298         sid = self.pool.get('crm.case.stage').search(cr, uid, [('object_id.model', '=', self._name)], context=context)
299         s = {}
300         previous = {}
301         for stage in self.pool.get('crm.case.stage').browse(cr, uid, sid, context=context):
302             section = stage.section_id.id or False
303             s.setdefault(section, {})
304             s[section][previous.get(section, False)] = stage.id
305             previous[section] = stage.id
306         return s
307     
308     def stage_previous(self, cr, uid, ids, context={}):
309         s = self.get_stage_dict(cr, uid, ids, context=context)
310         for case in self.browse(cr, uid, ids, context):
311             section = (case.section_id.id or False)
312             if section in s:
313                 st = case.stage_id.id  or False
314                 s[section] = dict([(v, k) for (k, v) in s[section].iteritems()])
315                 if st in s[section]:
316                     self.write(cr, uid, [case.id], {'stage_id': s[section][st]})
317         return True    
318     
319     def history(self, cr, uid, ids, keyword, history=False, email=False, details=None, email_from=False, context={}):
320         cases = self.browse(cr, uid, ids, context=context)
321         return self._history(cr, uid, cases, keyword=keyword,\
322                                history=history, email=email, details=details, email_from=email_from, \
323                                context=context)
324
325     def __history(self, cr, uid, cases, keyword, history=False, email=False, details=None, email_from=False, context={}):
326         model_obj = self.pool.get('ir.model')  
327         if email and type(email) == type([]):
328             email = ','.join(email) 
329         if email_from and type(email_from) == type([]):
330             email_from = ','.join(email_from) 
331        
332         for case in cases:
333             model_ids = model_obj.search(cr, uid, [('model','=',case._name)])            
334             data = {
335                 'name': keyword,                
336                 'user_id': uid,
337                 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
338                 'model_id' : model_ids and model_ids[0] or False,
339                 'res_id': case.id,
340                 'section_id': case.section_id.id
341             }
342             obj = self.pool.get('crm.case.log')
343             if history:
344                 obj = self.pool.get('crm.case.history')
345                 data['description'] = details or case.description
346                 data['email_to'] = email or \
347                         (case.user_id and case.user_id.address_id and \
348                             case.user_id.address_id.email) or False
349                 data['email_from'] = email_from or \
350                         (case.user_id and case.user_id.address_id and \
351                             case.user_id.address_id.email) or False
352             res = obj.create(cr, uid, data, context)            
353         return True
354     _history = __history
355
356     def create(self, cr, uid, *args, **argv):
357         res = super(crm_case, self).create(cr, uid, *args, **argv)
358         cases = self.browse(cr, uid, [res])
359         cases[0].state # to fill the browse record cache
360         self._action(cr,uid, cases, 'draft')
361         return res
362
363     def add_reply(self, cursor, user, ids, context=None):
364         for case in self.browse(cursor, user, ids, context=context):
365             if case.email_last:
366                 description = case.email_last
367                 self.write(cursor, user, case.id, {
368                     'description': '> ' + description.replace('\n','\n> '),
369                     }, context=context)
370         return True
371
372     def case_log(self, cr, uid, ids,context={}, email=False, *args):
373         cases = self.browse(cr, uid, ids)
374         self.__history(cr, uid, cases, _('Historize'), history=True, email=email)
375         return self.write(cr, uid, ids, {'description': False, 'som': False,
376             'canal_id': False})
377
378     def case_log_reply(self, cr, uid, ids, context={}, email=False, *args):
379         cases = self.browse(cr, uid, ids)
380         for case in cases:
381             if not case.email_from:
382                 raise osv.except_osv(_('Error!'),
383                         _('You must put a Partner eMail to use this action!'))
384             if not case.user_id:
385                 raise osv.except_osv(_('Error!'),
386                         _('You must define a responsible user for this case in order to use this action!'))
387             if not case.description:
388                 raise osv.except_osv(_('Error!'),
389                         _('Can not send mail with empty body,you should have description in the body'))
390         
391         for case in cases:
392             self.write(cr, uid, [case.id], {
393                 'description': False,                
394                 })
395             emails = [case.email_from] + (case.email_cc or '').split(',')
396             emails = filter(None, emails)
397             body = case.description or ''
398             if case.user_id.signature:
399                 body += '\n\n%s' % (case.user_id.signature)
400
401             emailfrom = case.user_id.address_id and case.user_id.address_id.email or False
402             if not emailfrom:
403                 raise osv.except_osv(_('Error!'),
404                         _("No E-Mail ID Found for your Company address!"))
405
406             tools.email_send(
407                 emailfrom,
408                 emails,
409                 '['+str(case.id)+'] '+case.name,
410                 self.format_body(body),
411                 reply_to=case.section_id.reply_to,
412                 openobject_id=str(case.id)
413             )
414             self.__history(cr, uid, [case], _('Send'), history=True, email=emails, details=body, email_from=emailfrom)
415         return True
416
417     def onchange_partner_id(self, cr, uid, ids, part, email=False):
418         if not part:
419             return {'value':{'partner_address_id': False, 
420                             'email_from': False,
421                             }}
422         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
423         data = {'partner_address_id': addr['contact']}
424         data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
425         return {'value':data}
426
427     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
428         data = {}
429         if not add:
430             return {'value': {'email_from': False, 'partner_name2': False}}
431         address= self.pool.get('res.partner.address').browse(cr, uid, add)
432         data['email_from'] = address.email
433         return {'value': data}
434
435     def case_close(self, cr, uid, ids, *args):
436         cases = self.browse(cr, uid, ids)
437         cases[0].state # to fill the browse record cache
438         self.__history(cr, uid, cases, _('Close'))
439         self.write(cr, uid, ids, {'state':'done', 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
440         #
441         # We use the cache of cases to keep the old case state
442         #
443         self._action(cr,uid, cases, 'done')
444         return True
445
446     def case_escalate(self, cr, uid, ids, *args):
447         cases = self.browse(cr, uid, ids)
448         for case in cases:
449             data = {'active':True, 'user_id': False}
450             if case.section_id.parent_id:
451                 data['section_id'] = case.section_id.parent_id.id
452                 if case.section_id.parent_id.user_id:
453                     data['user_id'] = case.section_id.parent_id.user_id.id
454             else:
455                 raise osv.except_osv(_('Error !'), _('You can not escalate this case.\nYou are already at the top level.'))
456             self.write(cr, uid, ids, data)
457         cases = self.browse(cr, uid, ids)
458         self.__history(cr, uid, cases, _('Escalate'))
459         self._action(cr,uid, cases, 'escalate')        
460         return True
461
462
463     def case_open(self, cr, uid, ids, *args):
464         cases = self.browse(cr, uid, ids)
465         self.__history(cr, uid, cases, _('Open'))
466         for case in cases:
467             data = {'state':'open', 'active':True}
468             if not case.user_id:
469                 data['user_id'] = uid
470             self.write(cr, uid, ids, data)
471         self._action(cr,uid, cases, 'open')
472         return True
473
474
475     def case_cancel(self, cr, uid, ids, *args):
476         cases = self.browse(cr, uid, ids)
477         cases[0].state # to fill the browse record cache
478         self.__history(cr, uid, cases, _('Cancel'))
479         self.write(cr, uid, ids, {'state':'cancel', 'active':True})
480         self._action(cr,uid, cases, 'cancel')
481         return True
482
483     def case_pending(self, cr, uid, ids, *args):
484         cases = self.browse(cr, uid, ids)
485         cases[0].state # to fill the browse record cache
486         self.__history(cr, uid, cases, _('Pending'))
487         self.write(cr, uid, ids, {'state':'pending', 'active':True})
488         self._action(cr,uid, cases, 'pending')
489         return True
490
491     def case_reset(self, cr, uid, ids, *args):
492         cases = self.browse(cr, uid, ids)
493         cases[0].state # to fill the browse record cache
494         self.__history(cr, uid, cases, _('Draft'))
495         self.write(cr, uid, ids, {'state':'draft', 'active':True})
496         self._action(cr,uid, cases, 'draft')
497         return True   
498
499 crm_case()
500
501
502 class crm_case_log(osv.osv):
503     _name = "crm.case.log"
504     _description = "Case Communication History"
505     _order = "id desc"
506     _columns = {
507         'name': fields.char('Status', size=64),
508         'date': fields.datetime('Date'),        
509         'section_id': fields.many2one('crm.case.section', 'Section'),
510         'user_id': fields.many2one('res.users', 'User Responsible', readonly=True),
511         'model_id': fields.many2one('ir.model', "Model"),
512         'res_id': fields.integer('Resource ID'),
513     }
514     _defaults = {
515         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
516     }
517 crm_case_log()
518
519 class crm_case_history(osv.osv):
520     _name = "crm.case.history"
521     _description = "Case history"
522     _order = "id desc"
523     _inherits = {'crm.case.log':"log_id"}    
524
525     def _note_get(self, cursor, user, ids, name, arg, context=None):
526         res = {}
527         for hist in self.browse(cursor, user, ids, context or {}):
528             res[hist.id] = (hist.email or '/') + ' (' + str(hist.date) + ')\n'
529             res[hist.id] += (hist.description or '')
530         return res
531     _columns = {
532         'description': fields.text('Description'),
533         'note': fields.function(_note_get, method=True, string="Description", type="text"),
534         'email_to': fields.char('Email TO', size=84),
535         'email_from' : fields.char('Email From', size=84),
536         'log_id': fields.many2one('crm.case.log','Log',ondelete='cascade'),
537     }
538 crm_case_history()
539
540 class crm_email_add_cc_wizard(osv.osv_memory):
541     _name = "crm.email.add.cc"
542     _description = "Email Add CC"
543     _columns = {
544         'name': fields.selection([('user','User'),('partner','Partner'),('email','Email Address')], 'Send to', required=True),
545         'user_id': fields.many2one('res.users',"User"),
546         'partner_id': fields.many2one('res.partner',"Partner"),
547         'email': fields.char('Email', size=32),
548         'subject': fields.char('Subject', size=32),
549     }
550
551     def change_email(self, cr, uid, ids, user, partner):
552         if (not partner and not user):
553             return {'value':{'email': False}}
554         email = False
555         if partner:
556             addr = self.pool.get('res.partner').address_get(cr, uid, [partner], ['contact'])
557             if addr:
558                 email = self.pool.get('res.partner.address').read(cr, uid,addr['contact'] , ['email'])['email']
559         elif user:
560             addr = self.pool.get('res.users').read(cr, uid, user, ['address_id'])['address_id']
561             if addr:
562                 email = self.pool.get('res.partner.address').read(cr, uid,addr[0] , ['email'])['email']
563         return {'value':{'email': email}}
564
565
566     def add_cc(self, cr, uid, ids, context={}):
567         data = self.read(cr, uid, ids[0])
568         email = data['email']
569         subject = data['subject']
570
571         if not context:
572             return {}
573         history_line = self.pool.get('crm.case.history').browse(cr, uid, context['active_id'])
574         model = history_line.log_id.model_id.model
575         model_pool = self.pool.get(model)
576         case = model_pool.browse(cr, uid, history_line.log_id.res_id)
577         body = history_line.description.replace('\n','\n> ')
578         flag = tools.email_send(
579             email,
580             [case.user_id.address_id.email],
581             subject or '['+str(case.id)+'] '+case.name,
582             model_pool.format_body(body),            
583             openobject_id=str(case.id),
584             subtype="html"
585         )
586         if flag:
587             model_pool.write(cr, uid, case.id, {'email_cc' : case.email_cc and case.email_cc +','+ email or email})
588             self.__history(cr, uid, [case], _('Send'), history=True, email=email, details=body, email_from=case.user_id.address_id.email)
589         else:
590             raise osv.except_osv(_('Email Fail!'),("Lastest Email is not sent successfully"))
591         return {}
592
593 crm_email_add_cc_wizard()
594
595 class users(osv.osv):
596     _inherit = 'res.users'
597     _description = "Users"
598     _columns = {
599         'context_section_id': fields.many2one('crm.case.section', 'Sales Team'),
600     }
601 users()
602
603
604 class res_partner(osv.osv):
605     _inherit = 'res.partner'
606     _columns = {
607         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
608     }
609
610 res_partner()
611
612
613 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: