Colin Copeland avatar Colin Copeland committed 36ed842 Merge

merge default into stable

Comments (0)

Files changed (10)

timepiece/management/commands/check_entries.py

         """
         main()
         """
-        self.verbosity = kwargs.get('verbosity', 1)
+        verbosity = kwargs.get('verbosity', 1)
         start = self.find_start(**kwargs)
         people = self.find_people(*args)
         self.show_init(start, *args, **kwargs)
         all_entries = self.find_entries(people, start, *args, **kwargs)
         all_overlaps = self.check_all(all_entries, *args, **kwargs)
-        if self.verbosity >= 1:
-            self.stdout.write('Total overlapping entries: %d\n' % all_overlaps)
+        if verbosity >= 1:
+            print 'Total overlapping entries: %d' % all_overlaps
 
     def check_all(self, all_entries, *args, **kwargs):
         """
         """
         With a list of entries, check each entry against every other
         """
+        verbosity = kwargs.get('verbosity', 1)
         user_total_overlaps = 0
         user = ''
         for index_a, entry_a in enumerate(entries):
             #Show the name the first time through
             if index_a == 0:
-                if args and self.verbosity >= 1 or self.verbosity >= 2:
+                if args and verbosity >= 1 or verbosity >= 2:
                     self.show_name(entry_a.user)
                     user = entry_a.user
             if entry_a.is_overlapping():
                 user_total_overlaps += 1
-                self.show_overlap(entry_a)
-                
+                if verbosity >= 1:
+                    self.show_overlap(entry_a)                
 #Removed until check_overlap is fixed
 #            for index_b in range(index_a, len(entries)):
 #                entry_b = entries[index_b]
-#                if entry_a.is_overlapping():
-#                if entry_a.check_overlap(entry_b):
-#                    user_total_overlaps += 1            
+#                if entry_a.check_overlap_pause(entry_b):
+#                    user_total_overlaps += 1
 #                    self.show_overlap(entry_a, entry_b)
                     
-        if user_total_overlaps and user and self.verbosity >= 1:
+        if user_total_overlaps and user and verbosity >= 1:
             overlap_data = {
                 'first': user.first_name,
                 'last': user.last_name,
                 'total': user_total_overlaps,
             }
-            self.stdout.write('Total overlapping entries for user ' + \
-                '%(first)s %(last)s: %(total)d\n' % overlap_data)
+            print 'Total overlapping entries for user ' + \
+                '%(first)s %(last)s: %(total)d' % overlap_data
         return user_total_overlaps
 
     def find_start(self, **kwargs):
         month = kwargs.get('month', False)
         year = kwargs.get('year', False)
         days = kwargs.get('days', 0)
-        #If no flags are True, set to 2 months ago
-        start = datetime.datetime.now() - datetime.timedelta(weeks=8)
+        #If no flags are True, set to the beginning of last billing window
+        #to assure we catch all recent violates
+        start = datetime.datetime.now() - relativedelta(months=1, day=1)
         #Set the start date based on arguments provided through options
         if week:
             start = utils.get_week_start()
             start = datetime.datetime.now() - relativedelta(day=1, month=1)
         if days:
             start = datetime.datetime.now() - \
-            datetime.timedelta(days=self.options.days)
+            datetime.timedelta(days=days)
         start = start - relativedelta(
             hour=0, minute=0, second=0, microsecond=0)
         return start
     #output methods
     def show_init(self, start, *args, **kwargs):
         forever = kwargs.get('all', False)
+        verbosity = kwargs.get('verbosity', 1)
         if forever:
-            if self.verbosity >= 1:
-                self.stdout.write('Checking overlaps from the beginning ' + \
-                    'of time\n')
+            if verbosity >= 1:
+                print 'Checking overlaps from the beginning ' + \
+                    'of time'
         else:
-            if self.verbosity >= 1:
-                self.stdout.write('Checking overlap starting on: ' + \
-                    start.strftime('%m/%d/%Y') + '\n')
+            if verbosity >= 1:
+                print 'Checking overlap starting on: ' + \
+                    start.strftime('%m/%d/%Y')
 
     def show_name(self, person):
-        self.stdout.write('Checking %s %s...\n' % \
-        (person.first_name, person.last_name))
+        print 'Checking %s %s...' % \
+        (person.first_name, person.last_name)
 
     def show_overlap(self, entry_a, entry_b=None):
         def make_output_data(entry):
             output = 'Entry %(entry)d for %(first_name)s %(last_name)s from ' \
             % data_a + '%(start_time)s to %(end_time)s on %(project)s overlaps ' \
             % data_a + 'with another entry.'
-        self.stdout.write(output + '\n')
+        print output

timepiece/models.py

     objects = EntryManager()
     worked = EntryWorkedManager()
 
-    def check_overlap(self, entry_b):
+    def check_overlap(self, entry_b, **kwargs):
         """
         Given two entries, return True if they overlap, otherwise return False
         """
+        pause = kwargs.get('pause', False)
         entry_a = self
         #if entries are open, consider them to be closed right now
         if not entry_a.end_time:
         if not entry_b.end_time:
             entry_b.end_time = datetime.datetime.now()
         #Check the two entries against each other        
-        start_is_inside = entry_a.start_time > entry_b.start_time \
+        start_inside = entry_a.start_time > entry_b.start_time \
             and entry_a.start_time < entry_b.end_time 
-        end_is_inside = entry_a.end_time > entry_b.start_time \
+        end_inside = entry_a.end_time > entry_b.start_time \
+            and entry_a.end_time < entry_b.end_time
+        a_is_inside = entry_a.start_time > entry_b.start_time \
             and entry_a.end_time < entry_b.end_time        
         b_is_inside = entry_a.start_time < entry_b.start_time \
             and entry_a.end_time > entry_b.end_time
-        overlap = start_is_inside or end_is_inside or b_is_inside
-        return overlap
+        overlap = start_inside or end_inside or a_is_inside or b_is_inside
+        if not pause:
+            return overlap
+        else:
+            if overlap:
+                max_end = max(entry_a.end_time, entry_b.end_time)
+                min_start = min(entry_a.start_time, entry_b.start_time)
+                diff = max_end - min_start
+                diff = diff.seconds + diff.days * 86400
+                total = entry_a.get_seconds() + entry_b.get_seconds()
+    #            paused = entry_a.seconds_paused + entry_b.seconds_paused
+    #            if total > diff or paused < diff - total:
+                if total > diff:
+                    return True
+            return False
 
     def is_overlapping(self):
         if self.start_time and self.end_time:
     def hours_in_week(self, date):
         left, right = utils.get_week_window(date)
         entries = Entry.worked.filter(user=self.user)
-        entries = entries.filter(end_time__gt=left,
-            end_time__lt=right, status='approved')
+        entries = entries.filter(
+            (Q(status='invoiced') | Q(status='approved')),
+            end_time__gt=left, end_time__lt=right,)
         return entries.aggregate(s=Sum('hours'))['s']
 
     def overtime_hours_in_week(self, date):
         """
         projects = getattr(settings, 'TIMEPIECE_PROJECTS', {})
         user = self.user
