# Reporting Module Implementation Plan with Excel Export ## Overview This document outlines the implementation plan for the reporting module, including Excel export functionality and various report types for the manufacturing application. ## Reporting Models ### 1. Report Template Model ```python # reports/models.py from django.db import models from django.contrib.auth.models import User class ReportTemplate(models.Model): REPORT_TYPES = [ ('inventory', 'Inventory Report'), ('sales', 'Sales Report'), ('purchasing', 'Purchasing Report'), ('manufacturing', 'Manufacturing Report'), ('financial', 'Financial Report'), ('custom', 'Custom Report'), ] name = models.CharField(max_length=100) description = models.TextField(blank=True) report_type = models.CharField(max_length=20, choices=REPORT_TYPES) template_file = models.FileField(upload_to='report_templates/', blank=True, null=True) is_active = models.BooleanField(default=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name class GeneratedReport(models.Model): REPORT_FORMATS = [ ('pdf', 'PDF'), ('excel', 'Excel'), ('csv', 'CSV'), ('html', 'HTML'), ] report_template = models.ForeignKey(ReportTemplate, on_delete=models.CASCADE) name = models.CharField(max_length=200) description = models.TextField(blank=True) format = models.CharField(max_length=10, choices=REPORT_FORMATS) file_path = models.CharField(max_length=500, blank=True) file_size = models.BigIntegerField(default=0) generated_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) generated_at = models.DateTimeField(auto_now_add=True) parameters = models.JSONField(default=dict, blank=True, help_text="Parameters used to generate this report") def __str__(self): return self.name def get_absolute_url(self): from django.urls import reverse return reverse('reports:report_detail', kwargs={'report_id': self.id}) ``` ## Report Generation System ### 2. Report Generator Classes ```python # reports/generators.py import pandas as pd from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils.dataframe import dataframe_to_rows from django.http import HttpResponse from django.template.loader import render_to_string import io import csv from datetime import datetime class BaseReportGenerator: """Base class for all report generators""" def __init__(self, report_template, parameters=None): self.report_template = report_template self.parameters = parameters or {} def generate_data(self): """Generate report data - to be implemented by subclasses""" raise NotImplementedError def generate_excel(self, data): """Generate Excel report""" df = pd.DataFrame(data) # Create Excel workbook wb = Workbook() ws = wb.active ws.title = self.report_template.name # Add data to worksheet for r in dataframe_to_rows(df, index=False, header=True): ws.append(r) # Style the header row header_font = Font(bold=True) header_fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") for cell in ws[1]: cell.font = header_font cell.fill = header_fill cell.alignment = Alignment(horizontal="center") # Auto-adjust column widths for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) ws.column_dimensions[column_letter].width = min(adjusted_width, 50) # Save to bytes output = io.BytesIO() wb.save(output) output.seek(0) return output def generate_csv(self, data): """Generate CSV report""" df = pd.DataFrame(data) output = io.StringIO() df.to_csv(output, index=False) output.seek(0) return output def generate_html(self, data): """Generate HTML report""" context = { 'report_template': self.report_template, 'data': data, 'generated_at': datetime.now(), 'parameters': self.parameters, } html = render_to_string('reports/report_template.html', context) return io.StringIO(html) def generate_report(self, format='excel'): """Generate report in specified format""" data = self.generate_data() if format == 'excel': return self.generate_excel(data) elif format == 'csv': return self.generate_csv(data) elif format == 'html': return self.generate_html(data) else: raise ValueError(f"Unsupported format: {format}") class InventoryReportGenerator(BaseReportGenerator): """Generate inventory reports""" def generate_data(self): from inventory.models import Product, Inventory, Warehouse # Get inventory data inventory_data = [] inventories = Inventory.objects.select_related('product', 'warehouse').all() for inv in inventories: inventory_data.append({ 'Product Code': inv.product.code, 'Product Name': inv.product.name, 'Warehouse': inv.warehouse.name, 'Quantity': float(inv.quantity), 'Reserved Quantity': float(inv.reserved_quantity), 'Available Quantity': float(inv.available_quantity), 'Unit of Measure': inv.product.unit_of_measure.name if inv.product.unit_of_measure else '', 'Reorder Level': float(inv.product.reorder_level), 'Cost Price': float(inv.product.cost_price) if inv.product.cost_price else 0, 'Selling Price': float(inv.product.selling_price) if inv.product.selling_price else 0, }) return inventory_data class SalesReportGenerator(BaseReportGenerator): """Generate sales reports""" def generate_data(self): from sales.models import SalesOrder, SalesOrderItem, Customer # Get sales data based on parameters start_date = self.parameters.get('start_date') end_date = self.parameters.get('end_date') customer_id = self.parameters.get('customer_id') sales_orders = SalesOrder.objects.select_related('customer').prefetch_related('items__product') if start_date: sales_orders = sales_orders.filter(order_date__gte=start_date) if end_date: sales_orders = sales_orders.filter(order_date__lte=end_date) if customer_id: sales_orders = sales_orders.filter(customer_id=customer_id) # Generate sales data sales_data = [] for order in sales_orders: for item in order.items.all(): sales_data.append({ 'Order Number': order.so_number, 'Order Date': order.order_date.strftime('%Y-%m-%d'), 'Customer': order.customer.name, 'Product Code': item.product.code, 'Product Name': item.product.name, 'Quantity': float(item.quantity), 'Unit Price': float(item.unit_price), 'Total Price': float(item.total_price), 'Status': order.get_status_display(), }) return sales_data class PurchasingReportGenerator(BaseReportGenerator): """Generate purchasing reports""" def generate_data(self): from purchasing.models import PurchaseOrder, PurchaseOrderItem, Supplier # Get purchasing data based on parameters start_date = self.parameters.get('start_date') end_date = self.parameters.get('end_date') supplier_id = self.parameters.get('supplier_id') purchase_orders = PurchaseOrder.objects.select_related('supplier').prefetch_related('items__product') if start_date: purchase_orders = purchase_orders.filter(order_date__gte=start_date) if end_date: purchase_orders = purchase_orders.filter(order_date__lte=end_date) if supplier_id: purchase_orders = purchase_orders.filter(supplier_id=supplier_id) # Generate purchasing data purchasing_data = [] for order in purchase_orders: for item in order.items.all(): purchasing_data.append({ 'PO Number': order.po_number, 'Order Date': order.order_date.strftime('%Y-%m-%d'), 'Supplier': order.supplier.name, 'Product Code': item.product.code, 'Product Name': item.product.name, 'Quantity': float(item.quantity), 'Unit Price': float(item.unit_price), 'Total Price': float(item.total_price), 'Status': order.get_status_display(), }) return purchasing_data class ManufacturingReportGenerator(BaseReportGenerator): """Generate manufacturing reports""" def generate_data(self): from manufacturing.models import ManufacturingOrder, BillOfMaterial # Get manufacturing data based on parameters start_date = self.parameters.get('start_date') end_date = self.parameters.get('end_date') status = self.parameters.get('status') mo_queryset = ManufacturingOrder.objects.select_related('bom__product') if start_date: mo_queryset = mo_queryset.filter(scheduled_start_date__gte=start_date) if end_date: mo_queryset = mo_queryset.filter(scheduled_start_date__lte=end_date) if status: mo_queryset = mo_queryset.filter(status=status) # Generate manufacturing data manufacturing_data = [] for mo in mo_queryset: manufacturing_data.append({ 'MO Number': mo.mo_number, 'Product': mo.bom.product.name, 'Quantity to Produce': float(mo.quantity_to_produce), 'Scheduled Start': mo.scheduled_start_date.strftime('%Y-%m-%d'), 'Scheduled End': mo.scheduled_end_date.strftime('%Y-%m-%d'), 'Actual Start': mo.actual_start_date.strftime('%Y-%m-%d') if mo.actual_start_date else '', 'Actual End': mo.actual_end_date.strftime('%Y-%m-%d') if mo.actual_end_date else '', 'Status': mo.get_status_display(), }) return manufacturing_data class FinancialReportGenerator(BaseReportGenerator): """Generate financial reports""" def generate_data(self): from sales.models import SalesOrder from purchasing.models import PurchaseOrder # Get date range from parameters start_date = self.parameters.get('start_date') end_date = self.parameters.get('end_date') # Get sales data sales_queryset = SalesOrder.objects.all() if start_date: sales_queryset = sales_queryset.filter(order_date__gte=start_date) if end_date: sales_queryset = sales_queryset.filter(order_date__lte=end_date) total_sales = sales_queryset.aggregate( total=models.Sum('total_amount') )['total'] or 0 # Get purchasing data purchase_queryset = PurchaseOrder.objects.all() if start_date: purchase_queryset = purchase_queryset.filter(order_date__gte=start_date) if end_date: purchase_queryset = purchase_queryset.filter(order_date__lte=end_date) total_purchases = purchase_queryset.aggregate( total=models.Sum('total_amount') )['total'] or 0 # Generate financial summary financial_data = [{ 'Period': f"{start_date} to {end_date}" if start_date and end_date else 'All Time', 'Total Sales': float(total_sales), 'Total Purchases': float(total_purchases), 'Net Profit/Loss': float(total_sales - total_purchases), }] return financial_data # Factory for creating report generators class ReportGeneratorFactory: """Factory for creating report generators""" generators = { 'inventory': InventoryReportGenerator, 'sales': SalesReportGenerator, 'purchasing': PurchasingReportGenerator, 'manufacturing': ManufacturingReportGenerator, 'financial': FinancialReportGenerator, } @classmethod def create_generator(cls, report_type, report_template, parameters=None): generator_class = cls.generators.get(report_type) if not generator_class: raise ValueError(f"Unsupported report type: {report_type}") return generator_class(report_template, parameters) ``` ## Report Views ### 3. Report Views Implementation ```python # reports/views.py from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required, permission_required from django.http import HttpResponse, JsonResponse from django.contrib import messages from django.db.models import Q from .models import ReportTemplate, GeneratedReport from .generators import ReportGeneratorFactory from .forms import ReportParametersForm import os @login_required @permission_required('reports.view_report', raise_exception=True) def reports_dashboard(request): """Main reports dashboard view""" # Get recent reports recent_reports = GeneratedReport.objects.select_related( 'report_template', 'generated_by' ).order_by('-generated_at')[:10] # Get available report templates report_templates = ReportTemplate.objects.filter(is_active=True) context = { 'recent_reports': recent_reports, 'report_templates': report_templates, } return render(request, 'reports/dashboard.html', context) @login_required @permission_required('reports.view_report', raise_exception=True) def inventory_report_view(request): """Inventory report view""" template = get_object_or_404(ReportTemplate, report_type='inventory', is_active=True) if request.method == 'POST': # Generate report return generate_report_view(request, template.id) context = { 'report_template': template, 'report_type': 'inventory', } return render(request, 'reports/inventory_report.html', context) @login_required @permission_required('reports.view_report', raise_exception=True) def sales_report_view(request): """Sales report view""" template = get_object_or_404(ReportTemplate, report_type='sales', is_active=True) if request.method == 'POST': # Generate report return generate_report_view(request, template.id) context = { 'report_template': template, 'report_type': 'sales', } return render(request, 'reports/sales_report.html', context) @login_required @permission_required('reports.view_report', raise_exception=True) def purchasing_report_view(request): """Purchasing report view""" template = get_object_or_404(ReportTemplate, report_type='purchasing', is_active=True) if request.method == 'POST': # Generate report return generate_report_view(request, template.id) context = { 'report_template': template, 'report_type': 'purchasing', } return render(request, 'reports/purchasing_report.html', context) @login_required @permission_required('reports.view_report', raise_exception=True) def manufacturing_report_view(request): """Manufacturing report view""" template = get_object_or_404(ReportTemplate, report_type='manufacturing', is_active=True) if request.method == 'POST': # Generate report return generate_report_view(request, template.id) context = { 'report_template': template, 'report_type': 'manufacturing', } return render(request, 'reports/manufacturing_report.html', context) @login_required @permission_required('reports.view_report', raise_exception=True) def financial_report_view(request): """Financial report view""" template = get_object_or_404(ReportTemplate, report_type='financial', is_active=True) if request.method == 'POST': # Generate report return generate_report_view(request, template.id) context = { 'report_template': template, 'report_type': 'financial', } return render(request, 'reports/financial_report.html', context) @login_required @permission_required('reports.generate_report', raise_exception=True) def generate_report_view(request, template_id): """Generate a report""" template = get_object_or_404(ReportTemplate, id=template_id) # Get parameters from POST data parameters = {} for key, value in request.POST.items(): if key not in ['csrfmiddlewaretoken', 'format']: parameters[key] = value # Get format format = request.POST.get('format', 'excel') try: # Create report generator generator = ReportGeneratorFactory.create_generator( template.report_type, template, parameters ) # Generate report data output = generator.generate_report(format) # Create GeneratedReport record report_name = f"{template.name} - {datetime.now().strftime('%Y%m%d_%H%M%S')}" generated_report = GeneratedReport.objects.create( report_template=template, name=report_name, format=format, generated_by=request.user, parameters=parameters, ) # Return file response if format == 'excel': response = HttpResponse( output.getvalue(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = f'attachment; filename="{report_name}.xlsx"' elif format == 'csv': response = HttpResponse( output.getvalue(), content_type='text/csv' ) response['Content-Disposition'] = f'attachment; filename="{report_name}.csv"' else: response = HttpResponse( output.getvalue(), content_type='text/html' ) return response except Exception as e: messages.error(request, f"Error generating report: {str(e)}") return redirect('reports:dashboard') @login_required @permission_required('reports.view_report', raise_exception=True) def report_detail_view(request, report_id): """View details of a generated report""" report = get_object_or_404(GeneratedReport, id=report_id) context = { 'report': report, } return render(request, 'reports/report_detail.html', context) @login_required @permission_required('reports.export_report', raise_exception=True) def export_to_excel_view(request, report_id): """Export a report to Excel""" report = get_object_or_404(GeneratedReport, id=report_id) # Check if file exists if report.file_path and os.path.exists(report.file_path): with open(report.file_path, 'rb') as f: response = HttpResponse( f.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = f'attachment; filename="{report.name}.xlsx"' return response else: messages.error(request, "Report file not found.") return redirect('reports:report_detail', report_id=report_id) @login_required @permission_required('reports.export_report', raise_exception=True) def export_to_pdf_view(request, report_id): """Export a report to PDF""" # Implementation would require a PDF generation library like WeasyPrint or ReportLab # This is a placeholder for future implementation messages.info(request, "PDF export is not yet implemented.") return redirect('reports:report_detail', report_id=report_id) ``` ## Report Forms ### 4. Report Parameter Forms ```python # reports/forms.py from django import forms from django.forms.widgets import DateInput from inventory.models import Product, Warehouse from sales.models import Customer from purchasing.models import Supplier class BaseReportForm(forms.Form): """Base form for report parameters""" start_date = forms.DateField( required=False, widget=DateInput(attrs={'type': 'date', 'class': 'form-control'}), label='Start Date' ) end_date = forms.DateField( required=False, widget=DateInput(attrs={'type': 'date', 'class': 'form-control'}), label='End Date' ) class InventoryReportForm(BaseReportForm): """Form for inventory report parameters""" product = forms.ModelChoiceField( queryset=Product.objects.all(), required=False, empty_label="All Products", widget=forms.Select(attrs={'class': 'form-control'}) ) warehouse = forms.ModelChoiceField( queryset=Warehouse.objects.all(), required=False, empty_label="All Warehouses", widget=forms.Select(attrs={'class': 'form-control'}) ) class SalesReportForm(BaseReportForm): """Form for sales report parameters""" customer = forms.ModelChoiceField( queryset=Customer.objects.all(), required=False, empty_label="All Customers", widget=forms.Select(attrs={'class': 'form-control'}) ) product = forms.ModelChoiceField( queryset=Product.objects.all(), required=False, empty_label="All Products", widget=forms.Select(attrs={'class': 'form-control'}) ) class PurchasingReportForm(BaseReportForm): """Form for purchasing report parameters""" supplier = forms.ModelChoiceField( queryset=Supplier.objects.all(), required=False, empty_label="All Suppliers", widget=forms.Select(attrs={'class': 'form-control'}) ) product = forms.ModelChoiceField( queryset=Product.objects.all(), required=False, empty_label="All Products", widget=forms.Select(attrs={'class': 'form-control'}) ) class ManufacturingReportForm(BaseReportForm): """Form for manufacturing report parameters""" product = forms.ModelChoiceField( queryset=Product.objects.all(), required=False, empty_label="All Products", widget=forms.Select(attrs={'class': 'form-control'}) ) status = forms.ChoiceField( choices=[ ('', 'All Statuses'), ('draft', 'Draft'), ('planned', 'Planned'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ], required=False, widget=forms.Select(attrs={'class': 'form-control'}) ) class FinancialReportForm(BaseReportForm): """Form for financial report parameters""" pass # Uses base form fields ``` ## Report Templates ### 5. Report Dashboard Template ```html {% extends 'base.html' %} {% block title %}Reports - Manufacturing App{% endblock %} {% block content %}
No recent reports generated.
{% endif %}{{ report_template.description }}