Work time tracking

The lino_xl.lib.working adds functionality for managing work time tracking.

A tested document

This is a tested document. The following instructions are used for initialization:

>>> from lino import startup
>>> startup('lino_book.projects.team.settings.doctests')
>>> from lino.api.doctest import *

Note that the demo data is on fictive demo date May 23, 2015:

>>> dd.today()
datetime.date(2015, 5, 23)

Overview

A work session is when a user works on a "ticket" for a given lapse of time.

The WorkedHours table shows the last seven days, one row per day, with your working hours.

A service report is a document used in various discussions with a stakeholder.

The ticket_model defines what a ticket actually is. Theoretically it can be any model which implements Workable. In Lino Noi this points to tickets.Ticket and currently it probably won't work on any other model

Work sessions

Extreme case of a work session:

  • I start to work on an existing ticket #1 at 9:23. A customer phones at 10:17 with a question. I create #2. That call is interrupted several times by the customer himself. During the first interruption another customer calls, with another problem (ticket #3) which we solve together within 5 minutes. During the second interruption of #2 (which lasts 7 minutes) I make a coffee break.

    During the third interruption I continue to analyze the customer's problem. When ticket #2 is solved, I decided that it's not worth to keep track of each interruption and that the overall session time for this ticket can be estimated to 0:40.

    Ticket start end    Pause  Duration
    #1      9:23 13:12  0:45
    #2     10:17 11:12  0:12       0:43
    #3     10:23 10:28             0:05
    

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    Ticket #
---------------------------------- --------- ------------ ------------ ------------ ---------- ------------ --------- ----------- -----------------
 #1 (⛶ Föö fails to bar when baz)   Jean      23/05/2015   09:00:00                                                                `#1 <Detail>`__
 #1 (⛶ Föö fails to bar when baz)   Luc       23/05/2015   09:00:00                                                                `#1 <Detail>`__
 #1 (⛶ Föö fails to bar when baz)   Mathieu   23/05/2015   09:00:00                                                                `#1 <Detail>`__
 #2 (☎ Bar is not always baz)       Jean      22/05/2015   09:00:00     22/05/2015   11:18:00                          2:18        `#2 <Detail>`__
 #2 (☎ Bar is not always baz)       Luc       22/05/2015   09:00:00     22/05/2015   12:29:00                          3:29        `#2 <Detail>`__
 #2 (☎ Bar is not always baz)       Mathieu   22/05/2015   09:00:00     22/05/2015   12:53:00                          3:53        `#2 <Detail>`__
 #4 (⚒ Foo and bar don't baz)       Mathieu   20/05/2015   09:05:00     20/05/2015   09:17:00                          0:12        `#4 <Detail>`__
 #3 (☉ Baz sucks)                   Jean      20/05/2015   09:00:00     20/05/2015   10:30:00                          1:30        `#3 <Detail>`__
 #3 (☉ Baz sucks)                   Luc       20/05/2015   09:00:00     20/05/2015   09:37:00                          0:37        `#3 <Detail>`__
 #3 (☉ Baz sucks)                   Mathieu   20/05/2015   09:00:00     20/05/2015   09:05:00                          0:05        `#3 <Detail>`__
 #4 (⚒ Foo and bar don't baz)       Jean      19/05/2015   09:00:00     19/05/2015   09:10:00                          0:10        `#4 <Detail>`__
 #4 (⚒ Foo and bar don't baz)       Luc       19/05/2015   09:00:00     19/05/2015   10:02:00                          1:02        `#4 <Detail>`__
 #5 (☾ Cannot create Foo)           Mathieu   19/05/2015   09:00:00     19/05/2015   11:18:00                          2:18        `#5 <Detail>`__
 **Total (13 rows)**                                                                                                   **15:34**
================================== ========= ============ ============ ============ ========== ============ ========= =========== =================

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)   Mathieu   0:12
 #4 (⚒ Foo and bar don't baz)   Jean      0:10
 #4 (⚒ Foo and bar don't baz)   Luc       1:02
 **Total (3 rows)**                       **1:24**
============================== ========= ==========

Worked hours

>>> rt.login('jean').show(working.WorkedHours)
... 
============================= ================= ========== ========== ====== ==========
 Description                   Worked tickets    Regular    Extra      Free   Total
----------------------------- ----------------- ---------- ---------- ------ ----------
 `Sat 23/05/2015 <Detail>`__   `#1 <Detail>`__   0:01                         0:01
 `Fri 22/05/2015 <Detail>`__   `#2 <Detail>`__              2:18              2:18
 `Thu 21/05/2015 <Detail>`__                                                  0:00
 `Wed 20/05/2015 <Detail>`__   `#3 <Detail>`__              1:30              1:30
 `Tue 19/05/2015 <Detail>`__   `#4 <Detail>`__   0:10                         0:10
 `Mon 18/05/2015 <Detail>`__                                                  0:00
 `Sun 17/05/2015 <Detail>`__                                                  0:00
 **Total (7 rows)**                              **0:11**   **3:48**          **3:59**
============================= ================= ========== ========== ====== ==========

In the "description" column you see a list of the tickets on which you worked that day. This is a convenient way to continue some work you started some days ago.

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.

Database model: ServiceReport.

