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
|
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:
|
class SmetaRowNormalizer:
|
||||||
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from src.services.export_service.export_normalizers import (
|
|||||||
ExcelValueSerializer,
|
ExcelValueSerializer,
|
||||||
Form1DepthRowNormalizer,
|
Form1DepthRowNormalizer,
|
||||||
Form2DepthRowNormalizer,
|
Form2DepthRowNormalizer,
|
||||||
|
Form4DepthRowNormalizer,
|
||||||
Form3DepthRowNormalizer,
|
Form3DepthRowNormalizer,
|
||||||
SmetaRowNormalizer,
|
SmetaRowNormalizer,
|
||||||
)
|
)
|
||||||
@ -125,6 +126,7 @@ class ExportService:
|
|||||||
self.form1_row_normalizer = Form1DepthRowNormalizer(self.value_serializer)
|
self.form1_row_normalizer = Form1DepthRowNormalizer(self.value_serializer)
|
||||||
self.form2_row_normalizer = Form2DepthRowNormalizer(self.value_serializer)
|
self.form2_row_normalizer = Form2DepthRowNormalizer(self.value_serializer)
|
||||||
self.form3_row_normalizer = Form3DepthRowNormalizer(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.smeta_row_normalizer = SmetaRowNormalizer()
|
||||||
self.sheet_writers: dict[str, TabularSheetWriter] = {
|
self.sheet_writers: dict[str, TabularSheetWriter] = {
|
||||||
"__default__": DepthSheetWriter(
|
"__default__": DepthSheetWriter(
|
||||||
@ -972,19 +974,19 @@ class ExportService:
|
|||||||
FORM4_OPER_DEPTH_COLUMNS,
|
FORM4_OPER_DEPTH_COLUMNS,
|
||||||
FORM4_OPER_COLORS,
|
FORM4_OPER_COLORS,
|
||||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||||
self.form1_row_normalizer,
|
self.form4_row_normalizer,
|
||||||
),
|
),
|
||||||
"AHR": (
|
"AHR": (
|
||||||
FORM4_DEPTH_COLUMNS,
|
FORM4_DEPTH_COLUMNS,
|
||||||
FORM4_COLORS["AHR"],
|
FORM4_COLORS["AHR"],
|
||||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||||
self.form1_row_normalizer,
|
self.form4_row_normalizer,
|
||||||
),
|
),
|
||||||
"CAP": (
|
"CAP": (
|
||||||
FORM4_DEPTH_COLUMNS,
|
FORM4_DEPTH_COLUMNS,
|
||||||
FORM4_COLORS["CAP"],
|
FORM4_COLORS["CAP"],
|
||||||
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
_vat_placeholders(FORM4_SHEET_VAT_WORDING),
|
||||||
self.form1_row_normalizer,
|
self.form4_row_normalizer,
|
||||||
),
|
),
|
||||||
"STRUCTURE": (
|
"STRUCTURE": (
|
||||||
FORM4_STRUCTURE_DEPTH_COLUMNS,
|
FORM4_STRUCTURE_DEPTH_COLUMNS,
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
from src.services.export_service.export_normalizers import (
|
from src.services.export_service.export_normalizers import (
|
||||||
ExcelValueSerializer,
|
ExcelValueSerializer,
|
||||||
|
Form4DepthRowNormalizer,
|
||||||
Form1RowNormalizer,
|
Form1RowNormalizer,
|
||||||
Form3DepthRowNormalizer,
|
Form3DepthRowNormalizer,
|
||||||
SmetaRowNormalizer,
|
SmetaRowNormalizer,
|
||||||
@ -78,3 +81,16 @@ def test_form3_depth_normalizer_flattens_nested_data():
|
|||||||
assert normalized["totals.total_actual"] == 40
|
assert normalized["totals.total_actual"] == 40
|
||||||
assert normalized["totals.economy"] == 2.1
|
assert normalized["totals.economy"] == 2.1
|
||||||
assert "line_id" not in normalized
|
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,
|
TabularSheetWriter,
|
||||||
XlsxDepthSheetWriter,
|
XlsxDepthSheetWriter,
|
||||||
XlsxTabularSheetWriter,
|
XlsxTabularSheetWriter,
|
||||||
|
_hierarchical_rows,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Эталонные порядки колонок листа "Смета" экрана каждой формы (AURORA-1903).
|
# Эталонные порядки колонок листа "Смета" экрана каждой формы (AURORA-1903).
|
||||||
@ -231,3 +232,10 @@ def test_export_service_uses_sheet_writer_strategy():
|
|||||||
|
|
||||||
assert default_writer.calls == 1
|
assert default_writer.calls == 1
|
||||||
assert smeta_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: "",
|
header: "",
|
||||||
accessorKey: "data.header.section_code",
|
accessorKey: "data.header.section_code",
|
||||||
|
valueLabel: "form4NodeType",
|
||||||
columnLetter: "C",
|
columnLetter: "C",
|
||||||
size: 150,
|
size: 150,
|
||||||
filterFn: "contains",
|
filterFn: "contains",
|
||||||
|
|||||||
@ -790,6 +790,7 @@ export const config = {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
accessorKey: "data.header.section_code",
|
accessorKey: "data.header.section_code",
|
||||||
|
valueLabel: "form4NodeType",
|
||||||
columnLetter: "C",
|
columnLetter: "C",
|
||||||
size: 150,
|
size: 150,
|
||||||
filterFn: "contains",
|
filterFn: "contains",
|
||||||
|
|||||||
@ -791,6 +791,7 @@ export const config = {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
accessorKey: "data.header.section_code",
|
accessorKey: "data.header.section_code",
|
||||||
|
valueLabel: "form4NodeType",
|
||||||
columnLetter: "C",
|
columnLetter: "C",
|
||||||
size: 150,
|
size: 150,
|
||||||
filterFn: "contains",
|
filterFn: "contains",
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { isCellEditable } from './utils/cellEditability';
|
|||||||
import { getColumnValue } from './utils/columnUtils';
|
import { getColumnValue } from './utils/columnUtils';
|
||||||
|
|
||||||
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
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 EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable }) => {
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
@ -252,7 +252,12 @@ export const getTableColumns = ({
|
|||||||
// Группирующие колонки-заголовки (есть col.columns) не рендерят Cell/Edit
|
// Группирующие колонки-заголовки (есть col.columns) не рендерят Cell/Edit
|
||||||
// по строкам сами — enableEditing им не нужен, в отличие от листовых.
|
// по строкам сами — enableEditing им не нужен, в отличие от листовых.
|
||||||
const isLeafColumn = !col.columns?.length;
|
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 для вложенных путей, отсутствующих
|
// MRT предупреждает об undefined для вложенных путей, отсутствующих
|
||||||
// у отдельных типов строк. Сохраняем исходный id для настроек и прав.
|
// у отдельных типов строк. Сохраняем исходный id для настроек и прав.
|
||||||
col.id = col.id ?? col.accessorKey;
|
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;
|
if (!msg || !target || msg.is_self !== true) return false;
|
||||||
return msg.event === `${target.kind}_updated` && msg.data?.[`${target.kind}_id`] === target.id;
|
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,
|
canAddRowForHeader,
|
||||||
getDataRowId,
|
getDataRowId,
|
||||||
getDeleteTarget,
|
getDeleteTarget,
|
||||||
|
getForm4TypeLabel,
|
||||||
getProjectAddTarget,
|
getProjectAddTarget,
|
||||||
getRenameTarget,
|
getRenameTarget,
|
||||||
isRenameResponse,
|
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, event: 'program_updated', data: { program_id: 21 } }, target), false);
|
||||||
assert.equal(isRenameResponse(msg, null), 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