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)]
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('Private Note'),
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 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
237 help="A Issue's kanban state indicates special situations affecting it:\n"
238 " * Normal is the default situation\n"
239 " * Blocked indicates something is preventing the progress of this issue\n"
240 " * Ready for next stage indicates the issue is ready to be pulled to the next stage",
241 readonly=True, required=False),
242 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
243 '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"),
244 'date_open': fields.datetime('Opened', readonly=True,select=True),
245 # Project Issue fields
246 'date_closed': fields.datetime('Closed', readonly=True,select=True),
247 'date': fields.datetime('Date'),
248 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
249 'categ_ids': fields.many2many('project.category', string='Tags'),
250 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
251 'version_id': fields.many2one('project.issue.version', 'Version'),
252 'stage_id': fields.many2one ('project.task.type', 'Stage',
253 domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
254 'project_id':fields.many2one('project.project', 'Project'),
255 'duration': fields.float('Duration'),
256 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
257 'day_open': fields.function(_compute_day, string='Days to Open', \
258 multi='compute_day', type="float", store=True),
259 'day_close': fields.function(_compute_day, string='Days to Close', \
260 multi='compute_day', type="float", store=True),
261 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
262 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
263 multi='compute_day', type="float", store=True),
264 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
265 multi='compute_day', type="float", store=True),
266 'inactivity_days': fields.function(_compute_day, string='Days since last action', \
267 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
268 'color': fields.integer('Color Index'),
269 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
270 'date_action_last': fields.datetime('Last Action', readonly=1),
271 'date_action_next': fields.datetime('Next Action', readonly=1),
272 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
274 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
275 'project.task': (_get_issue_task, ['progress'], 10),
276 'project.task.work': (_get_issue_work, ['hours'], 10),
282 'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
283 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
284 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
285 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
286 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
287 'priority': crm.AVAILABLE_PRIORITIES[2][0],
288 'kanban_state': 'normal',
292 'stage_id': _read_group_stage_ids
295 def set_priority(self, cr, uid, ids, priority, *args):
298 return self.write(cr, uid, ids, {'priority' : priority})
300 def set_high_priority(self, cr, uid, ids, *args):
301 """Set lead priority to high
303 return self.set_priority(cr, uid, ids, '1')
305 def set_normal_priority(self, cr, uid, ids, *args):
306 """Set lead priority to normal
308 return self.set_priority(cr, uid, ids, '3')
310 def convert_issue_task(self, cr, uid, ids, context=None):
314 case_obj = self.pool.get('project.issue')
315 data_obj = self.pool.get('ir.model.data')
316 task_obj = self.pool.get('project.task')
318 result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
319 res = data_obj.read(cr, uid, result, ['res_id'])
320 id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
321 id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
323 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
325 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
327 for bug in case_obj.browse(cr, uid, ids, context=context):
328 new_task_id = task_obj.create(cr, uid, {
330 'partner_id': bug.partner_id.id,
331 'description':bug.description,
332 'date_deadline': bug.date,
333 'project_id': bug.project_id.id,
334 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
335 'priority': str(int(bug.priority) - 1),
336 'user_id': bug.user_id.id,
337 'planned_hours': 0.0,
340 'task_id': new_task_id,
341 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
343 self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
344 case_obj.write(cr, uid, [bug.id], vals, context=context)
345 self.case_pending_send_note(cr, uid, [bug.id], context=context)
350 'view_mode': 'form,tree',
351 'res_model': 'project.task',
352 'res_id': int(new_task_id),
354 'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
355 'type': 'ir.actions.act_window',
356 'search_view_id': res['res_id'],
360 def copy(self, cr, uid, id, default=None, context=None):
361 issue = self.read(cr, uid, id, ['name'], context=context)
364 default = default.copy()
365 default.update(name=_('%s (copy)') % (issue['name']))
366 return super(project_issue, self).copy(cr, uid, id, default=default,
369 def _subscribe_project_followers_to_issue(self, cr, uid, task_id, context=None):
370 """ TDE note: not the best way to do this, we could override _get_followers
371 of issue, and perform a better mapping of subtypes than a mapping
373 However we will keep this implementation, maybe to be refactored
374 in 7.1 of future versions. """
375 # task followers are project followers, with matching subtypes
376 task_record = self.browse(cr, uid, task_id, context=context)
377 subtype_obj = self.pool.get('mail.message.subtype')
378 follower_obj = self.pool.get('mail.followers')
379 if task_record.project_id:
381 task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
382 task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
383 # fetch subscriptions
384 follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
386 for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
387 if not follower.subtype_ids:
389 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
390 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
391 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
392 subtype_ids=task_subtype_ids, context=context)
394 def write(self, cr, uid, ids, vals, context=None):
395 #Update last action date every time the user change the stage, the state or send a new email
396 logged_fields = ['stage_id', 'state', 'message_ids']
397 if any([field in vals for field in logged_fields]):
398 vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
400 # subscribe new project followers to the issue
401 if vals.get('project_id'):
403 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
405 return super(project_issue, self).write(cr, uid, ids, vals, context)
407 def onchange_task_id(self, cr, uid, ids, task_id, context=None):
410 task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
411 return {'value': {'user_id': task.user_id.id, }}
413 def case_reset(self, cr, uid, ids, context=None):
414 """Resets case as draft
416 res = super(project_issue, self).case_reset(cr, uid, ids, context)
417 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
420 def create(self, cr, uid, vals, context=None):
421 obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
423 # subscribe project follower to the issue
424 self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
425 self.create_send_note(cr, uid, [obj_id], context=context)
429 # -------------------------------------------------------
431 # -------------------------------------------------------
433 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
434 """ Override of the base.stage method
435 Parameter of the stage search taken from the issue:
436 - type: stage type must be the same or 'both'
437 - section_id: if set, stages must belong to this section or
440 if isinstance(cases, (int, long)):
441 cases = self.browse(cr, uid, cases, context=context)
442 # collect all section_ids
445 section_ids.append(section_id)
448 section_ids.append(task.project_id.id)
449 # OR all section_ids and OR with case_default
452 search_domain += [('|')] * (len(section_ids)-1)
453 for section_id in section_ids:
454 search_domain.append(('project_ids', '=', section_id))
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 # -------------------------------------------------------
543 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
544 """ Override of the (void) default notification method. """
545 stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
546 return self.message_post(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % (stage_name), context=context)
548 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
549 """ Override of default prefix for notifications. """
550 return 'Project issue'
552 def convert_to_task_send_note(self, cr, uid, ids, context=None):
553 message = _("Project issue <b>converted</b> to task.")
554 return self.message_post(cr, uid, ids, body=message, context=context)
556 def create_send_note(self, cr, uid, ids, context=None):
557 message = _("Project issue <b>created</b>.")
558 return self.message_post(cr, uid, ids, body=message, subtype="project_issue.mt_issue_new", context=context)
560 def case_open_send_note(self, cr, uid, ids, context=None):
561 return self.message_post(cr, uid, ids, body=_("Issue <b>started</b>."), subtype="project_issue.mt_issue_started", context=context)
563 def case_escalate_send_note(self, cr, uid, ids, context=None):
564 for obj in self.browse(cr, uid, ids, context=context):
566 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
567 obj.message_post(body=message)
569 message = _("<b>escalated</b>.")
570 obj.message_post(body=message)
573 def case_block_send_note(self, cr, uid, ids, context=None):
574 return self.message_post(cr, uid, ids, body=_("Issue <b>blocked</b>."), subtype="project_issue.mt_issue_blocked", context=context)
576 def case_close_send_note(self, cr, uid, ids, context=None):
577 return self.message_post(cr, uid, ids, body=_("Project issue <b>closed</b>."), subtype="project_issue.mt_issue_closed", context=context)
579 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
580 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
581 self.case_block_send_note(cr, uid, ids, context=context)
584 def set_kanban_state_normal(self, cr, uid, ids, context=None):
585 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
586 self.case_open_send_note(cr, uid, ids, context=context)
589 def set_kanban_state_done(self, cr, uid, ids, context=None):
590 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
594 class project(osv.osv):
595 _inherit = "project.project"
597 def _get_alias_models(self, cr, uid, context=None):
598 return [('project.task', "Tasks"), ("project.issue", "Issues")]
600 def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
601 res = dict.fromkeys(ids, 0)
602 issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
603 for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
604 res[issue.project_id.id] += 1
608 '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)]}),
609 'issue_count': fields.function(_issue_count, type='integer'),
612 def _check_escalation(self, cr, uid, ids, context=None):
613 project_obj = self.browse(cr, uid, ids[0], context=context)
614 if project_obj.project_escalation_id:
615 if project_obj.project_escalation_id.id == project_obj.id:
620 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
625 class account_analytic_account(osv.osv):
626 _inherit = 'account.analytic.account'
627 _description = 'Analytic Account'
630 'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
633 def on_change_template(self, cr, uid, ids, template_id, context=None):
634 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
635 if template_id and 'value' in res:
636 template = self.browse(cr, uid, template_id, context=context)
637 res['value']['use_issues'] = template.use_issues
640 def _trigger_project_creation(self, cr, uid, vals, context=None):
641 if context is None: context = {}
642 res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
643 return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
645 account_analytic_account()
647 class project_project(osv.osv):
648 _inherit = 'project.project'
653 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: