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.
Side note: Code snippets (lines starting with >>>
) in this document get
tested as part of our development workflow. The following
initialization snippet tells you which demo project is being used in
this document.
>>> from lino import startup
>>> startup('lino_book.projects.noi1e.settings.demo')
>>> from lino.api.doctest 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
----------------------------------- --------- ------------ ------------ ------------ ---------- ------------ ------------------------------- ---------- -------------------- ------------- ---------------------
#57 (Irritating message when bar) Jean 22/05/2015 09:00:00 fiddle with get_auth() `#57 <…>`__
#63 (Bars have no foo) Jean 22/05/2015 09:00:00 0:10 jitsi meeting claire and paul `#63 <…>`__
#59 (Misc optimizations in Baz) Luc 22/05/2015 09:00:00 0:10 commit and push `#59 <…>`__ SLA 5/2014 (dde)
#65 (Foo never bars) Luc 22/05/2015 09:00:00 empty recycle bin `#65 <…>`__ SLA 4/2014 (bcc)
#45 (Irritating message when bar) Mathieu 22/05/2015 09:00:00 0:10 meeting with john `#45 <…>`__
#51 (Bars have no foo) Mathieu 22/05/2015 09:00:00 response to email `#51 <…>`__
#57 (Irritating message when bar) Mathieu 22/05/2015 09:00:00 check for comments `#57 <…>`__
#63 (Bars have no foo) Mathieu 22/05/2015 09:00:00 0:10 keep door open `#63 <…>`__
#53 (Foo never bars) Luc 21/05/2015 12:58:00 21/05/2015 15:00:00 0:10 drive to brussels 1:52 1,87 `#53 <…>`__ SLA 1/2014 (welket)
#39 (Bars have no foo) Mathieu 21/05/2015 12:58:00 21/05/2015 13:10:00 catch the brown fox 0:12 0,20 `#39 <…>`__
#33 (Irritating message when bar) Mathieu 21/05/2015 12:53:00 21/05/2015 12:58:00 brainstorming lou & paul 0:05 0,08 `#33 <…>`__
#47 (Misc optimizations in Baz) Luc 21/05/2015 12:48:00 21/05/2015 12:58:00 keep door open 0:10 0,17 `#47 <…>`__ SLA 2/2014 (welsch)
#51 (Bars have no foo) Jean 21/05/2015 12:29:00 21/05/2015 13:06:00 peer review with mark 0:37 0,62 `#51 <…>`__
#41 (Foo never bars) Luc 21/05/2015 11:18:00 21/05/2015 12:48:00 check for comments 1:30 1,50 `#41 <…>`__ SLA 3/2014 (aab)
#45 (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 `#45 <…>`__
**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 <…>`__ `#57 <…>`__, `#63 <…>`__ 0:02 0:02
`Thursday 21 May 2015 <…>`__ `#51 <…>`__, `#45 <…>`__ 3:56 3:56
`Wednesday 20 May 2015 <…>`__ `#33 <…>`__, `#27 <…>`__, `#21 <…>`__, `#39 <…>`__ 5:40 5:40
`Tuesday 19 May 2015 <…>`__ `#9 <…>`__, `#3 <…>`__, `#15 <…>`__ 4:00 4:00
`Monday 18 May 2015 <…>`__ `#105 <…>`__, `#111 <…>`__ 3:51 3:51
`Sunday 17 May 2015 <…>`__ 0:00
`Saturday 16 May 2015 <…>`__ 0:00
**Total (7 rows)** **17:29** **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")
...
`09:00 #57 <…>`__, `09:00 #63 <…>`__, **New** **?**
(Yes he started working on two different tickets within the same second. Generated demo data…)
>>> ses.show(working.MySessionsByDay)
...
============ ========== ============ ========== =============================== =================================== ==========
Start time End Time Break Time Duration Summary Ticket Workflow
------------ ---------- ------------ ---------- ------------------------------- ----------------------------------- ----------
09:00:00 fiddle with get_auth() #57 (Irritating message when bar) [■]
09:00:00 0:10 jitsi meeting claire and paul #63 (Bars have no foo) [■]
============ ========== ============ ========== =============================== =================================== ==========
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
.
Tables reference¶
- class lino_xl.lib.working.Sessions¶
- class lino_xl.lib.working.SessionsByTicket¶
Show the working sessions on this ticket.
Example:
>>> ses = rt.login('robin') >>> obj = tickets.Ticket.objects.get(pk=59) >>> ses.show(working.SessionsByTicket, obj) Total 64:54 hours. Active sessions: `Luc 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 commit and push 09:00:00 0:10 Luc 1579 13/05/2015 catch the brown fox 10:02:00 13:01:00 0:10 2:49 Luc 1560 04/05/2015 empty recycle bin 12:58:00 13:10:00 0:12 Luc 1541 23/04/2015 meeting with john 12:48:00 12:58:00 0:10 Luc 1522 14/04/2015 peer review with mark 12:29:00 13:06:00 0:37 Luc 1503 ... 29/05/2014 drive to brussels 12:29:00 13:06:00 0:37 Luc 876 21/05/2014 brainstorming lou & paul 09:00:00 12:53:00 0:10 3:43 Luc 857 12/05/2014 commit and push 09:00:00 11:18:00 0:10 2:08 Luc 838 30/04/2014 catch the brown fox 12:58:00 15:00:00 0:10 1:52 Luc 819 22/04/2014 empty recycle bin 09:00:00 10:02:00 1:02 Luc 800 **Total (42 rows)** **64:54** ===================== =============================== ============ ========== ============ =========== ======== ======
- 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¶
>>> rt.show(working.OrderSummaries, exclude=dict(regular_hours=""))
...
==== ====== ======= ===================== ================ ================== ========= ======
ID Year Month Subscription Active tickets Inactive tickets Regular Free
---- ------ ------- --------------------- ---------------- ------------------ --------- ------
2 2014 SLA 1/2014 (welket) 0 0 167:24
3 2015 SLA 1/2014 (welket) 0 0 95:22
5 2014 SLA 2/2014 (welsch) 0 0 172:29
6 2015 SLA 2/2014 (welsch) 0 0 92:40
8 2014 SLA 3/2014 (aab) 0 0 170:57
9 2015 SLA 3/2014 (aab) 0 0 90:49
11 2014 SLA 4/2014 (bcc) 0 0 168:21
12 2015 SLA 4/2014 (bcc) 0 0 95:12
14 2014 SLA 5/2014 (dde) 0 0 127:31
15 2015 SLA 5/2014 (dde) 0 0 67:52
==== ====== ======= ===================== ================ ================== ========= ======
>>> 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 167:24
3 2015 SLA 1/2014 (welket) 0 0 95:22
4 2013 SLA 2/2014 (welsch) 0 0
5 2014 SLA 2/2014 (welsch) 0 0 172:29
6 2015 SLA 2/2014 (welsch) 0 0 92:40
7 2013 SLA 3/2014 (aab) 0 0
8 2014 SLA 3/2014 (aab) 0 0 170:57
9 2015 SLA 3/2014 (aab) 0 0 90:49
10 2013 SLA 4/2014 (bcc) 0 0
11 2014 SLA 4/2014 (bcc) 0 0 168:21
12 2015 SLA 4/2014 (bcc) 0 0 95:12
13 2013 SLA 5/2014 (dde) 0 0
14 2014 SLA 5/2014 (dde) 0 0 127:31
15 2015 SLA 5/2014 (dde) 0 0 67:52
==== ====== ======= ===================== ================ ================== ========= ======
Summaries by user¶
- class lino_xl.lib.working.UserSummaries¶
>>> rt.show(working.UserSummaries, exclude=dict(regular_hours=""))
...
===== ====== ====== ========= ========= =========
ID Year Week User Regular Free
----- ------ ------ --------- --------- ---------
53 2014 1 Jean 13:31
68 2014 16 Jean 9:11
69 2014 17 Jean 21:55
70 2014 18 Jean 21:27
71 2014 19 Jean 23:07
72 2014 20 Jean 21:23
73 2014 21 Jean 21:18
74 2014 22 Jean 21:27
75 2014 23 Jean 23:07
76 2014 24 Jean 21:23
77 2014 25 Jean 21:18
78 2014 26 Jean 21:27
79 2014 27 Jean 23:07
80 2014 28 Jean 21:23
81 2014 29 Jean 21:18
82 2014 30 Jean 21:27
83 2014 31 Jean 23:07
84 2014 32 Jean 21:23
85 2014 33 Jean 21:18
86 2014 34 Jean 21:27
87 2014 35 Jean 23:07
88 2014 36 Jean 21:23
89 2014 37 Jean 21:18
90 2014 38 Jean 21:27
91 2014 39 Jean 23:07
92 2014 40 Jean 21:23
93 2014 41 Jean 21:18
94 2014 42 Jean 21:27
95 2014 43 Jean 23:07
96 2014 44 Jean 21:23
97 2014 45 Jean 21:18
98 2014 46 Jean 21:27
99 2014 47 Jean 23:07
100 2014 48 Jean 21:23
101 2014 49 Jean 21:18
102 2014 50 Jean 21:27
103 2014 51 Jean 23:07
104 2014 52 Jean 21:23
105 2015 1 Jean 7:47
106 2015 2 Jean 21:27
107 2015 3 Jean 23:52 1202:41
108 2015 4 Jean 22:08 851:07
109 2015 5 Jean 21:18
110 2015 6 Jean 21:27
111 2015 7 Jean 23:07
112 2015 8 Jean 21:23
113 2015 9 Jean 21:18
114 2015 10 Jean 21:27
115 2015 11 Jean 23:07
116 2015 12 Jean 21:23
117 2015 13 Jean 21:18
118 2015 14 Jean 21:27
119 2015 15 Jean 23:07
120 2015 16 Jean 21:23
121 2015 17 Jean 21:18
122 2015 18 Jean 21:27
123 2015 19 Jean 23:07
124 2015 20 Jean 21:23
125 2015 21 Jean 17:27
209 2014 1 Luc 11:47
224 2014 16 Luc 9:40
225 2014 17 Luc 21:23
226 2014 18 Luc 21:18
227 2014 19 Luc 21:27
228 2014 20 Luc 23:07
229 2014 21 Luc 21:23
230 2014 22 Luc 21:18
231 2014 23 Luc 21:27
232 2014 24 Luc 23:07
233 2014 25 Luc 21:23
234 2014 26 Luc 21:18
235 2014 27 Luc 21:27
236 2014 28 Luc 23:07
237 2014 29 Luc 21:23
238 2014 30 Luc 21:18
239 2014 31 Luc 21:27
240 2014 32 Luc 23:07
241 2014 33 Luc 21:23
242 2014 34 Luc 21:18
243 2014 35 Luc 21:27
244 2014 36 Luc 23:07
245 2014 37 Luc 21:23
246 2014 38 Luc 21:18
247 2014 39 Luc 21:27
248 2014 40 Luc 23:07
249 2014 41 Luc 21:23
250 2014 42 Luc 21:18
251 2014 43 Luc 21:27
252 2014 44 Luc 23:07
253 2014 45 Luc 21:23
254 2014 46 Luc 21:18
255 2014 47 Luc 21:27
256 2014 48 Luc 23:07
257 2014 49 Luc 21:23
258 2014 50 Luc 21:18
259 2014 51 Luc 21:27
260 2014 52 Luc 23:07
261 2015 1 Luc 9:36
262 2015 2 Luc 21:18
263 2015 3 Luc 21:27
264 2015 4 Luc 23:07
265 2015 5 Luc 21:23
266 2015 6 Luc 21:18
267 2015 7 Luc 21:27
268 2015 8 Luc 23:07
269 2015 9 Luc 21:23
270 2015 10 Luc 21:18
271 2015 11 Luc 21:27
272 2015 12 Luc 23:07
273 2015 13 Luc 21:23
274 2015 14 Luc 21:18
275 2015 15 Luc 21:27
276 2015 16 Luc 23:07
277 2015 17 Luc 21:23
278 2015 18 Luc 21:18
279 2015 19 Luc 21:27
280 2015 20 Luc 23:07
281 2015 21 Luc 17:27
521 2014 1 Mathieu 13:27
536 2014 16 Mathieu 7:51
537 2014 17 Mathieu 23:07
538 2014 18 Mathieu 21:23
539 2014 19 Mathieu 21:18
540 2014 20 Mathieu 21:27
541 2014 21 Mathieu 23:07
542 2014 22 Mathieu 21:23
543 2014 23 Mathieu 21:18
544 2014 24 Mathieu 21:27
545 2014 25 Mathieu 23:07
546 2014 26 Mathieu 21:23
547 2014 27 Mathieu 21:18
548 2014 28 Mathieu 21:27
549 2014 29 Mathieu 23:07
550 2014 30 Mathieu 21:23
551 2014 31 Mathieu 21:18
552 2014 32 Mathieu 21:27
553 2014 33 Mathieu 23:07
554 2014 34 Mathieu 21:23
555 2014 35 Mathieu 21:18
556 2014 36 Mathieu 21:27
557 2014 37 Mathieu 23:07
558 2014 38 Mathieu 21:23
559 2014 39 Mathieu 21:18
560 2014 40 Mathieu 21:27
561 2014 41 Mathieu 23:07
562 2014 42 Mathieu 21:23
563 2014 43 Mathieu 21:18
564 2014 44 Mathieu 21:27
565 2014 45 Mathieu 23:07
566 2014 46 Mathieu 21:23
567 2014 47 Mathieu 21:18
568 2014 48 Mathieu 21:27
569 2014 49 Mathieu 23:07
570 2014 50 Mathieu 21:23
571 2014 51 Mathieu 21:18
572 2014 52 Mathieu 21:27
573 2015 1 Mathieu 9:40
574 2015 2 Mathieu 21:23
575 2015 3 Mathieu 21:18
576 2015 4 Mathieu 21:27
577 2015 5 Mathieu 23:07
578 2015 6 Mathieu 21:23
579 2015 7 Mathieu 21:18
580 2015 8 Mathieu 21:27
581 2015 9 Mathieu 23:07
582 2015 10 Mathieu 21:23
583 2015 11 Mathieu 21:18
584 2015 12 Mathieu 21:27
585 2015 13 Mathieu 23:07
586 2015 14 Mathieu 21:23
587 2015 15 Mathieu 21:18
588 2015 16 Mathieu 21:27
589 2015 17 Mathieu 23:07
590 2015 18 Mathieu 21:23
591 2015 19 Mathieu 21:18
592 2015 20 Mathieu 21:27
593 2015 21 Mathieu 17:27
===== ====== ====== ========= ========= =========
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 167:24
3 2015 SLA 1/2014 (welket) 0 0 95:22
4 2013 SLA 2/2014 (welsch) 0 0
5 2014 SLA 2/2014 (welsch) 0 0 172:29
6 2015 SLA 2/2014 (welsch) 0 0 92:40
7 2013 SLA 3/2014 (aab) 0 0
8 2014 SLA 3/2014 (aab) 0 0 170:57
9 2015 SLA 3/2014 (aab) 0 0 90:49
10 2013 SLA 4/2014 (bcc) 0 0
11 2014 SLA 4/2014 (bcc) 0 0 168:21
12 2015 SLA 4/2014 (bcc) 0 0 95:12
13 2013 SLA 5/2014 (dde) 0 0
14 2014 SLA 5/2014 (dde) 0 0 127:31
15 2015 SLA 5/2014 (dde) 0 0 67:52
==== ====== ======= ===================== ================ ================== ========= ======
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 #21)
- 2:49 0:10 (14.01.2015 10:02-13:01 Jean #27)
- 2:58 0:10 (15.01.2015 09:00-12:53 Jean #33)
- 0:45 None (15.01.2015 09:30-10:15 Jean #87)
- 0:05 None (15.01.2015 12:53-12:58 Jean #39)
- 0:12 None (15.01.2015 12:58-13:10 Jean #45)
- 2:08 0:10 (16.01.2015 09:00-11:18 Jean #51)
- 1:30 None (16.01.2015 11:18-12:48 Jean #57)
- 0:10 None (16.01.2015 12:48-12:58 Jean #63)
- 1:52 0:10 (16.01.2015 12:58-15:00 Jean #69)
- 3:19 0:10 (19.01.2015 09:00-12:29 Jean #75)
- 0:37 None (19.01.2015 12:29-13:06 Jean #81)
- 1:02 None (20.01.2015 09:00-10:02 Jean #87)
- 0:45 None (20.01.2015 09:30-10:15 Jean #9)
- 2:49 0:10 (20.01.2015 10:02-13:01 Jean #93)
- 3:43 0:10 (21.01.2015 09:00-12:53 Jean #99)
- 0:05 None (21.01.2015 12:53-12:58 Jean #105)
- 0:12 None (21.01.2015 12:58-13:10 Jean #111)
- 2:08 0:10 (22.01.2015 09:00-11:18 Jean #3)
- 1:30 None (22.01.2015 11:18-12:48 Jean #9)
- 0:10 None (22.01.2015 12:48-12:58 Jean #15)
- 1:52 0:10 (22.01.2015 12:58-15:00 Jean #21)
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 | 73 |
+--------------------+------------+-----------+------------+-----------+------------+
| Mathieu | 20:00 | 21:27 | 95:06 | 0 | 79 |
+--------------------+------------+-----------+------------+-----------+------------+
| Luc | 30:00 | 23:07 | 96:55 | 0 | 392 |
+--------------------+------------+-----------+------------+-----------+------------+
| Jean | 40:00 | 21:23 | 96:51 | 0 | 420 |
+--------------------+------------+-----------+------------+-----------+------------+
| **Total (4 rows)** | **92:00** | **65:57** | **288:52** | **0** | **964** |
+--------------------+------------+-----------+------------+-----------+------------+
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 #4 ('SLA 4/2014 (bcc)'), Subscription #3 ('SLA
3/2014 (aab)'), Subscription #2 ('SLA 2/2014 (welsch)'), Subscription #1 ('SLA
1/2014 (welket)'), Subscription #5 ('SLA 5/2014 (dde)')]
>>> 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('jean').user)
>>> res = test_client.get(url, REMOTE_USER="jean")
>>> 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('jean')
>>> u = ar.get_user()
>>> ar = working.WorkedHours.request(user=u)
>>> ar = ar.spawn(working.WorkedHours)
>>> lst = list(ar)
>>> len(lst)
7
>>> e = ar.table2xhtml()
>>> len(e.findall('./tbody/tr'))
8