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 ##############################################################################
22 from mx import DateTime
25 from osv import fields, osv, orm
28 from tools.translate import _
30 class scrum_project(osv.osv):
31 _inherit = 'project.project'
33 'product_owner_id': fields.many2one('res.users', 'Product Owner', help="The person who is responsible for the product"),
34 'sprint_size': fields.integer('Sprint Days', help="Number of days allocated for sprint"),
35 'scrum': fields.integer('Is a Scrum Project'),
38 'product_owner_id': lambda self,cr,uid,context={}: uid,
44 class scrum_sprint(osv.osv):
45 _name = 'scrum.sprint'
46 _description = 'Scrum Sprint'
48 def _calc_progress(self, cr, uid, ids, name, args, context):
50 for sprint in self.browse(cr, uid, ids):
53 for bl in sprint.backlog_ids:
54 tot += bl.expected_hours
55 prog += bl.expected_hours * bl.progress / 100.0
56 res.setdefault(sprint.id, 0.0)
58 res[sprint.id] = round(prog/tot*100)
61 def _calc_effective(self, cr, uid, ids, name, args, context):
63 for sprint in self.browse(cr, uid, ids):
64 res.setdefault(sprint.id, 0.0)
65 for bl in sprint.backlog_ids:
66 res[sprint.id] += bl.effective_hours
69 def _calc_planned(self, cr, uid, ids, name, args, context):
71 for sprint in self.browse(cr, uid, ids):
72 res.setdefault(sprint.id, 0.0)
73 for bl in sprint.backlog_ids:
74 res[sprint.id] += bl.expected_hours
77 def _calc_expected(self, cr, uid, ids, name, args, context):
79 for sprint in self.browse(cr, uid, ids):
80 res.setdefault(sprint.id, 0.0)
81 for bl in sprint.backlog_ids:
82 res[sprint.id] += bl.expected_hours
85 def button_cancel(self, cr, uid, ids, context={}):
86 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
89 def button_draft(self, cr, uid, ids, context={}):
90 self.write(cr, uid, ids, {'state':'draft'}, context=context)
93 def button_open(self, cr, uid, ids, context={}):
94 self.write(cr, uid, ids, {'state':'open'}, context=context)
97 def button_close(self, cr, uid, ids, context={}):
98 self.write(cr, uid, ids, {'state':'done'}, context=context)
101 def button_pending(self, cr, uid, ids, context={}):
102 self.write(cr, uid, ids, {'state':'pending'}, context=context)
106 'name' : fields.char('Sprint Name', required=True, size=64),
107 'date_start': fields.date('Starting Date', required=True),
108 'date_stop': fields.date('Ending Date', required=True),
109 '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."),
110 'product_owner_id': fields.many2one('res.users', 'Product Owner', required=True,help="The person who is responsible for the product"),
111 'scrum_master_id': fields.many2one('res.users', 'Scrum Manager', required=True,help="The person who is maintains the processes for the product"),
112 'meeting_ids': fields.one2many('scrum.meeting', 'sprint_id', 'Daily Scrum'),
113 'review': fields.text('Sprint Review'),
114 'retrospective': fields.text('Sprint Retrospective'),
115 'backlog_ids': fields.one2many('scrum.product.backlog', 'sprint_id', 'Sprint Backlog'),
116 'progress': fields.function(_calc_progress, method=True, string='Progress (0-100)', help="Computed as: Time Spent / Total Time."),
117 'effective_hours': fields.function(_calc_effective, method=True, string='Effective hours', help="Computed using the sum of the task work done."),
118 'expected_hours': fields.function(_calc_expected, method=True, string='Planned Hours', help='Estimated time to do the task.'),
119 'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('cancel','Cancelled'),('done','Done')], 'State', required=True),
123 'date_start' : time.strftime('%Y-%m-%d'),
126 def copy(self, cr, uid, id, default=None, context=None):
127 """Overrides orm copy method
128 @param self: The object pointer
129 @param cr: the current row, from the database cursor,
130 @param uid: the current user’s ID for security checks,
131 @param ids: List of case’s IDs
132 @param context: A standard dictionary for contextual values
138 default.update({'backlog_ids': [], 'meeting_ids': []})
139 return super(scrum_sprint, self).copy(cr, uid, id, default=default, context=context)
141 def onchange_project_id(self, cr, uid, ids, project_id):
144 proj = self.pool.get('project.project').browse(cr, uid, [project_id])[0]
145 v['product_owner_id']= proj.product_owner_id and proj.product_owner_id.id or False
146 v['scrum_master_id']= proj.user_id and proj.user_id.id or False
147 v['date_stop'] = (DateTime.now() + DateTime.RelativeDateTime(days=int(proj.sprint_size or 14))).strftime('%Y-%m-%d')
152 class scrum_product_backlog(osv.osv):
153 _name = 'scrum.product.backlog'
154 _description = 'Product Backlog'
156 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
161 match = re.match('^S\(([0-9]+)\)$', name)
163 ids = self.search(cr, uid, [('sprint_id','=', int(match.group(1)))], limit=limit, context=context)
164 return self.name_get(cr, uid, ids, context=context)
165 return super(scrum_product_backlog, self).name_search(cr, uid, name, args, operator,context, limit=limit)
167 def _calc_progress(self, cr, uid, ids, name, args, context):
169 for bl in self.browse(cr, uid, ids):
172 for task in bl.tasks_id:
173 tot += task.planned_hours
174 prog += task.planned_hours * task.progress / 100.0
175 res.setdefault(bl.id, 0.0)
177 res[bl.id] = round(prog/tot*100)
180 def _calc_effective(self, cr, uid, ids, name, args, context):
182 for bl in self.browse(cr, uid, ids):
183 res.setdefault(bl.id, 0.0)
184 for task in bl.tasks_id:
185 res[bl.id] += task.effective_hours
188 def _calc_task(self, cr, uid, ids, name, args, context):
190 for bl in self.browse(cr, uid, ids):
191 res.setdefault(bl.id, 0.0)
192 for task in bl.tasks_id:
193 res[bl.id] += task.total_hours
196 def button_cancel(self, cr, uid, ids, context={}):
197 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
198 for backlog in self.browse(cr, uid, ids, context=context):
199 self.pool.get('project.task').write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'cancelled'})
202 def button_draft(self, cr, uid, ids, context={}):
203 self.write(cr, uid, ids, {'state':'draft'}, context=context)
206 def button_open(self, cr, uid, ids, context={}):
207 self.write(cr, uid, ids, {'state':'open'}, context=context)
210 def button_close(self, cr, uid, ids, context={}):
211 self.write(cr, uid, ids, {'state':'done'}, context=context)
212 for backlog in self.browse(cr, uid, ids, context=context):
213 self.pool.get('project.task').write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'done'})
216 def button_pending(self, cr, uid, ids, context={}):
217 self.write(cr, uid, ids, {'state':'pending'}, context=context)
220 def button_postpone(self, cr, uid, ids, context=None):
221 for product in self.browse(cr, uid, ids, context=context):
223 for task in product.tasks_id:
224 if task.state != 'done':
225 tasks_id.append(task.id)
227 clone_id = self.copy(cr, uid, product.id, {
228 'name': 'PARTIAL:'+ product.name ,
230 'tasks_id':[(6, 0, tasks_id)],
232 self.write(cr, uid, ids, {'state':'cancel'}, context=context)
236 'name' : fields.char('Feature', size=64, required=True),
237 'note' : fields.text('Note'),
238 'active' : fields.boolean('Active', help="If Active field is set to true, it will allow you to hide the product backlog without removing it."),
239 '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."),
240 'user_id': fields.many2one('res.users', 'Responsible'),
241 'sprint_id': fields.many2one('scrum.sprint', 'Sprint'),
242 'sequence' : fields.integer('Sequence', help="Gives the sequence order when displaying a list of product backlog."),
243 'tasks_id': fields.one2many('project.task', 'product_backlog_id', 'Tasks Details'),
244 'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('done','Done'),('cancel','Cancelled')], 'State', required=True),
245 'progress': fields.function(_calc_progress, method=True, string='Progress', help="Computed as: Time Spent / Total Time."),
246 'effective_hours': fields.function(_calc_effective, method=True, string='Effective hours', help="Computed using the sum of the task work done (Time spent on tasks)"),
247 'expected_hours': fields.float('Planned Hours', help='Estimated total time to do the Backlog'),
248 'create_date': fields.datetime("Creation Date", readonly=True),
249 'task_hours': fields.function(_calc_task, method=True, string='Task Hours', help='Estimated time of the total hours of the tasks')
254 'user_id': lambda self, cr, uid, context: uid,
257 scrum_product_backlog()
259 class scrum_task(osv.osv):
260 _name = 'project.task'
261 _inherit = 'project.task'
263 def _get_task(self, cr, uid, ids, context={}):
265 for line in self.pool.get('scrum.product.backlog').browse(cr, uid, ids, context=context):
266 for task in line.tasks_id:
267 result[task.id] = True
270 'product_backlog_id': fields.many2one('scrum.product.backlog', 'Product Backlog',help="Related product backlog that contains this task. Used in SCRUM methodology"),
271 'sprint_id': fields.related('product_backlog_id','sprint_id', type='many2one', relation='scrum.sprint', string='Sprint',
273 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['product_backlog_id'], 10),
274 'scrum.product.backlog': (_get_task, ['sprint_id'], 10)
278 def onchange_backlog_id(self, cr, uid, backlog_id):
281 project_id = self.pool.get('scrum.product.backlog').browse(cr, uid, backlog_id).project_id.id
282 return {'value': {'project_id': project_id}}
285 class scrum_meeting(osv.osv):
286 _name = 'scrum.meeting'
287 _description = 'Scrum Meeting'
290 'name' : fields.char('Meeting Name', size=64),
291 'date': fields.date('Meeting Date', required=True),
292 'sprint_id': fields.many2one('scrum.sprint', 'Sprint', required=True),
293 'project_id': fields.many2one('project.project', 'Project'),
294 'question_yesterday': fields.text('Tasks since yesterday'),
295 'question_today': fields.text('Tasks for today'),
296 'question_blocks': fields.text('Blocks encountered'),
297 'question_backlog': fields.text('Backlog Accurate'),
298 'task_ids': fields.many2many('project.task', 'meeting_task_rel', 'metting_id', 'task_id', 'Tasks')
301 # TODO: Find the right sprint thanks to users and date
304 'date' : time.strftime('%Y-%m-%d'),
307 def button_send_to_master(self, cr, uid, ids, context=None):
308 meeting_id = self.browse(cr, uid, ids)[0]
309 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
310 if meeting_id and meeting_id.sprint_id.scrum_master_id.user_email:
311 self.email_send(cr, uid, ids, email)
313 raise osv.except_osv(_('Error !'), _('Please provide email address for scrum master defined on sprint.'))
316 def button_send_product_owner(self, cr, uid, ids, context=None):
317 meeting_id = self.browse(cr, uid, ids)[0]
318 if meeting_id.sprint_id.product_owner_id.user_email:
319 self.email_send(cr,uid,ids,email)
321 raise osv.except_osv(_('Error !'), _('Please provide email address for product owner defined on sprint.'))
324 def email_send(self, cr, uid, ids, email, context=None):
325 email_from = tools.config.get('email_from', False)
326 meeting_id = self.browse(cr,uid,ids)[0]
327 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
328 user_email = email_from or user.address_id.email or email_from
329 body = "Hello " + meeting_id.sprint_id.scrum_master_id.name+",\n" +" \nI am sending you Daily Meeting Details of date %s for the Sprint %s \n" % (meeting_id.date, meeting_id.sprint_id.name)
330 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')
331 body += "\n\nThank you,\n"+ user.name
332 sub_name = meeting_id.name or 'Scrum Meeting of %s '%meeting_id.date
333 flag = tools.email_send(user_email , [email], sub_name, body, reply_to=None, openobject_id=str(meeting_id.id))
335 raise osv.except_osv(_('Error !'), _(' Email Not send to %s!' % meeting_id.sprint_id.product_owner_id.name))