Skip to content

Home > Frontend > dashboards

Dynamic Dashboards

Dynamic dashboards allow you to build entire frontend dashboards from the backend

INFO

Before you start here, make sure that your API endpoints are configured to use drf. See: # Configuring your API endpoint for Actions and Dynamic Dashboards

Creating a dashboard

Dashboards are created off of a resource (a resource is a model that has a corresponding ModelViewSet in the API)

For example, if we want to create a dashboard called index for the Invoice model, we would need to create the following directory structure:

bash
# mkdir -p {appname}/{resourcename}/dashboards/{actionid}/
mkdir -p project/invoice/dashboards/index/

In the folder, create two files:

  • form.py - Contains the dashboard form definition
  • helpers.py - Contains helper functions for building charts (optional but recommended for complex dashboards)

Form Structure

  • The name of your form must be: {ACTION_ID}Form, e.g.: IndexForm, ForecastForm
  • You must include a Meta class with configuration details
  • Your response should include:
    • a meta section (which provides the frontend with useful information like queryset size, date range, etc.)
    • a results section which should be dashbuilder.dashboard
See example form.py
py
from django import forms
from rest_framework import serializers
from drf.actionhelpers import get_action_permission_string
from drf.actionmixins import ACTION_TYPE, ACTION_READINESS, ActionFilterMixin
from drf.charts import DashBuilder
from drf.helpers import get_start_and_end_dates, get_end_of_month
from drf.forms import HeirarchicalModelMultipleChoiceFilter
from querytools.tools import group_by_and_annotate, group_by_and_aggregate
from datetime import date
import django_filters

ACTION_ID = "index"
description = "Dashboard providing an overview of invoices"


# Define your FilterSet for the dashboard
class InvoiceDashboardFilterSet(ActionFilterMixin, django_filters.FilterSet):
    after = django_filters.DateFilter(field_name="date", lookup_expr="gte")
    before = django_filters.DateFilter(field_name="date", lookup_expr="lte")
    
    departments = HeirarchicalModelMultipleChoiceFilter(
        queryset=Department.objects.is_active(),
        field_name="department_id",
        widget=django_filters.widgets.CSVWidget(),
    )
    
    customer = django_filters.ModelChoiceFilter(
        field_name="customer",
        queryset=Customer.objects.all(),
    )
    
    status = django_filters.ChoiceFilter(choices=Invoice.STATUS_CHOICES)


# Define a serializer for table display
class InvoiceBasicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Invoice
        fields = ["id", "invoice_number", "customer_name", "sub_total", "date", "status"]


class IndexForm(forms.Form):
    
    # Define initial start and end dates
    start_date, end_date = get_start_and_end_dates(months_ago=2, months_ahead=0)
    
    after = forms.DateField(required=False, initial=str(start_date))
    before = forms.DateField(required=False, initial=str(end_date))
    page = forms.IntegerField(required=False)
    
    departments = forms.ModelMultipleChoiceField(
        queryset=Department.objects.is_active(),
        widget=django_filters.widgets.CSVWidget,
        required=False,
    )

    class Meta:
        title = "Invoice Dashboard"
        description = description
        action_type = ACTION_TYPE.BULK
        release_status = ACTION_READINESS.PRODUCTION.name
        auto_dashboard = True
        filterset_class = InvoiceDashboardFilterSet
        
        required_permissions = [get_action_permission_string(Invoice, ACTION_ID)]

    def clean_before(self):
        today = date.today()
        before = self.cleaned_data.get("before")
        if not before:
            return get_end_of_month(today.year, today.month)
        return before

    def clean_after(self):
        today = date.today()
        after = self.cleaned_data.get("after")
        if not after:
            return date(today.year, today.month, 1)
        return after

    def save(self, qs=None):
        dashbuilder = DashBuilder()
        page = self.cleaned_data.get("page")

        start_date = self.cleaned_data.get("after")
        end_date = self.cleaned_data.get("before")
        departments = self.cleaned_data.get("departments")

        if qs is None:
            qs = self.view.get_filtered_queryset(InvoiceDashboardFilterSet)

        # Apply additional filters as needed
        if start_date and end_date and departments:
            qs = qs.in_departments(departments)

        # Build your charts using dashbuilder
        # ... add charts here ...

        return {
            "meta": {
                "title": self.Meta.title,
                "description": self.Meta.description,
                "search": False,
                "page": page,
                "request_page": self.request.GET.get("page"),
                "start_date": start_date,
                "end_date": end_date,
                "qs": qs.count(),
                "params": {
                    "startDateField": "after",
                    "endDateField": "before",
                },
            },
            "results": dashbuilder.dashboard,
        }

