From e936bc869ef88266116713f4f91ac6ca395dea8e Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 21 Jul 2026 18:08:08 +0300 Subject: [PATCH] fix(finance): scope workbook import row lock --- backend/finance/payroll/workbook_import.py | 15 ++++++++++----- .../tests/finance/test_payroll_workbook_import.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/backend/finance/payroll/workbook_import.py b/backend/finance/payroll/workbook_import.py index c82a4f03..c14df3d5 100644 --- a/backend/finance/payroll/workbook_import.py +++ b/backend/finance/payroll/workbook_import.py @@ -486,6 +486,15 @@ def _ensure_import_allowed(period: PayrollPeriod): _error("PERIOD_INPUTS_LOCKED", "Сначала верните текущий расчёт на исправление.") +def _period_import_lock_queryset(): + # current_run is nullable, so PostgreSQL must lock only the period table. + # Locking every joined table raises an error for the outer join when a + # period has no current run. + return PayrollPeriod.objects.select_for_update(of=("self",)).select_related( + "current_run" + ) + + @transaction.atomic def apply_workbook_import( period_id: int, @@ -515,11 +524,7 @@ def apply_workbook_import( "WORKBOOK_CHANGED", "После предпросмотра выбран другой файл. Повторите проверку.", ) - period = ( - PayrollPeriod.objects.select_for_update() - .select_related("current_run") - .get(pk=period_id) - ) + period = _period_import_lock_queryset().get(pk=period_id) _ensure_import_allowed(period) if period.lock_version != expected_period_lock_version: _error("STALE_PERIOD", "Период уже изменён; повторите предпросмотр.") diff --git a/backend/tests/finance/test_payroll_workbook_import.py b/backend/tests/finance/test_payroll_workbook_import.py index 9751bb3a..33a1c3c6 100644 --- a/backend/tests/finance/test_payroll_workbook_import.py +++ b/backend/tests/finance/test_payroll_workbook_import.py @@ -10,7 +10,11 @@ from django.urls import reverse from openpyxl import Workbook from finance.models import PayrollDailyWorkEntry, PayrollPeriod, PayrollWorkRecord -from finance.payroll.workbook_import import _sanitize_styles, parse_workbook +from finance.payroll.workbook_import import ( + _period_import_lock_queryset, + _sanitize_styles, + parse_workbook, +) pytestmark = pytest.mark.django_db @@ -64,6 +68,13 @@ def make_period(creator): ) +def test_import_period_lock_targets_only_the_period_table(): + query = _period_import_lock_queryset().query + + assert query.select_for_update is True + assert query.select_for_update_of == ("self",) + + def test_invalid_legacy_colour_can_be_repaired_before_parsing(): payload = workbook_bytes() source = zipfile.ZipFile(io.BytesIO(payload))