Time tracking you can host anywhere, built on Django. Full export support in multiple formats and easily extensible.
djangodockerhandcodedpythonself-hostedtime-trackingtimetracker
1from datetime import date
2from decimal import Decimal, ROUND_DOWN
3
4from django.contrib.sites.models import Site
5from django.db import models
6from django.db.models import Sum
7
8from conf.utils import current_site_id
9from conf.managers import CurrentSiteManager
10
11from .utils import duration_string, duration_decimal
12
13
14class Client(models.Model):
15 name = models.CharField(max_length=255)
16 archive = models.BooleanField(default=False)
17 payment_id = models.CharField(max_length=255, blank=True, null=True)
18 sites = models.ManyToManyField(Site)
19
20 objects = models.Manager()
21 on_site = CurrentSiteManager()
22
23 class Meta:
24 default_permissions = ("view", "add", "change", "delete")
25 ordering = ["-id"]
26
27 def __str__(self):
28 return "Client: " + self.name
29
30 def get_total_projects(self):
31 return self.projects.count()
32
33 def get_total_duration(self):
34 return duration_string(
35 self.projects.aggregate(Sum("entries__duration"))["entries__duration__sum"]
36 )
37
38
39class Project(models.Model):
40 client = models.ForeignKey(
41 "Client", related_name="projects", on_delete=models.CASCADE
42 )
43 name = models.CharField(max_length=255)
44 archive = models.BooleanField(default=False)
45 estimate = models.DecimalField(
46 max_digits=10, decimal_places=2, blank=True, null=True
47 )
48
49 class Meta:
50 default_permissions = ("view", "add", "change", "delete")
51 ordering = ["client", "-id"]
52
53 def __str__(self):
54 return "Project: " + self.name
55
56 def get_total_entries(self):
57 return self.entries.count()
58
59 def get_total_cost(self):
60 total_cost = Decimal()
61 for entry in self.entries.iterator():
62 try:
63 if entry.task.hourly_rate:
64 total_cost += (
65 duration_decimal(entry.duration) * entry.task.hourly_rate
66 )
67 except: # noqa: E722
68 continue
69 return total_cost.quantize(Decimal(".01"), rounding=ROUND_DOWN)
70
71 def get_total_duration(self):
72 return duration_string(self.entries.aggregate(Sum("duration"))["duration__sum"])
73
74 def get_percent_done(self):
75 if self.estimate is not None:
76 total_cost = Decimal(self.get_total_cost())
77 total_estimate = Decimal(self.estimate)
78 return int(100 * (total_cost / total_estimate))
79 return None
80
81
82class Task(models.Model):
83 name = models.CharField(max_length=255)
84 hourly_rate = models.DecimalField(
85 max_digits=10, decimal_places=2, blank=True, null=True
86 )
87 sites = models.ManyToManyField(Site)
88
89 objects = models.Manager()
90 on_site = CurrentSiteManager()
91
92 class Meta:
93 default_permissions = ("view", "add", "change", "delete")
94
95 def __str__(self):
96 return "Task: " + self.name
97
98
99class Entry(models.Model):
100 project = models.ForeignKey(
101 "Project", related_name="entries", on_delete=models.CASCADE
102 )
103 task = models.ForeignKey(
104 "core.Task",
105 related_name="entries",
106 blank=True,
107 null=True,
108 on_delete=models.SET_NULL,
109 )
110 user = models.ForeignKey(
111 "auth.User", related_name="entries", on_delete=models.CASCADE
112 )
113 date = models.DateField(blank=True)
114 duration = models.DurationField(blank=True)
115 datetime_start = models.DateTimeField(blank=True, null=True)
116 datetime_end = models.DateTimeField(blank=True, null=True)
117 note = models.TextField(blank=True, null=True)
118 site = models.ForeignKey(Site, default=current_site_id(), on_delete=models.CASCADE)
119
120 objects = models.Manager()
121 on_site = CurrentSiteManager()
122
123 class Meta:
124 default_permissions = ("view", "add", "change", "delete")
125 ordering = ["-date", "-id"]
126 verbose_name_plural = "Entries"
127
128 def save(self, *args, **kwargs):
129 if not self.date:
130 self.date = date.today()
131 if not self.site:
132 self.site = Site.objects.get(id=current_site_id())
133 super().save(*args, **kwargs)
134
135 def __str__(self):
136 return "Entry for " + self.project.name + " by " + self.user.username