Helper Functions Pattern

For complex dashboards, separate chart-building logic into a helpers.py file:

See example helpers.py
py
from datetime import date
from querytools.tools import group_by_and_aggregate, group_by_and_annotate
from drf.helpers import get_months_between, normalize_date


def add_metrics(builder, qs, cols: int = 3):
    """Add metric cards to the dashboard"""
    builder.add(
        "invoice_sum",
        "metric",
        group_by_and_aggregate(qs, "sub_total", "Sum"),
        title="Total Invoice Value",
        cols=4,
        chart_options={"formatter": "currency"},
    )
    
    builder.add(
        "invoice_count",
        "metric",
        group_by_and_aggregate(qs, "id", "Count"),
        title="Number of Invoices",
        cols=4,
    )


def add_monthly_breakdown(builder, qs, start_date: date, end_date: date):
    """Add monthly breakdown charts"""
    series_configs = [
        {
            "title": "Invoice Total",
            "aggregation_field": "sub_total",
            "aggregation": "Sum",
            "date_field": "date",
            "color": "#009688",
        },
    ]

    context = {
        "series_configs": series_configs,
        "start_date": start_date,
        "end_date": end_date,
    }

    builder.add(
        "invoice_value_by_month",
        "monthly_columns",
        qs,
        title="Invoice Value by Month",
        context=context,
        cols=12,
        ui_options={"format": "currency", "large_number": True},
    )


def add_customer_treemap(builder, qs):
    """Add a treemap showing invoices by customer"""
    qs = qs.filter(sub_total__isnull=False)
    customer_data = group_by_and_annotate(
        qs,
        "customer__name",
        aggregation="Sum",
        aggregation_field="sub_total",
    )
    
    customer_series = [
        {"data": [{"x": customer, "y": count} for customer, count in customer_data.items()]}
    ]
    
    data = {
        "series": customer_series,
        "options": {
            "chart": {"height": "600px", "type": "treemap"},
            "plotOptions": {
                "treemap": {"distributed": True, "enableShades": False},
            },
        },
    }
    
    builder.add(
        "invoice_by_customer",
        "custom_chart",
        data,
        title="Invoice Total by Customer",
        cols=12,
        ui_options={"format": "currency", "large_number": True},
    )

Then in your form.py, import and use the helpers:

py
from .helpers import add_metrics, add_monthly_breakdown, add_customer_treemap

def save(self, qs=None):
    dashbuilder = DashBuilder()
    # ... setup code ...
    
    add_metrics(dashbuilder, qs)
    add_monthly_breakdown(dashbuilder, qs, start_date, end_date)
    add_customer_treemap(dashbuilder, qs)
    
    return {
        "meta": {...},
        "results": dashbuilder.dashboard,
    }

Adding charts to your dashboard

The add() method

All charts are added using dashbuilder.add():

py
dashbuilder.add(
    id="unique_chart_id",       # Unique identifier
    type="metric",              # Chart type
    data=value_or_queryset,     # Data to display
    title="Chart Title",        # Display title
    cols=4,                     # Grid columns (1-12)
    context={},                 # Type-specific options
    chart_options={},           # ApexChart options override
    ui_options={},              # Frontend UI options
    info="Optional tooltip",    # Info tooltip text
)

UI Options

The ui_options parameter controls frontend formatting:

py
ui_options={
    "format": "currency",    # Format values as currency
    # or
    "format": "percent",     # Format values as percentages
    # or
    "format": "number",      # Format as number with separators
    
    "large_number": True,    # Use compact notation for large numbers (1.2M)
}

Metric

A metric block that highlights a single number

py
dashbuilder.add(
    "invoice_count",
    "metric",
    group_by_and_aggregate(qs, "id", "Count"),
    title="Total Invoices",
    cols=4,
)

# With currency formatting
dashbuilder.add(
    "invoice_sum",
    "metric",
    group_by_and_aggregate(qs, "sub_total", "Sum"),
    title="Total Invoice Value",
    cols=4,
    chart_options={"formatter": "currency"},
)

# With percentage formatting
dashbuilder.add(
    "achievement_rate",
    "metric",
    85.5,
    title="Achievement Rate",
    cols=4,
    chart_options={"formatter": "percent"},
)

