AURORA-1979: подпись программы и проекта в третьем столбце и выгрузке
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
78227b46c0
commit
321943db56
@ -232,6 +232,24 @@ class Form1DepthRowNormalizer:
|
||||
normalized[f"{prefix}{key}"] = value
|
||||
|
||||
|
||||
class Form4DepthRowNormalizer(Form1DepthRowNormalizer):
|
||||
"""AURORA-1979: подпись «программа»/«проект» вместо кода в колонке header.section_code.
|
||||
|
||||
Нормализатор не получает row_type, поэтому узел определяется по line_id и id в header.
|
||||
Исходные данные не меняются: раскраска читает header.section_code из исходной строки.
|
||||
"""
|
||||
|
||||
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = super().normalize(data)
|
||||
header = data.get("header")
|
||||
if data.get("line_id") is None and isinstance(header, dict):
|
||||
if header.get("project_id") is not None:
|
||||
normalized["header.section_code"] = "проект"
|
||||
elif header.get("program_id") is not None:
|
||||
normalized["header.section_code"] = "программа"
|
||||
return normalized
|
||||
|
||||
|
||||
class SmetaRowNormalizer:
|
||||
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
|
||||
@ -28,6 +28,7 @@ from src.services.export_service.export_normalizers import (
|
||||
ExcelValueSerializer,
|
||||
Form1DepthRowNormalizer,
|
||||
Form2DepthRowNormalizer,
|
||||
Form4DepthRowNormalizer,
|
||||
Form3DepthRowNormalizer,
|
||||
SmetaRowNormalizer,
|
||||
)
|
||||
@ -125,6 +126,7 @@ class ExportService:
|
||||
self.form1_row_normalizer = Form1DepthRowNormalizer(self.value_serializer)
|
||||
self.form2_row_normalizer = Form2DepthRowNormalizer(self.value_serializer)
|
||||
self.form3_row_normalizer = Form3DepthRowNormalizer(self.value_serializer)
|
||||
self.form4_row_normalizer = Form4DepthRowNormalizer(self.value_serializer)
|
||||
self.smeta_row_normalizer = SmetaRowNormalizer()
|
||||
self.sheet_writers: dict[str, TabularSheetWriter] = {
|
||||
"__default__": DepthSheetWriter(
|
||||
@ -972,19 +974,19 @@ class ExportService:
|
||||
FORM4_OPER_DEPTH_COLUMNS,
|
||||
FORM4_OPER_COLORS,
|
||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||
self.form1_row_normalizer,
|
||||
self.form4_row_normalizer,
|
||||
),
|
||||
"AHR": (
|
||||
FORM4_DEPTH_COLUMNS,
|
||||
FORM4_COLORS["AHR"],
|
||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||
self.form1_row_normalizer,
|
||||
self.form4_row_normalizer,
|
||||
),
|
||||
"CAP": (
|
||||
FORM4_DEPTH_COLUMNS,
|
||||
FORM4_COLORS["CAP"],
|
||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||
self.form1_row_normalizer,
|
||||
self.form4_row_normalizer,
|
||||
),
|
||||
"STRUCTURE": (
|
||||
FORM4_STRUCTURE_DEPTH_COLUMNS,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from src.services.export_service.export_normalizers import (
|
||||
ExcelValueSerializer,
|
||||
Form4DepthRowNormalizer,
|
||||
Form1RowNormalizer,
|
||||
Form3DepthRowNormalizer,
|
||||
SmetaRowNormalizer,
|
||||
@ -78,3 +81,16 @@ def test_form3_depth_normalizer_flattens_nested_data():
|
||||
assert normalized["totals.total_actual"] == 40
|
||||
assert normalized["totals.economy"] == 2.1
|
||||
assert "line_id" not in normalized
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, expected", [
|
||||
({"line_id": None, "header": {"section_code": "CAP", "program_id": 1}}, "программа"),
|
||||
({"line_id": None, "header": {"section_code": "CAP", "program_id": None, "project_id": 2}}, "проект"),
|
||||
({"line_id": None, "header": {"section_code": "CAP", "program_id": 1, "project_id": 2}}, "проект"),
|
||||
({"line_id": None, "header": {"section_code": "CAP"}}, "CAP"),
|
||||
({"line_id": 9, "header": {"section_code": "3.01.", "program_id": 1, "project_id": 2}}, "3.01."),
|
||||
])
|
||||
def test_form4_depth_normalizer_type_label(data, expected):
|
||||
normalized = Form4DepthRowNormalizer(ExcelValueSerializer()).normalize(data)
|
||||
assert normalized["header.section_code"] == expected
|
||||
assert data["header"]["section_code"] != "программа"
|
||||
|
||||
@ -16,6 +16,7 @@ from src.services.export_service.export_writers import (
|
||||
TabularSheetWriter,
|
||||
XlsxDepthSheetWriter,
|
||||
XlsxTabularSheetWriter,
|
||||
_hierarchical_rows,
|
||||
)
|
||||
|
||||
# Эталонные порядки колонок листа "Смета" экрана каждой формы (AURORA-1903).
|
||||
@ -231,3 +232,10 @@ def test_export_service_uses_sheet_writer_strategy():
|
||||
|
||||
assert default_writer.calls == 1
|
||||
assert smeta_writer.calls == 1
|
||||
|
||||
|
||||
def test_standalone_project_outline_without_group():
|
||||
rows = [("ROOT", 0, 1, {}), ("ITEM", 2, 2, {}), ("INPUT", 3, 3, {}), ("GROUP", 1, 4, {})]
|
||||
assert [(row[0], level) for row, level in _hierarchical_rows(rows)] == [
|
||||
("ROOT", 0), ("ITEM", 1), ("INPUT", 2), ("GROUP", 1),
|
||||
]
|
||||
|
||||
@ -790,6 +790,7 @@ export const config = {
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.section_code",
|
||||
valueLabel: "form4NodeType",
|
||||
columnLetter: "C",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
|
||||
@ -790,6 +790,7 @@ export const config = {
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.section_code",
|
||||
valueLabel: "form4NodeType",
|
||||
columnLetter: "C",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
|
||||
@ -791,6 +791,7 @@ export const config = {
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.section_code",
|
||||
valueLabel: "form4NodeType",
|
||||
columnLetter: "C",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
|
||||
@ -11,7 +11,7 @@ import { isCellEditable } from './utils/cellEditability';
|
||||
import { getColumnValue } from './utils/columnUtils';
|
||||
|
||||
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
||||
import { getRowId } from './utils/rowUtils';
|
||||
import { getForm4TypeLabel, getRowId } from './utils/rowUtils';
|
||||
|
||||
const EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable }) => {
|
||||
const { formId, formType, sheetName, direction } = useParams();
|
||||
@ -252,7 +252,12 @@ export const getTableColumns = ({
|
||||
// Группирующие колонки-заголовки (есть col.columns) не рендерят Cell/Edit
|
||||
// по строкам сами — enableEditing им не нужен, в отличие от листовых.
|
||||
const isLeafColumn = !col.columns?.length;
|
||||
if (isLeafColumn && !col.accessorFn && typeof col.accessorKey === 'string' && col.accessorKey.includes('.')) {
|
||||
if (isLeafColumn && !col.accessorFn && col.valueLabel === 'form4NodeType') {
|
||||
// AURORA-1979: подпись «программа»/«проект»; функция назначается здесь,
|
||||
// а не в конфиге — structuredClone конфига не переносит функции.
|
||||
col.id = col.id ?? col.accessorKey;
|
||||
col.accessorFn = (row) => getForm4TypeLabel(row) ?? getColumnValue(row, col.accessorKey);
|
||||
} else if (isLeafColumn && !col.accessorFn && typeof col.accessorKey === 'string' && col.accessorKey.includes('.')) {
|
||||
// MRT предупреждает об undefined для вложенных путей, отсутствующих
|
||||
// у отдельных типов строк. Сохраняем исходный id для настроек и прав.
|
||||
col.id = col.id ?? col.accessorKey;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Конфиги читаются как текст (импорт без расширений не грузится в node).
|
||||
for (const sheet of ['AHR', 'CAP', 'OPER']) {
|
||||
test(`AURORA-1979: FORM_4/${sheet} — признак подписи без функций в конфиге`, () => {
|
||||
const source = readFileSync(fileURLToPath(new URL(`../constants/FORM_4/${sheet}.js`, import.meta.url)), 'utf8');
|
||||
assert.match(source, /accessorKey: "data\.header\.section_code",\s*valueLabel: "form4NodeType"/);
|
||||
assert.doesNotMatch(source, /accessorFn/);
|
||||
});
|
||||
}
|
||||
@ -122,3 +122,11 @@ export function isRenameResponse(msg, target) {
|
||||
if (!msg || !target || msg.is_self !== true) return false;
|
||||
return msg.event === `${target.kind}_updated` && msg.data?.[`${target.kind}_id`] === target.id;
|
||||
}
|
||||
|
||||
// AURORA-1979: подпись типа узла в колонке «Код статьи / подстатьи / позиции» формы 4.
|
||||
export function getForm4TypeLabel(row) {
|
||||
const header = row?.data?.header;
|
||||
if (row?.row_type === 'GROUP' && header?.program_id != null) return 'программа';
|
||||
if (row?.row_type === 'ITEM' && header?.project_id != null) return 'проект';
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
canAddRowForHeader,
|
||||
getDataRowId,
|
||||
getDeleteTarget,
|
||||
getForm4TypeLabel,
|
||||
getProjectAddTarget,
|
||||
getRenameTarget,
|
||||
isRenameResponse,
|
||||
@ -124,3 +125,12 @@ test('AURORA-1980: isRenameResponse — только свой ответ на с
|
||||
assert.equal(isRenameResponse({ ...msg, event: 'program_updated', data: { program_id: 21 } }, target), false);
|
||||
assert.equal(isRenameResponse(msg, null), false);
|
||||
});
|
||||
|
||||
test('AURORA-1979: getForm4TypeLabel', () => {
|
||||
assert.equal(getForm4TypeLabel(form4Row('GROUP', { program_id: 11 })), 'программа');
|
||||
assert.equal(getForm4TypeLabel(form4Row('ITEM', { project_id: 21 })), 'проект');
|
||||
assert.equal(getForm4TypeLabel(form4Row('ITEM', { program_id: 11, project_id: 21 })), 'проект');
|
||||
assert.equal(getForm4TypeLabel(form4Row('ROOT', { section_code: 'CAP' })), null);
|
||||
assert.equal(getForm4TypeLabel(form4Row('INPUT', { program_id: 11, project_id: 21 })), null);
|
||||
assert.equal(getForm4TypeLabel(form4Row('GROUP', { section_code: '1.' })), null);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user