2026-09-07 19:43:48 +03:00

237 lines
9.0 KiB
Python

from collections import defaultdict
from datetime import date, timedelta
from decimal import Decimal
from django.db.models import OuterRef, Prefetch, Subquery
from django.utils import timezone
from attendance.models import AttendanceRecord
from employees.models import Employee, EmployeeAction
from employees.services.personnel_state import (
calculate_employee_tenure_days,
resolve_employee_personnel_state,
)
from finance.analytics.models import ReportingEntry
from finance.enums import PayrollRunStatus
from finance.models import PayrollRun, PayrollStatement
from .sales import ZERO, months
def mean(values):
return sum(values, ZERO) / len(values) if values else None
def absence_days(employee, actions, start, end):
vacation = sick = 0
day = start
while day <= end:
status = (
resolve_employee_personnel_state(employee, day, actions=actions).status
if actions
else "normal"
)
vacation += int(status == "on_leave")
sick += int(status == "on_sick_leave")
day += timedelta(days=1)
return vacation, sick
def people_report(start, end, currency):
employees = list(
Employee.objects.prefetch_related(
Prefetch(
"actions",
queryset=EmployeeAction.objects.select_related(
"source_request"
).order_by("date", "pk"),
)
).order_by("last_name", "first_name", "pk")
)
latest_run = (
PayrollRun.objects.filter(
period_id=OuterRef("run__period_id"), status=PayrollRunStatus.PUBLISHED
)
.order_by("-revision")
.values("pk")[:1]
)
statements = PayrollStatement.objects.filter(
run_id=Subquery(latest_run),
currency=currency,
run__period__date_from__gte=start,
run__period__date_to__lte=end,
).select_related("run__period")
pay = defaultdict(lambda: defaultdict(lambda: ZERO))
for statement in statements:
month = statement.run.period.date_to.strftime("%Y-%m")
pay[statement.employee_id][month] += statement.gross_total
attendance = defaultdict(dict)
for record in AttendanceRecord.objects.filter(
date__range=(start, end), analysis_run__status="success"
):
attendance[record.employee_id][record.date] = record
taxes = defaultdict(lambda: ZERO)
tax_counts = defaultdict(int)
for entry in ReportingEntry.objects.filter(
kind="tax", date__range=(start, end), currency=currency, voided_at__isnull=True
):
taxes[entry.employee_id] += entry.amount
tax_counts[entry.employee_id] += 1
rows = []
month_keys = list(months(start, end))
comparison_months = sorted(
{month for employee_pay in pay.values() for month in employee_pay}
)
demographic = {month: [] for month in month_keys}
for employee in employees:
actions = list(employee.actions.all())
tenure = calculate_employee_tenure_days(employee, end, actions=actions)
had_future_hire = any(
a.action in ["hired", "rehired"] and timezone.localtime(a.date).date() > end
for a in actions
)
state = resolve_employee_personnel_state(employee, end, actions=actions)
employed = (tenure is not None and state.status != "dismissed") or (
tenure is None and employee.is_active and not had_future_hire
)
salary = pay[employee.pk]
records = attendance[employee.pk]
employed_at_start = (
calculate_employee_tenure_days(employee, start, actions=actions) is not None
and resolve_employee_personnel_state(
employee, start, actions=actions
).status
!= "dismissed"
)
worked_during_period = (tenure or 0) > (
calculate_employee_tenure_days(employee, start, actions=actions) or 0
)
if (
not employed
and not employed_at_start
and not worked_during_period
and not salary
and not records
and not tax_counts[employee.pk]
):
continue
vacation, sick = absence_days(employee, actions, start, end)
age = None
if employee.birth_date and employee.birth_date <= end:
born = employee.birth_date
age = end.year - born.year - ((end.month, end.day) < (born.month, born.day))
first = salary.get(comparison_months[0]) if comparison_months else None
last = salary.get(comparison_months[-1]) if comparison_months else None
growth = (
(last / first - 1) * 100
if len(comparison_months) > 1 and first and last is not None
else None
)
row = {
"id": employee.pk,
"name": " ".join(
filter(
None, [employee.last_name, employee.first_name, employee.patronymic]
)
),
"employed": employed,
"gender": employee.gender,
"age": age,
"tenure_days": tenure,
"salary_total": sum(salary.values(), ZERO),
"salary_months": len(salary),
"average_salary": mean(list(salary.values())),
"salary_growth_percent": growth,
"worked_days": sum(
1
for r in records.values()
if r.work_hours is not None and r.work_hours > 0 and not r.is_absent
),
"attendance_days": len(records),
"vacation_days": vacation,
"sick_days": sick,
"tax_paid": taxes[employee.pk] if tax_counts[employee.pk] else None,
"tax_payments": tax_counts[employee.pk],
}
rows.append(row)
for month in month_keys:
month_start = date.fromisoformat(month + "-01")
as_of = min(
end,
(month_start.replace(day=28) + timedelta(days=4)).replace(day=1)
- timedelta(days=1),
)
service = calculate_employee_tenure_days(employee, as_of, actions=actions)
if (
service is not None
and resolve_employee_personnel_state(
employee, as_of, actions=actions
).status
!= "dismissed"
):
born = employee.birth_date
historical_age = (
as_of.year
- born.year
- ((as_of.month, as_of.day) < (born.month, born.day))
if born and born <= as_of
else None
)
demographic[month].append((service, historical_age, employee.gender))
current = [r for r in rows if r["employed"]]
comparable = [r for r in rows if r["salary_growth_percent"] is not None]
ordered = sorted(comparable, key=lambda r: (r["salary_growth_percent"], r["id"]))
monthly = []
for month in month_keys:
salaries = [p[month] for p in pay.values() if month in p]
population = demographic[month]
monthly.append(
{
"id": month,
"average_salary": mean(salaries),
"salary_total": sum(salaries, ZERO),
"salary_employees": len(salaries),
"headcount_with_history": len(population),
"average_tenure_days": mean([Decimal(p[0]) for p in population]),
"average_age": mean(
[Decimal(p[1]) for p in population if p[1] is not None]
),
"men": sum(p[2] == 1 for p in population),
"women": sum(p[2] == 2 for p in population),
}
)
return {
"employees": rows,
"monthly": monthly,
"summary": {
"growth_from": comparison_months[0] if comparison_months else None,
"growth_to": comparison_months[-1] if comparison_months else None,
"headcount": len(current),
"men": sum(r["gender"] == 1 for r in current),
"women": sum(r["gender"] == 2 for r in current),
"unspecified_gender": sum(r["gender"] == 0 for r in current),
"average_age": mean(
[Decimal(r["age"]) for r in current if r["age"] is not None]
),
"average_tenure_days": mean(
[
Decimal(r["tenure_days"])
for r in current
if r["tenure_days"] is not None
]
),
"missing_hire_dates": sum(r["tenure_days"] is None for r in current),
"missing_birth_dates": sum(r["age"] is None for r in current),
"average_salary": mean(
[value for p in pay.values() for value in p.values()]
),
"salary_total": sum((sum(p.values(), ZERO) for p in pay.values()), ZERO),
"tax_paid": sum(taxes.values(), ZERO) if any(tax_counts.values()) else None,
"fastest_growth": ordered[-1]["id"] if ordered else None,
"slowest_growth": ordered[0]["id"] if ordered else None,
},
}