A header displays a Vuetify card for section headings

py
data = {
    "title": "Section Title",
    "subtitle": "Optional subtitle text",
    "text": "Optional descriptive text",
}
dashbuilder.add("section_header", "header", data=data)

Notice

A notice displays a Vuetify alert for important messages

py
data = {
    "title": "Notice Title",
    "text": "Notice content goes here",
    "type": "info",       # info, success, warning, error
    "variant": "text",
    "border": "start",
    "tile": True,
}
dashbuilder.add("notice_id", "notice", data=data)

INFO

The values of data are props passed to Vuetify's v-alert. See: https://vuetifyjs.com/en/api/v-alert/#props


Vuetify Component

Render any Vuetify component with custom props

py
card_data = {
    "title": "Custom Card Title",
    "subtitle": "Card subtitle",
    "variant": "elevated",
    "color": "blue-lighten-5",
    "class": "elevation-4 rounded-4 py-5",
    "style": "border-left: 6px solid #2096F3;",
}
dashbuilder.add("custom_card", "vuetify_component", "v-card", context=card_data)

Breakdown Chart

A breakdown chart shows a metric sliced by a single dimension (e.g., Invoices by status)

Features:

  • Automatically selects the best chart type based on data size:
    • Donut: Less than 10 categories
    • Horizontal bar: Less than 20 categories
    • Treemap: 20+ categories
  • Works seamlessly with querytools.tools.group_by_and_annotate
py
from querytools.tools import group_by_and_annotate

# Simple breakdown
dashbuilder.add(
    "invoices_by_status",
    "breakdown",
    group_by_and_annotate(qs, "status"),
    title="Invoices by Status",
    cols=4,
    context={"series_name": "Invoices"},
)

# Breakdown with aggregation
dashbuilder.add(
    "revenue_by_customer",
    "breakdown",
    group_by_and_annotate(qs, "customer__name", aggregation="Sum", aggregation_field="sub_total"),
    title="Revenue by Customer",
    cols=6,
    ui_options={"format": "currency"},
)

2D Breakdown

A 2D breakdown shows a metric broken down by a dimension and spread across a second dimension (typically time). e.g., Invoice value by status over time

Display types:

  • vertical_stacked - Stacked vertical bar chart
  • horizontal_stacked - Stacked horizontal bar chart
  • slope - Line chart showing trends

Options:

  • stacked_100 - Show as 100% stacked chart (default: True)
  • include_sum_annotations - Show sum annotations on bars
  • colors - Custom color array
py
from drf.helpers import months_between

month_range = months_between(start_date, end_date)
dimensions = [
    {"date__year": year, "date__month": month}
    for year, month in month_range
]

context = {
    "group_by_field": "status",
    "aggregation": "Sum",
    "aggregation_field": "sub_total",
    "dimensions": dimensions,
    "display_type": "vertical_stacked",  # or "horizontal_stacked" or "slope"
    "title": f"Invoice value by status by month",
    "labels": [f"{year}-{month}" for year, month in month_range],
    "stacked_100": False,                # Show actual values, not percentages
    "include_sum_annotations": True,     # Show totals on top of bars
    "colors": ["#028FFB", "#07E396", "#FEB01A"],  # Custom colors
}

dashbuilder.add(
    "status_over_time",
    "twod_breakdown",
    qs,
    title="Invoice Value by Status Over Time",
    cols=12,
    context=context,
    chart_options={"chart": {"height": "600px"}, "legend": {"show": True, "position": "bottom"}},
    ui_options={"format": "currency"},
)

Monthly Columns

Display monthly aggregated data with support for multiple series

py
series_configs = [
    {
        "title": "Fee Estimation",
        "qs": milestones_qs,              # Optional: use different queryset
        "aggregation_field": "fee_estimation",
        "aggregation": "Sum",
        "date_field": "end_date",
        "color": "#03A9F4",
    },
    {
        "title": "Invoice Total",
        "aggregation_field": "sub_total",
        "aggregation": "Sum",
        "date_field": "date",
        "color": "#009688",
    },
]

context = {
    "series_configs": series_configs,
    "start_date": start_date,
    "end_date": end_date,
}

dashbuilder.add(
    "monthly_comparison",
    "monthly_columns",
    qs,
    title="Monthly Fee vs Invoice Comparison",
    context=context,
    cols=12,
    ui_options={"format": "currency", "large_number": True},
)

