fix(finance): apply markers in next payroll period
All checks were successful
Deploy production / deploy (push) Successful in 13m13s
All checks were successful
Deploy production / deploy (push) Successful in 13m13s
This commit is contained in:
parent
e308d5916b
commit
38fa63e634
@ -151,12 +151,6 @@ class PayrollCustomComponentWriteSerializer(serializers.ModelSerializer):
|
||||
required=False,
|
||||
write_only=True,
|
||||
)
|
||||
scheduled_application_period_id = serializers.PrimaryKeyRelatedField(
|
||||
source="scheduled_application_period",
|
||||
queryset=PayrollPeriod.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PayrollCustomComponent
|
||||
@ -171,7 +165,6 @@ class PayrollCustomComponentWriteSerializer(serializers.ModelSerializer):
|
||||
"period_scope",
|
||||
"period_id",
|
||||
"scheduled_target_component_code",
|
||||
"scheduled_application_period_id",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
@ -187,32 +180,36 @@ class PayrollCustomComponentWriteSerializer(serializers.ModelSerializer):
|
||||
attrs["include_in_point_base"] = False
|
||||
attrs["include_in_carryover"] = False
|
||||
attrs["visible_to_employee"] = False
|
||||
application_period = attrs.get("scheduled_application_period")
|
||||
source_period = attrs.get("scope_period")
|
||||
target_code = attrs.get("scheduled_target_component_code", "")
|
||||
if source_period is None:
|
||||
raise serializers.ValidationError(
|
||||
{"period_id": "Выберите исходный расчётный период."}
|
||||
)
|
||||
application_period = (
|
||||
PayrollPeriod.objects.filter(date_from__gt=source_period.date_from)
|
||||
.order_by("date_from", "pk")
|
||||
.first()
|
||||
)
|
||||
if application_period is None:
|
||||
raise serializers.ValidationError(
|
||||
{"scheduled_application_period_id": ("Выберите период применения.")}
|
||||
)
|
||||
if (
|
||||
source_period
|
||||
and application_period.date_from <= source_period.date_from
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"scheduled_application_period_id": (
|
||||
"Период применения должен быть позже начального периода."
|
||||
"period_id": (
|
||||
"Следующий расчётный период ещё не создан. "
|
||||
"Сначала создайте его."
|
||||
)
|
||||
}
|
||||
)
|
||||
if application_period.status == "closed":
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"scheduled_application_period_id": (
|
||||
"Нельзя назначить изменение в закрытый период."
|
||||
"period_id": (
|
||||
"Следующий расчётный период уже закрыт. "
|
||||
"Запланированное изменение создать нельзя."
|
||||
)
|
||||
}
|
||||
)
|
||||
attrs["scheduled_application_period"] = application_period
|
||||
try:
|
||||
target = get_payroll_component(
|
||||
target_code,
|
||||
|
||||
@ -1118,7 +1118,11 @@ class PayrollCustomComponent(models.Model):
|
||||
)
|
||||
if self.scheduled_application_period_id is None:
|
||||
raise ValidationError(
|
||||
{"scheduled_application_period": "Выберите период применения."}
|
||||
{
|
||||
"scheduled_application_period": (
|
||||
"Следующий расчётный период не определён."
|
||||
)
|
||||
}
|
||||
)
|
||||
if (
|
||||
self.scope_period_id
|
||||
@ -1128,7 +1132,7 @@ class PayrollCustomComponent(models.Model):
|
||||
raise ValidationError(
|
||||
{
|
||||
"scheduled_application_period": (
|
||||
"Период применения должен быть позже начального периода."
|
||||
"Следующий период должен быть позже начального периода."
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -1136,7 +1140,7 @@ class PayrollCustomComponent(models.Model):
|
||||
raise ValidationError(
|
||||
{
|
||||
"scheduled_application_period": (
|
||||
"Нельзя назначить изменение в закрытый период."
|
||||
"Следующий расчётный период уже закрыт."
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"""Executable contract for payroll columns that schedule a one-period change.
|
||||
|
||||
The production implementation intentionally follows these tests. A scheduled
|
||||
change is configured once on a custom column, contains one signed value per
|
||||
change is configured once on a payroll column, contains one signed value per
|
||||
employee cell, does not affect its source period and changes one target column
|
||||
in one explicitly selected future period.
|
||||
in the immediately next payroll period.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
@ -67,7 +67,6 @@ def component_payload(
|
||||
*,
|
||||
name,
|
||||
source_period,
|
||||
application_period,
|
||||
target_component_code="BONUS",
|
||||
):
|
||||
return {
|
||||
@ -81,7 +80,6 @@ def component_payload(
|
||||
"period_scope": "current_and_future",
|
||||
"period_id": source_period.pk,
|
||||
"scheduled_target_component_code": target_component_code,
|
||||
"scheduled_application_period_id": application_period.pk,
|
||||
}
|
||||
|
||||
|
||||
@ -90,7 +88,6 @@ def create_scheduled_component(
|
||||
*,
|
||||
name,
|
||||
source_period,
|
||||
application_period,
|
||||
target_component_code="BONUS",
|
||||
):
|
||||
response = client.post(
|
||||
@ -98,7 +95,6 @@ def create_scheduled_component(
|
||||
component_payload(
|
||||
name=name,
|
||||
source_period=source_period,
|
||||
application_period=application_period,
|
||||
target_component_code=target_component_code,
|
||||
),
|
||||
format="json",
|
||||
@ -156,54 +152,62 @@ def scheduled_workspace(user_factory, auth_client_factory):
|
||||
}
|
||||
|
||||
|
||||
def test_scheduled_component_has_one_target_and_one_future_application_period(
|
||||
def test_scheduled_component_uses_the_immediately_next_period_automatically(
|
||||
scheduled_workspace,
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
component = create_scheduled_component(
|
||||
workspace["client"],
|
||||
payload = component_payload(
|
||||
name="Плановое изменение премии",
|
||||
source_period=workspace["periods"][6],
|
||||
application_period=workspace["periods"][8],
|
||||
source_period=workspace["periods"][7],
|
||||
)
|
||||
payload["scheduled_application_period_id"] = workspace["periods"][9].pk
|
||||
response = workspace["client"].post(
|
||||
api_url("admin-component-list"),
|
||||
payload,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201, response.content
|
||||
component = response.json()
|
||||
|
||||
assert component["kind"] == SCHEDULED_CHANGE_KIND
|
||||
assert component["scheduled_target_component_code"] == "BONUS"
|
||||
assert component["scheduled_application_period_id"] == workspace["periods"][8].pk
|
||||
assert component["period_scope"] == "current_and_future"
|
||||
assert component["scope_period_id"] == workspace["periods"][6].pk
|
||||
assert component["scope_period_id"] == workspace["periods"][7].pk
|
||||
assert component["include_in_point_base"] is False
|
||||
assert component["include_in_carryover"] is False
|
||||
assert component["visible_to_employee"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("target_code", "application_month", "expected_field"),
|
||||
[
|
||||
("MISSING_COMPONENT", 8, "scheduled_target_component_code"),
|
||||
("BONUS", 6, "scheduled_application_period_id"),
|
||||
],
|
||||
)
|
||||
def test_scheduled_component_rejects_missing_target_or_non_future_period(
|
||||
scheduled_workspace,
|
||||
target_code,
|
||||
application_month,
|
||||
expected_field,
|
||||
):
|
||||
def test_scheduled_component_rejects_missing_target(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
response = workspace["client"].post(
|
||||
api_url("admin-component-list"),
|
||||
component_payload(
|
||||
name="Некорректный маркер",
|
||||
source_period=workspace["periods"][6],
|
||||
application_period=workspace["periods"][application_month],
|
||||
target_component_code=target_code,
|
||||
source_period=workspace["periods"][7],
|
||||
target_component_code="MISSING_COMPONENT",
|
||||
),
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400, response.content
|
||||
assert expected_field in response.json()
|
||||
assert "scheduled_target_component_code" in response.json()
|
||||
|
||||
|
||||
def test_scheduled_component_requires_a_next_period(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
response = workspace["client"].post(
|
||||
api_url("admin-component-list"),
|
||||
component_payload(
|
||||
name="Маркер без следующего периода",
|
||||
source_period=workspace["periods"][9],
|
||||
),
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400, response.content
|
||||
assert "period_id" in response.json()
|
||||
|
||||
|
||||
def test_scheduled_component_rejects_closed_application_period(scheduled_workspace):
|
||||
@ -216,14 +220,13 @@ def test_scheduled_component_rejects_closed_application_period(scheduled_workspa
|
||||
api_url("admin-component-list"),
|
||||
component_payload(
|
||||
name="Маркер в закрытый период",
|
||||
source_period=workspace["periods"][6],
|
||||
application_period=target,
|
||||
source_period=workspace["periods"][7],
|
||||
),
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400, response.content
|
||||
assert "scheduled_application_period_id" in response.json()
|
||||
assert "period_id" in response.json()
|
||||
|
||||
|
||||
def test_empty_system_component_can_be_converted_to_a_scheduled_marker(
|
||||
@ -231,7 +234,7 @@ def test_empty_system_component_can_be_converted_to_a_scheduled_marker(
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
employee = workspace["employee"]
|
||||
response = workspace["client"].patch(
|
||||
@ -239,7 +242,6 @@ def test_empty_system_component_can_be_converted_to_a_scheduled_marker(
|
||||
component_payload(
|
||||
name="Будущее изменение премии",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
),
|
||||
format="json",
|
||||
)
|
||||
@ -297,13 +299,12 @@ def test_signed_marker_does_not_change_source_but_changes_selected_target_period
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name=f"Изменение премии {marker_amount}",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=source,
|
||||
@ -354,13 +355,12 @@ def test_marker_applies_without_any_normal_period_copy(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Самостоятельный прирост",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
|
||||
saved = set_cell(
|
||||
@ -382,13 +382,12 @@ def test_editing_target_cell_treats_entered_amount_as_final_visible_value(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Изменение перед ручной правкой",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=target,
|
||||
@ -438,13 +437,12 @@ def test_clearing_target_is_blocked_when_negative_marker_would_make_it_invalid(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Отрицательное изменение перед очисткой",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=target,
|
||||
@ -477,26 +475,23 @@ def test_clearing_target_is_blocked_when_negative_marker_would_make_it_invalid(
|
||||
assert row["component_amounts"]["BONUS"] == "750.00"
|
||||
|
||||
|
||||
def test_markers_from_same_column_across_periods_and_other_columns_are_summed(
|
||||
def test_multiple_marker_columns_are_summed_in_the_next_period(
|
||||
scheduled_workspace,
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
intermediate = workspace["periods"][7]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
first = create_scheduled_component(
|
||||
client,
|
||||
name="Первое изменение премии",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
second = create_scheduled_component(
|
||||
client,
|
||||
name="Второе изменение премии",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=target,
|
||||
@ -506,14 +501,13 @@ def test_markers_from_same_column_across_periods_and_other_columns_are_summed(
|
||||
created_by=workspace["manager"],
|
||||
)
|
||||
|
||||
for period, component, amount in (
|
||||
(source, first, "500.00"),
|
||||
(intermediate, first, "-125.00"),
|
||||
(intermediate, second, "75.00"),
|
||||
for component, amount in (
|
||||
(first, "500.00"),
|
||||
(second, "-50.00"),
|
||||
):
|
||||
response = set_cell(
|
||||
client,
|
||||
period=period,
|
||||
period=source,
|
||||
employee=employee,
|
||||
component_code=component["code"],
|
||||
amount=amount,
|
||||
@ -524,35 +518,33 @@ def test_markers_from_same_column_across_periods_and_other_columns_are_summed(
|
||||
assert target_row["component_amounts"]["BONUS"] == "1450.00"
|
||||
assert (
|
||||
sum(
|
||||
change["amount"] in {"500.00", "-125.00", "75.00"}
|
||||
change["amount"] in {"500.00", "-50.00"}
|
||||
for change in target_row["applied_scheduled_changes"]
|
||||
)
|
||||
== 3
|
||||
== 2
|
||||
)
|
||||
|
||||
|
||||
def test_marker_column_is_visible_before_but_not_from_application_period(
|
||||
def test_marker_column_is_visible_only_in_its_source_period(
|
||||
scheduled_workspace,
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
intermediate = workspace["periods"][7]
|
||||
source = workspace["periods"][7]
|
||||
previous = workspace["periods"][6]
|
||||
target = workspace["periods"][8]
|
||||
following = workspace["periods"][9]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Маркер с конечным периодом",
|
||||
name="Маркер следующего периода",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
|
||||
assert component["code"] in {
|
||||
column["code"] for column in period_table(client, source)["component_columns"]
|
||||
}
|
||||
assert component["code"] in {
|
||||
column["code"]
|
||||
for column in period_table(client, intermediate)["component_columns"]
|
||||
assert component["code"] not in {
|
||||
column["code"] for column in period_table(client, previous)["component_columns"]
|
||||
}
|
||||
assert component["code"] not in {
|
||||
column["code"] for column in period_table(client, target)["component_columns"]
|
||||
@ -569,13 +561,12 @@ def test_reentering_same_cell_replaces_its_net_value_instead_of_stacking(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Один итог на ячейку",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
|
||||
for amount in ("500.00", "700.00"):
|
||||
@ -601,13 +592,12 @@ def test_marker_value_cannot_be_entered_in_or_after_application_period(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Маркер только до назначения",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
|
||||
for period in (target, workspace["periods"][9]):
|
||||
@ -630,13 +620,12 @@ def test_manual_copy_order_does_not_drop_or_double_the_marker(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name=f"Изменение до переноса {existing_mode}",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=source,
|
||||
@ -686,14 +675,13 @@ def test_adjusted_value_copies_normally_later_without_copying_marker_badge(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
applied = workspace["periods"][8]
|
||||
following = workspace["periods"][9]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Разовый прирост до сентября",
|
||||
source_period=source,
|
||||
application_period=applied,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=applied,
|
||||
@ -747,13 +735,12 @@ def test_editing_and_clearing_marker_recalculates_open_target_once(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Редактируемое изменение",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=target,
|
||||
@ -797,13 +784,12 @@ def test_automatic_copy_carries_effective_value_but_not_marker(scheduled_workspa
|
||||
client = workspace["client"]
|
||||
manager = workspace["manager"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
applied = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Маркер перед автопереносом",
|
||||
source_period=source,
|
||||
application_period=applied,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=applied,
|
||||
@ -861,13 +847,12 @@ def test_marker_is_not_applied_to_employee_dismissed_before_target(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Изменение перед увольнением",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -894,13 +879,12 @@ def test_marker_is_applied_when_employee_is_rehired_before_target(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Изменение после возврата",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -934,13 +918,12 @@ def test_marker_column_with_values_must_be_cleared_before_deletion(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name=f"Удаляемый маркер {delete_mode}",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -982,7 +965,7 @@ def test_target_column_cannot_be_hidden_while_a_marker_references_it(
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
target_response = client.post(
|
||||
api_url("admin-component-list"),
|
||||
@ -1004,7 +987,6 @@ def test_target_column_cannot_be_hidden_while_a_marker_references_it(
|
||||
client,
|
||||
name="Изменение будущей надбавки",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
target_component_code=target_component["code"],
|
||||
)
|
||||
|
||||
@ -1023,8 +1005,7 @@ def test_target_column_cannot_be_scoped_away_from_application_period(
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
target = workspace["periods"][8]
|
||||
source = workspace["periods"][7]
|
||||
created = client.post(
|
||||
api_url("admin-component-list"),
|
||||
{
|
||||
@ -1045,7 +1026,6 @@ def test_target_column_cannot_be_scoped_away_from_application_period(
|
||||
client,
|
||||
name="Маркер защищённой цели",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
target_component_code=target_component["code"],
|
||||
)
|
||||
|
||||
@ -1076,8 +1056,7 @@ def test_marker_rejects_target_column_unavailable_in_application_period(
|
||||
):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
target = workspace["periods"][8]
|
||||
source = workspace["periods"][7]
|
||||
target_response = client.post(
|
||||
api_url("admin-component-list"),
|
||||
{
|
||||
@ -1100,7 +1079,6 @@ def test_marker_rejects_target_column_unavailable_in_application_period(
|
||||
component_payload(
|
||||
name="Маркер в отсутствующую колонку",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
target_component_code=target_response.json()["code"],
|
||||
),
|
||||
format="json",
|
||||
@ -1114,13 +1092,12 @@ def test_cosmetic_marker_edit_preserves_routing_and_effect(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Старое название маркера",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -1133,7 +1110,6 @@ def test_cosmetic_marker_edit_preserves_routing_and_effect(scheduled_workspace):
|
||||
payload = component_payload(
|
||||
name="Новое название маркера",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
payload["highlight_color"] = "violet"
|
||||
|
||||
@ -1157,13 +1133,11 @@ def test_marker_routing_cannot_change_after_values_are_entered(scheduled_workspa
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
target = workspace["periods"][8]
|
||||
source = workspace["periods"][7]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Зафиксированный маршрут",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -1176,7 +1150,7 @@ def test_marker_routing_cannot_change_after_values_are_entered(scheduled_workspa
|
||||
payload = component_payload(
|
||||
name=component["name"],
|
||||
source_period=source,
|
||||
application_period=workspace["periods"][9],
|
||||
target_component_code="VACATION_PAY",
|
||||
)
|
||||
|
||||
changed = client.patch(
|
||||
@ -1195,13 +1169,12 @@ def test_closed_target_prevents_retroactive_marker_change_or_clear(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Маркер закрываемого периода",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
@ -1238,13 +1211,12 @@ def test_negative_marker_cannot_make_target_cell_negative(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Слишком большое уменьшение",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
PayrollInputLine.objects.create(
|
||||
period=target,
|
||||
@ -1282,13 +1254,11 @@ def test_negative_marker_cannot_make_target_cell_negative(scheduled_workspace):
|
||||
def test_marker_cannot_target_itself_or_another_marker(scheduled_workspace):
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
source = workspace["periods"][6]
|
||||
target = workspace["periods"][8]
|
||||
source = workspace["periods"][7]
|
||||
first = create_scheduled_component(
|
||||
client,
|
||||
name="Первичный маркер",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
@ -1296,7 +1266,6 @@ def test_marker_cannot_target_itself_or_another_marker(scheduled_workspace):
|
||||
component_payload(
|
||||
name="Цепочка маркеров",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
target_component_code=first["code"],
|
||||
),
|
||||
format="json",
|
||||
@ -1312,13 +1281,12 @@ def test_marker_component_is_never_offered_or_copied_as_normal_input(
|
||||
workspace = scheduled_workspace
|
||||
client = workspace["client"]
|
||||
employee = workspace["employee"]
|
||||
source = workspace["periods"][6]
|
||||
source = workspace["periods"][7]
|
||||
target = workspace["periods"][8]
|
||||
component = create_scheduled_component(
|
||||
client,
|
||||
name="Непереносимый маркер",
|
||||
source_period=source,
|
||||
application_period=target,
|
||||
)
|
||||
response = set_cell(
|
||||
client,
|
||||
|
||||
@ -61,7 +61,11 @@ import type {
|
||||
PayrollPeriodTableRow,
|
||||
PayrollPeriodTableStatus,
|
||||
} from "@/lib/api/finance";
|
||||
import { formatPayrollMoney, formatPayrollPoints } from "@/lib/payroll";
|
||||
import {
|
||||
formatPayrollMoney,
|
||||
formatPayrollPoints,
|
||||
getPayrollPeriodRange,
|
||||
} from "@/lib/payroll";
|
||||
import { getPayrollAdminError } from "@/lib/payroll-admin";
|
||||
import { assessPayrollPointBaseChange } from "@/lib/payroll-point-base";
|
||||
import {
|
||||
@ -131,9 +135,9 @@ const kindHelpItems: Array<{
|
||||
{
|
||||
kind: "scheduled_change",
|
||||
description:
|
||||
"Внутренний маркер: сейчас ничего не меняет, а в выбранном будущем периоде один раз изменяет выбранную колонку.",
|
||||
"Внутренний маркер: сейчас ничего не меняет, а в следующем расчётном периоде один раз изменяет выбранную колонку.",
|
||||
formula:
|
||||
"Цель в периоде применения = обычное значение цели + сумма всех маркеров.",
|
||||
"Цель в следующем периоде = обычное значение цели + сумма всех маркеров.",
|
||||
},
|
||||
];
|
||||
|
||||
@ -764,12 +768,6 @@ function PayrollComponentSettingsModal({
|
||||
const [scheduledTargetCode, setScheduledTargetCode] = useState(
|
||||
component?.scheduled_target_component_code || "",
|
||||
);
|
||||
const [scheduledApplicationPeriodId, setScheduledApplicationPeriodId] =
|
||||
useState(
|
||||
component?.scheduled_application_period_id
|
||||
? String(component.scheduled_application_period_id)
|
||||
: "",
|
||||
);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
@ -799,15 +797,18 @@ function PayrollComponentSettingsModal({
|
||||
}, [onClose, pending]);
|
||||
|
||||
const currentPeriod = periods.find((item) => item.id === currentPeriodId);
|
||||
const futurePeriods = periods.filter(
|
||||
(item) =>
|
||||
item.status !== "closed" &&
|
||||
Boolean(
|
||||
const nextPeriod = periods
|
||||
.filter(
|
||||
(item) =>
|
||||
currentPeriod &&
|
||||
new Date(item.date_from).getTime() >
|
||||
new Date(currentPeriod.date_from).getTime(),
|
||||
),
|
||||
);
|
||||
new Date(item.date_from).getTime() >
|
||||
new Date(currentPeriod.date_from).getTime(),
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
new Date(left.date_from).getTime() -
|
||||
new Date(right.date_from).getTime() || left.id - right.id,
|
||||
)[0];
|
||||
const scheduledTargets = components.filter(
|
||||
(item) => item.code !== component?.code && item.kind !== "scheduled_change",
|
||||
);
|
||||
@ -824,8 +825,12 @@ function PayrollComponentSettingsModal({
|
||||
setError("Выберите колонку, значение которой нужно изменить.");
|
||||
return;
|
||||
}
|
||||
if (isScheduled && !scheduledApplicationPeriodId) {
|
||||
setError("Выберите будущий период применения.");
|
||||
if (isScheduled && !nextPeriod) {
|
||||
setError("Следующий расчётный период ещё не создан.");
|
||||
return;
|
||||
}
|
||||
if (isScheduled && nextPeriod.status === "closed") {
|
||||
setError("Следующий расчётный период уже закрыт.");
|
||||
return;
|
||||
}
|
||||
if (!isScheduled && pointBaseAssessment.negative.length) {
|
||||
@ -847,9 +852,6 @@ function PayrollComponentSettingsModal({
|
||||
highlight_color: highlightColor,
|
||||
period_scope: isScheduled ? "current_and_future" : periodScope,
|
||||
scheduled_target_component_code: isScheduled ? scheduledTargetCode : "",
|
||||
scheduled_application_period_id: isScheduled
|
||||
? Number(scheduledApplicationPeriodId)
|
||||
: null,
|
||||
});
|
||||
onClose();
|
||||
} catch (createError) {
|
||||
@ -896,10 +898,9 @@ function PayrollComponentSettingsModal({
|
||||
is_custom: component?.is_custom ?? true,
|
||||
highlight_color: highlightColor,
|
||||
scheduled_target_component_code: isScheduled ? scheduledTargetCode : "",
|
||||
scheduled_application_period_id:
|
||||
isScheduled && scheduledApplicationPeriodId
|
||||
? Number(scheduledApplicationPeriodId)
|
||||
: null,
|
||||
scheduled_application_period_id: isScheduled
|
||||
? nextPeriod?.id || null
|
||||
: null,
|
||||
};
|
||||
const calculationPreview = componentCalculationHelp(
|
||||
previewComponent,
|
||||
@ -1018,31 +1019,28 @@ function PayrollComponentSettingsModal({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="app-field-label">Когда применить</span>
|
||||
<select
|
||||
value={scheduledApplicationPeriodId}
|
||||
onChange={(event) =>
|
||||
setScheduledApplicationPeriodId(event.target.value)
|
||||
}
|
||||
className="app-input mt-1 w-full rounded-lg px-3 py-2.5 text-sm"
|
||||
required
|
||||
disabled={pending}
|
||||
>
|
||||
<option value="">Выберите будущий период</option>
|
||||
{futurePeriods.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name || item.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{!futurePeriods.length ? (
|
||||
<span className="app-text-muted mt-1.5 block text-xs">
|
||||
Нет открытых будущих периодов. Сначала создайте следующий
|
||||
расчётный период.
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<div className="app-surface rounded-lg px-3 py-2.5">
|
||||
<span className="app-field-label">Когда применится</span>
|
||||
{nextPeriod ? (
|
||||
<p className="mt-1 text-sm text-[var(--foreground)]">
|
||||
Автоматически в следующем периоде —{" "}
|
||||
<span className="font-medium">
|
||||
{nextPeriod.name || nextPeriod.code}
|
||||
</span>
|
||||
<span className="app-text-muted mt-0.5 block text-xs">
|
||||
{getPayrollPeriodRange(nextPeriod)}
|
||||
{nextPeriod.status === "closed"
|
||||
? " · период уже закрыт"
|
||||
: ""}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="app-feedback-warning mt-1 text-xs">
|
||||
Следующий расчётный период ещё не создан. Сначала создайте
|
||||
его.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="app-text-muted text-xs leading-relaxed">
|
||||
Маркер допускает плюс и минус, не участвует в текущем расчёте и
|
||||
исчезает после применения. В целевой колонке останется отметка
|
||||
@ -1561,7 +1559,7 @@ function componentCalculationHelp(
|
||||
);
|
||||
const targetLabel = target?.label || "выбранная колонка";
|
||||
const periodLabel =
|
||||
applicationPeriod?.name || applicationPeriod?.code || "выбранный период";
|
||||
applicationPeriod?.name || applicationPeriod?.code || "следующий период";
|
||||
return {
|
||||
title: component.label,
|
||||
logic: `Это внутренний маркер. В текущем периоде он не меняет расчёт и не показывается сотруднику. В периоде «${periodLabel}» сумма один раз прибавится к колонке «${targetLabel}»; отрицательная сумма уменьшит её. Несколько маркеров складываются.`,
|
||||
|
||||
@ -658,7 +658,6 @@ export type PayrollCustomComponentWrite = {
|
||||
highlight_color: PayrollColumnHighlightColor;
|
||||
period_scope: PayrollComponentPeriodScope;
|
||||
scheduled_target_component_code: string;
|
||||
scheduled_application_period_id: number | null;
|
||||
};
|
||||
|
||||
export type PayrollAdminPayRate = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user