Merge branch 'develop'

This commit is contained in:
lizerk 2026-07-21 09:17:49 +03:00
commit 893c7c3303
3 changed files with 17 additions and 8 deletions

View File

@ -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,

View File

@ -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(

View File

@ -293,8 +293,8 @@ function calculationHelps(pointPolicy: PayrollPeriodTable["calculation_rules"]["
},
personnelPoints: {
title: "Баллы по кадровым событиям",
logic: "Вычисляются на ходу от нормы баллов сотрудника, рабочего графика и официальных кадровых событий. Каждый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов за затронутые рабочие дни; удалённая работа считается присутствием.",
formula: "Баллы по кадровым событиям = Норма баллов × Рабочие дни без подтверждённого отсутствия ÷ Плановые рабочие дни",
logic: "Это накопительный показатель на начало текущего дня. Он вычисляется на ходу от нормы баллов сотрудника, рабочего графика и официальных кадровых событий. Текущий и будущие дни не учитываются. Каждый завершённый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов; удалённая работа считается присутствием.",
formula: "Баллы по кадровым событиям = Норма баллов × Завершённые рабочие дни без подтверждённого отсутствия ÷ Все плановые рабочие дни периода",
manual: "Нет. Значение меняется через норму баллов, заявление, кадровое событие или рабочий график.",
},
pointDelta: {