← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~codekaki/openerp-hr/7.0-hr_roster into lp:openerp-hr

 

Chang Phui-Hock has proposed merging lp:~codekaki/openerp-hr/7.0-hr_roster into lp:openerp-hr.

Requested reviews:
  HR Core Editors (hr-core-editors)

For more details, see:
https://code.launchpad.net/~codekaki/openerp-hr/7.0-hr_roster/+merge/217116

Add duty roster module. It is useful when used in conjunction with other modules such as hr_holidays module; when a leave application spans multiple days, and some of the days in between are off-days (eg. from date 1 to date 5, date 4 is off day). 

The data captured by this module can be used to calculate more accurately the actual number of days off, and remaining leaves after that.

Some screenshots are uploaded here: http://imgur.com/a/mo8jR#0
I have also made it available on apps.openerp.com (Duty Roster)
-- 
https://code.launchpad.net/~codekaki/openerp-hr/7.0-hr_roster/+merge/217116
Your team HR Core Editors is requested to review the proposed merge of lp:~codekaki/openerp-hr/7.0-hr_roster into lp:openerp-hr.
=== added directory 'hr_roster'
=== added file 'hr_roster/__init__.py'
--- hr_roster/__init__.py	1970-01-01 00:00:00 +0000
+++ hr_roster/__init__.py	2014-04-24 17:59:04 +0000
@@ -0,0 +1,1 @@
+import hr_roster

=== added file 'hr_roster/__openerp__.py'
--- hr_roster/__openerp__.py	1970-01-01 00:00:00 +0000
+++ hr_roster/__openerp__.py	2014-04-24 17:59:04 +0000
@@ -0,0 +1,40 @@
+{
+    'name': 'Duty Roster',
+    'version': '0.1',
+    'category': 'Human Resources',
+    'description': """
+Duty Roster
+===========
+
+This is a generic module to allow easy management of staff duty roster. It should be used
+together with hr_holidays such that leaves application that spans multiple days
+takes into account of half/off duty days.
+""",
+    'author' : "CODEKAKI SYSTEMS (R49045/14)",
+    'website': 'http://codekaki.com',
+    'depends': ['hr'],
+    'images': [
+        'images/shift_code_tree.png',
+        'images/shift_code.png',
+        'images/duty_roster.png',
+        'images/duty_roster_err.png',
+    ],
+    'data': [
+        'duty_roster_workflow.xml',
+        'duty_roster_view.xml',
+    ],
+    'css': [
+        'static/src/css/hr_roster.css',
+    ],
+    'js': [
+        'static/src/js/hr_roster.js',
+    ],
+    'qweb': [
+        'static/src/xml/hr_shift_code.xml',
+        'static/src/xml/hr_roster.xml', 
+    ],
+    'demo': [],
+    'test':[],
+    'installable': True,
+    'images': [],
+}

