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

Proposed by Chang Phui-Hock
Status: Work in progress
Proposed branch: lp:~codekaki/openerp-hr/7.0-hr_roster
Merge into: lp:openerp-hr
Diff against target: 866 lines (+796/-0)
11 files modified
hr_roster/__init__.py (+1/-0)
hr_roster/__openerp__.py (+41/-0)
hr_roster/duty_roster_view.xml (+149/-0)
hr_roster/duty_roster_workflow.xml (+71/-0)
hr_roster/hr_roster.py (+211/-0)
hr_roster/security/duty_roster_security.xml (+15/-0)
hr_roster/security/ir.model.access.csv (+9/-0)
hr_roster/static/src/css/hr_roster.css (+20/-0)
hr_roster/static/src/js/hr_roster.js (+208/-0)
hr_roster/static/src/xml/hr_roster.xml (+25/-0)
hr_roster/static/src/xml/hr_shift_code.xml (+46/-0)
To merge this branch: bzr merge lp:~codekaki/openerp-hr/7.0-hr_roster
Reviewer Review Type Date Requested Status
Daniel Reis Needs Fixing
Review via email: mp+217116@code.launchpad.net

Description of the change

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)

To post a comment you must log in.
Revision history for this message
Daniel Reis (dreis-pt) wrote :

First of all thanks for embracing the open-source spirit by sharing your module!

The current features are of course focused on your specific use case.
But it would be good for your module to be the foundation to build on for more complex usa cases.

For that, I would suggest a slightly different design, following the implementation strategy used in standard hr_timesheet, but where:
* each cell is actually a record, for a Date + Shift Code (instead of a column of a row).
* each line is an Employee (instead of an Analytic Account)
* the header is a Position / Location to be rostered (instead of an employee).

I can exemplify two possible extensions having this structure:
a) have richer information on each Cell, such as comments, specific duties, flags for overtime or night work, etc.
b) support other planning periods, such as rolling 4 week periods instead of full.
to the Timesheet module:would be a one month view over a "Position Roster":

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Hi Daniel,
Thanks for taking the time to review this MP.

I like your idea. It does sound like more flexible for future
customization. In fact, I studied the standard hr_timesheet module before
deciding to write this one, partly because we did not have the time budget
at that moment.

If this module is to be done "properly", it will then involve not so
trivial changes. Is it not advisable to get this module merged, and start a
new module which then deprecates this one?

*Chang Phui Hock*
CODEKAKI SYSTEMS (R49045/14) - A web-dev company
Address: TB 15588-1 1st Floor, Lrg Kubota, Kubota Square, 91000 Tawau.
Tel: +6018-263 9102
Skype: phuihock
Website: http://www.codekaki.com

On Sun, Apr 27, 2014 at 1:23 AM, Daniel Reis <email address hidden> wrote:

> First of all thanks for embracing the open-source spirit by sharing your
> module!
>
> The current features are of course focused on your specific use case.
> But it would be good for your module to be the foundation to build on for
> more complex usa cases.
>
> For that, I would suggest a slightly different design, following the
> implementation strategy used in standard hr_timesheet, but where:
> * each cell is actually a record, for a Date + Shift Code (instead of a
> column of a row).
> * each line is an Employee (instead of an Analytic Account)
> * the header is a Position / Location to be rostered (instead of an
> employee).
>
> I can exemplify two possible extensions having this structure:
> a) have richer information on each Cell, such as comments, specific
> duties, flags for overtime or night work, etc.
> b) support other planning periods, such as rolling 4 week periods instead
> of full.
> to the Timesheet module:would be a one month view over a "Position Roster":
>
> --
> https://code.launchpad.net/~codekaki/openerp-hr/7.0-hr_roster/+merge/217116
> You proposed lp:~codekaki/openerp-hr/7.0-hr_roster for merging.
>

Revision history for this message
Daniel Reis (dreis-pt) wrote :

The perfect is enemy of good; a perfectly working module should not go to waste, even more when it covers an area where there is no other module available.

IMO we should continue the review then, assuming the current design choices.

Revision history for this message
Daniel Reis (dreis-pt) wrote :

OK, I have a few nitpicks and suggestions. Comments below:

