def add_absences(self, queryset): for absence in queryset.filter( Q(user__in=self._user_ids), Q(starts_on__lte=max(self.weeks)), Q(ends_on__isnull=False, ends_on__gte=min(self.weeks)) | Q(ends_on__isnull=True, starts_on__gte=min(self.weeks)), ).select_related("user"): date_from = monday(absence.starts_on) date_until = monday(absence.ends_on or absence.starts_on) hours = absence.days * absence.user.planning_hours_per_day weeks = [ (idx, week) for idx, week in enumerate(self.weeks) if date_from <= week <= date_until ] for idx, week in weeks: self._absences[absence.user][idx].append( ( hours / len(weeks), f"{absence.get_reason_display()} - {absence.description}", absence.urls["detail"], ) ) self._by_week[week] += hours / len(weeks)
def absence_calendar(request, form): users = form.users() dates = {dt.date.today()} absences = defaultdict(list) cutoff = monday() - dt.timedelta(days=7) queryset = Absence.objects.annotate( _ends_on=Coalesce("ends_on", "starts_on")).filter( starts_on__lte=cutoff + dt.timedelta(days=366), _ends_on__gte=cutoff, user__in=users, ) for absence in queryset: absences[absence.user_id].append(absence) dates.add(max(absence.starts_on, cutoff)) dates.add(absence._ends_on) absences = [{ "name": user.get_full_name(), "id": user.id, "absences": [{ "id": absence.id, "reason": absence.reason, "reasonDisplay": absence.get_reason_display(), "startsOn": time.mktime(max(absence.starts_on, cutoff).timetuple()) * 1000, "endsOn": time.mktime( (absence.ends_on or absence.starts_on).timetuple()) * 1000, "days": absence.days, "description": absence.description, } for absence in absences[user.id]], } for user in users] return render( request, "awt/absence_calendar.html", { "absences_data": { "absencesByPerson": absences, "reasonList": Absence.REASON_CHOICES, "timeBoundaries": { "start": time.mktime(monday(min(dates)).timetuple()) * 1000, "end": time.mktime(max(dates).timetuple()) * 1000, }, "monday": time.mktime(monday().timetuple()), }, "form": form, }, )
def test_planned_work_crud(self): """Create, update and delete planned work""" service_types = factories.service_types() project = factories.ProjectFactory.create() self.client.force_login(project.owned_by) response = self.client.get(project.urls["creatework"] + "?request=bla") self.assertEqual(response.status_code, 200) # No crash response = self.client.post( project.urls["creatework"], { "modal-user": project.owned_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [monday().isoformat()], "modal-service_type": service_types.consulting.pk, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) pw = PlannedWork.objects.get() response = self.client.post( pw.urls["update"], { "modal-user": project.owned_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [monday().isoformat()], "modal-service_type": service_types.consulting.pk, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) response = self.client.post( project.urls["creatework"], { "modal-user": project.owned_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [ monday().isoformat(), (monday() + dt.timedelta(days=7)).isoformat(), ], "modal-service_type": service_types.consulting.pk, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) # print(response, response.content.decode("utf-8")) self.assertEqual(response.status_code, 201)
def campaign_planning(campaign, *, external_view=False): projects = Project.objects.filter(campaign=campaign) projects_ids = ([project.id for project in projects],) with connections["default"].cursor() as cursor: cursor.execute( """\ WITH sq AS ( SELECT unnest(weeks) AS week FROM planning_plannedwork WHERE project_id = ANY %s UNION ALL SELECT date_trunc('week', date)::date FROM planning_milestone WHERE project_id = ANY %s ) SELECT MIN(week), MAX(week) FROM sq """, [projects_ids, projects_ids], ) result = list(cursor)[0] if result[0]: result = (min(result[0], monday() - dt.timedelta(days=14)), result[1]) weeks = list( islice( recurring(result[0], "weekly"), 2 + (result[1] - result[0]).days // 7, ) ) else: weeks = list(islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80)) planning = Planning(weeks=weeks, projects=projects, external_view=external_view) planning.add_planned_work_and_milestones( PlannedWork.objects.filter(project__campaign=campaign).select_related( "service_type" ), Milestone.objects.filter(project__campaign=campaign), ExternalWork.objects.filter(project__campaign=campaign).select_related( "service_type" ), ) if not external_view: planning.add_worked_hours(LoggedHours.objects.all()) planning.add_absences(Absence.objects.all()) planning.add_public_holidays() planning.add_milestones(Milestone.objects.all()) return planning.report()
def date_ranges(): this_month = dt.date.today().replace(day=1) last_month = (this_month - dt.timedelta(days=1)).replace(day=1) next_month = (this_month + dt.timedelta(days=31)).replace(day=1) this_quarter = dt.date(this_month.year, 1 + (this_month.month - 1) // 3 * 3, 1) last_quarter = (this_quarter - dt.timedelta(days=75)).replace(day=1) next_quarter = (this_quarter + dt.timedelta(days=105)).replace(day=1) return [ ( (monday() + dt.timedelta(days=0)).isoformat(), (monday() + dt.timedelta(days=6)).isoformat(), _("this week"), ), ( (monday() - dt.timedelta(days=7)).isoformat(), (monday() - dt.timedelta(days=1)).isoformat(), _("last week"), ), ( this_month.isoformat(), (next_month - dt.timedelta(days=1)).isoformat(), _("this month"), ), ( last_month.isoformat(), (this_month - dt.timedelta(days=1)).isoformat(), _("last month"), ), ( this_quarter.isoformat(), (next_quarter - dt.timedelta(days=1)).isoformat(), _("this quarter"), ), ( last_quarter.isoformat(), (this_quarter - dt.timedelta(days=1)).isoformat(), _("last quarter"), ), ( dt.date(this_month.year, 1, 1).isoformat(), dt.date(this_month.year, 12, 31).isoformat(), _("this year"), ), ( dt.date(this_month.year - 1, 1, 1).isoformat(), dt.date(this_month.year - 1, 12, 31).isoformat(), _("last year"), ), ]
def __init__(self, data, *args, **kwargs): data = data.copy() data.setdefault("date_from", monday().isoformat()) data.setdefault("date_until", (monday() + dt.timedelta(days=6)).isoformat()) super().__init__(data, *args, **kwargs) self.fields["date_from"].help_text = format_html( "{}: {}", _("Set predefined period"), format_html_join(", ", '<a href="#" data-set-period="{}:{}">{}</a>', date_ranges()), )
def test_declined_offer_warning(self): """Warn when offer is declined""" offer = factories.OfferFactory.create(status=factories.Offer.DECLINED) self.client.force_login(offer.owned_by) response = self.client.post( offer.project.urls["creatework"], { "modal-user": offer.owned_by_id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [monday().isoformat()], "modal-offer": offer.id, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, 'value="offer-is-declined"') response = self.client.post( offer.project.urls["createrequest"], { "modal-title": "Request", "modal-earliest_start_on": "2020-06-29", "modal-completion_requested_on": "2020-07-27", "modal-requested_hours": "40", "modal-receivers": [offer.owned_by.id], "modal-offer": offer.id, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, 'value="offer-is-declined"')
def test_replanning(self): """Moving planned work between requests""" pr1 = factories.PlanningRequestFactory.create(requested_hours=50) pr2 = factories.PlanningRequestFactory.create(requested_hours=50) pw = factories.PlannedWorkFactory.create(request=pr1, weeks=[monday()], planned_hours=20) pr1.refresh_from_db() pr2.refresh_from_db() self.assertEqual(pr1.planned_hours, 20) self.assertEqual(pr2.planned_hours, 0) pw.request = pr2 pw.save() pr1.refresh_from_db() pr2.refresh_from_db() self.assertEqual(pr1.planned_hours, 0) self.assertEqual(pr2.planned_hours, 20) pw.delete() pr1.refresh_from_db() pr2.refresh_from_db() self.assertEqual(pr1.planned_hours, 0) self.assertEqual(pr2.planned_hours, 0)
def test_reporting_smoke(self): """Smoke test the planned work report""" pw = factories.PlannedWorkFactory.create(weeks=[monday()]) factories.EmploymentFactory.create(user=pw.user, date_from=dt.date(2020, 1, 1)) service = factories.ServiceFactory.create(project=pw.project) factories.LoggedHoursFactory.create(rendered_by=pw.user, service=service) factories.AbsenceFactory.create(user=pw.user) pr = factories.PlanningRequestFactory.create( project=pw.project, earliest_start_on=monday() - dt.timedelta(days=21), completion_requested_on=monday() + dt.timedelta(days=700), ) pr.receivers.add(pw.user) report = reporting.user_planning(pw.user) self.assertAlmostEqual(sum(report["by_week"]), Decimal("26")) self.assertEqual(len(report["projects_offers"]), 1) report = reporting.project_planning(pw.project) self.assertAlmostEqual(sum(report["by_week"]), Decimal("26")) self.assertEqual(len(report["projects_offers"]), 1) team = factories.TeamFactory.create() team.members.add(pw.user) report = reporting.team_planning(team) self.assertAlmostEqual(sum(report["by_week"]), Decimal("26")) self.assertEqual(len(report["projects_offers"]), 1) pw2 = factories.PlannedWorkFactory.create( project=pw.project, user=pw.user, weeks=[monday()], request=pr, ) report = reporting.user_planning(pw.user) self.assertAlmostEqual(sum(report["by_week"]), Decimal("46")) self.assertEqual(len(report["projects_offers"]), 1) work_list = report["projects_offers"][0]["offers"][0]["work_list"] self.assertEqual(len(work_list), 3) self.assertEqual(work_list[0]["work"]["id"], pr.id) self.assertEqual(work_list[1]["work"]["id"], pw2.id) self.assertEqual(work_list[2]["work"]["id"], pw.id)
def maybe_actionable(self, *, user): day = monday() weeks = [day + dt.timedelta(days=days) for days in [0, 7, 14, 21]] return self.filter( Q(user=user) | Q(created_by=user) | Q(project__owned_by=user), Q(is_provisional=True), Q(weeks__overlap=weeks), )
def add_worked_hours(self, queryset): for row in ( queryset.filter(service__project__in=self._project_ids) .values("service__project", "service__offer", "rendered_on") .annotate(Sum("hours")) ): self._worked_hours[row["service__project"]][ monday(row["rendered_on"]) ] += row["hours__sum"]
def user_planning(user): weeks = list( islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80)) planning = Planning(weeks=weeks, users=[user]) planning.add_planned_work(user.planned_work.all()) planning.add_planning_requests(user.received_planning_requests.all()) planning.add_worked_hours(user.loggedhours.all()) planning.add_absences(user.absences.all()) return planning.report()
def project_planning(project): with connections["default"].cursor() as cursor: cursor.execute( """\ WITH sq AS ( SELECT unnest(weeks) AS week FROM planning_plannedwork WHERE project_id=%s UNION ALL SELECT earliest_start_on AS week FROM planning_planningrequest WHERE project_id=%s UNION ALL SELECT completion_requested_on - 7 AS week FROM planning_planningrequest WHERE project_id=%s ) SELECT MIN(week), MAX(week) FROM sq """, [project.id, project.id, project.id], ) result = list(cursor)[0] if result[0]: result = (min(result[0], monday() - dt.timedelta(days=14)), result[1]) weeks = list( islice( recurring(result[0], "weekly"), 2 + (result[1] - result[0]).days // 7, )) else: weeks = list( islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80)) planning = Planning(weeks=weeks) planning.add_planned_work(project.planned_work.all()) planning.add_planning_requests(project.planning_requests.all()) planning.add_worked_hours(LoggedHours.objects.all()) planning.add_absences(Absence.objects.all()) return planning.report()
def test_reporting_smoke(self): """Smoke test the planned work report""" pw = factories.PlannedWorkFactory.create(weeks=[monday()]) factories.EmploymentFactory.create(user=pw.user, date_from=dt.date(2020, 1, 1)) service = factories.ServiceFactory.create(project=pw.project) factories.LoggedHoursFactory.create(rendered_by=pw.user, service=service) factories.AbsenceFactory.create(user=pw.user) report = reporting.user_planning(pw.user, date_range) self.assertAlmostEqual(sum(report["by_week"]), Decimal("28")) self.assertEqual(len(report["projects_offers"]), 1) report = reporting.project_planning(pw.project) self.assertAlmostEqual(sum(report["by_week"]), Decimal("28")) self.assertEqual(len(report["projects_offers"]), 1) team = factories.TeamFactory.create() team.members.add(pw.user) report = reporting.team_planning(team, date_range) self.assertAlmostEqual(sum(report["by_week"]), Decimal("28")) self.assertEqual(len(report["projects_offers"]), 1) pw2 = factories.PlannedWorkFactory.create( project=pw.project, user=pw.user, weeks=[monday()], ) report = reporting.user_planning(pw.user, date_range) self.assertAlmostEqual(sum(report["by_week"]), Decimal("48")) self.assertEqual(len(report["projects_offers"]), 1) work_list = report["projects_offers"][0]["offers"][0]["work_list"] self.assertEqual(len(work_list), 2) self.assertEqual(work_list[0]["work"]["id"], pw2.id) self.assertEqual(work_list[1]["work"]["id"], pw.id) report = reporting.planning_vs_logbook(date_range, users=User.objects.all()) self.assertAlmostEqual(report["logged"], Decimal("1.0")) self.assertAlmostEqual(report["planned"], Decimal("40.0")) # Exactly one customer (c,) = report["per_customer"] self.assertEqual(len(c["per_week"]), 1)
def hours(self): per_day = { row["rendered_on"]: row["hours__sum"] for row in self.loggedhours.filter(rendered_on__gte=monday()). order_by().values("rendered_on").annotate(Sum("hours")) } return { "today": per_day.get(dt.date.today(), Decimal("0.0")), "week": sum(per_day.values(), Decimal("0.0")), }
def test_receivers_with_work(self): """receivers_with_work returns all requested and planned work""" pr = factories.PlanningRequestFactory.create() only_receiver = factories.UserFactory.create() pr.receivers.add(only_receiver) only_pw = factories.PlannedWorkFactory.create(project=pr.project, request=pr, weeks=[monday()]) both = factories.PlannedWorkFactory.create(project=pr.project, request=pr, weeks=[monday()]) pr.receivers.add(both.user) self.assertEqual(set(pr.receivers.all()), {both.user, only_receiver}) self.assertEqual(len(pr.receivers_with_work), 3) receivers = dict(pr.receivers_with_work) self.assertEqual(receivers[only_receiver], []) self.assertEqual(receivers[both.user], [both]) self.assertEqual(receivers[only_pw.user], [only_pw])
def team_planning(team): weeks = list( islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80)) planning = Planning(weeks=weeks, users=list(team.members.active())) planning.add_planned_work(PlannedWork.objects.filter(user__teams=team)) planning.add_planning_requests( PlanningRequest.objects.filter( Q(receivers__teams=team) | Q(planned_work__user__teams=team))) planning.add_worked_hours( LoggedHours.objects.filter(rendered_by__teams=team)) planning.add_absences(Absence.objects.filter(user__teams=team)) return planning.report()
def team_planning(team, date_range): start, end = date_range weeks = list(takewhile(lambda x: x <= end, recurring(monday(start), "weekly"))) planning = Planning(weeks=weeks, users=list(team.members.active())) planning.add_planned_work_and_milestones( PlannedWork.objects.filter(user__teams=team).select_related("service_type"), Milestone.objects.filter(project__planned_work__user__teams=team), ) planning.add_worked_hours(LoggedHours.objects.filter(rendered_by__teams=team)) planning.add_absences(Absence.objects.filter(user__teams=team)) planning.add_public_holidays() planning.add_milestones(Milestone.objects.all()) return planning.report()
def report(self): try: this_week_index = self.weeks.index(monday()) except ValueError: this_week_index = None return { "this_week_index": this_week_index, "weeks": [ { "monday": week, "month": local_date_format(week, fmt="M"), "week": local_date_format(week, fmt="W"), "period": "{}–{}".format( local_date_format(week, fmt="j."), local_date_format(week + dt.timedelta(days=6), fmt="j."), ), } for week in self.weeks ], "projects_offers": sorted( filter( None, ( self._project_record(project, offers) for project, offers in self._projects_offers.items() ), ), key=lambda row: ( row["project"]["date_from"], row["project"]["date_until"], -row["project"]["planned_hours"], ) if row["project"]["date_from"] and row["project"]["date_until"] else (), ), "by_week": [self._by_week[week] for week in self.weeks], "by_week_provisional": [ self._by_week_provisional[week] for week in self.weeks ], "absences": [ (str(user), lst) for user, lst in sorted(self._absences.items()) ], "capacity": self.capacity() if self.users else None, "service_types": [ {"id": type.id, "title": type.title, "color": type.color} for type in ServiceType.objects.all() ], "external_view": self.external, }
def user_planning(user, date_range): start, end = date_range weeks = list(takewhile(lambda x: x <= end, recurring(monday(start), "weekly"))) planning = Planning(weeks=weeks, users=[user]) planning.add_planned_work_and_milestones( user.planned_work.select_related("service_type"), Milestone.objects.filter( Q(project__planned_work__user=user) | Q(project__owned_by=user) ), ) planning.add_worked_hours(user.loggedhours.all()) planning.add_absences(user.absences.all()) planning.add_public_holidays() planning.add_milestones(Milestone.objects.all()) return planning.report()
def add_project_milestone(self, project, milestone): if milestone and (not self._milestones[project][milestone]): start = ( milestone.phase_starts_on if milestone.phase_starts_on else milestone.date ) weeks = [ 1 if monday(start) <= w <= monday(milestone.date) else 0 for w in self.weeks ] graphical_weeks = [ 1 if monday(milestone.date) == w else 0 for w in self.weeks ] self._milestones[project][milestone].update( { "id": milestone.id, "title": milestone.title, "dow": local_date_format(milestone.date, fmt="l, j.n."), "date": local_date_format(milestone.date, fmt="j."), "range": "{} – {}".format( local_date_format(start, fmt="d.m."), local_date_format(milestone.date, fmt="d.m."), ) if milestone.phase_starts_on else None, "hours": milestone.estimated_total_hours, "phase_starts_on": start if milestone.phase_starts_on else None, "weekday": milestone.date.isocalendar()[2], "url": milestone.urls["detail"], "weeks": weeks, "graphical_weeks": graphical_weeks, } )
def report(self): try: this_week_index = self.weeks.index(monday()) except ValueError: this_week_index = None return { "this_week_index": this_week_index, "weeks": [{ "month": local_date_format(week, fmt="M"), "week": local_date_format(week, fmt="W"), "period": "{}–{}".format( local_date_format(week, fmt="j."), local_date_format(week + dt.timedelta(days=6), fmt="j."), ), } for week in self.weeks], "projects_offers": sorted( filter( None, (self._project_record(project, offers) for project, offers in self._projects_offers.items()), ), key=lambda row: ( row["project"]["date_from"], row["project"]["date_until"], -row["project"]["planned_hours"], ), ), "by_week": [self._by_week[week] for week in self.weeks], "requested_by_week": [self._requested_by_week[week] for week in self.weeks], "absences": [(str(user), lst) for user, lst in sorted(self._absences.items())], "capacity": self.capacity() if self.users else None, }
def this_monday(self): return monday()
def start_of_monday(): return timezone.make_aware(dt.datetime.combine(monday(), dt.time.min))
def add_public_holidays(self): ud = {user.id: user for user in User.objects.filter(id__in=self._user_ids)} for ( id, date, name, user_id, planning_hours_per_day, fraction, percentage, ) in query( """ select ph.id, ph.date, ph.name, user_id, planning_hours_per_day, fraction, percentage from planning_publicholiday ph left outer join lateral ( select user_id, date_from, date_until, percentage, planning_hours_per_day from awt_employment left join accounts_user on awt_employment.user_id=accounts_user.id where user_id = any (%s) ) as employment on employment.date_from <= ph.date and employment.date_until > ph.date where ph.date between %s and %s and user_id is not null order by ph.date """, [list(ud.keys()), min(self.weeks), max(self.weeks)], ): # Skip weekends if date.weekday() >= 5: continue week = monday(date) idx = self.weeks.index(week) user = ud[user_id] ph_hours = ( (planning_hours_per_day or 0) * (fraction or 0) * (percentage or 0) / 100 ) detail = " × ".join( ( f"{hours(planning_hours_per_day)}/d", f"{percentage}%", f"{fraction}d", ) ) self._absences[user][idx].append( ( ph_hours, f"{name} ({detail} = {hours(ph_hours)})", reverse("planning_publicholiday_detail", kwargs={"pk": id}), ) ) self._by_week[week] += ph_hours
} ) super().__init__(*args, **kwargs) self.instance.project = self.project self.fields[ "offer" ].choices = self.instance.project.offers.not_declined_choices( include=self.instance.offer_id ) self.fields["milestone"].queryset = self.project.milestones.all() self.fields["service_type"].required = True date_from_options = [ monday(), self.instance.weeks and min(self.instance.weeks), initial.get("weeks") and min(initial["weeks"]), ] date_from = min(filter(None, date_from_options)) - dt.timedelta(days=21) self.fields["weeks"] = forms.TypedMultipleChoiceField( label=capfirst(_("weeks")), choices=[ ( day, "KW{} ({} - {})".format( local_date_format(day, fmt="W"), local_date_format(day), local_date_format(day + dt.timedelta(days=6)), ),
def test_planned_work_crud(self): """Create, update and delete planned work""" pr = factories.PlanningRequestFactory.create( earliest_start_on=monday(), completion_requested_on=monday() + dt.timedelta(days=14), ) self.client.force_login(pr.created_by) response = self.client.get(pr.project.urls["creatework"] + "?request=bla") self.assertEqual(response.status_code, 200) # No crash response = self.client.post( pr.project.urls["creatework"], { "modal-user": pr.created_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [monday().isoformat()], }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) pw = PlannedWork.objects.get() response = self.client.post( pw.urls["update"], { "modal-user": pr.created_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [monday().isoformat()], }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) response = self.client.post( pr.project.urls["creatework"] + "?request={}".format(pr.id), { "modal-request": pr.id, "modal-user": pr.created_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [ monday().isoformat(), (monday() + dt.timedelta(days=7)).isoformat(), ], }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) # print(response, response.content.decode("utf-8")) self.assertEqual(response.status_code, 201) response = self.client.post( pr.project.urls["creatework"] + "?request={}".format(pr.id), { "modal-request": pr.id, "modal-user": pr.created_by.id, "modal-title": "bla", "modal-planned_hours": 50, "modal-weeks": [ monday().isoformat(), (monday() + dt.timedelta(days=14)).isoformat(), ], }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, "weeks-outside-request")
def logged_hours(user): stats = {} from_ = monday(in_days(-180)) hours_per_week = {} for week, type, hours in query( """ WITH sq AS ( SELECT date_trunc('week', rendered_on) AS week, project.type AS type, SUM(hours) AS hours FROM logbook_loggedhours hours LEFT JOIN projects_service service ON hours.service_id=service.id LEFT JOIN projects_project project ON service.project_id=project.id WHERE rendered_by_id=%s AND rendered_on>=%s GROUP BY week, project.type ) SELECT series.week, sq.type, COALESCE(sq.hours, 0) FROM generate_series(%s, %s, '7 days') AS series(week) LEFT OUTER JOIN sq ON series.week=sq.week ORDER BY series.week """, [user.id, from_, from_, monday() + dt.timedelta(days=6)], ): if week in hours_per_week: hours_per_week[week]["hours"] += hours hours_per_week[week]["by_type"][type] = hours else: hours_per_week[week] = { "week": week, "hours": hours, "by_type": { type: hours }, } stats["hours_per_week"] = [ row[1] for row in sorted(hours_per_week.items()) ] hours_per_customer = defaultdict(dict) total_hours_per_customer = defaultdict(int) for week, customer, hours in query( """ WITH sq AS ( SELECT date_trunc('week', rendered_on) AS week, customer.name AS customer, SUM(hours) AS hours FROM logbook_loggedhours hours LEFT JOIN projects_service service ON hours.service_id=service.id LEFT JOIN projects_project project ON service.project_id=project.id LEFT JOIN contacts_organization customer ON project.customer_id=customer.id WHERE rendered_by_id=%s AND rendered_on>=%s GROUP BY week, customer.name ) SELECT series.week, COALESCE(sq.customer, ''), COALESCE(sq.hours, 0) FROM generate_series(%s, %s, '7 days') AS series(week) LEFT OUTER JOIN sq ON series.week=sq.week ORDER BY series.week """, [user.id, from_, from_, monday() + dt.timedelta(days=6)], ): customer = customer.split("\n")[0] hours_per_customer[week][customer] = hours total_hours_per_customer[customer] += hours customers = [ row[0] for row in sorted(total_hours_per_customer.items(), key=lambda row: row[1], reverse=True) ][:10] weeks = sorted(hours_per_customer.keys()) stats["hours_per_customer"] = { "weeks": weeks, "by_customer": [{ "name": customer, "hours": [hours_per_customer[week].get(customer, 0) for week in weeks], } for customer in customers], } customers = set(customers) stats["hours_per_customer"]["by_customer"].append({ "name": _("All others"), "hours": [ sum( (hours for customer, hours in hours_per_customer[week].items() if customer not in customers), 0, ) for week in weeks ], }) dows = [ None, _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"), _("Friday"), _("Saturday"), _("Sunday"), ] stats["rendered_hours_per_weekday"] = [{ "dow": int(dow), "name": dows[int(dow)], "hours": hours } for dow, hours in query( """ WITH sq AS ( SELECT (extract(isodow from rendered_on)::integer) as dow, SUM(hours) AS hours FROM logbook_loggedhours WHERE rendered_by_id=%s AND rendered_on>=%s GROUP BY dow ORDER BY dow ) SELECT series.dow, COALESCE(sq.hours, 0) FROM generate_series(1, 7) AS series(dow) LEFT OUTER JOIN sq ON series.dow=sq.dow ORDER BY series.dow """, [user.id, from_], )] stats["created_hours_per_weekday"] = [{ "dow": int(dow), "name": dows[int(dow)], "hours": hours } for dow, hours in query( """ WITH sq AS ( SELECT (extract(isodow from timezone('CET', created_at))::integer) as dow, SUM(hours) AS hours FROM logbook_loggedhours WHERE rendered_by_id=%s AND rendered_on>=%s GROUP BY dow ORDER BY dow ) SELECT series.dow, COALESCE(sq.hours, 0) FROM generate_series(1, 7) AS series(dow) LEFT OUTER JOIN sq ON series.dow=sq.dow ORDER BY series.dow """, [user.id, from_], )] return stats
"notes": service.description, "planned_hours": service.service_hours, }) super().__init__(*args, **kwargs) self.instance.project = self.project self.fields[ "offer"].choices = self.instance.project.offers.not_declined_choices( include=self.instance.offer_id) self.fields[ "request"].queryset = self.instance.project.planning_requests.all( ) date_from_options = [ monday(), self.instance.weeks and min(self.instance.weeks), pr and min(pr.weeks), ] date_from = min(filter(None, date_from_options)) - dt.timedelta(days=21) self.fields["weeks"] = forms.TypedMultipleChoiceField( label=capfirst(_("weeks")), choices=[( day, "KW{} ({} - {})".format( local_date_format(day, fmt="W"), local_date_format(day), local_date_format(day + dt.timedelta(days=6)), ),
class ExternalWorkForm(ModelForm): class Meta: model = ExternalWork fields = ( "provided_by", "title", "service_type", "notes", "milestone", ) widgets = { "provided_by": Autocomplete(model=Organization), "notes": Textarea, } def __init__(self, *args, **kwargs): initial = kwargs.setdefault("initial", {}) request = kwargs["request"] self.project = kwargs.pop("project", None) if not self.project: # Updating self.project = kwargs["instance"].project else: initial["provided_by"] = self.project.customer_id if service_id := request.GET.get("service"): try: service = self.project.services.get(pk=service_id) except (self.project.services.model.DoesNotExist, TypeError, ValueError): pass else: initial.update( { "title": f"{self.project.title}: {service.title}", "notes": service.description, "planned_hours": service.service_hours, } ) # if pk := request.GET.get("copy"): # try: # pw = ExternalWork.objects.get(pk=pk) # except (ExternalWork.DoesNotExist, TypeError, ValueError): # pass # else: # initial.update( # { # "project": pw.project_id, # "offer": pw.offer_id, # "title": pw.title, # "notes": pw.notes, # "planned_hours": pw.planned_hours, # "weeks": pw.weeks, # "is_provisional": pw.is_provisional, # "service_type": pw.service_type_id, # "milestone": pw.milestone_id, # } # ) super().__init__(*args, **kwargs) self.instance.project = self.project self.fields["milestone"].queryset = self.project.milestones.all() # self.fields["service_type"].required = True date_from_options = [ monday(), self.instance.weeks and min(self.instance.weeks), initial.get("weeks") and min(initial["weeks"]), ] date_from = min(filter(None, date_from_options)) - dt.timedelta(days=21) self.fields["weeks"] = forms.TypedMultipleChoiceField( label=capfirst(_("weeks")), choices=[ ( day, "KW{} ({} - {})".format( local_date_format(day, fmt="W"), local_date_format(day), local_date_format(day + dt.timedelta(days=6)), ), ) for day in islice(recurring(date_from, "weekly"), 80) ], widget=forms.SelectMultiple(attrs={"size": 20}), initial=self.instance.weeks or [monday()], coerce=parse_date, )