=== added file 'hr_roster/duty_roster_view.xml'
--- hr_roster/duty_roster_view.xml	1970-01-01 00:00:00 +0000
+++ hr_roster/duty_roster_view.xml	2014-04-24 17:59:04 +0000
@@ -0,0 +1,134 @@
+<?xml version="1.0"?>
+<openerp>
+    <data>
+        <record id="view_duty_roster" model="ir.ui.view">
+            <field name="model">hr.duty_roster</field>
+            <field name="type">form</field>
+            <field name="arch" type="xml">
+                <form string="Duty Roster" version="7.0">
+                    <header>
+                        <button name="action_confirm" string="Submit for Checking" attrs="{'invisible': [('state', '!=', 'pending')]}"></button>
+                        <field name="state" widget="statusbar" statusbar_visible="draft,pending,checked"/>
+                    </header>
+                    <group col="6">
+                        <group colspan="2">
+                            <field name="department_id" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
+                            <field name="name" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
+                        </group>
+                        <group colspan="2">
+                            <field name="month" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
+                            <field name="year"  attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
+                        </group>
+                        <widget type="datepicker"></widget>
+                    </group>
+                    <notebook>
+                        <page string="Shifts">
+                            <field name="shifts" attrs="{'readonly': [('state', 'in', ['new', 'checked'])]}" context="{'active_id': active_id}">
+                                <tree string="Shifts" editable="bottom" class="shift_day_list">
+                                    <field name="employee_id" required="1" domain="[('department_id', '=', parent.department_id)]" widget="employees_filter"></field>
+                                    <field name="days" invisible="1" readonly="1"></field>
+                                    <field name="day_1" widget="shift_day" string="1" attrs="{'required': [('days', '>=', 1)]}"></field>
+                                    <field name="day_2" widget="shift_day" string="2" attrs="{'required': [('days', '>=', 2)]}"></field>
+                                    <field name="day_3" widget="shift_day" string="3" attrs="{'required': [('days', '>=', 3)]}"></field>
+                                    <field name="day_4" widget="shift_day" string="4" attrs="{'required': [('days', '>=', 4)]}"></field>
+                                    <field name="day_5" widget="shift_day" string="5" attrs="{'required': [('days', '>=', 5)]}"></field>
+                                    <field name="day_6" widget="shift_day" string="6" attrs="{'required': [('days', '>=', 6)]}"></field>
+                                    <field name="day_7" widget="shift_day" string="7" attrs="{'required': [('days', '>=', 6)]}"></field>
+                                    <field name="day_8" widget="shift_day" string="8" attrs="{'required': [('days', '>=', 8)]}"></field>
+                                    <field name="day_9" widget="shift_day" string="9" attrs="{'required': [('days', '>=', 9)]}"></field>
+                                    <field name="day_10" widget="shift_day" string="10" attrs="{'required': [('days', '>=', 10)]}"></field>
+                                    <field name="day_11" widget="shift_day" string="11" attrs="{'required': [('days', '>=', 11)]}"></field>
+                                    <field name="day_12" widget="shift_day" string="12" attrs="{'required': [('days', '>=', 12)]}"></field>
+                                    <field name="day_13" widget="shift_day" string="13" attrs="{'required': [('days', '>=', 13)]}"></field>
+                                    <field name="day_14" widget="shift_day" string="14" attrs="{'required': [('days', '>=', 14)]}"></field>
+                                    <field name="day_15" widget="shift_day" string="15" attrs="{'required': [('days', '>=', 15)]}"></field>
+                                    <field name="day_16" widget="shift_day" string="16" attrs="{'required': [('days', '>=', 16)]}"></field>
+                                    <field name="day_17" widget="shift_day" string="17" attrs="{'required': [('days', '>=', 17)]}"></field>
+                                    <field name="day_18" widget="shift_day" string="18" attrs="{'required': [('days', '>=', 18)]}"></field>
+                                    <field name="day_19" widget="shift_day" string="19" attrs="{'required': [('days', '>=', 19)]}"></field>
+                                    <field name="day_20" widget="shift_day" string="20" attrs="{'required': [('days', '>=', 20)]}"></field>
+                                    <field name="day_21" widget="shift_day" string="21" attrs="{'required': [('days', '>=', 21)]}"></field>
+                                    <field name="day_22" widget="shift_day" string="22" attrs="{'required': [('days', '>=', 22)]}"></field>
+                                    <field name="day_23" widget="shift_day" string="23" attrs="{'required': [('days', '>=', 23)]}"></field>
+                                    <field name="day_24" widget="shift_day" string="24" attrs="{'required': [('days', '>=', 24)]}"></field>
+                                    <field name="day_25" widget="shift_day" string="25" attrs="{'required': [('days', '>=', 25)]}"></field>
+                                    <field name="day_26" widget="shift_day" string="26" attrs="{'required': [('days', '>=', 26)]}"></field>
+                                    <field name="day_27" widget="shift_day" string="27" attrs="{'required': [('days', '>=', 27)]}"></field>
+                                    <field name="day_28" widget="shift_day" string="28" attrs="{'required': [('days', '>=', 28)]}"></field>
+                                    <field name="day_29" widget="shift_day" string="29" attrs="{'readonly': [('days', '&lt;', 29)], 'required': [('days', '>=', 29)]}"></field>
+                                    <field name="day_30" widget="shift_day" string="30" attrs="{'readonly': [('days', '&lt;', 30)], 'required': [('days', '>=', 30)]}"></field>
+                                    <field name="day_31" widget="shift_day" string="31" attrs="{'readonly': [('days', '&lt;', 31)], 'required': [('days', '>=', 31)]}"></field>
+                                </tree>
+                            </field>
+                        </page>
+                        <page string="Remarks">
+                            <field name="remarks" placeholder="Remarks..."></field>
+                        </page>
+                    </notebook>
+                    <group col="4">
+                        <widget type="shiftcodehelp" colspan="2"></widget>
+                        <field name="created_by_id" readonly="1"></field>
+                        <field name="checked_by_id" readonly="1" attrs="{'invisible': [('state', '!=', 'checked')]}"></field>
+                    </group>
+                </form>
+            </field>
+        </record>
+
+        <record id="view_hr_shift_code_tree" model="ir.ui.view">
+            <field name="model">hr.shift_code</field>
+            <field name="type">tree</field>
+            <field name="arch" type="xml">
+                <tree string="Shift Codes" version="7.0">
+                    <field name="code"></field>
+                    <field name="time_in"></field>
+                    <field name="time_out"></field>
+                    <field name="duration" widget="duration_mins"></field>
+                    <field name="break"></field>
+                    <field name="description"></field>
+                </tree>
+            </field>
+        </record>
+
+        <record id="view_hr_shift_code" model="ir.ui.view">
+            <field name="model">hr.shift_code</field>
+            <field name="type">form</field>
+            <field name="arch" type="xml">
+                <form string="Shift Code" version="7.0">
+                    <sheet>
+                        <group col="4">
+                            <group colspan="2">
+                                <field name="code"></field>
+                                <field name="description" widget="text"></field>
+                            </group>
+                            <group colspan="2">
+                                <field name="time_in" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
+                                <field name="time_out" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
+                                <field name="duration" readonly="1" widget="duration_mins"></field>
+                                <field name="break" widget="timepicker"></field>
+                            </group>
+                        </group>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+
+        <record model="ir.actions.act_window" id="action_hr_shift_code">
+            <field name="name">Shift Code</field>
+            <field name="res_model">hr.shift_code</field>
+            <field name="view_type">form</field>
+            <field name="view_mode">tree,form</field>
+        </record>
+
+        <record model="ir.actions.act_window" id="action_duty_roster">
+            <field name="name">Duty Roster</field>
+            <field name="res_model">hr.duty_roster</field>
+            <field name="view_type">form</field>
+            <field name="view_mode">tree,form</field>
+        </record>
+
+        <menuitem id="menu_duty_roster_root" name="Duty Roster" action="" parent="hr.menu_hr_root" groups="base.group_user"></menuitem>
+        <menuitem id="menu_shift_code" name="Shift Code" action="action_hr_shift_code" parent="menu_duty_roster_root" groups="base.group_user"></menuitem>
+        <menuitem id="menu_duty_roster" name="Duty Roster" action="action_duty_roster" parent="menu_duty_roster_root" groups="base.group_user"></menuitem>
+    </data>
+</openerp>

