diff --git a/backend/finance/payroll/work_norm.py b/backend/finance/payroll/work_norm.py index ab2366f4..5d468c07 100644 --- a/backend/finance/payroll/work_norm.py +++ b/backend/finance/payroll/work_norm.py @@ -3,6 +3,8 @@ from __future__ import annotations from datetime import date, timedelta from decimal import Decimal +from django.utils import timezone + from attendance.services import ( get_employee_work_schedule_payload, get_standard_work_schedule, @@ -102,18 +104,20 @@ def calculate_period_personnel_points( ) -> tuple[Decimal, int]: """Project points from the work calendar and official personnel events. - A scheduled day is treated as attended unless the personnel state explicitly - says that attendance is not expected (leave, sick leave, day off, maternity - leave or dismissal). The result is a read-only projection and is not stored - in payroll models. + Only completed scheduled days before today participate in the projection. + Such a day is treated as attended unless the personnel state explicitly says + that attendance is not expected (leave, sick leave, day off, maternity leave + or dismissal). The result is read-only and is not stored in payroll models. """ if schedule is None: schedule, _ = resolve_employee_schedule(employee) workdates = period_workdates(period, schedule) + today = timezone.localdate() + completed_workdates = [workdate for workdate in workdates if workdate < today] attended_dates = [ workdate - for workdate in workdates + for workdate in completed_workdates if resolve_employee_personnel_state( employee, workdate, diff --git a/backend/tests/finance/test_payroll_admin_api.py b/backend/tests/finance/test_payroll_admin_api.py index 134a4b4e..7246d0bd 100644 --- a/backend/tests/finance/test_payroll_admin_api.py +++ b/backend/tests/finance/test_payroll_admin_api.py @@ -274,6 +274,7 @@ def test_period_table_lists_all_employees_and_requires_view_all( def test_period_table_projects_attendance_and_official_personnel_points( user_factory, auth_client_factory, + monkeypatch, ): manager = user_factory(email="table.projections.manager@example.test") employee = user_factory( @@ -335,6 +336,10 @@ def test_period_table_projects_attendance_and_official_personnel_points( action = EmployeeAction.objects.get(source_request=vacation) assert action.date.date() == date(2026, 6, 3) + monkeypatch.setattr( + "finance.payroll.work_norm.timezone.localdate", + lambda: date(2026, 6, 4), + ) response = auth_client_factory(manager).get( api_url("admin-period-table", pk=period.pk), @@ -348,7 +353,7 @@ def test_period_table_projects_attendance_and_official_personnel_points( ) assert row["target_points"] == "220.0000" assert row["attendance_points"] == "15.0000" - assert row["personnel_points"] == "210.0000" + assert row["personnel_points"] == "20.0000" def test_component_cell_can_be_cleared_and_voids_all_active_lines( diff --git a/frontend/src/components/finance/admin/PayrollPeriodTable.tsx b/frontend/src/components/finance/admin/PayrollPeriodTable.tsx index 98c4cdbf..ac1999cb 100644 --- a/frontend/src/components/finance/admin/PayrollPeriodTable.tsx +++ b/frontend/src/components/finance/admin/PayrollPeriodTable.tsx @@ -293,8 +293,8 @@ function calculationHelps(pointPolicy: PayrollPeriodTable["calculation_rules"][" }, personnelPoints: { title: "Баллы по кадровым событиям", - logic: "Вычисляются на ходу от нормы баллов сотрудника, рабочего графика и официальных кадровых событий. Каждый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов за затронутые рабочие дни; удалённая работа считается присутствием.", - formula: "Баллы по кадровым событиям = Норма баллов × Рабочие дни без подтверждённого отсутствия ÷ Плановые рабочие дни", + logic: "Это накопительный показатель на начало текущего дня. Он вычисляется на ходу от нормы баллов сотрудника, рабочего графика и официальных кадровых событий. Текущий и будущие дни не учитываются. Каждый завершённый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов; удалённая работа считается присутствием.", + formula: "Баллы по кадровым событиям = Норма баллов × Завершённые рабочие дни без подтверждённого отсутствия ÷ Все плановые рабочие дни периода", manual: "Нет. Значение меняется через норму баллов, заявление, кадровое событие или рабочий график.", }, pointDelta: {