from django.contrib import admin from django.utils.html import format_html from django.urls import reverse from django.db.models import Sum from decimal import Decimal from inventory.models import ( Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer, Inventory, InventoryLine, Reservation, Stock, StockMovement, SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem ) # ===== SHOWCASE ===== @admin.register(Showcase) class ShowcaseAdmin(admin.ModelAdmin): list_display = ('name', 'warehouse', 'is_default', 'is_active', 'created_at') list_filter = ('is_active', 'is_default', 'warehouse', 'created_at') search_fields = ('name', 'warehouse__name') date_hierarchy = 'created_at' fieldsets = ( ('Основная информация', { 'fields': ('name', 'warehouse', 'description', 'is_active', 'is_default') }), ('Даты', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ('created_at', 'updated_at') # ===== WAREHOUSE ===== @admin.register(Warehouse) class WarehouseAdmin(admin.ModelAdmin): list_display = ('name', 'is_default_display', 'is_active', 'created_at') list_filter = ('is_active', 'is_default', 'created_at') search_fields = ('name',) fieldsets = ( ('Основная информация', { 'fields': ('name', 'description', 'is_active', 'is_default') }), ('Даты', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ('created_at', 'updated_at') def is_default_display(self, obj): if obj.is_default: return format_html('★ По умолчанию') return '-' is_default_display.short_description = 'По умолчанию' # ===== STOCK BATCH ===== @admin.register(StockBatch) class StockBatchAdmin(admin.ModelAdmin): list_display = ('product', 'warehouse', 'quantity_display', 'cost_price', 'created_at', 'is_active') list_filter = ('warehouse', 'is_active', 'created_at') search_fields = ('product__name', 'product__sku', 'warehouse__name') date_hierarchy = 'created_at' fieldsets = ( ('Партия', { 'fields': ('product', 'warehouse', 'quantity', 'is_active') }), ('Финансы', { 'fields': ('cost_price',) }), ('Даты', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ('created_at', 'updated_at') def quantity_display(self, obj): if obj.quantity <= 0: color = '#ff0000' # красный elif obj.quantity < 10: color = '#ff9900' # оранжевый else: color = '#008000' # зелёный return format_html( '{}', color, f'{obj.quantity} шт' ) quantity_display.short_description = 'Количество' # ===== INCOMING BATCH ===== @admin.register(IncomingBatch) class IncomingBatchAdmin(admin.ModelAdmin): list_display = ('document_number', 'warehouse', 'supplier_name', 'items_count', 'created_at') list_filter = ('warehouse', 'created_at') search_fields = ('document_number', 'supplier_name') date_hierarchy = 'created_at' fieldsets = ( ('Партия поступления', { 'fields': ('document_number', 'warehouse', 'supplier_name', 'notes') }), ('Даты', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ('created_at', 'updated_at') def items_count(self, obj): return obj.items.count() items_count.short_description = 'Товаров' # ===== INCOMING ===== @admin.register(Incoming) class IncomingAdmin(admin.ModelAdmin): list_display = ('product', 'batch', 'quantity', 'cost_price', 'created_at') list_filter = ('batch__warehouse', 'created_at', 'product') search_fields = ('product__name', 'batch__document_number') date_hierarchy = 'created_at' fieldsets = ( ('Товар в партии', { 'fields': ('batch', 'product', 'quantity', 'cost_price', 'stock_batch') }), ('Дата', { 'fields': ('created_at',), 'classes': ('collapse',) }), ) readonly_fields = ('created_at', 'stock_batch') # ===== SALE BATCH ALLOCATION (INLINE) ===== class SaleBatchAllocationInline(admin.TabularInline): model = SaleBatchAllocation extra = 0 readonly_fields = ('batch', 'quantity', 'cost_price') can_delete = False fields = ('batch', 'quantity', 'cost_price') # ===== SALE ===== @admin.register(Sale) class SaleAdmin(admin.ModelAdmin): list_display = ('product', 'warehouse', 'quantity', 'sale_price', 'order_display', 'processed_display', 'date') list_filter = ('warehouse', 'processed', 'date') search_fields = ('product__name', 'order__order_number') date_hierarchy = 'date' fieldsets = ( ('Продажа', { 'fields': ('product', 'warehouse', 'quantity', 'sale_price', 'order') }), ('Статус', { 'fields': ('processed',) }), ('Документ', { 'fields': ('document_number',) }), ('Дата', { 'fields': ('date',), 'classes': ('collapse',) }), ) readonly_fields = ('date',) inlines = [SaleBatchAllocationInline] def order_display(self, obj): if obj.order: return f"ORD-{obj.order.order_number}" return "-" order_display.short_description = 'Заказ' def processed_display(self, obj): if obj.processed: return format_html('✓ Обработана') return format_html('✗ Ожидает') processed_display.short_description = 'Статус' # ===== WRITE OFF ===== @admin.register(WriteOff) class WriteOffAdmin(admin.ModelAdmin): list_display = ('batch', 'quantity', 'reason_display', 'cost_price', 'date') list_filter = ('reason', 'date', 'batch__warehouse') search_fields = ('batch__product__name', 'document_number') date_hierarchy = 'date' fieldsets = ( ('Списание', { 'fields': ('batch', 'quantity', 'reason', 'cost_price') }), ('Документ', { 'fields': ('document_number', 'notes') }), ('Дата', { 'fields': ('date',), 'classes': ('collapse',) }), ) readonly_fields = ('date', 'cost_price') def reason_display(self, obj): return obj.get_reason_display() reason_display.short_description = 'Причина' # ===== TRANSFER ===== @admin.register(Transfer) class TransferAdmin(admin.ModelAdmin): list_display = ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'date') list_filter = ('date', 'from_warehouse', 'to_warehouse') search_fields = ('batch__product__name', 'document_number') date_hierarchy = 'date' fieldsets = ( ('Перемещение', { 'fields': ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'new_batch') }), ('Документ', { 'fields': ('document_number',) }), ('Дата', { 'fields': ('date',), 'classes': ('collapse',) }), ) readonly_fields = ('date', 'new_batch') # ===== INVENTORY LINE (INLINE) ===== class InventoryLineInline(admin.TabularInline): model = InventoryLine extra = 1 fields = ('product', 'quantity_system', 'quantity_fact', 'difference', 'processed') readonly_fields = ('difference', 'processed') # ===== INVENTORY ===== @admin.register(Inventory) class InventoryAdmin(admin.ModelAdmin): list_display = ('warehouse', 'status_display', 'date', 'conducted_by') list_filter = ('status', 'date', 'warehouse') search_fields = ('warehouse__name', 'conducted_by') date_hierarchy = 'date' fieldsets = ( ('Инвентаризация', { 'fields': ('warehouse', 'status', 'conducted_by', 'notes') }), ('Дата', { 'fields': ('date',), 'classes': ('collapse',) }), ) readonly_fields = ('date',) inlines = [InventoryLineInline] actions = ['process_inventory'] def status_display(self, obj): colors = { 'draft': '#ff9900', # оранжевый 'processing': '#0099ff', # синий 'completed': '#008000' # зелёный } return format_html( '{}', colors.get(obj.status, '#000000'), obj.get_status_display() ) status_display.short_description = 'Статус' def process_inventory(self, request, queryset): from inventory.services import InventoryProcessor for inventory in queryset: result = InventoryProcessor.process_inventory(inventory.id) self.message_user( request, f"Инвентаризация {inventory.warehouse.name}: " f"обработано {result['processed_lines']} строк, " f"создано {result['writeoffs_created']} списаний и " f"{result['incomings_created']} приходов" ) process_inventory.short_description = 'Обработать инвентаризацию' # ===== RESERVATION ===== @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display = ('product', 'warehouse', 'quantity', 'status_display', 'context_info', 'reserved_at') list_filter = ('status', 'reserved_at', 'warehouse', 'showcase') search_fields = ('product__name', 'order_item__order__order_number', 'showcase__name') date_hierarchy = 'reserved_at' fieldsets = ( ('Резерв', { 'fields': ('product', 'warehouse', 'quantity', 'status', 'order_item', 'showcase') }), ('Даты', { 'fields': ('reserved_at', 'released_at', 'converted_at') }), ) readonly_fields = ('reserved_at', 'released_at', 'converted_at') def status_display(self, obj): colors = { 'reserved': '#0099ff', # синий 'released': '#ff0000', # красный 'converted_to_sale': '#008000' # зелёный } return format_html( '{}', colors.get(obj.status, '#000000'), obj.get_status_display() ) status_display.short_description = 'Статус' def context_info(self, obj): if obj.order_item: return format_html( '📎 Заказ ORD-{}', obj.order_item.order.order_number ) elif obj.showcase: return format_html( '🌺 Витрина: {}', obj.showcase.name ) return "-" context_info.short_description = 'Контекст' # ===== STOCK ===== @admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display = ('product', 'warehouse', 'quantity_available', 'quantity_reserved', 'quantity_free', 'updated_at') list_filter = ('warehouse', 'updated_at') search_fields = ('product__name', 'product__sku', 'warehouse__name') fieldsets = ( ('Остаток', { 'fields': ('product', 'warehouse', 'quantity_available', 'quantity_reserved') }), ('Дата', { 'fields': ('updated_at',), 'classes': ('collapse',) }), ) readonly_fields = ('quantity_available', 'quantity_reserved', 'updated_at') # ===== STOCK MOVEMENT (для аудита) ===== @admin.register(StockMovement) class StockMovementAdmin(admin.ModelAdmin): list_display = ('product', 'change', 'reason', 'order', 'created_at') list_filter = ('reason', 'created_at') search_fields = ('product__name', 'order__order_number') date_hierarchy = 'created_at' readonly_fields = ('created_at',) # ===== WRITEOFF DOCUMENT (документы списания) ===== class WriteOffDocumentItemInline(admin.TabularInline): model = WriteOffDocumentItem extra = 0 fields = ('product', 'quantity', 'reason', 'notes', 'reservation') readonly_fields = ('reservation',) raw_id_fields = ('product',) @admin.register(WriteOffDocument) class WriteOffDocumentAdmin(admin.ModelAdmin): list_display = ('document_number', 'warehouse', 'status_display', 'date', 'items_count', 'total_quantity_display', 'created_by', 'created_at') list_filter = ('status', 'warehouse', 'date', 'created_at') search_fields = ('document_number', 'warehouse__name') date_hierarchy = 'date' readonly_fields = ('document_number', 'created_at', 'updated_at', 'confirmed_at', 'confirmed_by') inlines = [WriteOffDocumentItemInline] fieldsets = ( ('Документ', { 'fields': ('document_number', 'warehouse', 'status', 'date', 'notes') }), ('Аудит', { 'fields': ('created_by', 'created_at', 'confirmed_by', 'confirmed_at', 'updated_at'), 'classes': ('collapse',) }), ) def status_display(self, obj): colors = { 'draft': '#ff9900', 'confirmed': '#008000', 'cancelled': '#ff0000', } return format_html( '{}', colors.get(obj.status, '#000000'), obj.get_status_display() ) status_display.short_description = 'Статус' def items_count(self, obj): return obj.items.count() items_count.short_description = 'Позиций' def total_quantity_display(self, obj): return f"{obj.total_quantity} шт" total_quantity_display.short_description = 'Всего' @admin.register(WriteOffDocumentItem) class WriteOffDocumentItemAdmin(admin.ModelAdmin): list_display = ('document', 'product', 'quantity', 'reason', 'created_at') list_filter = ('reason', 'document__status', 'created_at') search_fields = ('product__name', 'document__document_number') raw_id_fields = ('product', 'document', 'reservation')