442 lines
15 KiB
Python
442 lines
15 KiB
Python
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)
|