Marwan Alwali 6b85b05882 update
2025-08-13 19:31:08 +03:00

783 lines
22 KiB
Python

"""
Inventory app admin configuration.
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from decimal import Decimal
from .models import (
InventoryItem, InventoryStock, InventoryLocation,
PurchaseOrder, PurchaseOrderItem, Supplier
)
class InventoryStockInline(admin.TabularInline):
"""
Inline admin for inventory stock.
"""
model = InventoryStock
extra = 0
fields = [
'location', 'lot_number', 'quantity_on_hand',
'quantity_reserved', 'expiration_date', 'quality_status'
]
readonly_fields = ['quantity_available']
class PurchaseOrderItemInline(admin.TabularInline):
"""
Inline admin for purchase order items.
"""
model = PurchaseOrderItem
extra = 0
fields = [
'line_number', 'inventory_item', 'quantity_ordered',
'quantity_received', 'unit_price', 'total_price'
]
readonly_fields = ['total_price', 'quantity_remaining']
@admin.register(InventoryItem)
class InventoryItemAdmin(admin.ModelAdmin):
"""
Admin interface for inventory items.
"""
list_display = [
'item_code', 'item_name', 'category', 'item_type',
'manufacturer', 'unit_cost', 'current_stock_display',
'reorder_point', 'needs_reorder_display', 'is_active'
]
list_filter = [
'tenant', 'category', 'item_type', 'manufacturer',
'is_active', 'is_tracked', 'has_expiration',
'controlled_substance'
]
search_fields = [
'item_code', 'item_name', 'description',
'manufacturer', 'model_number', 'part_number',
'upc_code', 'ndc_code'
]
readonly_fields = [
'item_id', 'current_stock', 'total_value', 'needs_reorder',
'created_at', 'updated_at'
]
fieldsets = [
('Item Information', {
'fields': [
'item_id', 'tenant', 'item_code', 'item_name', 'description'
]
}),
('Classification', {
'fields': [
'category', 'subcategory', 'item_type'
]
}),
('Manufacturer Information', {
'fields': [
'manufacturer', 'model_number', 'part_number'
]
}),
('Identification Codes', {
'fields': [
'upc_code', 'ndc_code', 'gtin_code'
],
'classes': ['collapse']
}),
('Unit and Packaging', {
'fields': [
'unit_of_measure', 'package_size', 'package_type'
]
}),
('Pricing', {
'fields': [
'unit_cost', 'list_price'
]
}),
('Storage Requirements', {
'fields': [
'storage_temperature_min', 'storage_temperature_max',
'storage_humidity_min', 'storage_humidity_max',
'storage_requirements'
],
'classes': ['collapse']
}),
('Expiration Information', {
'fields': [
'has_expiration', 'shelf_life_days'
]
}),
('Regulatory Information', {
'fields': [
'fda_approved', 'controlled_substance', 'dea_schedule'
],
'classes': ['collapse']
}),
('Inventory Management', {
'fields': [
'is_active', 'is_tracked', 'is_serialized', 'is_lot_tracked'
]
}),
('Reorder Information', {
'fields': [
'reorder_point', 'reorder_quantity', 'min_stock_level', 'max_stock_level'
]
}),
('Supplier Information', {
'fields': [
'primary_supplier'
]
}),
('Clinical Information', {
'fields': [
'clinical_use', 'contraindications'
],
'classes': ['collapse']
}),
('Stock Information', {
'fields': [
'current_stock', 'total_value', 'needs_reorder'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [InventoryStockInline]
date_hierarchy = 'created_at'
def current_stock_display(self, obj):
"""Display current stock with color coding."""
stock = obj.current_stock
if stock <= obj.reorder_point:
color = 'red'
elif stock <= obj.reorder_point * 1.5:
color = 'orange'
else:
color = 'green'
return format_html(
'<span style="color: {};">{}</span>',
color, stock
)
current_stock_display.short_description = 'Current Stock'
def needs_reorder_display(self, obj):
"""Display reorder status with icon."""
if obj.needs_reorder:
return format_html(
'<span style="color: red;">⚠️ Yes</span>'
)
return format_html(
'<span style="color: green;">✓ No</span>'
)
needs_reorder_display.short_description = 'Needs Reorder'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('primary_supplier')
@admin.register(InventoryStock)
class InventoryStockAdmin(admin.ModelAdmin):
"""
Admin interface for inventory stock.
"""
list_display = [
'item_name', 'location_name', 'lot_number',
'quantity_on_hand', 'quantity_reserved', 'quantity_available',
'expiration_date', 'days_to_expiry_display', 'quality_status'
]
list_filter = [
'inventory_item__tenant', 'location', 'quality_status',
'expiration_date', 'received_date'
]
search_fields = [
'inventory_item__item_name', 'inventory_item__item_code',
'location__name', 'lot_number', 'serial_number'
]
readonly_fields = [
'stock_id', 'quantity_available', 'total_cost',
'tenant', 'is_expired', 'days_to_expiry',
'created_at', 'updated_at'
]
fieldsets = [
('Stock Information', {
'fields': [
'stock_id', 'inventory_item', 'location'
]
}),
('Lot Information', {
'fields': [
'lot_number', 'serial_number'
]
}),
('Quantity Information', {
'fields': [
'quantity_on_hand', 'quantity_reserved', 'quantity_available'
]
}),
('Dates', {
'fields': [
'received_date', 'expiration_date'
]
}),
('Cost Information', {
'fields': [
'unit_cost', 'total_cost'
]
}),
('Quality Information', {
'fields': [
'quality_status'
]
}),
('Supplier Information', {
'fields': [
'supplier', 'purchase_order'
]
}),
('Expiration Status', {
'fields': [
'is_expired', 'days_to_expiry'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Related Information', {
'fields': [
'tenant'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at'
],
'classes': ['collapse']
})
]
date_hierarchy = 'expiration_date'
def item_name(self, obj):
"""Display item name."""
return obj.inventory_item.item_name
item_name.short_description = 'Item'
def location_name(self, obj):
"""Display location name."""
return obj.location.name
location_name.short_description = 'Location'
def days_to_expiry_display(self, obj):
"""Display days to expiry with color coding."""
days = obj.days_to_expiry
if days is None:
return "-"
if days < 0:
color = 'red'
text = f"Expired {abs(days)} days ago"
elif days <= 30:
color = 'red'
text = f"{days} days"
elif days <= 90:
color = 'orange'
text = f"{days} days"
else:
color = 'green'
text = f"{days} days"
return format_html(
'<span style="color: {};">{}</span>',
color, text
)
days_to_expiry_display.short_description = 'Days to Expiry'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(inventory_item__tenant=request.user.tenant)
return qs.select_related('inventory_item', 'location', 'supplier')
@admin.register(InventoryLocation)
class InventoryLocationAdmin(admin.ModelAdmin):
"""
Admin interface for inventory locations.
"""
list_display = [
'location_code', 'name', 'location_type', 'building',
'floor', 'room', 'total_items_display', 'total_quantity_display',
'is_active'
]
list_filter = [
'tenant', 'location_type', 'building', 'floor',
'temperature_controlled', 'humidity_controlled',
'secure_location', 'is_active'
]
search_fields = [
'location_code', 'name', 'description',
'building', 'room', 'zone'
]
readonly_fields = [
'location_id', 'full_address', 'total_items', 'total_quantity',
'created_at', 'updated_at'
]
fieldsets = [
('Location Information', {
'fields': [
'location_id', 'tenant', 'location_code', 'name', 'description'
]
}),
('Location Type', {
'fields': [
'location_type'
]
}),
('Physical Information', {
'fields': [
'building', 'floor', 'room', 'zone', 'aisle', 'shelf', 'bin'
]
}),
('Capacity Information', {
'fields': [
'capacity_cubic_feet', 'max_weight_pounds'
],
'classes': ['collapse']
}),
('Environmental Controls', {
'fields': [
'temperature_controlled', 'temperature_min', 'temperature_max',
'humidity_controlled', 'humidity_min', 'humidity_max'
]
}),
('Security and Access', {
'fields': [
'secure_location', 'access_control'
]
}),
('Location Status', {
'fields': [
'is_active'
]
}),
('Hierarchy', {
'fields': [
'parent_location'
],
'classes': ['collapse']
}),
('Staff', {
'fields': [
'location_manager'
]
}),
('Summary Information', {
'fields': [
'full_address', 'total_items', 'total_quantity'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
def total_items_display(self, obj):
"""Display total items count."""
return obj.total_items
total_items_display.short_description = 'Items'
def total_quantity_display(self, obj):
"""Display total quantity."""
return obj.total_quantity
total_quantity_display.short_description = 'Total Qty'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('location_manager')
@admin.register(PurchaseOrder)
class PurchaseOrderAdmin(admin.ModelAdmin):
"""
Admin interface for purchase orders.
"""
list_display = [
'po_number', 'supplier_name', 'order_date',
'total_amount', 'status', 'requested_delivery_date',
'days_outstanding_display', 'is_overdue_display'
]
list_filter = [
'tenant', 'supplier', 'status', 'order_type',
'priority', 'order_date', 'requested_delivery_date'
]
search_fields = [
'po_number', 'supplier__name', 'supplier__supplier_code',
'notes'
]
readonly_fields = [
'po_id', 'po_number', 'total_amount',
'is_overdue', 'days_outstanding',
'created_at', 'updated_at'
]
fieldsets = [
('Purchase Order Information', {
'fields': [
'po_id', 'po_number', 'tenant'
]
}),
('Supplier Information', {
'fields': [
'supplier'
]
}),
('Order Dates', {
'fields': [
'order_date', 'requested_delivery_date',
'promised_delivery_date', 'actual_delivery_date'
]
}),
('Order Type and Priority', {
'fields': [
'order_type', 'priority'
]
}),
('Financial Information', {
'fields': [
'subtotal', 'tax_amount', 'shipping_amount', 'total_amount'
]
}),
('Order Status', {
'fields': [
'status'
]
}),
('Delivery Information', {
'fields': [
'delivery_location', 'delivery_instructions'
]
}),
('Payment Terms', {
'fields': [
'payment_terms'
]
}),
('Approval Information', {
'fields': [
'requested_by', 'approved_by', 'approval_date'
]
}),
('Status Information', {
'fields': [
'is_overdue', 'days_outstanding'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [PurchaseOrderItemInline]
date_hierarchy = 'order_date'
def supplier_name(self, obj):
"""Display supplier name."""
return obj.supplier.name
supplier_name.short_description = 'Supplier'
def days_outstanding_display(self, obj):
"""Display days outstanding with color coding."""
days = obj.days_outstanding
if days <= 7:
color = 'green'
elif days <= 30:
color = 'orange'
else:
color = 'red'
return format_html(
'<span style="color: {};">{} days</span>',
color, days
)
days_outstanding_display.short_description = 'Days Outstanding'
def is_overdue_display(self, obj):
"""Display overdue status with icon."""
if obj.is_overdue:
return format_html(
'<span style="color: red;">⚠️ Yes</span>'
)
return format_html(
'<span style="color: green;">✓ No</span>'
)
is_overdue_display.short_description = 'Overdue'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('supplier', 'requested_by', 'approved_by')
@admin.register(PurchaseOrderItem)
class PurchaseOrderItemAdmin(admin.ModelAdmin):
"""
Admin interface for purchase order items.
"""
list_display = [
'po_number', 'line_number', 'item_name',
'quantity_ordered', 'quantity_received', 'quantity_remaining',
'unit_price', 'total_price', 'status'
]
list_filter = [
'purchase_order__tenant', 'status',
'purchase_order__order_date', 'requested_delivery_date'
]
search_fields = [
'purchase_order__po_number', 'inventory_item__item_name',
'inventory_item__item_code'
]
readonly_fields = [
'item_id', 'total_price', 'quantity_remaining',
'tenant', 'is_fully_received',
'created_at', 'updated_at'
]
fieldsets = [
('Item Information', {
'fields': [
'item_id', 'purchase_order', 'line_number'
]
}),
('Inventory Item', {
'fields': [
'inventory_item'
]
}),
('Quantity Information', {
'fields': [
'quantity_ordered', 'quantity_received', 'quantity_remaining'
]
}),
('Pricing Information', {
'fields': [
'unit_price', 'total_price'
]
}),
('Delivery Information', {
'fields': [
'requested_delivery_date'
]
}),
('Item Status', {
'fields': [
'status'
]
}),
('Status Information', {
'fields': [
'is_fully_received'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Related Information', {
'fields': [
'tenant'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at'
],
'classes': ['collapse']
})
]
def po_number(self, obj):
"""Display PO number."""
return obj.purchase_order.po_number
po_number.short_description = 'PO Number'
def item_name(self, obj):
"""Display item name."""
return obj.inventory_item.item_name
item_name.short_description = 'Item'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(purchase_order__tenant=request.user.tenant)
return qs.select_related('purchase_order', 'inventory_item')
@admin.register(Supplier)
class SupplierAdmin(admin.ModelAdmin):
"""
Admin interface for suppliers.
"""
list_display = [
'supplier_code', 'name', 'supplier_type', 'contact_person',
'phone', 'email', 'performance_rating_display',
'on_time_delivery_rate', 'is_active', 'is_preferred'
]
list_filter = [
'tenant', 'supplier_type', 'is_active', 'is_preferred',
'payment_terms', 'performance_rating'
]
search_fields = [
'supplier_code', 'name', 'contact_person',
'phone', 'email', 'city', 'state'
]
readonly_fields = [
'supplier_id', 'full_address', 'total_orders',
'total_order_value', 'has_active_contract',
'created_at', 'updated_at'
]
fieldsets = [
('Supplier Information', {
'fields': [
'supplier_id', 'tenant', 'supplier_code', 'name'
]
}),
('Supplier Type', {
'fields': [
'supplier_type'
]
}),
('Contact Information', {
'fields': [
'contact_person', 'phone', 'email', 'website'
]
}),
('Address Information', {
'fields': [
'address_line_1', 'address_line_2', 'city',
'state', 'postal_code', 'country'
]
}),
('Business Information', {
'fields': [
'tax_id', 'duns_number'
],
'classes': ['collapse']
}),
('Payment Information', {
'fields': [
'payment_terms'
]
}),
('Performance Information', {
'fields': [
'performance_rating', 'on_time_delivery_rate', 'quality_rating'
]
}),
('Supplier Status', {
'fields': [
'is_active', 'is_preferred'
]
}),
('Certification Information', {
'fields': [
'certifications'
],
'classes': ['collapse']
}),
('Contract Information', {
'fields': [
'contract_start_date', 'contract_end_date'
]
}),
('Summary Information', {
'fields': [
'full_address', 'total_orders', 'total_order_value',
'has_active_contract'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
def performance_rating_display(self, obj):
"""Display performance rating with stars."""
rating = obj.performance_rating
stars = '' * int(rating) + '' * (5 - int(rating))
return format_html(
'<span title="{}/5">{}</span>',
rating, stars
)
performance_rating_display.short_description = 'Performance'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs
# Customize admin site
admin.site.site_header = "Hospital Management System - Inventory"
admin.site.site_title = "Inventory Admin"
admin.site.index_title = "Inventory Administration"