555f4c65f4d4060895b52c53641230c88d2f84af
[odoo/odoo.git] / addons / project / project.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 from lxml import etree
23 from mx import DateTime
24 from mx.DateTime import now
25 import time
26 from tools.translate import _
27
28 from osv import fields, osv
29 from tools.translate import _
30
31 class project(osv.osv):
32     _name = "project.project"
33     _description = "Project"
34
35     def _complete_name(self, cr, uid, ids, name, args, context):
36         res = {}
37         for m in self.browse(cr, uid, ids, context=context):
38             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
39         return res
40
41
42     def check_recursion(self, cursor, user, ids, parent=None):
43         return super(project, self).check_recursion(cursor, user, ids,
44                 parent=parent)
45
46     def onchange_partner_id(self, cr, uid, ids, part):
47         if not part:
48             return {'value':{'contact_id': False, 'pricelist_id': False}}
49         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
50
51         pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
52         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
53
54     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
55         res = {}.fromkeys(ids, 0.0)
56         progress = {}
57         if not ids:
58             return res
59         ids2 = self.search(cr, uid, [('parent_id','child_of',ids)])
60         if ids2:
61             cr.execute('''SELECT
62                     project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
63                 FROM
64                     project_task
65                 WHERE
66                     project_id in ('''+','.join(map(str,ids2))+''') AND
67                     state<>'cancelled'
68                 GROUP BY
69                     project_id''')
70             progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3])), cr.fetchall()))
71         for project in self.browse(cr, uid, ids, context=context):
72             s = [0.0,0.0,0.0]
73             tocompute = [project]
74             while tocompute:
75                 p = tocompute.pop()
76                 tocompute += p.child_id
77                 for i in range(3):
78                     s[i] += progress.get(p.id, (0.0,0.0,0.0))[i]
79             res[project.id] = {
80                 'planned_hours': s[0],
81                 'effective_hours': s[2],
82                 'total_hours': s[1],
83                 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
84             }
85         return res
86
87     def unlink(self, cr, uid, ids, *args, **kwargs):
88         for proj in self.browse(cr, uid, ids):
89             if proj.tasks:
90                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
91         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
92     _columns = {
93         'name': fields.char("Project Name", size=128, required=True),
94         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128),
95         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
96         'category_id': fields.many2one('account.analytic.account','Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc."),
97         'priority': fields.integer('Sequence'),
98         'manager': fields.many2one('res.users', 'Project Manager'),
99         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team."),
100         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
101         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
102         'parent_id': fields.many2one('project.project', 'Parent Project',\
103             help="If you have [?] in the name, it means there are no analytic account linked to project."),
104         'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
105         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project."),
106         'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project."),
107         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project."),
108         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
109         'date_start': fields.date('Starting Date'),
110         'date_end': fields.date('Expected End'),
111         'partner_id': fields.many2one('res.partner', 'Partner'),
112         'contact_id': fields.many2one('res.partner.address', 'Contact'),
113         'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer."),
114         'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed."),
115         'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed."),
116         'notes': fields.text('Notes', help="Internal description of the project."),
117         'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
118         'state': fields.selection([('template', 'Template'), ('open', 'Running'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True,
119                                   help='The project can be in either if the states \'Template\' and \'Running\'.\n If it is template then we can make projects based on the template projects. If its in \'Running\' state it is a normal project.\
120                                  \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'),
121         'company_id': fields.many2one('res.company', 'Company'),
122      }
123
124     _defaults = {
125         'active': lambda *a: True,
126         'manager': lambda object,cr,uid,context: uid,
127         'priority': lambda *a: 1,
128         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
129         'state': lambda *a: 'open',
130         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.project', context=c)
131     }
132
133     _order = "parent_id,priority,name"
134     _constraints = [
135         (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
136     ]
137
138     # toggle activity of projects, their sub projects and their tasks
139     def set_template(self, cr, uid, ids, context={}):
140         res = self.setActive(cr, uid, ids, value=False, context=context)
141         return res
142
143     def set_done(self, cr, uid, ids, context={}):
144         self.write(cr, uid, ids, {'state':'done'}, context=context)
145         return True
146
147     def set_cancel(self, cr, uid, ids, context={}):
148         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
149         return True
150
151     def set_pending(self, cr, uid, ids, context={}):
152         self.write(cr, uid, ids, {'state':'pending'}, context=context)
153         return True
154
155     def set_open(self, cr, uid, ids, context={}):
156         self.write(cr, uid, ids, {'state':'open'}, context=context)
157         return True
158
159     def reset_project(self, cr, uid, ids, context={}):
160         res = self.setActive(cr, uid, ids,value=True, context=context)
161         return res
162
163     def copy(self, cr, uid, id, default={},context={}):
164         proj = self.browse(cr, uid, id, context=context)
165         default = default or {}
166         context['active_test'] = False
167         default['state'] = 'open'
168         if not default.get('name', False):
169             default['name'] = proj.name+_(' (copy)')
170         res = super(project, self).copy(cr, uid, id, default, context)
171         ids = self.search(cr, uid, [('parent_id','child_of', [res])])
172         cr.execute('update project_task set active=True where project_id in ('+','.join(map(str, ids))+')')
173         return res
174
175     def duplicate_template(self, cr, uid, ids,context={}):
176         for proj in self.browse(cr, uid, ids):
177             parent_id=context.get('parent_id',False)
178             new_id=self.pool.get('project.project').copy(cr, uid, proj.id,default={'name':proj.name+_(' (copy)'),'state':'open','parent_id':parent_id})
179             cr.execute('select id from project_task where project_id=%s', (proj.id,))
180             res = cr.fetchall()
181             for (tasks_id,) in res:
182                 self.pool.get('project.task').copy(cr, uid, tasks_id,default={'project_id':new_id,'active':True}, context=context)
183             cr.execute('select id from project_project where parent_id=%s', (proj.id,))
184             res = cr.fetchall()
185             project_ids = [x[0] for x in res]
186             for child in project_ids:
187                 self.duplicate_template(cr, uid, [child],context={'parent_id':new_id})
188
189         # TODO : Improve this to open the new project (using a wizard)
190
191         cr.commit()
192         raise osv.except_osv(_('Operation Done'), _('A new project has been created !\nWe suggest you to close this one and work on this new project.'))
193
194     # set active value for a project, its sub projects and its tasks
195     def setActive(self, cr, uid, ids, value=True, context={}):
196         for proj in self.browse(cr, uid, ids, context):
197             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
198             cr.execute('select id from project_task where project_id=%s', (proj.id,))
199             tasks_id = [x[0] for x in cr.fetchall()]
200             if tasks_id:
201                 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
202             cr.execute('select id from project_project where parent_id=%s', (proj.id,))
203             project_ids = [x[0] for x in cr.fetchall()]
204             for child in project_ids:
205                 self.setActive(cr, uid, [child], value, context)
206         return True
207 project()
208
209 class project_task_type(osv.osv):
210     _name = 'project.task.type'
211     _description = 'Project task type'
212     _columns = {
213         'name': fields.char('Type', required=True, size=64, translate=True),
214         'description': fields.text('Description'),
215     }
216 project_task_type()
217
218 class task(osv.osv):
219     _name = "project.task"
220     _description = "Tasks"
221     _date_name = "date_start"
222     def _str_get(self, task, level=0, border='***', context={}):
223         return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
224             border[0]+' '+(task.name or '')+'\n'+ \
225             (task.description or '')+'\n\n'
226
227     def _history_get(self, cr, uid, ids, name, args, context={}):
228         result = {}
229         for task in self.browse(cr, uid, ids, context=context):
230             result[task.id] = self._str_get(task, border='===')
231             t2 = task.parent_id
232             level = 0
233             while t2:
234                 level -= 1
235                 result[task.id] = self._str_get(t2, level) + result[task.id]
236                 t2 = t2.parent_id
237             t3 = map(lambda x: (x,1), task.child_ids)
238             while t3:
239                 t2 = t3.pop(0)
240                 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
241                 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
242         return result
243
244 # Compute: effective_hours, total_hours, progress
245     def _hours_get(self, cr, uid, ids, field_names, args, context):
246         task_set = ','.join(map(str, ids))
247         cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
248         hours = dict(cr.fetchall())
249         res = {}
250         for task in self.browse(cr, uid, ids, context=context):
251             res[task.id] = {}
252             res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
253             res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
254             if (task.remaining_hours + hours.get(task.id, 0.0)):
255                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2)
256             else:
257                 res[task.id]['progress'] = 0.0
258             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
259         return res
260
261     def onchange_planned(self, cr, uid, ids, planned, effective=0.0):
262         return {'value':{'remaining_hours': planned-effective}}
263
264     def _default_project(self, cr, uid, context={}):
265         if 'project_id' in context and context['project_id']:
266             return context['project_id']
267         return False
268
269     #_sql_constraints = [
270     #    ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
271     #]
272
273     def copy_data(self, cr, uid, id, default={},context={}):
274         default = default or {}
275         default['work_ids'] = []
276         return super(task, self).copy_data(cr, uid, id, default, context)
277
278     _columns = {
279         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it."),
280         'name': fields.char('Task summary', size=128, required=True),
281         'description': fields.text('Description'),
282         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
283         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
284         'type': fields.many2one('project.task.type', 'Type'),
285         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
286                                   help='If the task is created the state \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
287                                   \n If the task is over, the states is set to \'Done\'.'),
288         'date_start': fields.datetime('Starting Date'),
289         'date_deadline': fields.datetime('Deadline'),
290         'date_close': fields.datetime('Date Closed', readonly=True),
291         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
292             help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
293         'parent_id': fields.many2one('project.task', 'Parent Task'),
294         'child_ids': fields.one2many('project.task', 'parent_id', 'Delegated Tasks'),
295         'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
296         'notes': fields.text('Notes'),
297
298         'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
299         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
300         'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
301         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
302         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
303         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as: Total Time - Estimated Time. It gives the difference of the time estimated by the project manager and the real time to close the task."),
304
305         'user_id': fields.many2one('res.users', 'Assigned to'),
306         'delegated_user_id': fields.related('child_ids','user_id',type='many2one', relation='res.users', string='Delegated To'),
307         'partner_id': fields.many2one('res.partner', 'Partner'),
308         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
309         'manager_id': fields.related('project_id','manager', type='many2one', relation='res.users', string='Project Manager'),
310         'company_id': fields.many2one('res.company', 'Company'),
311     }
312     _defaults = {
313         'user_id': lambda obj,cr,uid,context: uid,
314         'state': lambda *a: 'draft',
315         'priority': lambda *a: '2',
316         'progress': lambda *a: 0,
317         'sequence': lambda *a: 10,
318         'active': lambda *a: True,
319         'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
320         'project_id': _default_project,
321         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
322     }
323     _order = "sequence, priority, date_deadline, id"
324
325     #
326     # Override view according to the company definition
327     #
328     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
329         obj_tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode_id
330         tm = obj_tm and obj_tm.name or 'Hours'
331
332         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
333
334         if tm in ['Hours','Hour']:
335             return res
336
337         eview = etree.fromstring(res['arch'])
338
339         def _check_rec(eview):
340             if eview.attrib.get('widget','') == 'float_time':
341                 eview.set('widget','float')
342             for child in eview:
343                 _check_rec(child)
344             return True
345
346         _check_rec(eview)
347
348         res['arch'] = etree.tostring(eview)
349
350         for f in res['fields']:
351             if 'Hours' in res['fields'][f]['string']:
352                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
353
354         return res
355
356     def do_close(self, cr, uid, ids, *args):
357         request = self.pool.get('res.request')
358         tasks = self.browse(cr, uid, ids)
359         for task in tasks:
360             project = task.project_id
361             if project:
362                 if project.warn_manager and project.manager and (project.manager.id != uid):
363                     request.create(cr, uid, {
364                         'name': _("Task '%s' closed") % task.name,
365                         'state': 'waiting',
366                         'act_from': uid,
367                         'act_to': project.manager.id,
368                         'ref_partner_id': task.partner_id.id,
369                         'ref_doc1': 'project.task,%d'% (task.id,),
370                         'ref_doc2': 'project.project,%d'% (project.id,),
371                     })
372             self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
373             if task.parent_id and task.parent_id.state in ('pending','draft'):
374                 reopen = True
375                 for child in task.parent_id.child_ids:
376                     if child.id != task.id and child.state not in ('done','cancelled'):
377                         reopen = False
378                 if reopen:
379                     self.do_reopen(cr, uid, [task.parent_id.id])
380         return True
381
382     def do_reopen(self, cr, uid, ids, *args):
383         request = self.pool.get('res.request')
384         tasks = self.browse(cr, uid, ids)
385         for task in tasks:
386             project = task.project_id
387             if project and project.warn_manager and project.manager.id and (project.manager.id != uid):
388                 request.create(cr, uid, {
389                     'name': _("Task '%s' set in progress") % task.name,
390                     'state': 'waiting',
391                     'act_from': uid,
392                     'act_to': project.manager.id,
393                     'ref_partner_id': task.partner_id.id,
394                     'ref_doc1': 'project.task,%d' % task.id,
395                     'ref_doc2': 'project.project,%d' % project.id,
396                 })
397
398             self.write(cr, uid, [task.id], {'state': 'open'})
399         return True
400
401     def do_cancel(self, cr, uid, ids, *args):
402         request = self.pool.get('res.request')
403         tasks = self.browse(cr, uid, ids)
404         for task in tasks:
405             project = task.project_id
406             if project.warn_manager and project.manager and (project.manager.id != uid):
407                 request.create(cr, uid, {
408                     'name': _("Task '%s' cancelled") % task.name,
409                     'state': 'waiting',
410                     'act_from': uid,
411                     'act_to': project.manager.id,
412                     'ref_partner_id': task.partner_id.id,
413                     'ref_doc1': 'project.task,%d' % task.id,
414                     'ref_doc2': 'project.project,%d' % project.id,
415                 })
416             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
417         return True
418
419     def do_open(self, cr, uid, ids, *args):
420         tasks= self.browse(cr,uid,ids)
421         for t in tasks:
422             self.write(cr, uid, [t.id], {'state': 'open'})
423         return True
424
425     def do_draft(self, cr, uid, ids, *args):
426         self.write(cr, uid, ids, {'state': 'draft'})
427         return True
428
429
430     def do_pending(self, cr, uid, ids, *args):
431         self.write(cr, uid, ids, {'state': 'pending'})
432         return True
433
434
435 task()
436
437 class project_work(osv.osv):
438     _name = "project.task.work"
439     _description = "Task Work"
440     _columns = {
441         'name': fields.char('Work summary', size=128),
442         'date': fields.datetime('Date'),
443         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
444         'hours': fields.float('Time Spent'),
445         'user_id': fields.many2one('res.users', 'Done by', required=True),
446         'company_id': fields.related('task_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
447     }
448     _defaults = {
449         'user_id': lambda obj,cr,uid,context: uid,
450         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
451     }
452     _order = "date desc"
453     def create(self, cr, uid, vals, *args, **kwargs):
454         if 'hours' in vals and (not vals['hours']):
455             vals['hours'] = 0.00
456         if 'task_id' in vals:
457             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
458         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
459
460     def write(self, cr, uid, ids,vals,context={}):
461         if 'hours' in vals and (not vals['hours']):
462             vals['hours'] = 0.00
463         if 'hours' in vals:
464             for work in self.browse(cr, uid, ids, context):
465                 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
466         return super(project_work,self).write(cr, uid, ids, vals, context)
467
468     def unlink(self, cr, uid, ids, *args, **kwargs):
469         for work in self.browse(cr, uid, ids):
470             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
471         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
472 project_work()
473
474 class config_compute_remaining(osv.osv_memory):
475     _name='config.compute.remaining'
476     def _get_remaining(self,cr, uid, ctx):
477         if 'active_id' in ctx:
478             return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
479         return False
480
481     _columns = {
482         'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
483             }
484
485     _defaults = {
486         'remaining_hours': _get_remaining
487         }
488
489     def compute_hours(self, cr, uid, ids, context=None):
490         if 'active_id' in context:
491             remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
492             self.pool.get('project.task').write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
493         return {
494                 'type': 'ir.actions.act_window_close',
495          }
496 config_compute_remaining()
497
498 class message(osv.osv):
499     _name = "project.message"
500     _description = "Message"
501     _columns = {
502         'subject': fields.char('Subject', size=128),
503         'description': fields.char('Description', size =128),
504         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
505         'date': fields.date('Date'),
506         'user_id': fields.many2one('res.users', 'User'),
507         }
508 message()
509
510 def _project_get(self, cr, uid, context={}):
511     obj = self.pool.get('project.project')
512     ids = obj.search(cr, uid, [])
513     res = obj.read(cr, uid, ids, ['id','name'], context)
514     res = [(str(r['id']),r['name']) for r in res]
515     return res
516
517 class users(osv.osv):
518     _inherit = 'res.users'
519     _description = "Users"
520     _columns = {
521         'context_project_id': fields.selection(_project_get, 'Project'),
522         }
523
524 users()
525
526 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: