feat(form2): колонка «СПОД» в сателлитах АХР (аренда/коммуналка/охрана)
Добавлен столбец «СПОД» между «Декабрь» и «Итог IV кв.» на листах AHR_RENT/AHR_UTILITY/AHR_SECURITY формы 2, с переносом значения в budget_line_quarter.actual_spod синхронизируемой строки АХР (Support) и, через уже существующую логику, в итог 4 кв. и года АХР/Смет/сводов. - rent_detail/utility_detail/security_detail: колонка actual_spod (ALTER TABLE + ORM). - v3.v_form2_view: fact_q4.spod и fact_q4.total с учётом spod в ветках AHR_RENT/AHR_UTILITY/AHR_SECURITY. - v3._apply_form_cell: fact_q4.spod разрешён как обычное поле месяца. - v3._upsert_form2_ahr_detail_sync_values: DROP+CREATE с новым параметром p_actual_spod (DROP обязателен из-за неоднозначности перегрузки с DEFAULT). - v3.sync_form2_ahr_satellite_lines/line: actual_spod передаётся из rd/ud/sd в апсерт. - v3.v_svod2_go: строка аренды также считается заполненной по actual_spod. - Data-миграция: 'fact_q4.spod' добавлен в column_keys этапов, где уже есть 'fact_q4.dec' (phase_template/form_phase), идемпотентно. - Тело CREATE OR REPLACE для v_form2_view/sync_form2_ahr_satellite_* собрано вручную по миграциям 0033/0034 + regexp-патчам из 0037 (pg_get_functiondef на живой БД недоступен в этой среде — см. Rulings в PR). Тесты: node --test на новых accessorKey в конфигах фронта + unit-тест на 0049/0050 (v_svod2_go теперь засчитывает actual_spod для аренды). Интеграционные тесты добавлены, но не выполнены — нет доступа к Postgres в этой рабочей среде (см. Rulings в PR). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a287480b6
commit
d41668dc27
15
api/alembic/versions/0050_form2_ahr_spod_column.py
Normal file
15
api/alembic/versions/0050_form2_ahr_spod_column.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0050"
|
||||
down_revision = "0049"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0050_form2_ahr_spod_column.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
1628
api/alembic/versions/sql/0050_form2_ahr_spod_column.sql
Normal file
1628
api/alembic/versions/sql/0050_form2_ahr_spod_column.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -37,6 +37,7 @@ class RentDetail(Base):
|
||||
actual_oct: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_nov: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_dec: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_spod: Mapped[float | None] = mapped_column(Numeric)
|
||||
comment: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
# line = relationship("BudgetLine")
|
||||
|
||||
@ -37,6 +37,7 @@ class SecurityDetail(Base):
|
||||
actual_oct: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_nov: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_dec: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_spod: Mapped[float | None] = mapped_column(Numeric)
|
||||
comment: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
# line = relationship("BudgetLine")
|
||||
|
||||
@ -37,6 +37,7 @@ class UtilityDetail(Base):
|
||||
actual_oct: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_nov: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_dec: Mapped[float | None] = mapped_column(Numeric)
|
||||
actual_spod: Mapped[float | None] = mapped_column(Numeric)
|
||||
comment: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
# line = relationship("BudgetLine")
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
"""AURORA-1906: колонка «СПОД» в сателлитах АХР (RENT/UTILITY/SECURITY) формы 2
|
||||
и её перенос в АХР (Support) через v3.sync_form2_ahr_satellite_line(s).
|
||||
|
||||
Сессия тестов работает в rollback-транзакции (см. tests/integration/conftest.py
|
||||
client fixture), поэтому изменения не переживают отдельный тест.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# parent-строка в АХР для каждого сателлита — budget_line с этим
|
||||
# expense_item_id (v3.add_budget_line / v3.sync_form2_ahr_satellite_line,
|
||||
# api/alembic/versions/sql/0034_ahr_rf_sort_order.sql).
|
||||
SATELLITE_PARENT_EXPENSE_ITEM = {
|
||||
"AHR_RENT": 314,
|
||||
"AHR_UTILITY": 24,
|
||||
"AHR_SECURITY": 779,
|
||||
}
|
||||
|
||||
|
||||
def _auth_headers(tokens: dict) -> dict:
|
||||
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
|
||||
|
||||
def _get_form2_id(client, admin_tokens) -> int | None:
|
||||
response = client.get(
|
||||
"/api/v1/form/",
|
||||
params={"form_type": "FORM_2"},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
forms = response.json()["result"]
|
||||
return forms[0]["id"] if forms else None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def form2_id(client, admin_tokens):
|
||||
form_id = _get_form2_id(client, admin_tokens)
|
||||
if form_id is None:
|
||||
pytest.skip("В фикстуре нет формы 2 (FORM_2) — тест СПОД пропущен")
|
||||
return form_id
|
||||
|
||||
|
||||
def _add_satellite_line(client, admin_tokens, form2_id, sheet, vsp_id=1):
|
||||
response = client.post(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/line",
|
||||
json={"vsp_id": vsp_id},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
rows = response.json()["result"]
|
||||
assert len(rows) == 1
|
||||
return rows[0]["data"]["id"]
|
||||
|
||||
|
||||
def _get_sheet_rows(client, admin_tokens, form2_id, sheet) -> list[dict]:
|
||||
response = client.get(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}",
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()["result"]
|
||||
|
||||
|
||||
def _find_row(rows: list[dict], predicate) -> dict:
|
||||
for row in rows:
|
||||
if predicate(row["data"]):
|
||||
return row
|
||||
raise AssertionError("строка не найдена")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
|
||||
def test_satellite_spod_written_and_included_in_q4_total(
|
||||
client, admin_tokens, form2_id, sheet
|
||||
):
|
||||
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
|
||||
|
||||
rows = _get_sheet_rows(client, admin_tokens, form2_id, sheet)
|
||||
row = _find_row(rows, lambda d: d["id"] == line_id)
|
||||
oct_ = row["data"]["fact_q4"]["oct"] or 0
|
||||
nov_ = row["data"]["fact_q4"]["nov"] or 0
|
||||
dec_ = row["data"]["fact_q4"]["dec"] or 0
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": "fact_q4.spod", "value": 5},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
rows = _get_sheet_rows(client, admin_tokens, form2_id, sheet)
|
||||
row = _find_row(rows, lambda d: d["id"] == line_id)
|
||||
assert row["data"]["fact_q4"]["spod"] == 5
|
||||
assert row["data"]["fact_q4"]["total"] == oct_ + nov_ + dec_ + 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
|
||||
def test_satellite_spod_syncs_into_ahr_support_quarter(
|
||||
client, admin_tokens, form2_id, sheet
|
||||
):
|
||||
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row_before = _find_row(
|
||||
ahr_rows,
|
||||
lambda d: d.get("header", {}).get("expense_item_id")
|
||||
== SATELLITE_PARENT_EXPENSE_ITEM[sheet],
|
||||
)
|
||||
actual_quarter_before = ahr_row_before["data"]["q4"]["actual_quarter"] or 0
|
||||
fact_year_before = ahr_row_before["data"]["totals"]["fact_year"] or 0
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": "fact_q4.spod", "value": 5},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row_after = _find_row(
|
||||
ahr_rows,
|
||||
lambda d: d.get("header", {}).get("expense_item_id")
|
||||
== SATELLITE_PARENT_EXPENSE_ITEM[sheet],
|
||||
)
|
||||
assert ahr_row_after["data"]["q4"]["actual_spod"] == 5
|
||||
assert ahr_row_after["data"]["q4"]["actual_quarter"] == actual_quarter_before + 5
|
||||
assert ahr_row_after["data"]["totals"]["fact_year"] == fact_year_before + 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
|
||||
def test_satellite_spod_rejects_other_quarters(
|
||||
client, admin_tokens, form2_id, sheet
|
||||
):
|
||||
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": "fact_q1.spod", "value": 5},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code in (400, 422), response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
|
||||
def test_satellite_spod_phase_gates_non_admin_user(
|
||||
client, admin_tokens, isp_tokens, form2_id, sheet
|
||||
):
|
||||
"""DFIP-пользователь (isp1): без 'fact_q4.spod' в этапе — отказ,
|
||||
после появления ключа (как после data-миграции) — успех."""
|
||||
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
|
||||
|
||||
phase_code = f"spod_test_{sheet.lower()}"
|
||||
create_body = {
|
||||
"budget_form_id": form2_id,
|
||||
"sheet": sheet,
|
||||
"phase_code": phase_code,
|
||||
"role": "DFIP",
|
||||
"column_keys": ["fact_q4.dec"],
|
||||
"opens_at": "2020-01-01T00:00:00",
|
||||
"closes_at": "2099-01-01T00:00:00",
|
||||
"direction": None,
|
||||
}
|
||||
response = client.post(
|
||||
f"/api/v1/stages/form/{form2_id}",
|
||||
json=create_body,
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": "fact_q4.spod", "value": 5},
|
||||
headers=_auth_headers(isp_tokens),
|
||||
)
|
||||
assert response.status_code in (400, 403, 409), response.text
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/stages/form/{form2_id}/{sheet}/{phase_code}",
|
||||
json={**create_body, "column_keys": ["fact_q4.dec", "fact_q4.spod"]},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": "fact_q4.spod", "value": 5},
|
||||
headers=_auth_headers(isp_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1/stages/form/{form2_id}/{sheet}/{phase_code}",
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
@ -7,6 +7,10 @@ from pathlib import Path
|
||||
|
||||
|
||||
SQL_PATH = Path(__file__).resolve().parents[2] / "alembic/versions/sql/0049_summary2_related_records.sql"
|
||||
# AURORA-1906 (0050) переопределяет v_svod2_go целиком (CREATE OR REPLACE) и
|
||||
# добавляет туда проверку rent_detail.actual_spod — патч применяется поверх
|
||||
# 0049 ниже (см. setUp), v_svod2_rf/v_svod2_go_export 0050 не трогает.
|
||||
SQL_PATCH_PATH = Path(__file__).resolve().parents[2] / "alembic/versions/sql/0050_form2_ahr_spod_column.sql"
|
||||
TABLES = (
|
||||
"plan", "reserve", "contract_summary", "allocation", "collegial_approval",
|
||||
"ckk", "contract_detail", "sequestration", "booking", "budget_line_quarter",
|
||||
@ -58,18 +62,28 @@ class Summary2RelatedRecordsSQLTest(unittest.TestCase):
|
||||
columns += ", expenses_q1, expenses_q2, expenses_q3, expenses_q4, vat_rate, deadline"
|
||||
self.db.execute(f"CREATE TABLE v3.{table} ({columns})")
|
||||
# Execute the eligibility SELECTs, adapting PostgreSQL casts, arrays and regex syntax.
|
||||
self.queries = []
|
||||
for part in SQL_PATH.read_text().split("eligible_lines AS (")[1:]:
|
||||
query = part.split("), ", 1)[0].replace("::varchar", "")
|
||||
def adapt(query):
|
||||
query = query.replace("::varchar", "")
|
||||
query = re.sub(r"(presence_\w+\.\w+) ~ '\[\^\[:space:\]\]'",
|
||||
r"has_text(\1)", query)
|
||||
query = query.replace("p_year", ":year")
|
||||
query = query.replace("cardinality(p_org_unit_ids)", "1")
|
||||
query = query.replace("ANY(p_org_unit_ids)", ":org_id")
|
||||
query = query.replace("p_org_unit_ids", ":org_id")
|
||||
self.queries.append(query)
|
||||
return query
|
||||
|
||||
self.queries = [
|
||||
adapt(part.split("), ", 1)[0])
|
||||
for part in SQL_PATH.read_text().split("eligible_lines AS (")[1:]
|
||||
]
|
||||
self.assertEqual(len(self.queries), 3)
|
||||
|
||||
# v_svod2_go (index 0) is redefined whole by 0050 (CREATE OR REPLACE),
|
||||
# which is the current source of truth for its eligible_lines CTE.
|
||||
patch_parts = SQL_PATCH_PATH.read_text().split("eligible_lines AS (")[1:]
|
||||
if patch_parts:
|
||||
self.queries[0] = adapt(patch_parts[0].split("), ", 1)[0])
|
||||
|
||||
def rows(self, index, year=2026, org_id=None):
|
||||
return self.db.execute(self.queries[index], {"year": year, "org_id": org_id}).fetchall()
|
||||
|
||||
@ -118,6 +132,11 @@ class Summary2RelatedRecordsSQLTest(unittest.TestCase):
|
||||
self.db.execute("DELETE FROM v3.reserve")
|
||||
|
||||
def test_each_meaningful_plan_quarter_or_rent_field_qualifies(self):
|
||||
# AURORA-1906 (0050) added rent_detail.actual_spod and made only
|
||||
# v_svod2_go (index 0) qualify a line by it — v_svod2_rf/
|
||||
# v_svod2_go_export (index 1/2) were out of that ticket's scope and
|
||||
# still don't check actual_spod (known gap, see PR Rulings).
|
||||
unpatched_spod_indexes = {1, 2}
|
||||
for index in range(3):
|
||||
self.db.execute("UPDATE v3.budget_form SET form_type_code = ?",
|
||||
("FORM_2" if index == 1 else "FORM_1",))
|
||||
@ -127,7 +146,12 @@ class Summary2RelatedRecordsSQLTest(unittest.TestCase):
|
||||
for value in values:
|
||||
with self.subTest(query=index, table=table, field=field, value=value):
|
||||
self.db.execute(f"INSERT INTO v3.{table}(line_id, {field}) VALUES (1, ?)", (value,))
|
||||
self.assertEqual(len(self.rows(index)), 1)
|
||||
expected = (
|
||||
0
|
||||
if table == "rent_detail" and field == "actual_spod" and index in unpatched_spod_indexes
|
||||
else 1
|
||||
)
|
||||
self.assertEqual(len(self.rows(index)), expected)
|
||||
self.db.execute(f"DELETE FROM v3.{table}")
|
||||
|
||||
def test_source_year_org_and_sheet_filters_are_preserved(self):
|
||||
|
||||
@ -118,9 +118,9 @@ export const config = {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'data.fact_q4.dec',
|
||||
},
|
||||
fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley: {
|
||||
'data.fact_q4.spod': {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
},
|
||||
'data.fact_q4.total': {
|
||||
color_type: whiteColumn,
|
||||
@ -903,6 +903,32 @@ export const config = {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'СПОД',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod',
|
||||
columns: [
|
||||
{
|
||||
header: '',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field',
|
||||
columns: [
|
||||
{
|
||||
header: 'В тыс. рублей',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
columnLetter: 'AR',
|
||||
size: 150,
|
||||
filterFn: 'contains',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Итог IV кв.',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_itog_iv_kv',
|
||||
|
||||
@ -110,9 +110,9 @@ export const config = {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'data.fact_q4.dec',
|
||||
},
|
||||
fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley: {
|
||||
'data.fact_q4.spod': {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
},
|
||||
'data.fact_q4.total': {
|
||||
color_type: whiteColumn,
|
||||
@ -857,6 +857,32 @@ export const config = {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'СПОД',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod',
|
||||
columns: [
|
||||
{
|
||||
header: '',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field',
|
||||
columns: [
|
||||
{
|
||||
header: 'В тыс. рублей',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
columnLetter: 'AP',
|
||||
size: 150,
|
||||
filterFn: 'contains',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Итог IV кв.',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_itog_iv_kv',
|
||||
|
||||
@ -110,9 +110,9 @@ export const config = {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'data.fact_q4.dec',
|
||||
},
|
||||
fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley: {
|
||||
'data.fact_q4.spod': {
|
||||
color_type: whiteColumn,
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
},
|
||||
'data.fact_q4.total': {
|
||||
color_type: whiteColumn,
|
||||
@ -858,6 +858,32 @@ export const config = {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'СПОД',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod',
|
||||
columns: [
|
||||
{
|
||||
header: '',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field',
|
||||
columns: [
|
||||
{
|
||||
header: 'В тыс. рублей',
|
||||
accessorKey: 'data.fact_q4.spod',
|
||||
columnLetter: 'AP',
|
||||
size: 150,
|
||||
filterFn: 'contains',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Итог IV кв.',
|
||||
accessorKey: 'fakticheskie_raskhody_za_iv_kv_2026_goda_itog_iv_kv',
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// AHR_RENT.js / AHR_UTILITY.js / AHR_SECURITY.js импортируют '../columnColors'
|
||||
// без расширения, поэтому node не может загрузить модуль напрямую — читаем
|
||||
// файл как текст и вытаскиваем accessorKey листьев регэкспом (см.
|
||||
// form2SmetaConfig.test.js, AURORA-1901, для образца этого подхода).
|
||||
|
||||
const SHEETS = ['AHR_RENT', 'AHR_UTILITY', 'AHR_SECURITY'];
|
||||
|
||||
function readConfigSource(sheet) {
|
||||
const configPath = fileURLToPath(new URL(`../constants/FORM_2/${sheet}.js`, import.meta.url));
|
||||
return readFileSync(configPath, 'utf8');
|
||||
}
|
||||
|
||||
function extractLeafAccessorKeys(source) {
|
||||
const matches = source.matchAll(/accessorKey:\s*'(data\.[^']+)'/g);
|
||||
return [...matches].map((m) => m[1]);
|
||||
}
|
||||
|
||||
for (const sheet of SHEETS) {
|
||||
test(`${sheet}: колонка СПОД стоит между Декабрём и Итогом IV кв.`, () => {
|
||||
const keys = extractLeafAccessorKeys(readConfigSource(sheet));
|
||||
const decIndex = keys.indexOf('data.fact_q4.dec');
|
||||
const spodIndex = keys.indexOf('data.fact_q4.spod');
|
||||
const totalIndex = keys.indexOf('data.fact_q4.total');
|
||||
|
||||
assert.notEqual(decIndex, -1, 'data.fact_q4.dec должен присутствовать в конфиге');
|
||||
assert.notEqual(spodIndex, -1, 'data.fact_q4.spod должен присутствовать в конфиге');
|
||||
assert.notEqual(totalIndex, -1, 'data.fact_q4.total должен присутствовать в конфиге');
|
||||
assert.deepEqual(
|
||||
[decIndex, spodIndex, totalIndex],
|
||||
[decIndex, decIndex + 1, decIndex + 2],
|
||||
'ожидается порядок ..., data.fact_q4.dec, data.fact_q4.spod, data.fact_q4.total',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${sheet}: колонка СПОД не оставляет висящий ключ в colors`, () => {
|
||||
const source = readConfigSource(sheet);
|
||||
assert.ok(
|
||||
!source.includes('fakticheskie_raskhody_za_iv_kv_2026_goda_spod_field_v_tys_rubley'),
|
||||
'старый висящий ключ цвета СПОД должен быть заменён на data.fact_q4.spod',
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user