Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reports to user-admin panel #43

Merged
merged 14 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions store/templates/store/reports/customer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "store/base.html" %}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might consider creating reports_base.html. This is so that you can set reports up independent of the base.html with its navigation etc that will not make much sense on a pdf report.

We see from he weasyprint documentation that using @page we can create a block rules that weasyprint will respond to. A practical example could be

  • setting the page size and orientation (which can also be passed in as variables in the context data).
  • setting up headers and footers so that reports look consistent
    Here's an example of a reports_base.html taken from bits and pieces of their documentation and a bit of regular html/django:
{% load static %}
<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Report {% block title %} {% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
      integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'css/reports/reports_base.css' %}">
    <style>
      @page {
        size: letter Portrait; 
        margin: 1in;
        font-family: 'Roboto', 'Arial', sans-serif;

        @bottom-left {
          content: 'Contempo Crafts';
          font-size: x-small;
        }

        @bottom-center {
          content: "Page " counter(page) " of " counter(pages);
          font-size: x-small;
        }

        @bottom-right {
          content: '{{report_date | default_if_none:""}} {{report_name}}';
          font-size: x-small;
        }

      }
    </style>
    {% block styles %}{% endblock %}

  </head>

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware these features existed, thanks! I will add this suggestion to #3 for my sanity as this seems more related to that issue. That way I can also close and complete this PR.


{% block content %}
<table>
<caption><h1>{{ store }}</h1></caption>
<thead>
<tr>
<th>First Name
<th>Last Name
<th>Email
<th>Phone Number
{% for customer in customers %}
<tr>
<td>{{ customer.first_name}}
<td>{{ customer.last_name}}
<td>{{ customer.email}}
{% if customer.phone_number %}<td>{{ customer.phone_number}}{% endif %}
<tr>
{% endfor %}
</table>
{% endblock %}
21 changes: 21 additions & 0 deletions store/templates/store/reports/product.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "store/base.html" %}

{% block content %}
<table>
<caption><h1>Product List for {{ store }}</h1></caption>
<thead>
<tr>
<th>Product Name
<th>Rating
<th>Price
<th>Description
{% for product in products %}
<tr>
<td>{{ product.name }}
<td>{{ product.rating }}
<td>{{ product.price }}
<td>{{ product.description }}
<tr>
{% endfor %}
</table>
{% endblock %}
34 changes: 34 additions & 0 deletions store/templates/store/reports/sales.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% extends "store/base.html" %}

{% block content %}
<h1>Sales Report</h1>
<p>Store: {{ store.name }}</p>
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Product Name</th>
<th>Product Rating</th>
<th>Product Price</th>
<th>Quantity</th>
<th>Total Quantity Cost</th>
<th>Percent Of Total Order</th>
<th>Total Cost Of Order</th>
</tr>
</thead>
<tbody>
{% for order_item in order_item_data %}
<tr>
<td>{{ order_item.order_id }}</td>
<td>{{ order_item.product_name }}</td>
<td>{{ order_item.product_rating }}/10</td>
<td>${{ order_item.product_price }}</td>
<td>{{ order_item.quantity }}</td>
<td>{{ order_item.total_quantity_cost }}</td>
<td>{{ order_item.percent_of_total_order }}%</td>
<td>${{ order_item.order_cost }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
36 changes: 36 additions & 0 deletions store/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from django.urls import path
from .views import (
DownloadCustomerPDFReport,
DownloadCustomerReport,
DownloadProductPDFReport,
DownloadProductReport,
DownloadSalesPDFReport,
DownloadSalesReport,
OrderAdmin,
StoreList,
StoreProducts,
Expand Down Expand Up @@ -36,4 +42,34 @@
order_admin_modify,
name="order_admin_modify",
),
path(
"user-admin/reports/customers/csv",
DownloadCustomerReport.as_view(),
name="download_customer_report"
),
path(
"user-admin/reports/customers/pdf",
DownloadCustomerPDFReport.as_view(),
name="download_customer_pdf_report"
),
path(
"user-admin/reports/products",
DownloadProductReport.as_view(),
name="download_product_report"
),
path(
"user-admin/reports/products/pdf",
DownloadProductPDFReport.as_view(),
name="download_product_pdf_report"
),
path(
"user-admin/reports/sales",
DownloadSalesReport.as_view(),
name="download_sales_report"
),
path(
"user-admin/reports/sales/pdf",
DownloadSalesPDFReport.as_view(),
name="download_sales_pdf_report"
),
]
28 changes: 28 additions & 0 deletions store/view_utilities.py → store/view_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
from collections import defaultdict
import csv

from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
from store.models import Order, OrderItem, Product, Store


