From 268b5037ef3aa31e5b8feac147754024cd7d11b1 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Wed, 24 Jul 2024 15:49:53 -0400 Subject: [PATCH 01/14] Rename view utility module to match the convention of the store utility. --- store/{view_utilities.py => view_utils.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename store/{view_utilities.py => view_utils.py} (100%) diff --git a/store/view_utilities.py b/store/view_utils.py similarity index 100% rename from store/view_utilities.py rename to store/view_utils.py From 41c70cff02f98014a666ab6a8065286aad2951e3 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Wed, 24 Jul 2024 16:28:49 -0400 Subject: [PATCH 02/14] Implement Mixin for handling CSV reports. --- store/view_utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/store/view_utils.py b/store/view_utils.py index 06f6df0..b24e0ca 100644 --- a/store/view_utils.py +++ b/store/view_utils.py @@ -1,7 +1,24 @@ from collections import defaultdict +import csv + +from django.http import HttpResponse 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 get_products_and_quantities_from_bag(request): products = [] if "bag" in request.session: From 21cba22e86d754f4011df41c40ae254066b59033 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Wed, 24 Jul 2024 16:30:26 -0400 Subject: [PATCH 03/14] Implement DownloadCustomerReport. View will eventually handle PDFs too. --- store/views.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/store/views.py b/store/views.py index d1f89d8..3214460 100644 --- a/store/views.py +++ b/store/views.py @@ -4,6 +4,7 @@ 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 @@ -11,11 +12,17 @@ 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 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): @@ -224,3 +231,25 @@ 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) From 9b74225e131e01b147ed51bdbb3ed097430f5afb Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Wed, 24 Jul 2024 16:31:25 -0400 Subject: [PATCH 04/14] Add DownloadCustomerReport view to store url patterns in store/urls.py. --- store/urls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/store/urls.py b/store/urls.py index 1065a58..a80c1b8 100644 --- a/store/urls.py +++ b/store/urls.py @@ -1,5 +1,6 @@ from django.urls import path from .views import ( + DownloadCustomerReport, OrderAdmin, StoreList, StoreProducts, @@ -36,4 +37,9 @@ order_admin_modify, name="order_admin_modify", ), + path( + "user-admin/reports/customers", + DownloadCustomerReport.as_view(), + name="download_customer_report" + ) ] From a4c8766bae6f32589e432fbd8fc110b63c388406 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:18:01 -0400 Subject: [PATCH 05/14] Update Customer CSV timestamp in filename to be snake case rather than slashes. --- store/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/views.py b/store/views.py index 3214460..d468155 100644 --- a/store/views.py +++ b/store/views.py @@ -250,6 +250,6 @@ def get(self, request, *args, **kwargs): for order in orders ] current_datetime = timezone.now() - str_datetime = current_datetime.strftime("%d/%m/%Y_%H:%M:%S") + str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") return self.generate_csv_report(f"customer_list_{str_datetime}", header, data) From c26e8ce8f2ce6e34e917f6292de43f8d2b251e43 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:18:51 -0400 Subject: [PATCH 06/14] Customer CSV URL path changed to better support PDF files. --- store/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/urls.py b/store/urls.py index a80c1b8..e86a51d 100644 --- a/store/urls.py +++ b/store/urls.py @@ -38,7 +38,7 @@ name="order_admin_modify", ), path( - "user-admin/reports/customers", + "user-admin/reports/customers/csv", DownloadCustomerReport.as_view(), name="download_customer_report" ) From d54fe2697abab168bad9f015210b6085a3cc537e Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:19:59 -0400 Subject: [PATCH 07/14] Implement functionality in ReportingMixin to support PDF generation. --- store/view_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/store/view_utils.py b/store/view_utils.py index b24e0ca..e983d93 100644 --- a/store/view_utils.py +++ b/store/view_utils.py @@ -2,6 +2,8 @@ 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 @@ -17,6 +19,15 @@ def generate_csv_report(self, filename, header, 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): From 6a042d1fdf5d734515afac92233ea650900fb2ae Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:25:08 -0400 Subject: [PATCH 08/14] Implement Product Listing CSV reports. --- store/urls.py | 7 ++++++- store/views.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/store/urls.py b/store/urls.py index e86a51d..6968b27 100644 --- a/store/urls.py +++ b/store/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import ( DownloadCustomerReport, + DownloadProductReport, OrderAdmin, StoreList, StoreProducts, @@ -41,5 +42,9 @@ "user-admin/reports/customers/csv", DownloadCustomerReport.as_view(), name="download_customer_report" - ) + path( + "user-admin/reports/products", + DownloadProductReport.as_view(), + name="download_product_report" + ), ] diff --git a/store/views.py b/store/views.py index d468155..74b91d5 100644 --- a/store/views.py +++ b/store/views.py @@ -253,3 +253,27 @@ def get(self, request, *args, **kwargs): str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") return self.generate_csv_report(f"customer_list_{str_datetime}", header, 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) + + From 257a16875d0b5a9886d2c7d1b8dd397308ed7704 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:28:02 -0400 Subject: [PATCH 09/14] Finish URL path for customer CSV reports. --- store/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/store/urls.py b/store/urls.py index 6968b27..66bac44 100644 --- a/store/urls.py +++ b/store/urls.py @@ -42,6 +42,7 @@ "user-admin/reports/customers/csv", DownloadCustomerReport.as_view(), name="download_customer_report" + ), path( "user-admin/reports/products", DownloadProductReport.as_view(), From afaafa3df625ece7a656c1bf04e5666c659b7930 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:29:31 -0400 Subject: [PATCH 10/14] Implement CSV Sales Report for user-admin. --- store/urls.py | 6 ++++++ store/views.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/store/urls.py b/store/urls.py index 66bac44..3dcc3d2 100644 --- a/store/urls.py +++ b/store/urls.py @@ -2,6 +2,7 @@ from .views import ( DownloadCustomerReport, DownloadProductReport, + DownloadSalesReport, OrderAdmin, StoreList, StoreProducts, @@ -48,4 +49,9 @@ DownloadProductReport.as_view(), name="download_product_report" ), + path( + "user-admin/reports/sales", + DownloadSalesReport.as_view(), + name="download_sales_report" + ), ] diff --git a/store/views.py b/store/views.py index 74b91d5..8a8635f 100644 --- a/store/views.py +++ b/store/views.py @@ -14,7 +14,7 @@ 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_utils import ( @@ -277,3 +277,30 @@ def get(self, request, *args, **kwargs): return self.generate_csv_report(f"product_list_{str_datetime}", header, 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 = ["store", "order_id", "product_name", + "product_rating", "product_price", "quantity", + "total_quantity_cost", "total_order_cost"] + data = [ + ( + store, + order_item.order.id, + order_item.product.name, + order_item.product.rating, + order_item.product.price, + order_item.quantity, + order_item.product.price * order_item.quantity, + 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"sales_report_{str_datetime}", header, data) + From 845d6e5874cb6bd7d86d70ff40a7e2259f23147c Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 13:54:11 -0400 Subject: [PATCH 11/14] Implement Customer Report in PDF format. --- store/templates/store/reports/customer.html | 21 +++++++++++++++++++++ store/urls.py | 6 ++++++ store/views.py | 16 ++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 store/templates/store/reports/customer.html diff --git a/store/templates/store/reports/customer.html b/store/templates/store/reports/customer.html new file mode 100644 index 0000000..439c856 --- /dev/null +++ b/store/templates/store/reports/customer.html @@ -0,0 +1,21 @@ +{% extends "store/base.html" %} + +{% block content %} + + + + + + + {% endfor %} +

{{ store }}

First Name + Last Name + Email + Phone Number + {% for customer in customers %} +
{{ customer.first_name}} + {{ customer.last_name}} + {{ customer.email}} + {% if customer.phone_number %}{{ customer.phone_number}}{% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/store/urls.py b/store/urls.py index 3dcc3d2..5f0c375 100644 --- a/store/urls.py +++ b/store/urls.py @@ -1,5 +1,6 @@ from django.urls import path from .views import ( + DownloadCustomerPDFReport, DownloadCustomerReport, DownloadProductReport, DownloadSalesReport, @@ -44,6 +45,11 @@ 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(), diff --git a/store/views.py b/store/views.py index 8a8635f..d0fc956 100644 --- a/store/views.py +++ b/store/views.py @@ -255,6 +255,22 @@ def get(self, request, *args, **kwargs): 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) From de541b645e9620f64df20015d5462ca4c395b2a1 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 14:03:48 -0400 Subject: [PATCH 12/14] Implement Product report in PDF format. --- store/templates/store/reports/product.html | 21 +++++++++++++++++++++ store/urls.py | 6 ++++++ store/views.py | 16 ++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 store/templates/store/reports/product.html diff --git a/store/templates/store/reports/product.html b/store/templates/store/reports/product.html new file mode 100644 index 0000000..e983b9a --- /dev/null +++ b/store/templates/store/reports/product.html @@ -0,0 +1,21 @@ +{% extends "store/base.html" %} + +{% block content %} + + + + + + + {% endfor %} +

Product List for {{ store }}

Product Name + Rating + Price + Description + {% for product in products %} +
{{ product.name }} + {{ product.rating }} + {{ product.price }} + {{ product.description }} +
+{% endblock %} \ No newline at end of file diff --git a/store/urls.py b/store/urls.py index 5f0c375..24586b2 100644 --- a/store/urls.py +++ b/store/urls.py @@ -2,6 +2,7 @@ from .views import ( DownloadCustomerPDFReport, DownloadCustomerReport, + DownloadProductPDFReport, DownloadProductReport, DownloadSalesReport, OrderAdmin, @@ -55,6 +56,11 @@ 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(), diff --git a/store/views.py b/store/views.py index d0fc956..572576a 100644 --- a/store/views.py +++ b/store/views.py @@ -293,6 +293,22 @@ def get(self, request, *args, **kwargs): 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) From 9602e6a06f9863de34db4ff9da5e038c3f69d35c Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 15:40:14 -0400 Subject: [PATCH 13/14] Improve CSV sales report. - Add column for displaying percentage of order. - Add dollar signs and other symbols to better describe the data. - Add store name into the file name. --- store/views.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/store/views.py b/store/views.py index 572576a..f9e4785 100644 --- a/store/views.py +++ b/store/views.py @@ -315,24 +315,28 @@ def get(self, request, *args, **kwargs): order_items: QuerySet[OrderItem] = OrderItem.objects.filter(order__store=store) - header = ["store", "order_id", "product_name", + header = ["order_id", "product_name", "product_rating", "product_price", "quantity", - "total_quantity_cost", "total_order_cost"] + "total_quantity_cost", "percent_of_total_order", "total_order_cost"] data = [ ( - store, order_item.order.id, order_item.product.name, order_item.product.rating, - order_item.product.price, + f"${order_item.product.price}", order_item.quantity, - order_item.product.price * order_item.quantity, - order_item.order.total_cost + 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") + 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) + return self.generate_csv_report(f"sales_report_{str_datetime}", header, data) From 0024304efb5cb0488159046d4eb9c3654ea87fc6 Mon Sep 17 00:00:00 2001 From: "Cioffi, Kevin" Date: Thu, 25 Jul 2024 15:45:19 -0400 Subject: [PATCH 14/14] Implement Sales PDF report. --- store/templates/store/reports/sales.html | 34 ++++++++++++++++++++++++ store/urls.py | 6 +++++ store/views.py | 34 +++++++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 store/templates/store/reports/sales.html diff --git a/store/templates/store/reports/sales.html b/store/templates/store/reports/sales.html new file mode 100644 index 0000000..035ab95 --- /dev/null +++ b/store/templates/store/reports/sales.html @@ -0,0 +1,34 @@ +{% extends "store/base.html" %} + +{% block content %} +

Sales Report

+

Store: {{ store.name }}

+ + + + + + + + + + + + + + + {% for order_item in order_item_data %} + + + + + + + + + + + {% endfor %} + +
Order IDProduct NameProduct RatingProduct PriceQuantityTotal Quantity CostPercent Of Total OrderTotal Cost Of Order
{{ order_item.order_id }}{{ order_item.product_name }}{{ order_item.product_rating }}/10${{ order_item.product_price }}{{ order_item.quantity }}{{ order_item.total_quantity_cost }}{{ order_item.percent_of_total_order }}%${{ order_item.order_cost }}
+{% endblock %} \ No newline at end of file diff --git a/store/urls.py b/store/urls.py index 24586b2..f5a18a2 100644 --- a/store/urls.py +++ b/store/urls.py @@ -4,6 +4,7 @@ DownloadCustomerReport, DownloadProductPDFReport, DownloadProductReport, + DownloadSalesPDFReport, DownloadSalesReport, OrderAdmin, StoreList, @@ -66,4 +67,9 @@ DownloadSalesReport.as_view(), name="download_sales_report" ), + path( + "user-admin/reports/sales/pdf", + DownloadSalesPDFReport.as_view(), + name="download_sales_pdf_report" + ), ] diff --git a/store/views.py b/store/views.py index f9e4785..50667d5 100644 --- a/store/views.py +++ b/store/views.py @@ -338,5 +338,37 @@ def get(self, request, *args, **kwargs): return self.generate_csv_report(f"{store.name.lower()}_sales_report_{str_datetime}", header, data) - return self.generate_csv_report(f"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)