From 537e6c41272919bfbcc84e1bad7fb1525112d12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 10 Dec 2025 07:25:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(procurement):=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B5=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B1=D1=8E=D0=B4=D0=B6=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены возможности: 1. Django Admin Actions: - 'Создать бюджеты на следующий квартал для всех отделов' - 'Скопировать выбранные бюджеты в следующий квартал' 2. Management Command: python manage.py create_next_quarter_budgets Опции: --year YEAR --quarter Q - конкретный период --dry-run - показать без создания --default-amount 100000 - сумма по умолчанию Логика: - Копирует allocated_amount из текущего квартала - Если нет текущего бюджета - использует значение по умолчанию (0) - Пропускает уже существующие бюджеты - Сбрасывает spent_amount в 0 --- backend/procurement/admin.py | 118 ++++++++++++ backend/procurement/management/__init__.py | 1 + .../management/commands/__init__.py | 1 + .../commands/create_next_quarter_budgets.py | 182 ++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 backend/procurement/management/__init__.py create mode 100644 backend/procurement/management/commands/__init__.py create mode 100644 backend/procurement/management/commands/create_next_quarter_budgets.py diff --git a/backend/procurement/admin.py b/backend/procurement/admin.py index 6acfae98..e9ce6820 100644 --- a/backend/procurement/admin.py +++ b/backend/procurement/admin.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.contrib import admin from django.utils.html import format_html @@ -346,6 +348,122 @@ class BudgetAdmin(admin.ModelAdmin): "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): """Остаток бюджета (для отображения в админке).""" diff --git a/backend/procurement/management/__init__.py b/backend/procurement/management/__init__.py new file mode 100644 index 00000000..5e7c3151 --- /dev/null +++ b/backend/procurement/management/__init__.py @@ -0,0 +1 @@ +# Пакет management diff --git a/backend/procurement/management/commands/__init__.py b/backend/procurement/management/commands/__init__.py new file mode 100644 index 00000000..07d9cfc3 --- /dev/null +++ b/backend/procurement/management/commands/__init__.py @@ -0,0 +1 @@ +# Пакет commands diff --git a/backend/procurement/management/commands/create_next_quarter_budgets.py b/backend/procurement/management/commands/create_next_quarter_budgets.py new file mode 100644 index 00000000..93013513 --- /dev/null +++ b/backend/procurement/management/commands/create_next_quarter_budgets.py @@ -0,0 +1,182 @@ +""" +Management команда для создания бюджетов на следующий квартал. + +Использование: + python manage.py create_next_quarter_budgets + + # Создать для конкретного года/квартала: + python manage.py create_next_quarter_budgets --year 2026 --quarter 1 + + # Dry-run (без изменений в БД): + python manage.py create_next_quarter_budgets --dry-run +""" + +from decimal import Decimal + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from employees.models import Department +from procurement.models import Budget + + +class Command(BaseCommand): + help = 'Создать бюджеты на следующий квартал для всех отделов' + + def add_arguments(self, parser): + parser.add_argument( + '--year', + type=int, + help='Год для создания бюджетов (по умолчанию следующий)', + ) + parser.add_argument( + '--quarter', + type=int, + choices=[1, 2, 3, 4], + help='Квартал (1-4)', + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Показать что будет создано, но не создавать', + ) + parser.add_argument( + '--default-amount', + type=float, + default=0.0, + help='Сумма по умолчанию если нет текущего бюджета (₽)', + ) + + def handle(self, *args, **options): + now = timezone.now() + current_quarter = (now.month - 1) // 3 + 1 + + # Определяем целевой квартал + if options['year'] and options['quarter']: + target_year = options['year'] + target_quarter = options['quarter'] + else: + # По умолчанию - следующий квартал + if current_quarter == 4: + target_quarter = 1 + target_year = now.year + 1 + else: + target_quarter = current_quarter + 1 + target_year = now.year + + self.stdout.write( + self.style.SUCCESS( + f'\n📊 Создание бюджетов на {target_year} Q{target_quarter}' + ) + ) + + if options['dry_run']: + self.stdout.write( + self.style.WARNING('🔍 DRY RUN - изменения не будут сохранены') + ) + + # Получаем все отделы + departments = Department.objects.all() + + if not departments.exists(): + self.stdout.write( + self.style.ERROR('❌ Не найдено ни одного отдела!') + ) + return + + created_count = 0 + skipped_count = 0 + errors_count = 0 + + default_amount = Decimal(str(options['default_amount'])) + + self.stdout.write(f'\nВсего отделов: {departments.count()}\n') + + for dept in departments: + # Проверяем существует ли уже бюджет + exists = Budget.objects.filter( + department=dept, + year=target_year, + quarter=target_quarter + ).exists() + + if exists: + self.stdout.write( + f'⏭️ {dept.name}: уже существует' + ) + skipped_count += 1 + continue + + # Пытаемся скопировать бюджет из текущего квартала + try: + current_budget = Budget.objects.get( + department=dept, + year=now.year, + quarter=current_quarter + ) + allocated = current_budget.allocated_amount + source = f'текущий Q{current_quarter}' + except Budget.DoesNotExist: + allocated = default_amount + source = 'по умолчанию' + + # Создаём бюджет + if not options['dry_run']: + try: + Budget.objects.create( + department=dept, + year=target_year, + quarter=target_quarter, + allocated_amount=allocated, + spent_amount=Decimal('0.00') + ) + self.stdout.write( + self.style.SUCCESS( + f'✅ {dept.name}: {allocated:,.0f}₽ ' + f'(из {source})' + ) + ) + created_count += 1 + except Exception as e: + self.stdout.write( + self.style.ERROR( + f'❌ {dept.name}: ошибка - {str(e)}' + ) + ) + errors_count += 1 + else: + self.stdout.write( + f'🔍 {dept.name}: будет создан с {allocated:,.0f}₽ ' + f'(из {source})' + ) + created_count += 1 + + # Итоги + self.stdout.write('\n' + '=' * 60) + + if options['dry_run']: + self.stdout.write( + self.style.SUCCESS( + f'\n✨ Будет создано: {created_count} бюджетов' + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + f'\n✨ Создано: {created_count} бюджетов' + ) + ) + + if skipped_count > 0: + self.stdout.write( + self.style.WARNING( + f'⏭️ Пропущено (уже существуют): {skipped_count}' + ) + ) + + if errors_count > 0: + self.stdout.write( + self.style.ERROR(f'❌ Ошибок: {errors_count}') + ) + + self.stdout.write('')