Column Chart

Standard column/bar chart

py
# From a dictionary
data = {
    "January": 100,
    "February": 150,
    "March": 200,
}

dashbuilder.add(
    "monthly_data",
    "column",
    data,
    title="Monthly Data",
    cols=12,
    context={
        "series_name": "Revenue",
        "labels": ["January", "February", "March"],
        "colors": ["#009688"],
    },
    ui_options={"format": "currency"},
)

Date Series

Render a line chart with results over time

  • Works with querytools.tools.as_timeseries
  • Supports multiple series
py
from querytools.tools import as_timeseries

qs_over_time = as_timeseries(
    qs,
    from_date=start_date.isoformat(),
    to_date=end_date.isoformat(),
    search_field="date",
    aggregation="Count",
    aggregate_field="id",
)

series = [
    {"name": "Invoices", "data": qs_over_time},
    # Add more series as needed
]

dashbuilder.add(
    "invoices_over_time",
    "date_series",
    series,
    title="Invoices Over Time",
    cols=12,
)

Heatmap (Calendar)

Calendar heatmap showing daily activity

py
# Single month heatmap
context = {
    "dt": date(2024, 1, 1),
    "date_field": "date",
    "aggregate_field": "sub_total",  # Optional: defaults to count
    "aggregate_func": "Sum",          # Optional: defaults to "Count"
}

dashbuilder.add(
    "january_heatmap",
    "heatmap",
    qs,
    title="January Activity",
    context=context,
    cols=4,
)

# Multiple months (auto-generates one chart per month)
heatmap_context = {
    "start_date": start_date,
    "end_date": end_date,
    "date_field": "date",
}
dashbuilder.add("activity_heatmaps", "heatmaps", qs, context=heatmap_context)

List

Display a list of items with metrics

py
items = [
    {
        "title": "Invoice #001 - Customer A",
        "subtitle": "Date: 2024-01-15",
        "to": "/invoices/1",
        "description": "Optional notes",
        "metrics": [
            {"title": "Invoice Total", "value": 1500, "formatter": "currency"},
            {"title": "Fee Estimation", "value": 1200, "formatter": "currency"},
            {"title": "Difference", "value": 300, "formatter": "currency"},
        ],
    },
    # ... more items
]

context = {
    "height": 350,
    "formatter": "currency",
    "count": len(items),
}

dashbuilder.add(
    "items_list",
    "list",
    items,
    context=context,
    title="Items with Issues",
    cols=12,
)

Custom Chart

Create any ApexChart with full control over series and options

py
# Treemap example
series = [
    {"data": [{"x": "Category A", "y": 100}, {"x": "Category B", "y": 200}]}
]

data = {
    "series": series,
    "options": {
        "chart": {"height": "600px", "type": "treemap"},
        "plotOptions": {
            "treemap": {"distributed": True, "enableShades": False},
        },
    },
}

dashbuilder.add(
    "custom_treemap",
    "custom_chart",
    data,
    title="Custom Treemap",
    cols=12,
    ui_options={"format": "currency"},
)
Full example: Dumbbell chart
py
product_types = qs.values_list("product_type", flat=True).distinct()
series_data = []
for product_type in product_types:
    product_qs = qs.filter(product_type=product_type)
    male_value = product_qs.filter(gender="Male").aggregate(Sum("amount"))["amount__sum"]
    female_value = product_qs.filter(gender="Female").aggregate(Sum("amount"))["amount__sum"]

    series_data.append({
        "x": product_type,
        "y": [male_value, female_value],
    })

series = [{"data": series_data}]
options = {
    "chart": {"height": 390, "type": "rangeBar"},
    "colors": ["#EC7D31", "#36BDCB"],
    "plotOptions": {
        "bar": {
            "horizontal": True,
            "isDumbbell": True,
            "dumbbellColors": [["#EC7D31", "#36BDCB"]],
        }
    },
    "legend": {
        "show": True,
        "showForSingleSeries": True,
        "customLegendItems": ["Male", "Female"],
    },
}

dashbuilder.add(
    "gender_comparison",
    "custom_chart",
    {"series": series, "options": options},
    title="Value by Gender",
    cols=12,
)

Gantt

Render an ApexGantt timeline using pre-shaped task data.

Use gantt for timelines like projects, assignments, milestones, renewals, or any other resource with a start and end date.