*.py: some small PEP8 issues; try running a PEP8 check for details.
L8: you should add the AGPL license header at the top of this file. Including a short text 'summary' attribute is highly recommended.
L50: 'images' is defined twice;
L69,70,74: I believe that in v7 forms 'col' and 'colspan' are inoperant. Nested groups are what makes that effect.
L86-116:
L114-116: wouldn't if be nicer to hide these on shorter months, instead of just making them readonly?
L192: honestly I think workflow is an overkill here; I would advise removing it.
L339: 'days' is a function field, I don't think you need to set a default for it.
L362: IMO it would be better for department_id to not be mandatory; that would allow for a roster mixing employees from more than one department (sooner or later you'll need this)
L363: the ORM already has "create_uid" and "create_date"; why nor leverage these instead of duplicating?
L371-373: again, defining defaults on function fields
L398: I suggest handling the case where time_out is lower than time_id; example: shift starts at 17:00 and ends at 01:00 if the next day.
L425: I strongly suggest making shift 'code' larger. For larger setups 1 will be clearly enough: it's easy to get hundreds of shift code. I suggest a size=10.

Many are suggestions for improvement, and I think the rest is easy to fix.
Hope to see it merged soon!

review: Needs Fixing
Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Thanks for the valuable suggestions, Daniel. I will get them fixed soon.

*Chang Phui Hock*
CODEKAKI SYSTEMS (R49045/14) - A web-dev company
Address: TB 15588-1 1st Floor, Lrg Kubota, Kubota Square, 91000 Tawau.
Tel: +6018-263 9102
Skype: phuihock
Website: http://www.codekaki.com

On Tue, Apr 29, 2014 at 4:32 AM, Daniel Reis <email address hidden> wrote:

> Review: Needs Fixing
>
> OK, I have a few nitpicks and suggestions. Comments below:
>
> *.py: some small PEP8 issues; try running a PEP8 check for details.
> L8: you should add the AGPL license header at the top of this file.
> Including a short text 'summary' attribute is highly recommended.
> L50: 'images' is defined twice;
> L69,70,74: I believe that in v7 forms 'col' and 'colspan' are inoperant.
> Nested groups are what makes that effect.
> L86-116:
> L114-116: wouldn't if be nicer to hide these on shorter months, instead of
> just making them readonly?
> L192: honestly I think workflow is an overkill here; I would advise
> removing it.
> L339: 'days' is a function field, I don't think you need to set a default
> for it.
> L362: IMO it would be better for department_id to not be mandatory; that
> would allow for a roster mixing employees from more than one department
> (sooner or later you'll need this)
> L363: the ORM already has "create_uid" and "create_date"; why nor leverage
> these instead of duplicating?
> L371-373: again, defining defaults on function fields
> L398: I suggest handling the case where time_out is lower than time_id;
> example: shift starts at 17:00 and ends at 01:00 if the next day.
> L425: I strongly suggest making shift 'code' larger. For larger setups 1
> will be clearly enough: it's easy to get hundreds of shift code. I suggest
> a size=10.
>
> Many are suggestions for improvement, and I think the rest is easy to fix.
> Hope to see it merged soon!
> --
> https://code.launchpad.net/~codekaki/openerp-hr/7.0-hr_roster/+merge/217116
> You proposed lp:~codekaki/openerp-hr/7.0-hr_roster for merging.
>

81. By Chang Phui-Hock

[FIX] pep8

82. By Chang Phui-Hock

[FIX] Change time_in and time_out to datetime fields, convert to UTC when passing to server. Remove redundant created_by_id, PEP8 fixes.

83. By Chang Phui-Hock

[FIX] duplicate images

84. By Chang Phui-Hock

[FIX] pep8

85. By Chang Phui-Hock

[IMP] set timepicker default time to 00:00:00

Revision history for this message
Daniel Reis (dreis-pt) wrote :

One more thing I forgot to mention: I notice the "days" fields are all mandatory.
How will you handle the case when a person enters or exists a roster during the month?
Won't it be more practical to leave the days fields optional?

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

User is allowed to edit the duty roster in 'new' or 'draft' state, but must fill the whole month for the selected employee.

This is an acceptable behavior, at least for for my client.

I do not allow user to edit the duty roster once it has been approved. This is because once it has been approved by a Manager, the duty roster is used to generate final work days calendar.

This is also why I have workflow in this module. Though it doesn't really do anything, but it provides hooks for which other modules can inherit and extend to do something in each stage, say, send email notification, generate work days calendar etc.

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Sorry, hasn't done typing :)

One of the problem for allowing days to be optional is that I don't get the nice "required error" bubble for other validation errors, and the effort to make sure every row is filled and correct doesn't seem to worth it.

But I might implement it later as an exercise ;)

Revision history for this message
Daniel Reis (dreis-pt) wrote :

You will have cases where a person will not be on the roster for the full month. How about adding start and end dates for employees on rosters? (though this may not be easy to implement)

I also noticed that access rules are missing ("WARNING openerp.modules.loading: The model hr.duty_roster_shift has no access rules, consider adding one. E.g. access_hr_duty_roster_shift,access_hr_duty_roster_shift,model_hr_duty_roster_shift,,1,1,1,1" ...)

I believe you are still working on this, so I'm setting the MP as WIP.

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Yes, still working on it.

About a person not on roster for full month, we use 'X' to indicate day off. But I do see your point, and I might take a bit longer to get that implemented.

Seeing the todo getting longer, should I delete this merge proposal and resubmit later when it is ready?

Revision history for this message
Daniel Reis (dreis-pt) wrote :

Then I suggest you to add that "x" shift code in the module's initial data, with a proper explanation.
And there's no need to delete the MP- the "Work in progress" status lets reviewers know it's being worked and is not waiting for reviews.

86. By Chang Phui-Hock

[IMP] Apply access control. Only duty roster managers have full access to shift code and duty roster.

87. By Chang Phui-Hock

[IMP] Add new hr.group_duty_roster_user

88. By Chang Phui-Hock

[IMP] Apply sensible default access permissions.

89. By Chang Phui-Hock

[IMP] Add rejected state. Minor ui adjustment.

Revision history for this message
Pedro Manuel Baeza (pedro.baeza) wrote :

This module is very interesting to be rescued from Launchpad and propose in the new host Github (https://github.com/OCA/hr). Please make the corresponding PR.

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Been meaning to do that. Will make a PR with the latest changes.

Revision history for this message
Chang Phui-Hock (phuihock) wrote :

Hey,
I've just created a pull request on github.

Unmerged revisions

89. By Chang Phui-Hock

[IMP] Add rejected state. Minor ui adjustment.

88. By Chang Phui-Hock

[IMP] Apply sensible default access permissions.

87. By Chang Phui-Hock

[IMP] Add new hr.group_duty_roster_user

86. By Chang Phui-Hock

[IMP] Apply access control. Only duty roster managers have full access to shift code and duty roster.

85. By Chang Phui-Hock

[IMP] set timepicker default time to 00:00:00

84. By Chang Phui-Hock

[FIX] pep8

83. By Chang Phui-Hock

[FIX] duplicate images

82. By Chang Phui-Hock

[FIX] Change time_in and time_out to datetime fields, convert to UTC when passing to server. Remove redundant created_by_id, PEP8 fixes.

81. By Chang Phui-Hock

[FIX] pep8

80. By Chang Phui-Hock

[FIX] tree name should be in plural

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'hr_roster'
2=== added file 'hr_roster/__init__.py'
3--- hr_roster/__init__.py 1970-01-01 00:00:00 +0000
4+++ hr_roster/__init__.py 2014-05-07 07:38:43 +0000
5@@ -0,0 +1,1 @@
6+import hr_roster
7
8=== added file 'hr_roster/__openerp__.py'
9--- hr_roster/__openerp__.py 1970-01-01 00:00:00 +0000
10+++ hr_roster/__openerp__.py 2014-05-07 07:38:43 +0000
11@@ -0,0 +1,41 @@
12+{
13+ 'name': 'Duty Roster',
14+ 'version': '0.1',
15+ 'category': 'Human Resources',
16+ 'description': """
17+Duty Roster
18+===========
19+
20+This is a generic module to allow easy management of staff duty roster.
21+It should be used together with hr_holidays such that leaves application
22+that spans multiple days takes into account of half/off duty days.
23+""",
24+ 'author': "CODEKAKI SYSTEMS (R49045/14)",
25+ 'website': 'http://codekaki.com',
26+ 'depends': ['hr'],
27+ 'images': [
28+ 'images/shift_code_tree.png',
29+ 'images/shift_code.png',
30+ 'images/duty_roster.png',
31+ 'images/duty_roster_err.png',
32+ ],
33+ 'data': [
34+ 'security/duty_roster_security.xml',
35+ 'security/ir.model.access.csv',
36+ 'duty_roster_workflow.xml',
37+ 'duty_roster_view.xml',
38+ ],
39+ 'css': [
40+ 'static/src/css/hr_roster.css',
41+ ],
42+ 'js': [
43+ 'static/src/js/hr_roster.js',
44+ ],
45+ 'qweb': [
46+ 'static/src/xml/hr_shift_code.xml',
47+ 'static/src/xml/hr_roster.xml',
48+ ],
49+ 'demo': [],
50+ 'test': [],
51+ 'installable': True,
52+}
53
54=== added file 'hr_roster/duty_roster_view.xml'
55--- hr_roster/duty_roster_view.xml 1970-01-01 00:00:00 +0000
56+++ hr_roster/duty_roster_view.xml 2014-05-07 07:38:43 +0000
57@@ -0,0 +1,149 @@
58+<?xml version="1.0"?>
59+<openerp>
60+ <data>
61+ <record id="view_duty_roster" model="ir.ui.view">
62+ <field name="model">hr.duty_roster</field>
63+ <field name="type">form</field>
64+ <field name="arch" type="xml">
65+ <form string="Duty Roster" version="7.0">
66+ <header>
67+ <button name="confirm" string="Submit for Approval" class="oe_highlight" attrs="{'invisible': ['|', ('state', 'not in', ['draft']), ('shifts', '=', [])]}" groups="hr.group_duty_roster_user"></button>
68+ <button name="resubmit" string="Resubmit" class="oe_highlight" attrs="{'invisible': ['|', ('state', 'not in', ['rejected']), ('shifts', '=', [])]}" groups="hr.group_duty_roster_user"></button>
69+ <button name="checked" string="Approve" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ['pending'])]}" groups="hr.group_duty_roster_manager"></button>
70+ <button name="rejected" string="Reject" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ['pending'])]}" groups="hr.group_duty_roster_manager"></button>
71+ <field name="state" widget="statusbar" statusbar_visible="draft,pending,checked"/>
72+ </header>
73+ <group col="4">
74+ <group>
75+ <field name="department_id" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
76+ <field name="name" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
77+ </group>
78+ <group>
79+ <field name="month" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
80+ <field name="year" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
81+ </group>
82+ <widget type="datepicker"></widget>
83+ </group>
84+ <group>
85+ <field name="remarks" placeholder="Remarks..."></field>
86+ </group>
87+ <notebook>
88+ <page string="Shifts">
89+ <field name="shifts" attrs="{'readonly': [('state', 'in', ['new', 'pending', 'checked'])]}" context="{'active_id': active_id}">
90+ <tree string="Shifts" editable="bottom" class="shift_day_list">
91+ <field name="employee_id" required="1" domain="[('department_id', '=', parent.department_id)]" widget="employees_filter"></field>
92+ <field name="days" invisible="1" readonly="1"></field>
93+ <field name="day_1" widget="shift_day" string="1" attrs="{'required': [('days', '>=', 1)]}"></field>
94+ <field name="day_2" widget="shift_day" string="2" attrs="{'required': [('days', '>=', 2)]}"></field>
95+ <field name="day_3" widget="shift_day" string="3" attrs="{'required': [('days', '>=', 3)]}"></field>
96+ <field name="day_4" widget="shift_day" string="4" attrs="{'required': [('days', '>=', 4)]}"></field>
97+ <field name="day_5" widget="shift_day" string="5" attrs="{'required': [('days', '>=', 5)]}"></field>
98+ <field name="day_6" widget="shift_day" string="6" attrs="{'required': [('days', '>=', 6)]}"></field>
99+ <field name="day_7" widget="shift_day" string="7" attrs="{'required': [('days', '>=', 6)]}"></field>
100+ <field name="day_8" widget="shift_day" string="8" attrs="{'required': [('days', '>=', 8)]}"></field>
101+ <field name="day_9" widget="shift_day" string="9" attrs="{'required': [('days', '>=', 9)]}"></field>
102+ <field name="day_10" widget="shift_day" string="10" attrs="{'required': [('days', '>=', 10)]}"></field>
103+ <field name="day_11" widget="shift_day" string="11" attrs="{'required': [('days', '>=', 11)]}"></field>
104+ <field name="day_12" widget="shift_day" string="12" attrs="{'required': [('days', '>=', 12)]}"></field>
105+ <field name="day_13" widget="shift_day" string="13" attrs="{'required': [('days', '>=', 13)]}"></field>
106+ <field name="day_14" widget="shift_day" string="14" attrs="{'required': [('days', '>=', 14)]}"></field>
107+ <field name="day_15" widget="shift_day" string="15" attrs="{'required': [('days', '>=', 15)]}"></field>
108+ <field name="day_16" widget="shift_day" string="16" attrs="{'required': [('days', '>=', 16)]}"></field>
109+ <field name="day_17" widget="shift_day" string="17" attrs="{'required': [('days', '>=', 17)]}"></field>
110+ <field name="day_18" widget="shift_day" string="18" attrs="{'required': [('days', '>=', 18)]}"></field>
111+ <field name="day_19" widget="shift_day" string="19" attrs="{'required': [('days', '>=', 19)]}"></field>
112+ <field name="day_20" widget="shift_day" string="20" attrs="{'required': [('days', '>=', 20)]}"></field>
113+ <field name="day_21" widget="shift_day" string="21" attrs="{'required': [('days', '>=', 21)]}"></field>
114+ <field name="day_22" widget="shift_day" string="22" attrs="{'required': [('days', '>=', 22)]}"></field>
115+ <field name="day_23" widget="shift_day" string="23" attrs="{'required': [('days', '>=', 23)]}"></field>
116+ <field name="day_24" widget="shift_day" string="24" attrs="{'required': [('days', '>=', 24)]}"></field>
117+ <field name="day_25" widget="shift_day" string="25" attrs="{'required': [('days', '>=', 25)]}"></field>
118+ <field name="day_26" widget="shift_day" string="26" attrs="{'required': [('days', '>=', 26)]}"></field>
119+ <field name="day_27" widget="shift_day" string="27" attrs="{'required': [('days', '>=', 27)]}"></field>
120+ <field name="day_28" widget="shift_day" string="28" attrs="{'required': [('days', '>=', 28)]}"></field>
121+ <field name="day_29" widget="shift_day" string="29" attrs="{'readonly': [('days', '&lt;', 29)], 'required': [('days', '>=', 29)]}"></field>
122+ <field name="day_30" widget="shift_day" string="30" attrs="{'readonly': [('days', '&lt;', 30)], 'required': [('days', '>=', 30)]}"></field>
123+ <field name="day_31" widget="shift_day" string="31" attrs="{'readonly': [('days', '&lt;', 31)], 'required': [('days', '>=', 31)]}"></field>
124+ </tree>
125+ </field>
126+ </page>
127+ </notebook>
128+ <group>
129+ <widget type="shiftcodehelp"></widget>
130+ <field name="checked_by_id" readonly="1" attrs="{'invisible': [('state', '!=', 'checked')]}"></field>
131+ </group>
132+ </form>
133+ </field>
134+ </record>
135+
136+ <record id="view_duty_roster_tree" model="ir.ui.view">
137+ <field name="model">hr.duty_roster</field>
138+ <field name="type">tree</field>
139+ <field name="arch" type="xml">
140+ <tree string="Duty Rosters">
141+ <field name="department_id"></field>
142+ <field name="name"></field>
143+ <field name="year"></field>
144+ <field name="month"></field>
145+ <field name="state"></field>
146+ </tree>
147+ </field>
148+ </record>
149+
150+ <record id="view_hr_shift_code_tree" model="ir.ui.view">
151+ <field name="model">hr.shift_code</field>
152+ <field name="type">tree</field>
153+ <field name="arch" type="xml">
154+ <tree string="Shift Codes">
155+ <field name="code"></field>
156+ <field name="time_in" widget="time"></field>
157+ <field name="time_out" widget="time"></field>
158+ <field name="duration" widget="duration_mins"></field>
159+ <field name="break"></field>
160+ <field name="description"></field>
161+ </tree>
162+ </field>
163+ </record>
164+
165+ <record id="view_hr_shift_code" model="ir.ui.view">
166+ <field name="model">hr.shift_code</field>
167+ <field name="type">form</field>
168+ <field name="arch" type="xml">
169+ <form string="Shift Code" version="7.0">
170+ <sheet>
171+ <group col="4">
172+ <group name="info">
173+ <field name="code"></field>
174+ <field name="description" widget="text"></field>
175+ </group>
176+ <group name="time">
177+ <field name="time_in" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
178+ <field name="time_out" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
179+ <field name="duration" readonly="1" widget="duration_mins"></field>
180+ <field name="break" widget="timepicker"></field>
181+ </group>
182+ </group>
183+ </sheet>
184+ </form>
185+ </field>
186+ </record>
187+
188+ <record model="ir.actions.act_window" id="action_hr_shift_code">
189+ <field name="name">Shift Code</field>
190+ <field name="res_model">hr.shift_code</field>
191+ <field name="view_type">form</field>
192+ <field name="view_mode">tree,form</field>
193+ </record>
194+
195+ <record model="ir.actions.act_window" id="action_duty_roster">
196+ <field name="name">Duty Roster</field>
197+ <field name="res_model">hr.duty_roster</field>
198+ <field name="view_type">form</field>
199+ <field name="view_mode">tree,form</field>
200+ </record>
201+
202+ <menuitem id="menu_duty_roster_root" name="Duty Rosters" action="" parent="hr.menu_hr_root" groups="base.group_user"></menuitem>
203+ <menuitem id="menu_shift_code" name="Shift Codes" sequence="20" action="action_hr_shift_code" parent="menu_duty_roster_root" groups="base.group_hr_manager"></menuitem>
204+ <menuitem id="menu_duty_roster" name="Duty Rosters" sequence="10" action="action_duty_roster" parent="menu_duty_roster_root" groups="hr.group_duty_roster_user,hr.group_duty_roster_manager"></menuitem>
205+ </data>
206+</openerp>
207
208=== added file 'hr_roster/duty_roster_workflow.xml'
209--- hr_roster/duty_roster_workflow.xml 1970-01-01 00:00:00 +0000
210+++ hr_roster/duty_roster_workflow.xml 2014-05-07 07:38:43 +0000
211@@ -0,0 +1,71 @@
212+<openerp>
213+ <data>
214+ <record id="wkf_duty_roster" model="workflow">
215+ <field name="name">hr.duty_roster.basic</field>
216+ <field name="osv">hr.duty_roster</field>
217+ <field name="on_create">True</field>
218+ </record>
219+
220+ <!-- activities -->
221+ <record id="act_new" model="workflow.activity">
222+ <field name="wkf_id" ref="wkf_duty_roster"/>
223+ <field name="flow_start">True</field>
224+ <field name="name">new</field>
225+ </record>
226+ <record id="act_draft" model="workflow.activity">
227+ <field name="wkf_id" ref="wkf_duty_roster"/>
228+ <field name="name">draft</field>
229+ <field name="kind">function</field>
230+ <field name="action">action_draft()</field>
231+ </record>
232+ <record id="act_pending" model="workflow.activity">
233+ <field name="wkf_id" ref="wkf_duty_roster"/>
234+ <field name="name">pending</field>
235+ <field name="kind">function</field>
236+ <field name="action">action_pending()</field>
237+ </record>
238+ <record id="act_checked" model="workflow.activity">
239+ <field name="wkf_id" ref="wkf_duty_roster"/>
240+ <field name="flow_end">True</field>
241+ <field name="name">checked</field>
242+ <field name="kind">function</field>
243+ <field name="action">action_checked()</field>
244+ </record>
245+
246+ <record id="act_rejected" model="workflow.activity">
247+ <field name="wkf_id" ref="wkf_duty_roster"/>
248+ <field name="name">rejected</field>
249+ <field name="kind">function</field>
250+ <field name="action">action_rejected()</field>
251+ </record>
252+
253+ <!-- transitions -->
254+ <record id="trans_new_draft" model="workflow.transition">
255+ <field name="act_from" ref="act_new"/>
256+ <field name="act_to" ref="act_draft"/>
257+ <field name="condition">True</field>
258+ </record>
259+ <record id="trans_draft_pending" model="workflow.transition">
260+ <field name="act_from" ref="act_draft"/>
261+ <field name="act_to" ref="act_pending"/>
262+ <field name="condition">has_shifts()</field>
263+ <field name="signal">confirm</field>
264+ </record>
265+ <record id="trans_pending_checked" model="workflow.transition">
266+ <field name="act_from" ref="act_pending"/>
267+ <field name="act_to" ref="act_checked"/>
268+ <field name="signal">checked</field>
269+ </record>
270+ <record id="trans_pending_rejected" model="workflow.transition">
271+ <field name="act_from" ref="act_pending"/>
272+ <field name="act_to" ref="act_rejected"/>
273+ <field name="signal">rejected</field>
274+ </record>
275+ <record id="trans_reject_pending" model="workflow.transition">
276+ <field name="act_from" ref="act_rejected"/>
277+ <field name="act_to" ref="act_pending"/>
278+ <field name="signal">resubmit</field>
279+ </record>
280+ </data>
281+</openerp>
282+
283
284=== added file 'hr_roster/hr_roster.py'
285--- hr_roster/hr_roster.py 1970-01-01 00:00:00 +0000
286+++ hr_roster/hr_roster.py 2014-05-07 07:38:43 +0000
287@@ -0,0 +1,211 @@
288+from datetime import datetime
289+from openerp.osv import fields, osv
290+from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
291+import calendar
292+import logging
293+
294+_logger = logging.getLogger(__name__)
295+
296+MONTHS = zip(range(1, 13), calendar.month_name[1:])
297+
298+
299+def monthrange(year=None, month=None):
300+ today = datetime.today()
301+ y = year or today.year
302+ m = month or today.month
303+ return y, m, calendar.monthrange(y, m)[1]
304+
305+
306+class hr_duty_roster_shift(osv.Model):
307+ _name = 'hr.duty_roster_shift'
308+
309+ def _get_days_default(self, cr, uid, context=None):
310+ if 'active_id' in context:
311+ active_id = context['active_id']
312+ row = self.pool.get('hr.duty_roster').read(cr, uid, active_id, [
313+ 'year', 'month'])
314+ mrange = calendar.monthrange(row['year'], row['month'])
315+ return mrange[1]
316+ return 0
317+
318+ def _get_days(self, cr, uid, ids, field_name, args=None, context=None):
319+ res = {}
320+ if ids:
321+ parent_row = self.read(cr, uid, ids[0], ['duty_roster_id'],
322+ context=context)
323+ row = self.pool.get('hr.duty_roster').read(
324+ cr,
325+ uid,
326+ parent_row['duty_roster_id'][0],
327+ ['year', 'month'],
328+ context=context)
329+ mrange = calendar.monthrange(row['year'], row['month'])
330+ for _id in ids:
331+ res[_id] = mrange[1]
332+ return res
333+
334+ def _get_shift_codes(self, cr, uid, context=None):
335+ context = context or {}
336+ if 'shift_codes' not in context:
337+ model = self.pool.get('hr.shift_code')
338+ ids = model.search(cr, uid, [], order="code", context=context)
339+ ret = model.read(cr, uid, ids, ['code'], context=context)
340+ context['shift_codes'] = [(r['code'], r['code']) for r in ret]
341+ return context['shift_codes']
342+
343+ _columns = {
344+ 'duty_roster_id': fields.many2one(
345+ 'hr.duty_roster', 'Duty Roster', required=True,
346+ ondelete="cascade"),
347+ 'employee_id': fields.many2one(
348+ 'hr.employee', 'Employee', required=True),
349+ 'days': fields.function(
350+ _get_days, type="integer", string='Days', required=True),
351+ 'day_1': fields.selection(_get_shift_codes, string='Day 1', size=1),
352+ 'day_2': fields.selection(_get_shift_codes, string='Day 2', size=1),
353+ 'day_3': fields.selection(_get_shift_codes, string='Day 3', size=1),
354+ 'day_4': fields.selection(_get_shift_codes, string='Day 4', size=1),
355+ 'day_5': fields.selection(_get_shift_codes, string='Day 5', size=1),
356+ 'day_6': fields.selection(_get_shift_codes, string='Day 6', size=1),
357+ 'day_7': fields.selection(_get_shift_codes, string='Day 7', size=1),
358+ 'day_8': fields.selection(_get_shift_codes, string='Day 8', size=1),
359+ 'day_9': fields.selection(_get_shift_codes, string='Day 9', size=1),
360+ 'day_10': fields.selection(_get_shift_codes, string='Day 10', size=1),
361+ 'day_11': fields.selection(_get_shift_codes, string='Day 1', size=1),
362+ 'day_12': fields.selection(_get_shift_codes, string='Day 12', size=1),
363+ 'day_13': fields.selection(_get_shift_codes, string='Day 13', size=1),
364+ 'day_14': fields.selection(_get_shift_codes, string='Day 14', size=1),
365+ 'day_15': fields.selection(_get_shift_codes, string='Day 15', size=1),
366+ 'day_16': fields.selection(_get_shift_codes, string='Day 16', size=1),
367+ 'day_17': fields.selection(_get_shift_codes, string='Day 17', size=1),
368+ 'day_18': fields.selection(_get_shift_codes, string='Day 18', size=1),
369+ 'day_19': fields.selection(_get_shift_codes, string='Day 19', size=1),
370+ 'day_20': fields.selection(_get_shift_codes, string='Day 20', size=1),
371+ 'day_21': fields.selection(_get_shift_codes, string='Day 21', size=1),
372+ 'day_22': fields.selection(_get_shift_codes, string='Day 22', size=1),
373+ 'day_23': fields.selection(_get_shift_codes, string='Day 23', size=1),
374+ 'day_24': fields.selection(_get_shift_codes, string='Day 24', size=1),
375+ 'day_25': fields.selection(_get_shift_codes, string='Day 25', size=1),
376+ 'day_26': fields.selection(_get_shift_codes, string='Day 26', size=1),
377+ 'day_27': fields.selection(_get_shift_codes, string='Day 27', size=1),
378+ 'day_28': fields.selection(_get_shift_codes, string='Day 28', size=1),
379+ 'day_29': fields.selection(_get_shift_codes, string='Day 29', size=1),
380+ 'day_30': fields.selection(_get_shift_codes, string='Day 30', size=1),
381+ 'day_31': fields.selection(_get_shift_codes, string='Day 31', size=1),
382+ }
383+
384+ _defaults = {
385+ 'days': _get_days_default,
386+ }
387+
388+ _sql_constraints = [
389+ ('shift_uniq', 'unique(duty_roster_id, employee_id)',
390+ 'Duplicate employee entries detected for this duty roster.')]
391+
392+
393+class hr_duty_roster(osv.Model):
394+ _name = 'hr.duty_roster'
395+
396+ def _get_date(self, cr, uid, ids, name, args, context=None):
397+ results = {}
398+ for row in self.read(cr, uid, ids, ['year', 'month'], context=context):
399+ row_id = row['id']
400+ results[row_id] = datetime(
401+ year=row['year'], month=row['month'], day=1).isoformat()
402+ return results
403+
404+ _columns = {
405+ 'name': fields.char('Name', size=60, required=True),
406+ 'month': fields.selection(MONTHS, 'Month', required=True),
407+ 'year': fields.integer('Year', required=True),
408+ 'date': fields.function(_get_date, type="datetime", method=True),
409+ 'state': fields.selection(
410+ [('new', 'New'), ('draft', 'Draft'), ('pending', 'Pending'),
411+ ('checked', 'Checked'), ('rejected', 'Rejected')], string="State"),
412+ 'shifts': fields.one2many(
413+ 'hr.duty_roster_shift', 'duty_roster_id', string="Shifts"),
414+ 'department_id': fields.many2one(
415+ 'hr.department', 'Department', required=True),
416+ 'checked_by_id': fields.many2one('hr.employee', 'Checked by'),
417+ 'checked_dt': fields.datetime('Checked at'),
418+ 'remarks': fields.text('Remarks')
419+ }
420+
421+ _defaults = {
422+ 'state': lambda self, cr, uid, context: 'new',
423+ 'month': lambda self, cr, uid, context: monthrange()[1],
424+ 'year': lambda self, cr, uid, context: monthrange()[0],
425+ }
426+
427+ def has_shifts(self, cr, uid, ids, context=None):
428+ return self.pool.get('hr.duty_roster_shift').search(
429+ cr, uid, [('duty_roster_id', 'in', ids)], context=context,
430+ count=True)
431+
432+ def is_checked(self, cr, uid, ids, context=None):
433+ return False
434+
435+ def action_draft(self, cr, uid, ids, context=None):
436+ self.write(cr, uid, ids, {'state': 'draft'}, context)
437+
438+ def action_pending(self, cr, uid, ids, context=None):
439+ self.write(cr, uid, ids, {'state': 'pending'}, context)
440+
441+ def action_checked(self, cr, uid, ids, context=None):
442+ self.write(cr, uid, ids, {'state': 'checked'}, context)
443+
444+ def action_rejected(self, cr, uid, ids, context=None):
445+ self.write(cr, uid, ids, {'state': 'rejected'}, context)
446+
447+ _sql_constraints = [
448+ ('uniq_duty_roster',
449+ 'unique(department_id, name, year, month)',
450+ 'Duty roster for the same department, month, year and name has already \
451+ been created.')]
452+
453+
454+class hr_shift_code(osv.Model):
455+ _rec_name = 'code'
456+ _name = 'hr.shift_code'
457+
458+ def _time_diff(self, time_in, time_out):
459+ """Returns the number of minutes if both time in and time out
460+ are given."""
461+
462+ duration = 0
463+ if time_out and time_in:
464+ time_in, time_out = [
465+ datetime.strptime(x, DEFAULT_SERVER_DATETIME_FORMAT) for x in (
466+ time_in, time_out)]
467+ duration = (time_out - time_in).seconds / 60
468+ return int(duration)
469+
470+ def _get_duration(self, cr, uid, ids, name, args, context=None):
471+ results = {}
472+ for row in self.read(cr, uid, ids, ['time_in', 'time_out'],
473+ context=context):
474+ row_id = row['id']
475+ time_in, time_out = row['time_in'], row['time_out']
476+ results[row_id] = self._time_diff(time_in, time_out)
477+ return results
478+
479+ def onchange_time(self, cr, uid, ids, time_in, time_out, context=None):
480+ ret = {}
481+ ret['value'] = {
482+ 'duration': self._time_diff(time_in, time_out)
483+ }
484+ return ret
485+
486+ _columns = {
487+ 'code': fields.char('Code', size=1, required=True),
488+ 'description': fields.char('Description', size=30),
489+ 'time_in': fields.datetime('Time In'),
490+ 'time_out': fields.datetime('Time Out'),
491+ 'break': fields.char('Break', size=8),
492+ 'duration': fields.function(_get_duration, string="Duration",
493+ type="integer", method=True),
494+ }
495+
496+ _defaults = {
497+ 'break': lambda self, cr, uid, context: '00:30:00',
498+ }
499
500=== added directory 'hr_roster/images'
501=== added file 'hr_roster/images/duty_roster.png'
502Binary files hr_roster/images/duty_roster.png 1970-01-01 00:00:00 +0000 and hr_roster/images/duty_roster.png 2014-05-07 07:38:43 +0000 differ
503=== added file 'hr_roster/images/duty_roster_err.png'
504Binary files hr_roster/images/duty_roster_err.png 1970-01-01 00:00:00 +0000 and hr_roster/images/duty_roster_err.png 2014-05-07 07:38:43 +0000 differ
505=== added file 'hr_roster/images/shift_code.png'
506Binary files hr_roster/images/shift_code.png 1970-01-01 00:00:00 +0000 and hr_roster/images/shift_code.png 2014-05-07 07:38:43 +0000 differ
507=== added file 'hr_roster/images/shift_code_tree.png'
508Binary files hr_roster/images/shift_code_tree.png 1970-01-01 00:00:00 +0000 and hr_roster/images/shift_code_tree.png 2014-05-07 07:38:43 +0000 differ
509=== added directory 'hr_roster/security'
510=== added file 'hr_roster/security/duty_roster_security.xml'
511--- hr_roster/security/duty_roster_security.xml 1970-01-01 00:00:00 +0000
512+++ hr_roster/security/duty_roster_security.xml 2014-05-07 07:38:43 +0000
513@@ -0,0 +1,15 @@
514+<?xml version="1.0" encoding="utf-8"?>
515+<openerp>
516+ <data noupdate="0">
517+ <record id="hr.group_duty_roster_user" model="res.groups">
518+ <field name="name">Duty Roster / Supervisor</field>
519+ <field name="category_id" ref="base.module_category_human_resources"/>
520+ <field name="comment">the user will have access to duty roster, shift codes and work days.</field>
521+ </record>
522+ <record id="hr.group_duty_roster_manager" model="res.groups">
523+ <field name="name">Duty Roster / Manager</field>
524+ <field name="category_id" ref="base.module_category_human_resources"/>
525+ <field name="comment">the user can approve duty roster.</field>
526+ </record>
527+ </data>
528+</openerp>
529
530=== added file 'hr_roster/security/ir.model.access.csv'
531--- hr_roster/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
532+++ hr_roster/security/ir.model.access.csv 2014-05-07 07:38:43 +0000
533@@ -0,0 +1,9 @@
534+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
535+access_shift_code_manager,hr.shift_code_manager,model_hr_shift_code,base.group_hr_user,1,1,1,1
536+access_shift_code_manager,hr.shift_code_manager,model_hr_shift_code,hr.group_duty_roster_manager,1,0,0,0
537+access_shift_code_user,hr.shift_code_user,model_hr_shift_code,base.group_user,1,0,0,0
538+access_duty_roster_manager,hr.duty_roster_manager,model_hr_duty_roster,hr.group_duty_roster_manager,1,1,1,1
539+access_duty_roster_user,hr.duty_roster_user,model_hr_duty_roster,hr.group_duty_roster_user,1,1,1,1
540+access_duty_roster_shift_manager,hr.duty_roster_shift_manager,model_hr_duty_roster_shift,hr.group_duty_roster_manager,1,1,1,1
541+access_duty_roster_shift_user,hr.duty_roster_shift_user,model_hr_duty_roster_shift,hr.group_duty_roster_user,1,1,1,1
542+access_duty_roster,hr.duty_roster,model_hr_duty_roster,base.group_user,1,0,0,0
543
544=== added directory 'hr_roster/static'
545=== added directory 'hr_roster/static/src'
546=== added directory 'hr_roster/static/src/css'
547=== added file 'hr_roster/static/src/css/hr_roster.css'
548--- hr_roster/static/src/css/hr_roster.css 1970-01-01 00:00:00 +0000
549+++ hr_roster/static/src/css/hr_roster.css 2014-05-07 07:38:43 +0000
550@@ -0,0 +1,20 @@
551+.openerp .oe_list_content td, .openerp .oe_list_content th {
552+ line-height: normal;
553+}
554+
555+.oe_list.shift_day_list [data-id=employee_id] {
556+ width: 30em;
557+ vertical-align: top;
558+}
559+
560+.oe_list_header_shift_day {
561+ font-size: 0.75em;
562+ vertical-align: top;
563+ width: 24px;
564+ padding: 3px 0 0 3px !important;
565+ text-align: center !important;
566+}
567+
568+.hr_shift_code_col {
569+ font-weight: bold;
570+}
571
572=== added directory 'hr_roster/static/src/js'
573=== added file 'hr_roster/static/src/js/hr_roster.js'
574--- hr_roster/static/src/js/hr_roster.js 1970-01-01 00:00:00 +0000
575+++ hr_roster/static/src/js/hr_roster.js 2014-05-07 07:38:43 +0000
576@@ -0,0 +1,208 @@
577+openerp.hr_roster = function(instance) {
578+ var _t = instance.web._t,
579+ _lt = instance.web._lt;
580+ var QWeb = instance.web.qweb;
581+
582+ var humanize_time = function(mins){
583+ var mins = Number.parseInt(mins),
584+ h = instance.web.format_value(Number.parseInt(mins / 60), this, 0),
585+ m = instance.web.format_value(mins % 60, this, 0);
586+
587+ var h_text = "", m_text = "";
588+ if(h > 0){
589+ h_text = h + " hour";
590+ if(h > 1) h_text += "s";
591+ }
592+ if(m > 0){
593+ m_text = m + " minute";
594+ if(m > 1) m_text += "s";
595+ }
596+ return _.string.sprintf("%s %s", h_text, m_text);
597+ };
598+
599+ // BEGIN hr_duty_roster
600+ instance.hr_duty_roster = {};
601+
602+ instance.hr_duty_roster.FieldShiftDay = instance.web.form.FieldChar.extend({
603+ is_false: function(){
604+ var selection = this.field_manager.get_field_desc(this.name).selection,
605+ v = this.get_value();
606+
607+ var r = this._super() || !(_.any(selection, function(choice, index, list){
608+ return choice[0] == v;
609+ }));
610+ return r;
611+ },
612+ start: function() {
613+ var self = this;
614+ self._super();
615+ self.$('input').on('keyup', function(e){
616+ var $i = $(this),
617+ v = $i.val().trim().toUpperCase();
618+ if($i.val() != v){
619+ $i.val(v);
620+
621+ $i.blur();
622+ self.$el.next().find('input:first').focus()
623+ }
624+ });
625+ }
626+ });
627+
628+ instance.hr_duty_roster.FieldEmployeesFilter = instance.web.form.FieldMany2One.extend({
629+ get_search_blacklist: function() {
630+ // WARNING: i am not sure if this will work for future v8
631+ var blacklist = _.reduce(this.field_manager.dataset.cache, function(memo, row){
632+ var employee_id = row.values.employee_id;
633+ // if a record is loaded from db, the employee_id is a tuple in the form of (id, name)
634+ // otherwise, it is just an id (integer)
635+ if(_.isArray(employee_id)){
636+ employee_id = employee_id[0];
637+ }
638+ memo.push(employee_id);
639+ return memo;
640+ }, []);
641+
642+ return blacklist;
643+ }
644+ });
645+
646+ instance.web.form.widgets.add('shift_day', 'instance.hr_duty_roster.FieldShiftDay');
647+ instance.web.form.widgets.add('employees_filter', 'instance.hr_duty_roster.FieldEmployeesFilter');
648+
649+ // we have to use customize _format for this column, or otherwise the value that is rendered into the cell
650+ // has an "id," prepended to it
651+ instance.hr_duty_roster.EmployeesFilterColumn = instance.web.list.Column.extend({
652+ _format: function(row_data, options){
653+ var value = row_data.employee_id.value;
654+ return value[1] ? value[1].split("\n")[0] : value[1];
655+ }
656+ });
657+
658+ instance.web.list.columns.add('field.employees_filter', 'instance.hr_duty_roster.EmployeesFilterColumn');
659+
660+ instance.hr_duty_roster.WidgetDatePicker = instance.web.form.FormWidget.extend({
661+ renderElement: function(){
662+ var today = new Date(), m = 1, y = 2014, d = 1;
663+ if(typeof this.field_manager.get_field_desc('month') === 'undefined'){
664+ m = today.getMonth();
665+ }
666+ else{
667+ m = this.field_manager.get_field_value('month') - 1; // Python month is 1-base
668+ }
669+
670+ if(typeof this.field_manager.get_field_desc('year') === 'undefined'){
671+ y = today.getFullYear();
672+ }
673+ else{
674+ y = this.field_manager.get_field_value('year');
675+ }
676+
677+ if(today.getFullYear() === y && today.getMonth() === m) d = today.day;
678+
679+ this.$el.html(QWeb.render('DatePickerWidget', {_id: 'hr_duty_roster_dp' + _.uniqueId(), year: y, month: m, day: d}));
680+ }
681+ });
682+
683+ instance.hr_duty_roster.WidgetShiftCodeHelp = instance.web.form.FormWidget.extend({
684+ renderElement: function(){
685+ var self = this,
686+ model = new instance.web.Model("hr.shift_code");
687+
688+ model.query(['code', 'time_in', 'time_out', 'duration', 'break', 'description']).all().then(function(rows){
689+ _.map(rows, function(v, k, list){
690+ list[k]['duration_text'] = humanize_time(v['duration']);
691+ list[k]['time_in'] = instance.web.format_value(v['time_in'], {'widget': 'time'});
692+ list[k]['time_out'] = instance.web.format_value(v['time_out'], {'widget': 'time'});
693+ });
694+ var out = QWeb.render('ShiftCodeHelpWidget', {'selection': rows});
695+ self.$el.html(out);
696+ });
697+ }
698+ });
699+
700+ instance.web.form.custom_widgets.add('datepicker', 'instance.hr_duty_roster.WidgetDatePicker');
701+ instance.web.form.custom_widgets.add('shiftcodehelp', 'instance.hr_duty_roster.WidgetShiftCodeHelp');
702+ // --- END hr_duty_roster
703+
704+
705+ // -- BEGIN hr_shift_code
706+ instance.hr_shift_code = {};
707+
708+ instance.hr_shift_code.FieldDuration = instance.web.form.FieldChar.extend({
709+ render_value: function(){
710+ this.$el.text(humanize_time(this.get_value()));
711+ }
712+ });
713+
714+ instance.web.form.widgets.add('duration_mins', 'instance.hr_shift_code.FieldDuration');
715+
716+ instance.hr_shift_code.TimeWidget = instance.web.DateTimeWidget.extend({
717+ jqueryui_object: "timepicker",
718+ type_of_date: "time",
719+ picker: function() {
720+ if(arguments[0] === 'setDate' && _.isEmpty(this.get('value'))){
721+ // NOTE: this is a hack - default time (now) is almost always useless.
722+ var args = Array.prototype.slice.call(arguments),
723+ time = args[1];
724+ time.setHours(0);
725+ time.setMinutes(0);
726+ time.setSeconds(0);
727+ return $.fn[this.jqueryui_object].apply(this.$input_picker, args);
728+ }
729+ return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
730+ },
731+ on_picker_select: function(text, instance_) {
732+ // NOTE: as of jQuery timepicker v0.9.9, retriving time from timepicker with getDate always returns jQuery object
733+ var time_str = this.picker('getDate').val(), // this.picker is a timepicker
734+ val = instance.web.str_to_time(time_str + ":00"); // seconds not returned
735+
736+ this.$input
737+ .val(val ? this.format_client(val) : '')
738+ .change()
739+ .focus();
740+ },
741+ parse_client: function(v) {
742+ return instance.web.parse_value(v, {"widget": 'datetime'});
743+ },
744+ format_client: function(v) {
745+ return instance.web.format_value(v, {"widget": 'time'});
746+ }
747+ });
748+
749+ instance.hr_shift_code.FieldTimePicker = instance.web.form.FieldDatetime.extend({
750+ build_widget: function() {
751+ return new instance.hr_shift_code.TimeWidget(this);
752+ },
753+ render_value: function() {
754+ if (!this.get("effective_readonly")) {
755+ this.datewidget.set_value(this.get('value'));
756+ } else {
757+ this.$el.text(instance.web.format_value(this.get('value'), {'widget': 'time'}, ''));
758+ }
759+ }
760+ });
761+
762+ instance.web.form.widgets.add('timepicker', 'instance.hr_shift_code.FieldTimePicker');
763+
764+ // custom columns used by shift code module
765+ instance.hr_shift_code.DurationMinsColumn = instance.web.list.Column.extend({
766+ _format: function(row_data, options){
767+ return _.escape(instance.web.format_value(
768+ humanize_time(row_data[this.id].value), this, options.value_if_empty));
769+ }
770+ });
771+
772+ instance.hr_shift_code.TimeColumn = instance.web.list.Column.extend({
773+ _format: function(row_data, options){
774+ return _.escape(instance.web.format_value(
775+ instance.web.format_value(row_data[this.id].value, {'widget': 'time'}),
776+ this,
777+ options.value_if_empty));
778+ }
779+ });
780+
781+ instance.web.list.columns.add('field.duration_mins', 'instance.hr_shift_code.DurationMinsColumn');
782+ instance.web.list.columns.add('field.time', 'instance.hr_shift_code.TimeColumn');
783+ // --- END hr_shift_code
784+}
785
786=== added directory 'hr_roster/static/src/xml'
787=== added file 'hr_roster/static/src/xml/hr_roster.xml'
788--- hr_roster/static/src/xml/hr_roster.xml 1970-01-01 00:00:00 +0000
789+++ hr_roster/static/src/xml/hr_roster.xml 2014-05-07 07:38:43 +0000
790@@ -0,0 +1,25 @@
791+<?xml version="1.0" encoding="UTF-8"?>
792+
793+<templates xml:space="preserve">
794+ <t t-name="DatePickerWidget">
795+ <div t-att-id="_id" class="datepicker"></div>
796+ <script type="text/javascript">
797+ setTimeout(function(){
798+ $('#<t t-raw="_id"/>').datepicker({'onSelect': function(){}, 'defaultDate': new Date(<t t-raw="year"/>, <t t-raw="month"/>, <t t-raw="day"/>)});
799+ }, 0);
800+ </script>
801+ </t>
802+
803+ <t t-name="ShiftField">
804+ <span class="oe_form_field hr_roster_month_shift">
805+ <t t-foreach="widget.get('value').length" t-as="day">
806+ <t t-if="! widget.get('readonly')">
807+ <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]"></input>
808+ </t>
809+ <t t-if="widget.get('readonly')">
810+ <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]" readonly="readonly"></input>
811+ </t>
812+ </t>
813+ </span>
814+ </t>
815+</templates>
816
817=== added file 'hr_roster/static/src/xml/hr_shift_code.xml'
818--- hr_roster/static/src/xml/hr_shift_code.xml 1970-01-01 00:00:00 +0000
819+++ hr_roster/static/src/xml/hr_shift_code.xml 2014-05-07 07:38:43 +0000
820@@ -0,0 +1,46 @@
821+<?xml version="1.0" encoding="UTF-8"?>
822+
823+<templates xml:space="preserve">
824+ <t t-name="ShiftCodeHelpWidget">
825+ <table class="oe_list_content">
826+ <thead>
827+ <tr>
828+ <th>Code</th>
829+ <th>Description</th>
830+ <th>Time In - Time Out</th>
831+ <th>Duration</th>
832+ <th>Break</th>
833+ </tr>
834+ </thead>
835+ <tbody>
836+ <t t-foreach="selection" t-as="choice">
837+ <tr>
838+ <td class="hr_shift_code_col">
839+ <t t-esc="choice.code"></t>
840+ </td>
841+ <td>
842+ <t t-if="choice.description">
843+ <t t-esc="choice.description"></t>
844+ </t>
845+ </td>
846+ <td>
847+ <t t-if="choice.time_in and choice.time_out">
848+ <t t-esc="choice.time_in"></t> - <t t-esc="choice.time_out"></t>
849+ </t>
850+ </td>
851+ <td>
852+ <t t-if="choice.duration > 0">
853+ <t t-esc="choice.duration_text"></t>
854+ </t>
855+ </td>
856+ <td>
857+ <t t-if="choice.break">
858+ <t t-esc="choice.break"></t>
859+ </t>
860+ </td>
861+ </tr>
862+ </t>
863+ </tbody>
864+ </table>
865+ </t>
866+</templates>

Subscribers

People subscribed via source and target branches