2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from openerp.addons.base_status.base_stage import base_stage
23 from openerp.addons.project.project import _TASK_STATE
24 from openerp.addons.crm import crm
25 from datetime import datetime
26 from openerp.osv import fields, osv, orm
27 from openerp.tools.translate import _
30 from openerp import tools
31 from openerp.tools import html2plaintext
33 class project_issue_version(osv.osv):
34 _name = "project.issue.version"
37 'name': fields.char('Version Number', size=32, required=True),
38 'active': fields.boolean('Active', required=False),
43 project_issue_version()
45 class project_issue(base_stage, osv.osv):
46 _name = "project.issue"
47 _description = "Project Issue"
48 _order = "priority, create_date desc"
49 _inherit = ['mail.thread', 'ir.needaction_mixin']
53 'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'],
54 'project_issue.mt_issue_closed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
55 'project_issue.mt_issue_started': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open',
58 'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'done', 'open'],
61 'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj['kanban_state'] == 'blocked',
65 def create(self, cr, uid, vals, context=None):
68 if vals.get('project_id') and not context.get('default_project_id'):
69 context['default_project_id'] = vals.get('project_id')
71 # context: no_log, because subtype already handle this
72 create_context = dict(context, mail_create_nolog=True)
73 return super(project_issue, self).create(cr, uid, vals, context=create_context)
75 def _get_default_partner(self, cr, uid, context=None):
76 """ Override of base_stage to add project specific behavior """
77 project_id = self._get_default_project_id(cr, uid, context)
79 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
80 if project and project.partner_id:
81 return project.partner_id.id
82 return super(project_issue, self)._get_default_partner(cr, uid, context=context)
84 def _get_default_project_id(self, cr, uid, context=None):
85 """ Gives default project by checking if present in the context """
86 return self._resolve_project_id_from_context(cr, uid, context=context)
88 def _get_default_stage_id(self, cr, uid, context=None):
89 """ Gives default stage_id """
90 project_id = self._get_default_project_id(cr, uid, context=context)
91 return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
93 def _resolve_project_id_from_context(self, cr, uid, context=None):
94 """ Returns ID of project based on the value of 'default_project_id'
95 context key, or None if it cannot be resolved to a single
100 if type(context.get('default_project_id')) in (int, long):
101 return context.get('default_project_id')
102 if isinstance(context.get('default_project_id'), basestring):
103 project_name = context['default_project_id']
104 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
105 if len(project_ids) == 1:
106 return int(project_ids[0][0])
109 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
110 access_rights_uid = access_rights_uid or uid
111 stage_obj = self.pool.get('project.task.type')
112 order = stage_obj._order
113 # lame hack to allow reverting search, should just work in the trivial case
114 if read_group_order == 'stage_id desc':
115 order = "%s desc" % order
116 # retrieve section_id from the context and write the domain
117 # - ('id', 'in', 'ids'): add columns that should be present
118 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
119 # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
121 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
123 search_domain += ['|', ('project_ids', '=', project_id)]
124 search_domain += [('id', 'in', ids)]
126 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
127 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
128 # restore order of the search
129 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
132 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
133 fold[stage.id] = stage.fold or False
136 def _compute_day(self, cr, uid, ids, fields, args, context=None):
138 @param cr: the current row, from the database cursor,
139 @param uid: the current user’s ID for security checks,
140 @param ids: List of Openday’s IDs
141 @return: difference between current date and log date
142 @param context: A standard dictionary for contextual values
144 cal_obj = self.pool.get('resource.calendar')
145 res_obj = self.pool.get('resource.resource')
148 for issue in self.browse(cr, uid, ids, context=context):
150 # if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None
151 if not issue.project_id or not issue.project_id.resource_calendar_id:
154 working_hours = issue.project_id.resource_calendar_id.id
162 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
163 if field in ['working_hours_open','day_open']:
165 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
166 ans = date_open - date_create
167 date_until = issue.date_open
168 #Calculating no. of working hours to open the issue
169 hours = cal_obj._interval_hours_get(cr, uid, working_hours,
172 timezone_from_uid=issue.user_id.id or uid,
173 exclude_leaves=False,
175 elif field in ['working_hours_close','day_close']:
176 if issue.date_closed:
177 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
178 date_until = issue.date_closed
179 ans = date_close - date_create
180 #Calculating no. of working hours to close the issue
181 hours = cal_obj._interval_hours_get(cr, uid, working_hours,
184 timezone_from_uid=issue.user_id.id or uid,
185 exclude_leaves=False,
187 elif field in ['days_since_creation']:
188 if issue.create_date:
189 days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
190 res[issue.id][field] = days_since_creation.days
193 elif field in ['inactivity_days']:
194 res[issue.id][field] = 0
195 if issue.date_action_last:
196 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
197 res[issue.id][field] = inactive_days.days
202 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
203 if resource_ids and len(resource_ids):
204 resource_id = resource_ids[0]
205 duration = float(ans.days) + float(ans.seconds)/(24*3600)
207 if field in ['working_hours_open','working_hours_close']:
208 res[issue.id][field] = hours
209 elif field in ['day_open','day_close']:
210 res[issue.id][field] = duration
214 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
215 task_pool = self.pool.get('project.task')
217 for issue in self.browse(cr, uid, ids, context=context):
220 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
221 res[issue.id] = {'progress' : progress}
224 def on_change_project(self, cr, uid, ids, project_id, context=None):
226 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
227 if project and project.partner_id:
228 return {'value': {'partner_id': project.partner_id.id}}
231 def _get_issue_task(self, cr, uid, ids, context=None):
233 issue_pool = self.pool.get('project.issue')
234 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
235 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
238 def _get_issue_work(self, cr, uid, ids, context=None):
240 issue_pool = self.pool.get('project.issue')
241 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
243 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
247 'id': fields.integer('ID', readonly=True),
248 'name': fields.char('Issue', size=128, required=True),
249 'active': fields.boolean('Active', required=False),
250 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
251 'write_date': fields.datetime('Update Date', readonly=True),
252 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
253 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
254 'date_deadline': fields.date('Deadline'),
255 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
256 select=True, help='Sales team to which Case belongs to.\
257 Define Responsible user and Email account for mail gateway.'),
258 'partner_id': fields.many2one('res.partner', 'Contact', select=1),
259 'company_id': fields.many2one('res.company', 'Company'),
260 'description': fields.text('Private Note'),
261 'state': fields.related('stage_id', 'state', type="selection", store=True,
262 selection=_TASK_STATE, string="Status", readonly=True, select=True,
263 help='The status is set to \'Draft\', when a case is created.\
264 If the case is in progress the status is set to \'Open\'.\
265 When the case is over, the status is set to \'Done\'.\
266 If the case needs to be reviewed then the status is \
267 set to \'Pending\'.'),
268 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
269 track_visibility='onchange',
270 help="A Issue's kanban state indicates special situations affecting it:\n"
271 " * Normal is the default situation\n"
272 " * Blocked indicates something is preventing the progress of this issue\n"
273 " * Ready for next stage indicates the issue is ready to be pulled to the next stage",
274 readonly=True, required=False),
275 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
276 'email_cc': fields.char('Watchers Emails', size=256, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
277 'date_open': fields.datetime('Opened', readonly=True,select=True),
278 # Project Issue fields
279 'date_closed': fields.datetime('Closed', readonly=True,select=True),
280 'date': fields.datetime('Date'),
281 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
282 'categ_ids': fields.many2many('project.category', string='Tags'),
283 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
284 'version_id': fields.many2one('project.issue.version', 'Version'),
285 'stage_id': fields.many2one ('project.task.type', 'Stage',
286 track_visibility='onchange', select=True,
287 domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
288 'project_id': fields.many2one('project.project', 'Project', track_visibility='onchange', select=True),
289 'duration': fields.float('Duration'),
290 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
291 'day_open': fields.function(_compute_day, string='Days to Open', \
292 multi='compute_day', type="float", store=True),
293 'day_close': fields.function(_compute_day, string='Days to Close', \
294 multi='compute_day', type="float", store=True),
295 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1, track_visibility='onchange'),
296 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
297 multi='compute_day', type="float", store=True),
298 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
299 multi='compute_day', type="float", store=True),
300 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
301 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
302 'color': fields.integer('Color Index'),
303 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
304 'date_action_last': fields.datetime('Last Action', readonly=1),
305 'date_action_next': fields.datetime('Next Action', readonly=1),
306 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
308 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
309 'project.task': (_get_issue_task, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
310 'project.task.work': (_get_issue_work, ['hours'], 10),
316 'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
317 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
318 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
319 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
320 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
321 'priority': crm.AVAILABLE_PRIORITIES[2][0],
322 'kanban_state': 'normal',
323 'user_id': lambda obj, cr, uid, context: uid,
327 'stage_id': _read_group_stage_ids
330 def set_priority(self, cr, uid, ids, priority, *args):
333 return self.write(cr, uid, ids, {'priority' : priority})
335 def set_high_priority(self, cr, uid, ids, *args):
336 """Set lead priority to high
338 return self.set_priority(cr, uid, ids, '1')
340 def set_normal_priority(self, cr, uid, ids, *args):
341 """Set lead priority to normal
343 return self.set_priority(cr, uid, ids, '3')
345 def convert_issue_task(self, cr, uid, ids, context=None):
349 case_obj = self.pool.get('project.issue')
350 data_obj = self.pool.get('ir.model.data')
351 task_obj = self.pool.get('project.task')
353 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
354 res = data_obj.read(cr, uid, result, ['res_id'])
355 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
356 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
358 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
360 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
362 for bug in case_obj.browse(cr, uid, ids, context=context):
363 new_task_id = task_obj.create(cr, uid, {
365 'partner_id': bug.partner_id.id,
366 'description':bug.description,
367 'date_deadline': bug.date,
368 'project_id': bug.project_id.id,
369 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
370 'priority': str(int(bug.priority) - 1),
371 'user_id': bug.user_id.id,
372 'planned_hours': 0.0,
375 'task_id': new_task_id,
376 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
378 message = _("Project issue <b>converted</b> to task.")
379 self.message_post(cr, uid, [bug.id], body=message, context=context)
380 case_obj.write(cr, uid, [bug.id], vals, context=context)
385 'view_mode': 'form,tree',
386 'res_model': 'project.task',
387 'res_id': int(new_task_id),
389 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
390 'type': 'ir.actions.act_window',
391 'search_view_id': res['res_id'],
395 def copy(self, cr, uid, id, default=None, context=None):
396 issue = self.read(cr, uid, id, ['name'], context=context)
399 default = default.copy()
400 default.update(name=_('%s (copy)') % (issue['name']))
401 return super(project_issue, self).copy(cr, uid, id, default=default,
404 def write(self, cr, uid, ids, vals, context=None):
406 #Update last action date every time the user changes the stage
407 if 'stage_id' in vals:
408 vals['date_action_last'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
409 if 'kanban_state' not in vals:
410 vals.update(kanban_state='normal')
411 state = self.pool.get('project.task.type').browse(cr, uid, vals['stage_id'], context=context).state
412 for issue in self.browse(cr, uid, ids, context=context):
413 # Change from draft to not draft EXCEPT cancelled: The issue has been opened -> set the opening date
414 if issue.state == 'draft' and state not in ('draft', 'cancelled'):
415 vals['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
416 # Change from not done to done: The issue has been closed -> set the closing date
417 if issue.state != 'done' and state == 'done':
418 vals['date_closed'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
420 return super(project_issue, self).write(cr, uid, ids, vals, context)
422 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
425 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
426 return {'value': {'user_id': task.user_id.id, }}
428 def case_reset(self, cr, uid, ids, context=None):
429 """Resets case as draft
431 res = super(project_issue, self).case_reset(cr, uid, ids, context)
432 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
435 # -------------------------------------------------------
437 # -------------------------------------------------------
439 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
440 return self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
442 def set_kanban_state_normal(self, cr, uid, ids, context=None):
443 return self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
445 def set_kanban_state_done(self, cr, uid, ids, context=None):
446 return self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
448 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
449 """ Override of the base.stage method
450 Parameter of the stage search taken from the issue:
451 - type: stage type must be the same or 'both'
452 - section_id: if set, stages must belong to this section or
455 if isinstance(cases, (int, long)):
456 cases = self.browse(cr, uid, cases, context=context)
457 # collect all section_ids
460 section_ids.append(section_id)
463 section_ids.append(task.project_id.id)
464 # OR all section_ids and OR with case_default
467 search_domain += [('|')] * (len(section_ids)-1)
468 for section_id in section_ids:
469 search_domain.append(('project_ids', '=', section_id))
470 search_domain += list(domain)
471 # perform search, return the first found
472 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
477 def case_cancel(self, cr, uid, ids, context=None):
479 self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
482 def case_escalate(self, cr, uid, ids, context=None):
483 cases = self.browse(cr, uid, ids)
486 if case.project_id.project_escalation_id:
487 data['project_id'] = case.project_id.project_escalation_id.id
488 if case.project_id.project_escalation_id.user_id:
489 data['user_id'] = case.project_id.project_escalation_id.user_id.id
491 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
493 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
494 self.case_set(cr, uid, ids, 'draft', data, context=context)
497 # -------------------------------------------------------
499 # -------------------------------------------------------
501 def message_get_reply_to(self, cr, uid, ids, context=None):
502 """ Override to get the reply_to of the parent project. """
503 return [issue.project_id.message_get_reply_to()[0] if issue.project_id else False
504 for issue in self.browse(cr, uid, ids, context=context)]
506 def message_get_suggested_recipients(self, cr, uid, ids, context=None):
507 recipients = super(project_issue, self).message_get_suggested_recipients(cr, uid, ids, context=context)
509 for issue in self.browse(cr, uid, ids, context=context):
511 self._message_add_suggested_recipient(cr, uid, recipients, issue, partner=issue.partner_id, reason=_('Customer'))
512 elif issue.email_from:
513 self._message_add_suggested_recipient(cr, uid, recipients, issue, email=issue.email_from, reason=_('Customer Email'))
514 except (osv.except_osv, orm.except_orm): # no read access rights -> just ignore suggested recipients because this imply modifying followers
518 def message_new(self, cr, uid, msg, custom_values=None, context=None):
519 """ Overrides mail_thread message_new that is called by the mailgateway
520 through message_process.
521 This override updates the document according to the email.
523 if custom_values is None:
527 context['state_to'] = 'draft'
529 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
532 'name': msg.get('subject') or _("No Subject"),
534 'email_from': msg.get('from'),
535 'email_cc': msg.get('cc'),
536 'partner_id': msg.get('author_id', False),
539 defaults.update(custom_values)
540 res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
543 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs):
544 """ Overrides mail_thread message_post so that we can set the date of last action field when
545 a new message is posted on the issue.
550 res = super(project_issue, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
552 if thread_id and subtype:
553 self.write(cr, uid, thread_id, {'date_action_last': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
557 class project(osv.osv):
558 _inherit = "project.project"
560 def _get_alias_models(self, cr, uid, context=None):
561 return [('project.task', "Tasks"), ("project.issue", "Issues")]
563 def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
564 res = dict.fromkeys(ids, 0)
565 issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
566 for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
567 if issue.state not in ('done', 'cancelled'):
568 res[issue.project_id.id] += 1
572 'project_escalation_id' : fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
573 'issue_count': fields.function(_issue_count, type='integer', string="Unclosed Issues"),
576 def _check_escalation(self, cr, uid, ids, context=None):
577 project_obj = self.browse(cr, uid, ids[0], context=context)
578 if project_obj.project_escalation_id:
579 if project_obj.project_escalation_id.id == project_obj.id:
584 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
589 class account_analytic_account(osv.osv):
590 _inherit = 'account.analytic.account'
591 _description = 'Analytic Account'
594 'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
597 def on_change_template(self, cr, uid, ids, template_id, context=None):
598 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
599 if template_id and 'value' in res:
600 template = self.browse(cr, uid, template_id, context=context)
601 res['value']['use_issues'] = template.use_issues
604 def _trigger_project_creation(self, cr, uid, vals, context=None):
605 if context is None: context = {}
606 res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
607 return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
609 account_analytic_account()
611 class project_project(osv.osv):
612 _inherit = 'project.project'
617 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: