from decimal import Decimal from django import forms from django.contrib import admin from django.utils.html import format_html from .models import ( Approval, ApprovalRoute, Budget, Equipment, EquipmentCategory, MaintenanceRecord, ProcurementItem, ProcurementRequest, ProcurementRequestView, ProcurementSettings, Supplier, ) class ProcurementItemInline(admin.TabularInline): """Инлайн для позиций заявки.""" model = ProcurementItem extra = 1 fields = [ "name", "quantity", "unit", "estimated_unit_price", "actual_unit_price", "ordered_quantity", "received_quantity", "execution_status", "expected_delivery_dates", "supplier_info", ] class ApprovalInline(admin.TabularInline): """Инлайн для согласований.""" model = Approval extra = 0 fields = [ "approver", "priority", "step_name", "status", "comment", "created_at", ] readonly_fields = ["created_at"] can_delete = False @admin.register(ProcurementRequestView) class ProcurementRequestViewAdmin(admin.ModelAdmin): """Админка для персональных отметок просмотра заявок.""" list_display = [ "request", "user", "is_viewed", "viewed_at", "updated_at", ] list_filter = ["is_viewed", "viewed_at", "updated_at"] search_fields = [ "request__title", "user__email", "user__first_name", "user__last_name", ] readonly_fields = ["updated_at"] class ProcurementSettingsAdminForm(forms.ModelForm): class Meta: model = ProcurementSettings fields = "__all__" def clean(self): cleaned_data = super().clean() default_department = cleaned_data.get("default_processing_department") available_departments = cleaned_data.get("available_processing_departments") if ( default_department and available_departments is not None and available_departments.exists() and default_department not in available_departments ): self.add_error( "default_processing_department", ( "Отдел по умолчанию должен входить в список доступных " "отделов-исполнителей." ), ) return cleaned_data @admin.register(ProcurementSettings) class ProcurementSettingsAdmin(admin.ModelAdmin): """Админка настроек закупок.""" form = ProcurementSettingsAdminForm list_display = [ "name", "default_processing_department", "available_processing_departments_count", "updated_at", ] fields = [ "name", "default_processing_department", "available_processing_departments", "updated_at", ] readonly_fields = ["updated_at"] filter_horizontal = ["available_processing_departments"] def has_add_permission(self, request): if ProcurementSettings.objects.exists(): return False return super().has_add_permission(request) def has_delete_permission(self, request, obj=None): return request.user.is_superuser def available_processing_departments_count(self, obj): count = obj.available_processing_departments.count() return count or "Все отделы" available_processing_departments_count.short_description = "Доступные отделы" @admin.register(ProcurementRequest) class ProcurementRequestAdmin(admin.ModelAdmin): """Админка для заявок на закупку.""" list_display = [ "id", "title", "department", "processing_department", "requestor", "status_badge", "fulfillment_status", "urgency_badge", "get_total_cost", "created_at", ] list_filter = [ "status", "fulfillment_status", "urgency", "department", "processing_department", "created_at", ] search_fields = ["title", "description", "requestor__username"] readonly_fields = [ "created_at", "updated_at", "submitted_at", "completed_at", "get_total_cost", ] date_hierarchy = "created_at" inlines = [ProcurementItemInline, ApprovalInline] fieldsets = ( ( "Основная информация", { "fields": ( "title", "description", "department", "processing_department", "requestor", ) }, ), ( "Параметры", { "fields": ( "status", "fulfillment_status", "urgency", "get_total_cost", "actual_cost", ) }, ), ( "Временные метки", { "fields": ( "created_at", "updated_at", "submitted_at", "completed_at", ), "classes": ("collapse",), }, ), ) def status_badge(self, obj): """Цветной бадж статуса.""" colors = { "draft": "#6c757d", "waiting": "#ffc107", "pending": "#0dcaf0", "approved": "#198754", "rejected": "#dc3545", "in_progress": "#0d6efd", "completed": "#198754", "cancelled": "#6c757d", } color = colors.get(obj.status, "#6c757d") return format_html( '{}', color, obj.get_status_display(), ) status_badge.short_description = "Статус" def urgency_badge(self, obj): """Цветной бадж срочности.""" colors = { "low": "#198754", "medium": "#ffc107", "high": "#fd7e14", "critical": "#dc3545", } color = colors.get(obj.urgency, "#6c757d") return format_html( '{}', color, obj.get_urgency_display(), ) urgency_badge.short_description = "Срочность" def get_total_cost(self, obj): """Сумма заявки из позиций.""" return f"{obj.total_cost:,.2f} ₽" get_total_cost.short_description = "Сумма" @admin.register(ProcurementItem) class ProcurementItemAdmin(admin.ModelAdmin): """Админка для позиций заявок.""" list_display = [ "id", "request", "name", "quantity", "unit", "estimated_unit_price", "actual_unit_price", "ordered_quantity", "received_quantity", "execution_status", "expected_delivery_dates", "total_price", ] list_filter = ["unit", "execution_status", "created_at"] search_fields = ["name", "description", "request__title"] readonly_fields = ["created_at", "total_price"] @admin.register(ApprovalRoute) class ApprovalRouteAdmin(admin.ModelAdmin): """Маршруты согласования закупок.""" list_display = [ "priority", "min_amount", "name", "resolver_type", "employee", ] ordering = ["priority"] autocomplete_fields = ["employee"] @admin.register(Approval) class ApprovalAdmin(admin.ModelAdmin): """Админка для согласований.""" list_display = [ "id", "request", "approver", "priority", "step_name", "status_badge", "created_at", ] list_filter = ["status", "priority", "created_at"] search_fields = [ "request__title", "approver__username", "approver__email", ] readonly_fields = ["created_at", "updated_at"] def status_badge(self, obj): """Цветной бадж статуса.""" colors = { "pending": "#ffc107", "approved": "#198754", "rejected": "#dc3545", } color = colors.get(obj.status, "#6c757d") return format_html( '{}', color, obj.get_status_display(), ) status_badge.short_description = "Статус" @admin.register(EquipmentCategory) class EquipmentCategoryAdmin(admin.ModelAdmin): """Админка для категорий оборудования.""" list_display = ["id", "name", "parent", "icon", "created_at"] list_filter = ["parent"] search_fields = ["name", "description"] readonly_fields = ["created_at", "full_path"] @admin.register(Equipment) class EquipmentAdmin(admin.ModelAdmin): """Админка для оборудования.""" list_display = [ "inventory_number", "name", "category", "status_badge", "department", "responsible_person", "warranty_status", ] list_filter = ["status", "category", "department", "purchase_date"] search_fields = [ "name", "inventory_number", "serial_number", "responsible_person__username", ] readonly_fields = [ "created_at", "updated_at", "is_under_warranty", ] date_hierarchy = "purchase_date" fieldsets = ( ( "Основная информация", { "fields": ( "name", "inventory_number", "serial_number", "category", ) }, ), ( "Статус и расположение", { "fields": ( "status", "department", "responsible_person", "location", ) }, ), ( "Финансовая информация", { "fields": ( "purchase_date", "warranty_until", "purchase_cost", "is_under_warranty", ) }, ), ( "Дополнительно", { "fields": ("notes", "created_at", "updated_at"), "classes": ("collapse",), }, ), ) def status_badge(self, obj): """Цветной бадж статуса.""" colors = { "available": "#198754", "in_use": "#0d6efd", "maintenance": "#ffc107", "repair": "#fd7e14", "retired": "#6c757d", "lost": "#dc3545", } color = colors.get(obj.status, "#6c757d") return format_html( '{}', color, obj.get_status_display(), ) status_badge.short_description = "Статус" def warranty_status(self, obj): """Статус гарантии.""" if obj.is_under_warranty: return format_html('✓ На гарантии') return format_html('— Гарантия истекла') warranty_status.short_description = "Гарантия" @admin.register(MaintenanceRecord) class MaintenanceRecordAdmin(admin.ModelAdmin): """Админка для записей обслуживания.""" list_display = [ "id", "equipment", "date", "type", "cost", "performed_by", "next_maintenance_date", ] list_filter = ["type", "date", "performed_by"] search_fields = [ "equipment__name", "equipment__inventory_number", "description", ] readonly_fields = ["created_at"] date_hierarchy = "date" @admin.register(Budget) class BudgetAdmin(admin.ModelAdmin): """Админка для бюджетов.""" list_display = [ "id", "department", "year", "quarter", "allocated_amount", "spent_amount", "get_remaining_amount", "utilization_badge", ] list_filter = ["year", "quarter", "department"] search_fields = ["department__name"] readonly_fields = [ "created_at", "updated_at", "get_remaining_amount", "get_utilization_percentage", ] actions = ["create_next_quarter_budgets", "copy_selected_budgets"] def create_next_quarter_budgets(self, request, queryset): """Создать бюджеты на следующий квартал для всех отделов.""" from django.utils import timezone from employees.models import Department now = timezone.now() current_quarter = (now.month - 1) // 3 + 1 # Определяем следующий квартал if current_quarter == 4: next_quarter = 1 next_year = now.year + 1 else: next_quarter = current_quarter + 1 next_year = now.year # Получаем все активные отделы departments = Department.objects.all() created_count = 0 skipped_count = 0 for dept in departments: # Проверяем существует ли уже бюджет exists = Budget.objects.filter( department=dept, year=next_year, quarter=next_quarter ).exists() if exists: skipped_count += 1 continue # Пытаемся скопировать бюджет из текущего квартала try: current_budget = Budget.objects.get( department=dept, year=now.year, quarter=current_quarter ) allocated = current_budget.allocated_amount except Budget.DoesNotExist: # Если нет текущего бюджета - ставим 0 allocated = Decimal("0.00") Budget.objects.create( department=dept, year=next_year, quarter=next_quarter, allocated_amount=allocated, spent_amount=Decimal("0.00"), ) created_count += 1 self.message_user( request, f"Создано {created_count} бюджетов на " f"{next_year} Q{next_quarter}. " f"Пропущено (уже существуют): {skipped_count}.", ) create_next_quarter_budgets.short_description = ( "🔄 Создать бюджеты на следующий квартал для всех отделов" ) def copy_selected_budgets(self, request, queryset): """Копировать выбранные бюджеты в следующий квартал.""" from django.utils import timezone now = timezone.now() current_quarter = (now.month - 1) // 3 + 1 # Определяем следующий квартал if current_quarter == 4: next_quarter = 1 next_year = now.year + 1 else: next_quarter = current_quarter + 1 next_year = now.year created_count = 0 skipped_count = 0 for budget in queryset: # Проверяем существует ли уже бюджет exists = Budget.objects.filter( department=budget.department, year=next_year, quarter=next_quarter, ).exists() if exists: skipped_count += 1 continue Budget.objects.create( department=budget.department, year=next_year, quarter=next_quarter, allocated_amount=budget.allocated_amount, spent_amount=Decimal("0.00"), ) created_count += 1 self.message_user( request, f"Скопировано {created_count} бюджетов в " f"{next_year} Q{next_quarter}. " f"Пропущено (уже существуют): {skipped_count}.", ) copy_selected_budgets.short_description = ( "📋 Скопировать выбранные бюджеты в следующий квартал" ) def get_remaining_amount(self, obj): """Остаток бюджета (для отображения в админке).""" return obj.remaining_amount get_remaining_amount.short_description = "Остаток" def get_utilization_percentage(self, obj): """Процент использования (для отображения в админке).""" return f"{obj.utilization_percentage}%" get_utilization_percentage.short_description = "Использовано" def utilization_badge(self, obj): """Цветной бадж использования бюджета.""" percentage = obj.utilization_percentage if percentage < 50: color = "#198754" elif percentage < 75: color = "#ffc107" elif percentage < 90: color = "#fd7e14" else: color = "#dc3545" percentage_str = f"{float(percentage):.1f}" return format_html( '{}%', color, percentage_str, ) utilization_badge.short_description = "Использовано" @admin.register(Supplier) class SupplierAdmin(admin.ModelAdmin): """Админка для поставщиков.""" list_display = [ "id", "name", "contact_person", "phone", "email", "rating", "is_active", ] list_filter = ["is_active", "rating"] search_fields = ["name", "contact_person", "email", "inn"] readonly_fields = ["created_at", "updated_at"] fieldsets = ( ("Основная информация", {"fields": ("name", "is_active", "rating")}), ( "Контактная информация", { "fields": ( "contact_person", "phone", "email", "website", "address", ) }, ), ("Юридическая информация", {"fields": ("inn",)}), ( "Дополнительно", { "fields": ("notes", "created_at", "updated_at"), "classes": ("collapse",), }, ), )