fix(finance): scale projected points by employee norm
This commit is contained in:
parent
d55657f7c3
commit
bc965c412e
@ -97,7 +97,7 @@ def _is_explicit_absence(record: AttendanceRecord) -> bool:
|
||||
def calculate_attendance_points_projection(
|
||||
records: list[AttendanceRecord],
|
||||
*,
|
||||
daily_target_points: Decimal,
|
||||
daily_point_value: Decimal,
|
||||
) -> Decimal:
|
||||
"""Calculate a read-only point total from the available attendance rows.
|
||||
|
||||
@ -132,7 +132,7 @@ def calculate_attendance_points_projection(
|
||||
continue
|
||||
if worked == 0 and not record.arrival_time and not _is_explicit_absence(record):
|
||||
continue
|
||||
actual_points += daily_target_points * worked / expected
|
||||
actual_points += daily_point_value * worked / expected
|
||||
return actual_points.quantize(QUANTUM)
|
||||
|
||||
|
||||
|
||||
@ -286,28 +286,12 @@ def build_payroll_period_table(period: PayrollPeriod):
|
||||
work = work_records.get(employee.pk)
|
||||
source_lines = input_lines.get(employee.pk, [])
|
||||
schedule, target_points_source = resolve_employee_schedule(employee)
|
||||
automatic_target_points, _, _ = calculate_period_target_points(
|
||||
automatic_target_points, scheduled_workdays, _ = calculate_period_target_points(
|
||||
period,
|
||||
employee=employee,
|
||||
daily_target_points=daily_target_points,
|
||||
schedule=schedule,
|
||||
)
|
||||
attendance_records_for_employee = attendance_by_employee.get(employee.pk, [])
|
||||
attendance_points = (
|
||||
calculate_attendance_points_projection(
|
||||
attendance_records_for_employee,
|
||||
daily_target_points=daily_target_points,
|
||||
)
|
||||
if attendance_records_for_employee
|
||||
else None
|
||||
)
|
||||
personnel_points, _ = calculate_period_personnel_points(
|
||||
period,
|
||||
employee=employee,
|
||||
actions=employee.payroll_personnel_actions,
|
||||
daily_target_points=daily_target_points,
|
||||
schedule=schedule,
|
||||
)
|
||||
if statement is not None:
|
||||
snapshot = statement.input_snapshot or {}
|
||||
result_lines = list(statement.lines.all())
|
||||
@ -396,6 +380,29 @@ def build_payroll_period_table(period: PayrollPeriod):
|
||||
else:
|
||||
status = "ready"
|
||||
|
||||
projection_target_points = Decimal(str(target_points or "0"))
|
||||
daily_point_value = (
|
||||
projection_target_points / Decimal(scheduled_workdays)
|
||||
if scheduled_workdays
|
||||
else Decimal("0")
|
||||
)
|
||||
attendance_records_for_employee = attendance_by_employee.get(employee.pk, [])
|
||||
attendance_points = (
|
||||
calculate_attendance_points_projection(
|
||||
attendance_records_for_employee,
|
||||
daily_point_value=daily_point_value,
|
||||
)
|
||||
if attendance_records_for_employee
|
||||
else None
|
||||
)
|
||||
personnel_points, _ = calculate_period_personnel_points(
|
||||
period,
|
||||
employee=employee,
|
||||
actions=employee.payroll_personnel_actions,
|
||||
period_target_points=projection_target_points,
|
||||
schedule=schedule,
|
||||
)
|
||||
|
||||
point_base_amount = _point_base_amount(rate_amount, component_amounts)
|
||||
in_norm_point_rate = _in_norm_point_rate(
|
||||
point_base_amount,
|
||||
|
||||
@ -97,7 +97,7 @@ def calculate_period_personnel_points(
|
||||
*,
|
||||
employee,
|
||||
actions,
|
||||
daily_target_points: Decimal | None = None,
|
||||
period_target_points: Decimal,
|
||||
schedule: dict | None = None,
|
||||
) -> tuple[Decimal, int]:
|
||||
"""Project points from the work calendar and official personnel events.
|
||||
@ -110,19 +110,19 @@ def calculate_period_personnel_points(
|
||||
|
||||
if schedule is None:
|
||||
schedule, _ = resolve_employee_schedule(employee)
|
||||
daily_target = (
|
||||
daily_target_points
|
||||
if daily_target_points is not None
|
||||
else PayrollWorkSettings.get_daily_target_points()
|
||||
)
|
||||
workdates = period_workdates(period, schedule)
|
||||
attended_dates = [
|
||||
workdate
|
||||
for workdate in period_workdates(period, schedule)
|
||||
for workdate in workdates
|
||||
if resolve_employee_personnel_state(
|
||||
employee,
|
||||
workdate,
|
||||
actions=actions,
|
||||
).expects_attendance
|
||||
]
|
||||
points = (daily_target * len(attended_dates)).quantize(QUANTUM)
|
||||
points = (
|
||||
period_target_points * Decimal(len(attended_dates)) / Decimal(len(workdates))
|
||||
if workdates
|
||||
else Decimal("0")
|
||||
).quantize(QUANTUM)
|
||||
return points, len(attended_dates)
|
||||
|
||||
@ -302,6 +302,14 @@ def test_period_table_projects_attendance_and_official_personnel_points(
|
||||
expected_hours=9,
|
||||
effective_is_workday=True,
|
||||
)
|
||||
PayrollWorkRecord.objects.create(
|
||||
period=period,
|
||||
employee=employee,
|
||||
target_points="220",
|
||||
target_points_overridden=True,
|
||||
actual_points="0",
|
||||
created_by=manager,
|
||||
)
|
||||
AttendanceRecord.objects.create(
|
||||
analysis_run=attendance_run,
|
||||
employee=employee,
|
||||
@ -338,8 +346,9 @@ def test_period_table_projects_attendance_and_official_personnel_points(
|
||||
for item in response.json()["rows"]
|
||||
if item["employee"]["id"] == employee.pk
|
||||
)
|
||||
assert row["attendance_points"] == "7.5000"
|
||||
assert row["personnel_points"] == "105.0000"
|
||||
assert row["target_points"] == "220.0000"
|
||||
assert row["attendance_points"] == "15.0000"
|
||||
assert row["personnel_points"] == "210.0000"
|
||||
|
||||
|
||||
def test_component_cell_can_be_cleared_and_voids_all_active_lines(
|
||||
|
||||
@ -287,15 +287,15 @@ function calculationHelps(pointPolicy: PayrollPeriodTable["calculation_rules"]["
|
||||
},
|
||||
attendancePoints: {
|
||||
title: "Баллы по посещаемости",
|
||||
logic: "Вычисляются на ходу по доступным корректным записям посещаемости и не сохраняются в зарплате. Полная смена даёт дневную норму, неполная смена и переработка учитываются пропорционально. Если данных посещаемости нет, показывается прочерк.",
|
||||
formula: "Баллы дня = Дневная норма × Отработанные часы ÷ Плановые часы\nБаллы по посещаемости = Σ баллов доступных рабочих дней",
|
||||
manual: "Нет. Для изменения нужно исправить исходные данные посещаемости.",
|
||||
logic: "Вычисляются на ходу от нормы баллов сотрудника за период и доступных корректных записей посещаемости. Полная смена даёт дневную долю нормы, неполная смена и переработка учитываются пропорционально. Если данных посещаемости нет, показывается прочерк.",
|
||||
formula: "Дневная доля нормы = Норма баллов ÷ Плановые рабочие дни\nБаллы дня = Дневная доля нормы × Отработанные часы ÷ Плановые часы\nБаллы по посещаемости = Σ баллов доступных рабочих дней",
|
||||
manual: "Нет. Значение меняется через норму баллов или исходные данные посещаемости.",
|
||||
},
|
||||
personnelPoints: {
|
||||
title: "Баллы по кадровым событиям",
|
||||
logic: "Вычисляются на ходу по рабочему графику и официальным кадровым событиям. Каждый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов за затронутые рабочие дни; удалённая работа считается присутствием.",
|
||||
formula: "Баллы по кадровым событиям = Дневная норма × Рабочие дни без подтверждённого отсутствия",
|
||||
manual: "Нет. Для изменения нужно исправить заявление, его статус, кадровое событие или рабочий график.",
|
||||
logic: "Вычисляются на ходу от нормы баллов сотрудника, рабочего графика и официальных кадровых событий. Каждый плановый рабочий день считается отработанным полностью, пока одобренное заявление или кадровое событие не подтверждает отсутствие. Отпуск, больничный, отгул, декрет и увольнение дают 0 баллов за затронутые рабочие дни; удалённая работа считается присутствием.",
|
||||
formula: "Баллы по кадровым событиям = Норма баллов × Рабочие дни без подтверждённого отсутствия ÷ Плановые рабочие дни",
|
||||
manual: "Нет. Значение меняется через норму баллов, заявление, кадровое событие или рабочий график.",
|
||||
},
|
||||
pointDelta: {
|
||||
title: "Отклонение",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user