from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required, permission_required from django.contrib import messages from django.http import HttpResponse from django.db.models import Count, Q, Sum from django.utils import timezone from datetime import timedelta from django import forms from django.forms import inlineformset_factory from decimal import Decimal from .models import BillOfMaterial, BOMItem, ManufacturingOrder, MOComponent from apps.inventory.models import Product class BOMItemForm(forms.ModelForm): class Meta: model = BOMItem fields = ['component', 'quantity', 'unit_of_measure'] widgets = { 'quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), } class BOMForm(forms.ModelForm): class Meta: model = BillOfMaterial fields = ['product', 'version', 'is_active'] widgets = { 'version': forms.TextInput(attrs={'placeholder': '1.0'}), } BOMItemFormSet = inlineformset_factory( BillOfMaterial, BOMItem, form=BOMItemForm, extra=1, can_delete=True, min_num=1, validate_min=True ) class MOComponentForm(forms.ModelForm): class Meta: model = MOComponent fields = ['component', 'required_quantity'] widgets = { 'required_quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), } class MOForm(forms.ModelForm): class Meta: model = ManufacturingOrder fields = ['mo_number', 'bom', 'quantity_to_produce', 'scheduled_start_date', 'scheduled_end_date'] widgets = { 'scheduled_start_date': forms.DateInput(attrs={'type': 'date'}), 'scheduled_end_date': forms.DateInput(attrs={'type': 'date'}), 'quantity_to_produce': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), } MOComponentFormSet = inlineformset_factory( ManufacturingOrder, MOComponent, form=MOComponentForm, extra=0, can_delete=True, min_num=0 ) @login_required def manufacturing_dashboard(request): """Manufacturing dashboard view""" total_boms = BillOfMaterial.objects.filter(is_active=True) active_orders = ManufacturingOrder.objects.filter( Q(status='in_progress') | Q(status='scheduled') ) completed_orders = ManufacturingOrder.objects.filter( status='completed', actual_end_date__gte=timezone.now() - timedelta(days=30) ) recent_orders = ManufacturingOrder.objects.select_related('bom__product').order_by('-created_at')[:5] context = { 'module_title': 'Manufacturing Management', 'total_boms': total_boms, 'active_orders': active_orders, 'completed_orders': completed_orders, 'recent_orders': recent_orders, } return render(request, 'manufacturing/dashboard.html', context) # Bill of Material Views @login_required @permission_required('manufacturing.view_billofmaterial', raise_exception=True) def bom_list_view(request): """List all bills of materials""" boms = BillOfMaterial.objects.all() context = { 'module_title': 'Bill of Materials List', 'boms': boms, } return render(request, 'manufacturing/bom_list.html', context) @login_required @permission_required('manufacturing.add_billofmaterial', raise_exception=True) def create_bom_view(request): """Create a new bill of material""" if request.method == 'POST': form = BOMForm(request.POST) formset = BOMItemFormSet(request.POST) if form.is_valid() and formset.is_valid(): # Save the BOM bom = form.save(commit=False) bom.save() # Save the BOM items items = formset.save(commit=False) for item in items: item.bom = bom item.save() messages.success(request, f'Bill of Material "{bom.bom_code}" created successfully!') return redirect('manufacturing:bom_detail', bom_id=bom.id) else: form = BOMForm() formset = BOMItemFormSet() context = { 'form': form, 'formset': formset, 'module_title': 'Create Bill of Material', 'is_create': True, } return render(request, 'manufacturing/bom_form.html', context) @login_required @permission_required('manufacturing.view_billofmaterial', raise_exception=True) def bom_detail_view(request, bom_id): """View bill of material details""" try: bom = BillOfMaterial.objects.get(id=bom_id) context = { 'module_title': 'Bill of Material Details', 'bom': bom, } return render(request, 'manufacturing/bom_detail.html', context) except BillOfMaterial.DoesNotExist: messages.error(request, 'Bill of material not found') return redirect('manufacturing:bom_list') @login_required @permission_required('manufacturing.change_billofmaterial', raise_exception=True) def edit_bom_view(request, bom_id): """Edit an existing bill of material""" try: bom = get_object_or_404(BillOfMaterial, id=bom_id) except: messages.error(request, 'Bill of Material not found.') return redirect('manufacturing:bom_list') if request.method == 'POST': form = BOMForm(request.POST, instance=bom) formset = BOMItemFormSet(request.POST, instance=bom) if form.is_valid() and formset.is_valid(): # Save the BOM bom = form.save(commit=False) bom.save() # Delete existing items and save new ones BOMItem.objects.filter(bom=bom).delete() items = formset.save(commit=False) for item in items: item.bom = bom item.save() messages.success(request, f'Bill of Material "{bom.bom_code}" updated successfully!') return redirect('manufacturing:bom_detail', bom_id=bom.id) else: form = BOMForm(instance=bom) formset = BOMItemFormSet(instance=bom) context = { 'form': form, 'formset': formset, 'bom': bom, 'module_title': f'Edit Bill of Material: {bom.bom_code}', 'is_create': False, } return render(request, 'manufacturing/bom_form.html', context) @login_required @permission_required('manufacturing.delete_billofmaterial', raise_exception=True) def delete_bom_view(request, bom_id): """Delete a bill of material""" try: bom = get_object_or_404(BillOfMaterial, id=bom_id) except: messages.error(request, 'Bill of Material not found.') return redirect('manufacturing:bom_list') if request.method == 'POST': bom_code = bom.bom_code bom.delete() messages.success(request, f'Bill of Material "{bom_code}" deleted successfully!') return redirect('manufacturing:bom_list') context = { 'bom': bom, 'module_title': f'Delete Bill of Material: {bom.bom_code}', } return render(request, 'manufacturing/bom_confirm_delete.html', context) # Manufacturing Order Views @login_required @permission_required('manufacturing.view_manufacturingorder', raise_exception=True) def mo_list_view(request): """List all manufacturing orders""" mos = ManufacturingOrder.objects.all() context = { 'module_title': 'Manufacturing Order List', 'mos': mos, } return render(request, 'manufacturing/mo_list.html', context) @login_required @permission_required('manufacturing.add_manufacturingorder', raise_exception=True) def create_mo_view(request): """Create a new manufacturing order""" if request.method == 'POST': form = MOForm(request.POST) formset = MOComponentFormSet(request.POST) if form.is_valid() and formset.is_valid(): # Save the MO mo = form.save(commit=False) mo.save() # Save the MO components components = formset.save(commit=False) for component in components: component.manufacturing_order = mo component.save() messages.success(request, f'Manufacturing Order "{mo.mo_number}" created successfully!') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) else: form = MOForm() formset = MOComponentFormSet() context = { 'form': form, 'formset': formset, 'module_title': 'Create Manufacturing Order', 'is_create': True, } return render(request, 'manufacturing/mo_form.html', context) @login_required @permission_required('manufacturing.view_manufacturingorder', raise_exception=True) def mo_detail_view(request, mo_number): """View manufacturing order details""" try: mo = ManufacturingOrder.objects.get(mo_number=mo_number) context = { 'module_title': 'Manufacturing Order Details', 'mo': mo, } return render(request, 'manufacturing/mo_detail.html', context) except ManufacturingOrder.DoesNotExist: messages.error(request, 'Manufacturing order not found') return redirect('manufacturing:mo_list') @login_required @permission_required('manufacturing.change_manufacturingorder', raise_exception=True) def edit_mo_view(request, mo_number): """Edit an existing manufacturing order""" try: mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number) except: messages.error(request, 'Manufacturing Order not found.') return redirect('manufacturing:mo_list') if request.method == 'POST': form = MOForm(request.POST, instance=mo) formset = MOComponentFormSet(request.POST, instance=mo) if form.is_valid() and formset.is_valid(): # Save the MO mo = form.save(commit=False) mo.save() # Delete existing components and save new ones MOComponent.objects.filter(manufacturing_order=mo).delete() components = formset.save(commit=False) for component in components: component.manufacturing_order = mo component.save() messages.success(request, f'Manufacturing Order "{mo.mo_number}" updated successfully!') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) else: form = MOForm(instance=mo) formset = MOComponentFormSet(instance=mo) context = { 'form': form, 'formset': formset, 'mo': mo, 'module_title': f'Edit Manufacturing Order: {mo.mo_number}', 'is_create': False, } return render(request, 'manufacturing/mo_form.html', context) @login_required @permission_required('manufacturing.change_manufacturingorder', raise_exception=True) def start_mo_view(request, mo_number): """Start a manufacturing order""" try: mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number) except: messages.error(request, 'Manufacturing Order not found.') return redirect('manufacturing:mo_list') if mo.status != 'scheduled': messages.error(request, f'Cannot start Manufacturing Order in {mo.get_status_display()} status.') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) if request.method == 'POST': mo.status = 'in_progress' mo.actual_start_date = timezone.now() mo.save() messages.success(request, f'Manufacturing Order "{mo.mo_number}" started successfully!') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) context = { 'mo': mo, 'module_title': f'Start Manufacturing Order: {mo.mo_number}', 'action': 'Start', } return render(request, 'manufacturing/mo_confirm_action.html', context) @login_required @permission_required('manufacturing.change_manufacturingorder', raise_exception=True) def complete_mo_view(request, mo_number): """Complete a manufacturing order""" try: mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number) except: messages.error(request, 'Manufacturing Order not found.') return redirect('manufacturing:mo_list') if mo.status != 'in_progress': messages.error(request, f'Cannot complete Manufacturing Order in {mo.get_status_display()} status.') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) if request.method == 'POST': mo.status = 'completed' mo.actual_end_date = timezone.now().date() # Using date() since actual_end_date is DateField mo.save() messages.success(request, f'Manufacturing Order "{mo.mo_number}" completed successfully!') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) context = { 'mo': mo, 'module_title': f'Complete Manufacturing Order: {mo.mo_number}', 'action': 'Complete', } return render(request, 'manufacturing/mo_confirm_action.html', context) @login_required @permission_required('manufacturing.change_manufacturingorder', raise_exception=True) def cancel_mo_view(request, mo_number): """Cancel a manufacturing order""" try: mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number) except: messages.error(request, 'Manufacturing Order not found.') return redirect('manufacturing:mo_list') if mo.status in ['completed', 'cancelled']: messages.error(request, f'Cannot cancel Manufacturing Order in {mo.get_status_display()} status.') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) if request.method == 'POST': mo.status = 'cancelled' mo.actual_end_date = timezone.now() mo.save() messages.success(request, f'Manufacturing Order "{mo.mo_number}" cancelled successfully!') return redirect('manufacturing:mo_detail', mo_number=mo.mo_number) context = { 'mo': mo, 'module_title': f'Cancel Manufacturing Order: {mo.mo_number}', 'action': 'Cancel', } return render(request, 'manufacturing/mo_confirm_action.html', context) @login_required @permission_required('manufacturing.delete_manufacturingorder', raise_exception=True) def delete_mo_view(request, mo_number): """Delete a manufacturing order""" try: mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number) except: messages.error(request, 'Manufacturing Order not found.') return redirect('manufacturing:mo_list') if request.method == 'POST': mo_number = mo.mo_number mo.delete() messages.success(request, f'Manufacturing Order "{mo_number}" deleted successfully!') return redirect('manufacturing:mo_list') context = { 'mo': mo, 'module_title': f'Delete Manufacturing Order: {mo.mo_number}', } return render(request, 'manufacturing/mo_confirm_delete.html', context)