Important:

  • DashBuilder.add_gantt() owns the shared ApexGantt defaults in drf/charts.py
  • pass pre-shaped ApexGantt task records into dashbuilder.add(..., "gantt", tasks, ...)
  • do not pass a raw queryset unless you also transform it into ApexGantt task fields
  • use chart_options only for chart-specific overrides such as resourceLabel, click metadata, or extra ApexGantt options

Task Data Shape

py
[
    {
        "id": "123",
        "name": "UiPath Developer License Renewal",
        "startTime": "2025-12-16",
        "endTime": "2026-12-15",
        "progress": 0,
        "parentId": None,
        "barBackgroundColor": "#FFA726",
    }
]

Typical serializer pattern:

py
class ProjectGanttSerializer(serializers.ModelSerializer):
    id = serializers.SerializerMethodField()
    startTime = serializers.DateField(source="start_date")
    endTime = serializers.DateField(source="end_date", allow_null=True, required=False)
    progress = serializers.SerializerMethodField()
    parentId = serializers.SerializerMethodField()
    barBackgroundColor = serializers.SerializerMethodField()

    class Meta:
        model = Project
        fields = ["id", "name", "startTime", "endTime", "progress", "parentId", "barBackgroundColor"]

    def get_id(self, obj):
        return str(obj.id)

    def get_progress(self, obj):
        return 0

Then:

py
tasks = ProjectGanttSerializer(
    qs.order_by("end_date", "start_date", "id"),
    many=True,
    context={"request": self.request},
).data

dashbuilder.add(
    "project_renewals_gantt",
    "gantt",
    tasks,
    title="Renewals Timeline",
    cols=12,
    chart_options={
        "resourceLabel": "Project",
        "click": {
            "action": "detail",
            "resource": "project",
            "idField": "id",
        },
    },
)

Shared Defaults

add_gantt() in drf/charts.py provides shared ApexGantt defaults such as:

  • viewMode: "month"
  • inputDateFormat: "YYYY-MM-DD"
  • disabled drag / resize / inline edit
  • enabled selection / export / tooltip
  • alternating row colors and sizing
  • a vertical Today annotation

Keep those shared defaults in drf/charts.py rather than repeating them in every dashboard.

Renaming Task

Use resourceLabel to rename the built-in tooltip label from Task to the resource name:

py
chart_options={
    "resourceLabel": "Assignment",
}

Generic Click Handling

Use shared click metadata so the frontend can open detail views generically:

py
chart_options={
    "click": {
        "action": "detail",
        "resource": "assignment",
        "idField": "id",
    },
}

When To Use gantt

  • Use gantt when you want the shared ApexGantt pipeline and reusable defaults
  • Use custom_chart for normal ApexCharts charts like line, bar, donut, treemap, funnel, and other standard ApexCharts visualizations

Table

Paginated table with customizable columns

py
from rest_framework import serializers

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ["id", "name", "customer_name", "amount", "date", "status"]

table_context = {
    "request": self.request,
    "serializer": ItemSerializer,
    "field_options": {
        "id": {"hidden": True},
        "name": {
            "component": "data-btn",
            "props": {
                "toPrefix": "/items/",
                "idField": "id",
                "maxLength": 30,
                "quickView": True,
                "resource": "item",
                "resourceId": "id",
            },
        },
        "amount": {"formatter": "currency"},
        "date": {"formatter": "date"},
        "status": {"formatter": "boolean"},
    },
}

dashbuilder.add(
    "items_table",
    "table",
    qs,
    context=table_context,
    title="Items",
)

Table Field Options

OptionDescription
hiddenHide the column
formatterUse a predefined formatter: currency, date, boolean, number, percent
componentUse a custom component for rendering
propsProps to pass to the custom component

Custom Components for Tables

data-btn - Renders a link button:

py
"name": {
    "component": "data-btn",
    "props": {
        "toPrefix": "/items/",      # URL prefix
        "idField": "id",            # Field to use for ID in URL
        "maxLength": 30,            # Truncate text
        "quickView": True,          # Enable quick view popup
        "resource": "item",         # Resource type for quick view
        "resourceId": "id",         # ID field for quick view
    },
}

data-btn-list - Renders multiple link buttons from a list field:

py
"related_items": {
    "component": "data-btn-list",
    "props": {
        "toPrefix": "/related/",
        "idField": "related__id",
        "labelField": "related__name",
        "emptyText": "-",
        "quickView": True,
    },
}

