⟨/⟩ Ntr/80_Subsystems/Reports/NtrR01CodeMap.cs

513 строк · в начало

using System;
using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Ntr.Subsystems.Reports
{
    /// <summary>
    /// Перенесено дословно из ntr-01/reports/ntr-r01.md (Карта реализации отчёта NTR-R01 в src/Ntr.Reports: раскладка папок, контракты, конвейер сборки, Excel-шаблон.).
    /// </summary>
    public class DOC_NtrR01CodeMap : IApplicationDocument, IHasStructuralLinks, IFolder<NtrSubsystemsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Backend.Reports.DOC_NtrR01>(),
        };

        public static string _refName = "NTR-R01: карта реализации";

        public string Name => "Спецификация реализации отчёта NTR-R01";

        public string Description => "Карта реализации отчёта NTR-R01 в src/Ntr.Reports: раскладка папок, контракты, конвейер сборки, Excel-шаблон.";

        public string Version => "1.0";
        public string Status => "accepted";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplSpecifikaciyaRealizacii : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(object);
        public int Order => 10;
        public string Number => "";
        public string Name => "Спецификация реализации";

        public string Body => """"

"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplRaspolozhenieIStrukturaPapok : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplSpecifikaciyaRealizacii);
        public int Order => 20;
        public string Number => "";
        public string Name => "Расположение и структура папок";

        public string Body => """"
Отчёт реализуется в проекте `src/Ntr.Reports/` в папке `NTR-R01/`. В корне папки лежит только один основной файл-оркестратор `NtrR01ReportGenerator.cs`; всё остальное разнесено по тематическим подпапкам.

```
NTR-R01/
├── NtrR01ReportGenerator.cs              ← оркестратор сборки отчёта (единственный файл в корне папки)
├── readme.md
│
├── Abstractions/                         публичные контракты модуля
│   ├── INtrR01ReportGenerator.cs
│   ├── INtrR01ReportDataProvider.cs
│   ├── INtrR01SectionBuilder.cs
│   └── NtrR01ReportGenerationResult.cs
│
├── Params/                               входные параметры отчёта
│   └── GenerateNtrR01ReportParams.cs     параметры запуска отчёта (NavId, BuildingId?,
│                                         CollectionTaskId, EnergyProfileStatusIds,
│                                         EnergyProfileConditionsStatusIds, DateOfCreation)
│
├── Model/                                модель готового отчёта (immutable records)
│   ├── NtrR01Report.cs                   корень: ReportParams + Sections
│   ├── SectionKind.cs                    enum секций данных: ElectricalEnergyConsumption /
│   │                                                         HeatEnergyConsumption /
│   │                                                         EnergyEfficiencyClass / Walls / Roofs
│   ├── SectionMetadata.cs                заголовки и имена листов Excel
│   ├── CalcKind.cs                       enum видов расчёта: Normative / Standard /
│   │                                                         Technological / Custom
│   ├── RowLevel.cs                       enum уровней строки секции:
│   │                                     Company / ExecutiveApparatus / Mes / Pmes /
│   │                                     Substation / Building
│   ├── SectionRow.cs                     узел дерева строк: RowLevel, Children,
│   │                                     путь иерархии, per-Building поля,
│   │                                     4 слота по CalcKind с per-секционными колонками
│   └── Sections/                         per-секционные колонки слотов
│       ├── ElectricalEnergyConsumptionSlot.cs
│       ├── HeatEnergyConsumptionSlot.cs
│       ├── EnergyEfficiencyClassSlot.cs
│       ├── WallsSlot.cs
│       └── RoofsSlot.cs
│
├── DataProvider/                         выборка сырых данных из NtrReadDbContext
│   ├── NtrR01ReportDataProvider.cs
│   ├── NtrR01SourceData.cs               агрегат сырых данных
│   └── Sources/                          сырые DTO с подтянутой иерархией ИА/МЭС/ПМЭС/ПС
│       ├── ConsumptionModelSource.cs
│       ├── BuildingSource.cs
│       ├── EnergyProfileSource.cs
│       └── EnergyProfileCollectionTaskSource.cs
│
├── Sections/                             по одному builder'у на каждую секцию
│   ├── ElectricalEnergyConsumptionSectionBuilder.cs
│   ├── HeatEnergyConsumptionSectionBuilder.cs
│   ├── EnergyEfficiencyClassSectionBuilder.cs
│   ├── WallsSectionBuilder.cs
│   └── RoofsSectionBuilder.cs
│
├── Hierarchy/                            построение дерева орг-структуры из выбранных зданий
│   └── OrgHierarchyTreeBuilder.cs        строит дерево по фактически присутствующим зданиям
│
└── Excel/
    ├── INtrR01ReportExcelRenderer.cs
    ├── NtrR01ReportExcelRenderer.cs
    └── Templates/
        └── ntr-r01-template.xlsx         встроенный ресурс
```
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplArhitekturnyeResheniya : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplSpecifikaciyaRealizacii);
        public int Order => 30;
        public string Number => "";
        public string Name => "Архитектурные решения";

        public string Body => """"

"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplObektOtchetaNtrr01report : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 40;
        public string Number => "";
        public string Name => "Объект отчёта `NtrR01Report`";

        public string Body => """"