=== added file 'hr_roster/duty_roster_workflow.xml'
--- hr_roster/duty_roster_workflow.xml	1970-01-01 00:00:00 +0000
+++ hr_roster/duty_roster_workflow.xml	2014-04-24 17:59:04 +0000
@@ -0,0 +1,54 @@
+<openerp>
+    <data>
+        <record id="wkf_duty_roster" model="workflow">
+            <field name="name">hr.duty_roster.basic</field>
+            <field name="osv">hr.duty_roster</field>
+            <field name="on_create">True</field>
+        </record>
+
+        <!-- activities -->
+        <record id="act_new" model="workflow.activity">
+            <field name="wkf_id" ref="wkf_duty_roster"/>
+            <field name="flow_start">True</field>
+            <field name="name">new</field>
+        </record>
+        <record id="act_draft" model="workflow.activity">
+            <field name="wkf_id" ref="wkf_duty_roster"/>
+            <field name="name">draft</field>
+            <field name="kind">function</field>
+            <field name="action">action_draft()</field>
+        </record>
+        <record id="act_pending" model="workflow.activity">
+            <field name="wkf_id" ref="wkf_duty_roster"/>
+            <field name="name">pending</field>
+            <field name="kind">function</field>
+            <field name="action">action_pending()</field>
+        </record>
+        <record id="act_checked" model="workflow.activity">
+            <field name="wkf_id" ref="wkf_duty_roster"/>
+            <field name="flow_end">True</field>
+            <field name="name">checked</field>
+            <field name="kind">function</field>
+            <field name="action">action_checked()</field>
+        </record>
+
+        <!-- transitions -->
+        <record id="trans_new_draft" model="workflow.transition">
+            <field name="act_from" ref="act_new"/>
+            <field name="act_to" ref="act_draft"/>
+            <field name="condition">True</field>
+        </record>
+        <record id="trans_draft_pending" model="workflow.transition">
+            <field name="act_from" ref="act_draft"/>
+            <field name="act_to" ref="act_pending"/>
+            <field name="condition">has_shifts()</field>
+            <field name="signal">confirm</field>
+        </record>
+        <record id="trans_pending_checked" model="workflow.transition">
+            <field name="act_from" ref="act_pending"/>
+            <field name="act_to" ref="act_checked"/>
+            <field name="signal">checked</field>
+        </record>
+    </data>
+</openerp>
+

