⟨/⟩ Nts/35_ChangeRequests/DOC_CR_2026_11_05_ActualConsumptionAndCosts.cs

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

using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Nts.ChangeRequests
{
    /// <summary>
    /// Учёт фактического потребления и затрат на автомобильное топливо: периодическая загрузка
    /// из АИС ОИА ПЭО и вывод в UI ЭНТС рядом с нормативным расходом.
    /// </summary>
    public class DOC_CR_2026_11_05_ActualConsumptionAndCosts : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtsChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.BC_Nts>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Infrastructure.AisOiaPeo.DOC_NtsAisImport>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleCard>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_NtsAisImportPage>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_NtsImportRun>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Vision.DOC_NtsVision>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.EPIC_NtsFuelCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Componetns.DOC_ActualConsumptionTab>(),
        };

        public static string _refName = "CR 2026-11-05: фактическое потребление и затраты";

        public string Name => "Учёт фактического потребления и затрат";

        public string Description =>
            @"Периодическая загрузка данных о фактическом потреблении автомобильного топлива и затратах из АИС ОИА ПЭО, их хранение по ТС и периоду, вывод в карточке ТС и в сводной статистике рядом с нормативным расходом.";

        public string Version => "0.1";
        public string Status => "в работе";
        public string[] Comments => new string[0];

        public static string Source = """
            ## Источник (как есть)

            загрузка периодических данных о фактических потреблении автомобильного топлива и
            затратах из АИС ОИА ПЭО: процедура загрузки и структуры хранения в ЭНТС
            вывод данных в UI ЭНТС

            SQL-запрос, показывающий какие и как берутся данные из АИС:

            ```sql
            --Фактическое потребление Бензина/ДТ и затраты
            --doc_ter_cars_petrol__values/disel__values
            --doc_ter_cars_petrol/disel
            --ref_car_points_petrol/disel
            select
                   p.car_fk as ais_vehicle_id,
                   d.year,
                   v.period_fk as ais_period_id,
                   sum(v.consume_miliage) as consumption_per_mileage_l,
                   sum(v.consume_hours) as consumption_per_hour_l,
                   sum(v.consume_avr) as consumption_per_avr_l,
                   sum(v.miliage) as miliage_tkm,
                   sum(v.hours) as hours_h,
                   sum(v.expenses) as expenses_trub
            from doc_ter_cars_petrol__values as v
                left join ref_periods periods on periods.id = v.period_fk
                left join doc_ter_cars_petrol d on d.id = v.docid
                left join ref_car_points_petrol p on p.id = d.car_point_fk
                left join navs on navs.id = d.navid
            where 1=1
                  and navs.o_fsk_name like('ПАО "Россети"')
                  and d.year >= 2025
                and periods.type = 'month'
            group by ais_vehicle_id, year, ais_period_id
            order by ais_vehicle_id, year, ais_period_id
            ;
            ```

            Запрос приведён для бензина (`doc_ter_cars_petrol*`); для ДТ — параллельный набор таблиц
            `doc_ter_cars_disel*`, `ref_car_points_disel`, та же структура колонок. Алиасы `tkm`/
            `trub` — суффикс `t` означает «тысяча»: `miliage` — тыс. км, `expenses` — тыс. руб.

            Справочник периодов АИС, на который ссылается `period_fk`:

            ```sql
            --Periods
            --Периоды
            select
                d.id as ais_id,
                d.name,
                d.code,
                d.number,
                d.type
            from ref_periods d
            ;
            ```
            """;

        public static string Analysis = $"""
            ## Разбор

            1. **Источник АИС.** Данные берутся из таблиц АИС ОИА ПЭО
               `doc_ter_cars_petrol`/`doc_ter_cars_disel` (документ по ТС) и их табличных частей
               `doc_ter_cars_petrol__values`/`doc_ter_cars_disel__values` (значения по периоду),
               с привязкой ТС через `ref_car_points_petrol`/`ref_car_points_disel`. Бензин и ДТ —
               две параллельные пары таблиц одной структуры, не различающее поле внутри одной
               таблицы. Отбор — по организации (`navs.o_fsk_name`) и году (`d.year`); период —
               `ref_periods` с `type = 'month'`. На ключ «ТС + год + период» из таблицы `__values`
               агрегируются (`sum`) по ТС, году и периоду: `consume_miliage`, `consume_hours`,
               `consume_avr` (три компоненты фактического расхода топлива, л — по пробегу, по
               моточасам, по АВР — аварийным и ремонтным работам), `miliage` (пробег), `hours`
               (моточасы), `expenses` (затраты). Единицы: `miliage` — тыс. км (`tkm`), `expenses` —
               тыс. руб. (`trub`); `hours` — часы, множителя нет.
            2. **Структура хранения.** Новая сущность `ActualConsumption`
               (`Nts/50_Domain/ActualConsumption.cs`), по аналогии со связями
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()} на
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>()} — ссылками на
               сущности, не на `Guid`/примитивы напрямую: ссылка на
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>()} (сопоставление по
               `ais_vehicle_id`/`car_fk`), ссылка на вид топлива (бензин/ДТ — определяется тем, из
               какой пары таблиц АИС загружена запись, отдельным полем в АИС не хранится), ссылка на
               **период** — отдельный справочник `Period` (`ref_periods` в АИС: `ais_id`, `name`,
               `code`, `number`, `type`), не примитивное поле: `ref_periods.type = 'month'` отбирает
               только периоды нужной детализации, сам справочник может содержать периоды и другой
               детализации. Плюс год (`d.year` в источнике — отдельное поле, не выводится из
               `Period`), три компоненты фактического расхода топлива (по пробегу/по моточасам/по
               АВР, л), пробег за период (тыс. км), моточасы за период (ч), затраты за период
               (тыс. руб.). Ключ записи — «ТС + год + период + вид топлива».

            """ + """
               ```mermaid
               classDiagram
                   class Vehicle {
                       +Guid Id
                       +Guid AisId
                   }

                   class FuelKind {
                       +Guid Id
                       +string Name
                   }

                   class Period {
                       +Guid Id
                       +Guid AisId
                       +string Name
                       +string Code
                       +int Number
                       +string Type
                   }

                   class ActualConsumptionLoadStatus {
                       +Guid Id
                       +string Name
                   }

                   class ActualConsumption {
                       +Guid Id
                       +Vehicle Vehicle
                       +FuelKind FuelKind
                       +Period Period
                       +int Year
                       +decimal ConsumptionByMileage
                       +decimal ConsumptionByHours
                       +decimal ConsumptionByAvr
                       +decimal Mileage
                       +decimal Hours
                       +decimal Expenses
                       +ActualConsumptionLoadStatus LoadStatus
                   }

                   ActualConsumption --> Vehicle
                   ActualConsumption --> FuelKind
                   ActualConsumption --> Period
                   ActualConsumption --> ActualConsumptionLoadStatus
               ```

            """ + $"""
            3. **Процедура загрузки.** Контракт `DOC_NtsActualConsumptionImport`
               (`Nts/70_Infrastructure/AisOiaPeo/ActualConsumptionImport.cs`), отдельный от
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Infrastructure.AisOiaPeo.DOC_NtsAisImport>()}.
               Источники — `doc_ter_cars_petrol`/`disel`, `__values`, `ref_car_points_*`; результат
               — записи `ActualConsumption` (см. п.2) по ключу «ТС + год + период + вид топлива».

               Точки входа — те же, что у {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Infrastructure.AisOiaPeo.DOC_NtsAisImport>()}:
               пакетная — со страницы «Импорт»
               ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_NtsAisImportPage>()}), роль
               «Администратор»; точечная, по своему ТС — из карточки ТС
               ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleCard>()}), роль «Владелец
               ТС». Запуск — по требованию пользователя, без расписания.

               Загружаются данные за три последних завершённых года плюс прошедшие месяцы текущего
               года. Данные за период, уже имеющиеся в `ActualConsumption`, перезаписываются
               загружаемыми без накопления дублей. Статус загрузки — отдельная сущность, не
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_NtsAisLoadStatus>()} (тот — про
               готовность ТС к расчёту, не про загрузку периодических данных).
            4. **Ролевая модель — переименование и новая роль, на весь модуль ЭНТС.** Текущая роль
               «Пользователь» ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Vision.DOC_NtsVision>()},
               «Роли пользователей») переименовывается в «Владелец ТС» без изменения объёма прав —
               вносит данные по ТС и организационным объектам в своём периметре доступа (подчинённым
               своего орг. объекта). Заводится новая роль «Пользователь» — read-only: видит всё, не
               меняет ничего. Решение распространяется на весь модуль ЭНТС, не только на функционал
               этой CR — на все операции, которые сейчас выполняет роль «Пользователь» (подбор
               модификации, ввод параметров методики, запуск расчёта).
            5. **Новый экран — вкладка «Расход автотоплива».** Назначение — показать загруженные
               месячные факты потребления и объяснить исходные данные для расчёта аналитических показателей:
               «Фактический расход» в карточке ТС (п.6), «Фактический расход» сводной
               статистики (п.7). Только свод над загруженными фактами (сумма/группировка по оргструктуре,
               году, периоду, виду топлива). Рядом с вкладкой «Реестр ТС»
               ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>()})
               заводится вкладка «Расход автотоплива». К уже принятым в
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>()} переключателям
               «С потомками / Без потомков» и «Все / Действующие» (переиспользуются без изменений)
               добавляются: выбор периодичности отображения (месяц / год) и выбор отображаемых
               параметров — состав определён полями `ActualConsumption` (п.2): Расход на пробег,
               Расход на моточасы, Расход на АВР, Пробег, Моточасы, Затраты.
            6. **«Фактический расход» — расчётный показатель `VehicleCalc`.** По тому же правилу,
               что и остальные расчётные параметры карточки (вход, результат, подсказка) — новое
               поле {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()}:
               `ActualConsumption`, л, `Calculated = "Да"`. Вход — записи `ActualConsumption` (п.2)
               этого ТС за отчётный год: три компоненты расхода (по пробегу/по моточасам/по АВР),
               каждая — сумма месячных записей года. Результат — одно число: свод трёх годовых
               компонент в «Фактический расход»; формула свода — открытый вопрос ниже (OQ-E), до его
               решения поле не вычисляется. Нет записей `ActualConsumption` за год — прочерк, как и
               у прочих расчётных полей без данных. Подсказка — та же иконка-алгоритм, что у прочих
               расчётных полей, с формулой свода после решения OQ-E.

               В {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleCard>()} поле выводится в
               группе «Результаты расчёта», рядом с Нормативным и Эталонным расходом, только чтение.
               Периодная детализация — в отдельной вкладке «Расход автотоплива» (п.5); в карточке
               ТС — сравнение одним числом с нормативным/эталонным, ровно то сравнение, которое
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Vision.DOC_NtsVision>()} называет целью
               модуля.
            7. **Сводная статистика — закрывает зарезервированное поле.** В
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>()} поле
               `ActualConsumption` уже заведено как резерв «источник фактических данных не
               определён». Эта CR вводит источник — `ActualConsumption`; период агрегации — тот же
               год, что и в п.6 (сумма месячных); свод трёх компонент — тот же открытый вопрос.
            8. **Затраты — только на топливо, одной суммой.** `expenses` — затраты на расход
               топлива за период; не включают прочие статьи (ГСМ, обслуживание). Источник отдаёт
               одно значение на пробег, моточасы и АВР вместе — без разбивки по этим трём
               компонентам, как `consume_miliage`/`consume_hours`/`consume_avr` для расхода.
               Структура хранения заводит одно поле «затраты за период» (п.2).

            ## Открытые вопросы (для пользователя — не домысливаются)

            - **OQ-C. Общая история запусков.** Переиспользовать для запусков загрузки фактического
              потребления ту же сущность {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_NtsImportRun>()}
              (добавив измерение «вид данных»: характеристики ТС / фактическое потребление), или
              завести отдельную историю запусков?
            - **OQ-E. Свод компонент расхода.** Как три компоненты расхода (по пробегу/моточасам/
              АВР), каждая просуммированная за год, сводятся в одно число «Фактический расход»
              для карточки ТС (п.6) и
              {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>()} (п.7) —
              сумма всех трёх компонент, или иначе?
            """;

        public static string BlastRadius = $"""
            ## Blast radius

            ### Спецификация

            | Документ | Правка |
            |---|---|
            | `Nts/50_Domain/ActualConsumption.cs` | новая сущность: ссылки на ТС/вид топлива/период (`Period` — новый справочник), год, три компоненты расхода (по пробегу/моточасам/АВР), пробег, моточасы, затраты за период, статус загрузки пакета |
            | `Nts/50_Domain/Period.cs` | новый справочник периодов АИС (`ref_periods`): `AisId`, `Name`, `Code`, `Number`, `Type` |
            | `Nts/50_Domain/BC_Nts.cs` | добавить `Rel.Uses<DOC_ActualConsumption>()`, `Rel.Uses<DOC_Period>()` в состав домена |
            | `Nts/50_Domain/VehicleSummaryStatistics.cs` | снять резерв с `ActualConsumption` после решения OQ-E (не в этой CR — отдельный шаг) |
            | `Nts/70_Infrastructure/AisOiaPeo/ActualConsumptionImport.cs` | новый контракт периодической загрузки из `doc_ter_cars_petrol`/`disel` (+ `__values`, `ref_car_points_*`): ключ сопоставления, статус пакета, модель `(nav, car?)` по аналогии с `DOC_NtsAisImport` — детали после ответа на OQ-C |
            | `Nts/50_Domain/NtsImportRun.cs` | возможно — добавить измерение «вид данных» для переиспользования под запуски загрузки фактического потребления (решение — OQ-C) |
            | `Nts/50_Domain/VehicleCalc.cs` | новое расчётное поле `ActualConsumption`, л (`Calculated = "Да"`) — свод трёх компонент `ActualConsumption`-записей (п.2) за год; формула свода — после OQ-E |
            | `Nts/65_Ui/VehicleCard.cs` | в группе «Результаты расчёта» — поле «Фактический расход, л» (= `VehicleCalc.ActualConsumption`), только чтение, рядом с Нормативным/Эталонным расходом, с иконкой подсказки-алгоритма |
            | `Nts/65_Ui/ActualConsumptionTab.cs` | новая вкладка «Расход автотоплива» рядом с «Реестр ТС»: свод по оргструктуре и году по всем параметрам, с измерением «Вид топлива»; по аналогии с `Ntr/65_Ui/Componetns/ActualConsumptionTab.cs` — детальная спека отдельным шагом |
            | `Nts/10_Vision/NtsVision.cs` | раздел «Роли пользователей» — переименование «Пользователь» → «Владелец ТС», новая read-only роль «Пользователь» (действует на весь модуль ЭНТС, п.4); раздел «Модель данных»/«Пользовательский интерфейс» — ссылка на новую сущность, контракт и вкладку |
            | `Nts/60_Epics/NtsFuelCalc/EPIC_NtsFuelCalc.cs` | новая OQ-секция после ответа пользователя на OQ-C, OQ-E (не добавляется этой CR без ответа) |

            ### Прототип

            Прототип (`Nts/65_Ui/html/vehicle-card.html`, `vehicle-registry.html`, общий `shell.js`)
            не трогается до согласования макетов новой вкладки и поля карточки ТС — по правилу
            «UI-изменения сначала макет».
            """;

        public static string Plan = """
            ## План

            1. Получить ответы пользователя на OQ-C, OQ-E.
            2. Спека: `ActualConsumption.cs`, `Period.cs`, `BC_Nts.cs`, `ActualConsumptionImport.cs`,
               правка `VehicleCalc.cs` (поле `ActualConsumption`) и `VehicleCard.cs` (поле
               «Фактический расход»), новая `ActualConsumptionTab.cs`, правка `NtsVision.cs` (роли),
               при необходимости `VehicleSummaryStatistics.cs` и `NtsImportRun.cs`.
            3. Макеты — поле в карточке ТС и новая вкладка «Расход автотоплива» — показать
               пользователю, получить «ок».
            4. Обновить прототипы (`vehicle-card.html`, новый `actual-consumption-tab.html`,
               `shell.js`) по согласованным макетам.
            5. Собрать (`dotnet build`, `build-html.cmd`).
            6. `doc`/`testovich` — отдельным проходом.
            7. Зафиксировать «Итог».
            """;

        public static string Result = """
            ## Итог

            - Разбор архитектурных решений (структура хранения, контракт загрузки, модель запуска,
              ролевая модель, вкладка свода, расчётное поле в карточке ТС, связь с зарезервированным
              полем сводной статистики, состав затрат) — выполнено; реализация зависит от OQ-C, OQ-E.
            - Доменная сущность `ActualConsumption` — в работе, не создана.
            - Контракт загрузки `DOC_NtsActualConsumptionImport` — в работе, не создан.
            - Поле `VehicleCalc.ActualConsumption` и его вывод в карточке ТС — в работе, не созданы.
            - Вкладка «Расход автотоплива» — в работе, спека и макет не созданы.
            - Снятие резерва `ActualConsumption` в сводной статистике — в работе.
            - Ролевая модель (переименование «Пользователь» → «Владелец ТС», новая read-only роль
              «Пользователь», на весь модуль ЭНТС) — в работе, `NtsVision.cs` не правится этой CR.
            """;
    }
}