Merge branch 'develop'
This commit is contained in:
commit
2bf79f6c67
@ -61,6 +61,7 @@ DAYS_RU = {
|
||||
MATRIX_STATUS_LABELS = {
|
||||
"empty": "Нет записи",
|
||||
"technical": "Техсбой",
|
||||
"personnel_issue": "Кадровое событие",
|
||||
"underwork": "Недоработка",
|
||||
"late": "Опоздание",
|
||||
"overtime": "Переработка",
|
||||
@ -72,6 +73,7 @@ MATRIX_STATUS_LABELS = {
|
||||
MATRIX_STATUS_SHORT_LABELS = {
|
||||
"empty": "",
|
||||
"technical": "Тех",
|
||||
"personnel_issue": "Кадр",
|
||||
"underwork": "НД",
|
||||
"late": "ОП",
|
||||
"overtime": "ПР",
|
||||
@ -99,6 +101,7 @@ PERSONNEL_STATUS_LEGEND = {
|
||||
MATRIX_STATUS_FILLS = {
|
||||
"empty": "F1F5F9",
|
||||
"technical": "FEE2E2",
|
||||
"personnel_issue": "FEF3C7",
|
||||
"underwork": "FEF3C7",
|
||||
"late": "FEF3C7",
|
||||
"absent": "FEF3C7",
|
||||
@ -262,6 +265,10 @@ def get_attendance_auto_sync_settings() -> AttendanceAutoSyncSettings:
|
||||
return settings
|
||||
|
||||
|
||||
def get_attendance_auto_sync_employees():
|
||||
return Employee.objects.prefetch_related("actions").order_by("-id")
|
||||
|
||||
|
||||
def run_attendance_auto_sync(*, force: bool = False) -> AttendanceAutoSyncSettings:
|
||||
from common.logstorm_attendance import (
|
||||
analyze_employee_attendance,
|
||||
@ -316,7 +323,7 @@ def run_attendance_auto_sync(*, force: bool = False) -> AttendanceAutoSyncSettin
|
||||
error_count = 0
|
||||
errors: list[str] = []
|
||||
|
||||
for employee in Employee.objects.get_active():
|
||||
for employee in get_attendance_auto_sync_employees():
|
||||
schedule_payload = (
|
||||
get_employee_work_schedule_payload(employee)
|
||||
or get_standard_work_schedule_payload()
|
||||
@ -614,6 +621,7 @@ def _append_attendance_matrix_legend_sheet(workbook: Workbook) -> None:
|
||||
("ОТГ", "Отгул"),
|
||||
("ДЕКР", "Декрет"),
|
||||
("Вне штата", "Уволен или еще не принят в выбранный день"),
|
||||
("Кадровое событие", "Проход при кадровом статусе вне штата"),
|
||||
("+ Nч", "Зафиксирована работа в нерабочий день"),
|
||||
("К: N", "Количество комментариев EUSRR к записи"),
|
||||
("Ссылка", "Клик по ячейке записи открывает модал событий LogStorm в EUSRR"),
|
||||
@ -1172,6 +1180,8 @@ def _matrix_detail_lines(
|
||||
|
||||
def _monthly_matrix_status(record: AttendanceRecord) -> str:
|
||||
if not record.effective_is_workday:
|
||||
if record.personnel_status == ACTION_DISMISSED and record.is_overtime:
|
||||
return "personnel_issue"
|
||||
if record.is_overtime:
|
||||
return "overtime"
|
||||
return "non_working"
|
||||
|
||||
@ -15,7 +15,11 @@ from attendance.models import (
|
||||
StandardWorkSchedule,
|
||||
)
|
||||
from common.logstorm_client import LogStormClientError
|
||||
from employees.constants import ACTION_ON_LEAVE, ACTION_RETURNED_FROM_LEAVE
|
||||
from employees.constants import (
|
||||
ACTION_DISMISSED,
|
||||
ACTION_ON_LEAVE,
|
||||
ACTION_RETURNED_FROM_LEAVE,
|
||||
)
|
||||
from employees.models import EmployeeAction
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@ -482,12 +486,18 @@ def test_auto_sync_disabled_settings_do_not_run_analysis(user_factory):
|
||||
assert settings.last_status == "idle"
|
||||
|
||||
|
||||
def test_auto_sync_updates_due_active_employees_and_uses_schedules(user_factory):
|
||||
def test_auto_sync_updates_due_employees_and_uses_schedules(user_factory):
|
||||
from attendance.services import run_attendance_auto_sync
|
||||
|
||||
employee = user_factory(first_name="Ivan", last_name="Petrov")
|
||||
other = user_factory(first_name="Anna", last_name="Sidorova")
|
||||
user_factory(active=False)
|
||||
inactive = user_factory(first_name="Inactive", last_name="User", active=False)
|
||||
dismissed = user_factory(first_name="Dismissed", last_name="User", active=False)
|
||||
EmployeeAction.objects.create(
|
||||
employee=dismissed,
|
||||
action=ACTION_DISMISSED,
|
||||
date=timezone.now() - timedelta(days=10),
|
||||
)
|
||||
EmployeeWorkSchedule.objects.create(
|
||||
employee=employee,
|
||||
start_time="10:00",
|
||||
@ -519,22 +529,28 @@ def test_auto_sync_updates_due_active_employees_and_uses_schedules(user_factory)
|
||||
) as analyze:
|
||||
settings = run_attendance_auto_sync(force=False)
|
||||
|
||||
assert analyze.call_count == 2
|
||||
assert analyze.call_count == 4
|
||||
calls_by_employee_id = {
|
||||
call.kwargs["employee"].id: call.kwargs
|
||||
for call in analyze.call_args_list
|
||||
}
|
||||
employee_call = calls_by_employee_id[employee.id]
|
||||
other_call = calls_by_employee_id[other.id]
|
||||
inactive_call = calls_by_employee_id[inactive.id]
|
||||
dismissed_call = calls_by_employee_id[dismissed.id]
|
||||
assert employee_call["period_start"] == today - timedelta(days=2)
|
||||
assert employee_call["period_end"] == today
|
||||
assert employee_call["schedule"]["start_time"] == "10:00"
|
||||
assert other_call["schedule"]["start_time"] == "07:30"
|
||||
assert inactive_call["schedule"]["start_time"] == "07:30"
|
||||
assert dismissed_call["schedule"]["start_time"] == "07:30"
|
||||
assert settings.last_status == "success"
|
||||
assert settings.last_success_count == 2
|
||||
assert settings.last_success_count == 4
|
||||
assert settings.last_error_count == 0
|
||||
assert settings.next_run_at > settings.last_finished_at
|
||||
assert AttendanceRecord.objects.filter(employee__in=[employee, other]).count() == 2
|
||||
assert AttendanceRecord.objects.filter(
|
||||
employee__in=[employee, other, inactive, dismissed],
|
||||
).count() == 4
|
||||
|
||||
|
||||
def test_auto_sync_continues_after_employee_error(user_factory):
|
||||
@ -1481,6 +1497,50 @@ def test_personnel_non_working_state_counts_work_as_overtime(
|
||||
assert worked_record.statuses == ["work_outside_personnel_schedule"]
|
||||
|
||||
|
||||
def test_dismissed_employee_work_is_highlighted_as_personnel_issue(
|
||||
auth_client_factory,
|
||||
user_factory,
|
||||
):
|
||||
staff = user_factory(staff=True)
|
||||
employee = user_factory(active=False)
|
||||
_move_auto_hired_before_test_period(employee)
|
||||
client = auth_client_factory(staff)
|
||||
EmployeeAction.objects.create(
|
||||
employee=employee,
|
||||
action=ACTION_DISMISSED,
|
||||
date=_aware_datetime(2026, 4, 20),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"api.v1.attendance.views.analyze_employee_attendance",
|
||||
return_value=_logstorm_result(),
|
||||
):
|
||||
response = client.post(_analyze_url(), _payload(employee.id), format="json")
|
||||
|
||||
assert response.status_code == 200
|
||||
worked_record = AttendanceRecord.objects.get(employee=employee, date="2026-04-20")
|
||||
assert worked_record.personnel_status == ACTION_DISMISSED
|
||||
assert worked_record.effective_is_workday is False
|
||||
assert worked_record.is_overtime is True
|
||||
assert worked_record.statuses == ["work_outside_personnel_schedule"]
|
||||
|
||||
matrix_response = client.get(
|
||||
_monthly_matrix_url(),
|
||||
{
|
||||
"employee_ids": str(employee.id),
|
||||
"month": "2026-04",
|
||||
},
|
||||
)
|
||||
matrix_row = next(
|
||||
row for row in matrix_response.json()["rows"] if row["date"] == "2026-04-20"
|
||||
)
|
||||
matrix_cell = matrix_row["cells"][str(employee.id)]
|
||||
assert matrix_cell["status"] == "personnel_issue"
|
||||
assert matrix_cell["short_label"] == "Вне штата"
|
||||
assert matrix_cell["display_text"] == "Вне штата + 8.75ч"
|
||||
assert matrix_cell["primary_label"] == "Уволен / вне штата"
|
||||
|
||||
|
||||
def test_personnel_state_is_recalculated_when_actions_change(
|
||||
auth_client_factory,
|
||||
user_factory,
|
||||
|
||||
@ -183,7 +183,12 @@ function getMonthKeysBetween(start: string, end: string) {
|
||||
|
||||
function matrixCellTone(status: string) {
|
||||
if (status === "technical") return "bg-red-500/15 text-red-200 hover:bg-red-500/20";
|
||||
if (status === "underwork" || status === "late" || status === "absent") {
|
||||
if (
|
||||
status === "personnel_issue"
|
||||
|| status === "underwork"
|
||||
|| status === "late"
|
||||
|| status === "absent"
|
||||
) {
|
||||
return "bg-amber-500/15 text-amber-100 hover:bg-amber-500/20";
|
||||
}
|
||||
if (status === "overtime") return "bg-emerald-500/15 text-emerald-100 hover:bg-emerald-500/20";
|
||||
@ -201,6 +206,7 @@ function matrixStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
empty: "Нет записи",
|
||||
technical: "Техсбой",
|
||||
personnel_issue: "Кадровое событие",
|
||||
underwork: "Недоработка",
|
||||
late: "Опоздание",
|
||||
overtime: "Переработка",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user