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
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,1 @@
1import hr_roster
02
=== added file 'hr_roster/__openerp__.py'
--- hr_roster/__openerp__.py 1970-01-01 00:00:00 +0000
+++ hr_roster/__openerp__.py 2014-05-07 07:38:43 +0000
@@ -0,0 +1,41 @@
1{
2 'name': 'Duty Roster',
3 'version': '0.1',
4 'category': 'Human Resources',
5 'description': """
6Duty Roster
7===========
8
9This is a generic module to allow easy management of staff duty roster.
10It should be used together with hr_holidays such that leaves application
11that spans multiple days takes into account of half/off duty days.
12""",
13 'author': "CODEKAKI SYSTEMS (R49045/14)",
14 'website': 'http://codekaki.com',
15 'depends': ['hr'],
16 'images': [
17 'images/shift_code_tree.png',
18 'images/shift_code.png',
19 'images/duty_roster.png',
20 'images/duty_roster_err.png',
21 ],
22 'data': [
23 'security/duty_roster_security.xml',
24 'security/ir.model.access.csv',
25 'duty_roster_workflow.xml',
26 'duty_roster_view.xml',
27 ],
28 'css': [
29 'static/src/css/hr_roster.css',
30 ],
31 'js': [
32 'static/src/js/hr_roster.js',
33 ],
34 'qweb': [
35 'static/src/xml/hr_shift_code.xml',
36 'static/src/xml/hr_roster.xml',
37 ],
38 'demo': [],
39 'test': [],
40 'installable': True,
41}
042
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,149 @@
1<?xml version="1.0"?>
2<openerp>
3 <data>
4 <record id="view_duty_roster" model="ir.ui.view">
5 <field name="model">hr.duty_roster</field>
6 <field name="type">form</field>
7 <field name="arch" type="xml">
8 <form string="Duty Roster" version="7.0">
9 <header>
10 <button name="confirm" string="Submit for Approval" class="oe_highlight" attrs="{'invisible': ['|', ('state', 'not in', ['draft']), ('shifts', '=', [])]}" groups="hr.group_duty_roster_user"></button>
11 <button name="resubmit" string="Resubmit" class="oe_highlight" attrs="{'invisible': ['|', ('state', 'not in', ['rejected']), ('shifts', '=', [])]}" groups="hr.group_duty_roster_user"></button>
12 <button name="checked" string="Approve" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ['pending'])]}" groups="hr.group_duty_roster_manager"></button>
13 <button name="rejected" string="Reject" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ['pending'])]}" groups="hr.group_duty_roster_manager"></button>
14 <field name="state" widget="statusbar" statusbar_visible="draft,pending,checked"/>
15 </header>
16 <group col="4">
17 <group>
18 <field name="department_id" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
19 <field name="name" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
20 </group>
21 <group>
22 <field name="month" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
23 <field name="year" attrs="{'readonly': [('state', 'not in', ['new'])]}"></field>
24 </group>
25 <widget type="datepicker"></widget>
26 </group>
27 <group>
28 <field name="remarks" placeholder="Remarks..."></field>
29 </group>
30 <notebook>
31 <page string="Shifts">
32 <field name="shifts" attrs="{'readonly': [('state', 'in', ['new', 'pending', 'checked'])]}" context="{'active_id': active_id}">
33 <tree string="Shifts" editable="bottom" class="shift_day_list">
34 <field name="employee_id" required="1" domain="[('department_id', '=', parent.department_id)]" widget="employees_filter"></field>
35 <field name="days" invisible="1" readonly="1"></field>
36 <field name="day_1" widget="shift_day" string="1" attrs="{'required': [('days', '>=', 1)]}"></field>
37 <field name="day_2" widget="shift_day" string="2" attrs="{'required': [('days', '>=', 2)]}"></field>
38 <field name="day_3" widget="shift_day" string="3" attrs="{'required': [('days', '>=', 3)]}"></field>
39 <field name="day_4" widget="shift_day" string="4" attrs="{'required': [('days', '>=', 4)]}"></field>
40 <field name="day_5" widget="shift_day" string="5" attrs="{'required': [('days', '>=', 5)]}"></field>
41 <field name="day_6" widget="shift_day" string="6" attrs="{'required': [('days', '>=', 6)]}"></field>
42 <field name="day_7" widget="shift_day" string="7" attrs="{'required': [('days', '>=', 6)]}"></field>
43 <field name="day_8" widget="shift_day" string="8" attrs="{'required': [('days', '>=', 8)]}"></field>
44 <field name="day_9" widget="shift_day" string="9" attrs="{'required': [('days', '>=', 9)]}"></field>
45 <field name="day_10" widget="shift_day" string="10" attrs="{'required': [('days', '>=', 10)]}"></field>
46 <field name="day_11" widget="shift_day" string="11" attrs="{'required': [('days', '>=', 11)]}"></field>
47 <field name="day_12" widget="shift_day" string="12" attrs="{'required': [('days', '>=', 12)]}"></field>
48 <field name="day_13" widget="shift_day" string="13" attrs="{'required': [('days', '>=', 13)]}"></field>
49 <field name="day_14" widget="shift_day" string="14" attrs="{'required': [('days', '>=', 14)]}"></field>
50 <field name="day_15" widget="shift_day" string="15" attrs="{'required': [('days', '>=', 15)]}"></field>
51 <field name="day_16" widget="shift_day" string="16" attrs="{'required': [('days', '>=', 16)]}"></field>
52 <field name="day_17" widget="shift_day" string="17" attrs="{'required': [('days', '>=', 17)]}"></field>
53 <field name="day_18" widget="shift_day" string="18" attrs="{'required': [('days', '>=', 18)]}"></field>
54 <field name="day_19" widget="shift_day" string="19" attrs="{'required': [('days', '>=', 19)]}"></field>
55 <field name="day_20" widget="shift_day" string="20" attrs="{'required': [('days', '>=', 20)]}"></field>
56 <field name="day_21" widget="shift_day" string="21" attrs="{'required': [('days', '>=', 21)]}"></field>
57 <field name="day_22" widget="shift_day" string="22" attrs="{'required': [('days', '>=', 22)]}"></field>
58 <field name="day_23" widget="shift_day" string="23" attrs="{'required': [('days', '>=', 23)]}"></field>
59 <field name="day_24" widget="shift_day" string="24" attrs="{'required': [('days', '>=', 24)]}"></field>
60 <field name="day_25" widget="shift_day" string="25" attrs="{'required': [('days', '>=', 25)]}"></field>
61 <field name="day_26" widget="shift_day" string="26" attrs="{'required': [('days', '>=', 26)]}"></field>
62 <field name="day_27" widget="shift_day" string="27" attrs="{'required': [('days', '>=', 27)]}"></field>
63 <field name="day_28" widget="shift_day" string="28" attrs="{'required': [('days', '>=', 28)]}"></field>
64 <field name="day_29" widget="shift_day" string="29" attrs="{'readonly': [('days', '&lt;', 29)], 'required': [('days', '>=', 29)]}"></field>
65 <field name="day_30" widget="shift_day" string="30" attrs="{'readonly': [('days', '&lt;', 30)], 'required': [('days', '>=', 30)]}"></field>
66 <field name="day_31" widget="shift_day" string="31" attrs="{'readonly': [('days', '&lt;', 31)], 'required': [('days', '>=', 31)]}"></field>
67 </tree>
68 </field>
69 </page>
70 </notebook>
71 <group>
72 <widget type="shiftcodehelp"></widget>
73 <field name="checked_by_id" readonly="1" attrs="{'invisible': [('state', '!=', 'checked')]}"></field>
74 </group>
75 </form>
76 </field>
77 </record>
78
79 <record id="view_duty_roster_tree" model="ir.ui.view">
80 <field name="model">hr.duty_roster</field>
81 <field name="type">tree</field>
82 <field name="arch" type="xml">
83 <tree string="Duty Rosters">
84 <field name="department_id"></field>
85 <field name="name"></field>
86 <field name="year"></field>
87 <field name="month"></field>
88 <field name="state"></field>
89 </tree>
90 </field>
91 </record>
92
93 <record id="view_hr_shift_code_tree" model="ir.ui.view">
94 <field name="model">hr.shift_code</field>
95 <field name="type">tree</field>
96 <field name="arch" type="xml">
97 <tree string="Shift Codes">
98 <field name="code"></field>
99 <field name="time_in" widget="time"></field>
100 <field name="time_out" widget="time"></field>
101 <field name="duration" widget="duration_mins"></field>
102 <field name="break"></field>
103 <field name="description"></field>
104 </tree>
105 </field>
106 </record>
107
108 <record id="view_hr_shift_code" model="ir.ui.view">
109 <field name="model">hr.shift_code</field>
110 <field name="type">form</field>
111 <field name="arch" type="xml">
112 <form string="Shift Code" version="7.0">
113 <sheet>
114 <group col="4">
115 <group name="info">
116 <field name="code"></field>
117 <field name="description" widget="text"></field>
118 </group>
119 <group name="time">
120 <field name="time_in" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
121 <field name="time_out" widget="timepicker" on_change="onchange_time(time_in, time_out)"></field>
122 <field name="duration" readonly="1" widget="duration_mins"></field>
123 <field name="break" widget="timepicker"></field>
124 </group>
125 </group>
126 </sheet>
127 </form>
128 </field>
129 </record>
130
131 <record model="ir.actions.act_window" id="action_hr_shift_code">
132 <field name="name">Shift Code</field>
133 <field name="res_model">hr.shift_code</field>
134 <field name="view_type">form</field>
135 <field name="view_mode">tree,form</field>
136 </record>
137
138 <record model="ir.actions.act_window" id="action_duty_roster">
139 <field name="name">Duty Roster</field>
140 <field name="res_model">hr.duty_roster</field>
141 <field name="view_type">form</field>
142 <field name="view_mode">tree,form</field>
143 </record>
144
145 <menuitem id="menu_duty_roster_root" name="Duty Rosters" action="" parent="hr.menu_hr_root" groups="base.group_user"></menuitem>
146 <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>
147 <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>
148 </data>
149</openerp>
0150
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,71 @@
1<openerp>
2 <data>
3 <record id="wkf_duty_roster" model="workflow">
4 <field name="name">hr.duty_roster.basic</field>
5 <field name="osv">hr.duty_roster</field>
6 <field name="on_create">True</field>
7 </record>
8
9 <!-- activities -->
10 <record id="act_new" model="workflow.activity">
11 <field name="wkf_id" ref="wkf_duty_roster"/>
12 <field name="flow_start">True</field>
13 <field name="name">new</field>
14 </record>
15 <record id="act_draft" model="workflow.activity">
16 <field name="wkf_id" ref="wkf_duty_roster"/>
17 <field name="name">draft</field>
18 <field name="kind">function</field>
19 <field name="action">action_draft()</field>
20 </record>
21 <record id="act_pending" model="workflow.activity">
22 <field name="wkf_id" ref="wkf_duty_roster"/>
23 <field name="name">pending</field>
24 <field name="kind">function</field>
25 <field name="action">action_pending()</field>
26 </record>
27 <record id="act_checked" model="workflow.activity">
28 <field name="wkf_id" ref="wkf_duty_roster"/>
29 <field name="flow_end">True</field>
30 <field name="name">checked</field>
31 <field name="kind">function</field>
32 <field name="action">action_checked()</field>
33 </record>
34
35 <record id="act_rejected" model="workflow.activity">
36 <field name="wkf_id" ref="wkf_duty_roster"/>
37 <field name="name">rejected</field>
38 <field name="kind">function</field>
39 <field name="action">action_rejected()</field>
40 </record>
41
42 <!-- transitions -->
43 <record id="trans_new_draft" model="workflow.transition">
44 <field name="act_from" ref="act_new"/>
45 <field name="act_to" ref="act_draft"/>
46 <field name="condition">True</field>
47 </record>
48 <record id="trans_draft_pending" model="workflow.transition">
49 <field name="act_from" ref="act_draft"/>
50 <field name="act_to" ref="act_pending"/>
51 <field name="condition">has_shifts()</field>
52 <field name="signal">confirm</field>
53 </record>
54 <record id="trans_pending_checked" model="workflow.transition">
55 <field name="act_from" ref="act_pending"/>
56 <field name="act_to" ref="act_checked"/>
57 <field name="signal">checked</field>
58 </record>
59 <record id="trans_pending_rejected" model="workflow.transition">
60 <field name="act_from" ref="act_pending"/>
61 <field name="act_to" ref="act_rejected"/>
62 <field name="signal">rejected</field>
63 </record>
64 <record id="trans_reject_pending" model="workflow.transition">
65 <field name="act_from" ref="act_rejected"/>
66 <field name="act_to" ref="act_pending"/>
67 <field name="signal">resubmit</field>
68 </record>
69 </data>
70</openerp>
71
072
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,211 @@
1from datetime import datetime
2from openerp.osv import fields, osv
3from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
4import calendar
5import logging
6
7_logger = logging.getLogger(__name__)
8
9MONTHS = zip(range(1, 13), calendar.month_name[1:])
10
11
12def monthrange(year=None, month=None):
13 today = datetime.today()
14 y = year or today.year
15 m = month or today.month
16 return y, m, calendar.monthrange(y, m)[1]
17
18
19class hr_duty_roster_shift(osv.Model):
20 _name = 'hr.duty_roster_shift'
21
22 def _get_days_default(self, cr, uid, context=None):
23 if 'active_id' in context:
24 active_id = context['active_id']
25 row = self.pool.get('hr.duty_roster').read(cr, uid, active_id, [
26 'year', 'month'])
27 mrange = calendar.monthrange(row['year'], row['month'])
28 return mrange[1]
29 return 0
30
31 def _get_days(self, cr, uid, ids, field_name, args=None, context=None):
32 res = {}
33 if ids:
34 parent_row = self.read(cr, uid, ids[0], ['duty_roster_id'],
35 context=context)
36 row = self.pool.get('hr.duty_roster').read(
37 cr,
38 uid,
39 parent_row['duty_roster_id'][0],
40 ['year', 'month'],
41 context=context)
42 mrange = calendar.monthrange(row['year'], row['month'])
43 for _id in ids:
44 res[_id] = mrange[1]
45 return res
46
47 def _get_shift_codes(self, cr, uid, context=None):
48 context = context or {}
49 if 'shift_codes' not in context:
50 model = self.pool.get('hr.shift_code')
51 ids = model.search(cr, uid, [], order="code", context=context)
52 ret = model.read(cr, uid, ids, ['code'], context=context)
53 context['shift_codes'] = [(r['code'], r['code']) for r in ret]
54 return context['shift_codes']
55
56 _columns = {
57 'duty_roster_id': fields.many2one(
58 'hr.duty_roster', 'Duty Roster', required=True,
59 ondelete="cascade"),
60 'employee_id': fields.many2one(
61 'hr.employee', 'Employee', required=True),
62 'days': fields.function(
63 _get_days, type="integer", string='Days', required=True),
64 'day_1': fields.selection(_get_shift_codes, string='Day 1', size=1),
65 'day_2': fields.selection(_get_shift_codes, string='Day 2', size=1),
66 'day_3': fields.selection(_get_shift_codes, string='Day 3', size=1),
67 'day_4': fields.selection(_get_shift_codes, string='Day 4', size=1),
68 'day_5': fields.selection(_get_shift_codes, string='Day 5', size=1),
69 'day_6': fields.selection(_get_shift_codes, string='Day 6', size=1),
70 'day_7': fields.selection(_get_shift_codes, string='Day 7', size=1),
71 'day_8': fields.selection(_get_shift_codes, string='Day 8', size=1),
72 'day_9': fields.selection(_get_shift_codes, string='Day 9', size=1),
73 'day_10': fields.selection(_get_shift_codes, string='Day 10', size=1),
74 'day_11': fields.selection(_get_shift_codes, string='Day 1', size=1),
75 'day_12': fields.selection(_get_shift_codes, string='Day 12', size=1),
76 'day_13': fields.selection(_get_shift_codes, string='Day 13', size=1),
77 'day_14': fields.selection(_get_shift_codes, string='Day 14', size=1),
78 'day_15': fields.selection(_get_shift_codes, string='Day 15', size=1),
79 'day_16': fields.selection(_get_shift_codes, string='Day 16', size=1),
80 'day_17': fields.selection(_get_shift_codes, string='Day 17', size=1),
81 'day_18': fields.selection(_get_shift_codes, string='Day 18', size=1),
82 'day_19': fields.selection(_get_shift_codes, string='Day 19', size=1),
83 'day_20': fields.selection(_get_shift_codes, string='Day 20', size=1),
84 'day_21': fields.selection(_get_shift_codes, string='Day 21', size=1),
85 'day_22': fields.selection(_get_shift_codes, string='Day 22', size=1),
86 'day_23': fields.selection(_get_shift_codes, string='Day 23', size=1),
87 'day_24': fields.selection(_get_shift_codes, string='Day 24', size=1),
88 'day_25': fields.selection(_get_shift_codes, string='Day 25', size=1),
89 'day_26': fields.selection(_get_shift_codes, string='Day 26', size=1),
90 'day_27': fields.selection(_get_shift_codes, string='Day 27', size=1),
91 'day_28': fields.selection(_get_shift_codes, string='Day 28', size=1),
92 'day_29': fields.selection(_get_shift_codes, string='Day 29', size=1),
93 'day_30': fields.selection(_get_shift_codes, string='Day 30', size=1),
94 'day_31': fields.selection(_get_shift_codes, string='Day 31', size=1),
95 }
96
97 _defaults = {
98 'days': _get_days_default,
99 }
100
101 _sql_constraints = [
102 ('shift_uniq', 'unique(duty_roster_id, employee_id)',
103 'Duplicate employee entries detected for this duty roster.')]
104
105
106class hr_duty_roster(osv.Model):
107 _name = 'hr.duty_roster'
108
109 def _get_date(self, cr, uid, ids, name, args, context=None):
110 results = {}
111 for row in self.read(cr, uid, ids, ['year', 'month'], context=context):
112 row_id = row['id']
113 results[row_id] = datetime(
114 year=row['year'], month=row['month'], day=1).isoformat()
115 return results
116
117 _columns = {
118 'name': fields.char('Name', size=60, required=True),
119 'month': fields.selection(MONTHS, 'Month', required=True),
120 'year': fields.integer('Year', required=True),
121 'date': fields.function(_get_date, type="datetime", method=True),
122 'state': fields.selection(
123 [('new', 'New'), ('draft', 'Draft'), ('pending', 'Pending'),
124 ('checked', 'Checked'), ('rejected', 'Rejected')], string="State"),
125 'shifts': fields.one2many(
126 'hr.duty_roster_shift', 'duty_roster_id', string="Shifts"),
127 'department_id': fields.many2one(
128 'hr.department', 'Department', required=True),
129 'checked_by_id': fields.many2one('hr.employee', 'Checked by'),
130 'checked_dt': fields.datetime('Checked at'),
131 'remarks': fields.text('Remarks')
132 }
133
134 _defaults = {
135 'state': lambda self, cr, uid, context: 'new',
136 'month': lambda self, cr, uid, context: monthrange()[1],
137 'year': lambda self, cr, uid, context: monthrange()[0],
138 }
139
140 def has_shifts(self, cr, uid, ids, context=None):
141 return self.pool.get('hr.duty_roster_shift').search(
142 cr, uid, [('duty_roster_id', 'in', ids)], context=context,
143 count=True)
144
145 def is_checked(self, cr, uid, ids, context=None):
146 return False
147
148 def action_draft(self, cr, uid, ids, context=None):
149 self.write(cr, uid, ids, {'state': 'draft'}, context)
150
151 def action_pending(self, cr, uid, ids, context=None):
152 self.write(cr, uid, ids, {'state': 'pending'}, context)
153
154 def action_checked(self, cr, uid, ids, context=None):
155 self.write(cr, uid, ids, {'state': 'checked'}, context)
156
157 def action_rejected(self, cr, uid, ids, context=None):
158 self.write(cr, uid, ids, {'state': 'rejected'}, context)
159
160 _sql_constraints = [
161 ('uniq_duty_roster',
162 'unique(department_id, name, year, month)',
163 'Duty roster for the same department, month, year and name has already \
164 been created.')]
165
166
167class hr_shift_code(osv.Model):
168 _rec_name = 'code'
169 _name = 'hr.shift_code'
170
171 def _time_diff(self, time_in, time_out):
172 """Returns the number of minutes if both time in and time out
173 are given."""
174
175 duration = 0
176 if time_out and time_in:
177 time_in, time_out = [
178 datetime.strptime(x, DEFAULT_SERVER_DATETIME_FORMAT) for x in (
179 time_in, time_out)]
180 duration = (time_out - time_in).seconds / 60
181 return int(duration)
182
183 def _get_duration(self, cr, uid, ids, name, args, context=None):
184 results = {}
185 for row in self.read(cr, uid, ids, ['time_in', 'time_out'],
186 context=context):
187 row_id = row['id']
188 time_in, time_out = row['time_in'], row['time_out']
189 results[row_id] = self._time_diff(time_in, time_out)
190 return results
191
192 def onchange_time(self, cr, uid, ids, time_in, time_out, context=None):
193 ret = {}
194 ret['value'] = {
195 'duration': self._time_diff(time_in, time_out)
196 }
197 return ret
198
199 _columns = {
200 'code': fields.char('Code', size=1, required=True),
201 'description': fields.char('Description', size=30),
202 'time_in': fields.datetime('Time In'),
203 'time_out': fields.datetime('Time Out'),
204 'break': fields.char('Break', size=8),
205 'duration': fields.function(_get_duration, string="Duration",
206 type="integer", method=True),
207 }
208
209 _defaults = {
210 'break': lambda self, cr, uid, context: '00:30:00',
211 }
0212
=== added directory 'hr_roster/images'
=== added file 'hr_roster/images/duty_roster.png'
1Binary 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 differ213Binary 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
=== added file 'hr_roster/images/duty_roster_err.png'
2Binary 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 differ214Binary 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
=== added file 'hr_roster/images/shift_code.png'
3Binary 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 differ215Binary 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
=== added file 'hr_roster/images/shift_code_tree.png'
4Binary 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 differ216Binary 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
=== added directory 'hr_roster/security'
=== added file 'hr_roster/security/duty_roster_security.xml'
--- hr_roster/security/duty_roster_security.xml 1970-01-01 00:00:00 +0000
+++ hr_roster/security/duty_roster_security.xml 2014-05-07 07:38:43 +0000
@@ -0,0 +1,15 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4 <record id="hr.group_duty_roster_user" model="res.groups">
5 <field name="name">Duty Roster / Supervisor</field>
6 <field name="category_id" ref="base.module_category_human_resources"/>
7 <field name="comment">the user will have access to duty roster, shift codes and work days.</field>
8 </record>
9 <record id="hr.group_duty_roster_manager" model="res.groups">
10 <field name="name">Duty Roster / Manager</field>
11 <field name="category_id" ref="base.module_category_human_resources"/>
12 <field name="comment">the user can approve duty roster.</field>
13 </record>
14 </data>
15</openerp>
016
=== added file 'hr_roster/security/ir.model.access.csv'
--- hr_roster/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ hr_roster/security/ir.model.access.csv 2014-05-07 07:38:43 +0000
@@ -0,0 +1,9 @@
1id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2access_shift_code_manager,hr.shift_code_manager,model_hr_shift_code,base.group_hr_user,1,1,1,1
3access_shift_code_manager,hr.shift_code_manager,model_hr_shift_code,hr.group_duty_roster_manager,1,0,0,0
4access_shift_code_user,hr.shift_code_user,model_hr_shift_code,base.group_user,1,0,0,0
5access_duty_roster_manager,hr.duty_roster_manager,model_hr_duty_roster,hr.group_duty_roster_manager,1,1,1,1
6access_duty_roster_user,hr.duty_roster_user,model_hr_duty_roster,hr.group_duty_roster_user,1,1,1,1
7access_duty_roster_shift_manager,hr.duty_roster_shift_manager,model_hr_duty_roster_shift,hr.group_duty_roster_manager,1,1,1,1
8access_duty_roster_shift_user,hr.duty_roster_shift_user,model_hr_duty_roster_shift,hr.group_duty_roster_user,1,1,1,1
9access_duty_roster,hr.duty_roster,model_hr_duty_roster,base.group_user,1,0,0,0
010
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,20 @@
1.openerp .oe_list_content td, .openerp .oe_list_content th {
2 line-height: normal;
3}
4
5.oe_list.shift_day_list [data-id=employee_id] {
6 width: 30em;
7 vertical-align: top;
8}
9
10.oe_list_header_shift_day {
11 font-size: 0.75em;
12 vertical-align: top;
13 width: 24px;
14 padding: 3px 0 0 3px !important;
15 text-align: center !important;
16}
17
18.hr_shift_code_col {
19 font-weight: bold;
20}
021
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,208 @@
1openerp.hr_roster = function(instance) {
2 var _t = instance.web._t,
3 _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
5
6 var humanize_time = function(mins){
7 var mins = Number.parseInt(mins),
8 h = instance.web.format_value(Number.parseInt(mins / 60), this, 0),
9 m = instance.web.format_value(mins % 60, this, 0);
10
11 var h_text = "", m_text = "";
12 if(h > 0){
13 h_text = h + " hour";
14 if(h > 1) h_text += "s";
15 }
16 if(m > 0){
17 m_text = m + " minute";
18 if(m > 1) m_text += "s";
19 }
20 return _.string.sprintf("%s %s", h_text, m_text);
21 };
22
23 // BEGIN hr_duty_roster
24 instance.hr_duty_roster = {};
25
26 instance.hr_duty_roster.FieldShiftDay = instance.web.form.FieldChar.extend({
27 is_false: function(){
28 var selection = this.field_manager.get_field_desc(this.name).selection,
29 v = this.get_value();
30
31 var r = this._super() || !(_.any(selection, function(choice, index, list){
32 return choice[0] == v;
33 }));
34 return r;
35 },
36 start: function() {
37 var self = this;
38 self._super();
39 self.$('input').on('keyup', function(e){
40 var $i = $(this),
41 v = $i.val().trim().toUpperCase();
42 if($i.val() != v){
43 $i.val(v);
44
45 $i.blur();
46 self.$el.next().find('input:first').focus()
47 }
48 });
49 }
50 });
51
52 instance.hr_duty_roster.FieldEmployeesFilter = instance.web.form.FieldMany2One.extend({
53 get_search_blacklist: function() {
54 // WARNING: i am not sure if this will work for future v8
55 var blacklist = _.reduce(this.field_manager.dataset.cache, function(memo, row){
56 var employee_id = row.values.employee_id;
57 // if a record is loaded from db, the employee_id is a tuple in the form of (id, name)
58 // otherwise, it is just an id (integer)
59 if(_.isArray(employee_id)){
60 employee_id = employee_id[0];
61 }
62 memo.push(employee_id);
63 return memo;
64 }, []);
65
66 return blacklist;
67 }
68 });
69
70 instance.web.form.widgets.add('shift_day', 'instance.hr_duty_roster.FieldShiftDay');
71 instance.web.form.widgets.add('employees_filter', 'instance.hr_duty_roster.FieldEmployeesFilter');
72
73 // we have to use customize _format for this column, or otherwise the value that is rendered into the cell
74 // has an "id," prepended to it
75 instance.hr_duty_roster.EmployeesFilterColumn = instance.web.list.Column.extend({
76 _format: function(row_data, options){
77 var value = row_data.employee_id.value;
78 return value[1] ? value[1].split("\n")[0] : value[1];
79 }
80 });
81
82 instance.web.list.columns.add('field.employees_filter', 'instance.hr_duty_roster.EmployeesFilterColumn');
83
84 instance.hr_duty_roster.WidgetDatePicker = instance.web.form.FormWidget.extend({
85 renderElement: function(){
86 var today = new Date(), m = 1, y = 2014, d = 1;
87 if(typeof this.field_manager.get_field_desc('month') === 'undefined'){
88 m = today.getMonth();
89 }
90 else{
91 m = this.field_manager.get_field_value('month') - 1; // Python month is 1-base
92 }
93
94 if(typeof this.field_manager.get_field_desc('year') === 'undefined'){
95 y = today.getFullYear();
96 }
97 else{
98 y = this.field_manager.get_field_value('year');
99 }
100
101 if(today.getFullYear() === y && today.getMonth() === m) d = today.day;
102
103 this.$el.html(QWeb.render('DatePickerWidget', {_id: 'hr_duty_roster_dp' + _.uniqueId(), year: y, month: m, day: d}));
104 }
105 });
106
107 instance.hr_duty_roster.WidgetShiftCodeHelp = instance.web.form.FormWidget.extend({
108 renderElement: function(){
109 var self = this,
110 model = new instance.web.Model("hr.shift_code");
111
112 model.query(['code', 'time_in', 'time_out', 'duration', 'break', 'description']).all().then(function(rows){
113 _.map(rows, function(v, k, list){
114 list[k]['duration_text'] = humanize_time(v['duration']);
115 list[k]['time_in'] = instance.web.format_value(v['time_in'], {'widget': 'time'});
116 list[k]['time_out'] = instance.web.format_value(v['time_out'], {'widget': 'time'});
117 });
118 var out = QWeb.render('ShiftCodeHelpWidget', {'selection': rows});
119 self.$el.html(out);
120 });
121 }
122 });
123
124 instance.web.form.custom_widgets.add('datepicker', 'instance.hr_duty_roster.WidgetDatePicker');
125 instance.web.form.custom_widgets.add('shiftcodehelp', 'instance.hr_duty_roster.WidgetShiftCodeHelp');
126 // --- END hr_duty_roster
127
128
129 // -- BEGIN hr_shift_code
130 instance.hr_shift_code = {};
131
132 instance.hr_shift_code.FieldDuration = instance.web.form.FieldChar.extend({
133 render_value: function(){
134 this.$el.text(humanize_time(this.get_value()));
135 }
136 });
137
138 instance.web.form.widgets.add('duration_mins', 'instance.hr_shift_code.FieldDuration');
139
140 instance.hr_shift_code.TimeWidget = instance.web.DateTimeWidget.extend({
141 jqueryui_object: "timepicker",
142 type_of_date: "time",
143 picker: function() {
144 if(arguments[0] === 'setDate' && _.isEmpty(this.get('value'))){
145 // NOTE: this is a hack - default time (now) is almost always useless.
146 var args = Array.prototype.slice.call(arguments),
147 time = args[1];
148 time.setHours(0);
149 time.setMinutes(0);
150 time.setSeconds(0);
151 return $.fn[this.jqueryui_object].apply(this.$input_picker, args);
152 }
153 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
154 },
155 on_picker_select: function(text, instance_) {
156 // NOTE: as of jQuery timepicker v0.9.9, retriving time from timepicker with getDate always returns jQuery object
157 var time_str = this.picker('getDate').val(), // this.picker is a timepicker
158 val = instance.web.str_to_time(time_str + ":00"); // seconds not returned
159
160 this.$input
161 .val(val ? this.format_client(val) : '')
162 .change()
163 .focus();
164 },
165 parse_client: function(v) {
166 return instance.web.parse_value(v, {"widget": 'datetime'});
167 },
168 format_client: function(v) {
169 return instance.web.format_value(v, {"widget": 'time'});
170 }
171 });
172
173 instance.hr_shift_code.FieldTimePicker = instance.web.form.FieldDatetime.extend({
174 build_widget: function() {
175 return new instance.hr_shift_code.TimeWidget(this);
176 },
177 render_value: function() {
178 if (!this.get("effective_readonly")) {
179 this.datewidget.set_value(this.get('value'));
180 } else {
181 this.$el.text(instance.web.format_value(this.get('value'), {'widget': 'time'}, ''));
182 }
183 }
184 });
185
186 instance.web.form.widgets.add('timepicker', 'instance.hr_shift_code.FieldTimePicker');
187
188 // custom columns used by shift code module
189 instance.hr_shift_code.DurationMinsColumn = instance.web.list.Column.extend({
190 _format: function(row_data, options){
191 return _.escape(instance.web.format_value(
192 humanize_time(row_data[this.id].value), this, options.value_if_empty));
193 }
194 });
195
196 instance.hr_shift_code.TimeColumn = instance.web.list.Column.extend({
197 _format: function(row_data, options){
198 return _.escape(instance.web.format_value(
199 instance.web.format_value(row_data[this.id].value, {'widget': 'time'}),
200 this,
201 options.value_if_empty));
202 }
203 });
204
205 instance.web.list.columns.add('field.duration_mins', 'instance.hr_shift_code.DurationMinsColumn');
206 instance.web.list.columns.add('field.time', 'instance.hr_shift_code.TimeColumn');
207 // --- END hr_shift_code
208}
0209
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,25 @@
1<?xml version="1.0" encoding="UTF-8"?>
2
3<templates xml:space="preserve">
4 <t t-name="DatePickerWidget">
5 <div t-att-id="_id" class="datepicker"></div>
6 <script type="text/javascript">
7 setTimeout(function(){
8 $('#<t t-raw="_id"/>').datepicker({'onSelect': function(){}, 'defaultDate': new Date(<t t-raw="year"/>, <t t-raw="month"/>, <t t-raw="day"/>)});
9 }, 0);
10 </script>
11 </t>
12
13 <t t-name="ShiftField">
14 <span class="oe_form_field hr_roster_month_shift">
15 <t t-foreach="widget.get('value').length" t-as="day">
16 <t t-if="! widget.get('readonly')">
17 <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]"></input>
18 </t>
19 <t t-if="widget.get('readonly')">
20 <input type="text" maxlength="1" size="1" t-att-value="widget.get('value')[day_index]" readonly="readonly"></input>
21 </t>
22 </t>
23 </span>
24 </t>
25</templates>
026
=== 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-05-07 07:38:43 +0000
@@ -0,0 +1,46 @@
1<?xml version="1.0" encoding="UTF-8"?>
2
3<templates xml:space="preserve">
4 <t t-name="ShiftCodeHelpWidget">
5 <table class="oe_list_content">
6 <thead>
7 <tr>
8 <th>Code</th>
9 <th>Description</th>
10 <th>Time In - Time Out</th>
11 <th>Duration</th>
12 <th>Break</th>
13 </tr>
14 </thead>
15 <tbody>
16 <t t-foreach="selection" t-as="choice">
17 <tr>
18 <td class="hr_shift_code_col">
19 <t t-esc="choice.code"></t>
20 </td>
21 <td>
22 <t t-if="choice.description">
23 <t t-esc="choice.description"></t>
24 </t>
25 </td>
26 <td>
27 <t t-if="choice.time_in and choice.time_out">
28 <t t-esc="choice.time_in"></t> - <t t-esc="choice.time_out"></t>
29 </t>
30 </td>
31 <td>
32 <t t-if="choice.duration > 0">
33 <t t-esc="choice.duration_text"></t>
34 </t>
35 </td>
36 <td>
37 <t t-if="choice.break">
38 <t t-esc="choice.break"></t>
39 </t>
40 </td>
41 </tr>
42 </t>
43 </tbody>
44 </table>
45 </t>
46</templates>

Subscribers

People subscribed via source and target branches