=== added file 'hr_roster/hr_roster.py'
--- hr_roster/hr_roster.py	1970-01-01 00:00:00 +0000
+++ hr_roster/hr_roster.py	2014-04-24 17:59:04 +0000
@@ -0,0 +1,183 @@
+from openerp.osv import fields, osv
+from datetime import datetime
+import calendar
+import logging
+
+_logger = logging.getLogger(__name__)
+
+MONTHS = zip(range(1, 13), calendar.month_name[1:]) 
+
+def monthrange(year=None, month=None):
+    today = datetime.today()
+    y = year or today.year
+    m = month or today.month
+    return y, m, calendar.monthrange(y, m)[1]
+
+
+class hr_duty_roster_shift(osv.Model):
+    _name = 'hr.duty_roster_shift'
+
+    def _get_days_default(self, cr, uid, context=None):
+        if 'active_id' in context:
+            active_id = context['active_id']
+            row = self.pool.get('hr.duty_roster').read(cr, uid, active_id, ['year', 'month'])
+            mrange = calendar.monthrange(row['year'], row['month'])
+            return mrange[1]
+        return 0
+
+    def _get_days(self, cr, uid, ids, field_name, args=None, context=None):
+        res = {}
+        if ids:
+            parent_row = self.read(cr, uid, ids[0], ['duty_roster_id'], context=context)
+            row = self.pool.get('hr.duty_roster').read(cr, uid, parent_row['duty_roster_id'][0], ['year', 'month'], context=context)
+            mrange = calendar.monthrange(row['year'], row['month'])
+            for _id in ids:
+                res[_id] = mrange[1]
+        return res
+
+    def _get_shift_codes(self, cr, uid, context=None):
+        context = context or {}
+        if 'shift_codes' not in context:
+            model = self.pool.get('hr.shift_code')
+            ids = model.search(cr, uid, [], order="code", context=context)
+            ret = model.read(cr, uid, ids, ['code'], context=context)
+            context['shift_codes'] = [(r['code'], r['code']) for r in ret]
+        return context['shift_codes']
+
+    _columns = {
+        'duty_roster_id': fields.many2one('hr.duty_roster', 'Duty Roster', required=True, ondelete="cascade"),
+        'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
+        'days': fields.function(_get_days, type="integer", string='Days', required=True),
+        'day_1': fields.selection(_get_shift_codes, string='Day 1', size=1),
+        'day_2': fields.selection(_get_shift_codes, string='Day 2', size=1),
+        'day_3': fields.selection(_get_shift_codes, string='Day 3', size=1),
+        'day_4': fields.selection(_get_shift_codes, string='Day 4', size=1),
+        'day_5': fields.selection(_get_shift_codes, string='Day 5', size=1),
+        'day_6': fields.selection(_get_shift_codes, string='Day 6', size=1),
+        'day_7': fields.selection(_get_shift_codes, string='Day 7', size=1),
+        'day_8': fields.selection(_get_shift_codes, string='Day 8', size=1),
+        'day_9': fields.selection(_get_shift_codes, string='Day 9', size=1),
+        'day_10': fields.selection(_get_shift_codes, string='Day 10', size=1),
+        'day_11': fields.selection(_get_shift_codes, string='Day 1', size=1),
+        'day_12': fields.selection(_get_shift_codes, string='Day 12', size=1),
+        'day_13': fields.selection(_get_shift_codes, string='Day 13', size=1),
+        'day_14': fields.selection(_get_shift_codes, string='Day 14', size=1),
+        'day_15': fields.selection(_get_shift_codes, string='Day 15', size=1),
+        'day_16': fields.selection(_get_shift_codes, string='Day 16', size=1),
+        'day_17': fields.selection(_get_shift_codes, string='Day 17', size=1),
+        'day_18': fields.selection(_get_shift_codes, string='Day 18', size=1),
+        'day_19': fields.selection(_get_shift_codes, string='Day 19', size=1),
+        'day_20': fields.selection(_get_shift_codes, string='Day 20', size=1),
+        'day_21': fields.selection(_get_shift_codes, string='Day 21', size=1),
+        'day_22': fields.selection(_get_shift_codes, string='Day 22', size=1),
+        'day_23': fields.selection(_get_shift_codes, string='Day 23', size=1),
+        'day_24': fields.selection(_get_shift_codes, string='Day 24', size=1),
+        'day_25': fields.selection(_get_shift_codes, string='Day 25', size=1),
+        'day_26': fields.selection(_get_shift_codes, string='Day 26', size=1),
+        'day_27': fields.selection(_get_shift_codes, string='Day 27', size=1),
+        'day_28': fields.selection(_get_shift_codes, string='Day 28', size=1),
+        'day_29': fields.selection(_get_shift_codes, string='Day 29', size=1),
+        'day_30': fields.selection(_get_shift_codes, string='Day 30', size=1),
+        'day_31': fields.selection(_get_shift_codes, string='Day 31', size=1),
+    }
+
+    _defaults = {
+        'days': _get_days_default,
+    }
+
+    _sql_constraints = [('shift_uniq', 'unique(duty_roster_id, employee_id)', 'Duplicate employee entries detected for this duty roster.')]
+
+
+class hr_duty_roster(osv.Model):
+    _name = 'hr.duty_roster'
+
+    def _get_date(self, cr, uid, ids, name, args, context=None):
+        results = {}
+        for row in self.read(cr, uid, ids, ['year', 'month'], context=context):
+            row_id = row['id']
+            results[row_id] = datetime(year=result['year'], month=result['month'], day=1).isoformat()
+        return results
+
+    _columns = {
+        'name': fields.char('Name', size=60, required=True),
+        'month': fields.selection(MONTHS, 'Month', required=True),
+        'year': fields.integer('Year', required=True),
+        'date': fields.function(_get_date, type="datetime", method=True),
+        'state': fields.selection([('new', 'New'), ('draft', 'Draft'), ('pending', 'Pending'), ('checked', 'Checked')]),
+        'shifts': fields.one2many('hr.duty_roster_shift', 'duty_roster_id', string="Shifts"),
+        'department_id': fields.many2one('hr.department', 'Department', required=True),
+        'created_by_id': fields.many2one('hr.employee', 'Prepared by', required=True),
+        'checked_by_id': fields.many2one('hr.employee', 'Checked by'),
+        'checked_dt': fields.datetime('Checked at'),
+        'remarks': fields.text('Remarks')
+    }
+
+    _defaults = {
+        'created_by_id': lambda self, cr, uid, context: uid,
+        'state': lambda self, cr, uid, context: 'new',
+        'month': lambda self, cr, uid, context: monthrange()[1],
+        'year':  lambda self, cr, uid, context: monthrange()[0],
+    }
+
+    def has_shifts(self, cr, uid, ids, context=None):
+        return self.pool.get('hr.duty_roster_shift').search(cr, uid, [('duty_roster_id', 'in', ids)], context=context, count=True)
+
+    def is_checked(self, cr, uid, ids, context=None):
+        return False
+
+    def action_draft(self, cr, uid, ids, context=None):
+        self.write(cr, uid, ids, {'state': 'draft'}, context)
+
+    def action_pending(self, cr, uid, ids, context=None):
+        self.write(cr, uid, ids, {'state': 'pending'}, context)
+
+    def action_checked(self, cr, uid, ids, context=None):
+        self.write(cr, uid, ids, {'state': 'checked'}, context)
+
+    _sql_constraints = [('uniq_duty_roster', 'unique(department_id, name, year, month)', 'Duty roster for the same department, month, year and name has already been created.')]
+
+
+class hr_shift_code(osv.Model):
+    _rec_name = 'code'
+    _name = 'hr.shift_code'
+
+    def _time_diff(self, time_in, time_out):
+        """Returns the number of minutes if both time in and time out are given."""
+        
+        duration = 0
+        if time_out and time_in:
+            time_in, time_out = [datetime.strptime(x, "%H:%M:%S") for x in (time_in, time_out)]
+            diff = time_out - time_in
+            duration = (time_out - time_in).seconds / 60
+        return int(duration)
+
+    def _get_duration(self, cr, uid, ids, name, args, context=None):
+        results = {}
+        for row in self.read(cr, uid, ids, ['time_in', 'time_out']):
+            row_id = row['id']
+            time_in, time_out = row['time_in'], row['time_out']
+            if time_out and time_in:
+                results[row_id] = self._time_diff(time_in, time_out)
+        return results
+
+    def onchange_time(self, cr, uid, ids, time_in, time_out, context=None):
+        ret = {}
+        ret['value'] = {
+            'duration': self._time_diff(time_in, time_out)
+        }
+        return ret
+
+    _columns = {
+        'code': fields.char('Code', size=1, required=True),
+        'description': fields.char('Description', size=30),
+        'time_in': fields.char('Time In', size=8),
+        'time_out': fields.char('Time Out', size=8),
+        'break': fields.char('Break', size=8),
+        'duration': fields.function(_get_duration, string="Duration", type="integer", method=True),
+    }
+
+    _defaults = {
+        'time_in':  lambda self, cr, uid, context: '00:00:00',
+        'time_out': lambda self, cr, uid, context: '00:00:00',
+        'break':    lambda self, cr, uid, context: '00:30:00',
+    }

