Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
working
: Work time tracking¶
The lino_xl.lib.working
adds functionality for registering work time and
generating service reports.
See also the end-user docs The working plugin.
This page contains code snippets (lines starting with >>>
), which are
being tested during our development workflow. The following
snippet initializes the demo project used throughout this page.
>>> from lino_book.projects.noi1e.startup import *
The demo project is on fictive demo date May 22, 2015:
>>> dd.today()
datetime.date(2015, 5, 22)
Plugin settings¶
- working.reports_master¶
The model used as the master for service reports.
This model must have three fields company, start_date and end_date.
Default value is ‘trading.VatProductInvoice’.
- working.default_reporting_type¶
The reporting type to use when no explicit reporting type has been selected for a session.
- working.ticket_model¶
The database model to be used as the “ticket”.
This must be no longer a subclass of
lino_xl.lib.working.mixins.Workable
, it must just have a
method on_worked.
The ticket_model
defines what a ticket actually
is. In Lino Noi this points to tickets.Ticket
. It can be any model that implements
Workable
.
Working sessions¶
A site manager can see all sessions of the demo project:
>>> rt.show(working.Sessions, limit=15)
...
==================================================== ========= ============ ============ ============ ========== ============ =============================== ========== ==================== ============= =====================
Ticket Worker Start date Start time End Date End Time Break Time Summary Duration Duration (decimal) Ticket # Subscription
---------------------------------------------------- --------- ------------ ------------ ------------ ---------- ------------ ------------------------------- ---------- -------------------- ------------- ---------------------
#87 (Bars have no foo) Jean 22/05/2015 09:00:00 fiddle with get_auth() `#87 <…>`__
#90 (No more foo when bar is gone) Jean 22/05/2015 09:00:00 0:10 jitsi meeting claire and paul `#90 <…>`__
#25 ('NoneType' object has no attribute 'isocode') Luc 22/05/2015 09:00:00 0:10 commit and push `#25 <…>`__ SLA 2/2014 (welsch)
#28 (How to get bar from foo) Luc 22/05/2015 09:00:00 empty recycle bin `#28 <…>`__ SLA 4/2014 (bcc)
#81 (Irritating message when bar) Mathieu 22/05/2015 09:00:00 0:10 meeting with john `#81 <…>`__
#84 (Default account in invoices per partner) Mathieu 22/05/2015 09:00:00 response to email `#84 <…>`__
#87 (Bars have no foo) Mathieu 22/05/2015 09:00:00 check for comments `#87 <…>`__
#90 (No more foo when bar is gone) Mathieu 22/05/2015 09:00:00 0:10 keep door open `#90 <…>`__
#22 (How can I see where bar?) Luc 21/05/2015 12:58:00 21/05/2015 15:00:00 0:10 drive to brussels 1:52 1,87 `#22 <…>`__ SLA 5/2014 (dde)
#78 (No more foo when bar is gone) Mathieu 21/05/2015 12:58:00 21/05/2015 13:10:00 catch the brown fox 0:12 0,20 `#78 <…>`__
#75 (Bars have no foo) Mathieu 21/05/2015 12:53:00 21/05/2015 12:58:00 brainstorming lou & paul 0:05 0,08 `#75 <…>`__
#19 (Cannot delete foo) Luc 21/05/2015 12:48:00 21/05/2015 12:58:00 keep door open 0:10 0,17 `#19 <…>`__ SLA 3/2014 (aab)
#84 (Default account in invoices per partner) Jean 21/05/2015 12:29:00 21/05/2015 13:06:00 peer review with mark 0:37 0,62 `#84 <…>`__
#16 (How to get bar from foo) Luc 21/05/2015 11:18:00 21/05/2015 12:48:00 check for comments 1:30 1,50 `#16 <…>`__ SLA 1/2014 (welket)
#81 (Irritating message when bar) Jean 21/05/2015 09:00:00 21/05/2015 12:29:00 0:10 empty recycle bin 3:19 3,32 `#81 <…>`__
**Total (2384 rows)** **7:45** **7,75**
==================================================== ========= ============ ============ ============ ========== ============ =============================== ========== ==================== ============= =====================
Some sessions are on private tickets:
>>> from django.db.models import Q
>>> rt.show(working.Sessions, column_names="ticket user duration", filter=Q(ticket__private=True))
...
============================== ======== ==========
Ticket Worker Duration
------------------------------ -------- ----------
#4 (⚒ Foo and bar don't baz) Luc 2:18
#12 (⚒ Foo cannot bar) Luc 1:30
**Total (2 rows)** **3:48**
============================== ======== ==========
Worked hours¶
Example of WorkedHours
table
>>> ses = rt.login('jean')
>>> ses.show(working.WorkedHours)
...
================================ ==================================================== =========== ========== ===========
Description Worked tickets Regular Free Total
-------------------------------- ---------------------------------------------------- ----------- ---------- -----------
`Friday, 22 May 2015 <…>`__ `#90 <…>`__, `#87 <…>`__ 0:02 0:02
`Thursday, 21 May 2015 <…>`__ `#81 <…>`__, `#84 <…>`__ 3:19 0:37 3:56
`Wednesday, 20 May 2015 <…>`__ `#72 <…>`__, `#75 <…>`__, `#69 <…>`__, `#78 <…>`__ 4:10 1:30 5:40
`Tuesday, 19 May 2015 <…>`__ `#66 <…>`__, `#60 <…>`__, `#63 <…>`__ 0:17 3:43 4:00
`Monday, 18 May 2015 <…>`__ `#57 <…>`__, `#54 <…>`__ 3:51 3:51
`Sunday, 17 May 2015 <…>`__ 0:00
`Saturday, 16 May 2015 <…>`__ 0:00
**Total (7 rows)** **11:39** **5:50** **17:29**
================================ ==================================================== =========== ========== ===========
The detail view of WorkedHours
has a slave panel showing the
MySessionsByDay
table, the list of my sessions on this date. When Jean
clicks on the date of the first row (“Friday 22 May 2015”), he sees
>>> ses.show(working.MySessionsByDay, display_mode="summary")
...
**New** **ⓘ** `⏏ <…>`__ | `09:00 #87 <…>`__, `09:00 #90 <…>`__
(Yes he started working on two different tickets within the same minute. Generated demo data…)
>>> ses.show(working.MySessionsByDay)
...
============ ========== ============ ========== =============================== ==================================== ==========
Start time End Time Break Time Duration Summary Ticket Workflow
------------ ---------- ------------ ---------- ------------------------------- ------------------------------------ ----------
09:00:00 fiddle with get_auth() #87 (Bars have no foo) [■]
09:00:00 0:10 jitsi meeting claire and paul #90 (No more foo when bar is gone) [■]
============ ========== ============ ========== =============================== ==================================== ==========
Service reports¶
A service report is a document used in various discussions with a stakeholder. It reports about the working time invested during a given date range. This report can serve as a base for writing invoices.
It can be addressed to a recipient (a user) and in that case will consider only the tickets for which this user has specified interest.
A service report currently contains three tables:
a list of working sessions
a list of the tickets mentioned in the working sessions and their invested time
a list of sites mentioned in the working sessions and their invested time
Reporting type¶
The columns “Regular”, “Free” and “Total” in reports like WorkedHours
,
OrderSummaries
and UserSummaries
are a way to distribute
worktime into basic categories.
These basic categories are defined by the ReportingTypes
choicelist.
The reporting_type
field of a working
session defines where the duration of this session is to appear in these
reports.
The default configuration has two reporting types:
>>> rt.show(working.ReportingTypes)
======= ========= =========
value name text
------- --------- ---------
10 regular Regular
20 free Free
======= ========= =========
- class lino_xl.lib.working.ReportingTypes¶
The list of reporting types available on this site.
A server administrator can adapt the ReportingTypes
choicelist
to the site’s needs. He may also change the default reporting type by setting
the working.default_reporting_type
plugin attribute.
>>> dd.plugins.working.default_reporting_type
<working.ReportingTypes.regular:10>
Reporting rules¶
The reporting rules specify which product to use when reporting a work in a service report.
The choice of the product to be reported was originally directly deduced from
the session’s Session.reporting_type
, but meanwhile it can also be based
on the value of the urgent
checkbox. And it is
conceivable to add more selection criteria by extending the plugin.
The default Noi demo fixtures define the following reporting rules:
>>> rt.show(working.ReportingRules)
==== ===== ================ ======== =========================
ID No. Reporting type urgent Product
---- ----- ---------------- -------- -------------------------
1 1 Free Not invoiced
2 2 No Hourly rate
3 3 Yes Hourly rate (emergency)
==== ===== ================ ======== =========================
Which means: sessions with reporting type “Free” will be reported using the “Not invoiced” product (which has a sales price of 0 and no storage history), and all other sessions will be reported using either “Hourly rate” or “Hourly rate (emergency)” depending on whether their ticket is marked “urgent” or not.
Here is the database structure of a reporting rule:
- class lino_xl.lib.working.ReportingRule¶
Django model used to configure reporting rules.
- seqno¶
The sequence number of this rule. Lino looks up reporting rules ordered by their sequence number and uses the first rule that applies.
- reporting_type¶
Select the reporting type for which this rule applies. Leave blank if this rule applies independently of the session’s
reporting_type
.
Components¶
In Lino Noi, when a working session is invoiced using the “Emergency”
tariff because the ticket is marked as urgent
, the Emergency tariff (a product instance)
“knows” that it is actually just a virtual product. Each hour of Emergency
service is equivalent to 1.5 hours of regular service.
Database models¶
- class lino_xl.lib.working.SessionType¶
Django model representing the type of a working session.
- class lino_xl.lib.working.Session¶
Django model representing a working session.
- start_date¶
The date when you started to work.
- start_time¶
The time (in hh:mm) when you started working on this session.
This is your local time according to the time zone specified in your preferences.
- end_date¶
Leave this field blank if it is the same date as start_date.
- end_time¶
The time (in hh:mm) when the worker stopped to work.
An empty
end_time
means that the user is still busy with that session, the session is not yet closed.end_session()
sets this to the current time.
- break_time¶
The time (in hh:mm) to remove from the duration resulting from the difference between
start_time
andend_time
.
- faculty¶
The faculty that has been used during this session. On a new session this defaults to the needed faculty currently specified on the ticket.
- duration¶
Virtual field returning the
computed_duration
.
- computed_duration¶
The duration of this session as a
Duration
.This is the mathematical difference between
start_time
andend_time
, minus thebreak_time
and the durations of sub-sessions of this session. (Details seelino_xl.lib.working.models.Session.compute_duration()
).
- duration_decimal¶
The
computed_duration
expressed as a decimal number rather than aDuration
. This format can be more convenient when processing the data in a spreadsheet.
- reporting_type¶
The reporting type to use for this session.
- ticket¶
The ticket that has been worked on during this session.
- end_session()¶
Tell Lino that you stop this session for now. This will simply set the
end_time
to the current time.Implemented by
EndThisSession
.
Sessions by ticket¶
Shows the working sessions on a ticket.
Example:
>>> set([o.ticket.pk for o in working.Session.objects.filter(end_time__isnull=True)])
{81, 84, 87, 25, 90, 28}
>>> ses = rt.login('robin')
>>> obj = tickets.Ticket.objects.get(pk=81)
>>> ses.show(working.SessionsByTicket, obj)
**New** **ⓘ** `⏏ <…>`__ | Total 66:58 hours.
Active sessions: `Mathieu since 09:00:00 <…>`__ **■**
The default display mode is “summary”. Here is how it looks in display mode “grid”:
>>> ses.show(working.SessionsByTicket, obj, display_mode=DISPLAY_MODE_GRID)
...
===================== =============================== ============ ========== ============ =========== ========= ======
Start date Summary Start time End Time Break Time Duration Worker ID
--------------------- ------------------------------- ------------ ---------- ------------ ----------- --------- ------
22/05/2015 meeting with john 09:00:00 0:10 Mathieu 2367
21/05/2015 empty recycle bin 09:00:00 12:29:00 0:10 3:19 Jean 787
04/05/2015 response to email 09:00:00 10:02:00 1:02 Mathieu 2329
01/05/2015 peer review with mark 12:58:00 13:10:00 0:12 Jean 749
14/04/2015 check for comments 11:18:00 12:48:00 1:30 Mathieu 2291
...
09/06/2014 commit and push 12:53:00 12:58:00 0:05 Mathieu 1683
06/06/2014 brainstorming lou & paul 12:48:00 12:58:00 0:10 Jean 103
20/05/2014 catch the brown fox 09:00:00 12:53:00 0:10 3:43 Jean 65
20/05/2014 empty recycle bin 09:00:00 12:29:00 0:10 3:19 Mathieu 1645
30/04/2014 peer review with mark 12:58:00 13:10:00 0:12 Mathieu 1607
29/04/2014 meeting with john 12:58:00 15:00:00 0:10 1:52 Jean 27
**Total (42 rows)** **66:58**
===================== =============================== ============ ========== ============ =========== ========= ======
Tables reference¶
- class lino_xl.lib.working.Sessions¶
- class lino_xl.lib.working.SessionsByTicket¶
Show the working sessions on this ticket.
- class lino_xl.lib.working.MySessions¶
Shows all my sessions.
Use the
button to filter them. You can export them to Excel.
- class lino_xl.lib.working.MySessionsByDate¶
Shows my working sessions of a given day.
Use this to manually edit your working sessions.
- class lino_xl.lib.working.TicketsReport¶
- class lino_xl.lib.working.SitesByReport¶
The list of tickets mentioned in a service report.
- class lino_xl.lib.working.WorkersByReport¶
Actions reference¶
- class lino_xl.lib.working.StartTicketSession¶
The action behind
Workable.start_session
.
- class lino_xl.lib.working.EndSession¶
Common base for
EndThisSession
andEndTicketSession
.
- class lino_xl.lib.working.EndTicketSession¶
The action behind
Workable.end_session
.
- class lino_xl.lib.working.EndThisSession¶
The action behind
Session.end_session
.
The Workable model mixin¶
- class lino_xl.lib.working.Workable¶
Base class for things that workers can work on.
The model specified in
ticket_model
must be a subclass of this. For example, in Lino Noi tickets are workable.- is_workable_for()¶
Return True if the given user can start a working session on this object.
- on_worked()¶
This is automatically called when a working session has been created or modified.
- start_session()¶
Start a working session on this ticket.
See
StartTicketSession
.
- end_session()¶
Tell Lino that you stop working on this ticket for now. This will simply set the
Session.end_time
to the current time.Implemented by
EndTicketSession
.
Actions reference¶
- class lino_xl.lib.working.ShowMySessionsByDay¶
Shows your working sessions per day.
- class lino_xl.lib.working.TicketHasSessions¶
Select only tickets for which there has been at least one session during the given period.
This is added as item to
lino_xl.lib.tickets.TicketEvents
.
- class lino_xl.lib.working.ProjectHasSessions¶
Select only projects for which there has been at least one session during the given period.
This is added as item to
lino_xl.lib.tickets.ProjectEvents
.
- class lino_xl.lib.working.Worker¶
A user who is candidate for working on a ticket.
Summaries¶
- class lino_xl.lib.working.SiteSummary¶
An automatically generated record with yearly summary data about a site.
- class lino_xl.lib.working.SummariesBySite¶
Shows the summary records for a given site.
- class lino_xl.lib.working.UserSummary¶
An automatically generated record with monthly summary data about a user.
- class lino_xl.lib.working.SummariesByUser¶
Shows the summary records for a given user.
Summaries by order¶
- class lino_xl.lib.working.OrderSummaries¶
>>> ses.show(working.OrderSummaries, exclude=Q(regular_hours=""))
...
==== ====== ======= ===================== ================ ================== ========= =======
ID Year Month Subscription Active tickets Inactive tickets Regular Free
---- ------ ------- --------------------- ---------------- ------------------ --------- -------
2 2014 SLA 1/2014 (welket) 0 0 123:46 41:57
3 2015 SLA 1/2014 (welket) 0 0 70:06 24:13
5 2014 SLA 2/2014 (welsch) 0 0 123:22 41:18
6 2015 SLA 2/2014 (welsch) 0 0 66:10 22:07
8 2014 SLA 3/2014 (aab) 0 0 126:08 41:29
9 2015 SLA 3/2014 (aab) 0 0 65:31 23:55
11 2014 SLA 4/2014 (bcc) 0 0 101:42 40:57
12 2015 SLA 4/2014 (bcc) 0 0 58:27 24:09
14 2014 SLA 5/2014 (dde) 0 0 124:53 41:10
15 2015 SLA 5/2014 (dde) 0 0 65:42 21:35
==== ====== ======= ===================== ================ ================== ========= =======
>>> rt.show(working.OrderSummaries)
...
==== ====== ======= ===================== ================ ================== ========= =======
ID Year Month Subscription Active tickets Inactive tickets Regular Free
---- ------ ------- --------------------- ---------------- ------------------ --------- -------
1 2013 SLA 1/2014 (welket) 0 0
2 2014 SLA 1/2014 (welket) 0 0 123:46 41:57
3 2015 SLA 1/2014 (welket) 0 0 70:06 24:13
4 2013 SLA 2/2014 (welsch) 0 0
5 2014 SLA 2/2014 (welsch) 0 0 123:22 41:18
6 2015 SLA 2/2014 (welsch) 0 0 66:10 22:07
7 2013 SLA 3/2014 (aab) 0 0
8 2014 SLA 3/2014 (aab) 0 0 126:08 41:29
9 2015 SLA 3/2014 (aab) 0 0 65:31 23:55
10 2013 SLA 4/2014 (bcc) 0 0
11 2014 SLA 4/2014 (bcc) 0 0 101:42 40:57
12 2015 SLA 4/2014 (bcc) 0 0 58:27 24:09
13 2013 SLA 5/2014 (dde) 0 0
14 2014 SLA 5/2014 (dde) 0 0 124:53 41:10
15 2015 SLA 5/2014 (dde) 0 0 65:42 21:35
==== ====== ======= ===================== ================ ================== ========= =======
Summaries by user¶
- class lino_xl.lib.working.UserSummaries¶
>>> rt.show(working.UserSummaries, exclude=Q(regular_hours=""))
...
===== ====== ====== ========= ========= =========
ID Year Week User Regular Free
----- ------ ------ --------- --------- ---------
53 2014 1 Jean 13:16 0:15
68 2014 16 Jean 9:01 0:10
69 2014 17 Jean 16:00 5:55
70 2014 18 Jean 12:17 9:10
71 2014 19 Jean 16:40 6:27
72 2014 20 Jean 12:14 9:09
73 2014 21 Jean 20:01 1:17
74 2014 22 Jean 19:15 2:12
75 2014 23 Jean 19:51 3:16
76 2014 24 Jean 16:26 4:57
77 2014 25 Jean 15:23 5:55
78 2014 26 Jean 12:17 9:10
79 2014 27 Jean 18:52 4:15
80 2014 28 Jean 12:14 9:09
81 2014 29 Jean 20:01 1:17
82 2014 30 Jean 20:33 0:54
83 2014 31 Jean 19:51 3:16
84 2014 32 Jean 13:07 8:16
85 2014 33 Jean 16:25 4:53
86 2014 34 Jean 12:17 9:10
87 2014 35 Jean 20:55 2:12
88 2014 36 Jean 12:14 9:09
89 2014 37 Jean 20:01 1:17
90 2014 38 Jean 19:18 2:09
91 2014 39 Jean 19:51 3:16
92 2014 40 Jean 13:07 8:16
93 2014 41 Jean 12:54 8:24
94 2014 42 Jean 12:17 9:10
95 2014 43 Jean 20:45 2:22
96 2014 44 Jean 15:33 5:50
97 2014 45 Jean 20:01 1:17
98 2014 46 Jean 16:34 4:53
99 2014 47 Jean 19:51 3:16
100 2014 48 Jean 13:07 8:16
101 2014 49 Jean 13:16 8:02
102 2014 50 Jean 12:17 9:10
103 2014 51 Jean 20:45 2:22
104 2014 52 Jean 18:14 3:09
105 2015 1 Jean 6:45 1:02
106 2015 2 Jean 16:34 4:53
107 2015 3 Jean 18:38 1207:55
108 2015 4 Jean 13:52 859:23
109 2015 5 Jean 15:28 5:50
110 2015 6 Jean 12:17 9:10
111 2015 7 Jean 20:45 2:22
112 2015 8 Jean 19:32 1:51
113 2015 9 Jean 20:01 1:17
114 2015 10 Jean 16:34 4:53
115 2015 11 Jean 15:36 7:31
116 2015 12 Jean 13:07 8:16
117 2015 13 Jean 15:28 5:50
118 2015 14 Jean 14:20 7:07
119 2015 15 Jean 20:45 2:22
120 2015 16 Jean 18:17 3:06
121 2015 17 Jean 20:01 1:17
122 2015 18 Jean 16:34 4:53
123 2015 19 Jean 12:05 11:02
124 2015 20 Jean 13:07 8:16
125 2015 21 Jean 11:37 5:50
209 2014 1 Luc 8:46 3:01
224 2014 16 Luc 9:25 0:15
225 2014 17 Luc 18:17 3:06
226 2014 18 Luc 13:02 8:16
227 2014 19 Luc 9:12 12:15
228 2014 20 Luc 20:45 2:22
229 2014 21 Luc 18:17 3:06
230 2014 22 Luc 13:36 7:42
231 2014 23 Luc 12:17 9:10
232 2014 24 Luc 20:45 2:22
233 2014 25 Luc 20:31 0:52
234 2014 26 Luc 15:23 5:55
235 2014 27 Luc 12:17 9:10
236 2014 28 Luc 12:27 10:40
237 2014 29 Luc 20:31 0:52
238 2014 30 Luc 17:10 4:08
239 2014 31 Luc 16:34 4:53
240 2014 32 Luc 12:27 10:40
241 2014 33 Luc 18:34 2:49
242 2014 34 Luc 20:01 1:17
243 2014 35 Luc 16:34 4:53
244 2014 36 Luc 12:43 10:24
245 2014 37 Luc 12:14 9:09
246 2014 38 Luc 20:01 1:17
247 2014 39 Luc 19:51 1:36
248 2014 40 Luc 16:18 6:49
249 2014 41 Luc 12:14 9:09
250 2014 42 Luc 15:28 5:50
251 2014 43 Luc 19:58 1:29
252 2014 44 Luc 14:26 8:41
253 2014 45 Luc 13:07 8:16
254 2014 46 Luc 15:28 5:50
255 2014 47 Luc 20:05 1:22
256 2014 48 Luc 19:51 3:16
257 2014 49 Luc 13:07 8:16
258 2014 50 Luc 12:46 8:32
259 2014 51 Luc 15:32 5:55
260 2014 52 Luc 19:51 3:16
261 2015 1 Luc 7:44 1:52
262 2015 2 Luc 13:02 8:16
263 2015 3 Luc 15:32 5:55
264 2015 4 Luc 22:05 1:02
265 2015 5 Luc 18:17 3:06
266 2015 6 Luc 13:02 8:16
267 2015 7 Luc 8:34 12:53
268 2015 8 Luc 20:45 2:22
269 2015 9 Luc 18:17 3:06
270 2015 10 Luc 15:23 5:55
271 2015 11 Luc 12:17 9:10
272 2015 12 Luc 19:25 3:42
273 2015 13 Luc 20:31 0:52
274 2015 14 Luc 15:23 5:55
275 2015 15 Luc 13:11 8:16
276 2015 16 Luc 12:27 10:40
277 2015 17 Luc 20:31 0:52
278 2015 18 Luc 18:52 2:26
279 2015 19 Luc 16:34 4:53
280 2015 20 Luc 12:27 10:40
281 2015 21 Luc 11:37 5:50
521 2014 1 Mathieu 11:20 2:07
536 2014 16 Mathieu 7:46 0:05
537 2014 17 Mathieu 19:51 3:16
538 2014 18 Mathieu 13:07 8:16
539 2014 19 Mathieu 12:54 8:24
540 2014 20 Mathieu 12:17 9:10
541 2014 21 Mathieu 20:45 2:22
542 2014 22 Mathieu 15:33 5:50
543 2014 23 Mathieu 20:01 1:17
544 2014 24 Mathieu 16:34 4:53
545 2014 25 Mathieu 19:51 3:16
546 2014 26 Mathieu 13:07 8:16
547 2014 27 Mathieu 13:16 8:02
548 2014 28 Mathieu 12:17 9:10
549 2014 29 Mathieu 20:45 2:22
550 2014 30 Mathieu 18:14 3:09
551 2014 31 Mathieu 20:01 1:17
552 2014 32 Mathieu 16:34 4:53
553 2014 33 Mathieu 17:53 5:14
554 2014 34 Mathieu 13:07 8:16
555 2014 35 Mathieu 15:28 5:50
556 2014 36 Mathieu 12:17 9:10
557 2014 37 Mathieu 20:45 2:22
558 2014 38 Mathieu 19:32 1:51
559 2014 39 Mathieu 20:01 1:17
560 2014 40 Mathieu 16:34 4:53
561 2014 41 Mathieu 15:36 7:31
562 2014 42 Mathieu 13:07 8:16
563 2014 43 Mathieu 15:28 5:50
564 2014 44 Mathieu 14:20 7:07
565 2014 45 Mathieu 20:45 2:22
566 2014 46 Mathieu 18:17 3:06
567 2014 47 Mathieu 20:01 1:17
568 2014 48 Mathieu 16:34 4:53
569 2014 49 Mathieu 12:05 11:02
570 2014 50 Mathieu 13:07 8:16
571 2014 51 Mathieu 15:28 5:50
572 2014 52 Mathieu 17:29 3:58
573 2015 1 Mathieu 9:25 0:15
574 2015 2 Mathieu 18:17 3:06
575 2015 3 Mathieu 17:17 4:01
576 2015 4 Mathieu 16:34 4:53
577 2015 5 Mathieu 12:27 10:40
578 2015 6 Mathieu 13:07 8:16
579 2015 7 Mathieu 15:28 5:50
580 2015 8 Mathieu 20:10 1:17
581 2015 9 Mathieu 20:45 2:22
582 2015 10 Mathieu 18:17 3:06
583 2015 11 Mathieu 15:19 5:59
584 2015 12 Mathieu 16:34 4:53
585 2015 13 Mathieu 12:27 10:40
586 2015 14 Mathieu 15:19 6:04
587 2015 15 Mathieu 15:28 5:50
588 2015 16 Mathieu 19:58 1:29
589 2015 17 Mathieu 22:15 0:52
590 2015 18 Mathieu 18:17 3:06
591 2015 19 Mathieu 13:02 8:16
592 2015 20 Mathieu 16:34 4:53
593 2015 21 Mathieu 8:17 9:10
===== ====== ====== ========= ========= =========
Some projects have more than 999:59 hours per year of work, which would be
indicated by a -1:00
if summaries.duration_max_length
was at its
default value of 6. But in lino_noi.lib.noi.settings
the default value is
modified to 10.
>>> dd.plugins.summaries.duration_max_length
10
>>> rt.login('jean').show(working.AllSummaries)
...
==== ====== ======= ===================== ================ ================== ========= =======
ID Year Month Subscription Active tickets Inactive tickets Regular Free
---- ------ ------- --------------------- ---------------- ------------------ --------- -------
1 2013 SLA 1/2014 (welket) 0 0
2 2014 SLA 1/2014 (welket) 0 0 123:46 41:57
3 2015 SLA 1/2014 (welket) 0 0 70:06 24:13
4 2013 SLA 2/2014 (welsch) 0 0
5 2014 SLA 2/2014 (welsch) 0 0 123:22 41:18
6 2015 SLA 2/2014 (welsch) 0 0 66:10 22:07
7 2013 SLA 3/2014 (aab) 0 0
8 2014 SLA 3/2014 (aab) 0 0 126:08 41:29
9 2015 SLA 3/2014 (aab) 0 0 65:31 23:55
10 2013 SLA 4/2014 (bcc) 0 0
11 2014 SLA 4/2014 (bcc) 0 0 101:42 40:57
12 2015 SLA 4/2014 (bcc) 0 0 58:27 24:09
13 2013 SLA 5/2014 (dde) 0 0
14 2014 SLA 5/2014 (dde) 0 0 124:53 41:10
15 2015 SLA 5/2014 (dde) 0 0 65:42 21:35
==== ====== ======= ===================== ================ ================== ========= =======
Sub-sessions¶
To find the primary key of a session with sub-sessions, there is a print
statement in book/lino_book/projects/noi1e/settings/fixtures/demo.py
. So
if the following snippet fails, run go noi1e
followed by pm prep
and watch the output for two lines of text similar to the following ones:
20230117 Session 2373 has a subsession (compare docs/specs/working.rst)
20230117 Session 2379 has a subsession (compare docs/specs/working.rst)
And then use one of these numbers as the pk in the following snippet.
>>> obj = working.Session.objects.get(pk=2373)
>>> print(obj.break_time)
None
>>> st, et = obj.get_datetime('start'), obj.get_datetime('end')
>>> print("from {} to {}".format(st, et))
from 2015-01-14 09:00:00+00:00 to 2015-01-22 17:00:00+00:00
The session starts on 2015-02-14 at 09:00 and ends on 2015-01-22 at 17:00, so it lasts exactly 8 days and 8 hours, in other words 200 hours or 12000 minutes.
>>> delta = et - st
>>> print(delta)
8 days, 8:00:00
>>> print(delta.total_seconds()/3600)
200.0
>>> 8*24+8
200
But the computed_duration
is less than 200 hours:
>>> obj.computed_duration
Duration('168:17')
This is because there are sub-sessions. The get_sub_sessions()
method
iterates over them :
>>> from lino.utils.quantities import Duration
>>> for s in obj.get_sub_sessions():
... print("- {} {} ({})".format(s.computed_duration, s.break_time, s))
...
- 1:02 None (14/01/2015 09:00-10:02 Jean #12)
- 2:49 0:10 (14/01/2015 10:02-13:01 Jean #15)
- 2:58 0:10 (15/01/2015 09:00-12:53 Jean #18)
- 0:45 None (15/01/2015 09:30-10:15 Jean #102)
- 0:05 None (15/01/2015 12:53-12:58 Jean #21)
- 0:12 None (15/01/2015 12:58-13:10 Jean #24)
- 2:08 0:10 (16/01/2015 09:00-11:18 Jean #27)
- 1:30 None (16/01/2015 11:18-12:48 Jean #30)
- 0:10 None (16/01/2015 12:48-12:58 Jean #33)
- 1:52 0:10 (16/01/2015 12:58-15:00 Jean #36)
- 3:19 0:10 (19/01/2015 09:00-12:29 Jean #39)
- 0:37 None (19/01/2015 12:29-13:06 Jean #42)
- 1:02 None (20/01/2015 09:00-10:02 Jean #45)
- 0:45 None (20/01/2015 09:30-10:15 Jean #6)
- 2:49 0:10 (20/01/2015 10:02-13:01 Jean #48)
- 3:43 0:10 (21/01/2015 09:00-12:53 Jean #51)
- 0:05 None (21/01/2015 12:53-12:58 Jean #54)
- 0:12 None (21/01/2015 12:58-13:10 Jean #57)
- 2:08 0:10 (22/01/2015 09:00-11:18 Jean #60)
- 1:30 None (22/01/2015 11:18-12:48 Jean #63)
- 0:10 None (22/01/2015 12:48-12:58 Jean #66)
- 1:52 0:10 (22/01/2015 12:58-15:00 Jean #69)
Here is the sum of the durations of these sub-sessions:
>>> duration_of_sub_sessions = sum([s.computed_duration for s in obj.get_sub_sessions()])
>>> duration_of_sub_sessions
Duration('31:43')
And the sum of these two sums is indeed 200:00:
>>> duration_of_sub_sessions + obj.computed_duration
Duration('200:00')
Worker contracts¶
- class lino_xl.lib.working.Contract¶
Django model used to represent a worker contract.
- user¶
The worker.
- hours_per_week¶
How many hours this worker is expected to provide per week.
- class lino_xl.lib.working.Contracts¶
Shows the list of all worker contracts for managing them.
>>> rt.show(working.Contracts)
...
==================== ============ ============ ==========
Worker Hours/week Start date End date
-------------------- ------------ ------------ ----------
Marc 2:00
Mathieu 20:00
Luc 30:00
Jean 40:00
**Total (4 rows)** **92:00**
==================== ============ ============ ==========
- class lino_xl.lib.working.ActiveContracts¶
Shows the list of all worker contracts with statistic data about their activity.
>>> rt.show(working.ActiveContracts)
...
+--------------------+------------+-----------+------------+-----------+------------+
| Worker | Hours/week | Hours | Hours | Comments | Comments |
| | | last week | last month | last week | last month |
+====================+============+===========+============+===========+============+
| Marc | 2:00 | 0:00 | 0:00 | 0 | 72 |
+--------------------+------------+-----------+------------+-----------+------------+
| Mathieu | 20:00 | 21:27 | 95:06 | 0 | 72 |
+--------------------+------------+-----------+------------+-----------+------------+
| Luc | 30:00 | 23:07 | 96:55 | 0 | 72 |
+--------------------+------------+-----------+------------+-----------+------------+
| Jean | 40:00 | 21:23 | 96:51 | 0 | 72 |
+--------------------+------------+-----------+------------+-----------+------------+
| **Total (4 rows)** | **92:00** | **65:57** | **288:52** | **0** | **288** |
+--------------------+------------+-----------+------------+-----------+------------+
The somewhat unrealistic values for comments in above table are because in our demo database, all comments get created between 2015-04-22 and 2015-04-24, which is within the last month but not within the last week:
>>> str(dd.today())
'2015-05-22'
>>> sorted(set([str(c.created.date()) for c in comments.Comment.objects.all()]))
['2015-04-22', '2015-04-23', '2015-04-24']
Weekly report¶
The plugin defines a system task procedure send_weekly_report()
, which
sends a configurable weekly report to all workers with a contract.
- lino_xl.lib.working.send_weekly_report()¶
The procedure used by a system task that sends a report to all workers with a contract. The content of the report can be customized by editing the
working/weekly_report.eml
template.
- working/weekly_report.eml¶
The template used to generate the content of the Weekly report.
Don’t read me¶
>>> working.WorkedHours
lino_xl.lib.working.ui.WorkedHours
>>> print(working.WorkedHours.column_names)
detail_link worked_tickets vc0:5 vc1:5 vc2:5 *
>>> working.WorkedHours.get_data_elem('detail_link')
lino.core.actors.Actor.detail_link
Testing for equality of quantities¶
Remind the pitfall described in A possible pitfall. Here is a list of
sessions where the computed_duration
field is not exaclty the same as
the return value of compute_duration()
. They look the same when you
print them, but actually they differ.
>>> [obj.pk for obj in working.Session.objects.all()
... if obj.computed_duration != obj.compute_duration()]
...
[5, 6, 16, 17, ... 2382, 2383, 2384]
We verify this with the first one.
>>> obj = working.Session.objects.get(pk=5)
>>> obj.computed_duration
Duration('1:52')
>>> obj.compute_duration()
Duration('1:52')
But:
>>> obj.computed_duration == obj.compute_duration()
False
That’s why the SessionChecker
must call str()
in order to get the
expected result:
>>> str(obj.computed_duration) == str(obj.compute_duration())
True
‘NoneType’ object has no attribute ‘start_date’¶
Occurred when trying to print working.WorkedHours (#523).
In order to reproduce the issue, let’s find the users who worked on more than one subscription and then render this table to HTML.
>>> for u in users.User.objects.all():
... qs = subscriptions.Subscription.objects.filter(tickets_by_order__sessions_by_ticket__user=u).distinct()
... if qs.count() > 1:
... print("{} {} {}".format(str(u.username), "worked on", [o for o in qs]))
...
luc worked on [Subscription #1 ('SLA 1/2014 (welket)'), Subscription #3 ('SLA
3/2014 (aab)'), Subscription #5 ('SLA 5/2014 (dde)'), Subscription #2 ('SLA
2/2014 (welsch)'), Subscription #4 ('SLA 4/2014 (bcc)')]
We see that only luc is a candidate.
>>> url = "/api/working/WorkedHours?"
>>> #url += "_dc=1583295523012&cw=398&cw=398&cw=76&cw=76&cw=76&cw=76&cw=281&cw=76&ch=&ch=&ch=&ch=&ch=&ch=&ch=true&ch=true&ci=detail_link&ci=worked_tickets&ci=vc0&ci=vc1&ci=vc2&ci=vc3&ci=description&ci=day_number&name=0&pv=188&pv=&pv=&pv=&lv=1583294270.8095002&an=show_as_html&sr="
>>> url += "an=show_as_html"
>>> test_client.force_login(rt.login('luc').user)
>>> res = test_client.get(url, REMOTE_USER="luc")
>>> json.loads(res.content.decode()) == {'open_url': '/bs3/working/WorkedHours?limit=15', 'success': True}
True
The html version of this table has only 5 rows (4 data rows and the total row) because valueless rows are not included by default:
>>> ar = rt.login('luc')
>>> u = ar.get_user()
>>> ar = working.WorkedHours.create_request(user=u)
>>> ar = ar.spawn(working.WorkedHours)
>>> lst = list(ar)
>>> len(lst)
7
>>> e = ar.table2xhtml()
>>> len(e.findall('./tbody/tr'))
8