- Убрать remaining_amount и utilization_percentage из list_display (это @property) - Добавить методы get_remaining_amount и get_utilization_percentage - Защитить от None в allocated_amount - Исправить prefetch_related в views_frontend
247 lines
7.8 KiB
Markdown
247 lines
7.8 KiB
Markdown
# Система бюджетов отделов в модуле закупок
|
||
|
||
## Где выставляется бюджет?
|
||
|
||
### 1️⃣ **Django Admin (Основное место)**
|
||
|
||
```
|
||
http://localhost:9000/admin/procurement/budget/
|
||
```
|
||
|
||
**Процесс:**
|
||
1. Перейти в Django Admin → Procurement → Budget
|
||
2. Нажать "Add budget"
|
||
3. Заполнить форму:
|
||
- **Department** — выбрать отдел (IT, HR, финансы и т.д.)
|
||
- **Year** — год (2025, 2026)
|
||
- **Quarter** — квартал (1, 2, 3, 4)
|
||
- **Allocated amount** — сумма выделенного бюджета в рублях
|
||
- **Spent amount** — (заполняется автоматически, если оставить 0)
|
||
|
||
### 2️⃣ **REST API (для приложений)**
|
||
|
||
**Создание бюджета:**
|
||
```bash
|
||
POST /api/procurement/budgets/
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"department": 5, # ID отдела
|
||
"year": 2025,
|
||
"quarter": 4,
|
||
"allocated_amount": "500000.00"
|
||
}
|
||
```
|
||
|
||
**Редактирование:**
|
||
```bash
|
||
PATCH /api/procurement/budgets/1/
|
||
{
|
||
"allocated_amount": "600000.00"
|
||
}
|
||
```
|
||
|
||
**Получение текущего квартала:**
|
||
```bash
|
||
GET /api/procurement/budgets/current_quarter/
|
||
```
|
||
|
||
**Получить бюджет своего отдела:**
|
||
```bash
|
||
GET /api/procurement/budgets/my-department/
|
||
```
|
||
|
||
## Структура модели Budget
|
||
|
||
```python
|
||
class Budget(models.Model):
|
||
department # ForeignKey на Department
|
||
year # 2025, 2026, ...
|
||
quarter # 1, 2, 3, 4
|
||
allocated_amount # Выделено денег (основное число!)
|
||
spent_amount # Потрачено (меняется при одобрении заявок)
|
||
created_at # Когда создан
|
||
updated_at # Когда обновлён
|
||
```
|
||
|
||
**Ключевое свойство:** `(department, year, quarter)` — уникальный набор!
|
||
```
|
||
Нельзя создать два бюджета для одного отдела на один период!
|
||
```
|
||
|
||
## Примеры бюджетов
|
||
|
||
### Компания с 3 отделами на Q4 2025:
|
||
|
||
```
|
||
┌────────────────┬──────┬─────────┬──────────────┐
|
||
│ Отдел │ 2025 │ Q4 │ Бюджет │
|
||
├────────────────┼──────┼─────────┼──────────────┤
|
||
│ IT │ 2025 │ 4 │ 1,000,000₽ │
|
||
│ HR │ 2025 │ 4 │ 300,000₽ │
|
||
│ Финансы │ 2025 │ 4 │ 500,000₽ │
|
||
│ Маркетинг │ 2025 │ 4 │ 250,000₽ │
|
||
└────────────────┴──────┴─────────┴──────────────┘
|
||
```
|
||
|
||
## Как работает проверка бюджета?
|
||
|
||
Когда пользователь отправляет заявку на согласование:
|
||
|
||
```python
|
||
# 1. Система получает текущий квартал
|
||
now = timezone.now()
|
||
quarter = (now.month - 1) // 3 + 1
|
||
|
||
# 2. Ищет бюджет отдела на этот период
|
||
budget = Budget.objects.get(
|
||
department=procurement_request.department,
|
||
year=now.year,
|
||
quarter=quarter
|
||
)
|
||
|
||
# 3. Проверяет остаток
|
||
remaining = budget.remaining_amount # allocated - spent
|
||
if remaining >= procurement_request.estimated_cost:
|
||
# ✅ Можно отправить на согласование
|
||
else:
|
||
# ❌ Недостаточно бюджета
|
||
return error
|
||
```
|
||
|
||
## Ключевые свойства Budget
|
||
|
||
```python
|
||
# Остаток бюджета
|
||
remaining_amount = allocated_amount - spent_amount
|
||
|
||
# Зарезервировано (pending заявки)
|
||
reserved_amount = sum(pending_requests.estimated_cost)
|
||
|
||
# Доступно с учётом резерва
|
||
available_amount = remaining_amount - reserved_amount
|
||
|
||
# Процент использования
|
||
utilization_percentage = (spent_amount / allocated_amount) * 100
|
||
|
||
# Можно ли потратить X?
|
||
can_spend(amount: Decimal) → bool
|
||
```
|
||
|
||
## Пример в коде
|
||
|
||
```python
|
||
# Проверка в views.py при отправке заявки
|
||
available, remaining = procurement_request.check_budget_available()
|
||
|
||
if not available:
|
||
return Response({
|
||
'error': 'Недостаточно бюджета',
|
||
'required': float(procurement_request.estimated_cost),
|
||
'remaining': float(remaining),
|
||
}, status=400)
|
||
```
|
||
|
||
## Алерты при низком бюджете
|
||
|
||
Система автоматически отправляет уведомления руководителю отдела:
|
||
|
||
```python
|
||
# budget_low (остаток < 20%)
|
||
"Бюджет отдела снижается. Остаток: 45,000₽ (18%)"
|
||
|
||
# budget_critical (остаток < 10%)
|
||
"⚠️ Критически низкий бюджет! Остаток: 8,000₽ (3%)"
|
||
```
|
||
|
||
**Когда:** При одобрении заявки если бюджет падает ниже порогов
|
||
|
||
## Как менять бюджет?
|
||
|
||
### ✅ Правильно (сумма выделенного):
|
||
```
|
||
Отдел IT на Q4 2025: 1,000,000₽
|
||
Потом изменили: 1,500,000₽ (увеличили бюджет)
|
||
```
|
||
|
||
### ❌ Неправильно (not spent_amount):
|
||
```
|
||
❌ Не меняйте spent_amount вручную - это меняется автоматически
|
||
```
|
||
|
||
## Права доступа
|
||
|
||
| Action | Кто может |
|
||
|--------|-----------|
|
||
| Создать бюджет | `CanManageBudget` (админ) |
|
||
| Редактировать | `CanManageBudget` (админ) |
|
||
| Удалить | `CanManageBudget` (админ) |
|
||
| Просмотреть свой | Руководитель отдела |
|
||
| Просмотреть все | Суперпользователь/Админ |
|
||
|
||
## Типичный цикл
|
||
|
||
```
|
||
Q1 2025: Выделили IT-отделу 1,000,000₽
|
||
↓
|
||
Заявка #1: 50,000₽ → одобрена
|
||
Заявка #2: 100,000₽ → одобрена
|
||
Zaявка #3: 30,000₽ → одобрена
|
||
↓
|
||
Потрачено: 180,000₽
|
||
Остаток: 820,000₽
|
||
Использовано: 18%
|
||
↓
|
||
Q2 2025: Новый квартал, новый бюджет
|
||
```
|
||
|
||
## SQL для статистики
|
||
|
||
```sql
|
||
-- Сколько потрачено каждый отдел на Q4 2025?
|
||
SELECT
|
||
d.name,
|
||
b.allocated_amount,
|
||
b.spent_amount,
|
||
(b.allocated_amount - b.spent_amount) as remaining
|
||
FROM procurement_budget b
|
||
JOIN employees_department d ON b.department_id = d.id
|
||
WHERE b.year = 2025 AND b.quarter = 4;
|
||
|
||
-- Самый загруженный отдел по бюджету
|
||
SELECT
|
||
d.name,
|
||
b.allocated_amount,
|
||
(b.spent_amount::float / b.allocated_amount * 100) as utilization
|
||
FROM procurement_budget b
|
||
JOIN employees_department d ON b.department_id = d.id
|
||
WHERE b.year = 2025
|
||
ORDER BY utilization DESC;
|
||
```
|
||
|
||
## Интеграция с заявками
|
||
|
||
```
|
||
ProcurementRequest (заявка)
|
||
├─ department_id
|
||
├─ estimated_cost
|
||
└─ check_budget_available()
|
||
↓ ищет
|
||
Budget (бюджет)
|
||
├─ department
|
||
├─ allocated_amount
|
||
└─ spent_amount
|
||
```
|
||
|
||
## Автоматические обновления spent_amount
|
||
|
||
```python
|
||
# На данный момент spent_amount меняется вручную в:
|
||
# - Django Admin
|
||
# - API PATCH запрос
|
||
|
||
# В будущем можно автоматизировать при:
|
||
# - Завершении заявки (status=COMPLETED)
|
||
# - Поступлении счёта (через другую модель Invoice)
|
||
```
|