=== added directory 'hr_roster/images'
=== added file 'hr_roster/images/duty_roster.png'
Binary files hr_roster/images/duty_roster.png	1970-01-01 00:00:00 +0000 and hr_roster/images/duty_roster.png	2014-04-24 17:59:04 +0000 differ
=== added file 'hr_roster/images/duty_roster_err.png'
Binary files hr_roster/images/duty_roster_err.png	1970-01-01 00:00:00 +0000 and hr_roster/images/duty_roster_err.png	2014-04-24 17:59:04 +0000 differ
=== added file 'hr_roster/images/shift_code.png'
Binary files hr_roster/images/shift_code.png	1970-01-01 00:00:00 +0000 and hr_roster/images/shift_code.png	2014-04-24 17:59:04 +0000 differ
=== added file 'hr_roster/images/shift_code_tree.png'
Binary files hr_roster/images/shift_code_tree.png	1970-01-01 00:00:00 +0000 and hr_roster/images/shift_code_tree.png	2014-04-24 17:59:04 +0000 differ
=== added directory 'hr_roster/static'
=== added directory 'hr_roster/static/src'
=== added directory 'hr_roster/static/src/css'
=== added file 'hr_roster/static/src/css/hr_roster.css'
--- hr_roster/static/src/css/hr_roster.css	1970-01-01 00:00:00 +0000
+++ hr_roster/static/src/css/hr_roster.css	2014-04-24 17:59:04 +0000
@@ -0,0 +1,20 @@
+.openerp .oe_list_content td, .openerp .oe_list_content th {
+    line-height: normal;
+}
+
+.oe_list.shift_day_list [data-id=employee_id] {
+    width: 30em;
+    vertical-align: top;
+}
+
+.oe_list_header_shift_day {
+    font-size: 0.75em;
+    vertical-align: top;
+    width: 24px;
+    padding: 3px 0 0 3px !important;
+    text-align: center !important;
+}
+
+.hr_shift_code_col {
+    font-weight: bold;
+}