A service report currently contains three tables:

  • a list of work sessions

  • a list of the tickets mentioned in the work sessions and their invested time

  • a list of sites mentioned in the work sessions and their invested time

>>> obj = working.ServiceReport.objects.get(pk=1)
>>> obj.printed_by.build_method
<printing.BuildMethods.weasy2html:weasy2html>
>>> obj.interesting_for
Partner #100 ('Rumma & Ko OÜ')
>>> rt.show(working.SessionsByReport, obj)
... 
==================== ============ ========== ============ ================== ========== ======= ======
 Start date           Start time   End Time   Break Time   Description        Regular    Extra   Free
-------------------- ------------ ---------- ------------ ------------------ ---------- ------- ------
 23/05/2015           09:00:00                             `#1 <Detail>`__    0:01
 22/05/2015           09:00:00     12:29:00                `#11 <Detail>`__   3:29
 20/05/2015           09:00:00     09:05:00                `#6 <Detail>`__    0:05
 **Total (3 rows)**                                                           **3:35**
==================== ============ ========== ============ ================== ========== ======= ======

Note that sessions on #1 have actually no duration because they are active. But in the report they are shown with one minute. That's a bug (TODO: fix it). But it's not an urgent bug because that's not any normal situation (you are not going to write reports for a date range when there are still active session).

>>> rt.login("robin").show(working.TicketsByReport, obj)
... 
==== =============================================== =============== ======== ========= ========== ======= ======
 ID   Ticket                                          End user        Site     State     Regular    Extra   Free
---- ----------------------------------------------- --------------- -------- --------- ---------- ------- ------
 1    `#1 (⛶ Föö fails to bar when baz) <Detail>`__   Andreas Arens   welket   New       0:03
 4    `#4 (⚒ Foo and bar don't baz) <Detail>`__       Andreas Arens   welket   Working   1:24
                                                                                         **1:27**
==== =============================================== =============== ======== ========= ========== ======= ======

Reporting type

The reporting_type of a site indicates how the client is going to pay for the work done.

The default implementation offers three choices "Worker", "Employer" and "Customer". "Worker" is for volunteer work and "private fun" where the worker does not get paid by anybody. "Employer" is when working time should be reported to the employer (but no customer is going to pay for it directly). "Customer" is when working time should be reported to the customer.

>>> rt.show(working.ReportingTypes)
======= ========= =========
 value   name      text
------- --------- ---------
 10      regular   Regular
 20      extra     Extra
 30      free      Free
======= ========= =========

The local site admin can adapt above list to the site's needs. He also defines a default reporting type:

>>> dd.plugins.working.default_reporting_type
<working.ReportingTypes.regular:10>

Class reference

class lino_xl.lib.working.Plugin
class lino_xl.lib.working.SessionType

The type of a Session.

class lino_xl.lib.working.Session

Django model representing a work 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 and end_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.

site_ref
class lino_xl.lib.working.Sessions
class lino_xl.lib.working.SessionsByTicket

The "Sessions" panel in the detail of a ticket.

slave_summary

This panel shows:

class lino_xl.lib.working.MySessions
class lino_xl.lib.working.MySessionsByDate
class lino_xl.lib.working.StartTicketSession

Start a session on this ticket.

class lino_xl.lib.working.EndTicketSession

Close this session, i.e. stop working it for now.

Common base for EndThisSession and EndTicketSession.

class lino_xl.lib.working.EndTicketSession

End your running session on this ticket.

class lino_xl.lib.working.EndThisSession

Close this session, i.e. stop working on that ticket now.

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 work session on this object.

on_worked()

This is automatically called when a work session has been created or modified.

start_session()

See StartTicketSession.

end_session()

See EndTicketSession.

class lino_xl.lib.working.ServiceReport

The Django model representing a service report.

Database fields:

user

This can be empty and will then show the working time of all users.

start_date
end_date
interesting_for

Show only tickets on sites assigned to this partner.

ticket_state

Show only tickets having this state.

printed

See lino_xl.lib.exerpts.Certifiable.printed

class lino_xl.lib.working.SessionsByReport
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
class lino_xl.lib.working.ShowMySessionsByDay

Show all sessions on the same 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.

class lino_xl.lib.working.WorkedHours

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.

>>> rt.show(working.SiteSummaries, exclude=dict(regular_hours=""))
... 
==== ====== ======= ======== ================ ================== ========= ======= ======
 ID   Year   Month   Site     Active tickets   Inactive tickets   Regular   Extra   Free
---- ------ ------- -------- ---------------- ------------------ --------- ------- ------
 3    2015           welket   0                0                  1:24
==== ====== ======= ======== ================ ================== ========= ======= ======
>>> rt.show(working.UserSummaries, exclude=dict(regular_hours=""))
... 
===== ====== ======= ========= ========= ======= ======
 ID    Year   Month   User      Regular   Extra   Free
----- ------ ------- --------- --------- ------- ------
 29    2015   5       Jean      0:10      3:48
 65    2015   5       Luc       1:02      4:06
 137   2015   5       Mathieu   0:12      3:58    2:18
===== ====== ======= ========= ========= ======= ======

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 vc3:5 *
>>> working.WorkedHours.get_data_elem('detail_link')
lino.core.actors.Actor.detail_link