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.
""";
}
}