class ReportingMixin:
def generate_csv_report(self, filename, header, data):
response: HttpResponse = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={filename}.csv"

report = csv.writer(response)
report.writerow(header)

for row in data:
report.writerow(row)

return response

def generate_pdf_report(self, filename, template_src, data, inline=True):
response: HttpResponse = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = f"{'inline' if inline else 'attachment'}; filename={filename}.pdf"

template = render_to_string(template_src, data)
HTML(string=template).write_pdf(response)

return response


def get_products_and_quantities_from_bag(request):
products = []
if "bag" in request.session:
Expand Down
152 changes: 150 additions & 2 deletions store/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@
from django.http import HttpResponseRedirect, JsonResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.views import View
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView
from django.views.decorators.http import require_http_methods
from django.contrib import messages
from store.forms import OrderAdminForm, OrderForm, ProductAdminForm
from django.contrib.auth.decorators import login_required
from django.utils import timezone

from store.models import Order, Product, Store
from store.models import Order, OrderItem, Product, Store
from django.contrib.auth.mixins import LoginRequiredMixin

from store.view_utilities import create_orders_for_stores, get_order_items_by_store, get_products_and_quantities_from_bag
from store.view_utils import (
ReportingMixin,
create_orders_for_stores,
get_order_items_by_store,
get_products_and_quantities_from_bag,
)


class StoreProducts(ListView):
Expand Down Expand Up @@ -224,3 +231,144 @@ def order_admin_modify(request, pk):
return render(
request, "store/user-admin/order/order_admin_modify.html", {"form": form, "order": order}
)


class DownloadCustomerReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
orders: QuerySet[Order] = Order.objects.filter(store=store)

header = ["Store", "First Name", "Last Name", "Email", "Phone Number"]
data = [
(
order.store,
order.first_name,
order.last_name,
order.email,
order.phone_number,
)
for order in orders
]
current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_csv_report(f"customer_list_{str_datetime}", header, data)


class DownloadCustomerPDFReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
orders: QuerySet[Order] = Order.objects.filter(store=store)

data = {
"store": store,
"customers": orders
}

current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_pdf_report(f"customer_list_{str_datetime}", "store/reports/customer.html", data)


class DownloadProductReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
products: QuerySet[Product] = Product.objects.filter(store=store)

header = ["store", "name", "rating", "price", "description"]
data = [
(
product.store,
product.name,
product.rating,
product.price,
product.description
)
for product in products
]
current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_csv_report(f"product_list_{str_datetime}", header, data)


class DownloadProductPDFReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
products: QuerySet[Product] = Product.objects.filter(store=store)

data = {
"store": store,
"products": products
}

current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_pdf_report(f"product_list_{str_datetime}", "store/reports/product.html", data)


class DownloadSalesReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
order_items: QuerySet[OrderItem] = OrderItem.objects.filter(order__store=store)


header = ["order_id", "product_name",
"product_rating", "product_price", "quantity",
"total_quantity_cost", "percent_of_total_order", "total_order_cost"]
data = [
(
order_item.order.id,
order_item.product.name,
order_item.product.rating,
f"${order_item.product.price}",
order_item.quantity,
f"${order_item.product.price * order_item.quantity}",
f"{round(((order_item.product.price * order_item.quantity) / order_item.order.total_cost) * 100)}%",
f"${order_item.order.total_cost}",
)

for order_item in order_items
]

current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_csv_report(f"{store.name.lower()}_sales_report_{str_datetime}", header, data)


class DownloadSalesPDFReport(LoginRequiredMixin, ReportingMixin, View):
def get(self, request, *args, **kwargs):
store = Store.objects.for_user_admin(self.request.user)
order_items: QuerySet[OrderItem] = OrderItem.objects.filter(order__store=store)

order_data = []
order_cost_map = {}
for order_item in order_items:
order_id = order_item.order.id
total_quantity_cost = order_item.product.price * order_item.quantity
if order_id not in order_cost_map:
order_cost_map[order_id] = 0

order_data.append({
"order_id": order_id,
"product_name": order_item.product.name,
"product_rating": order_item.product.rating,
"product_price": order_item.product.price,
"quantity": order_item.quantity,
"total_quantity_cost": total_quantity_cost,
"percent_of_total_order": round((total_quantity_cost / order_item.order.total_cost) * 100),
"order_cost": order_item.order.total_cost,
})

data = {
"store": store,
"order_item_data": order_data
}

current_datetime = timezone.now()
str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S")

return self.generate_pdf_report(f"{store.name.lower()}_sales_report_{str_datetime}", "store/reports/sales.html", data)