=== added directory 'hr_roster/static/src/js'
=== added file 'hr_roster/static/src/js/hr_roster.js'
--- hr_roster/static/src/js/hr_roster.js	1970-01-01 00:00:00 +0000
+++ hr_roster/static/src/js/hr_roster.js	2014-04-24 17:59:04 +0000
@@ -0,0 +1,160 @@
+openerp.hr_roster = function(instance) {
+    var _t  = instance.web._t,
+        _lt = instance.web._lt;
+    var QWeb = instance.web.qweb;
+
+    var humanize_time = function(mins){
+        var mins = Number.parseInt(mins),
+            h = instance.web.format_value(Number.parseInt(mins / 60), this, 0),
+            m = instance.web.format_value(mins % 60, this, 0);
+
+        var h_text = "", m_text = "";
+        if(h > 0){
+            h_text = h + " hour";
+            if(h > 1) h_text += "s";
+        }
+        if(m > 0){
+            m_text = m + " minute";
+            if(m > 1) m_text += "s";
+        }
+        return _.string.sprintf("%s %s", h_text, m_text);
+    };
+
+    // BEGIN hr_duty_roster
+    instance.hr_duty_roster = {};
+
+    instance.hr_duty_roster.FieldShiftDay = instance.web.form.FieldChar.extend({
+        is_false: function(){
+            var selection = this.field_manager.get_field_desc(this.name).selection,
+                v = this.get_value();
+
+            var r = this._super() || !(_.any(selection, function(choice, index, list){
+                return choice[0] == v;
+            }));
+            return r;
+        },
+        start: function() {
+            var self = this;
+            self._super();
+            self.$('input').on('keyup', function(e){
+                var $i = $(this),
+                    v = $i.val().trim().toUpperCase();
+                if($i.val() != v){
+                    $i.val(v);
+
+                    $i.blur();
+                    self.$el.next().find('input:first').focus()
+                }
+            });
+        }
+    });
+
+    instance.hr_duty_roster.FieldEmployeesFilter = instance.web.form.FieldMany2One.extend({
+        get_search_blacklist: function() {
+            // WARNING: i am not sure if this will work for future v8
+            var blacklist = _.reduce(this.field_manager.dataset.cache, function(memo, row){
+                var employee_id = row.values.employee_id;
+                // if a record is loaded from db, the employee_id is a tuple in the form of (id, name)
+                // otherwise, it is just an id (integer)
+                if(_.isArray(employee_id)){
+                    employee_id = employee_id[0];  
+                }
+                memo.push(employee_id);
+                return memo;
+            }, []);
+
+            return blacklist;
+        }
+    });
+
+    instance.web.form.widgets.add('shift_day', 'instance.hr_duty_roster.FieldShiftDay');
+    instance.web.form.widgets.add('employees_filter', 'instance.hr_duty_roster.FieldEmployeesFilter');
+
+    // we have to use customize _format for this column, or otherwise the value that is rendered into the cell
+    // has an "id," prepended to it
+    instance.hr_duty_roster.EmployeesFilterColumn = instance.web.list.Column.extend({
+        _format: function(row_data, options){
+            var value = row_data.employee_id.value;
+            return value[1] ? value[1].split("\n")[0] : value[1];
+        }
+    });
+
+    instance.web.list.columns.add('field.employees_filter', 'instance.hr_duty_roster.EmployeesFilterColumn');
+
+    instance.hr_duty_roster.WidgetDatePicker = instance.web.form.FormWidget.extend({
+        renderElement: function(){
+            var m = this.field_manager.get_field_value('month') - 1,  // Python month is 1-base
+                y = this.field_manager.get_field_value('year'),
+                d = 1,
+                today = new Date();
+            if(today.getFullYear() == y && today.getMonth() == m) d = today.day;
+
+            this.$el.html(QWeb.render('DatePickerWidget', {_id: 'hr_duty_roster_dp' + _.uniqueId(), year: y, month: m, day: d}));
+        }
+    });
+
+    instance.hr_duty_roster.WidgetShiftCodeHelp = instance.web.form.FormWidget.extend({
+        renderElement: function(){
+            var self = this,
+                model = new instance.web.Model("hr.shift_code");
+            
+            model.query(['code', 'time_in', 'time_out', 'duration', 'break', 'description']).all().then(function(rows){
+                _.map(rows, function(v, k, list){
+                    list[k]['duration_text'] = humanize_time(v['duration']);
+                });
+                var out = QWeb.render('ShiftCodeHelpWidget', {'selection': rows});
+                self.$el.html(out);
+            });
+        }
+    });
+
+    instance.web.form.custom_widgets.add('datepicker', 'instance.hr_duty_roster.WidgetDatePicker');
+    instance.web.form.custom_widgets.add('shiftcodehelp', 'instance.hr_duty_roster.WidgetShiftCodeHelp');
+    // --- END hr_duty_roster
+
+
+    // -- BEGIN hr_shift_code
+    instance.hr_shift_code = {};
+
+    instance.hr_shift_code.FieldDuration = instance.web.form.FieldChar.extend({
+        render_value: function(){
+            this.$el.text(humanize_time(this.get_value()));
+        }
+    });
+
+    instance.web.form.widgets.add('duration_mins', 'instance.hr_shift_code.FieldDuration');
+
+    instance.hr_shift_code.TimeWidget = instance.web.DateTimeWidget.extend({
+        jqueryui_object: "timepicker",
+        type_of_date: "time",
+        on_picker_select: function(text, instance_) {
+            // NOTE: as of jQuery timepicker v0.9.9, retriving time from timepicker with getDate always returns jQuery object
+            var time_str = this.picker('getDate').val(), // this.picker is a timepicker
+                val = instance.web.str_to_time(time_str + ":00");
+
+            this.$input
+                .val(val ? this.format_client(val) : '')
+                .change()
+                .focus();
+        }
+    });
+
+    instance.hr_shift_code.FieldTimePicker = instance.web.form.FieldDatetime.extend({
+        build_widget: function() {
+            return new instance.hr_shift_code.TimeWidget(this);
+        }
+    });
+
+    instance.web.form.widgets.add('timepicker', 'instance.hr_shift_code.FieldTimePicker');
+
+    // custom columns used by shift code module
+    instance.hr_shift_code.DurationMinsColumn = instance.web.list.Column.extend({
+        _format: function(row_data, options){
+            return _.escape(instance.web.format_value(
+                humanize_time(row_data[this.id].value), this, options.value_if_empty));
+        }
+    });
+
+    instance.web.list.columns.add('field.duration_mins', 'instance.hr_shift_code.DurationMinsColumn');
+    // --- END hr_shift_code
+}

