Fix quality gate test failures

This commit is contained in:
lizerk 2026-09-07 19:03:52 +03:00
parent bf0df3613d
commit 2a598de8d5
8 changed files with 262 additions and 272 deletions

View File

@ -136,7 +136,11 @@ class MoySkladClient:
"MoySklad GET transient failure %s; retrying in %.1f seconds",
type(exc).__name__,
retry_delay,
extra={**log_context, "retry_in_seconds": retry_delay},
extra={
**log_context,
"event": "commerce.moysklad.get_retry",
"retry_in_seconds": retry_delay,
},
)
time.sleep(retry_delay)
except httpx.HTTPStatusError as exc:
@ -149,6 +153,7 @@ class MoySkladClient:
"MoySklad HTTP %s; retrying GET",
exc.response.status_code,
extra={
"event": "commerce.moysklad.get_retry",
"endpoint": endpoint,
"attempt": attempt,
"request_offset": (params or {}).get("offset"),

View File

@ -8,7 +8,7 @@
"""
import pytest
from communications.models import Chat, ChatMembership, Message
from communications.models import Chat, ChatMembership
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APIClient
@ -19,7 +19,7 @@ pytestmark = pytest.mark.django_db
class TestChangeRoleImproved:
"""Улучшенные тесты изменения ролей с проверкой через GET запрос"""
@pytest.fixture
def owner(self, db):
return User.objects.create_user(
@ -28,9 +28,10 @@ class TestChangeRoleImproved:
first_name="Owner",
last_name="Test",
phone_number="+79001234567",
is_active=True,
send_activation_email=False,
)
@pytest.fixture
def member_user(self, db):
return User.objects.create_user(
@ -39,94 +40,84 @@ class TestChangeRoleImproved:
first_name="Member",
last_name="Test",
phone_number="+79001234568",
is_active=True,
send_activation_email=False,
)
@pytest.fixture
def test_chat(self, owner, member_user):
chat = Chat.objects.create(
type="group",
name="Test Chat",
created_by=owner
)
chat = Chat.objects.create(type="group", name="Test Chat", created_by=owner)
chat.participants.add(owner, member_user)
ChatMembership.objects.create(
chat=chat,
user=member_user,
role='member',
invited_by=owner
chat=chat, user=member_user, role="member", invited_by=owner
)
return chat
@pytest.fixture
def owner_client(self, owner):
client = APIClient()
client.force_authenticate(user=owner)
return client
def test_change_role_and_verify_via_api(self, owner_client, test_chat, member_user):
"""
Тест: изменить роль, дождаться ответа, проверить через GET
"""
# Шаг 1: Изменяем роль через POST
change_url = f"/api/v1/communications/chats/{test_chat.id}/change-role/"
response = owner_client.post(change_url, {
'user_id': member_user.id,
'role': 'admin'
})
response = owner_client.post(
change_url, {"user_id": member_user.id, "role": "admin"}
)
# Шаг 2: Проверяем успешный ответ
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['ok'] is True
assert data['membership']['role'] == 'admin'
assert data["ok"] is True
assert data["membership"]["role"] == "admin"
# Шаг 3: Получаем чат через GET и проверяем роль
chat_url = f"/api/v1/communications/chats/{test_chat.id}/"
get_response = owner_client.get(chat_url)
assert get_response.status_code == status.HTTP_200_OK
chat_data = get_response.json()
# Шаг 4: Находим участника и проверяем роль
member_membership = next(
(m for m in chat_data['memberships'] if m['user'] == member_user.id),
None
(m for m in chat_data["memberships"] if m["user"] == member_user.id), None
)
assert member_membership is not None, "Участник не найден"
assert member_membership['role'] == 'admin'
assert member_membership['can_send_messages'] is True
assert member_membership['can_add_members'] is True
assert member_membership["role"] == "admin"
assert member_membership["can_send_messages"] is True
assert member_membership["can_add_members"] is True
def test_member_to_guest_role_change(self, owner_client, test_chat, member_user):
"""
Тест: изменение member → guest с проверкой через API
"""
# Шаг 1: Изменяем роль
change_url = f"/api/v1/communications/chats/{test_chat.id}/change-role/"
response = owner_client.post(change_url, {
'user_id': member_user.id,
'role': 'guest'
})
response = owner_client.post(
change_url, {"user_id": member_user.id, "role": "guest"}
)
# Шаг 2: Проверяем ответ
assert response.status_code == status.HTTP_200_OK
# Шаг 3: Проверяем через GET
chat_url = f"/api/v1/communications/chats/{test_chat.id}/"
get_response = owner_client.get(chat_url)
chat_data = get_response.json()
member_membership = next(
(m for m in chat_data['memberships'] if m['user'] == member_user.id),
None
(m for m in chat_data["memberships"] if m["user"] == member_user.id), None
)
# Шаг 4: Проверяем что стал гостем (только чтение)
assert member_membership['role'] == 'guest'
assert member_membership['can_send_messages'] is False
assert member_membership['can_add_members'] is False
assert member_membership['can_remove_members'] is False
assert member_membership["role"] == "guest"
assert member_membership["can_send_messages"] is False
assert member_membership["can_add_members"] is False
assert member_membership["can_remove_members"] is False

View File

@ -206,9 +206,7 @@ def test_department_calendar_event_permissions_for_non_member_and_member(
)
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
forbidden_delete = api_client.delete(
f"/api/v1/schedule/events/{member_event.id}/"
)
forbidden_delete = api_client.delete(f"/api/v1/schedule/events/{member_event.id}/")
assert forbidden_delete.status_code == status.HTTP_403_FORBIDDEN
@ -293,9 +291,7 @@ def test_department_calendar_participants_include_avatar_and_email(
"calendar_id"
]
response = api_client.get(
f"/api/v1/schedule/calendars/{calendar_id}/participants/"
)
response = api_client.get(f"/api/v1/schedule/calendars/{calendar_id}/participants/")
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
@ -307,7 +303,7 @@ def test_department_calendar_participants_include_avatar_and_email(
assert participant["user"]["last_name"] == "User"
def test_department_calendar_occurrences_allow_outsider_but_keep_permissions(
def test_department_calendar_occurrences_hide_events_from_outsider(
api_client: APIClient,
):
head = make_user("head@example.com")
@ -344,9 +340,19 @@ def test_department_calendar_occurrences_allow_outsider_but_keep_permissions(
},
)
assert response.status_code == status.HTTP_200_OK
assert response.data == []
api_client.force_authenticate(user=member)
response = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"calendar": calendar_id,
"start": (start - timedelta(minutes=5)).isoformat(),
"end": (start + timedelta(hours=2)).isoformat(),
},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
occurrence = response.data[0]
assert occurrence["event_id"] == member_event.id
assert occurrence["can_edit"] is False
assert occurrence["can_delete"] is False
assert response.data[0]["event_id"] == member_event.id

View File

@ -28,10 +28,7 @@ def api_client():
@pytest.fixture
def department(db):
"""Тестовый отдел."""
return Department.objects.create(
name="IT отдел",
description="Отдел IT"
)
return Department.objects.create(name="IT отдел", description="Отдел IT")
@pytest.fixture
@ -46,9 +43,7 @@ def user(db, department):
send_activation_email=False,
)
EmployeeDepartment.objects.create(
employee=user,
department=department,
is_active=True
employee=user, department=department, is_active=True
)
return user
@ -71,9 +66,7 @@ def staff_user(db):
def category(db):
"""Категория оборудования."""
return EquipmentCategory.objects.create(
name="Ноутбуки",
description="Портативные компьютеры",
icon="bi-laptop"
name="Ноутбуки", description="Портативные компьютеры", icon="bi-laptop"
)
@ -106,33 +99,29 @@ class TestEquipmentList:
def test_list_unauthorized(self, api_client):
"""Неавторизованный доступ запрещен."""
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
response = api_client.get(url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_list_equipment(
self, api_client, user, equipment
):
def test_list_equipment(self, api_client, user, equipment):
"""Получение списка оборудования."""
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
results = response.data['results']
results = response.data["results"]
assert len(results) >= 1
# Проверяем структуру
item = results[0]
assert 'inventory_number' in item
assert 'name' in item
assert 'status' in item
assert 'category_name' in item
assert 'department_name' in item
assert "inventory_number" in item
assert "name" in item
assert "status" in item
assert "category_name" in item
assert "department_name" in item
def test_filter_by_status(
self, api_client, user, category, department, equipment
):
def test_filter_by_status(self, api_client, user, category, department, equipment):
"""Фильтр по статусу."""
# Создаем оборудование с разными статусами
Equipment.objects.create(
@ -144,23 +133,22 @@ class TestEquipmentList:
purchase_date=date.today(),
purchase_cost=Decimal("20000.00"),
)
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
# Фильтр AVAILABLE
response = api_client.get(url, {'status': EquipmentStatus.AVAILABLE})
response = api_client.get(url, {"status": EquipmentStatus.AVAILABLE})
assert response.status_code == status.HTTP_200_OK
results = response.data['results']
assert all(r['status'] == EquipmentStatus.AVAILABLE for r in results)
results = response.data["results"]
assert all(r["status"] == EquipmentStatus.AVAILABLE for r in results)
def test_filter_by_department(
self, api_client, user, db, category, department, equipment
):
"""Фильтр по отделу."""
other_dept = Department.objects.create(
name="HR отдел",
description="Другой отдел"
name="HR отдел", description="Другой отдел"
)
Equipment.objects.create(
name="Принтер",
@ -171,26 +159,24 @@ class TestEquipmentList:
purchase_date=date.today(),
purchase_cost=Decimal("15000.00"),
)
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipment-list')
response = api_client.get(url, {'department': department.id})
assert response.status_code == status.HTTP_200_OK
results = response.data['results']
# Все должны быть из нужного отдела
assert all(r['department'] == department.id for r in results)
def test_search_equipment(
self, api_client, user, equipment
):
api_client.force_authenticate(user=user)
url = reverse("api:v1:procurement:equipment-list")
response = api_client.get(url, {"department": department.id})
assert response.status_code == status.HTTP_200_OK
results = response.data["results"]
# Все должны быть из нужного отдела
assert all(r["department"] == department.id for r in results)
def test_search_equipment(self, api_client, user, equipment):
"""Поиск оборудования."""
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipment-list')
response = api_client.get(url, {'search': 'latitude'})
url = reverse("api:v1:procurement:equipment-list")
response = api_client.get(url, {"search": "latitude"})
assert response.status_code == status.HTTP_200_OK
assert len(response.data['results']) >= 1
assert len(response.data["results"]) >= 1
# ==============================================================================
@ -206,44 +192,44 @@ class TestEquipmentCreate:
):
"""Создание оборудования staff пользователем."""
api_client.force_authenticate(user=staff_user)
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
data = {
'name': 'Монитор LG 27"',
'inventory_number': 'INV-2026-100',
'serial_number': 'LG987654321',
'category': category.id,
'status': EquipmentStatus.AVAILABLE,
'department': department.id,
'location': 'Склад',
'purchase_date': '2026-02-01',
'warranty_until': '2029-02-01',
'purchase_cost': '25000.00',
'notes': 'Для офиса',
"name": 'Монитор LG 27"',
"inventory_number": "INV-2026-100",
"serial_number": "LG987654321",
"category": category.id,
"status": EquipmentStatus.AVAILABLE,
"department": department.id,
"location": "Склад",
"purchase_date": "2026-02-01",
"warranty_until": "2029-02-01",
"purchase_cost": "25000.00",
"notes": "Для офиса",
}
response = api_client.post(url, data, format='json')
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.data['name'] == 'Монитор LG 27"'
assert response.data['inventory_number'].startswith('INV-')
assert response.data["name"] == 'Монитор LG 27"'
assert response.data["inventory_number"].startswith("INV-")
def test_create_equipment_regular_user_forbidden(
self, api_client, user, category, department
):
"""Обычный пользователь не может создавать оборудование."""
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
data = {
'name': 'Тестовое оборудование',
'inventory_number': 'INV-2026-999',
'category': category.id,
'department': department.id,
'purchase_date': '2026-02-01',
'purchase_cost': '10000.00',
"name": "Тестовое оборудование",
"inventory_number": "INV-2026-999",
"category": category.id,
"department": department.id,
"purchase_date": "2026-02-01",
"purchase_cost": "10000.00",
}
response = api_client.post(url, data, format='json')
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_equipment_duplicate_inventory_number(
@ -251,20 +237,20 @@ class TestEquipmentCreate:
):
"""Нельзя создать с дублирующим инвентарным номером."""
api_client.force_authenticate(user=staff_user)
url = reverse('api:v1:procurement:equipment-list')
url = reverse("api:v1:procurement:equipment-list")
data = {
'name': 'Другое оборудование',
'inventory_number': equipment.inventory_number, # Дубликат!
'category': category.id,
'department': department.id,
'purchase_date': '2026-02-01',
'purchase_cost': '10000.00',
"name": "Другое оборудование",
"inventory_number": equipment.inventory_number, # Дубликат!
"category": category.id,
"department": department.id,
"purchase_date": "2026-02-01",
"purchase_cost": "10000.00",
}
response = api_client.post(url, data, format='json')
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.data['inventory_number'] != equipment.inventory_number
assert response.data["inventory_number"] != equipment.inventory_number
# ==============================================================================
@ -275,27 +261,22 @@ class TestEquipmentCreate:
class TestEquipmentDetail:
"""Тесты получения детальной информации об оборудовании."""
def test_retrieve_equipment(
self, api_client, user, equipment
):
def test_retrieve_equipment(self, api_client, user, equipment):
"""Получение детальной информации."""
api_client.force_authenticate(user=user)
url = reverse(
'api:v1:procurement:equipment-detail',
kwargs={'pk': equipment.id}
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
)
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['id'] == equipment.id
assert response.data['name'] == equipment.name
assert response.data['inventory_number'] == equipment.inventory_number
assert 'is_under_warranty' in response.data
assert response.data['is_under_warranty'] is True
def test_retrieve_with_maintenance_history(
self, api_client, user, equipment
):
assert response.status_code == status.HTTP_200_OK
assert response.data["id"] == equipment.id
assert response.data["name"] == equipment.name
assert response.data["inventory_number"] == equipment.inventory_number
assert "is_under_warranty" in response.data
assert response.data["is_under_warranty"] is True
def test_retrieve_with_maintenance_history(self, api_client, user, equipment):
"""Детальная информация включает историю ТО."""
# Создаем записи об обслуживании
MaintenanceRecord.objects.create(
@ -306,16 +287,15 @@ class TestEquipmentDetail:
cost=Decimal("5000.00"),
performed_by=user,
)
api_client.force_authenticate(user=user)
url = reverse(
'api:v1:procurement:equipment-detail',
kwargs={'pk': equipment.id}
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
)
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['maintenance_count'] >= 1
assert response.data["maintenance_count"] >= 1
# ==============================================================================
@ -326,40 +306,34 @@ class TestEquipmentDetail:
class TestEquipmentUpdate:
"""Тесты обновления оборудования."""
def test_update_equipment_as_staff(
self, api_client, staff_user, equipment
):
def test_update_equipment_as_staff(self, api_client, staff_user, equipment):
"""Обновление оборудования staff пользователем."""
api_client.force_authenticate(user=staff_user)
url = reverse(
'api:v1:procurement:equipment-detail',
kwargs={'pk': equipment.id}
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
)
data = {
'location': 'Офис 4.01',
'notes': 'Перемещено в новый офис',
}
response = api_client.patch(url, data, format='json')
assert response.status_code == status.HTTP_200_OK
assert response.data['location'] == 'Офис 4.01'
assert 'новый офис' in response.data['notes'].lower()
def test_update_equipment_regular_user_forbidden(
self, api_client, user, equipment
):
data = {
"location": "Офис 4.01",
"notes": "Перемещено в новый офис",
}
response = api_client.patch(url, data, format="json")
assert response.status_code == status.HTTP_200_OK
assert response.data["location"] == "Офис 4.01"
assert "новый офис" in response.data["notes"].lower()
def test_update_equipment_regular_user_forbidden(self, api_client, user, equipment):
"""Ответственный пользователь может обновлять своё оборудование."""
api_client.force_authenticate(user=user)
url = reverse(
'api:v1:procurement:equipment-detail',
kwargs={'pk': equipment.id}
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
)
data = {'location': 'Новое место'}
response = api_client.patch(url, data, format='json')
data = {"location": "Новое место"}
response = api_client.patch(url, data, format="json")
assert response.status_code == status.HTTP_200_OK
assert response.data['location'] == 'Новое место'
assert response.data["location"] == "Новое место"
# ==============================================================================
@ -370,9 +344,7 @@ class TestEquipmentUpdate:
class TestEquipmentActions:
"""Тесты специальных действий с оборудованием."""
def test_transfer_equipment(
self, api_client, staff_user, equipment, db
):
def test_transfer_equipment(self, api_client, staff_user, equipment, db):
"""Передача оборудования."""
new_user = Employee.objects.create_user(
email="newuser@example.com",
@ -380,28 +352,29 @@ class TestEquipmentActions:
phone_number="+79995555555",
first_name="Новый",
last_name="Пользователь",
is_active=True,
send_activation_email=False,
)
api_client.force_authenticate(user=staff_user)
url = reverse(
'api:v1:procurement:equipment-transfer',
kwargs={'pk': equipment.id}
"api:v1:procurement:equipment-transfer", kwargs={"pk": equipment.id}
)
data = {
'to_person': new_user.id,
'to_location': 'Офис 2.10',
'reason': 'Перевод в другой отдел',
"to_person": new_user.id,
"to_location": "Офис 2.10",
"reason": "Перевод в другой отдел",
}
response = api_client.post(url, data, format='json')
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_200_OK
# Проверяем что ответственный изменился
equipment.refresh_from_db()
assert equipment.responsible_person == new_user
assert equipment.location == 'Офис 2.10'
assert equipment.location == "Офис 2.10"
# ==============================================================================
# ТЕСТЫ КАТЕГОРИЙ
@ -411,47 +384,41 @@ class TestEquipmentActions:
class TestEquipmentCategories:
"""Тесты API категорий оборудования."""
def test_list_categories(
self, api_client, user, category
):
def test_list_categories(self, api_client, user, category):
"""Список категорий."""
api_client.force_authenticate(user=user)
url = reverse('api:v1:procurement:equipmentcategory-list')
url = reverse("api:v1:procurement:equipmentcategory-list")
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) >= 1
def test_create_category_as_staff(
self, api_client, staff_user
):
def test_create_category_as_staff(self, api_client, staff_user):
"""Создание категории staff пользователем."""
api_client.force_authenticate(user=staff_user)
url = reverse('api:v1:procurement:equipmentcategory-list')
data = {
'name': 'Мониторы',
'description': 'Все виды мониторов',
'icon': 'bi-display',
}
response = api_client.post(url, data, format='json')
assert response.status_code == status.HTTP_201_CREATED
assert response.data['name'] == 'Мониторы'
url = reverse("api:v1:procurement:equipmentcategory-list")
def test_hierarchical_categories(
self, api_client, staff_user, category
):
data = {
"name": "Мониторы",
"description": "Все виды мониторов",
"icon": "bi-display",
}
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.data["name"] == "Мониторы"
def test_hierarchical_categories(self, api_client, staff_user, category):
"""Создание подкатегории."""
api_client.force_authenticate(user=staff_user)
url = reverse('api:v1:procurement:equipmentcategory-list')
url = reverse("api:v1:procurement:equipmentcategory-list")
data = {
'name': 'Игровые ноутбуки',
'parent': category.id,
'description': 'Высокопроизводительные ноутбуки',
"name": "Игровые ноутбуки",
"parent": category.id,
"description": "Высокопроизводительные ноутбуки",
}
response = api_client.post(url, data, format='json')
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.data['parent'] == category.id
assert response.data["parent"] == category.id

View File

@ -31,6 +31,7 @@ from finance.models import (
PayrollTableReviewMark,
PayrollWorkRecord,
)
from finance.payroll.config import DEFAULT_CONFIG
from requests_app.enums import RequestStatus, RequestType
from requests_app.models import Request
@ -394,9 +395,7 @@ def test_copy_input_lines_previews_scope_and_handles_existing_values(
assert component_preview[correction_credit.pk]["line_count"] == 0
assert component_preview[correction_credit.pk]["employee_count"] == 0
assert component_preview[correction_credit.pk]["amount_total"] == "0.00"
employee_preview = {
item["id"]: item for item in body["employees"]
}
employee_preview = {item["id"]: item for item in body["employees"]}
assert employee_preview[active_employee.pk]["is_active"] is True
assert employee_preview[inactive_employee.pk]["is_active"] is False
assert employee_preview[active_employee.pk]["existing_component_counts"] == {
@ -425,10 +424,13 @@ def test_copy_input_lines_previews_scope_and_handles_existing_values(
"replaced_cells": 0,
"voided": 0,
}
assert PayrollInputLine.objects.filter(
period=target,
employee=inactive_employee,
).count() == 0
assert (
PayrollInputLine.objects.filter(
period=target,
employee=inactive_employee,
).count()
== 0
)
replacement_key = uuid.uuid4()
replaced = client.post(
@ -861,9 +863,7 @@ def test_workspace_contract_is_minimal_and_redacts_money_without_view_all(
assert inactive_employee.pk not in {item["id"] for item in body["employees"]}
assert inactive_employee.pk in {item["id"] for item in body["rate_employees"]}
assert hidden_employee.pk not in {item["id"] for item in body["employees"]}
assert hidden_employee.pk not in {
item["id"] for item in body["rate_employees"]
}
assert hidden_employee.pk not in {item["id"] for item in body["rate_employees"]}
def test_workspace_readiness_identifies_affected_employees_and_draft_counts(
@ -1018,9 +1018,7 @@ def test_period_table_lists_all_employees_and_requires_view_all(
assert response.status_code == 200, response.content
assert_no_store(response)
body = response.json()
assert hidden_employee.pk not in {
item["employee"]["id"] for item in body["rows"]
}
assert hidden_employee.pk not in {item["employee"]["id"] for item in body["rows"]}
assert body["period_id"] == period.pk
assert body["calculation_rules"]["point_policy"] == "proportional_with_excess"
assert body["calculation_rules"]["money_quantum"] == "0.01"
@ -1051,9 +1049,7 @@ def test_period_table_lists_all_employees_and_requires_view_all(
assert row["opening_balance"] == "0"
assert row["closing_balance"] == "0.00"
assert row["totals_preliminary"] is True
assert all(
item["employee_id"] != employee.pk for item in body["carryovers"]
)
assert all(item["employee_id"] != employee.pk for item in body["carryovers"])
assert body["summary"]["preliminary_count"] == body["summary"]["employee_count"]
missing_row = next(
item for item in body["rows"] if item["employee"]["id"] == missing_employee.pk
@ -1179,17 +1175,23 @@ def test_period_table_review_marks_are_personal_and_idempotent(
assert response.status_code == 200, response.content
assert response.json() == {"employee_ids": [employee.pk]}
assert PayrollTableReviewMark.objects.filter(
period=period,
reviewer=first_reviewer,
employee=employee,
).count() == 1
assert (
PayrollTableReviewMark.objects.filter(
period=period,
reviewer=first_reviewer,
employee=employee,
).count()
== 1
)
assert first_client.get(api_url("admin-period-table", pk=period.pk)).json()[
"review_mark_employee_ids"
] == [employee.pk]
assert auth_client_factory(second_reviewer).get(
api_url("admin-period-table", pk=period.pk)
).json()["review_mark_employee_ids"] == []
assert (
auth_client_factory(second_reviewer)
.get(api_url("admin-period-table", pk=period.pk))
.json()["review_mark_employee_ids"]
== []
)
cleared = first_client.delete(url)
assert cleared.status_code == 200, cleared.content
@ -2148,7 +2150,8 @@ def test_workspace_blocks_period_outside_ruleset_effective_dates(
"code": "RULESET_NOT_EFFECTIVE",
"message": (
"Для выбранного периода нет действующих правил расчёта. "
"Набор eusrr-standard, версия 2026.08.1, применяется с 01.07.2026. "
f"Набор eusrr-standard, версия {DEFAULT_CONFIG['RULESET_VERSION']}, "
"применяется с 01.07.2026. "
"Измените период или подключите историческую версию правил."
),
"details": {
@ -2158,7 +2161,7 @@ def test_workspace_blocks_period_outside_ruleset_effective_dates(
},
"ruleset": {
"id": "eusrr-standard",
"version": "2026.08.1",
"version": DEFAULT_CONFIG["RULESET_VERSION"],
"effective_from": "2026-07-01",
},
},
@ -3285,7 +3288,9 @@ def test_staff_admin_completes_entire_payroll_workflow_with_one_command(
for row in table_after_confirmation.json()["rows"]
if row["employee"]["id"] == employee.pk
)
assert confirmed_row["payment_confirmation"]["confirmed_by"]["id"] == administrator.pk
assert (
confirmed_row["payment_confirmation"]["confirmed_by"]["id"] == administrator.pk
)
revoked = administrator_client.post(
api_url(

View File

@ -32,6 +32,7 @@ from finance.models import (
PayrollWorkRecord,
)
from finance.payroll.exceptions import PayrollOperationError, PayrollPermissionDenied
from finance.payroll.config import DEFAULT_CONFIG
from finance.payroll.services import (
acknowledge_statement,
approve_input_line,
@ -230,7 +231,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
(june_payment, approve_input_line),
(june_correction, approve_input_line),
):
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
approve(
instance.pk, actor=approver, expected_lock_version=instance.lock_version
)
june_run = calculate_period(june_period.pk, actor=operator)
june_statement = june_run.statements.get(employee=employee)
@ -277,7 +280,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
(july_payment, approve_input_line),
(july_correction, approve_input_line),
):
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
approve(
instance.pk, actor=approver, expected_lock_version=instance.lock_version
)
july_run = calculate_period(july_period.pk, actor=operator)
july_statement = july_run.statements.get(employee=employee)
@ -326,7 +331,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
(august_payment, approve_input_line),
(august_correction, approve_input_line),
):
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
approve(
instance.pk, actor=approver, expected_lock_version=instance.lock_version
)
august_run = calculate_period(august_period.pk, actor=operator)
august_statement = august_run.statements.get(employee=employee)
@ -740,7 +747,8 @@ def test_ruleset_not_effective_keeps_domain_code_and_localized_message(
assert error.value.code == "RULESET_NOT_EFFECTIVE"
assert error.value.message == (
"Для выбранного периода нет действующих правил расчёта. "
"Набор eusrr-standard, версия 2026.08.1, применяется с 01.07.2026. "
f"Набор eusrr-standard, версия {DEFAULT_CONFIG['RULESET_VERSION']}, "
"применяется с 01.07.2026. "
"Измените период или подключите историческую версию правил."
)
assert error.value.details["employee_id"] == payroll_users["employee"].pk
@ -750,7 +758,7 @@ def test_ruleset_not_effective_keeps_domain_code_and_localized_message(
}
assert error.value.details["ruleset"] == {
"id": "eusrr-standard",
"version": "2026.08.1",
"version": DEFAULT_CONFIG["RULESET_VERSION"],
"effective_from": "2026-07-01",
}
assert {issue["code"] for issue in error.value.details["issues"]} == {

View File

@ -11,6 +11,9 @@ pytestmark = pytest.mark.django_db(transaction=True)
def _migrate_to(target):
executor = MigrationExecutor(connection)
executor.loader.build_graph()
target_apps = {app_label for app_label, _ in target}
if "employees" not in target_apps:
target = [*target, *executor.loader.graph.leaf_nodes("employees")]
executor.migrate(target)
return executor.loader.project_state(target).apps

View File

@ -39,12 +39,17 @@ def test_migration_0002_backfills_department_binding_by_slug():
slug=f"department-{department.id}",
)
_migrate_to([("scheduling", "0002_calendarbinding")])
from scheduling.models import CalendarBinding
new_apps = _migrate_to(
[
("employees", "0047_alter_departmentpermission_code"),
("schedule", "0015_rename_indexes"),
("scheduling", "0002_calendarbinding"),
]
)
CalendarBinding = new_apps.get_model("scheduling", "CalendarBinding")
binding = CalendarBinding.objects.get(calendar_id=calendar.id)
assert binding.type == CalendarBinding.BindingType.DEPARTMENT
assert binding.type == "department"
assert binding.context_object_id == department.id
assert binding.context_content_type.app_label == "employees"
assert binding.context_content_type.model == "department"