2025-08-12 13:33:25 +03:00

570 lines
21 KiB
Python

"""
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('location_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('supplier_name')
self.fields['delivery_location'].queryset = InventoryLocation.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('location_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']
)