Merge branch 'develop'
This commit is contained in:
commit
fd49111573
@ -106,19 +106,20 @@ def _calculate_attendance_day_points(
|
||||
# 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:
|
||||
has_verified_shift = bool(
|
||||
worked is not None
|
||||
and worked > 0
|
||||
and record.arrival_time
|
||||
and record.departure_time
|
||||
)
|
||||
if (not record.is_workday and not has_verified_shift) 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 {})
|
||||
)
|
||||
or (
|
||||
worked is not None
|
||||
and worked > 0
|
||||
and record.arrival_time
|
||||
and record.departure_time
|
||||
)
|
||||
or has_verified_shift
|
||||
):
|
||||
return None
|
||||
if bool(record.arrival_time) != bool(record.departure_time):
|
||||
@ -274,6 +275,7 @@ def _analyze_attendance(
|
||||
manual_days = 0
|
||||
overtime_days = 0
|
||||
personnel_conflict_days = 0
|
||||
outside_schedule_days = 0
|
||||
missing_schedule = False
|
||||
|
||||
for record in records:
|
||||
@ -289,18 +291,16 @@ 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.is_workday:
|
||||
blockers.append(
|
||||
_issue(
|
||||
"OUTSIDE_SCHEDULE_WORK_REQUIRES_REVIEW",
|
||||
"Есть работа вне рабочего графика; её нужно проверить вручную.",
|
||||
)
|
||||
)
|
||||
continue
|
||||
if not record.is_workday:
|
||||
has_verified_shift = bool(
|
||||
worked is not None
|
||||
and worked > 0
|
||||
and record.arrival_time
|
||||
and record.departure_time
|
||||
)
|
||||
if not record.is_workday and not has_verified_shift:
|
||||
continue
|
||||
|
||||
effective_workdays += 1
|
||||
effective_workdays += int(record.is_workday)
|
||||
if record.technical_issues:
|
||||
technical_issue_days += 1
|
||||
continue
|
||||
@ -309,12 +309,7 @@ def _analyze_attendance(
|
||||
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
|
||||
)
|
||||
or has_verified_shift
|
||||
):
|
||||
blockers.append(
|
||||
_issue(
|
||||
@ -358,12 +353,14 @@ def _analyze_attendance(
|
||||
|
||||
expected_hours += expected
|
||||
worked_hours += worked
|
||||
target_points += daily_target_points
|
||||
if record.is_workday:
|
||||
target_points += daily_target_points
|
||||
actual_points += daily_target_points * worked / expected
|
||||
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)
|
||||
outside_schedule_days += int(not record.is_workday and worked > 0)
|
||||
|
||||
if technical_issue_days:
|
||||
blockers.append(
|
||||
@ -415,6 +412,14 @@ def _analyze_attendance(
|
||||
f"{personnel_conflict_days} дн.",
|
||||
)
|
||||
)
|
||||
if outside_schedule_days:
|
||||
warnings.append(
|
||||
_issue(
|
||||
"ATTENDANCE_OUTSIDE_SCHEDULE_INCLUDED",
|
||||
"Фактическая работа в выходные учтена сверх нормы периода: "
|
||||
f"{outside_schedule_days} дн.",
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"target_points": str(target_points.quantize(QUANTUM)),
|
||||
|
||||
@ -229,6 +229,41 @@ def test_personnel_non_working_day_keeps_verified_attendance_points(
|
||||
}
|
||||
|
||||
|
||||
def test_weekend_with_verified_shift_adds_points_without_raising_period_target(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
):
|
||||
manager = user_factory(email="attendance.weekend.manager@example.test")
|
||||
employee = user_factory(email="attendance.weekend.employee@example.test")
|
||||
grant(manager, "manage_payroll_inputs")
|
||||
period = make_period(manager, suffix="weekend-attendance")
|
||||
records = make_attendance(employee)
|
||||
weekend_record = records[2]
|
||||
weekend_record.work_hours = 4
|
||||
weekend_record.arrival_time = "10:00"
|
||||
weekend_record.departure_time = "14:00"
|
||||
weekend_record.is_overtime = True
|
||||
weekend_record.overtime_hours = 4
|
||||
weekend_record.statuses = ["work_outside_personnel_schedule"]
|
||||
weekend_record.save()
|
||||
|
||||
assert calculate_attendance_day_points(
|
||||
weekend_record,
|
||||
daily_point_value=Decimal("5"),
|
||||
) == Decimal("2.5000")
|
||||
|
||||
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"] == "7.5000"
|
||||
assert item["blockers"] == []
|
||||
assert "ATTENDANCE_OUTSIDE_SCHEDULE_INCLUDED" in {
|
||||
warning["code"] for warning in item["warnings"]
|
||||
}
|
||||
|
||||
|
||||
def test_replace_mode_revises_approved_and_preserves_excel_controls(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
|
||||
@ -68,6 +68,17 @@ function formatDate(value: string): string {
|
||||
return new Date(`${value}T00:00:00`).toLocaleDateString("ru-RU");
|
||||
}
|
||||
|
||||
function hasAttendanceActivity(day: PayrollPointBreakdownDay): boolean {
|
||||
const record = day.attendance_record;
|
||||
if (!record) return false;
|
||||
const worked = Number(record.work_hours);
|
||||
return Boolean(
|
||||
record.arrival_time
|
||||
|| record.departure_time
|
||||
|| (Number.isFinite(worked) && worked > 0),
|
||||
);
|
||||
}
|
||||
|
||||
function differenceTitle(
|
||||
row: PointRow,
|
||||
day: PayrollPointBreakdownDay,
|
||||
@ -249,11 +260,12 @@ export function PayrollPointBreakdownModal({
|
||||
</th>
|
||||
{data.dates.map((day) => {
|
||||
const parts = dateParts(day.date);
|
||||
const hasActivity = hasAttendanceActivity(day) || Boolean(day.work_entry);
|
||||
return (
|
||||
<th
|
||||
key={day.date}
|
||||
className={`app-surface-muted border-b border-r border-[var(--border-subtle)] px-1 py-2 text-center text-[10px] font-semibold ${day.is_workday ? "text-[var(--muted-foreground)]" : "opacity-55 text-[var(--muted-foreground)]"}`}
|
||||
title={day.is_workday ? "Плановый рабочий день" : "Нерабочий день"}
|
||||
className={`app-surface-muted border-b border-r border-[var(--border-subtle)] px-1 py-2 text-center text-[10px] font-semibold ${day.is_workday || hasActivity ? "text-[var(--muted-foreground)]" : "opacity-55 text-[var(--muted-foreground)]"}`}
|
||||
title={day.is_workday ? "Плановый рабочий день" : hasActivity ? "Нерабочий день с фактической активностью" : "Нерабочий день"}
|
||||
>
|
||||
<span className="block tabular-nums">{parts.date}</span>
|
||||
<span className="mt-0.5 block font-normal uppercase">{parts.weekday}</span>
|
||||
@ -286,10 +298,15 @@ export function PayrollPointBreakdownModal({
|
||||
: row.id === "personnel_points"
|
||||
? day.personnel_points != null || day.personnel_detail.actions.length > 0 || day.personnel_detail.requests.length > 0
|
||||
: Boolean(day.work_entry);
|
||||
const hasSourceActivity = row.id === "attendance_points"
|
||||
? hasAttendanceActivity(day)
|
||||
: row.id === "work_points"
|
||||
? Boolean(day.work_entry)
|
||||
: day.personnel_points != null || day.personnel_detail.actions.length > 0 || day.personnel_detail.requests.length > 0;
|
||||
return (
|
||||
<td
|
||||
key={day.date}
|
||||
className={`border-b border-r border-[var(--border-subtle)] p-0 text-center text-[11px] tabular-nums ${difference ? differenceClasses[difference.color] : day.is_workday ? "text-[var(--foreground)]" : "bg-[var(--surface-secondary)] text-[var(--muted-foreground)]"}`}
|
||||
className={`border-b border-r border-[var(--border-subtle)] p-0 text-center text-[11px] tabular-nums ${difference ? differenceClasses[difference.color] : day.is_workday || day[row.id] != null || hasSourceActivity ? "text-[var(--foreground)]" : "bg-[var(--surface-secondary)] text-[var(--muted-foreground)]"}`}
|
||||
title={differenceTitle(row, day, difference)}
|
||||
>
|
||||
{canOpen ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user