803 lines
27 KiB
Python
803 lines
27 KiB
Python
"""
|
||
Сериализаторы для модуля закупок.
|
||
"""
|
||
|
||
from drf_spectacular.utils import extend_schema_field
|
||
from rest_framework import serializers
|
||
|
||
from procurement.constants import get_default_approval_step_name
|
||
from procurement.services import ProcurementApprovalResolver
|
||
from procurement.models import (
|
||
Approval,
|
||
Budget,
|
||
Equipment,
|
||
EquipmentCategory,
|
||
MaintenanceRecord,
|
||
ProcurementItem,
|
||
ProcurementRequest,
|
||
Supplier,
|
||
)
|
||
|
||
|
||
class ProcurementItemSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для позиций заявки."""
|
||
|
||
total_price = serializers.DecimalField(
|
||
max_digits=12, decimal_places=2, read_only=True
|
||
)
|
||
|
||
class Meta:
|
||
model = ProcurementItem
|
||
fields = [
|
||
"id",
|
||
"request",
|
||
"name",
|
||
"description",
|
||
"quantity",
|
||
"unit",
|
||
"estimated_unit_price",
|
||
"total_price",
|
||
"supplier_info",
|
||
"equipment",
|
||
"created_at",
|
||
]
|
||
read_only_fields = ["id", "created_at", "total_price"]
|
||
|
||
|
||
class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для создания позиций (без request)."""
|
||
|
||
class Meta:
|
||
model = ProcurementItem
|
||
fields = [
|
||
"name",
|
||
"description",
|
||
"quantity",
|
||
"unit",
|
||
"estimated_unit_price",
|
||
"supplier_info",
|
||
]
|
||
|
||
|
||
class ApprovalSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для согласований."""
|
||
|
||
approver_name = serializers.CharField(
|
||
source="approver.get_full_name", read_only=True
|
||
)
|
||
status_display = serializers.CharField(
|
||
source="get_status_display", read_only=True
|
||
)
|
||
step_label = serializers.SerializerMethodField()
|
||
|
||
@extend_schema_field(serializers.CharField())
|
||
def get_step_label(self, obj):
|
||
return obj.step_name or get_default_approval_step_name(obj.priority)
|
||
|
||
class Meta:
|
||
model = Approval
|
||
fields = [
|
||
"id",
|
||
"request",
|
||
"approver",
|
||
"approver_name",
|
||
"priority",
|
||
"step_name",
|
||
"step_label",
|
||
"status",
|
||
"status_display",
|
||
"comment",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_at",
|
||
"updated_at",
|
||
"approver_name",
|
||
"step_name",
|
||
"step_label",
|
||
"status_display",
|
||
]
|
||
|
||
|
||
class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для списка заявок (краткий)."""
|
||
|
||
department_name = serializers.CharField(
|
||
source="department.name", read_only=True
|
||
)
|
||
requestor_name = serializers.CharField(
|
||
source="requestor.get_full_name", read_only=True
|
||
)
|
||
status_display = serializers.CharField(
|
||
source="get_status_display", read_only=True
|
||
)
|
||
urgency_display = serializers.CharField(
|
||
source="get_urgency_display", read_only=True
|
||
)
|
||
items_count = serializers.IntegerField(read_only=True)
|
||
total_cost = serializers.DecimalField(
|
||
max_digits=12, decimal_places=2, read_only=True
|
||
)
|
||
executor_name = serializers.CharField(
|
||
source="executor.get_full_name", read_only=True, allow_null=True
|
||
)
|
||
comments_count = serializers.IntegerField(read_only=True, default=0)
|
||
can_current_user_approve = serializers.SerializerMethodField()
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_can_current_user_approve(self, obj):
|
||
request = self.context.get("request")
|
||
user = getattr(request, "user", None)
|
||
return ProcurementApprovalResolver.user_can_approve(user, obj)
|
||
|
||
class Meta:
|
||
model = ProcurementRequest
|
||
fields = [
|
||
"id",
|
||
"title",
|
||
"department",
|
||
"department_name",
|
||
"requestor",
|
||
"requestor_name",
|
||
"executor",
|
||
"executor_name",
|
||
"status",
|
||
"status_display",
|
||
"urgency",
|
||
"urgency_display",
|
||
"total_cost",
|
||
"items_count",
|
||
"comments_count",
|
||
"can_current_user_approve",
|
||
"created_at",
|
||
"submitted_at",
|
||
"started_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_at",
|
||
"submitted_at",
|
||
"started_at",
|
||
"department_name",
|
||
"requestor_name",
|
||
"executor_name",
|
||
"status_display",
|
||
"urgency_display",
|
||
"items_count",
|
||
"total_cost",
|
||
"comments_count",
|
||
"can_current_user_approve",
|
||
]
|
||
|
||
def update(self, instance, validated_data):
|
||
"""
|
||
При обновлении заявки запрещаем изменение requestor и department.
|
||
Эти поля игнорируются при обновлении.
|
||
"""
|
||
# Игнорируем попытки изменить requestor
|
||
validated_data.pop("requestor", None)
|
||
# Игнорируем попытки изменить department
|
||
validated_data.pop("department", None)
|
||
return super().update(instance, validated_data)
|
||
|
||
|
||
class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для детальной информации о заявке."""
|
||
|
||
department_name = serializers.CharField(
|
||
source="department.name", read_only=True
|
||
)
|
||
requestor_name = serializers.CharField(
|
||
source="requestor.get_full_name", read_only=True
|
||
)
|
||
requestor_email = serializers.CharField(
|
||
source="requestor.email", read_only=True
|
||
)
|
||
executor_name = serializers.CharField(
|
||
source="executor.get_full_name", read_only=True, allow_null=True
|
||
)
|
||
status_display = serializers.CharField(
|
||
source="get_status_display", read_only=True
|
||
)
|
||
urgency_display = serializers.CharField(
|
||
source="get_urgency_display", read_only=True
|
||
)
|
||
total_cost = serializers.DecimalField(
|
||
max_digits=12, decimal_places=2, read_only=True
|
||
)
|
||
items = ProcurementItemSerializer(many=True, read_only=True)
|
||
approvals = ApprovalSerializer(many=True, read_only=True)
|
||
required_approval_priorities = serializers.ListField(
|
||
source="get_required_approval_priorities", read_only=True
|
||
)
|
||
is_editable = serializers.BooleanField(read_only=True)
|
||
comments_count = serializers.IntegerField(read_only=True, default=0)
|
||
can_current_user_approve = serializers.SerializerMethodField()
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_can_current_user_approve(self, obj):
|
||
request = self.context.get("request")
|
||
user = getattr(request, "user", None)
|
||
return ProcurementApprovalResolver.user_can_approve(user, obj)
|
||
|
||
class Meta:
|
||
model = ProcurementRequest
|
||
fields = [
|
||
"id",
|
||
"title",
|
||
"description",
|
||
"department",
|
||
"department_name",
|
||
"requestor",
|
||
"requestor_name",
|
||
"requestor_email",
|
||
"executor",
|
||
"executor_name",
|
||
"status",
|
||
"status_display",
|
||
"urgency",
|
||
"urgency_display",
|
||
"total_cost",
|
||
"actual_cost",
|
||
"items",
|
||
"approvals",
|
||
"required_approval_priorities",
|
||
"is_editable",
|
||
"comments_count",
|
||
"can_current_user_approve",
|
||
"created_at",
|
||
"updated_at",
|
||
"submitted_at",
|
||
"started_at",
|
||
"completed_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_at",
|
||
"updated_at",
|
||
"submitted_at",
|
||
"started_at",
|
||
"completed_at",
|
||
"is_editable",
|
||
"total_cost",
|
||
"executor_name",
|
||
"comments_count",
|
||
"can_current_user_approve",
|
||
]
|
||
|
||
|
||
class ProcurementRequestCreateSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для создания заявки с позициями."""
|
||
|
||
items = ProcurementItemCreateSerializer(many=True, required=False)
|
||
|
||
class Meta:
|
||
model = ProcurementRequest
|
||
fields = [
|
||
"id",
|
||
"title",
|
||
"description",
|
||
"department",
|
||
"requestor",
|
||
"urgency",
|
||
"items",
|
||
"status",
|
||
"created_at",
|
||
]
|
||
read_only_fields = ["id", "requestor", "status", "created_at"]
|
||
|
||
def create(self, validated_data):
|
||
"""Создать заявку с позициями."""
|
||
items_data = validated_data.pop("items", [])
|
||
request = self.context["request"]
|
||
|
||
# Создаем заявку
|
||
procurement_request = ProcurementRequest.objects.create(
|
||
requestor=request.user, **validated_data
|
||
)
|
||
|
||
# Создаем позиции (если они переданы)
|
||
for item_data in items_data:
|
||
ProcurementItem.objects.create(
|
||
request=procurement_request, **item_data
|
||
)
|
||
|
||
return procurement_request
|
||
|
||
|
||
class EquipmentCategorySerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для категорий оборудования."""
|
||
|
||
full_path = serializers.CharField(read_only=True)
|
||
children_count = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = EquipmentCategory
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
"parent",
|
||
"description",
|
||
"icon",
|
||
"full_path",
|
||
"children_count",
|
||
"created_at",
|
||
]
|
||
read_only_fields = ["id", "created_at", "full_path"]
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_children_count(self, obj):
|
||
"""Количество подкатегорий."""
|
||
return obj.children.count()
|
||
|
||
|
||
class BudgetSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор бюджета отдела."""
|
||
|
||
department_name = serializers.CharField(
|
||
source="department.name", read_only=True
|
||
)
|
||
remaining_amount = serializers.DecimalField(
|
||
max_digits=14, decimal_places=2, read_only=True
|
||
)
|
||
reserved_amount = serializers.DecimalField(
|
||
max_digits=14, decimal_places=2, read_only=True
|
||
)
|
||
available_amount = serializers.DecimalField(
|
||
max_digits=14, decimal_places=2, read_only=True
|
||
)
|
||
utilization_percentage = serializers.DecimalField(
|
||
max_digits=5, decimal_places=2, read_only=True
|
||
)
|
||
|
||
class Meta:
|
||
model = Budget
|
||
fields = [
|
||
"id",
|
||
"department",
|
||
"department_name",
|
||
"year",
|
||
"quarter",
|
||
"allocated_amount",
|
||
"spent_amount",
|
||
"remaining_amount",
|
||
"reserved_amount",
|
||
"available_amount",
|
||
"utilization_percentage",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = fields
|
||
|
||
|
||
class EquipmentListSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для списка оборудования."""
|
||
|
||
category_name = serializers.CharField(
|
||
source="category.name", read_only=True
|
||
)
|
||
category_icon = serializers.CharField(
|
||
source="category.icon", read_only=True, default="bi-box-seam"
|
||
)
|
||
department_name = serializers.CharField(
|
||
source="department.name", read_only=True
|
||
)
|
||
responsible_name = serializers.CharField(
|
||
source="responsible_person.get_full_name", read_only=True
|
||
)
|
||
status_display = serializers.CharField(
|
||
source="get_status_display", read_only=True
|
||
)
|
||
is_under_warranty = serializers.BooleanField(read_only=True)
|
||
comments_count = serializers.IntegerField(read_only=True, default=0)
|
||
|
||
class Meta:
|
||
model = Equipment
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
"inventory_number",
|
||
"serial_number",
|
||
"category",
|
||
"category_name",
|
||
"category_icon",
|
||
"status",
|
||
"status_display",
|
||
"department",
|
||
"department_name",
|
||
"responsible_person",
|
||
"responsible_name",
|
||
"location",
|
||
"purchase_date",
|
||
"purchase_cost",
|
||
"notes",
|
||
"is_under_warranty",
|
||
"comments_count",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"inventory_number",
|
||
"category_name",
|
||
"category_icon",
|
||
"department_name",
|
||
"responsible_name",
|
||
"status_display",
|
||
"is_under_warranty",
|
||
"comments_count",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
|
||
def create(self, validated_data):
|
||
"""Автоматическая генерация инвентарного номера при создании."""
|
||
import re
|
||
from datetime import datetime
|
||
|
||
year = datetime.now().year
|
||
prefix = f"INV-{year}-"
|
||
|
||
# Находим максимальный номер с текущим префиксом
|
||
last_equipment = (
|
||
Equipment.objects.filter(inventory_number__startswith=prefix)
|
||
.order_by("-inventory_number")
|
||
.first()
|
||
)
|
||
|
||
if last_equipment:
|
||
match = re.search(r"(\d+)$", last_equipment.inventory_number)
|
||
if match:
|
||
next_num = int(match.group(1)) + 1
|
||
else:
|
||
next_num = 1
|
||
else:
|
||
next_num = 1
|
||
|
||
validated_data["inventory_number"] = f"{prefix}{next_num:04d}"
|
||
return super().create(validated_data)
|
||
|
||
def _get_user_permission_level(self, user):
|
||
"""Определяет уровень прав пользователя.
|
||
|
||
Возвращает:
|
||
'full' - админ/модельные права
|
||
(свободный выбор отдела/ответственного)
|
||
'dept_head' - начальник отдела
|
||
(свой отдел, выбор ответственного)
|
||
'scoped' - скоуп-право (свой отдел, ответственный = начальник)
|
||
None - нет прав на создание
|
||
"""
|
||
from api.v1.permissions import has_dept_perm
|
||
from employees.constants import DeptPerm
|
||
from employees.models import Department, EmployeeDepartment
|
||
|
||
if not user or not user.is_authenticated:
|
||
return None
|
||
|
||
# Админ или модельные права = полный доступ
|
||
if (
|
||
user.is_staff
|
||
or user.is_superuser
|
||
or user.has_perm("procurement.add_equipment")
|
||
):
|
||
return "full"
|
||
|
||
# Проверяем, является ли начальником какого-то отдела
|
||
headed_depts = Department.objects.filter(head_id=user.id)
|
||
if headed_depts.exists():
|
||
return "dept_head"
|
||
|
||
# Проверяем скоуп-право в активных отделах
|
||
user_dept_links = EmployeeDepartment.objects.filter(
|
||
employee_id=user.id, is_active=True
|
||
).select_related("department", "role")
|
||
|
||
for link in user_dept_links:
|
||
if has_dept_perm(
|
||
user, link.department_id, DeptPerm.MANAGE_EQUIPMENT
|
||
):
|
||
return "scoped"
|
||
|
||
return None
|
||
|
||
def _get_user_allowed_departments(self, user, perm_level):
|
||
"""Возвращает отделы, где пользователь может создать оборудование."""
|
||
from employees.models import Department, EmployeeDepartment
|
||
|
||
if perm_level == "full":
|
||
return list(Department.objects.all())
|
||
|
||
if perm_level == "dept_head":
|
||
return list(Department.objects.filter(head_id=user.id))
|
||
|
||
if perm_level == "scoped":
|
||
from api.v1.permissions import has_dept_perm
|
||
from employees.constants import DeptPerm
|
||
|
||
result = []
|
||
user_dept_links = EmployeeDepartment.objects.filter(
|
||
employee_id=user.id, is_active=True
|
||
).select_related("department")
|
||
|
||
for link in user_dept_links:
|
||
if has_dept_perm(
|
||
user, link.department_id, DeptPerm.MANAGE_EQUIPMENT
|
||
):
|
||
result.append(link.department)
|
||
return result
|
||
|
||
return []
|
||
|
||
def validate(self, attrs):
|
||
"""Проверка и автозаполнение полей в зависимости от прав.
|
||
|
||
Уровни прав:
|
||
- full (админ/модельные права): свободный выбор отдела
|
||
и ответственного
|
||
- dept_head (начальник): только свой отдел,
|
||
выбор ответственного из отдела
|
||
- scoped (скоуп-право): только свой отдел, ответственный = начальник
|
||
"""
|
||
request = self.context.get("request")
|
||
user = request.user if request else None
|
||
|
||
# При update (есть instance) используем облегчённую валидацию
|
||
if self.instance is not None:
|
||
return self._validate_update(attrs, user)
|
||
|
||
# Создание — полная проверка прав
|
||
perm_level = self._get_user_permission_level(user)
|
||
|
||
# Проверяем отдел
|
||
department = attrs.get("department")
|
||
|
||
if perm_level == "full":
|
||
# Полный доступ — можно указать любой отдел
|
||
pass
|
||
|
||
elif perm_level == "dept_head":
|
||
# Начальник — только свои отделы
|
||
allowed_depts = self._get_user_allowed_departments(user, perm_level)
|
||
allowed_ids = [d.id for d in allowed_depts]
|
||
|
||
if department and department.id not in allowed_ids:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"department": "Вы можете создавать оборудование "
|
||
"только в своём отделе."
|
||
}
|
||
)
|
||
|
||
# Если отдел не указан — берём первый
|
||
if not department and allowed_depts:
|
||
attrs["department"] = allowed_depts[0]
|
||
department = allowed_depts[0]
|
||
|
||
# Ответственный по умолчанию — сам начальник
|
||
resp = attrs.get("responsible_person")
|
||
if resp is None:
|
||
attrs["responsible_person"] = user
|
||
|
||
elif perm_level == "scoped":
|
||
# Скоуп-право — только свой отдел, ответственный = начальник
|
||
allowed_depts = self._get_user_allowed_departments(user, perm_level)
|
||
allowed_ids = [d.id for d in allowed_depts]
|
||
|
||
if department and department.id not in allowed_ids:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"department": "Вы можете создавать оборудование "
|
||
"только в своём отделе."
|
||
}
|
||
)
|
||
|
||
# Если отдел не указан — берём первый
|
||
if not department and allowed_depts:
|
||
attrs["department"] = allowed_depts[0]
|
||
department = allowed_depts[0]
|
||
|
||
# Ответственный ВСЕГДА = начальник отдела (без выбора)
|
||
if department and department.head:
|
||
attrs["responsible_person"] = department.head
|
||
else:
|
||
attrs["responsible_person"] = None
|
||
|
||
else:
|
||
# Нет прав — ошибка
|
||
raise serializers.ValidationError(
|
||
{"non_field_errors": "У вас нет прав на создание оборудования."}
|
||
)
|
||
|
||
# Валидация ответственного
|
||
return self._validate_responsible_in_department(attrs)
|
||
|
||
def _validate_update(self, attrs, user):
|
||
"""Валидация при обновлении (облегчённая)."""
|
||
# При update не проверяем права создания,
|
||
# только валидацию ответственного если он меняется
|
||
department = attrs.get("department", self.instance.department)
|
||
responsible = attrs.get("responsible_person")
|
||
|
||
# Если меняется ответственный — проверяем принадлежность к отделу
|
||
if responsible is not None:
|
||
attrs["department"] = department # Для валидации
|
||
attrs["responsible_person"] = responsible
|
||
return self._validate_responsible_in_department(attrs)
|
||
|
||
return attrs
|
||
|
||
def _validate_responsible_in_department(self, attrs):
|
||
"""Проверяет, что ответственный состоит в отделе оборудования."""
|
||
from employees.models import EmployeeDepartment
|
||
|
||
department = attrs.get("department")
|
||
responsible = attrs.get("responsible_person")
|
||
|
||
if department and responsible:
|
||
# Проверяем, что ответственный в этом отделе
|
||
is_in_dept = (
|
||
# Он начальник отдела
|
||
department.head_id == responsible.id
|
||
or
|
||
# Или состоит в отделе
|
||
EmployeeDepartment.objects.filter(
|
||
employee_id=responsible.id,
|
||
department_id=department.id,
|
||
is_active=True,
|
||
).exists()
|
||
)
|
||
|
||
if not is_in_dept:
|
||
raise serializers.ValidationError(
|
||
{
|
||
"responsible_person": (
|
||
"Ответственный должен состоять в том же отделе, "
|
||
"что и оборудование."
|
||
)
|
||
}
|
||
)
|
||
|
||
return attrs
|
||
|
||
|
||
class EquipmentDetailSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для детальной информации об оборудовании."""
|
||
|
||
category_name = serializers.CharField(
|
||
source="category.name", read_only=True
|
||
)
|
||
department_name = serializers.CharField(
|
||
source="department.name", read_only=True
|
||
)
|
||
responsible_name = serializers.CharField(
|
||
source="responsible_person.get_full_name", read_only=True
|
||
)
|
||
status_display = serializers.CharField(
|
||
source="get_status_display", read_only=True
|
||
)
|
||
is_under_warranty = serializers.BooleanField(read_only=True)
|
||
maintenance_count = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Equipment
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
"inventory_number",
|
||
"serial_number",
|
||
"category",
|
||
"category_name",
|
||
"status",
|
||
"status_display",
|
||
"department",
|
||
"department_name",
|
||
"responsible_person",
|
||
"responsible_name",
|
||
"location",
|
||
"purchase_date",
|
||
"warranty_until",
|
||
"is_under_warranty",
|
||
"purchase_cost",
|
||
"notes",
|
||
"maintenance_count",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_at",
|
||
"updated_at",
|
||
"is_under_warranty",
|
||
"maintenance_count",
|
||
]
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_maintenance_count(self, obj):
|
||
"""Количество записей обслуживания."""
|
||
return obj.maintenance_history.count()
|
||
|
||
|
||
class MaintenanceRecordSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для записей обслуживания."""
|
||
|
||
equipment_name = serializers.CharField(
|
||
source="equipment.name", read_only=True
|
||
)
|
||
equipment_inventory = serializers.CharField(
|
||
source="equipment.inventory_number", read_only=True
|
||
)
|
||
performed_by_name = serializers.CharField(
|
||
source="performed_by.get_full_name", read_only=True
|
||
)
|
||
type_display = serializers.CharField(
|
||
source="get_type_display", read_only=True
|
||
)
|
||
|
||
class Meta:
|
||
model = MaintenanceRecord
|
||
fields = [
|
||
"id",
|
||
"equipment",
|
||
"equipment_name",
|
||
"equipment_inventory",
|
||
"date",
|
||
"type",
|
||
"type_display",
|
||
"description",
|
||
"cost",
|
||
"performed_by",
|
||
"performed_by_name",
|
||
"next_maintenance_date",
|
||
"created_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_at",
|
||
"equipment_name",
|
||
"equipment_inventory",
|
||
"performed_by_name",
|
||
"type_display",
|
||
]
|
||
|
||
|
||
class SupplierSerializer(serializers.ModelSerializer):
|
||
"""Сериализатор для поставщиков."""
|
||
|
||
class Meta:
|
||
model = Supplier
|
||
fields = [
|
||
"id",
|
||
"name",
|
||
"contact_person",
|
||
"phone",
|
||
"email",
|
||
"address",
|
||
"website",
|
||
"inn",
|
||
"rating",
|
||
"is_active",
|
||
"notes",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = ["id", "created_at", "updated_at"]
|
||
|
||
|
||
class ProcurementOverviewStatsSerializer(serializers.Serializer):
|
||
total_requests = serializers.IntegerField()
|
||
pending_requests = serializers.IntegerField()
|
||
approved_this_month = serializers.IntegerField()
|
||
completed_this_month = serializers.IntegerField()
|
||
total_spent_this_year = serializers.CharField()
|
||
by_status = serializers.DictField(child=serializers.IntegerField())
|
||
by_urgency = serializers.DictField(child=serializers.IntegerField())
|
||
|
||
|
||
class ProcurementDepartmentStatsSerializer(serializers.Serializer):
|
||
department = serializers.DictField()
|
||
total_requests = serializers.IntegerField()
|
||
total_spent = serializers.CharField()
|