From 048ce18ac3be812e439e84bafa60d7abe951e6a9 Mon Sep 17 00:00:00 2001 From: Esmat-Farjad Date: Mon, 6 Jan 2025 14:54:43 +0430 Subject: [PATCH 1/4] stock dashboard completed --- src/project_reports/exports.py | 50 ---- .../project_reports/org_5w_dashboard.html | 12 +- src/project_reports/urls.py | 5 - src/project_reports/utils.py | 8 +- src/stock/filter.py | 41 ++++ .../templates/stock/stock_dashboard.html | 221 ++++++++++++++++++ src/stock/urls.py | 10 + src/stock/utils.py | 78 +++---- src/stock/views.py | 220 ++++++++++++++++- 9 files changed, 533 insertions(+), 112 deletions(-) create mode 100644 src/stock/templates/stock/stock_dashboard.html diff --git a/src/project_reports/exports.py b/src/project_reports/exports.py index 58566e07..52198006 100644 --- a/src/project_reports/exports.py +++ b/src/project_reports/exports.py @@ -229,53 +229,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/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.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..e0482492 --- /dev/null +++ b/src/stock/templates/stock/stock_dashboard.html @@ -0,0 +1,221 @@ +{% 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..1ee675ca 100644 --- a/src/stock/views.py +++ b/src/stock/views.py @@ -1,10 +1,13 @@ import calendar 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, Q, Sum from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse_lazy @@ -12,7 +15,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 +374,221 @@ 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 + # Get the warehouses for the user's organization with the required annotation. + 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 + # Prepare the aggregated data for the response. + data = { + "total_beneficiary_coverage": warehouses.aggregate( + total_beneficiary_coverage_sum=Sum( + "stockmonthlyreport__stockreport__beneficiary_coverage", filter=Q(stockmonthlyreport__state="submitted") + ) + )["total_beneficiary_coverage_sum"] + or 0, + "total_reports": warehouses.aggregate( + total_reports_sum=Count( + "stockmonthlyreport__stockreport__id", filter=Q(stockmonthlyreport__state="submitted") + ) + )["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", filter=Q(stockmonthlyreport__state="submitted") + ) + )["total_stock_sum"] + or 0, + "total_pipeline": warehouses.aggregate( + total_pipeline_sum=Sum( + "stockmonthlyreport__stockreport__qty_in_pipeline", filter=Q(stockmonthlyreport__state="submitted") + ) + )["total_pipeline_sum"] + or 0, + "total_people_assisted": warehouses.aggregate( + total_people_assisted_sum=Sum( + "stockmonthlyreport__stockreport__people_to_assisted", filter=Q(stockmonthlyreport__state="submitted") + ) + )["total_people_assisted_sum"] + or 0, + "total_unit_required": warehouses.aggregate( + total_unit_required_sum=Sum( + "stockmonthlyreport__stockreport__unit_required", filter=Q(stockmonthlyreport__state="submitted") + ) + )["total_unit_required_sum"] + or 0, + } + + clusters_beneficiary_dict = {} + cluster_pipleline_list = {} + cluster_stock_list = {} + months_beneficiary = {} + total_clusters = 0 + for warehouse in warehouses: + for report in warehouse.stockmonthlyreport_set.all(): + for stock in report.stockreport_set.all(): + if report.from_date not in months_beneficiary.keys(): + months_beneficiary[report.from_date] = stock.beneficiary_coverage + else: + months_beneficiary[report.from_date] += stock.beneficiary_coverage + if stock.cluster not in clusters_beneficiary_dict.keys(): + total_clusters += 1 + clusters_beneficiary_dict[stock.cluster.code] = stock.beneficiary_coverage + cluster_pipleline_list[stock.cluster.code] = stock.qty_in_pipeline + cluster_stock_list[stock.cluster.code] = stock.qty_in_stock + else: + clusters_beneficiary_dict[stock.cluster.code] += stock.beneficiary_coverage + cluster_pipleline_list[stock.cluster.code] += stock.qty_in_pipeline + cluster_stock_list[stock.cluster.code] += stock.qty_in_stock + + clusters = [key.upper() for key in clusters_beneficiary_dict.keys()] + number_in_stock = [value for value in cluster_stock_list.values()] + number_in_pipeline = [value for value in cluster_pipleline_list.values()] + beneficiary_coverage = [value for value in clusters_beneficiary_dict.values()] + + warehouse_beneficiary = { + "warehouse_name": [warehouse.name if warehouse.name is not None else "Unknown" for warehouse in warehouses], + "total_beneficiary": [ + data.total_beneficiary if data.total_beneficiary is not None else 0 for data in warehouses + ], + } + 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 + df = pd.DataFrame( + dict( + x=[months if months is not None else "unknonw" for months in months_beneficiary.keys()], + y=[value if value is not None else 0 for value in months_beneficiary.values()], + ) + ) + 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 Trend", + "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["total_cluster"] = total_clusters + context = { + "bar_chart": fig.to_html(), + "pie_chart": fig2.to_html(), + "line_chart": line_chart.to_html(), + "data": data, + "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) From bbfa72e01199b2a08bdda01b95fc196e30799e41 Mon Sep 17 00:00:00 2001 From: Esmat-Farjad Date: Mon, 6 Jan 2025 15:00:58 +0430 Subject: [PATCH 2/4] plotly added --- poetry.lock | 34 ++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 33 insertions(+), 2 deletions(-) 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] From c63184955c4897be55433bf0f357b88632ffbba8 Mon Sep 17 00:00:00 2001 From: Esmat-Farjad Date: Tue, 7 Jan 2025 10:59:26 +0430 Subject: [PATCH 3/4] stock dashboard code optimized --- src/project_reports/exports.py | 54 ------ .../project_reports/cluster_5w_dashboard.html | 8 +- src/project_reports/urls.py | 10 +- .../templates/stock/stock_dashboard.html | 6 - src/stock/views.py | 154 ++++++++++-------- src/templates/components/header.html | 15 ++ 6 files changed, 113 insertions(+), 134 deletions(-) diff --git a/src/project_reports/exports.py b/src/project_reports/exports.py index 52198006..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) 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

    {% endblock content %} - {% block scripts %} - - - {% endblock scripts %} diff --git a/src/stock/views.py b/src/stock/views.py index 1ee675ca..c6a1a854 100644 --- a/src/stock/views.py +++ b/src/stock/views.py @@ -1,4 +1,5 @@ import calendar +from collections import defaultdict from datetime import datetime import pandas as pd @@ -7,7 +8,8 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.db.models import Count, Prefetch, Q, 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 @@ -385,86 +387,107 @@ def report_details_view(request, report): @login_required def stock_dashbaord(request): user_org = request.user.profile.organization - # Get the warehouses for the user's organization with the required annotation. - ware = Warehouse.objects.filter(organization=user_org).annotate( - total_beneficiary=Sum("stockmonthlyreport__stockreport__beneficiary_coverage") + # 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 = { + data_calculate = { "total_beneficiary_coverage": warehouses.aggregate( - total_beneficiary_coverage_sum=Sum( - "stockmonthlyreport__stockreport__beneficiary_coverage", filter=Q(stockmonthlyreport__state="submitted") - ) + 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", filter=Q(stockmonthlyreport__state="submitted") - ) - )["total_reports_sum"] + "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", filter=Q(stockmonthlyreport__state="submitted") - ) - )["total_stock_sum"] + "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", filter=Q(stockmonthlyreport__state="submitted") - ) + 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", filter=Q(stockmonthlyreport__state="submitted") - ) + 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", filter=Q(stockmonthlyreport__state="submitted") - ) + total_unit_required_sum=Sum("stockmonthlyreport__stockreport__unit_required") )["total_unit_required_sum"] or 0, } - clusters_beneficiary_dict = {} - cluster_pipleline_list = {} - cluster_stock_list = {} - months_beneficiary = {} + # 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 - for warehouse in warehouses: - for report in warehouse.stockmonthlyreport_set.all(): - for stock in report.stockreport_set.all(): - if report.from_date not in months_beneficiary.keys(): - months_beneficiary[report.from_date] = stock.beneficiary_coverage - else: - months_beneficiary[report.from_date] += stock.beneficiary_coverage - if stock.cluster not in clusters_beneficiary_dict.keys(): - total_clusters += 1 - clusters_beneficiary_dict[stock.cluster.code] = stock.beneficiary_coverage - cluster_pipleline_list[stock.cluster.code] = stock.qty_in_pipeline - cluster_stock_list[stock.cluster.code] = stock.qty_in_stock - else: - clusters_beneficiary_dict[stock.cluster.code] += stock.beneficiary_coverage - cluster_pipleline_list[stock.cluster.code] += stock.qty_in_pipeline - cluster_stock_list[stock.cluster.code] += stock.qty_in_stock - - clusters = [key.upper() for key in clusters_beneficiary_dict.keys()] - number_in_stock = [value for value in cluster_stock_list.values()] - number_in_pipeline = [value for value in cluster_pipleline_list.values()] - beneficiary_coverage = [value for value in clusters_beneficiary_dict.values()] - - warehouse_beneficiary = { - "warehouse_name": [warehouse.name if warehouse.name is not None else "Unknown" for warehouse in warehouses], - "total_beneficiary": [ - data.total_beneficiary if data.total_beneficiary is not None else 0 for data in warehouses - ], - } + 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") @@ -483,11 +506,12 @@ def stock_dashbaord(request): }, ) # Line chart + # Create the DataFrame for the line chart using pd.Series constructor df = pd.DataFrame( - dict( - x=[months if months is not None else "unknonw" for months in months_beneficiary.keys()], - y=[value if value is not None else 0 for value in months_beneficiary.values()], - ) + { + "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( @@ -562,12 +586,12 @@ def stock_dashbaord(request): }, }, ) - data["total_cluster"] = total_clusters + 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, + "data": data_calculate, "warehouse_filter": warehouses_filter, } return render(request, "stock/stock_dashboard.html", context) 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 +
    +
    +
  • From efd62c6a9e31b3a490bfadcce8f742fd6d230059 Mon Sep 17 00:00:00 2001 From: Esmat-Farjad Date: Tue, 7 Jan 2025 11:12:52 +0430 Subject: [PATCH 4/4] spell correction --- src/stock/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stock/views.py b/src/stock/views.py index c6a1a854..1207def7 100644 --- a/src/stock/views.py +++ b/src/stock/views.py @@ -525,7 +525,7 @@ def stock_dashbaord(request): showlegend=True, margin=dict(r=0, t=50, b=0, l=0), title={ - "text": "Stock Report Monthly Trend", + "text": "Stock Report Monthly Trends", "font": { "size": 14, "color": "#A52824",