Отчёт содержит секции и параметры. Все модели — `record` с init-свойствами.

```
NtrR01Report:
  ReportParams              : GenerateNtrR01ReportParams
  ElectricalEnergyConsumption: Section<ElectricalEnergyConsumptionSlot>
  HeatEnergyConsumption     : Section<HeatEnergyConsumptionSlot>
  EnergyEfficiencyClass     : Section<EnergyEfficiencyClassSlot>
  Walls                     : Section<WallsSlot>
  Roofs                     : Section<RoofsSlot>          // ВОК

Section<TSlot>:
  Header                    : string           // из SectionMetadata
  TabName                   : string           // имя листа в Excel
  Roots                     : List<SectionRow<TSlot>>     // корни дерева строк секции
```

Cекций данных — пять (`SectionKind`). Шестой пункт спецификации, «Параметры отчёта», самостоятельной секцией не является — его значения хранятся прямо в `NtrR01Report.ReportParams` и при рендере выводятся на отдельный лист Excel.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplParametryOtcheta : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 50;
        public string Number => "";
        public string Name => "Параметры отчёта";

        public string Body => """"
```
GenerateNtrR01ReportParams:
  DateOfCreation                    : DateTime
  NavId                             : Guid              // обязателен; орг-объект (Компания/ИА/МЭС/ПМЭС/ПС)
  BuildingId                        : Guid?             // опционален; если задан — отчёт по одному Зданию
  CollectionTaskId                  : Guid              // выбранное Задание на сбор энергопрофилей
  EnergyProfileStatusIds            : IReadOnlyList<Guid>   // соответствуют EpEnergyProfileStatus.Id; пусто = «все»
  EnergyProfileConditionsStatusIds  : IReadOnlyList<Guid>   // соответствуют EpModelConsistencyStatus.Id; пусто = «все»
```

В блоке навигации пользователь выбирает узел в едином иерархическом дереве, в котором присутствуют и орг-объекты, и здания. В отчёт прилетает пара (`NavId`, `BuildingId?`):

- `BuildingId` задан → отчёт по выбранному Зданию (его Энергопрофиль в выбранном Задании). `NavId` при этом содержит идентификатор орг-объекта, к которому привязано Здание (используется для построения цепочки иерархии в строках отчёта).
- `BuildingId` не задан → отчёт по всем Зданиям, находящимся в узле `NavId` или ниже.

Задание на сбор энергопрофилей (`CollectionTaskId`) обязательно. По умолчанию UI подставляет последнее созданное задание.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplObertkaRezultata : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 60;
        public string Number => "";
        public string Name => "Обёртка результата";

        public string Body => """"
```
NtrR01ReportGenerationResult:
  Report    : NtrR01Report?
  Warnings  : IReadOnlyList<string>    // мягкие проблемы (расхождения per-Building полей и т.п.)
  Errors    : IReadOnlyList<string>    // блокирующие ошибки
  IsSuccess : bool                     // Report != null && Errors.Count == 0
```
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplKonveyerSborki : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 70;
        public string Number => "";
        public string Name => "Конвейер сборки";

        public string Body => """"
`INtrR01ReportDataProvider` одним проходом тянет сырьё из `NtrReadDbContext` с join'ами на `Navs`/`NavIa`/`NavMeses`/`NavPmeses`/`NavSubstations`/`RegBuildings` и справочники → `OrgHierarchyTreeBuilder` строит дерево орг-структуры по фактически присутствующим в выборке зданиям → набор `INtrR01SectionBuilder` (по одному на секцию) раскладывает данные по узлам дерева и per-`CalcKind` слотам → `NtrR01ReportGenerator` собирает корневой `NtrR01Report` → `INtrR01ReportExcelRenderer` обходит дерево preorder'ом, выводит строки в xlsx поверх шаблона и попутно формирует Excel-формулы агрегации.

**DI.** Один `INtrR01ReportGenerator` / `INtrR01ReportDataProvider` / `INtrR01ReportExcelRenderer` + N `INtrR01SectionBuilder` через `IEnumerable<>`. Регистрация — в `NtrReportsDiExtension`.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplSostavSekciy : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 80;
        public string Number => "";
        public string Name => "Состав секций";

        public string Body => """"
