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 base_status.base_stage import base_stage
24 from datetime import datetime
25 from osv import fields,osv
26 from tools.translate import _
30 from tools import html2plaintext
32 class project_issue_version(osv.osv):
33 _name = "project.issue.version"
36 'name': fields.char('Version Number', size=32, required=True),
37 'active': fields.boolean('Active', required=False),
42 project_issue_version()
44 _ISSUE_STATE= [('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending')]
46 class project_issue(base_stage, osv.osv):
47 _name = "project.issue"
48 _description = "Project Issue"
49 _order = "priority, create_date desc"
50 _inherit = ['mail.thread', 'ir.needaction_mixin']
52 def _get_default_project_id(self, cr, uid, context=None):
53 """ Gives default project by checking if present in the context """
54 return self._resolve_project_id_from_context(cr, uid, context=context)
56 def _get_default_stage_id(self, cr, uid, context=None):
57 """ Gives default stage_id """
58 project_id = self._get_default_project_id(cr, uid, context=context)
59 return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
61 def _resolve_project_id_from_context(self, cr, uid, context=None):
62 """ Returns ID of project based on the value of 'default_project_id'
63 context key, or None if it cannot be resolved to a single
68 if type(context.get('default_project_id')) in (int, long):
69 return context.get('default_project_id')
70 if isinstance(context.get('default_project_id'), basestring):
71 project_name = context['default_project_id']
72 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
73 if len(project_ids) == 1:
74 return int(project_ids[0][0])
77 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
78 access_rights_uid = access_rights_uid or uid
79 stage_obj = self.pool.get('project.task.type')
80 order = stage_obj._order
81 # lame hack to allow reverting search, should just work in the trivial case
82 if read_group_order == 'stage_id desc':
83 order = "%s desc" % order
84 # retrieve section_id from the context and write the domain
85 # - ('id', 'in', 'ids'): add columns that should be present
86 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
87 # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
89 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
91 search_domain += ['|', ('project_ids', '=', project_id)]
92 search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
94 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
95 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
96 # restore order of the search
97 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
100 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
101 fold[stage.id] = stage.fold or False
104 def _compute_day(self, cr, uid, ids, fields, args, context=None):
106 @param cr: the current row, from the database cursor,
107 @param uid: the current user’s ID for security checks,
108 @param ids: List of Openday’s IDs
109 @return: difference between current date and log date
110 @param context: A standard dictionary for contextual values
112 cal_obj = self.pool.get('resource.calendar')
113 res_obj = self.pool.get('resource.resource')
116 for issue in self.browse(cr, uid, ids, context=context):
123 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
124 if field in ['working_hours_open','day_open']:
126 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
127 ans = date_open - date_create
128 date_until = issue.date_open
129 #Calculating no. of working hours to open the issue
130 if issue.project_id.resource_calendar_id:
131 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
134 elif field in ['working_hours_close','day_close']:
135 if issue.date_closed:
136 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
137 date_until = issue.date_closed
138 ans = date_close - date_create
139 #Calculating no. of working hours to close the issue
140 if issue.project_id.resource_calendar_id:
141 hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
144 elif field in ['days_since_creation']:
145 if issue.create_date:
146 days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
147 res[issue.id][field] = days_since_creation.days
150 elif field in ['inactivity_days']:
151 res[issue.id][field] = 0
152 if issue.date_action_last:
153 inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
154 res[issue.id][field] = inactive_days.days
159 resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
160 if resource_ids and len(resource_ids):
161 resource_id = resource_ids[0]
162 duration = float(ans.days)
163 if issue.project_id and issue.project_id.resource_calendar_id:
164 duration = float(ans.days) * 24
166 new_dates = cal_obj.interval_min_get(cr, uid,
167 issue.project_id.resource_calendar_id.id,
169 duration, resource=resource_id)
171 date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
172 for in_time, out_time in new_dates:
173 if in_time.date not in no_days:
174 no_days.append(in_time.date)
175 if out_time > date_until:
177 duration = len(no_days)
179 if field in ['working_hours_open','working_hours_close']:
180 res[issue.id][field] = hours
182 res[issue.id][field] = abs(float(duration))
186 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
187 task_pool = self.pool.get('project.task')
189 for issue in self.browse(cr, uid, ids, context=context):
192 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
193 res[issue.id] = {'progress' : progress}
196 def on_change_project(self, cr, uid, ids, project_id, context=None):
199 def _get_issue_task(self, cr, uid, ids, context=None):
201 issue_pool = self.pool.get('project.issue')
202 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
203 issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
206 def _get_issue_work(self, cr, uid, ids, context=None):
208 issue_pool = self.pool.get('project.issue')
209 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
211 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
215 'id': fields.integer('ID', readonly=True),
216 'name': fields.char('Issue', size=128, required=True),
217 'active': fields.boolean('Active', required=False),
218 'create_date': fields.datetime('Creation Date', readonly=True,select=True),
219 'write_date': fields.datetime('Update Date', readonly=True),
220 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
221 multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
222 'date_deadline': fields.date('Deadline'),
223 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
224 select=True, help='Sales team to which Case belongs to.\
225 Define Responsible user and Email account for mail gateway.'),
226 'partner_id': fields.many2one('res.partner', 'Contact', select=1),
227 'company_id': fields.many2one('res.company', 'Company'),
228 'description': fields.text('Description'),
229 'state': fields.related('stage_id', 'state', type="selection", store=True,
230 selection=_ISSUE_STATE, string="Status", readonly=True,
231 help='The status is set to \'Draft\', when a case is created.\
232 If the case is in progress the status is set to \'Open\'.\
233 When the case is over, the status is set to \'Done\'.\
234 If the case needs to be reviewed then the status is \
235 set to \'Pending\'.'),
236 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
237 '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"),
238 'date_open': fields.datetime('Opened', readonly=True,select=True),
239 # Project Issue fields
240 'date_closed': fields.datetime('Closed', readonly=True,select=True),
241 'date': fields.datetime('Date'),
242 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
243 'categ_ids': fields.many2many('project.category', string='Tags'),
244 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
245 'version_id': fields.many2one('project.issue.version', 'Version'),
246 'stage_id': fields.many2one ('project.task.type', 'Stage',
247 domain="['&', ('fold', '=', False), '|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
248 'project_id':fields.many2one('project.project', 'Project'),
249 'duration': fields.float('Duration'),
250 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
251 'day_open': fields.function(_compute_day, string='Days to Open', \
252 multi='compute_day', type="float", store=True),
253 'day_close': fields.function(_compute_day, string='Days to Close', \
254 multi='compute_day', type="float", store=True),
255 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
256 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
257 multi='compute_day', type="float", store=True),
258 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
259 multi='compute_day', type="float", store=True),
260 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
261 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
262 'color': fields.integer('Color Index'),
263 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
264 'date_action_last': fields.datetime('Last Action', readonly=1),
265 'date_action_next': fields.datetime('Next Action', readonly=1),
266 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
268 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
269 'project.task': (_get_issue_task, ['progress'], 10),
270 'project.task.work': (_get_issue_work, ['hours'], 10),
276 'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
277 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
278 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
279 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
280 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
281 'priority': crm.AVAILABLE_PRIORITIES[2][0],
285 'stage_id': _read_group_stage_ids
288 def set_priority(self, cr, uid, ids, priority, *args):
291 return self.write(cr, uid, ids, {'priority' : priority})
293 def set_high_priority(self, cr, uid, ids, *args):
294 """Set lead priority to high
296 return self.set_priority(cr, uid, ids, '1')
298 def set_normal_priority(self, cr, uid, ids, *args):
299 """Set lead priority to normal
301 return self.set_priority(cr, uid, ids, '3')
303 def convert_issue_task(self, cr, uid, ids, context=None):
307 case_obj = self.pool.get('project.issue')
308 data_obj = self.pool.get('ir.model.data')
309 task_obj = self.pool.get('project.task')
311 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
312 res = data_obj.read(cr, uid, result, ['res_id'])
313 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
314 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
316 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
318 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
320 for bug in case_obj.browse(cr, uid, ids, context=context):
321 new_task_id = task_obj.create(cr, uid, {
323 'partner_id': bug.partner_id.id,
324 'description':bug.description,
325 'date_deadline': bug.date,
326 'project_id': bug.project_id.id,
327 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
328 'priority': str(int(bug.priority) - 1),
329 'user_id': bug.user_id.id,
330 'planned_hours': 0.0,
333 'task_id': new_task_id,
334 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
336 self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
337 case_obj.write(cr, uid, [bug.id], vals, context=context)
338 self.case_pending_send_note(cr, uid, [bug.id], context=context)
343 'view_mode': 'form,tree',
344 'res_model': 'project.task',
345 'res_id': int(new_task_id),
347 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
348 'type': 'ir.actions.act_window',
349 'search_view_id': res['res_id'],
353 def copy(self, cr, uid, id, default=None, context=None):
354 issue = self.read(cr, uid, id, ['name'], context=context)
357 default = default.copy()
358 default.update(name=_('%s (copy)') % (issue['name']))
359 return super(project_issue, self).copy(cr, uid, id, default=default,
362 def _subscribe_project_followers_to_issue(self, cr, uid, task_id, context=None):
363 """ TDE note: not the best way to do this, we could override _get_followers
364 of issue, and perform a better mapping of subtypes than a mapping
366 However we will keep this implementation, maybe to be refactored
367 in 7.1 of future versions. """
368 # task followers are project followers, with matching subtypes
369 task_record = self.browse(cr, uid, task_id, context=context)
370 subtype_obj = self.pool.get('mail.message.subtype')
371 follower_obj = self.pool.get('mail.followers')
372 if task_record.project_id:
374 task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
375 task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
376 # fetch subscriptions
377 follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
379 for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
380 if not follower.subtype_ids:
382 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
383 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
384 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
385 subtype_ids=task_subtype_ids, context=context)
387 def write(self, cr, uid, ids, vals, context=None):
388 #Update last action date every time the user change the stage, the state or send a new email
389 logged_fields = ['stage_id', 'state', 'message_ids']
390 if any([field in vals for field in logged_fields]):
391 vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
393 # subscribe new project followers to the issue
394 if vals.get('project_id'):
396 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
397 st = self.pool.get('project.task.type').browse(cr, uid, vals.get('stage_id'), context=context);
398 if st.state =='open':
399 self.issue_start_send_note(cr, uid, ids, context=context)
400 return super(project_issue, self).write(cr, uid, ids, vals, context)
402 def issue_start_send_note(self, cr, uid, ids, context=None):
403 return self.message_post(cr, uid, ids, body=_("Issue has been <b>started</b>."), subtype="mt_issue_change", context=context)
405 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
408 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
409 return {'value': {'user_id': task.user_id.id, }}
411 def case_reset(self, cr, uid, ids, context=None):
412 """Resets case as draft
414 res = super(project_issue, self).case_reset(cr, uid, ids, context)
415 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
418 def create(self, cr, uid, vals, context=None):
419 obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
421 # subscribe project follower to the issue
422 self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
423 self.create_send_note(cr, uid, [obj_id], context=context)
427 # -------------------------------------------------------
429 # -------------------------------------------------------
431 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
432 """ Override of the base.stage method
433 Parameter of the stage search taken from the issue:
434 - type: stage type must be the same or 'both'
435 - section_id: if set, stages must belong to this section or
438 if isinstance(cases, (int, long)):
439 cases = self.browse(cr, uid, cases, context=context)
440 # collect all section_ids
443 section_ids.append(section_id)
446 section_ids.append(task.project_id.id)
447 # OR all section_ids and OR with case_default
450 search_domain += [('|')] * len(section_ids)
451 for section_id in section_ids:
452 search_domain.append(('project_ids', '=', section_id))
453 search_domain.append(('case_default', '=', True))
454 # AND with the domain in parameter
455 search_domain += list(domain)
456 # perform search, return the first found
457 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
462 def case_cancel(self, cr, uid, ids, context=None):
464 self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
467 def case_escalate(self, cr, uid, ids, context=None):
468 cases = self.browse(cr, uid, ids)
471 if case.project_id.project_escalation_id:
472 data['project_id'] = case.project_id.project_escalation_id.id
473 if case.project_id.project_escalation_id.user_id:
474 data['user_id'] = case.project_id.project_escalation_id.user_id.id
476 self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
478 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
479 self.case_set(cr, uid, ids, 'draft', data, context=context)
480 self.case_escalate_send_note(cr, uid, [case.id], context=context)
483 # -------------------------------------------------------
485 # -------------------------------------------------------
487 def message_new(self, cr, uid, msg, custom_values=None, context=None):
488 """ Overrides mail_thread message_new that is called by the mailgateway
489 through message_process.
490 This override updates the document according to the email.
492 if custom_values is None: custom_values = {}
493 if context is None: context = {}
494 context['state_to'] = 'draft'
496 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
498 custom_values.update({
499 'name': msg.get('subject') or _("No Subject"),
501 'email_from': msg.get('from'),
502 'email_cc': msg.get('cc'),
505 if msg.get('priority'):
506 custom_values['priority'] = msg.get('priority')
508 res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
509 # self.convert_to_bug(cr, uid, [res_id], context=context)
512 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
513 """ Overrides mail_thread message_update that is called by the mailgateway
514 through message_process.
515 This method updates the document according to the email.
517 if isinstance(ids, (str, int, long)):
519 if update_vals is None: update_vals = {}
521 # Update doc values according to the message
522 if msg.get('priority'):
523 update_vals['priority'] = msg.get('priority')
524 # Parse 'body' to find values to update
526 'cost': 'planned_cost',
527 'revenue': 'planned_revenue',
528 'probability': 'probability',
530 for line in msg.get('body', '').split('\n'):
532 res = tools.misc.command_re.match(line)
533 if res and maps.get(res.group(1).lower(), False):
534 key = maps.get(res.group(1).lower())
535 update_vals[key] = res.group(2).lower()
537 return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
539 # -------------------------------------------------------
540 # OpenChatter methods and notifications
541 # -------------------------------------------------------
542 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
543 """ Override of the (void) default notification method. """
544 stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
545 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="mt_issue_new", context=context)
547 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
548 """ Override of default prefix for notifications. """
549 return 'Project issue'
551 def convert_to_task_send_note(self, cr, uid, ids, context=None):
552 message = _("Project issue <b>converted</b> to task.")
553 return self.message_post(cr, uid, ids, body=message, context=context)
555 def create_send_note(self, cr, uid, ids, context=None):
556 message = _("Project issue <b>created</b>.")
557 return self.message_post(cr, uid, ids, body=message, subtype="mt_issue_new", context=context)
559 def case_escalate_send_note(self, cr, uid, ids, context=None):
560 for obj in self.browse(cr, uid, ids, context=context):
562 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
563 obj.message_post(body=message)
565 message = _("<b>escalated</b>.")
566 obj.message_post(body=message)
571 class project(osv.osv):
572 _inherit = "project.project"
574 def _get_alias_models(self, cr, uid, context=None):
575 return [('project.task', "Tasks"), ("project.issue", "Issues")]
577 def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
578 res = dict.fromkeys(ids, 0)
579 issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
580 for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
581 res[issue.project_id.id] += 1
585 '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)]}),
586 'issue_count': fields.function(_issue_count, type='integer'),
589 def _check_escalation(self, cr, uid, ids, context=None):
590 project_obj = self.browse(cr, uid, ids[0], context=context)
591 if project_obj.project_escalation_id:
592 if project_obj.project_escalation_id.id == project_obj.id:
597 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
602 class account_analytic_account(osv.osv):
603 _inherit = 'account.analytic.account'
604 _description = 'Analytic Account'
607 'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
610 def on_change_template(self, cr, uid, ids, template_id, context=None):
611 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
612 if template_id and 'value' in res:
613 template = self.browse(cr, uid, template_id, context=context)
614 res['value']['use_issues'] = template.use_issues
617 def _trigger_project_creation(self, cr, uid, vals, context=None):
618 if context is None: context = {}
619 res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
620 return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
622 account_analytic_account()
624 class project_project(osv.osv):
625 _inherit = 'project.project'
630 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: