Show payroll inputs outside period roster
This commit is contained in:
parent
ecd65abb2d
commit
de37b5c55d
@ -25,6 +25,7 @@ class PeriodInputState:
|
||||
roster_employee_ids: list[int]
|
||||
missing_rate_employee_ids: list[int]
|
||||
approved_input_count: int
|
||||
approved_input_outside_roster_employee_ids: list[int]
|
||||
draft_input_count: int
|
||||
draft_input_employee_ids: list[int]
|
||||
pending_rate_count: int
|
||||
@ -102,11 +103,17 @@ def _period_input_state(period) -> PeriodInputState:
|
||||
for employee_id in roster_employee_ids
|
||||
if employee_id not in employees_with_rate
|
||||
]
|
||||
approved_input_count = PayrollInputLine.objects.filter(
|
||||
approved_inputs = PayrollInputLine.objects.filter(
|
||||
period=period,
|
||||
employee__is_hidden_in_ui=False,
|
||||
status=ApprovalStatus.APPROVED,
|
||||
)
|
||||
approved_input_count = approved_inputs.filter(
|
||||
employee__is_hidden_in_ui=False,
|
||||
).count()
|
||||
approved_input_outside_roster_employee_ids = sorted(
|
||||
set(approved_inputs.values_list("employee_id", flat=True))
|
||||
- set(roster_employee_ids)
|
||||
)
|
||||
draft_inputs = PayrollInputLine.objects.filter(
|
||||
period=period,
|
||||
employee__is_hidden_in_ui=False,
|
||||
@ -125,6 +132,9 @@ def _period_input_state(period) -> PeriodInputState:
|
||||
roster_employee_ids=roster_employee_ids,
|
||||
missing_rate_employee_ids=missing_rate_employee_ids,
|
||||
approved_input_count=approved_input_count,
|
||||
approved_input_outside_roster_employee_ids=(
|
||||
approved_input_outside_roster_employee_ids
|
||||
),
|
||||
draft_input_count=draft_inputs.count(),
|
||||
draft_input_employee_ids=list(
|
||||
draft_inputs.order_by().values_list("employee_id", flat=True).distinct()
|
||||
@ -172,6 +182,21 @@ def _calculation_blockers(period, state: PeriodInputState) -> list[dict]:
|
||||
}
|
||||
)
|
||||
|
||||
if state.approved_input_outside_roster_employee_ids:
|
||||
employee_ids = state.approved_input_outside_roster_employee_ids
|
||||
blockers.append(
|
||||
{
|
||||
"code": "INPUT_EMPLOYEE_NOT_IN_ROSTER",
|
||||
"message": (
|
||||
"Утверждённых начислений или удержаний для сотрудников "
|
||||
f"вне состава периода: {len(employee_ids)}."
|
||||
),
|
||||
"count": len(employee_ids),
|
||||
"employee_ids": employee_ids[:20],
|
||||
"employee_count": len(employee_ids),
|
||||
}
|
||||
)
|
||||
|
||||
missing_without_draft = [
|
||||
employee_id
|
||||
for employee_id in state.missing_rate_employee_ids
|
||||
|
||||
@ -331,7 +331,7 @@ def _attendance_alert(
|
||||
|
||||
|
||||
def build_payroll_period_table(period: PayrollPeriod):
|
||||
"""Include every active employee and inactive employees with period work."""
|
||||
"""Include active employees and inactive employees with period data."""
|
||||
|
||||
current_run = period.current_run
|
||||
statements = {}
|
||||
@ -352,6 +352,12 @@ def build_payroll_period_table(period: PayrollPeriod):
|
||||
actual_points__gt=0,
|
||||
).values_list("employee_id", flat=True)
|
||||
)
|
||||
approved_input_employee_ids = set(
|
||||
PayrollInputLine.objects.filter(
|
||||
period=period,
|
||||
status=ApprovalStatus.APPROVED,
|
||||
).values_list("employee_id", flat=True)
|
||||
)
|
||||
employee_ids = (
|
||||
set(
|
||||
Employee.objects.visible_in_ui()
|
||||
@ -359,6 +365,7 @@ def build_payroll_period_table(period: PayrollPeriod):
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
| period_employee_ids
|
||||
| approved_input_employee_ids
|
||||
)
|
||||
visible_employee_ids = set(
|
||||
Employee.objects.visible_in_ui()
|
||||
|
||||
@ -1418,6 +1418,81 @@ def test_period_table_lists_all_employees_and_requires_view_all(
|
||||
assert missing_row["payable"] == "0.00"
|
||||
|
||||
|
||||
def test_workspace_and_period_table_expose_approved_inputs_outside_roster(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
):
|
||||
manager = user_factory(email="outside-roster.manager@example.test")
|
||||
checker = user_factory(email="outside-roster.checker@example.test")
|
||||
roster_employee = user_factory(email="outside-roster.active@example.test")
|
||||
outside_employee = user_factory(
|
||||
email="outside-roster.inactive@example.test",
|
||||
first_name="Александр",
|
||||
last_name="Внесоставный",
|
||||
active=False,
|
||||
)
|
||||
grant(manager, "manage_payroll_inputs", "view_all_payroll")
|
||||
period = make_period(creator=manager)
|
||||
make_approved_period_inputs(
|
||||
period=period,
|
||||
employee=roster_employee,
|
||||
creator=manager,
|
||||
approver=checker,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=period,
|
||||
employee=outside_employee,
|
||||
component=PayrollComponent.objects.get(code="BONUS"),
|
||||
amount="15000",
|
||||
status="approved",
|
||||
created_by=manager,
|
||||
approved_by=checker,
|
||||
approved_at=timezone.now(),
|
||||
)
|
||||
client = auth_client_factory(manager)
|
||||
|
||||
workspace_response = client.get(
|
||||
api_url("admin-workspace"),
|
||||
{"period_id": period.pk},
|
||||
)
|
||||
|
||||
assert workspace_response.status_code == 200, workspace_response.content
|
||||
readiness = workspace_response.json()["readiness"]["calculation"]
|
||||
blocker = next(
|
||||
item
|
||||
for item in readiness["blockers"]
|
||||
if item["code"] == "INPUT_EMPLOYEE_NOT_IN_ROSTER"
|
||||
)
|
||||
assert readiness["ready"] is False
|
||||
assert blocker == {
|
||||
"code": "INPUT_EMPLOYEE_NOT_IN_ROSTER",
|
||||
"message": (
|
||||
"Утверждённых начислений или удержаний для сотрудников "
|
||||
"вне состава периода: 1."
|
||||
),
|
||||
"count": 1,
|
||||
"employee_ids": [outside_employee.pk],
|
||||
"employee_count": 1,
|
||||
}
|
||||
assert outside_employee.pk in {
|
||||
item["id"] for item in workspace_response.json()["rate_employees"]
|
||||
}
|
||||
|
||||
table_response = client.get(api_url("admin-period-table", pk=period.pk))
|
||||
|
||||
assert table_response.status_code == 200, table_response.content
|
||||
row = next(
|
||||
item
|
||||
for item in table_response.json()["rows"]
|
||||
if item["employee"]["id"] == outside_employee.pk
|
||||
)
|
||||
assert row["employee"]["display_name"] == "Александр Внесоставный"
|
||||
assert row["employee"]["is_active"] is False
|
||||
assert row["status"] == "incomplete"
|
||||
assert row["work_status"] is None
|
||||
assert row["component_amounts"]["BONUS"] == "15000.00"
|
||||
|
||||
|
||||
def test_period_table_cell_comments_use_shared_discussion_flow(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
|
||||
@ -47,6 +47,14 @@ test("payroll readiness blockers link to the relevant filtered source data", ()
|
||||
status: "",
|
||||
label: "Открыть ставки",
|
||||
});
|
||||
assert.deepEqual(
|
||||
getPayrollBlockerTarget("INPUT_EMPLOYEE_NOT_IN_ROSTER"),
|
||||
{
|
||||
tab: "inputs",
|
||||
status: "approved",
|
||||
label: "Показать операции вне состава",
|
||||
},
|
||||
);
|
||||
assert.equal(getPayrollBlockerTarget("PERIOD_CLOSED"), null);
|
||||
});
|
||||
|
||||
|
||||
@ -49,6 +49,11 @@ const PAYROLL_BLOCKER_TARGETS: Record<string, PayrollBlockerTarget> = {
|
||||
status: "draft",
|
||||
label: "Показать черновики операций",
|
||||
},
|
||||
INPUT_EMPLOYEE_NOT_IN_ROSTER: {
|
||||
tab: "inputs",
|
||||
status: "approved",
|
||||
label: "Показать операции вне состава",
|
||||
},
|
||||
DRAFT_PAY_RATES: {
|
||||
tab: "rates",
|
||||
status: "draft",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user