diff --git a/beesdoo_shift/__init__.py b/beesdoo_shift/__init__.py
new file mode 100644
index 0000000..8d752fb
--- /dev/null
+++ b/beesdoo_shift/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import models
+import wizard
diff --git a/beesdoo_shift/__openerp__.py b/beesdoo_shift/__openerp__.py
new file mode 100644
index 0000000..4a2a996
--- /dev/null
+++ b/beesdoo_shift/__openerp__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Beescoop Shift Management",
+
+ 'summary': """
+ Volonteer Timetable Management""",
+
+ 'description': """
+
+ """,
+
+ 'author': "Thibault Francois",
+ 'website': "https://github.com/beescoop/Obeesdoo",
+
+ 'category': 'Coop',
+ 'version': '0.1',
+
+ 'depends': ['beesdoo_base'],
+
+ 'data': [
+ "security/ir.model.access.csv",
+ "views/task_template.xml",
+ "views/task.xml",
+ "views/planning.xml",
+ "wizard/instanciate_planning.xml",
+ "wizard/batch_template.xml",
+ ],
+}
diff --git a/beesdoo_shift/models/__init__.py b/beesdoo_shift/models/__init__.py
new file mode 100644
index 0000000..14cfb0e
--- /dev/null
+++ b/beesdoo_shift/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import planning
+import task
\ No newline at end of file
diff --git a/beesdoo_shift/models/planning.py b/beesdoo_shift/models/planning.py
new file mode 100644
index 0000000..421c6bc
--- /dev/null
+++ b/beesdoo_shift/models/planning.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+
+from openerp import models, fields, api, _
+from openerp.exceptions import UserError
+
+from pytz import UTC
+import math
+from datetime import datetime, timedelta
+
+def float_to_time(f):
+ decimal, integer = math.modf(f)
+ return "%s:%s" % (str(int(integer)).zfill(2), str(int(round(decimal * 60))).zfill(2))
+
+def floatime_to_hour_minute(f):
+ decimal, integer = math.modf(f)
+ return int(integer), int(round(decimal * 60))
+
+def get_first_day_of_week():
+ today = datetime.now()
+ return datetime.now() - timedelta(days=today.weekday())
+
+class TaskType(models.Model):
+ _name = 'beesdoo.shift.type'
+
+ name = fields.Char()
+ description = fields.Text()
+ active = fields.Boolean(default=True)
+
+class DayNumber(models.Model):
+ _name = 'beesdoo.shift.daynumber'
+
+ _order = 'number asc'
+
+ name = fields.Char()
+ number = fields.Integer("Day Number", help="From 1 to N, When you will instanciate your planning, Day 1 will be the start date of the instance, Day 2 the second, etc...")
+ active = fields.Boolean(default=True)
+
+class Planning(models.Model):
+ _name = 'beesdoo.shift.planning'
+
+ name = fields.Char()
+ task_template_ids = fields.One2many('beesdoo.shift.template', 'planning_id')
+
+class TaskTemplate(models.Model):
+ _name = 'beesdoo.shift.template'
+
+ name = fields.Char(required=True)
+ planning_id = fields.Many2one('beesdoo.shift.planning', required=True)
+ day_nb_id = fields.Many2one('beesdoo.shift.daynumber', string='Day', required=True)
+ task_type_id = fields.Many2one('beesdoo.shift.type', string="Type")
+ start_time = fields.Float(required=True)
+ end_time = fields.Float(required=True)
+
+ duration = fields.Float(help="Duration in Hour")
+ worker_nb = fields.Integer(string="Number of worker", help="Max number of worker for this task", default=1)
+ worker_ids = fields.Many2many('res.partner', string="Recurrent worker assigned", domain=[('eater', '=', 'worker_eater')])
+ remaining_worker = fields.Integer(compute="_get_remaining", store=True, string="Remaining Place")
+ active = fields.Boolean(default=True)
+ #For Kanban View Only
+ color = fields.Integer('Color Index')
+ worker_name = fields.Char(compute="_get_worker_name")
+ #For calendar View
+ start_date = fields.Datetime(compute="_get_fake_date", search="_dummy_search")
+ end_date = fields.Datetime(compute="_get_fake_date", search="_dummy_search")
+
+ @api.depends('start_time', 'end_time')
+ def _get_fake_date(self):
+ today = datetime.strptime(self._context.get('visualize_date'), '%Y-%m-%d') if self._context.get('visualize_date') else get_first_day_of_week()
+ for rec in self:
+ day = today + timedelta(days=rec.day_nb_id.number - 1)
+ h_begin, m_begin = floatime_to_hour_minute(rec.start_time)
+ h_end, m_end = floatime_to_hour_minute(rec.end_time)
+ rec.start_date = fields.Datetime.context_timestamp(self, day).replace(hour=h_begin, minute=m_begin, second=0).astimezone(UTC)
+ rec.end_date = fields.Datetime.context_timestamp(self, day).replace(hour=h_end, minute=m_end, second=0).astimezone(UTC)
+
+ def _dummy_search(self, operator, value):
+ return []
+
+ @api.depends('worker_ids', 'worker_nb')
+ def _get_remaining(self):
+ for rec in self:
+ rec.remaining_worker = rec.worker_nb - len(rec.worker_ids)
+
+ @api.depends("worker_ids")
+ def _get_worker_name(self):
+ for rec in self:
+ rec.worker_name = ','.join(rec.worker_ids.mapped('display_name'))
+
+ @api.constrains('worker_nb', 'worker_ids')
+ def _nb_worker_max(self):
+ for rec in self:
+ if len(rec.worker_ids) > rec.worker_nb:
+ raise UserError(_('you cannot assign more worker then the number maximal define on the template'))
+
+
+ @api.onchange('start_time', 'end_time')
+ def _get_duration(self):
+ if self.start_time and self.end_time:
+ self.duration = self.end_time - self.start_time
+
+ @api.onchange('duration')
+ def _set_duration(self):
+ if self.start_time:
+ self.end_time = self.start_time +self.duration
+
+ def _generate_task_day(self):
+ tasks = self.env['beesdoo.shift.shift']
+ for rec in self:
+ for i in xrange(0, rec.worker_nb):
+ tasks |= tasks.create({
+ 'name' : "%s (%s) - (%s) [%s]" % (rec.name, float_to_time(rec.start_time), float_to_time(rec.end_time), i),
+ 'task_template_id' : rec.id,
+ 'task_type_id' : rec.task_type_id.id,
+ 'worker_id' : rec.worker_ids[i].id if len(rec.worker_ids) > i else False,
+ 'start_time' : rec.start_date,
+ 'end_time' : rec.end_date,
+ })
+ return tasks
\ No newline at end of file
diff --git a/beesdoo_shift/models/task.py b/beesdoo_shift/models/task.py
new file mode 100644
index 0000000..d603b7e
--- /dev/null
+++ b/beesdoo_shift/models/task.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+from openerp import models, fields
+
+STATES = [
+ ('draft', 'Unconfirmed'),
+ ('open', 'Confirmed'),
+ ('done', 'Attended'),
+ ('absent', 'Absent'),
+ ('excused', 'Excused'),
+ ('replaced', 'Replaced'),
+ ('cancel', 'Cancelled'),
+]
+
+class Task(models.Model):
+ _name = 'beesdoo.shift.shift'
+
+ #EX01 ADD inheritance
+ _inherit = ['mail.thread']
+
+ name = fields.Char(track_visibility='always')
+ task_template_id = fields.Many2one('beesdoo.shift.template')
+ planning_id = fields.Many2one(related='task_template_id.planning_id', store=True)
+ task_type_id = fields.Many2one('beesdoo.shift.type', string="Task Type")
+ worker_id = fields.Many2one('res.partner', track_visibility='onchange', domain=[('eater', '=', 'worker_eater')])
+ start_time = fields.Datetime(track_visibility='always')
+ end_time = fields.Datetime(track_visibility='always')
+ state = fields.Selection(STATES, default='draft', track_visibility='onchange')
+
+ def message_auto_subscribe(self, updated_fields, values=None):
+ self._add_follower(values)
+ return super(Task, self).message_auto_subscribe(updated_fields, values=values)
+
+ def _add_follower(self, vals):
+ if vals.get('worker_id'):
+ worker = self.env['res.partner'].browse(vals['worker_id'])
+ self.message_subscribe(partner_ids=worker.ids)
\ No newline at end of file
diff --git a/beesdoo_shift/security/ir.model.access.csv b/beesdoo_shift/security/ir.model.access.csv
new file mode 100644
index 0000000..a63e74b
--- /dev/null
+++ b/beesdoo_shift/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+access_coopplanning_task_type,access_coopplanning_task_type,model_beesdoo_shift_type,,1,1,1,1
+access_coopplanning_daynumber,access_coopplanning_daynumber,model_beesdoo_shift_daynumber,,1,1,1,1
+access_coopplanning_planning,access_coopplanning_planning,model_beesdoo_shift_planning,,1,1,1,1
+access_coopplanning_task_template,access_coopplanning_task_template,model_beesdoo_shift_template,,1,1,1,1
+access_coopplanning_task,access_coopplanning_task,model_beesdoo_shift_shift,,1,1,1,1
diff --git a/beesdoo_shift/views/planning.xml b/beesdoo_shift/views/planning.xml
new file mode 100644
index 0000000..1ed7edb
--- /dev/null
+++ b/beesdoo_shift/views/planning.xml
@@ -0,0 +1,80 @@
+
+
+ Planning List
+ beesdoo.shift.planning
+
+
+
+
+
+
+
+
+ Planning Action
+ beesdoo.shift.template
+ kanban,tree,form,calendar,pivot
+ {'group_by': 'day_nb_id',
+ 'search_default_planning_id': active_id,
+ 'default_planning_id': active_id}
+
+
+
+ Instanciate Planning Action
+ beesddoo.shift.generate_planning
+ form
+ new
+
+
+
+ Planning Form
+ beesdoo.shift.planning
+
+
+
+
+
+
+ Planning Tree
+ beesdoo.shift.planning
+
+
+
+
+
+
+
+
+
+
+ Planning Action
+ beesdoo.shift.planning
+ tree,form
+
+
+
+
diff --git a/beesdoo_shift/views/task.xml b/beesdoo_shift/views/task.xml
new file mode 100644
index 0000000..304b8c2
--- /dev/null
+++ b/beesdoo_shift/views/task.xml
@@ -0,0 +1,77 @@
+
+
+ Task Template List
+ beesdoo.shift.shift
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Task Template List
+ beesdoo.shift.shift
+
+
+
+
+
+
+
+
+
+ Task Template Form
+ beesdoo.shift.shift
+
+
+
+
+
+
+
+ Task Action
+ beesdoo.shift.shift
+ calendar,tree,form,pivot
+
+
+
+
+
+
+
diff --git a/beesdoo_shift/views/task_template.xml b/beesdoo_shift/views/task_template.xml
new file mode 100644
index 0000000..d9cf550
--- /dev/null
+++ b/beesdoo_shift/views/task_template.xml
@@ -0,0 +1,246 @@
+
+
+ Shift Template List
+ beesdoo.shift.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shift Template Search
+ beesdoo.shift.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Task Template Form
+ beesdoo.shift.template
+
+
+
+
+
+
+ Shift Template Calendar
+ beesdoo.shift.template
+
+
+
+
+
+
+
+
+
+ Task Template Kanban
+ beesdoo.shift.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Type:
+
+
+
+ Worker Number:
+
+
+
+
+ -
+
+
+
+
+
+
Recurring Workers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Day Number List
+ beesdoo.shift.daynumber
+
+
+
+
+
+
+
+
+
+
+ Shift Type List
+ beesdoo.shift.type
+
+
+
+
+
+
+
+
+
+
+ Generate Shift Template
+ beesddoo.shift.generate_shift_template
+ form
+ new
+
+
+
+ Shift Type Form
+ beesdoo.shift.type
+
+
+
+
+
+
+
+ Day Number
+ beesdoo.shift.daynumber
+ tree
+
+
+
+
+
+ Shift Type
+ beesdoo.shift.type
+ tree,form
+
+
+
+
diff --git a/beesdoo_shift/wizard/__init__.py b/beesdoo_shift/wizard/__init__.py
new file mode 100644
index 0000000..f405bb9
--- /dev/null
+++ b/beesdoo_shift/wizard/__init__.py
@@ -0,0 +1,2 @@
+import instanciate_planning
+import batch_template
\ No newline at end of file
diff --git a/beesdoo_shift/wizard/batch_template.py b/beesdoo_shift/wizard/batch_template.py
new file mode 100644
index 0000000..a81bb54
--- /dev/null
+++ b/beesdoo_shift/wizard/batch_template.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+'''
+Created on 2 janv. 2017
+
+@author: Thibault Francois
+'''
+
+from openerp import models, fields, api, _
+
+class GenerateShiftTemplate(models.TransientModel):
+ _name = 'beesddoo.shift.generate_shift_template'
+
+ day_ids = fields.Many2many('beesdoo.shift.daynumber', relation='template_gen_day_number_rel', column1='wizard_id', column2='day_id')
+ planning_id = fields.Many2one('beesdoo.shift.planning', required=True)
+ type_id = fields.Many2one('beesdoo.shift.type', default=lambda self: self._context.get('active_id'))
+ line_ids = fields.One2many('beesddoo.shift.generate_shift_template.line', 'wizard_id')
+
+ @api.multi
+ def generate(self):
+ self.ensure_one()
+ ids = []
+ for day in self.day_ids:
+ for line in self.line_ids:
+ shift_template_data = {
+ 'name': '%s' % self.type_id.name,
+ 'planning_id': self.planning_id.id,
+ 'task_type_id': self.type_id.id,
+ 'day_nb_id': day.id,
+ 'start_time': line.start_time,
+ 'end_time': line.end_time,
+ 'duration': line.end_time - line.start_time,
+ 'worker_nb': line.worker_nb,
+ }
+ new_rec = self.env['beesdoo.shift.template'].create(shift_template_data)
+ ids.append(new_rec.id)
+ return {
+ 'name': _('Generated Shift Template'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'kanban,tree,form',
+ 'res_model': 'beesdoo.shift.template',
+ 'target': 'current',
+ 'domain': [('id', 'in', ids)],
+ 'context': {'group_by': 'day_nb_id'},
+ }
+
+class GenerateShiftTemplateLine(models.TransientModel):
+ _name = 'beesddoo.shift.generate_shift_template.line'
+
+ wizard_id = fields.Many2one('beesddoo.shift.generate_shift_template')
+ start_time = fields.Float(required=True)
+ end_time = fields.Float(required=True)
+ worker_nb = fields.Integer(default=1)
\ No newline at end of file
diff --git a/beesdoo_shift/wizard/batch_template.xml b/beesdoo_shift/wizard/batch_template.xml
new file mode 100644
index 0000000..556e95e
--- /dev/null
+++ b/beesdoo_shift/wizard/batch_template.xml
@@ -0,0 +1,41 @@
+
+
+ Planning Form
+ beesddoo.shift.generate_shift_template
+
+
+
+
+
+
diff --git a/beesdoo_shift/wizard/instanciate_planning.py b/beesdoo_shift/wizard/instanciate_planning.py
new file mode 100644
index 0000000..dfd770d
--- /dev/null
+++ b/beesdoo_shift/wizard/instanciate_planning.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from openerp import models, fields, api
+
+
+class InstanciatePlanning(models.TransientModel):
+ _name = 'beesddoo.shift.generate_planning'
+
+ def _get_planning(self):
+ return self._context.get('active_id')
+
+ date_start = fields.Date("First Day of planning", required=True)
+ planning_id = fields.Many2one('beesdoo.shift.planning', readonly=True, default=_get_planning)
+
+ @api.multi
+ def generate_task(self):
+ self.ensure_one()
+ self = self.with_context(visualize_date=self.date_start)
+ self.planning_id.task_template_ids._generate_task_day()
diff --git a/beesdoo_shift/wizard/instanciate_planning.xml b/beesdoo_shift/wizard/instanciate_planning.xml
new file mode 100644
index 0000000..3d917c4
--- /dev/null
+++ b/beesdoo_shift/wizard/instanciate_planning.xml
@@ -0,0 +1,20 @@
+
+
+ Planning Form
+ beesddoo.shift.generate_planning
+
+
+
+
+
+