Utility Methods

Combined Breakdown + 2D Crosssection

Add both a breakdown chart and a time-based crosssection in one call

py
dashbuilder.add_overview_and_crosssection(
    qs,
    field="status",                      # Field to break down by
    start_date=start_date,
    end_date=end_date,
    field_title="Status",                # Human-readable field name
    aggregation="Sum",                   # Count or Sum
    aggregation_field="sub_total",       # Field to aggregate
    start_date_field="date",             # Date field for time dimension
    chart_type="vertical_stacked",       # slope, vertical_stacked, horizontal_stacked
    breakdown_cols=4,                    # Cols for breakdown chart
    two_d_cols=8,                        # Cols for 2D chart
    ui_options={"format": "currency"},
)

Top and Bottom Performers

Add two charts showing top and bottom performers

py
dashbuilder.top_and_bottom_summary(
    qs,
    field="customer__name",
    title="Customers",
    num_items=10,
    aggregation="Sum",
    aggregation_field="sub_total",
    ui_options={"format": "currency", "large_number": True},
    context={"chart_type": "column"},
)

Filters

Filters are automatically generated from the filterset_class defined in Meta

FilterSet Definition

py
from drf.actionmixins import ActionFilterMixin
from drf.forms import HeirarchicalModelMultipleChoiceFilter
import django_filters

class MyDashboardFilterSet(ActionFilterMixin, django_filters.FilterSet):
    # Date range filters
    after = django_filters.DateFilter(field_name="date", lookup_expr="gte")
    before = django_filters.DateFilter(field_name="date", lookup_expr="lte")
    
    # Hierarchical filter (for nested relationships)
    departments = HeirarchicalModelMultipleChoiceFilter(
        queryset=Department.objects.is_active(),
        field_name="department_id",
        widget=django_filters.widgets.CSVWidget(),
    )
    
    # Model choice filter
    customer = django_filters.ModelChoiceFilter(
        field_name="customer",
        queryset=Customer.objects.all(),
    )
    
    # Choice filter
    status = django_filters.ChoiceFilter(choices=MyModel.STATUS_CHOICES)
    
    # Boolean filter
    is_active = django_filters.BooleanFilter(field_name="is_active", lookup_expr="exact")

Supported Filter Types

Filter TypeFrontend Display
ChoiceFilterv-select with choices
BooleanFilterFilterable chips (yes/no)
ModelChoiceFilterv-select with queryset lookup
ModelMultipleChoiceFilterv-select with multiple selection
HeirarchicalModelMultipleChoiceFilterHierarchical multi-select

Meta Response Structure

The save() method must return a dictionary with meta and results:

py
return {
    "meta": {
        "title": "Dashboard Title",
        "description": "Dashboard description",
        "search": False,                          # Enable/disable search
        "page": page,
        "request_page": self.request.GET.get("page"),
        "start_date": start_date,
        "end_date": end_date,
        "qs": qs.count(),                         # Total items in queryset
        "params": {
            "startDateField": "after",            # URL param for start date
            "endDateField": "before",             # URL param for end date
        },
    },
    "results": dashbuilder.dashboard,
}

Helper Utilities

Date Utilities

py
from drf.helpers import (
    get_start_and_end_dates,    # Get default date range
    get_end_of_month,           # Get last day of month
    get_month_start_and_end,    # Get first and last day of month
    months_between,             # Get list of (year, month) tuples
    get_months_between,         # Alias for months_between
    normalize_date,             # Ensure date object (not datetime)
)

# Example usage
start_date, end_date = get_start_and_end_dates(months_ago=2, months_ahead=0)
month_range = months_between(start_date, end_date)

Query Utilities

py
from querytools.tools import (
    group_by_and_annotate,     # Group by field and count/sum
    group_by_and_aggregate,    # Get single aggregate value
    as_timeseries,             # Convert queryset to time series
)

# Count by field
status_counts = group_by_and_annotate(qs, "status")
# Returns: {"pending": 10, "paid": 25, "overdue": 5}

# Sum by field
customer_totals = group_by_and_annotate(qs, "customer__name", aggregation="Sum", aggregation_field="amount")
# Returns: {"Customer A": 1500, "Customer B": 2300}

# Single aggregate
total = group_by_and_aggregate(qs, "amount", "Sum")
# Returns: 3800

Testing

..