1 # -*- coding: utf-8 -*-
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 ##############################################################################
21 from osv import fields, osv
22 from tools.translate import _
26 from datetime import datetime
27 from dateutil.relativedelta import relativedelta
29 class project_scrum_project(osv.osv):
30 _inherit = 'project.project'
32 'product_owner_id': fields.many2one('res.users', 'Product Owner', help="The person who is responsible for the product"),
33 'sprint_size': fields.integer('Sprint Days', help="Number of days allocated for sprint"),
34 'scrum': fields.integer('Is a Scrum Project'),
37 'product_owner_id': lambda self, cr, uid, context={}: uid,
41 project_scrum_project()
43 class project_scrum_sprint(osv.osv):
44 _name = 'project.scrum.sprint'
45 _description = 'Project Scrum Sprint'
47 def _compute(self, cr, uid, ids, fields, arg, context=None):
48 res = {}.fromkeys(ids, 0.0)
54 for sprint in self.browse(cr, uid, ids, context=context):
59 for bl in sprint.backlog_ids:
60 tot += bl.expected_hours
61 effective += bl.effective_hours
62 prog += bl.expected_hours * bl.progress / 100.0
64 progress = round(prog/tot*100)
66 'progress' : progress,
67 'expected_hours' : tot,
68 'effective_hours': effective,
72 def button_cancel(self, cr, uid, ids, context=None):
75 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
78 def button_draft(self, cr, uid, ids, context=None):
81 self.write(cr, uid, ids, {'state':'draft'}, context=context)
84 def button_open(self, cr, uid, ids, context=None):
87 self.write(cr, uid, ids, {'state':'open'}, context=context)
88 for (id, name) in self.name_get(cr, uid, ids):
89 message = _('Sprint ') + " '" + name + "' "+ _("is Open.")
90 self.log(cr, uid, id, message)
93 def button_close(self, cr, uid, ids, context=None):
96 self.write(cr, uid, ids, {'state':'done'}, context=context)
97 for (id, name) in self.name_get(cr, uid, ids):
98 message = _('Sprint ') + " '" + name + "' "+ _("is Closed.")
99 self.log(cr, uid, id, message)
102 def button_pending(self, cr, uid, ids, context=None):
105 self.write(cr, uid, ids, {'state':'pending'}, context=context)
109 'name' : fields.char('Sprint Name', required=True, size=64),
110 'date_start': fields.date('Starting Date', required=True),
111 'date_stop': fields.date('Ending Date', required=True),
112 'project_id': fields.many2one('project.project', 'Project', required=True, domain=[('scrum','=',1)], help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
113 'product_owner_id': fields.many2one('res.users', 'Product Owner', required=True,help="The person who is responsible for the product"),
114 'scrum_master_id': fields.many2one('res.users', 'Scrum Master', required=True,help="The person who is maintains the processes for the product"),
115 'meeting_ids': fields.one2many('project.scrum.meeting', 'sprint_id', 'Daily Scrum'),
116 'review': fields.text('Sprint Review'),
117 'retrospective': fields.text('Sprint Retrospective'),
118 'backlog_ids': fields.one2many('project.scrum.product.backlog', 'sprint_id', 'Sprint Backlog'),
119 'progress': fields.function(_compute, group_operator="avg", type='float', multi="progress", method=True, string='Progress (0-100)', help="Computed as: Time Spent / Total Time."),
120 'effective_hours': fields.function(_compute, multi="effective_hours", method=True, string='Effective hours', help="Computed using the sum of the task work done."),
121 'expected_hours': fields.function(_compute, multi="expected_hours", method=True, string='Planned Hours', help='Estimated time to do the task.'),
122 'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('cancel','Cancelled'),('done','Done')], 'State', required=True),
126 'date_start' : time.strftime('%Y-%m-%d'),
129 def copy(self, cr, uid, id, default=None, context=None):
130 """Overrides orm copy method
131 @param self: The object pointer
132 @param cr: the current row, from the database cursor,
133 @param uid: the current user’s ID for security checks,
134 @param ids: List of case’s IDs
135 @param context: A standard dictionary for contextual values
141 default.update({'backlog_ids': [], 'meeting_ids': []})
142 return super(project_scrum_sprint, self).copy(cr, uid, id, default=default, context=context)
144 def onchange_project_id(self, cr, uid, ids, project_id=False):
147 proj = self.pool.get('project.project').browse(cr, uid, [project_id])[0]
148 v['product_owner_id']= proj.product_owner_id and proj.product_owner_id.id or False
149 v['scrum_master_id']= proj.user_id and proj.user_id.id or False
150 v['date_stop'] = (datetime.now() + relativedelta(days=int(proj.sprint_size or 14))).strftime('%Y-%m-%d')
153 project_scrum_sprint()
155 class project_scrum_product_backlog(osv.osv):
156 _name = 'project.scrum.product.backlog'
157 _description = 'Product Backlog'
159 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
165 match = re.match('^S\(([0-9]+)\)$', name)
167 ids = self.search(cr, uid, [('sprint_id','=', int(match.group(1)))], limit=limit, context=context)
168 return self.name_get(cr, uid, ids, context=context)
169 return super(project_scrum_product_backlog, self).name_search(cr, uid, name, args, operator,context, limit=limit)
171 def _compute(self, cr, uid, ids, fields, arg, context=None):
172 res = {}.fromkeys(ids, 0.0)
178 for backlog in self.browse(cr, uid, ids, context=context):
184 for task in backlog.tasks_id:
185 task_hours += task.total_hours
186 effective += task.effective_hours
187 tot += task.planned_hours
188 prog += task.planned_hours * task.progress / 100.0
190 progress = round(prog/tot*100)
192 'progress' : progress,
193 'effective_hours': effective,
194 'task_hours' : task_hours
198 def button_cancel(self, cr, uid, ids, context=None):
199 obj_project_task = self.pool.get('project.task')
202 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
203 for backlog in self.browse(cr, uid, ids, context=context):
204 obj_project_task.write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'cancelled'})
207 def button_draft(self, cr, uid, ids, context=None):
210 self.write(cr, uid, ids, {'state':'draft'}, context=context)
213 def button_open(self, cr, uid, ids, context=None):
216 self.write(cr, uid, ids, {'state':'open'}, context=context)
217 for (id, name) in self.name_get(cr, uid, ids):
218 message = _('Product Backlog ') + " '" + name + "' "+ _("is Open.")
219 self.log(cr, uid, id, message)
222 def button_close(self, cr, uid, ids, context=None):
225 obj_project_task = self.pool.get('project.task')
226 self.write(cr, uid, ids, {'state':'done'}, context=context)
227 for backlog in self.browse(cr, uid, ids, context=context):
228 obj_project_task.write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'done'})
229 message = _('Product Backlog ') + " '" + backlog.name + "' "+ _("is Closed.")
230 self.log(cr, uid, backlog.id, message)
233 def button_pending(self, cr, uid, ids, context=None):
236 self.write(cr, uid, ids, {'state':'pending'}, context=context)
239 def button_postpone(self, cr, uid, ids, context=None):
242 for product in self.browse(cr, uid, ids, context=context):
244 for task in product.tasks_id:
245 if task.state != 'done':
246 tasks_id.append(task.id)
248 clone_id = self.copy(cr, uid, product.id, {
249 'name': 'PARTIAL:'+ product.name ,
251 'tasks_id':[(6, 0, tasks_id)],
253 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
257 'name' : fields.char('Feature', size=64, required=True),
258 'note' : fields.text('Note'),
259 'active' : fields.boolean('Active', help="If Active field is set to true, it will allow you to hide the product backlog without removing it."),
260 'project_id': fields.many2one('project.project', 'Project', required=True, domain=[('scrum','=',1)]),
261 'user_id': fields.many2one('res.users', 'Author'),
262 'sprint_id': fields.many2one('project.scrum.sprint', 'Sprint'),
263 'sequence' : fields.integer('Sequence', help="Gives the sequence order when displaying a list of product backlog."),
264 'tasks_id': fields.one2many('project.task', 'product_backlog_id', 'Tasks Details'),
265 'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('done','Done'),('cancel','Cancelled')], 'State', required=True),
266 'progress': fields.function(_compute, multi="progress", group_operator="avg", type='float', method=True, string='Progress', help="Computed as: Time Spent / Total Time."),
267 'effective_hours': fields.function(_compute, multi="effective_hours", method=True, string='Spent Hours', help="Computed using the sum of the time spent on every related tasks", store=True),
268 'expected_hours': fields.float('Planned Hours', help='Estimated total time to do the Backlog'),
269 'create_date': fields.datetime("Creation Date", readonly=True),
270 'task_hours': fields.function(_compute, multi="task_hours", method=True, string='Task Hours', help='Estimated time of the total hours of the tasks')
275 'user_id': lambda self, cr, uid, context: uid,
278 project_scrum_product_backlog()
280 class project_scrum_task(osv.osv):
281 _name = 'project.task'
282 _inherit = 'project.task'
284 def _get_task(self, cr, uid, ids, context=None):
288 for line in self.pool.get('project.scrum.product.backlog').browse(cr, uid, ids, context=context):
289 for task in line.tasks_id:
290 result[task.id] = True
293 'product_backlog_id': fields.many2one('project.scrum.product.backlog', 'Product Backlog',help="Related product backlog that contains this task. Used in SCRUM methodology"),
294 'sprint_id': fields.related('product_backlog_id','sprint_id', type='many2one', relation='project.scrum.sprint', string='Sprint',
296 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['product_backlog_id'], 10),
297 'project.scrum.product.backlog': (_get_task, ['sprint_id'], 10)
301 def onchange_backlog_id(self, cr, uid, backlog_id=False):
304 project_id = self.pool.get('project.scrum.product.backlog').browse(cr, uid, backlog_id).project_id.id
305 return {'value': {'project_id': project_id}}
308 class project_scrum_meeting(osv.osv):
309 _name = 'project.scrum.meeting'
310 _description = 'Scrum Meeting'
313 'name' : fields.char('Meeting Name', size=64),
314 'date': fields.date('Meeting Date', required=True),
315 'sprint_id': fields.many2one('project.scrum.sprint', 'Sprint', required=True),
316 'project_id': fields.many2one('project.project', 'Project'),
317 'question_yesterday': fields.text('Tasks since yesterday'),
318 'question_today': fields.text('Tasks for today'),
319 'question_blocks': fields.text('Blocks encountered'),
320 'question_backlog': fields.text('Backlog Accurate'),
321 'task_ids': fields.many2many('project.task', 'meeting_task_rel', 'metting_id', 'task_id', 'Tasks'),
322 'user_id': fields.related('sprint_id', 'scrum_master_id', type='many2one', relation='res.users', string='Responsible', readonly=True),
325 # TODO: Find the right sprint thanks to users and date
328 'date' : time.strftime('%Y-%m-%d'),
331 def button_send_to_master(self, cr, uid, ids, context=None):
334 meeting_id = self.browse(cr, uid, ids)[0]
335 if meeting_id and meeting_id.sprint_id.scrum_master_id.user_email:
336 res = self.email_send(cr, uid, ids, meeting_id.sprint_id.scrum_master_id.user_email)
338 raise osv.except_osv(_('Error !'), _(' Email Not send to the scrum master %s!' % meeting_id.sprint_id.scrum_master_id.name))
340 raise osv.except_osv(_('Error !'), _('Please provide email address for scrum master defined on sprint.'))
343 def button_send_product_owner(self, cr, uid, ids, context=None):
346 context.update({'button_send_product_owner': True})
347 meeting_id = self.browse(cr, uid, ids)[0]
348 if meeting_id.sprint_id.product_owner_id.user_email:
349 res = self.email_send(cr,uid,ids,meeting_id.sprint_id.product_owner_id.user_email)
351 raise osv.except_osv(_('Error !'), _(' Email Not send to the product owner %s!' % meeting_id.sprint_id.product_owner_id.name))
353 raise osv.except_osv(_('Error !'), _('Please provide email address for product owner defined on sprint.'))
356 def email_send(self, cr, uid, ids, email, context=None):
359 email_from = tools.config.get('email_from', False)
360 meeting_id = self.browse(cr,uid,ids)[0]
361 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
362 user_email = email_from or user.address_id.email or email_from
363 body = _('Hello ') + meeting_id.sprint_id.scrum_master_id.name + ",\n" + " \n" +_('I am sending you Daily Meeting Details of date')+ ' %s ' % (meeting_id.date)+ _('for the Sprint')+ ' %s\n' % (meeting_id.sprint_id.name)
364 body += "\n"+ _('*Tasks since yesterday:')+ '\n_______________________%s' % (meeting_id.question_yesterday) + '\n' +_("*Task for Today:")+ '\n_______________________ %s\n' % (meeting_id.question_today )+ '\n' +_('*Blocks encountered:') +'\n_______________________ %s' % (meeting_id.question_blocks or _('No Blocks'))
365 body += "\n\n"+_('Thank you')+",\n"+ user.name
366 sub_name = meeting_id.name or _('Scrum Meeting of')+ "%s" %meeting_id.date
367 flag = tools.email_send(user_email , [email], sub_name, body, reply_to=None, openobject_id=str(meeting_id.id))
372 project_scrum_meeting()