-
- Export Stock Reports
-
-
-
-
+ Stock Dashboard
/export",
- export_views.cluster_5w_stock_report_export,
- name="cluster-5w-stock-report-export",
- ),
+ # path(
+ # "stock-reports/cluster//export",
+ # export_views.cluster_5w_stock_report_export,
+ # name="cluster-5w-stock-report-export",
+ # ),
path(
"project/monthly-report/export/",
export_views.export_monthly_report_view,
name="export_monthly_report",
),
- path(
- "stock/report/organization//export",
- export_views.export_org_stock_monthly_report,
- name="export-org-5w-stock-reports",
- ),
]
diff --git a/src/project_reports/utils.py b/src/project_reports/utils.py
index a1853598..d94b4760 100644
--- a/src/project_reports/utils.py
+++ b/src/project_reports/utils.py
@@ -99,9 +99,6 @@ def write_projects_reports_to_csv(monthly_progress_report, response):
else:
continue
- disaggregation_list.append("total")
- disaggregation_cols.append("total")
-
if disaggregations:
for disaggregation_col in disaggregation_cols:
columns.append(disaggregation_col)
@@ -256,10 +253,7 @@ def write_projects_reports_to_csv(monthly_progress_report, response):
disaggregation_location.disaggregation.name: disaggregation_location.reached
for disaggregation_location in disaggregation_locations
}
- total_disaggregation = 0
- for value in disaggregation_location_list.values():
- total_disaggregation += value
- disaggregation_location_list["total"] = total_disaggregation
+
# Update disaggregation_data with values from disaggregation_location_list
for disaggregation_entry in disaggregation_list:
if disaggregation_entry not in disaggregation_location_list:
diff --git a/src/stock/filter.py b/src/stock/filter.py
index 6ec481e9..a97c9f83 100644
--- a/src/stock/filter.py
+++ b/src/stock/filter.py
@@ -61,3 +61,44 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+
+class StockDashboardFilter(django_filters.FilterSet):
+ from_date = django_filters.DateFilter(
+ field_name="stockmonthlyreport__from_date",
+ label="From Date",
+ lookup_expr="exact",
+ widget=forms.DateInput(
+ attrs={
+ "type": "date",
+ }
+ ),
+ )
+ due_date = django_filters.DateFilter(
+ field_name="stockmonthlyreport__due_date",
+ label="To Date",
+ lookup_expr="exact",
+ widget=forms.DateInput(attrs={"type": "date"}),
+ )
+ name = django_filters.ModelChoiceFilter(
+ lookup_expr="icontains", queryset=Warehouse.objects.all(), label="Warehouse Name"
+ )
+ status = django_filters.ChoiceFilter(
+ field_name="stockmonthlyreport__stockreport__status",
+ choices=StockReport.STATUS_TYPES,
+ label="Stock Status",
+ )
+ cluster = django_filters.ModelMultipleChoiceFilter(
+ field_name="stockmonthlyreport__stockreport__cluster",
+ queryset=Cluster.objects.all(),
+ lookup_expr="exact",
+ label="Clusters / Sectors",
+ widget=forms.SelectMultiple(attrs={"class": "custom-select"}),
+ )
+
+ class Meta:
+ mode = Warehouse
+ fields = ["from_date", "due_date", "name", "status", "cluster"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
diff --git a/src/stock/templates/stock/stock_dashboard.html b/src/stock/templates/stock/stock_dashboard.html
new file mode 100644
index 00000000..9ad31655
--- /dev/null
+++ b/src/stock/templates/stock/stock_dashboard.html
@@ -0,0 +1,215 @@
+{% extends "_base.html" %}
+
+{% load static %}
+{% load template_tags %}
+{% load humanize %}
+
+{% block title %}
+ {{ org.name }} 5W Dashboard
+{% endblock title %}
+
+{% block breadcrumb_li %}
+
+ {{ request.user.profile.organization.code }}
+
+
+ Stock Dashboard
+{% endblock %}
+
+{% block content %}
+
+
+
{{ request.user.profile.organization }}'s Stock Dashboard
+
+
+
+ Organization Stock dashboard.
+
+
+ {% comment %} cards {% endcomment %}
+
+
+
+
+
+
+
Warehouse Locations
+
{{ data.total_warehouse|intcomma }}
+
+
+
+
+
+
+
+
+
+
Stock Reports
+
{{ data.total_reports|intcomma }}
+
+
+
+
+
+
+
+
+
+
Clusters
+
{{ data.total_cluster|intcomma }}
+
+
+
+
+
+
+
+
+
+
Quantity in Stock
+
{{ data.total_stock|intcomma }}
+
+
+
+
+
+
+
+
+
+
Quantity in Pipeline
+
{{ data.total_pipeline|intcomma }}
+
+
+
+
+
+
+
+
+
+
+
Beneificiary Coverage
+
{{ data.total_beneficiary_coverage|intcomma }}
+
+
+
+
+
+
+
+
+
+
+
People to be Assisted
+
{{ data.total_people_assisted|intcomma }}
+
+
+
+
+
+
+
+
+
+
No.of Units Required
+
{{ data.total_unit_required|intcomma }}
+
+
+
+
+
+
+ {% comment %} Grid {% endcomment %}
+
+ {{ pie_chart|safe }}
+
+
+
+ {{ bar_chart|safe }}
+
+
+ {{ line_chart|safe }}
+
+
+
+
+{% endblock content %}
+{% block scripts %}
+{% endblock scripts %}
diff --git a/src/stock/urls.py b/src/stock/urls.py
index 12c5cc38..3e238552 100644
--- a/src/stock/urls.py
+++ b/src/stock/urls.py
@@ -92,4 +92,14 @@
user_views.report_details_view,
name="report-details-view",
),
+ path(
+ "stock/dashboard",
+ user_views.stock_dashbaord,
+ name="stock-dashboard",
+ ),
+ path(
+ "stock/export/organization/stock",
+ user_views.export_org_stock_beneficiary,
+ name="export-org-stock-beneficiary",
+ ),
]
diff --git a/src/stock/utils.py b/src/stock/utils.py
index f2820fa2..05681ac0 100644
--- a/src/stock/utils.py
+++ b/src/stock/utils.py
@@ -1,7 +1,7 @@
import csv
-def write_csv_columns_and_rows(all_monthly_report, response):
+def write_csv_columns_and_rows(warehouses, response):
writer = csv.writer(response)
columns = [
"project",
@@ -36,41 +36,41 @@ def write_csv_columns_and_rows(all_monthly_report, response):
"updated_at",
]
writer.writerow(columns)
-
- for monthly_report in all_monthly_report:
- stock_reports = monthly_report.stockreport_set.all()
- for report in stock_reports:
- row = [
- report.project.code if report.project else None,
- report.cluster if report.cluster else None,
- report.stock_item_type if report.stock_item_type else None,
- report.stock_purpose if report.stock_purpose else None,
- report.stock_unit if report.stock_unit else None,
- report.status if report.status else None,
- # reporting
- monthly_report.from_date.strftime("%B"),
- monthly_report.from_date.strftime("%Y"),
- monthly_report.from_date,
- monthly_report.warehouse_location.province.parent.code,
- monthly_report.warehouse_location.province.parent.name,
- monthly_report.warehouse_location.province.code,
- monthly_report.warehouse_location.province.name,
- monthly_report.warehouse_location.district.code,
- monthly_report.warehouse_location.district.name,
- monthly_report.warehouse_location.district.lat,
- monthly_report.warehouse_location.district.long,
- monthly_report.warehouse_location.organization,
- monthly_report.warehouse_location.user if monthly_report.warehouse_location.user else None,
- monthly_report.warehouse_location.user.email if monthly_report.warehouse_location.user else None,
- monthly_report.warehouse_location.name,
- report.qty_in_stock if report.qty_in_stock else None,
- report.qty_in_pipeline if report.qty_in_pipeline else None,
- report.beneficiary_coverage if report.beneficiary_coverage else None,
- report.targeted_groups if report.targeted_groups else None,
- report.people_to_assisted if report.people_to_assisted else None,
- report.unit_required if report.unit_required else None,
- monthly_report.note if monthly_report.note else None,
- monthly_report.warehouse_location.created_at,
- monthly_report.warehouse_location.updated_at,
- ]
- writer.writerow(row)
+ for warehouse in warehouses:
+ for monthly_report in warehouse.stockmonthlyreport_set.filter(state="submitted"):
+ stock_reports = monthly_report.stockreport_set.all()
+ for report in stock_reports:
+ row = [
+ report.project.code if report.project else None,
+ report.cluster if report.cluster else None,
+ report.stock_item_type if report.stock_item_type else None,
+ report.stock_purpose if report.stock_purpose else None,
+ report.stock_unit if report.stock_unit else None,
+ report.status if report.status else None,
+ # reporting
+ monthly_report.from_date.strftime("%B"),
+ monthly_report.from_date.strftime("%Y"),
+ monthly_report.from_date,
+ monthly_report.warehouse_location.province.parent.code,
+ monthly_report.warehouse_location.province.parent.name,
+ monthly_report.warehouse_location.province.code,
+ monthly_report.warehouse_location.province.name,
+ monthly_report.warehouse_location.district.code,
+ monthly_report.warehouse_location.district.name,
+ monthly_report.warehouse_location.district.lat,
+ monthly_report.warehouse_location.district.long,
+ monthly_report.warehouse_location.organization,
+ monthly_report.warehouse_location.user if monthly_report.warehouse_location.user else None,
+ monthly_report.warehouse_location.user.email if monthly_report.warehouse_location.user else None,
+ monthly_report.warehouse_location.name,
+ report.qty_in_stock if report.qty_in_stock else None,
+ report.qty_in_pipeline if report.qty_in_pipeline else None,
+ report.beneficiary_coverage if report.beneficiary_coverage else None,
+ report.targeted_groups if report.targeted_groups else None,
+ report.people_to_assisted if report.people_to_assisted else None,
+ report.unit_required if report.unit_required else None,
+ monthly_report.note if monthly_report.note else None,
+ monthly_report.warehouse_location.created_at,
+ monthly_report.warehouse_location.updated_at,
+ ]
+ writer.writerow(row)
diff --git a/src/stock/views.py b/src/stock/views.py
index 708d365f..1207def7 100644
--- a/src/stock/views.py
+++ b/src/stock/views.py
@@ -1,10 +1,15 @@
import calendar
+from collections import defaultdict
from datetime import datetime
+import pandas as pd
+import plotly.express as px
+import plotly.graph_objects as go
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
-from django.db.models import Prefetch, Sum
+from django.db.models import Count, Prefetch, Sum
+from django.db.models.functions import Coalesce
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
@@ -12,7 +17,7 @@
from django_htmx.http import HttpResponseClientRedirect
from extra_settings.models import Setting
-from stock.filter import StockFilter, StockMonthlyReportFilter, StockReportFilter
+from stock.filter import StockDashboardFilter, StockFilter, StockMonthlyReportFilter, StockReportFilter
from stock.utils import write_csv_columns_and_rows
from .forms import StockMonthlyReportForm, StockReportForm, WarehouseForm
@@ -371,8 +376,243 @@ def export_stock_monthly_report(request, warehouse):
return response
+@login_required
def report_details_view(request, report):
stock_report = get_object_or_404(StockReport, pk=report)
monthly_report = stock_report.monthly_report
context = {"stock_report": stock_report, "monthly_report": monthly_report}
return render(request, "stock/report_details_view.html", context)
+
+
+@login_required
+def stock_dashbaord(request):
+ user_org = request.user.profile.organization
+ # Prefetch related stock reports for submitted stock monthly reports
+ stock_report_prefetch = Prefetch("stockreport_set", queryset=StockReport.objects.all())
+
+ # Prefetch related stock monthly reports with the submitted state
+ stock_monthly_report_prefetch = Prefetch(
+ "stockmonthlyreport_set",
+ queryset=StockMonthlyReport.objects.filter(state="submitted").prefetch_related(stock_report_prefetch),
+ )
+
+ # Annotate the total beneficiary coverage
+ ware = (
+ Warehouse.objects.filter(organization=user_org)
+ .prefetch_related(stock_monthly_report_prefetch)
+ .annotate(total_beneficiary=Coalesce(Sum("stockmonthlyreport__stockreport__beneficiary_coverage"), 0))
+ )
+
+ warehouses_filter = StockDashboardFilter(request.GET, queryset=ware, request=request)
+ warehouses = warehouses_filter.qs
+ # Prepare the aggregated data for the response.
+ data_calculate = {
+ "total_beneficiary_coverage": warehouses.aggregate(
+ total_beneficiary_coverage_sum=Sum("stockmonthlyreport__stockreport__beneficiary_coverage")
+ )["total_beneficiary_coverage_sum"]
+ or 0,
+ "total_reports": warehouses.aggregate(total_reports_sum=Count("stockmonthlyreport__stockreport__id"))[
+ "total_reports_sum"
+ ]
+ or 0, # Counting reports as the sum of ids
+ "total_warehouse": warehouses.count(),
+ "total_stock": warehouses.aggregate(total_stock_sum=Sum("stockmonthlyreport__stockreport__qty_in_stock"))[
+ "total_stock_sum"
+ ]
+ or 0,
+ "total_pipeline": warehouses.aggregate(
+ total_pipeline_sum=Sum("stockmonthlyreport__stockreport__qty_in_pipeline")
+ )["total_pipeline_sum"]
+ or 0,
+ "total_people_assisted": warehouses.aggregate(
+ total_people_assisted_sum=Sum("stockmonthlyreport__stockreport__people_to_assisted")
+ )["total_people_assisted_sum"]
+ or 0,
+ "total_unit_required": warehouses.aggregate(
+ total_unit_required_sum=Sum("stockmonthlyreport__stockreport__unit_required")
+ )["total_unit_required_sum"]
+ or 0,
+ }
+
+ # Initialize defaultdicts to handle default values
+ clusters_beneficiary_dict = defaultdict(int)
+ cluster_pipeline_list = defaultdict(int)
+ cluster_stock_list = defaultdict(int)
+ months_beneficiary = defaultdict(int)
+ total_clusters = 0
+ warehouse_beneficiary = {"warehouse_name": [], "total_beneficiary": []}
+
+ # Create a single query to fetch all necessary data
+ warehouse_data = warehouses.annotate(
+ total_beneficiary=Coalesce(Sum("stockmonthlyreport__stockreport__beneficiary_coverage"), 0),
+ total_stock=Coalesce(Sum("stockmonthlyreport__stockreport__qty_in_stock"), 0),
+ total_pipeline=Coalesce(Sum("stockmonthlyreport__stockreport__qty_in_pipeline"), 0),
+ total_people_assisted=Coalesce(Sum("stockmonthlyreport__stockreport__people_to_assisted"), 0),
+ total_unit_required=Coalesce(Sum("stockmonthlyreport__stockreport__unit_required"), 0),
+ ).values(
+ "name",
+ "total_beneficiary",
+ "total_stock",
+ "total_pipeline",
+ "total_people_assisted",
+ "total_unit_required",
+ "stockmonthlyreport__stockreport__cluster__code",
+ "stockmonthlyreport__stockreport__beneficiary_coverage",
+ "stockmonthlyreport__stockreport__qty_in_pipeline",
+ "stockmonthlyreport__stockreport__qty_in_stock",
+ "stockmonthlyreport__from_date",
+ )
+
+ # Process the data
+ for data in warehouse_data:
+ warehouse_name = data["name"] if data["name"] is not None else "Unknown"
+ total_beneficiary = data["total_beneficiary"] if data["total_beneficiary"] is not None else 0
+
+ warehouse_beneficiary["warehouse_name"].append(warehouse_name)
+ warehouse_beneficiary["total_beneficiary"].append(total_beneficiary)
+
+ report_date = data["stockmonthlyreport__from_date"]
+ months_beneficiary[report_date] += data["stockmonthlyreport__stockreport__beneficiary_coverage"]
+
+ cluster_code = data["stockmonthlyreport__stockreport__cluster__code"]
+ if cluster_code not in clusters_beneficiary_dict:
+ total_clusters += 1
+
+ clusters_beneficiary_dict[cluster_code] += data["stockmonthlyreport__stockreport__beneficiary_coverage"]
+ cluster_pipeline_list[cluster_code] += data["stockmonthlyreport__stockreport__qty_in_pipeline"]
+ cluster_stock_list[cluster_code] += data["stockmonthlyreport__stockreport__qty_in_stock"]
+
+ # Create lists using comprehensions
+ clusters = list(clusters_beneficiary_dict.keys())
+ number_in_stock = list(cluster_stock_list.values())
+ number_in_pipeline = list(cluster_pipeline_list.values())
+ beneficiary_coverage = list(clusters_beneficiary_dict.values())
+
+ df = pd.DataFrame(warehouse_beneficiary)
+ fig = px.bar(df, x="warehouse_name", y="total_beneficiary")
+
+ fig.update_traces(marker=dict(color="#a52824"))
+ fig.update_layout(
+ xaxis_title="Warehouses",
+ yaxis_title="Beneficiaries",
+ showlegend=True,
+ margin=dict(r=0, t=50, b=0, l=0),
+ title={
+ "text": "
Warehouse Beneficiary Coverage Trends",
+ "font": {
+ "size": 14,
+ "color": "#A52824",
+ },
+ },
+ )
+ # Line chart
+ # Create the DataFrame for the line chart using pd.Series constructor
+ df = pd.DataFrame(
+ {
+ "x": pd.Series(months_beneficiary.keys()).fillna("unknown"),
+ "y": pd.Series(months_beneficiary.values()).fillna(0),
+ }
+ )
+ df = df.sort_values(by="x")
+ line_chart = px.line(
+ df,
+ x="x",
+ y="y",
+ )
+ line_chart.update_layout(
+ xaxis_title="Months",
+ yaxis_title="Beneficiary Coverage",
+ showlegend=True,
+ margin=dict(r=0, t=50, b=0, l=0),
+ title={
+ "text": "
Stock Report Monthly Trends",
+ "font": {
+ "size": 14,
+ "color": "#A52824",
+ },
+ },
+ )
+ line_chart.update_traces(
+ line=dict(shape="spline", color="#a52824"),
+ mode="lines+markers",
+ hovertemplate="
Month: %{x}
Beneficiary: %{y}
",
+ )
+
+ fig2 = go.Figure()
+ # Plot each metric as a line
+ fig2.add_trace(
+ go.Scatter(
+ x=clusters,
+ y=number_in_stock,
+ mode="lines+markers",
+ name="Quantity in Stock",
+ hovertemplate="
cluster: %{x}
quantity in stock: %{y}
",
+ line=dict(shape="spline"),
+ )
+ )
+ fig2.add_trace(
+ go.Scatter(
+ x=clusters,
+ y=number_in_pipeline,
+ mode="lines+markers",
+ name="Quantity in Pipeline",
+ hovertemplate="
cluster: %{x}
quantity in pipeline: %{y}
",
+ line=dict(shape="spline"),
+ )
+ )
+ fig2.add_trace(
+ go.Scatter(
+ x=clusters,
+ y=beneficiary_coverage,
+ mode="lines+markers",
+ name="Beneficiary Coverage",
+ hovertemplate="
cluster: %{x}
beneficiary coverage: %{y}
",
+ line=dict(shape="spline"),
+ )
+ )
+
+ # Update layout
+ fig2.update_layout(
+ xaxis_title="Clusters",
+ yaxis_title="Stock Data Values",
+ showlegend=True,
+ margin=dict(r=0, t=50, b=0, l=0),
+ height=400,
+ title={
+ "text": "
Cluster-wise In Stock, In Pipeline, and Beneficiary Coverage Trends",
+ "font": {
+ "size": 14,
+ "color": "#A52824",
+ },
+ },
+ )
+ data_calculate["total_cluster"] = total_clusters
+ context = {
+ "bar_chart": fig.to_html(),
+ "pie_chart": fig2.to_html(),
+ "line_chart": line_chart.to_html(),
+ "data": data_calculate,
+ "warehouse_filter": warehouses_filter,
+ }
+ return render(request, "stock/stock_dashboard.html", context)
+
+
+def export_org_stock_beneficiary(request):
+ user_org = request.user.profile.organization
+ ware = Warehouse.objects.filter(organization=user_org).annotate(
+ total_beneficiary=Sum("stockmonthlyreport__stockreport__beneficiary_coverage")
+ )
+ warehouses_filter = StockDashboardFilter(request.GET, queryset=ware, request=request)
+ warehouses = warehouses_filter.qs
+
+ today = datetime.now()
+ filename = today.today().strftime("%d-%m-%Y")
+
+ try:
+ response = HttpResponse(content_type="text/csv")
+ response["Content-Disposition"] = f"attachment; filename={user_org}_stock_reports_exported_on_{filename}.csv"
+ write_csv_columns_and_rows(warehouses, response)
+ return response
+ except Exception as e:
+ print(f"Error: {e}")
+ return HttpResponse(status=500)
diff --git a/src/templates/components/header.html b/src/templates/components/header.html
index fc63e366..d12dbfd4 100644
--- a/src/templates/components/header.html
+++ b/src/templates/components/header.html
@@ -188,6 +188,21 @@
+