Cекций данных — пять, по одной на каждый `SectionKind`:

| `SectionKind` | Заголовок (краткое наименование) | Колонки слота (per-`CalcKind`) |
| --- | --- | --- |
| `ElectricalEnergyConsumption` | Расход ЭЭ | Общее потребление ЭЭ зданием, кВт·ч |
| `HeatEnergyConsumption` | Расход ТЭ | Общее потребление ТЭ зданием, Гкал |
| `EnergyEfficiencyClass` | Класс | Удельная характеристика расхода ТЭ (расч.) Вт/(м³·℃); Удельная характеристика расхода ТЭ (норм.) Вт/(м³·℃); Отклонение расчётной от нормативной, %; Класс энергоэффективности |
| `Walls` | Стены | По 3 колонки (материал / теплопроводность Вт/(м·К) / толщина мм) для несущего слоя, основного утеплителя и дополнительного утеплителя — 9 колонок |
| `Roofs` | ВОК | По 3 колонки для несущего слоя верхнего перекрытия, утеплителя верхнего перекрытия и утеплителя перекрытия крыши — 9 колонок |
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplStrukturaStrokiSekcii : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 90;
        public string Number => "";
        public string Name => "Структура строки секции";

        public string Body => """"
Одна **листовая** строка секции = одно **Здание**. Каждая листовая строка содержит четыре набора per-секционных колонок (слотов) — по одному на `CalcKind`: «Характеристики нормативной модели», «Характеристики эталонной модели», «Характеристики технологической модели», «Характеристики произвольной модели». Каждый слот заполняется значениями соответствующей Модели потребления из Энергопрофиля Здания. Если модели для какого-то вида расчёта в Энергопрофиле здания нет — соответствующий слот пуст.

Кроме листовых, в секции присутствуют **строки-агрегаты** уровней Компания / ИА / МЭС / ПМЭС / Подстанция (см. иерархию ниже).

Строки секции образуют **дерево**: узлы агрегатов содержат дочерние узлы; листья — здания. Рендер обходит дерево preorder'ом.

```
SectionRow<TSlot>  (узел дерева, общая часть для всех секций):
  RowLevel          : Company | ExecutiveApparatus | Mes | Pmes | Substation | Building
  Children          : List<SectionRow<TSlot>>           // у Building пуст
  Hierarchy         : имена Компании / ИА / МЭС / ПМЭС / Подстанции / Здания
                      (повторяются на потомках строки)
  PerBuildingFields : Адрес, Климатический район, Функциональное назначение,
                      Тип системы отопления, Объём отапливаемый м³, Этажность,
                      Статус энергопрофиля, Статус выполнения условий расчётов
                      // заполняются ТОЛЬКО на строках уровня Building
  Slots             : Dictionary<CalcKind, TSlot?>      // 4 ключа
                      // на Building заполняются из соответствующей модели Энергопрофиля
                      // на агрегатах остаются null; суммируемые колонки рендер
                      //   заполняет формулой SUM (см. «Агрегация по орг-иерархии»)
```
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplIerarhiyaISostavStrokSekcii : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 100;
        public string Number => "";
        public string Name => "Иерархия и состав строк секции";

        public string Body => """"
`RowLevel`: `Company → ExecutiveApparatus (ИА) → Mes → Pmes → Substation → Building`. Состав прямых детей по уровням, правила образования и наполнения дерева — см. функциональную часть выше (раздел «Состав агрегатных записей по уровням»).

