""" Inventory app forms with healthcare-focused validation and user experience. """ from django import forms from django.core.exceptions import ValidationError from django.utils import timezone from datetime import date, timedelta from .models import ( InventoryItem, InventoryStock, InventoryLocation, PurchaseOrder, PurchaseOrderItem, Supplier ) class SupplierForm(forms.ModelForm): """ Form for creating and updating suppliers. """ class Meta: model = Supplier fields = [ 'name', 'supplier_code', 'supplier_type', 'contact_person', 'email', 'phone', 'address_line_1', 'address_line_2', 'city', 'state', 'postal_code', 'country', 'tax_id', 'payment_terms', 'is_preferred', 'is_active', 'notes' ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Enter supplier name' }), 'supplier_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Unique supplier code' }), 'supplier_type': forms.Select(attrs={'class': 'form-control'}), 'contact_person': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Primary contact person' }), 'email': forms.EmailInput(attrs={ 'class': 'form-control', 'placeholder': 'supplier@example.com' }), 'phone': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '+1-555-123-4567' }), 'address_line_1': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Street address line 1' }), 'address_line_2': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Street address line 2 (optional)' }), 'city': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'City' }), 'state': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'State/Province' }), 'postal_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Postal/ZIP code' }), 'country': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Country' }), 'tax_id': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Tax ID/VAT number' }), 'payment_terms': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0', 'max': '365' }), 'is_preferred': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Additional notes about this supplier' }), } help_texts = { 'supplier_code': 'Unique code to identify this supplier', 'payment_terms': 'Payment terms in days (e.g., Net 30)', 'credit_limit': 'Maximum credit limit for this supplier', 'is_preferred': 'Mark as preferred supplier for priority ordering', } def clean_supplier_code(self): supplier_code = self.cleaned_data.get('supplier_code') if supplier_code: # Check for uniqueness within tenant (excluding current instance) queryset = Supplier.objects.filter(supplier_code=supplier_code) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError('Supplier code must be unique.') return supplier_code def clean_email(self): email = self.cleaned_data.get('email') if email: # Basic email validation is handled by EmailField # Additional custom validation can be added here pass return email def clean_credit_limit(self): credit_limit = self.cleaned_data.get('credit_limit') if credit_limit is not None and credit_limit < 0: raise ValidationError('Credit limit cannot be negative.') return credit_limit class InventoryLocationForm(forms.ModelForm): """ Form for creating and updating inventory locations. """ class Meta: model = InventoryLocation fields = [ 'name', 'location_code', 'location_type', 'description', 'floor', 'room', 'temperature_controlled', 'secure_location', 'capacity_cubic_feet', 'is_active' ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Enter location name' }), 'location_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Unique location code' }), 'location_type': forms.Select(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Describe this location' }), 'floor': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Floor number or level' }), 'room': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Room or area number' }), 'temperature_controlled': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'secure_location': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'capacity_cubic_feet': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0' }), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } help_texts = { 'location_code': 'Unique code to identify this location', 'temperature_controlled': 'Check if this location has temperature control', 'secure_location': 'Check if this location requires security access', 'capacity_cubic_feet': 'Maximum capacity of this location in cubic feet (optional)', } def clean_location_code(self): location_code = self.cleaned_data.get('location_code') if location_code: # Check for uniqueness within tenant (excluding current instance) queryset = InventoryLocation.objects.filter(location_code=location_code) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError('Location code must be unique.') return location_code def clean_capacity(self): capacity = self.cleaned_data.get('capacity') if capacity is not None and capacity < 0: raise ValidationError('Capacity cannot be negative.') return capacity class InventoryItemForm(forms.ModelForm): """ Form for creating and updating inventory items. """ class Meta: model = InventoryItem fields = [ 'item_name', 'item_code', 'description', 'category', 'unit_of_measure', 'manufacturer', 'model_number', 'storage_requirements', 'reorder_point', 'reorder_quantity', 'unit_cost', 'controlled_substance', 'fda_approved', 'is_active', 'notes' ] widgets = { 'item_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Enter item name' }), 'item_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Unique item code or SKU' }), 'description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Detailed description of the item' }), 'category': forms.Select(attrs={'class': 'form-control'}), 'unit_of_measure': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'e.g., each, box, bottle, mg' }), 'manufacturer': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Manufacturer name' }), 'model_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Model or catalog number' }), 'storage_requirements': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Storage requirements and conditions' }), 'reorder_point': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0' }), 'reorder_quantity': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '1' }), 'unit_cost': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'controlled_substance': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'fda_approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Additional notes' }), } help_texts = { 'item_code': 'Unique code to identify this item (SKU, barcode, etc.)', 'reorder_point': 'Minimum stock level that triggers reordering', 'reorder_quantity': 'Quantity to order when reorder point is reached', 'unit_cost': 'Standard unit cost for this item', 'controlled_substance': 'Check if this is a controlled substance', 'fda_approved': 'Check if this item is FDA approved', } def clean_item_code(self): item_code = self.cleaned_data.get('item_code') if item_code: # Check for uniqueness within tenant (excluding current instance) queryset = InventoryItem.objects.filter(item_code=item_code) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError('Item code must be unique.') return item_code def clean_reorder_point(self): reorder_point = self.cleaned_data.get('reorder_point') if reorder_point is not None and reorder_point < 0: raise ValidationError('Reorder point cannot be negative.') return reorder_point def clean_reorder_quantity(self): reorder_quantity = self.cleaned_data.get('reorder_quantity') if reorder_quantity is not None and reorder_quantity <= 0: raise ValidationError('Reorder quantity must be greater than zero.') return reorder_quantity def clean_unit_cost(self): unit_cost = self.cleaned_data.get('unit_cost') if unit_cost is not None and unit_cost < 0: raise ValidationError('Unit cost cannot be negative.') return unit_cost class InventoryStockForm(forms.ModelForm): """ Form for creating and updating inventory stock. """ class Meta: model = InventoryStock fields = [ 'inventory_item', 'location', 'quantity_on_hand', 'quantity_reserved', 'unit_cost', 'lot_number', 'expiration_date', 'notes' ] widgets = { 'inventory_item': forms.Select(attrs={'class': 'form-control'}), 'location': forms.Select(attrs={'class': 'form-control'}), 'quantity_on_hand': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0' }), 'quantity_reserved': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0' }), 'unit_cost': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'lot_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Lot or batch number' }), 'expiration_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Additional notes' }), } help_texts = { 'quantity_on_hand': 'Current quantity in stock', 'quantity_reserved': 'Quantity reserved for orders', 'unit_cost': 'Cost per unit for this stock', 'lot_number': 'Lot or batch number for tracking', 'expiration_date': 'Expiration date (if applicable)', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): # Filter items and locations by tenant self.fields['inventory_item'].queryset = InventoryItem.objects.filter( tenant=user.tenant, is_active=True ).order_by('item_name') self.fields['location'].queryset = InventoryLocation.objects.filter( tenant=user.tenant, is_active=True ).order_by('name') def clean_quantity_on_hand(self): quantity_on_hand = self.cleaned_data.get('quantity_on_hand') if quantity_on_hand is not None and quantity_on_hand < 0: raise ValidationError('Quantity cannot be negative.') return quantity_on_hand def clean_minimum_stock_level(self): minimum_level = self.cleaned_data.get('minimum_stock_level') if minimum_level is not None and minimum_level < 0: raise ValidationError('Minimum stock level cannot be negative.') return minimum_level def clean_maximum_stock_level(self): maximum_level = self.cleaned_data.get('maximum_stock_level') if maximum_level is not None and maximum_level < 0: raise ValidationError('Maximum stock level cannot be negative.') return maximum_level def clean(self): cleaned_data = super().clean() minimum_level = cleaned_data.get('minimum_stock_level') maximum_level = cleaned_data.get('maximum_stock_level') # Validate min/max relationship if minimum_level and maximum_level: if maximum_level <= minimum_level: raise ValidationError('Maximum stock level must be greater than minimum stock level.') # Validate expiry date expiry_date = cleaned_data.get('expiry_date') if expiry_date and expiry_date <= timezone.now().date(): # Allow past dates but warn about expired items pass return cleaned_data class PurchaseOrderForm(forms.ModelForm): """ Form for creating and updating purchase orders. """ class Meta: model = PurchaseOrder fields = [ 'supplier', 'order_date', 'requested_delivery_date', 'delivery_location', 'priority', 'payment_terms', 'shipping_amount', 'notes' ] widgets = { 'supplier': forms.Select(attrs={'class': 'form-control'}), 'order_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'requested_delivery_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'delivery_location': forms.Select(attrs={'class': 'form-control'}), 'priority': forms.Select(attrs={'class': 'form-control'}), 'payment_terms': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0', 'max': '365' }), 'shipping_amount': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0', 'placeholder': 'Shipping cost' }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Special instructions or notes' }), } help_texts = { 'order_date': 'Date when the order is placed', 'requested_delivery_date': 'Requested delivery date', 'delivery_location': 'Location where items will be delivered', 'priority': 'Priority level for this order', 'payment_terms': 'Payment terms in days (e.g., Net 30)', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): # Filter suppliers and locations by tenant self.fields['supplier'].queryset = Supplier.objects.filter( tenant=user.tenant, is_active=True ).order_by('name') self.fields['delivery_location'].queryset = InventoryLocation.objects.filter( tenant=user.tenant, is_active=True ).order_by('name') def clean_order_date(self): order_date = self.cleaned_data.get('order_date') if order_date and order_date > timezone.now().date(): # Allow future dates for planned orders pass return order_date def clean_expected_delivery_date(self): expected_date = self.cleaned_data.get('expected_delivery_date') order_date = self.cleaned_data.get('order_date') if expected_date and order_date: if expected_date < order_date: raise ValidationError('Expected delivery date cannot be before order date.') return expected_date def clean_payment_terms(self): payment_terms = self.cleaned_data.get('payment_terms') if payment_terms is not None and payment_terms < 0: raise ValidationError('Payment terms cannot be negative.') return payment_terms class PurchaseOrderItemForm(forms.ModelForm): """ Form for creating and updating purchase order items. """ class Meta: model = PurchaseOrderItem fields = [ 'inventory_item', 'quantity_ordered', 'unit_price', 'notes' ] widgets = { 'inventory_item': forms.Select(attrs={'class': 'form-control'}), 'quantity_ordered': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '1' }), 'unit_price': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Item-specific notes' }), } help_texts = { 'quantity_ordered': 'Quantity to order', 'unit_price': 'Price per unit', 'notes': 'Special instructions for this item', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): # Filter items by tenant self.fields['inventory_item'].queryset = InventoryItem.objects.filter( tenant=user.tenant, is_active=True ).order_by('item_name') def clean_quantity_ordered(self): quantity_ordered = self.cleaned_data.get('quantity_ordered') if quantity_ordered is not None and quantity_ordered <= 0: raise ValidationError('Quantity must be greater than zero.') return quantity_ordered def clean_unit_price(self): unit_price = self.cleaned_data.get('unit_price') if unit_price is not None and unit_price < 0: raise ValidationError('Unit price cannot be negative.') return unit_price # ============================================================================ # INLINE FORMSETS FOR RELATED MODELS # ============================================================================ from django.forms import inlineformset_factory # Purchase Order Items formset PurchaseOrderItemFormSet = inlineformset_factory( PurchaseOrder, PurchaseOrderItem, form=PurchaseOrderItemForm, extra=1, can_delete=True, fields=['inventory_item', 'quantity_ordered', 'unit_price', 'notes'] )