From 38fa63e634bb85acf6d5dacd277b98b2c2727194 Mon Sep 17 00:00:00 2001 From: lizerk Date: Fri, 11 Sep 2026 14:51:37 +0300 Subject: [PATCH] fix(finance): apply markers in next payroll period --- .../v1/finance/payroll_serializers/common.py | 35 ++-- backend/finance/models.py | 10 +- .../finance/test_payroll_scheduled_changes.py | 178 +++++++----------- .../finance/admin/PayrollPeriodTable.tsx | 102 +++++----- frontend/src/lib/api/finance.ts | 1 - 5 files changed, 146 insertions(+), 180 deletions(-) diff --git a/backend/api/v1/finance/payroll_serializers/common.py b/backend/api/v1/finance/payroll_serializers/common.py index ceba077e..98e24d21 100644 --- a/backend/api/v1/finance/payroll_serializers/common.py +++ b/backend/api/v1/finance/payroll_serializers/common.py @@ -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, diff --git a/backend/finance/models.py b/backend/finance/models.py index 7927ec2b..d4645029 100644 --- a/backend/finance/models.py +++ b/backend/finance/models.py @@ -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": ( - "Нельзя назначить изменение в закрытый период." + "Следующий расчётный период уже закрыт." ) } ) diff --git a/backend/tests/finance/test_payroll_scheduled_changes.py b/backend/tests/finance/test_payroll_scheduled_changes.py index a2a78ade..1ce9e23e 100644 --- a/backend/tests/finance/test_payroll_scheduled_changes.py +++ b/backend/tests/finance/test_payroll_scheduled_changes.py @@ -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, diff --git a/frontend/src/components/finance/admin/PayrollPeriodTable.tsx b/frontend/src/components/finance/admin/PayrollPeriodTable.tsx index 4156c89c..c347ca0a 100644 --- a/frontend/src/components/finance/admin/PayrollPeriodTable.tsx +++ b/frontend/src/components/finance/admin/PayrollPeriodTable.tsx @@ -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(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({ ))} - +
+ Когда применится + {nextPeriod ? ( +

+ Автоматически в следующем периоде —{" "} + + {nextPeriod.name || nextPeriod.code} + + + {getPayrollPeriodRange(nextPeriod)} + {nextPeriod.status === "closed" + ? " · период уже закрыт" + : ""} + +

+ ) : ( +

+ Следующий расчётный период ещё не создан. Сначала создайте + его. +

+ )} +

Маркер допускает плюс и минус, не участвует в текущем расчёте и исчезает после применения. В целевой колонке останется отметка @@ -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}»; отрицательная сумма уменьшит её. Несколько маркеров складываются.`, diff --git a/frontend/src/lib/api/finance.ts b/frontend/src/lib/api/finance.ts index 25857e00..55b2446b 100644 --- a/frontend/src/lib/api/finance.ts +++ b/frontend/src/lib/api/finance.ts @@ -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 = {