Merge branch 'develop'
This commit is contained in:
commit
0016c70d1a
@ -101,13 +101,24 @@ def _calculate_attendance_day_points(
|
||||
) -> Decimal | None:
|
||||
worked = _finite_decimal(record.work_hours)
|
||||
expected = _finite_decimal(record.expected_hours)
|
||||
if worked is not None and worked > 0 and not record.effective_is_workday:
|
||||
return None
|
||||
if not record.effective_is_workday or record.technical_issues:
|
||||
# Attendance and personnel projections are deliberately independent.
|
||||
# A vacation/dismissal makes ``effective_is_workday`` false, but verified
|
||||
# entrance/exit events on an underlying scheduled day still earn
|
||||
# attendance points. The personnel column represents the conflicting
|
||||
# personnel state separately.
|
||||
if not record.is_workday or record.technical_issues:
|
||||
return None
|
||||
if record.personnel_status == ACTION_REMOTE and not (
|
||||
record.is_manually_edited
|
||||
and "work_hours" in (record.manual_edit_payload or {})
|
||||
(
|
||||
record.is_manually_edited
|
||||
and "work_hours" in (record.manual_edit_payload or {})
|
||||
)
|
||||
or (
|
||||
worked is not None
|
||||
and worked > 0
|
||||
and record.arrival_time
|
||||
and record.departure_time
|
||||
)
|
||||
):
|
||||
return None
|
||||
if bool(record.arrival_time) != bool(record.departure_time):
|
||||
@ -262,6 +273,7 @@ def _analyze_attendance(
|
||||
absence_days = 0
|
||||
manual_days = 0
|
||||
overtime_days = 0
|
||||
personnel_conflict_days = 0
|
||||
missing_schedule = False
|
||||
|
||||
for record in records:
|
||||
@ -277,7 +289,7 @@ def _analyze_attendance(
|
||||
|
||||
worked = _finite_decimal(record.work_hours)
|
||||
expected = _finite_decimal(record.expected_hours)
|
||||
if worked is not None and worked > 0 and not record.effective_is_workday:
|
||||
if worked is not None and worked > 0 and not record.is_workday:
|
||||
blockers.append(
|
||||
_issue(
|
||||
"OUTSIDE_SCHEDULE_WORK_REQUIRES_REVIEW",
|
||||
@ -285,7 +297,7 @@ def _analyze_attendance(
|
||||
)
|
||||
)
|
||||
continue
|
||||
if not record.effective_is_workday:
|
||||
if not record.is_workday:
|
||||
continue
|
||||
|
||||
effective_workdays += 1
|
||||
@ -293,8 +305,16 @@ def _analyze_attendance(
|
||||
technical_issue_days += 1
|
||||
continue
|
||||
if record.personnel_status == ACTION_REMOTE and not (
|
||||
record.is_manually_edited
|
||||
and "work_hours" in (record.manual_edit_payload or {})
|
||||
(
|
||||
record.is_manually_edited
|
||||
and "work_hours" in (record.manual_edit_payload or {})
|
||||
)
|
||||
or (
|
||||
worked is not None
|
||||
and worked > 0
|
||||
and record.arrival_time
|
||||
and record.departure_time
|
||||
)
|
||||
):
|
||||
blockers.append(
|
||||
_issue(
|
||||
@ -343,6 +363,7 @@ def _analyze_attendance(
|
||||
absence_days += int(explicit_absence)
|
||||
manual_days += int(record.is_manually_edited)
|
||||
overtime_days += int(record.is_overtime)
|
||||
personnel_conflict_days += int(not record.effective_is_workday and worked > 0)
|
||||
|
||||
if technical_issue_days:
|
||||
blockers.append(
|
||||
@ -386,6 +407,14 @@ def _analyze_attendance(
|
||||
f"Факт включает переработку в рабочие дни: {overtime_days} дн.",
|
||||
)
|
||||
)
|
||||
if personnel_conflict_days:
|
||||
warnings.append(
|
||||
_issue(
|
||||
"ATTENDANCE_PERSONNEL_CONFLICT_INCLUDED",
|
||||
"Фактическая работа в кадрово нерабочие дни учтена в баллах: "
|
||||
f"{personnel_conflict_days} дн.",
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"target_points": str(target_points.quantize(QUANTUM)),
|
||||
|
||||
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||
from attendance.models import AttendanceAnalysisRun, AttendanceRecord
|
||||
from finance.enums import ApprovalStatus, InputSource
|
||||
from finance.models import PayrollAuditEvent, PayrollPeriod, PayrollWorkRecord
|
||||
from finance.payroll.attendance import calculate_attendance_day_points
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@ -123,9 +124,7 @@ def test_attendance_preview_and_missing_only_create_audited_draft(
|
||||
"Неполный день и переработка рассчитываются пропорционально "
|
||||
"плановым часам."
|
||||
),
|
||||
"formula": (
|
||||
"Баллы дня = дневная норма × отработанные часы ÷ плановые часы"
|
||||
),
|
||||
"formula": ("Баллы дня = дневная норма × отработанные часы ÷ плановые часы"),
|
||||
"daily_target_points": "5.0000",
|
||||
}
|
||||
assert preview["summary"] == {
|
||||
@ -194,6 +193,42 @@ def test_attendance_preview_and_missing_only_create_audited_draft(
|
||||
).exists()
|
||||
|
||||
|
||||
def test_personnel_non_working_day_keeps_verified_attendance_points(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
):
|
||||
manager = user_factory(email="attendance.personnel.manager@example.test")
|
||||
employee = user_factory(email="attendance.personnel.employee@example.test")
|
||||
grant(manager, "manage_payroll_inputs")
|
||||
period = make_period(manager, suffix="personnel-separation")
|
||||
records = make_attendance(employee)
|
||||
worked_record = records[0]
|
||||
worked_record.effective_is_workday = False
|
||||
worked_record.personnel_status = "on_leave"
|
||||
worked_record.personnel_status_label = "В отпуске"
|
||||
worked_record.statuses = ["work_outside_personnel_schedule"]
|
||||
worked_record.is_overtime = True
|
||||
worked_record.overtime_hours = worked_record.work_hours
|
||||
worked_record.save()
|
||||
|
||||
assert calculate_attendance_day_points(
|
||||
worked_record,
|
||||
daily_point_value=Decimal("5"),
|
||||
) == Decimal("5.0000")
|
||||
|
||||
preview_response = auth_client_factory(manager).get(endpoint(period))
|
||||
|
||||
assert preview_response.status_code == 200
|
||||
item = preview_response.json()["items"][0]
|
||||
assert item["target_points"] == "10.0000"
|
||||
assert item["actual_points"] == "5.0000"
|
||||
assert item["blockers"] == []
|
||||
assert {warning["code"] for warning in item["warnings"]} >= {
|
||||
"ATTENDANCE_OVERTIME_INCLUDED",
|
||||
"ATTENDANCE_PERSONNEL_CONFLICT_INCLUDED",
|
||||
}
|
||||
|
||||
|
||||
def test_replace_mode_revises_approved_and_preserves_excel_controls(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user