=== added directory 'hr_roster/static/src/xml'
=== added file 'hr_roster/static/src/xml/hr_roster.xml'
--- hr_roster/static/src/xml/hr_roster.xml	1970-01-01 00:00:00 +0000
+++ hr_roster/static/src/xml/hr_roster.xml	2014-04-24 17:59:04 +0000
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<templates xml:space="preserve">
+    <t t-name="DatePickerWidget">
+        <div t-att-id="_id" class="datepicker"></div>
+        <script type="text/javascript">
+            setTimeout(function(){
+                $('#<t t-raw="_id"/>').datepicker({'onSelect': function(){}, 'defaultDate': new Date(<t t-raw="year"/>, <t t-raw="month"/>, <t t-raw="day"/>)});
+            }, 0);
+        </script>
+    </t>
+    
+    <t t-name="ShiftField">
+        <span class="oe_form_field hr_roster_month_shift">
+            <t t-foreach="widget.get('value').length" t-as="day">
+                <t t-if="! widget.get('readonly')">
+                    <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]"></input>
+                </t>
+                <t t-if="widget.get('readonly')">
+                    <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]" readonly="readonly"></input>
+                </t>
+            </t>
+        </span>
+    </t>
+</templates>

=== added file 'hr_roster/static/src/xml/hr_shift_code.xml'
--- hr_roster/static/src/xml/hr_shift_code.xml	1970-01-01 00:00:00 +0000
+++ hr_roster/static/src/xml/hr_shift_code.xml	2014-04-24 17:59:04 +0000
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<templates xml:space="preserve">
+    <t t-name="ShiftCodeHelpWidget">
+        <table class="oe_list_content">
+            <thead>
+                <tr>
+                    <th>Code</th>
+                    <th>Time In - Time Out</th>
+                    <th>Duration</th>
+                    <th>Break</th>
+                    <th>Description</th>
+                </tr>
+            </thead>
+            <tbody>
+                <t t-foreach="selection" t-as="choice">
+                    <tr>
+                        <td class="hr_shift_code_col">
+                            <t t-esc="choice.code"></t>
+                        </td>
+                        <td>
+                            <t t-esc="choice.time_in"></t> - <t t-esc="choice.time_out"></t> 
+                        </td>
+                        <td>
+                            <t t-if="choice.duration > 0">
+                                <t t-esc="choice.duration_text"></t>
+                            </t>
+                        </td>
+                        <td>
+                            <t t-if="choice.break">
+                                <t t-esc="choice.break"></t>
+                            </t>
+                        </td>
+                        <td>
+                            <t t-if="choice.description">
+                                <t t-esc="choice.description"></t>
+                            </t>
+                        </td>
+                    </tr>
+                </t>
+            </tbody>
+        </table>
+    </t>
+</templates>


Follow ups