diff --git a/poetry.lock b/poetry.lock index ccf031cc..474f4912 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -835,6 +835,21 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "plotly" +version = "5.24.1" +description = "An open-source, interactive data visualization library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089"}, + {file = "plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae"}, +] + +[package.dependencies] +packaging = "*" +tenacity = ">=6.2.0" + [[package]] name = "ply" version = "3.11" @@ -1114,6 +1129,21 @@ files = [ ply = ">=3.4" six = ">=1.12.0" +[[package]] +name = "tenacity" +version = "9.0.0" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "textual" version = "1.0.0" @@ -1579,4 +1609,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "299d9e35e8ada6ecb6a792ac41af183981334ef69e996ab4d0259ed1626d4e7e" +content-hash = "d5176acbe9e60c3204343a839cc2f96686e464820b6a5bcd6cb728e33fb16176" diff --git a/pyproject.toml b/pyproject.toml index f40caded..dec351c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ markdown = "^3.6" django-mailer = "^2.3.2" gunicorn = "^23.0.0" django-extensions = "^3.2.3" +plotly = "5.24.1" [tool.poetry.group.dev.dependencies] diff --git a/src/project_reports/exports.py b/src/project_reports/exports.py index 58566e07..3bf38d59 100644 --- a/src/project_reports/exports.py +++ b/src/project_reports/exports.py @@ -1,5 +1,4 @@ import datetime -import json from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied @@ -10,8 +9,6 @@ from project_reports.filters import MonthlyReportsFilter, Organization5WFilter from rh.models import Cluster, Organization, Project -from stock.models import StockMonthlyReport, StockReport -from stock.utils import write_csv_columns_and_rows from users.utils import is_cluster_lead from .models import ActivityPlanReport, DisaggregationLocationReport, ProjectMonthlyReport, TargetLocationReport @@ -87,57 +84,6 @@ def cluster_5w_dashboard_export(request, code): return HttpResponse(response, status=500) -@login_required -def cluster_5w_stock_report_export(request, code): - cluster = get_object_or_404(Cluster, code=code) - - body = json.loads(request.body) - - from_date = body.get("from") - if not from_date: - from_date = datetime.date(datetime.datetime.now().year, 1, 1) - - to_date = body.get("to") - if not to_date: - to_date = datetime.datetime.now().date() - - user_country = request.user.profile.country - - filter_params = { - "stockreport__cluster__in": [cluster], - "state__in": ["submitted"], - "from_date__lte": to_date, - "due_date__gte": from_date, - "warehouse_location__user__profile__country": user_country, - } - - organization_code = body.get("organization") - if organization_code: - filter_params["warehouse_location__organization__code"] = organization_code - - if not is_cluster_lead( - user=request.user, - clusters=[ - cluster.code, - ], - ): - raise PermissionDenied - try: - stock_monthly_reports = ( - StockMonthlyReport.objects.select_related("warehouse_location") - .prefetch_related(Prefetch("stockreport_set", queryset=StockReport.objects.all())) - .filter(**filter_params) - .distinct() - ) - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = "attachment; filename=reports.csv" - write_csv_columns_and_rows(stock_monthly_reports, response) - return response - except Exception as e: - response = {"error": str(e)} - return HttpResponse(response, status=500) - - @login_required def org_5w_dashboard_export(request, code): org = get_object_or_404(Organization, code=code) @@ -229,53 +175,3 @@ def export_monthly_report_view(request, pk): except Exception as e: response = {"error": str(e)} return HttpResponse(response, status=500) - - -def export_org_stock_monthly_report(request, code): - org = get_object_or_404(Organization, code=code) - - if not org.code == request.user.profile.organization.code and not is_cluster_lead( - request.user, org.clusters.values_list("code", flat=True) - ): - raise PermissionDenied - - # body = json.loads(request.body) - - # cluster_code = body.get("cluster") - # from_date = body.get("from") - # if not from_date: - # from_date = datetime.date(datetime.datetime.now().year, 1, 1) - - # to_date = body.get("to") - # if not to_date: - # to_date = datetime.datetime.now().date() - - filter_params = { - "warehouse_location__organization": org, - "state__in": ["submitted"], - # "from_date__lte": to_date, - # "due_date__gte": from_date, - } - - # cluster_code = body.get("cluster") - # if cluster_code: - # filter_params["stockreport__cluster__code__in"] = [ - # cluster_code, - # ] - stock_monthly_report = ( - StockMonthlyReport.objects.select_related("warehouse_location") - .prefetch_related(Prefetch("stockreport_set", queryset=StockReport.objects.all())) - .filter(**filter_params) - .distinct() - ) - today = datetime.datetime.now() - filename = today.today().strftime("%d-%m-%Y") - - try: - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = f"attachment; filename={code}_stock_reports_exported_on_{filename}.csv" - write_csv_columns_and_rows(stock_monthly_report, response) - return response - except Exception as e: - print(f"Error: {e}") - return HttpResponse(status=500) diff --git a/src/project_reports/templates/project_reports/cluster_5w_dashboard.html b/src/project_reports/templates/project_reports/cluster_5w_dashboard.html index 636ba112..5d13b452 100644 --- a/src/project_reports/templates/project_reports/cluster_5w_dashboard.html +++ b/src/project_reports/templates/project_reports/cluster_5w_dashboard.html @@ -22,17 +22,17 @@

{{ request.resolver_match.kwargs.cluster|capfirst }} 5W Dashboard

- Export Stocks Reports - + spinner start - - + spinner end + --> diff --git a/src/project_reports/templates/project_reports/org_5w_dashboard.html b/src/project_reports/templates/project_reports/org_5w_dashboard.html index 480c4887..ed25d60a 100644 --- a/src/project_reports/templates/project_reports/org_5w_dashboard.html +++ b/src/project_reports/templates/project_reports/org_5w_dashboard.html @@ -21,16 +21,10 @@

{{ request.user.profile.organization }}'s 5W Dashboard

- - 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

    +
    + + Export Stock Beneificiary + + + {% include "components/_filter_drawer.html" with filter=warehouse_filter %} +
    +
    +

    + 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 @@
  • +
  • + +
    + + + + Stock Dashboard +
    +
    +