def test_get(self): """Get without param gets entries for this week.""" response = self.client.get(reverse('dashboard')) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['week_start'], utils.get_week_start().date()) self.assertEqual(response.context['week_end'], utils.get_week_start().date() + relativedelta(days=6))
def test_week_start(self): """ Test that all days Sun. through Sat. return the previous Monday""" monday = datetime.date(2011, 1, 10) self.assertEqual(monday, utils.get_week_start(monday).date()) sunday = datetime.date(2011, 1, 16) self.assertEqual(monday, utils.get_week_start(sunday).date()) following_monday = datetime.date(2011, 1, 17) saturday = datetime.date(2011, 1, 22) self.assertEqual(following_monday, utils.get_week_start(saturday).date())
def test_assignment_active_within_week(self): """ Test manager returns assignments that contain entire week """ start = utils.get_week_start() - datetime.timedelta(weeks=1) end = start + datetime.timedelta(weeks=3) ca = self._assign(start, end) week = utils.get_week_start() next_week = week + datetime.timedelta(weeks=1) assignments = timepiece.ContractAssignment.objects assignments = assignments.active_during_week(week, next_week) self.assertTrue(assignments.filter(pk=ca.pk).exists())
def test_assignment_active_starts_mid_week(self): """ Test manager returns assignments that start before window """ start = utils.get_week_start() + datetime.timedelta(days=2) end = start + datetime.timedelta(weeks=2) ca = self._assign(start, end) week = utils.get_week_start() next_week = week + datetime.timedelta(weeks=1) assignments = timepiece.ContractAssignment.objects assignments = assignments.active_during_week(week, next_week) self.assertTrue(assignments.filter(pk=ca.pk).exists())
def setUp(self): super(ProjectHoursListViewTestCase, self).setUp() self.past_week = utils.get_week_start(datetime.date(2012, 4, 1)).date() self.current_week = utils.get_week_start().date() for i in range(5): self.create_project_hours_entry(self.past_week, published=True) self.create_project_hours_entry(self.current_week, published=True) self.url = reverse('view_schedule') self.client.login(username='******', password='******') self.date_format = '%Y-%m-%d'
def setUp(self): super(ProjectHoursListViewTestCase, self).setUp() self.past_week = utils.get_week_start(datetime.date(2012, 4, 1)).date() self.current_week = utils.get_week_start().date() for i in range(5): factories.ProjectHours(week_start=self.past_week, published=True) factories.ProjectHours(week_start=self.current_week, published=True) self.url = reverse("view_schedule") self.login_user(self.user) self.date_format = "%Y-%m-%d"
def dispatch(self, request, *args, **kwargs): # Since we use get param in multiple places, attach it to the class default_week = utils.get_week_start(datetime.date.today()).date() if request.method == 'GET': week_start_str = request.GET.get('week_start', '') else: week_start_str = request.POST.get('week_start', '') # Account for an empty string self.week_start = default_week if week_start_str == '' \ else utils.get_week_start(datetime.datetime.strptime( week_start_str, '%Y-%m-%d').date()) return super(ScheduleMixin, self).dispatch(request, *args, **kwargs)
def grouped_totals(entries): select = { "day": {"date": """DATE_TRUNC('day', end_time)"""}, "week": {"date": """DATE_TRUNC('week', end_time)"""}, } weekly = entries.extra(select=select["week"]).values('date', 'billable') weekly = weekly.annotate(hours=Sum('hours')).order_by('date') daily = entries.extra(select=select["day"]).values('date', 'project__name', 'billable') daily = daily.annotate(hours=Sum('hours')).order_by('date', 'project__name') weeks = {} for week, week_entries in groupby(weekly, lambda x: x['date']): if week is not None: week = add_timezone(week) weeks[week] = get_hours_summary(week_entries) days = [] last_week = None for day, day_entries in groupby(daily, lambda x: x['date']): week = get_week_start(day) if last_week and week > last_week: yield last_week, weeks.get(last_week, {}), days days = [] days.append((day, daily_summary(day_entries))) last_week = week yield week, weeks.get(week, {}), days
def test_week_filter_midweek(self): """Filter corrects mid-week date to Monday of specified week.""" wednesday = datetime.date(2012, 7, 4) monday = utils.get_week_start(wednesday).date() data = {"week_start": wednesday.strftime(self.date_format), "submit": ""} response = self.client.get(self.url, data) self.assertEquals(response.context["week"].date(), monday)
def weekly_commitment(self, day=None): self._log("Commitment for {0}".format(day)) # earlier assignments may have already allocated time for this week unallocated = self.unallocated_hours_for_week(day) self._log('Unallocated hours {0}'.format(unallocated)) reserved = self.remaining_min_hours() self._log('Reserved hours {0}'.format(reserved)) # start with unallocated hours commitment = unallocated # reserve required hours on later assignments (min_hours_per_week) commitment -= self.remaining_min_hours() self._log('Commitment after reservation {0}'.format(commitment)) # if we're under the needed minimum hours and we have available # time, then raise our commitment to the desired level if commitment < self.min_hours_per_week \ and unallocated >= self.min_hours_per_week: commitment = self.min_hours_per_week self._log('Commitment after minimum weekly hours {0}'\ .format(commitment)) # calculate hours left on contract (subtract worked hours this week) week_start = utils.get_week_start(day) remaining = self.num_hours - self._filtered_hours_worked(week_start) total_allocated = self.blocks.aggregate(s=Sum('hours'))['s'] or 0 remaining -= total_allocated if remaining < 0: remaining = 0 self._log('Remaining {0}'.format(remaining)) # reduce commitment to remaining hours if commitment > remaining: commitment = remaining self._log('Final commitment {0}'.format(commitment)) return commitment
def test_weekly_commitment_over_remaining(self): # 1 week assignment, 20 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=1) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=20) # only 20 hours left on assignment self.assertEqual(ca.weekly_commitment(start), 20)
def testFindStart(self): """ With various kwargs, find_start should return the correct date """ # Establish some datetimes now = timezone.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 test_single_assignment_projection(self): # 2 weeks, 60 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=60) run_projection() self.assertEqual(60, ca.blocks.aggregate(s=Sum('hours'))['s'])
def test_generate_dates(self): """ Test generation of full date ranges """ ### test WEEKLY # 2 weeks start = datetime.date(2011, 1, 17) end = datetime.date(2011, 1, 29) weeks = utils.generate_dates(start=start, end=end) self.assertEqual(2, weeks.count()) # 3 weeks start = datetime.date(2011, 1, 17) end = datetime.date(2011, 1, 31) weeks = utils.generate_dates(start=start, end=end) self.assertEqual(3, weeks.count()) # random weeks num = random.randint(5, 20) start = utils.get_week_start(datetime.date.today()) end = start + datetime.timedelta(weeks=num - 1) weeks = utils.generate_dates(start=start, end=end) self.assertEqual(num, weeks.count()) ### test MONTHLY start = datetime.date(2011, 1, 17) end = datetime.date(2011, 4, 29) months = utils.generate_dates(start=start, end=end, by='month') self.assertEqual(4, months.count()) for index, month in enumerate(months): self.assertEqual(month.date(), datetime.date(2011, index + 1, 1)) ### test DAILY start = datetime.date(2011, 2, 1) end = datetime.date(2011, 2, 15) days = utils.generate_dates(start=start, end=end, by='day') self.assertEqual(15, days.count()) for index, day in enumerate(days): self.assertEqual(day.date(), datetime.date(2011, 2, index + 1))
def create_project_hours_entry(self, week_start=None, project=None, user=None, hours=None, **kwargs): week_start = week_start or utils.get_week_start(add_tzinfo=False) project = project or self.create_project() user = user or self.create_user() hours = Decimal(str(random.random() * 20)) if hours is None else hours return ProjectHours.objects.create(week_start=week_start, project=project, user=user, hours=hours, **kwargs)
def test_no_entries(self): date = utils.get_week_start(datetime.date(2012, 3, 15)) data = { 'week_start': date.strftime('%Y-%m-%d'), 'submit': '', } response = self.client.get(self.url, data) self.assertEquals(len(response.context['projects']), 0) self.assertEquals(len(response.context['users']), 0)
def get_entries_data(self): projects = utils.get_setting('TIMEPIECE_PAID_LEAVE_PROJECTS') # Account for the day added by the form query = Q(end_time__gt=utils.get_week_start(self.from_date), end_time__lt=self.to_date + relativedelta(days=1)) query &= ~Q(project__in=projects.values()) entries = timepiece.Entry.objects.date_trunc('week', extra_values=('activity', 'project__status')).filter(query) return entries
def test_weekly_commmitment_with_hours_worked(self): """ Test weekly commitment with previously logged hours """ start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=30) self.log_time(ca, start=start, delta=(10, 0)) self.assertEqual(ca.hours_worked, 10) self.assertEqual(ca.hours_remaining, 20) self.assertEqual(ca.weekly_commitment(start), 30)
def setUp(self): super(ProjectHoursEditTestCase, self).setUp() self.permission = Permission.objects.filter(codename='add_projecthours') self.manager = self.create_user('manager', '*****@*****.**', 'abc') self.manager.user_permissions = self.permission self.view_url = reverse('edit_project_hours') self.ajax_url = reverse('project_hours_ajax_view') self.week_start = utils.get_week_start(datetime.date.today()) self.next_week = self.week_start + relativedelta(days=7) self.future = self.week_start + relativedelta(days=14)
def test_unallocated_hours(self): """ Test unallocated hours calculation """ start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=40) unallocated_hours = ca.unallocated_hours_for_week(start) self.assertEqual(unallocated_hours, 40) ca.blocks.create(date=start, hours=5) unallocated_hours = ca.unallocated_hours_for_week(start) self.assertEqual(unallocated_hours, 35)
def setUp(self): super(ProjectHoursEditTestCase, self).setUp() self.permission = Permission.objects.filter(codename="add_projecthours") self.manager = factories.User() self.manager.user_permissions = self.permission self.view_url = reverse("edit_schedule") self.ajax_url = reverse("ajax_schedule") self.week_start = utils.get_week_start(datetime.date.today()) self.next_week = self.week_start + relativedelta(days=7) self.future = self.week_start + relativedelta(days=14)
def create_project_hours_entry(self, week_start=None, project=None, user=None, hours=None, published=None): week_start = (utils.get_week_start(add_tzinfo=False) if not week_start else week_start) project = self.create_project() if not project else project user = self.create_user() if not user else user hours = Decimal(str(random.random() * 20)) if not hours else hours return timepiece.ProjectHours.objects.create(week_start=week_start, project=project, user=user, hours=hours, published=published or False)
def hourly_report(request, date_form, from_date, to_date, status, activity): if not from_date: from_date = utils.get_month_start(datetime.datetime.today()).date() if not to_date: to_date = from_date + relativedelta(months=1) header_to = to_date - relativedelta(days=1) trunc = timepiece_forms.ProjectFiltersForm.DEFAULT_TRUNC query = Q(end_time__gt=utils.get_week_start(from_date), end_time__lt=to_date) if 'ok' in request.GET or 'export' in request.GET: form = timepiece_forms.ProjectFiltersForm(request.GET) if form.is_valid(): trunc = form.cleaned_data['trunc'] if not form.cleaned_data['paid_leave']: projects = getattr(settings, 'TIMEPIECE_PROJECTS', {}) query &= ~Q(project__in=projects.values()) if form.cleaned_data['pj_select']: query &= Q(project__in=form.cleaned_data['pj_select']) else: form = timepiece_forms.ProjectFiltersForm() hour_type = form.get_hour_type() entries = timepiece.Entry.objects.date_trunc(trunc).filter(query) date_headers = utils.generate_dates(from_date, header_to, by=trunc) project_totals = utils.project_totals(entries, date_headers, hour_type, total_column=True) if entries else '' if not request.GET.get('export', False): return { 'date_form': date_form, 'from_date': from_date, 'date_headers': date_headers, 'pj_filters': form, 'trunc': trunc, 'project_totals': project_totals, } else: from_date_str = from_date.strftime('%m-%d') to_date_str = to_date.strftime('%m-%d') response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = \ 'attachment; filename="%s_hours_%s_to_%s_by_%s.csv"' % ( hour_type, from_date_str, to_date_str, trunc) writer = csv.writer(response) headers = ['Name'] headers.extend([date.strftime('%m/%d/%Y') for date in date_headers]) headers.append('Total') writer.writerow(headers) for rows, totals in project_totals: for name, hours in rows: data = [name] data.extend(hours) writer.writerow(data) total = ['Totals'] total.extend(totals) writer.writerow(total) return response
def test_this_weeks_priority_type(self): """ Test categories for this week. """ start = utils.get_week_start(datetime.date.today()) end = start + datetime.timedelta(weeks=1) ca_starting = self._assign(start, end, hours=40) self.assertEqual('starting', ca_starting.this_weeks_priority_type) end = utils.get_week_start() start = end - datetime.timedelta(days=2) ca_ending = self._assign(start, end, hours=40) self.assertEqual('ending', ca_ending.this_weeks_priority_type) start = utils.get_week_start() end = start + datetime.timedelta(days=6) ca_starting_ending = self._assign(start, end, hours=40) self.assertEqual('ending', ca_starting_ending.this_weeks_priority_type) start = utils.get_week_start() - datetime.timedelta(days=1) end = start + datetime.timedelta(days=9) ca_ongoing = self._assign(start, end, hours=40) self.assertEqual('ongoing', ca_ongoing.this_weeks_priority_type) ## Need to test order goes ending, starting, ongoing. assignments = timepiece.ContractAssignment.objects.sort_by_priority()
def setUp(self): super(ProjectHoursEditTestCase, self).setUp() self.permission = Permission.objects.filter( codename='add_projecthours') self.manager = factories.User() self.manager.user_permissions = self.permission self.view_url = reverse('edit_schedule') self.ajax_url = reverse('ajax_schedule') self.week_start = utils.get_week_start(datetime.date.today()) self.next_week = self.week_start + relativedelta(days=7) self.future = self.week_start + relativedelta(days=14)
def setUp(self): super(ProjectHoursEditTestCase, self).setUp() self.permission = Permission.objects.filter( codename='add_projecthours') self.manager = self.create_user('manager', '*****@*****.**', 'abc') self.manager.user_permissions = self.permission self.view_url = reverse('edit_project_hours') self.ajax_url = reverse('project_hours_ajax_view') self.week_start = utils.get_week_start(datetime.date.today()) self.next_week = self.week_start + relativedelta(days=7) self.future = self.week_start + relativedelta(days=14)
def setUp(self): self.today = datetime.date(2012, 11, 7) self.this_week = utils.get_week_start(self.today) self.next_week = self.this_week + relativedelta(days=7) self.user = factories.User() self.project = factories.Project() self.activity = factories.Activity() self.location = factories.Location() self.status = Entry.UNVERIFIED
def test_no_remaining_hours(self): """ Gurantee no overcommittment """ # 1 week, 40 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=1) - datetime.timedelta(days=1) ca1 = self._assign(start, end, hours=40) ca1.blocks.create(date=start, hours=40) self.assertEqual(ca1.weekly_commitment(start), 0) # 2 weeks, 40 hours end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca2 = self._assign(start, end, hours=40) self.assertEqual(ca2.weekly_commitment(start), 0)
def get_dates(self): today = datetime.date.today() day = today if 'week_start' in self.request.GET: param = self.request.GET.get('week_start') try: day = datetime.datetime.strptime(param, '%Y-%m-%d').date() except: pass week_start = utils.get_week_start(day) week_end = week_start + relativedelta(days=6) return today, week_start, week_end
def test_weekly_commmitment_with_earlier_allocation(self): """ Test calculation of contract assignment's weekly commitment """ # 1 week assignment, 20 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=1) - datetime.timedelta(days=1) ca1 = self._assign(start, end, hours=20) # allocate 20 hours to this assignment ca1.blocks.create(date=start, hours=20) # 1 week assignment, 20 hours ca2 = self._assign(start, end, hours=20) # only 20 hours left this week self.assertEqual(ca2.weekly_commitment(start), 20)
def view_entries(request): week_start = utils.get_week_start() entries = timepiece.Entry.objects.select_related( 'project__business', ).filter( user=request.user, end_time__gte=week_start, ).select_related('project', 'activity', 'location') today = datetime.date.today() assignments = timepiece.ContractAssignment.objects.filter( user=request.user, user__project_relationships__project=F('contract__project'), end_date__gte=today, contract__status='current', ).order_by('contract__project__type', 'end_date') assignments = assignments.select_related('user', 'contract__project__type') activity_entries = entries.values( 'billable', ).annotate(sum=Sum('hours')).order_by('-sum') current_total = entries.aggregate(sum=Sum('hours'))['sum'] others_active_entries = timepiece.Entry.objects.filter( end_time__isnull=True, ).exclude(user=request.user, ).select_related( 'user', 'project', 'activity') my_active_entries = timepiece.Entry.objects.select_related( 'project__business', ).only('user', 'project', 'activity', 'start_time').filter( user=request.user, end_time__isnull=True, ) # temporarily disabled until the allocations represent accurate goals # -TM 6/27 allocations = [] allocated_projects = timepiece.Project.objects.none() # allocations = timepiece.AssignmentAllocation.objects.during_this_week( # request.user # ).order_by('assignment__contract__project__name') # allocated_projects = allocations.values_list( # 'assignment__contract__project',) project_entries = entries.exclude(project__in=allocated_projects, ).values( 'project__name', 'project__pk').annotate(sum=Sum('hours')).order_by('project__name') schedule = timepiece.PersonSchedule.objects.filter(user=request.user) context = { 'this_weeks_entries': entries.order_by('-start_time'), 'assignments': assignments, 'allocations': allocations, 'schedule': schedule, 'project_entries': project_entries, 'activity_entries': activity_entries, 'current_total': current_total, 'others_active_entries': others_active_entries, 'my_active_entries': my_active_entries, } return context
def test_this_weeks_hours(self): start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=60) run_projection() self.log_time(ca, start=start, delta=(10, 0)) assignments = timepiece.AssignmentAllocation.objects.during_this_week( self.ps.user) self.assertEquals(assignments.count(), 1) assignment = assignments[0] self.assertEquals(assignment.hours_worked, 10) self.assertEquals(assignment.hours_left, assignment.hours - 10)
def report_payroll_summary(request): date = timezone.now() - relativedelta(months=1) from_date = utils.get_month_start(date).date() to_date = from_date + relativedelta(months=1) year_month_form = PayrollSummaryReportForm(request.GET or None, initial={ 'month': from_date.month, 'year': from_date.year }) if year_month_form.is_valid(): from_date, to_date = year_month_form.save() last_billable = utils.get_last_billable_day(from_date) projects = utils.get_setting('TIMEPIECE_PAID_LEAVE_PROJECTS') weekQ = Q(end_time__gt=utils.get_week_start(from_date), end_time__lt=last_billable + relativedelta(days=1)) monthQ = Q(end_time__gt=from_date, end_time__lt=to_date) workQ = ~Q(project__in=projects.values()) statusQ = Q(status=Entry.INVOICED) | Q(status=Entry.APPROVED) # Weekly totals week_entries = Entry.objects.date_trunc('week').filter( weekQ, statusQ, workQ) date_headers = generate_dates(from_date, last_billable, by='week') weekly_totals = list( get_project_totals(week_entries, date_headers, 'total', overtime=True)) # Monthly totals leave = Entry.objects.filter(monthQ, ~workQ).values('user', 'hours', 'project__name') extra_values = ('project__type__label', ) month_entries = Entry.objects.date_trunc('month', extra_values) month_entries_valid = month_entries.filter(monthQ, statusQ, workQ) labels, monthly_totals = get_payroll_totals(month_entries_valid, leave) # Unapproved and unverified hours entries = Entry.objects.filter(monthQ).order_by() # No ordering user_values = ['user__pk', 'user__first_name', 'user__last_name'] unverified = entries.filter(status=Entry.UNVERIFIED, user__is_active=True) \ .values_list(*user_values).distinct() unapproved = entries.filter(status=Entry.VERIFIED) \ .values_list(*user_values).distinct() return render( request, 'timepiece/reports/payroll_summary.html', { 'from_date': from_date, 'year_month_form': year_month_form, 'date_headers': date_headers, 'weekly_totals': weekly_totals, 'monthly_totals': monthly_totals, 'unverified': unverified, 'unapproved': unapproved, 'labels': labels, })
def test_weekly_commmitment_with_look_ahead(self): """ Test later assignment's min hours factor into weekly commitment """ # 1 week assignment, 40 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=1) - datetime.timedelta(days=1) ca1 = self._assign(start, end, hours=40) # 2 week assignment, 40 hours end = start + datetime.timedelta(weeks=1) - datetime.timedelta(days=1) ca2 = self._assign(start, end, hours=40) ca2.min_hours_per_week = 5 ca2.save() self.assertEqual(ca1.weekly_commitment(start), 35) ca1.blocks.create(date=start, hours=35) self.assertEqual(ca2.weekly_commitment(start), 5)
def setUp(self): self.today = datetime.date(2012, 11, 7) self.this_week = utils.get_week_start(self.today) self.next_week = self.this_week + relativedelta(days=7) self.uname = 'test' self.pword = 'password' self.user = self.create_user(username=self.uname, password=self.pword) self.client.login(username=self.uname, password=self.pword) self.project = self.create_project() self.activity = self.create_activity() self.location = self.create_location() self.status = 'unverified'
def report_payroll_summary(request): date = timezone.now() - relativedelta(months=1) from_date = utils.get_month_start(date).date() to_date = from_date + relativedelta(months=1) year_month_form = PayrollSummaryReportForm(request.GET or None, initial={ 'month': from_date.month, 'year': from_date.year, }) if year_month_form.is_valid(): from_date, to_date = year_month_form.save() last_billable = utils.get_last_billable_day(from_date) projects = utils.get_setting('TIMEPIECE_PAID_LEAVE_PROJECTS') weekQ = Q(end_time__gt=utils.get_week_start(from_date), end_time__lt=last_billable + relativedelta(days=1)) monthQ = Q(end_time__gt=from_date, end_time__lt=to_date) workQ = ~Q(project__in=projects.values()) statusQ = Q(status=Entry.INVOICED) | Q(status=Entry.APPROVED) # Weekly totals week_entries = Entry.objects.date_trunc('week').filter( weekQ, statusQ, workQ ) date_headers = generate_dates(from_date, last_billable, by='week') weekly_totals = list(get_project_totals(week_entries, date_headers, 'total', overtime=True)) # Monthly totals leave = Entry.objects.filter(monthQ, ~workQ) leave = leave.values('user', 'hours', 'project__name') extra_values = ('project__type__label',) month_entries = Entry.objects.date_trunc('month', extra_values) month_entries_valid = month_entries.filter(monthQ, statusQ, workQ) labels, monthly_totals = get_payroll_totals(month_entries_valid, leave) # Unapproved and unverified hours entries = Entry.objects.filter(monthQ).order_by() # No ordering user_values = ['user__pk', 'user__first_name', 'user__last_name'] unverified = entries.filter(status=Entry.UNVERIFIED, user__is_active=True) \ .values_list(*user_values).distinct() unapproved = entries.filter(status=Entry.VERIFIED) \ .values_list(*user_values).distinct() return render(request, 'timepiece/reports/payroll_summary.html', { 'from_date': from_date, 'year_month_form': year_month_form, 'date_headers': date_headers, 'weekly_totals': weekly_totals, 'monthly_totals': monthly_totals, 'unverified': unverified, 'unapproved': unapproved, 'labels': labels, })
def generate_dates(start=None, end=None, by='week'): if start: start = add_timezone(start) if end: end = add_timezone(end) if by == 'year': start = get_year_start(start) return rrule.rrule(rrule.YEARLY, dtstart=start, until=end) if by == 'month': start = get_month_start(start) return rrule.rrule(rrule.MONTHLY, dtstart=start, until=end) if by == 'week': start = get_week_start(start) return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, byweekday=0) if by == 'day': return rrule.rrule(rrule.DAILY, dtstart=start, until=end)
def setUp(self): self.today = datetime.date(2012, 11, 7) self.this_week = utils.get_week_start(self.today) self.next_week = self.this_week + relativedelta(days=7) get_params = {'week_start': self.this_week.strftime('%Y-%m-%d')} self.url = reverse('dashboard') + '?' + urlencode(get_params) self.user = factories.User() self.permission = Permission.objects.get(codename='can_clock_in') self.user.user_permissions.add(self.permission) self.login_user(self.user) self.project = factories.Project() self.activity = factories.Activity() self.location = factories.Location() self.status = Entry.UNVERIFIED
def payroll_summary(request): year_month_form = timepiece_forms.YearMonthForm(request.GET or None) if request.GET and year_month_form.is_valid(): from_date, to_date, user = year_month_form.save() else: from_date = utils.get_month_start(datetime.datetime.today()).date() to_date = from_date + relativedelta(months=1) last_billable = utils.get_last_billable_day(from_date) projects = getattr(settings, 'TIMEPIECE_PROJECTS', {}) weekQ = Q(end_time__gt=utils.get_week_start(from_date), end_time__lt=last_billable + datetime.timedelta(days=1)) monthQ = Q(end_time__gt=from_date, end_time__lt=to_date) workQ = ~Q(project__in=projects.values()) statusQ = Q(status='invoiced') | Q(status='approved') # Weekly totals week_entries = timepiece.Entry.objects.date_trunc('week') week_entries = week_entries.filter(weekQ, statusQ, workQ) date_headers = utils.generate_dates(from_date, last_billable, by='week') weekly_totals = list( utils.project_totals(week_entries, date_headers, 'total', overtime=True)) # Monthly totals leave = timepiece.Entry.objects.filter(monthQ, ~workQ).values( 'user', 'hours', 'project__name') month_entries = timepiece.Entry.objects.date_trunc('month') month_entries_valid = month_entries.filter(monthQ, statusQ, workQ) monthly_totals = list( utils.payroll_totals(month_entries_valid, from_date, leave)) # Unapproved and unverified hours entries = timepiece.Entry.objects.filter(monthQ) user_values = ['user__pk', 'user__first_name', 'user__last_name'] unverified = entries.filter(monthQ, status='unverified', user__is_active=True) unapproved = entries.filter(monthQ, status='verified') return { 'from_date': from_date, 'year_month_form': year_month_form, 'date_headers': date_headers, 'weekly_totals': weekly_totals, 'monthly_totals': monthly_totals, 'unverified': unverified.values_list(*user_values).distinct(), 'unapproved': unapproved.values_list(*user_values).distinct(), }
def test_min_hours_per_week_weighted(self): """ Test minimum hours/week with weighting based on assignment end date """ start = utils.get_week_start() end = start + datetime.timedelta(weeks=1) ca1 = self._assign(start, end, hours=40) ca2 = self._assign(start, end + datetime.timedelta(days=1), hours=40) ca1.min_hours_per_week = 30 ca1.save() ca2.min_hours_per_week = 30 ca2.save() run_projection() projection = ca1.blocks.filter(date=start).aggregate(s=Sum('hours')) self.assertEqual(30, projection['s']) projection = ca2.blocks.filter(date=start).aggregate(s=Sum('hours')) self.assertEqual(10, projection['s'])
def setUp(self): self.today = datetime.date(2012, 11, 7) self.this_week = utils.get_week_start(self.today) self.next_week = self.this_week + relativedelta(days=7) get_params = {'week_start': self.this_week.strftime('%Y-%m-%d')} self.url = reverse('dashboard') + '?' + urlencode(get_params) self.uname = 'test' self.pword = 'password' self.user = self.create_user(username=self.uname, password=self.pword) self.permission = Permission.objects.get(codename='can_clock_in') self.user.user_permissions.add(self.permission) self.client.login(username=self.uname, password=self.pword) self.project = self.create_project() self.activity = self.create_activity() self.location = self.create_location() self.status = 'unverified'
def test_this_weeks_allocations(self): # 2 weeks, 60 hours start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=20) person = User.objects.create_user('test2', '*****@*****.**', 'abc') ps = self.create_person_schedule(data={'user': person}) run_projection() assignments = timepiece.AssignmentAllocation.objects.during_this_week( self.ps.user) self.assertEquals(assignments.count(), 1) assignments = timepiece.AssignmentAllocation.objects.during_this_week( person) self.assertEquals(assignments.count(), 0) ca_2 = self._assign(start, end, hours=30) run_projection() assignments = timepiece.AssignmentAllocation.objects.during_this_week( self.ps.user) self.assertEquals(assignments.count(), 2)
def find_start(self, **kwargs): """ Determine the starting point of the query using CLI keyword arguments """ week = kwargs.get('week', False) month = kwargs.get('month', False) year = kwargs.get('year', False) days = kwargs.get('days', 0) #If no flags are True, set to the beginning of last billing window #to assure we catch all recent violations start = timezone.now() - relativedelta(months=1, day=1) #Set the start date based on arguments provided through options if week: start = utils.get_week_start() if month: start = timezone.now() - relativedelta(day=1) if year: start = timezone.now() - relativedelta(day=1, month=1) if days: start = timezone.now() - datetime.timedelta(days=days) start -= relativedelta(hour=0, minute=0, second=0, microsecond=0) return start
def week_start(date): return utils.get_week_start(date).strftime('%m/%d/%Y')
def view_person_time_sheet(request, user_id): user = get_object_or_404(User, pk=user_id) if not (request.user.has_perm('timepiece.view_entry_summary') or \ user.pk == request.user.pk): return HttpResponseForbidden('Forbidden') from_date = utils.get_month_start(datetime.datetime.today()).date() to_date = from_date + relativedelta(months=1) initial = { 'request_user': request.user, 'user': request.GET.get('user', user_id) } year_month_form = timepiece_forms.YearMonthForm(request.GET or None, initial=initial) if year_month_form.is_valid(): from_date, to_date, form_user = year_month_form.save() if form_user: url = reverse('view_person_time_sheet', args=(form_user.pk, )) # Do not use request.GET in urlencode in case it has the # user parameter (redirect loop otherwise) request_data = { 'month': request.GET.get('month', from_date.month), 'year': request.GET.get('year', from_date.year) } url += '?{0}'.format(urllib.urlencode(request_data)) return HttpResponseRedirect(url) entries_qs = timepiece.Entry.objects.filter(user=user) month_qs = entries_qs.timespan(from_date, span='month') month_entries = month_qs.date_trunc('month', True) # For grouped entries, back date up to the start of the week. first_week = utils.get_week_start(from_date) month_week = first_week + datetime.timedelta(weeks=1) grouped_qs = entries_qs.timespan(first_week, to_date=to_date) intersection = grouped_qs.filter(start_time__lt=month_week, start_time__gte=from_date) # If the month of the first week starts in the previous # month and we dont have entries in that previous ISO # week, then update the first week to start at the first # of the actual month if not intersection and first_week.month < from_date.month: grouped_qs = entries_qs.timespan(from_date, to_date=to_date) grouped_totals = utils.grouped_totals(grouped_qs) if month_entries else '' project_entries = month_qs.order_by().values('project__name').annotate( sum=Sum('hours')).order_by('-sum') summary = timepiece.Entry.summary(user, from_date, to_date) show_approve = show_verify = False if request.user.has_perm('timepiece.change_entry') or \ user == request.user: statuses = list(entries_qs.values_list('status', flat=True)) total_statuses = len(statuses) unverified_count = statuses.count('unverified') verified_count = statuses.count('verified') approved_count = statuses.count('approved') show_verify = unverified_count != 0 if request.user.has_perm('timepiece.change_entry'): show_approve = verified_count + approved_count == total_statuses \ and verified_count > 0 and total_statuses != 0 context = { 'year_month_form': year_month_form, 'from_date': from_date, 'to_date': to_date - datetime.timedelta(days=1), 'show_verify': show_verify, 'show_approve': show_approve, 'timesheet_user': user, 'entries': month_entries, 'grouped_totals': grouped_totals, 'project_entries': project_entries, 'summary': summary, } return render_to_response('timepiece/time-sheet/people/view.html', context, context_instance=RequestContext(request))
def view_user_timesheet(request, user_id, active_tab): # User can only view their own time sheet unless they have a permission. user = get_object_or_404(User, pk=user_id) has_perm = request.user.has_perm('entries.view_entry_summary') if not (has_perm or user.pk == request.user.pk): return HttpResponseForbidden('Forbidden') FormClass = UserYearMonthForm if has_perm else YearMonthForm form = FormClass(request.GET or None) if form.is_valid(): if has_perm: from_date, to_date, form_user = form.save() if form_user and request.GET.get('yearmonth', None): # Redirect to form_user's time sheet. # Do not use request.GET in urlencode to prevent redirect # loop caused by yearmonth parameter. url = reverse('view_user_timesheet', args=(form_user.pk, )) request_data = { 'month': from_date.month, 'year': from_date.year, 'user': form_user.pk, # Keep so that user appears in form. } url += '?{0}'.format(urllib.urlencode(request_data)) return HttpResponseRedirect(url) else: # User must be viewing their own time sheet; no redirect needed. from_date, to_date = form.save() from_date = utils.add_timezone(from_date) to_date = utils.add_timezone(to_date) else: # Default to showing this month. from_date = utils.get_month_start() to_date = from_date + relativedelta(months=1) entries_qs = Entry.objects.filter(user=user) month_qs = entries_qs.timespan(from_date, span='month') extra_values = ('start_time', 'end_time', 'comments', 'seconds_paused', 'id', 'location__name', 'project__name', 'activity__name', 'status') month_entries = month_qs.date_trunc('month', extra_values) # For grouped entries, back date up to the start of the week. first_week = utils.get_week_start(from_date) month_week = first_week + relativedelta(weeks=1) grouped_qs = entries_qs.timespan(first_week, to_date=to_date) intersection = grouped_qs.filter(start_time__lt=month_week, start_time__gte=from_date) # If the month of the first week starts in the previous # month and we dont have entries in that previous ISO # week, then update the first week to start at the first # of the actual month if not intersection and first_week.month < from_date.month: grouped_qs = entries_qs.timespan(from_date, to_date=to_date) totals = grouped_totals(grouped_qs) if month_entries else '' project_entries = month_qs.order_by().values('project__name').annotate( sum=Sum('hours')).order_by('-sum') summary = Entry.summary(user, from_date, to_date) show_approve = show_verify = False can_change = request.user.has_perm('entries.change_entry') can_approve = request.user.has_perm('entries.approve_timesheet') if can_change or can_approve or user == request.user: statuses = list(month_qs.values_list('status', flat=True)) total_statuses = len(statuses) unverified_count = statuses.count(Entry.UNVERIFIED) verified_count = statuses.count(Entry.VERIFIED) approved_count = statuses.count(Entry.APPROVED) if can_change or user == request.user: show_verify = unverified_count != 0 if can_approve: show_approve = verified_count + approved_count == total_statuses \ and verified_count > 0 and total_statuses != 0 return render( request, 'timepiece/user/timesheet/view.html', { 'active_tab': active_tab or 'overview', 'year_month_form': form, 'from_date': from_date, 'to_date': to_date - relativedelta(days=1), 'show_verify': show_verify, 'show_approve': show_approve, 'timesheet_user': user, 'entries': month_entries, 'grouped_totals': totals, 'project_entries': project_entries, 'summary': summary, })
def report_productivity(request): report = [] organize_by = None form = ProductivityReportForm(request.GET or None) if form.is_valid(): project = form.cleaned_data['project'] organize_by = form.cleaned_data['organize_by'] export = request.GET.get('export', False) actualsQ = Q(project=project, end_time__isnull=False) actuals = Entry.objects.filter(actualsQ) projections = ProjectHours.objects.filter(project=project) entry_count = actuals.count() + projections.count() if organize_by == 'week' and entry_count > 0: # Determine the project's time range. amin, amax, pmin, pmax = (None, None, None, None) if actuals.count() > 0: amin = actuals.aggregate(Min('start_time')).values()[0] amin = utils.get_week_start(amin).date() amax = actuals.aggregate(Max('start_time')).values()[0] amax = utils.get_week_start(amax).date() if projections.count() > 0: pmin = projections.aggregate(Min('week_start')).values()[0] pmax = projections.aggregate(Max('week_start')).values()[0] current = min(amin, pmin) if (amin and pmin) else (amin or pmin) latest = max(amax, pmax) if (amax and pmax) else (amax or pmax) # Report for each week during the project's time range. while current <= latest: next_week = current + relativedelta(days=7) actual_hours = actuals.filter( start_time__gte=current, start_time__lt=next_week).aggregate( Sum('hours')).values()[0] projected_hours = projections.filter( week_start__gte=current, week_start__lt=next_week).aggregate( Sum('hours')).values()[0] report.append([ date_format_filter(current, 'M j, Y'), actual_hours or 0, projected_hours or 0 ]) current = next_week elif organize_by == 'user' and entry_count > 0: # Determine all users who worked on or were assigned to the # project. vals = ('user', 'user__first_name', 'user__last_name') ausers = list(actuals.values_list(*vals).distinct()) pusers = list(projections.values_list(*vals).distinct()) key = lambda x: (x[1] + x[2]).lower() # Sort by name users = sorted(list(set(ausers + pusers)), key=key) # Report for each user. for user in users: name = '{0} {1}'.format(user[1], user[2]) actual_hours = actuals.filter(user=user[0]) \ .aggregate(Sum('hours')).values()[0] projected_hours = projections.filter(user=user[0]) \ .aggregate(Sum('hours')).values()[0] report.append([name, actual_hours or 0, projected_hours or 0]) col_headers = [organize_by.title(), 'Worked Hours', 'Assigned Hours'] report.insert(0, col_headers) if export: response = HttpResponse(content_type='text/csv') filename = '{0}_productivity'.format(project.name) content_disp = 'attachment; filename={0}.csv'.format(filename) response['Content-Disposition'] = content_disp writer = csv.writer(response) for row in report: writer.writerow(row) return response return render( request, 'timepiece/reports/productivity.html', { 'form': form, 'report': json.dumps(report, cls=DecimalEncoder), 'type': organize_by or '', 'total_worked': sum([r[1] for r in report[1:]]), 'total_assigned': sum([r[2] for r in report[1:]]), })
def clean_week_start(self): week_start = self.cleaned_data.get('week_start', None) return utils.get_week_start(week_start, False) if week_start else None
def save(self, *args, **kwargs): # Ensure that week_start is the Monday of a given week. self.week_start = utils.get_week_start(self.week_start) return super(ProjectHours, self).save(*args, **kwargs)
def get_week_window(day=None): """Returns (Monday, Sunday) of the requested week.""" start = get_week_start(day) return (start, start + relativedelta(days=6))
def test_weekly_commmitment(self): """ Test calculation of contract assignment's weekly commitment """ start = utils.get_week_start() end = start + datetime.timedelta(weeks=2) - datetime.timedelta(days=1) ca = self._assign(start, end, hours=40) self.assertEqual(ca.weekly_commitment(start), 40)
def view_user_timesheet(request, user_id, active_tab): user = get_object_or_404(User, pk=user_id) has_perm = request.user.has_perm('entries.view_entry_summary') if not (has_perm or user.pk == request.user.pk): return HttpResponseForbidden('Forbidden') FormClass = UserYearMonthForm if has_perm else YearMonthForm form = FormClass(request.GET or None) if form.is_valid(): if has_perm: from_date, to_date, form_user = form.save() if form_user and request.GET.get('yearmonth', None): url = reverse('view_user_timesheet', args=(form_user.pk, )) request_data = { 'month': from_date.month, 'year': from_date.year, 'user': form_user.pk, } url += '?{0}'.format(urlencode(request_data)) return HttpResponseRedirect(url) else: from_date, to_date = form.save() from_date = add_timezone(from_date) to_date = add_timezone(to_date) else: # from_date = get_month_start() # to_date = from_date + relativedelta(months=1) from_date = Entry.objects.last().start_time to_date = datetime.datetime.today() entries_qs = Entry.objects.filter(user=user) if form.is_valid(): month_qs = entries_qs.timespan(from_date, span='month') else: month_qs = entries_qs.timespan(from_date) extra_values = ('start_time', 'end_time', 'comments', 'seconds_paused', 'id', 'location__name', 'project__name', 'activity__name', 'status') month_entries = month_qs.date_trunc('month', extra_values).order_by('start_time') entry_ids = month_entries.values_list('id', flat=True) tasks = TaskList.objects.filter(entry_id__in=entry_ids).values( 'entry_id', 'tasks').order_by('entry_id') task_values = [ tasks.get(entry_id=entry_id)['tasks'] if entry_id in tasks.values_list('entry_id', flat=True) else '' for entry_id in entry_ids ] first_week = get_week_start(from_date) month_week = first_week + relativedelta(weeks=1) grouped_qs = entries_qs.timespan(first_week, to_date=to_date) intersection = grouped_qs.filter(start_time__lt=month_week, start_time__gte=from_date) if not intersection and first_week.month < from_date.month: grouped_qs = entries_qs.timespan(from_date, to_date=to_date) totals = grouped_totals(grouped_qs) if month_entries else '' project_entries = month_qs.order_by().values('project__name').annotate( sum=Sum('hours')).order_by('-sum') summary = Entry.summary(user, from_date, to_date) show_approve = show_verify = False can_change = request.user.has_perm('entries.can_change_entry') can_approve = request.user.has_perm('entries.approve_timesheet') if can_change or can_approve or user == request.user: statuses = list(month_qs.values_list('status', flat=True)) total_statuses = len(statuses) unverified_count = statuses.count(Entry.UNVERIFIED) verified_count = statuses.count(Entry.VERIFIED) approved_count = statuses.count(Entry.APPROVED) if can_change or user == request.user: show_verify = unverified_count != 0 if can_approve: show_approve = all([ verified_count + approved_count == total_statuses, verified_count > 0, total_statuses != 0, ]) return render( request, 'user/timesheet_view.html', { 'active_tab': active_tab or 'overview', 'year_month_form': form, 'from_date': from_date, 'to_date': to_date - relativedelta(days=1), 'show_verify': show_verify, 'show_approve': show_approve, 'timesheet_user': user, 'entries': zip(month_entries, task_values), 'grouped_totals': totals, 'project_entries': project_entries, 'summary': summary })
def week_start(date): """Return the starting day of the week with the given date.""" return utils.get_week_start(date)