Skip to content

Commit

Permalink
Merge pull request #402 from dimagi/hy/network-manager-delivery-perfo…
Browse files Browse the repository at this point in the history
…rmance-report

Delivery Performance Report
  • Loading branch information
hemant10yadav authored Nov 28, 2024
2 parents e6a4603 + f8622c9 commit 44d2a9e
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 24 deletions.
72 changes: 62 additions & 10 deletions commcare_connect/program/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
from commcare_connect.opportunity.models import UserVisit, VisitValidationStatus
from commcare_connect.program.models import ManagedOpportunity, Program

EXCLUDED_STATUS = [
VisitValidationStatus.over_limit,
VisitValidationStatus.trial,
]

FILTER_FOR_VALID_VISIT_DATE = ~Q(opportunityaccess__uservisit__status__in=EXCLUDED_STATUS)


def calculate_safe_percentage(numerator, denominator):
return Case(
Expand All @@ -27,18 +34,12 @@ def calculate_safe_percentage(numerator, denominator):


def get_annotated_managed_opportunity(program: Program):
excluded_status = [
VisitValidationStatus.over_limit,
VisitValidationStatus.trial,
]

filter_for_valid__visit_date = ~Q(opportunityaccess__uservisit__status__in=excluded_status)

earliest_visits = (
UserVisit.objects.filter(
opportunity_access=OuterRef("opportunityaccess"),
user=OuterRef("opportunityaccess__uservisit__user"),
)
.exclude(status__in=excluded_status)
.exclude(status__in=EXCLUDED_STATUS)
.order_by("visit_date")
.values("visit_date")[:1]
)
Expand All @@ -57,15 +58,66 @@ def get_annotated_managed_opportunity(program: Program):
),
workers_starting_delivery=Count(
"opportunityaccess__uservisit__user",
filter=filter_for_valid__visit_date,
filter=FILTER_FOR_VALID_VISIT_DATE,
distinct=True,
),
percentage_conversion=calculate_safe_percentage("workers_starting_delivery", "workers_invited"),
average_time_to_convert=Avg(
ExpressionWrapper(
Subquery(earliest_visits) - F("opportunityaccess__invited_date"), output_field=DurationField()
),
filter=filter_for_valid__visit_date,
filter=FILTER_FOR_VALID_VISIT_DATE,
distinct=True,
),
)
)
return managed_opportunities


def get_delivery_performance_report(program: Program, start_date, end_date):
date_filter = FILTER_FOR_VALID_VISIT_DATE

if start_date:
date_filter &= Q(opportunityaccess__uservisit__visit_date__gte=start_date)

if end_date:
date_filter &= Q(opportunityaccess__uservisit__visit_date__lte=end_date)

flagged_visits_filter = (
Q(opportunityaccess__uservisit__flagged=True)
& date_filter
& Q(opportunityaccess__uservisit__completed_work__isnull=False)
)

managed_opportunities = (
ManagedOpportunity.objects.filter(program=program)
.order_by("start_date")
.annotate(
total_workers_starting_delivery=Count(
"opportunityaccess__uservisit__user",
filter=FILTER_FOR_VALID_VISIT_DATE,
distinct=True,
),
active_workers=Count(
"opportunityaccess__uservisit__user",
filter=date_filter,
distinct=True,
),
total_payment_units_with_flags=Count(
"opportunityaccess__uservisit", distinct=True, filter=flagged_visits_filter
),
total_payment_since_start_date=Count(
"opportunityaccess__uservisit",
distinct=True,
filter=date_filter & Q(opportunityaccess__uservisit__completed_work__isnull=False),
),
deliveries_per_day_per_worker=Case(
When(active_workers=0, then=Value(0)),
default=Round(F("total_payment_since_start_date") / F("active_workers"), 2),
output_field=FloatField(),
),
records_flagged_percentage=calculate_safe_percentage(
"total_payment_units_with_flags", "total_payment_since_start_date"
),
)
)
Expand Down
49 changes: 48 additions & 1 deletion commcare_connect/program/tables.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import django_tables2 as tables
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -237,18 +238,20 @@ def get_manage_buttons_html(buttons, request):

class FunnelPerformanceTable(tables.Table):
organization = tables.Column()
opportunity = tables.Column(accessor="name", verbose_name="Opportunity")
start_date = tables.DateColumn()
workers_invited = tables.Column(verbose_name=_("Workers Invited"))
workers_passing_assessment = tables.Column(verbose_name=_("Workers Passing Assessment"))
workers_starting_delivery = tables.Column(verbose_name=_("Workers Starting Delivery"))
percentage_conversion = tables.Column(verbose_name=_("Percentage Conversion"))
percentage_conversion = tables.Column(verbose_name=_("% Conversion"))
average_time_to_convert = tables.Column(verbose_name=_("Average Time To convert"))

class Meta:
model = ManagedOpportunity
empty_text = "No data available yet."
fields = (
"organization",
"opportunity",
"start_date",
"workers_invited",
"workers_passing_assessment",
Expand All @@ -258,9 +261,53 @@ class Meta:
)
orderable = False

def render_opportunity(self, value, record):
url = reverse(
"opportunity:detail",
kwargs={
"org_slug": record.organization.slug,
"pk": record.id,
},
)
return format_html('<a href="{}">{}</a>', url, value)

def render_average_time_to_convert(self, record):
if not record.average_time_to_convert:
return "---"
total_seconds = record.average_time_to_convert.total_seconds()
hours = total_seconds / 3600
return f"{round(hours, 2)}hr"


class DeliveryPerformanceTable(tables.Table):
organization = tables.Column()
opportunity = tables.Column(accessor="name", verbose_name="Opportunity")
start_date = tables.DateColumn()
total_workers_starting_delivery = tables.Column(verbose_name=_("Workers Starting Delivery"))
active_workers = tables.Column(verbose_name=_("Active Workers"))
deliveries_per_day_per_worker = tables.Column(verbose_name=_("Deliveries per Day per Worker"))
records_flagged_percentage = tables.Column(verbose_name=_("% Records flagged"))

class Meta:
model = ManagedOpportunity
empty_text = "No data available yet."
fields = (
"organization",
"opportunity",
"start_date",
"total_workers_starting_delivery",
"active_workers",
"deliveries_per_day_per_worker",
"records_flagged_percentage",
)
orderable = False

def render_opportunity(self, value, record):
url = reverse(
"opportunity:detail",
kwargs={
"org_slug": record.organization.slug,
"pk": record.id,
},
)
return format_html('<a href="{}">{}</a>', url, value)
Loading

0 comments on commit 44d2a9e

Please sign in to comment.