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