-        entries = user.timepiece_entries.filter(end_time__gt=date,
-                                                end_time__lte=end_date,
-                                                status='approved')
-        data = {'billable': Decimal('0'), 'non_billable': Decimal('0')}
-        data['total'] = entries.aggregate(s=Sum('hours'))['s']
+        entries = user.timepiece_entries.filter(
+            (Q(status='invoiced') | Q(status='approved')),
+            end_time__gt=date, end_time__lt=end_date)
+        data = {
+            'billable': Decimal('0'), 'non_billable': Decimal('0'),
+            'invoiced': Decimal('0'), 'uninvoiced': Decimal('0'),
+            'total': Decimal('0')
+            }
+        invoiced = entries.filter(
+            status='invoiced').aggregate(i=Sum('hours'))['i']
+        uninvoiced = entries.exclude(
+            status='invoiced').aggregate(uninv=Sum('hours'))['uninv']
+        total = entries.aggregate(s=Sum('hours'))['s']
+        if invoiced:
+            data['invoiced'] = invoiced
+        if uninvoiced:
+            data['uninvoiced'] = uninvoiced
+        if total:
+            data['total'] = total
         billable = entries.exclude(project__in=projects.values())
         billable = billable.values(
             'billable',

timepiece/templates/timepiece/time-sheet/people/view.html

     </table>
 {% endif %}
 
-{% if activity_entries %}
-    <table id='activity-summary'>
-        <caption>Billable Summary</caption>
-    {% for row in activity_entries %}
-        <tr>
-            <td>{% if row.billable %}Billable{% else %}Non-billable{% endif %}</td>
-            <td>{{ row.sum }}</td>
-        </tr>
-    {% endfor %}        
-    </table>
-{% endif %}
+<table id='activity-summary'>
+    <caption>Billable Summary</caption>
+    <tr>
+        <td>Billable</td><td>{{summary.billable}}</td>
+    </tr>
+    <tr>
+        <td>Non-billable</td><td>{{summary.non_billable}}</td>
+    </tr>
+    <tr>
+        <td>Invoiced</td><td>{{summary.invoiced}}</td>
+    </tr>
+    <tr>
+        <td>Uninvoiced</td><td>{{summary.uninvoiced}}</td>
+    </tr>
+</table>
 
 <br style='clear: both' />
 
Add a comment to this file

timepiece/templatetags/timepiece_tags.py

File contents unchanged.

timepiece/tests/__init__.py.orig

 from timepiece.tests.projection import *
 from timepiece.tests.projects import *
 from timepiece.tests.user_settings import *
+from timepiece.tests.management import *
 #Test does not reflect current state of the code. Import here when fixed
-from timepiece.tests.management import *
-

timepiece/tests/base.py

     def setUp(self):
         self.user = User.objects.create_user('user', 'u@abc.com', 'abc')
         self.user2 = User.objects.create_user('user2', 'u2@abc.com', 'abc')
+        self.superuser = User.objects.create_user('superuser', 'super@abc.com', 'abc')
         permissions = Permission.objects.filter(
             content_type=ContentType.objects.get_for_model(timepiece.Entry),
             codename__in=('can_clock_in', 'can_clock_out',
         )
         self.user.user_permissions = permissions
         self.user2.user_permissions = permissions
-
+        self.superuser.is_superuser = True
+        self.superuser.save()
         self.user = self.user
         self.activity = timepiece.Activity.objects.create(
             code="WRK",

timepiece/tests/management.py

 import datetime
-import re
 from StringIO import StringIO
+from dateutil.relativedelta import relativedelta
 
 from django.core.urlresolvers import reverse
 from django.contrib.auth.models import User, Permission
 from django.core.exceptions import ValidationError
-from django.core.management import call_command
 
 from timepiece.tests.base import TimepieceDataTestCase
 
 from timepiece import models as timepiece
 from timepiece import forms as timepiece_forms
+from timepiece import utils
 from timepiece.management.commands import check_entries
 
 
             'seconds_paused': 0,
             'status': 'verified',
         }
+        self.good_start = datetime.datetime.now() - datetime.timedelta(days=0, hours=8)
+        self.good_end = datetime.datetime.now() - datetime.timedelta(days=0)
+        self.bad_start = datetime.datetime.now() - datetime.timedelta(days=1, hours=8)
+        self.bad_end = datetime.datetime.now() - datetime.timedelta(days=1)
         #Create users for the test
         self.user.first_name = 'first1'
         self.user.last_name = 'last1'
         self.user2.first_name = 'first2'
         self.user2.last_name = 'last2'
         self.user2.save()
+        self.all_users = [self.user, self.user2, self.superuser]
+        #Create a valid entry for all users on every day since 60 days ago
+        self.make_entry_bulk(self.all_users, 60)
 
-        #Create a list of valid entries.
+    #helper functions           
+    def make_entry(self, **kwargs):
+        """
+        Make a valid or invalid entry
+
+        make_entry(**kwargs) 
+        **kwargs can include: start_time, end_time, valid    
+        Without any kwargs, make_entry makes a valid entry. (first time called)
+        With valid=False, makes an invalid entry
+        start_time and end_time can be specified.
+        If start_time is used without end_time, end_time is 10 mintues later
+        """
+        valid = kwargs.get('valid', True)
+        if valid:
+            default_start = self.good_start
+            default_end = self.good_end
+        else:
+            default_start = self.bad_start
+            default_end = self.bad_end
+        user = kwargs.get('user', self.user)
+        start = kwargs.get('start_time', default_start)
+        if 'end_time' in kwargs:
+            end = kwargs.get('end_time', default_end)
+        else:
+            if 'start_time' in kwargs:
+                end = start + relativedelta(minutes=10)
+            else:
+                end = default_end
+        data = self.default_data
+        data.update({
+            'user': user,
+            'start_time': start,
+            'end_time': end,
+        })
+        self.create_entry(data)
+
+    def make_entry_bulk(self, users, days, *args, **kwargs):
+        """
+        Create entries for users listed, from n days ago (but not today)
+        
+        make_entry_bulk(users_list, num_days)
+        """
         #Test cases may create overlapping entries later
-        for user in [self.user, self.user2]:
-            self.default_data.update({
-                'user': user,
-            })
-            for day in range(0, 80):
+        for user in users:
+            self.default_data.update({'user': user})
+            #Range uses 1 so that good_start/good_end use today as valid times.
+            for day in range(1, days + 1):
                 self.default_data.update({
-                    'start_time': datetime.datetime.now() - datetime.timedelta(days=day, hours=8),
-                    'end_time': datetime.datetime.now() - datetime.timedelta(days=day,),
+                    'start_time': datetime.datetime.now() - \
+                        datetime.timedelta(days=day, minutes=1),
+                    'end_time': datetime.datetime.now() - \
+                        datetime.timedelta(days=day,)
                 })
-                self.create_entry(self.default_data)        
-#        print timepiece.Entry.objects.all()
+                self.create_entry(self.default_data)
 
-    #helper functions
-    buffer_dict = {}
-    overlap_cp = re.compile(
-        '(?P<entry>\d+) for ' + \
-        '(?P<first>\w+) (?P<last>\w+) from ' + \
-        '(?P<start_time_str>\d+-\d+-\d+ \d+:\d+:\d+.\d+) to ' + \
-        '(?P<end_time_str>\d+-\d+-\d+ \d+:\d+:\d+.\d+) on ' + \
-        '(?P<project>\w+)',
-    )
+    #tests
+    def testFindStart(self):
+        """
+        With various kwargs, find_start should return the correct date
+        """
+        #Establish some datetimes
+        now = datetime.datetime.now()
+        today = now - relativedelta(
+            hour=0, minute=0, second=0, microsecond=0)
+        last_billing = today - relativedelta(months=1, day=1)
+        yesterday = today - relativedelta(days=1)
+        ten_days_ago = today - relativedelta(days=10)
+        thisweek = utils.get_week_start(today)
+        thismonth = today - relativedelta(day=1)
+        thisyear = today - relativedelta(month=1, day=1)
+        #Use command flags to obtain datetimes
+        start_default = check_entries.Command().find_start()
+        start_yesterday = check_entries.Command().find_start(days=1)
+        start_ten_days_ago = check_entries.Command().find_start(days=10)
+        start_of_week = check_entries.Command().find_start(week=True)
+        start_of_month = check_entries.Command().find_start(month=True)
+        start_of_year = check_entries.Command().find_start(year=True)
+        #assure the returned datetimes are correct
+        self.assertEqual(start_default, last_billing)
+        self.assertEqual(start_yesterday, yesterday)
+        self.assertEqual(start_ten_days_ago, ten_days_ago)
+        self.assertEqual(start_of_week, thisweek)
+        self.assertEqual(start_of_month, thismonth)
+        self.assertEqual(start_of_year, thisyear)
 
-    def overlaps_to_dict(self, tupe_in):
-        dict_out = {
-            'entry': tupe_in[0],
-            'first_name': tupe_in[1],
-            'last_name': tupe_in[2],
-            'start_time_str': tupe_in[3],
-            'end_time_str': tupe_in[4],
-            'project': tupe_in[5],
-        }
-        return dict_out
+    def testFindPeople(self):
+        """
+        With args, find_people should search and return those user objects
+        Without args, find_people should return all user objects
+        """
+        #Find one person by icontains first or last name, return all if no args
+        people1 = check_entries.Command().find_people('firsT1')
+        people2 = check_entries.Command().find_people('LasT2')
+        all_people = check_entries.Command().find_people()
+        #obtain instances from the querysets
+        person1 = people1.get(pk=self.user.pk)
+        person2 = people2.get(pk=self.user2.pk)
+        all_1 = all_people.get(pk=self.user.pk)
+        all_2 = all_people.get(pk=self.user2.pk)
+        all_3 = all_people.get(pk=self.superuser.pk)
+        self.assertEqual(people1.count(), 1)
+        self.assertEqual(people2.count(), 1)
+        self.assertEqual(all_people.count(), 3)
+        self.assertEqual(person1, self.user)
+        self.assertEqual(person2, self.user2)
+        self.assertEqual(all_1, person1)
+        self.assertEqual(all_2, person2)
+        self.assertEqual(all_3, self.superuser)
 
-    def check(self, *args, **kwargs):
-        output = err = StringIO()
-        call_command('check_entries', *args, stdout=output, stderr=err, **kwargs)
-        output.seek(0)
-        err.seek(0)
-        out = output.read()
-        overlap_tupes = self.overlap_cp.findall(out)
-        overlaps = []
-        for tupe in overlap_tupes:
-            overlaps.append(self.overlaps_to_dict(tupe))
-        out_list = out.split('\n')
-        err_list = err.read().split('\n')
-        self.buffer_dict = {
-            'out': out_list,
-            'err': err_list,
-            'overlap': overlaps
-        }
-        return self.buffer_dict
+    def testFindEntries(self):
+        """
+        Given a list of users and a starting point, entries should generate a
+        list of all entries for each user from that time until now.
+        """
+        start = check_entries.Command().find_start()
+        all_people = check_entries.Command().find_people()
+        entries = check_entries.Command().find_entries(all_people, start)
+        #Determine the number of days checked
+        today = datetime.datetime.now() - \
+            relativedelta(hour=0, minute=0, second=0, microsecond=0)
+        diff = today - start
+        days_checked = diff.days
+        total_entries = 0
+        while True:
+            try:
+                person_entries = entries.next()
+                for entry in person_entries:
+                    total_entries += 1
+            except StopIteration:
+                #Verify that every entry from the start point was returned
+                self.assertEqual(total_entries, days_checked * len(self.all_users))
+                return
 
-    def get_output(self, dict_in=buffer_dict):
-        return dict_in.get('out', [])
-    
-    def get_err(self, dict_in=buffer_dict):
-        return dict_in.get('err', [])
+    def testCheckEntry(self):
+        """
+        Given lists of entries from users, check_entry should return all 
+        overlapping entries.
+        """
+        start = check_entries.Command().find_start()
+        all_people = check_entries.Command().find_people()
+        entries = check_entries.Command().find_entries(all_people, start)
+        total_overlaps = 0
+        #make some bad entries
+        num_days = 5
+        self.make_entry_bulk(self.all_users, num_days)
+        while True:
+            try:
+                person_entries = entries.next()
+                user_overlaps = check_entries.Command().check_entry(
+                    person_entries, verbosity=0)
+                total_overlaps += user_overlaps
+            except StopIteration:
+                self.assertEqual(
+                    total_overlaps, num_days * len(self.all_users) * 2)
+                return
 
-    def get_overlap(self, dict_in=buffer_dict):
-        return dict_in.get('overlap', [])
+    def testCheckOverlap(self):
+        """
+        With every possbile type of overlap, check_overlap should return True
+        With valid entries, check_overlap should return False
+        """
+        #define start and end times relative to a valid entry
+        a_start_before = self.good_start - datetime.timedelta(minutes=2)
+        a_start_inside = self.good_end - datetime.timedelta(minutes=2)
+        a_end_inside = self.good_start + datetime.timedelta(minutes=2)
+        a_end_after = self.good_end + datetime.timedelta(minutes=2)
+        #Create a valid entry for today
+        self.make_entry(valid=True)
 
-    def show_output(self, dict_in=buffer_dict):
-        for string in self.get_output(dict_in):
-            print string
-
-    def show_err(self, dict_in=buffer_dict):
-        for string in self.get_err(dict_in):
-            print string
-
-    def show_overlap(self, dict_in=buffer_dict):
-        for dict_item in self.get_overlap(dict_in):
-            print dict_item.items()
-
-    def testAllEntries(self):
-        self.default_data.update({
-            'start_time': datetime.datetime.now() - datetime.timedelta(days=0, hours=8),
-            'end_time': datetime.datetime.now() - datetime.timedelta(days=0,),
-        })
-        self.create_entry(self.default_data)
-#        check_1 = self.check('first1', verbosity=2)
-#        check_2 = self.check('first2', verbosity=2)
-        bars = check_1_2 = self.check('first1', 'first2', verbosity=2)
-        self.show_overlap(bars)
-        foos = self.get_overlap(check_1_2)
-
-#        print foos[0].get('entry', '')
-#        print check_1_2['out']
+        #Create a bad entry starting inside the valid one
+        self.make_entry(start_time=a_start_inside, end_time=a_end_after)
+        #Create a bad entry ending inside the valid one
+        self.make_entry(start_time=a_start_before, end_time=a_end_inside)
+        #Create a bad entry that starts and ends inside a valid one
+        self.make_entry(start_time=a_start_inside, end_time=a_end_inside)
+        #Bump the day back one so this entry only conflicts with a valid entry
+        a_start_before -= relativedelta(days=1)
+        a_end_after -= relativedelta(days=1)
+        #Create a bad entry that starts and ends outside a valid one
+        self.make_entry(start_time=a_start_before, end_time=a_end_after)
+        entries = timepiece.Entry.objects.filter(user=self.user)
+        user_total_overlaps = 0
+        for index_a, entry_a in enumerate(entries):
+            for index_b in range(index_a, len(entries)):
+                entry_b = entries[index_b]
+                if entry_a.check_overlap(entry_b):
+                    user_total_overlaps += 1
+        self.assertEqual(user_total_overlaps, 4)

timepiece/tests/payroll.py

 from timepiece import forms as timepiece_forms
 from timepiece.tests.base import TimepieceDataTestCase
 
-from dateutil import relativedelta
+from dateutil.relativedelta import relativedelta
 
 
 class PayrollTest(TimepieceDataTestCase):
             hours = 4
             minutes = 0
         if not start:
-            start = datetime.datetime.now()
+            start = datetime.datetime.now() - relativedelta(hour=0)
+            #In case the default would fall off the end of the billing period
+            if start.day >= 28:
+                start -= relativedelta(days=1)
         end = start + datetime.timedelta(hours=hours, minutes=minutes)
         data = {'user': self.user,
                 'start_time': start,
         if status:
             data['status'] = status
         return self.create_entry(data)
-
-    def testPersonSummary(self):
+    def make_logs(self):
         sick = self.create_project()
         vacation = self.create_project()
         settings.TIMEPIECE_PROJECTS = {
             'sick': sick.pk, 'vacation': vacation.pk
         }
-        rp = self.create_person_repeat_period({'user': self.user})
-        start = datetime.date.today().replace(day=1)
-        end = start + relativedelta.relativedelta(months=1)
         billable = self.log_time(delta=(3, 30), status='approved')
         non_billable = self.log_time(delta=(2, 0),
             billable=False, status='approved')
         sick = self.log_time(delta=(8, 0), project=sick, status='approved')
         vacation = self.log_time(delta=(4, 0), project=vacation,
             status='approved')
-        #if the billing period can't contain these hours, expand it
-        if end < datetime.date.today() + relativedelta.relativedelta(days=2):
-            end += relativedelta.relativedelta(days=1)
+        #make an entry on the very last day no matter the current time 
+        #but start in the morning to stay in the billing period.
+        end_day = datetime.datetime.now() + \
+            relativedelta(months=1, day=1, hour=0) - \
+            relativedelta(days=1)
+        last_day = self.log_time(start=end_day, status='approved', delta=(8,0))
+
+    def testPersonSummary(self):
+        self.make_logs()
+        rp = self.create_person_repeat_period({'user': self.user})
+        start = datetime.date.today().replace(day=1)
+        end = start + relativedelta(months=1)
         summary = rp.summary(start, end)
-        self.assertEqual(summary['billable'], Decimal('3.50'))
+        self.check_summary(summary)
+    
+    def testPersonSummaryView(self):
+        from timepiece.templatetags import timepiece_tags as tags
+        self.client.login(username='superuser', password='abc')
+        self.make_logs()
+        rp = self.create_person_repeat_period({'user': self.user})
+        response = self.client.get(reverse('payroll_summary'), follow=True)
+        context = response.context
+        date_filters = tags.date_filters(context, 'months')
+        this_month = date_filters['filters'].values()[0][-1]
+        this_month_url = this_month[1]
+        response = self.client.get(this_month_url, follow=True)
+        start = response.context['from_date']
+        end = response.context['to_date']
+        this_user = response.context['periods'].get(user=self.user.pk)
+        summary = this_user.summary(start, end)
+        self.check_summary(summary)
+
+    def check_summary(self, summary):
+        self.assertEqual(summary['billable'], Decimal('11.50'))
         self.assertEqual(summary['non_billable'], Decimal('2.00'))
         self.assertEqual(summary['paid_leave']['sick'], Decimal('8.00'))
         self.assertEqual(summary['paid_leave']['vacation'], Decimal('4.00'))
-        self.assertEqual(summary['total'], Decimal('17.50'))
+        self.assertEqual(summary['total'], Decimal('25.50'))
 
     def testWeeklyHours(self):
         """ Test basic functionality of hours worked per week """

timepiece/utils.py

         if request.GET:
             form = timepiece_forms.DateForm(request.GET)
             if form.is_valid():
-                from_date = form.cleaned_data.get('from_date')
-                to_date = form.cleaned_data.get('to_date')
-                form.save()
+                from_date, to_date = form.save()
                 status = form.cleaned_data.get('status')
                 activity = form.cleaned_data.get('activity')
             else:

timepiece/views.py

     if to_date:
         dates &= Q(end_time__lte=to_date)
     project_totals = entries.filter(dates).annotate(total_hours=Sum('hours'))
+    project_totals = project_totals.order_by('project__name')
     total_hours = timepiece.Entry.objects.filter(dates).aggregate(
         hours=Sum('hours')
     )['hours']
         end_time__lt=window.end_date,
     ).select_related(
         'user',
+        'project',
+        'activity',
+        'location',
     ).order_by('start_time')
     if project:
         entries = entries.filter(project=project)
     project_entries = entries.order_by().values(
         'project__name',
     ).annotate(sum=Sum('hours')).order_by('-sum')
-    activity_entries = entries.order_by().values(
-        'billable',
-    ).annotate(sum=Sum('hours')).order_by('-sum')
 
     show_approve = show_verify = False
     if request.user.has_perm('timepiece.edit_person_time_sheet') or \
         total_statuses = len(statuses)
         unverified_count = statuses.count('unverified')
         verified_count = statuses.count('verified')
+        approved_count = statuses.count('approved')
 
     if time_sheet.user.pk == request.user.pk:
         show_verify = unverified_count != 0
 
     if request.user.has_perm('timepiece.edit_person_time_sheet'):
-        show_approve = verified_count == total_statuses and total_statuses != 0
+        show_approve = verified_count + approved_count == total_statuses \
+        and verified_count > 0 and total_statuses != 0
 
+    summary = time_sheet.summary(window.date, window.end_date)
     context = {
         'show_verify': show_verify,
         'show_approve': show_approve,
         'entries': entries,
         'total': total_hours,
         'project_entries': project_entries,
-        'activity_entries': activity_entries,
+        'summary': summary,
     }
     return context
 
             )
         if request.GET and form.cleaned_data.get('project'):
             entries = entries.filter(project=form.cleaned_data.get('project'))
+    to_date -= relativedelta(days=1)
     if action == 'invoice':
         return_url = reverse('invoice_projects',)
         get_str = urllib.urlencode({
             'from_date': from_date or '',
-            'to_date': to_date or '',
+            'to_date': to_date  or '',
         })
         return_url += '?%s' % get_str
     else:
         'form': form,
         'cals': cals,
         'project_totals': project_totals,
-        'to_date': to_date,
+        'to_date': to_date - relativedelta(days=1),
         'from_date': from_date,
         'unverified': unverified,
         'unapproved': unapproved,
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.