Здание принадлежит **ровно одному** орг-объекту (ИА / МЭС / ПМЭС / Подстанция) — это гарантировано доменом. Дерево строится только по фактически присутствующим в выборке зданиям; пустых веток не создаётся. Порядок строк — preorder обхода дерева (агрегат, ниже его потомки).
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplIstochnikiZnacheniyVListovoyStroke : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 110;
        public string Number => "";
        public string Name => "Источники значений в листовой строке";

        public string Body => """"
Общая часть листовой строки (имена орг-иерархии и per-Building поля, включая статус энергопрофиля и статус выполнения условий расчётов) — состав колонок и источники см. в функциональной части выше (таблица «Общие для всех секций параметры записей секции»). Значения, источник которых — Модель потребления (Функциональное назначение, Тип системы отопления, Объём отапливаемый, Этажность), берутся из Нормативной модели Энергопрофиля Здания; при расхождении этих полей между 4 моделями фиксируется предупреждение в `NtrR01ReportGenerationResult.Warnings`.

**Per-`CalcKind` слоты по секциям** (маппинг на свойства доменных сущностей для реализации)

В строке здания на каждый `CalcKind` заполняется свой слот, значения берутся из соответствующей Модели потребления:

- **Расход ЭЭ** — `ConsumptionModel.TotalElectricalEnergyConsumptionKwh`.
- **Расход ТЭ** — `ConsumptionModel.TotalHeatEnergyConsumptionGcal`.
- **Класс** — `ConsumptionModel.CalculatedHeatLossCoeff`, `ConsumptionModel.NormativeHeatLossCoeff`, `ConsumptionModel.DeviationPercent`, `ConsumptionModel → RefEnergyEfficiencyClass.Name`.
- **Стены** — `ConsumptionModel → WallsModel`:
    - Несущий слой: `→ RefMaterial.Name`, `→ RefMaterial.ThermalConductivity`, `LoadBearingThicknessMm`.
    - Основной утеплитель: `→ RefInsulationMaterial.Name`, `→ RefInsulationMaterial.ThermalConductivity`, `MainInsulationThicknessMm`.
    - Дополнительный утеплитель: аналогично, поля `AdditionalInsulation*`.
- **ВОК** — `ConsumptionModel → RoofModel`:
    - Верхнее перекрытие, несущий слой: `→ RefMaterial.Name`, `→ RefMaterial.ThermalConductivity`, `UpperFloorLoadBearingThicknessMm`.
    - Верхнее перекрытие, утеплитель: `→ RefInsulationMaterial.Name`, `→ RefInsulationMaterial.ThermalConductivity`, `UpperFloorInsulationThicknessMm`.
    - Перекрытие крыши, утеплитель: аналогично, поля `RoofInsulation*`.

Конкретные имена свойств уточняются по `Ntr.Domain.Modules.Consumption.Entities` при реализации.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplAgregaciyaPoOrgIerarhii : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 120;
        public string Number => "";
        public string Name => "Агрегация по орг-иерархии";

        public string Body => """"
В дополнение к листовым строкам в каждой секции присутствуют строки-агрегаты по орг-иерархии (см. таблицу выше). На этих строках заполняется только имя соответствующего уровня иерархии; per-Building поля и большинство per-секционных колонок остаются пустыми.

Агрегируются (суммой по листовым потомкам, отдельно по каждому слоту `CalcKind`) только показатели потребления:

- секция «Расход ЭЭ» — «Общее потребление ЭЭ зданием, кВт·ч»;
- секция «Расход ТЭ» — «Общее потребление ТЭ зданием, Гкал».

Остальные показатели (удельные характеристики, отклонение, класс энергоэффективности, материалы / теплопроводности / толщины стен и ВОК) по орг-иерархии **не агрегируются** — ячейки остаются пустыми.

**Расчёт агрегатов выполняется Excel-формулами в самом файле отчёта.** В `NtrR01Report` суммируемые поля на агрегатных строках имеют значение `null`; `INtrR01ReportExcelRenderer` при выводе строки-агрегата подставляет в эти ячейки формулу `SUM(...)`, диапазон которой собирается из Excel-номеров строк листовых потомков (рендер ведёт `SectionRow → ExcelRowNumber` при preorder-обходе дерева). Это даёт пользователю прозрачность (формула видна в ячейке) и снимает с генератора арифметику.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplVyborkaModeleyPotrebleniya : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 130;
        public string Number => "";
        public string Name => "Выборка моделей потребления";

        public string Body => """"
- **Задание на сбор энергопрофилей** создаётся на конкретный отчётный период. В рамках одного Задания на каждое Здание создаётся один Энергопрофиль; Энергопрофиль ссылается на 4 Модели потребления — по одной на каждый `CalcKind` (Нормативный/Эталонный/Технологический/Произвольный; «Базовый» в Энергопрофиль не входит).
- **У одного Здания могут существовать Энергопрофили в разных Заданиях** — за разные отчётные периоды, в разных Заданиях. Выбор `CollectionTaskId` в параметрах отчёта однозначно выбирает один Энергопрофиль на Здание из этого Задания.
- **Определение слота модели.** Принадлежность Модели потребления к слоту `CalcKind` определяется по тому, какой именно ссылкой Энергопрофиля (`NormativeModelId` / `StandardModelId` / `TechnologicalModelId` / `CustomModelId`) она указана. Поле `ConsumptionModel.CalcKindId` для отчёта не используется — слот задаётся именно ссылкой из Энергопрофиля.
- **Сужение по `BuildingId`.** Если `GenerateNtrR01ReportParams.BuildingId` задан — выборка сужается до Энергопрофиля этого Здания в выбранном Задании. Иначе берутся все Здания под узлом `NavId` и их Энергопрофили в выбранном Задании.
- **Фильтр `EnergyProfileStatusIds`** («Включать энергопрофили в статусах»): если список не пуст — фильтруем `EpEnergyProfile.StatusId` по этим Id напрямую; если пуст — фильтр не применяется (включаются все статусы).
- **Фильтр `EnergyProfileConditionsStatusIds`** («Включать энергопрофили со статусом выполнения условий расчётов»): если список не пуст — фильтруем `EpEnergyProfile.ModelConsistencyStatusId` по этим Id напрямую; если пуст — фильтр не применяется.
- Неизвестные Id (не соответствующие ни одной известной доменной константе) валидацией не отсекаются — они просто не пройдут как фильтр по БД (ни одного профиля с таким Id не найдётся).
- **Порядок слотов** в шапке таблиц секций — по справочнику `CalcKind`: Нормативный → Эталонный → Технологический → Произвольный.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplValidaciyaParametrov : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 140;
        public string Number => "";
        public string Name => "Валидация параметров";

        public string Body => """"
