feat(procurement): массовое создание бюджетов
Добавлены возможности: 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
This commit is contained in:
parent
10e9c5afa7
commit
537e6c4127
@ -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):
|
||||
"""Остаток бюджета (для отображения в админке)."""
|
||||
|
||||
1
backend/procurement/management/__init__.py
Normal file
1
backend/procurement/management/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Пакет management
|
||||
1
backend/procurement/management/commands/__init__.py
Normal file
1
backend/procurement/management/commands/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Пакет commands
|
||||
@ -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('')
|
||||
Loading…
x
Reference in New Issue
Block a user