fix(finance): apply markers in next payroll period
All checks were successful
Deploy production / deploy (push) Successful in 13m13s

This commit is contained in:
lizerk 2026-09-11 14:51:37 +03:00
parent e308d5916b
commit 38fa63e634
5 changed files with 146 additions and 180 deletions

View File

@ -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,

View File

@ -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": (
"Нельзя назначить изменение в закрытый период."
"Следующий расчётный период уже закрыт."
)
}
)

View File

@ -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,

View File

@ -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}»; отрицательная сумма уменьшит её. Несколько маркеров складываются.`,

View File

@ -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 = {