- `NavId` обязателен (`Guid.Empty` недопустим).
- `CollectionTaskId` обязателен.
- `BuildingId` опционален; если задан — проверяется, что Здание принадлежит узлу `NavId` или его потомкам.
- При нарушении — `NtrR01ReportGenerationResult.Errors` заполняется, `Report` = `null`, `IsSuccess` = false.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplPustayaVyborka : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 150;
        public string Number => "";
        public string Name => "Пустая выборка";

        public string Body => """"
Если по выбранному Заданию и фильтрам не нашлось ни одного Энергопрофиля:

- `NtrR01Report` собирается с пустыми секциями (`Roots = []`) и заполненным `ReportParams`;
- в `Warnings` добавляется сообщение «По выбранному заданию и фильтрам не найдено энергопрофилей»;
- `IsSuccess` = true (это не ошибка).
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplExcelShablon : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 160;
        public string Number => "";
        public string Name => "Excel-шаблон";

        public string Body => """"
`Templates/ntr-r01-template.xlsx` — встроенный ресурс. 6 листов: 5 листов секций (имена = `Section.TabName` из `SectionMetadata`) + лист «Параметры отчёта». На каждом листе данных — sheet-scoped именованная ячейка `DataAnchor`, с которой начинается вывод строк. Форматы чисел и дат заданы прямо в шаблоне на колонках (в каждой колонке однотипные значения — формат привязан к колонке шаблона). На листе «Параметры отчёта» — именованные ячейки `Param_DateOfCreation`, `Param_NavObject`, `Param_BuildingObject`, `Param_CollectionTask`, `Param_StatusFilter`, `Param_ConditionsStatusFilter`. Рендер обходит дерево `SectionRow` preorder'ом, по ходу записи запоминает Excel-номер строки каждого узла и для агрегатов формирует Excel-формулы `SUM` по Excel-номерам своих листовых потомков.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }

    /// <summary>Секция отчёта NTR-R01.</summary>
    public class ImplReadKontekst : ISdaidDocumentSection
    {
        public Type Document => typeof(DOC_NtrR01CodeMap);
        public Type Parent => typeof(ImplArhitekturnyeResheniya);
        public int Order => 170;
        public string Number => "";
        public string Name => "Read-контекст";

        public string Body => """"
Используется существующий `NtrReadDbContext` (`src/Infrastructure.Read/`). Основные `DbSet`:

- `CmConsumptionModels`, `CmOperatingConditions`, `CmShapeAndDimensions`, `CmWallsModels`, `CmRoofModels`, `CmCalcKinds`, `CmCalcStatuses`;
- `EpEnergyProfiles`, `EpEnergyProfileCollectionTasks`, `EpEnergyProfileStatuses`, `EpEnergyProfileCollectionTaskStatuses`;
- `RegBuildings`;
- `Navs`, `NavCompanies`, `NavIa`, `NavMeses`, `NavPmeses`, `NavSubstations`;
- справочники: `RefClimateRegion`, `RefBuildingPurpose`, `RefHeatingType`, `RefMaterial`, `RefInsulationMaterial`, `RefEnergyEfficiencyClass`.
"""";

        public string Version => "";
        public string Status => "";
        public string[] Comments => new string[0];
    }
}