Как спроектировать универсальный документ для расчета услуг ЖКХ в 1С и избежать перегрузки интерфейса

Программист 1С v8.3 (Управляемые формы) Управленческий учет Сфера услуг, туризм и социальный сектор
← На главную

Разработка подсистемы расчета начислений для сферы ЖКХ в среде «1С:Предприятие 8» — одна из сложнейших архитектурных задач. Нам необходимо учесть огромное разнообразие услуг (отопление, электроэнергия, водоснабжение, водоотведение, вывоз ТКО, интернет, телефония), каждая из которых рассчитывается по своим уникальным алгоритмам (по индивидуальным приборам учета, общедомовым нуждам, нормативам потребления, площади помещения, количеству проживающих или по фиксированным тарифам). Прямолинейное решение — создать один универсальный документ и вывести все возможные реквизиты в одну табличную часть — неизбежно приводит к созданию интерфейсного «монстра» с десятками колонок, который невозможно поддерживать, заполнять и тестировать.

В этой статье мы подробно проанализируем различные архитектурные паттерны проектирования документов расчета услуг ЖКХ, взвесим их плюсы и минусы, разберем опыт тиражных решений и спроектируем оптимальную схему данных с примерами программного кода.

---

Вариант 1. Создание отдельных документов под каждый вид услуги

Самое очевидное решение, которое часто выбирают на начальном этапе разработки, — создать в конфигураторе отдельные документы: НачислениеЭлектроэнергии, НачислениеВодоснабжения, НачислениеОтопления и так далее. На первый взгляд это кажется логичным, поскольку интерфейс каждого документа получается простым и содержит только специфические для данной услуги реквизиты.

Преимущества:

Недостатки:

---

Вариант 2. Связанные табличные части в одном документе (Основная + Вспомогательная)

Этот паттерн активно применяется в сложных типовых конфигурациях фирмы 1С (например, в «1С:Зарплата и управление персоналом»). Суть подхода заключается в разделении информации на две или более табличные части в рамках одного универсального документа:

  1. Основная табличная часть (например, Начисления): содержит только универсальные поля, необходимые для финансового учета и формирования движений по регистрам. Это лицевой счет, услуга, объем, тариф и итоговая сумма.
  2. Вспомогательная табличная часть (например, ПараметрыРасчета): содержит показатели, которые использовались для вычисления объема и суммы (показания приборов учета, нормативы, площади, формулы расчета). Записи в этой ТЧ связываются со строками основной ТЧ по уникальному ключу связи (GUID).

Разберем программную реализацию такого механизма. Ниже представлен пример заполнения связанных табличных частей в модуле объекта документа:


// Процедура заполняет основную и подчиненную табличные части документа
Процедура РассчитатьУслугиЖКХ() Экспорт

    ЭтотОбъект.Начисления.Очистить();
    ЭтотОбъект.ПараметрыРасчета.Очистить();

    // Получим выборку лицевых счетов и актуальных тарифов (упрощенный пример)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ЛицевыеСчета.Ссылка КАК ЛицевойСчет,
        |   ЛицевыеСчета.Площадь КАК Площадь,
        |   ТарифыУслугСрезПоследних.Услуга КАК Услуга,
        |   ТарифыУслугСрезПоследних.Цена КАК Тариф
        |ИЗ
        |   Справочник.ЛицевыеСчета КАК ЛицевыеСчета
        |       ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ТарифыУслуг.СрезПоследних КАК ТарифыУслугСрезПоследних
        |       ПО (ИСТИНА)";
    
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();

    Пока Выборка.Следующий() Цикл
        
        // 1. Создаем строку в основной табличной части
        СтрокаНачисления = ЭтотОбъект.Начисления.Добавить();
        СтрокаНачисления.КлючСвязи = Новый УникальныйИдентификатор();
        СтрокаНачисления.ЛицевойСчет = Выборка.ЛицевойСчет;
        СтрокаНачисления.Услуга = Выборка.Услуга;
        СтрокаНачисления.Тариф = Выборка.Тариф;
        
        // Пример расчета услуги по площади помещения
        Если Выборка.Услуга = Справочники.УслугиЖКХ.Отопление Тогда
            СтрокаНачисления.Объем = Выборка.Площадь;
            СтрокаНачисления.Сумма = СтрокаНачисления.Объем * СтрокаНачисления.Тариф;
            
            // 2. Записываем расчетный показатель во вспомогательную ТЧ
            СтрокаПараметра = ЭтотОбъект.ПараметрыРасчета.Добавить();
            СтрокаПараметра.КлючСвязи = СтрокаНачисления.КлючСвязи;
            СтрокаПараметра.ИмяПараметра = "ПлощадьПомещения";
            СтрокаПараметра.ЗначениеПараметра = Выборка.Площадь;
        КонецЕсли;
        
    КонецЦикла;

КонецПроцедуры

Преимущества:

Недостатки:

---

Вариант 3. Хранение параметров в JSON (NoSQL подход внутри 1С)

Интересный альтернативный подход — хранить всю специфическую детализацию расчета непосредственно в основной табличной части, но не в отдельных колонках, а упакованной в одно строковое поле неограниченной длины в формате JSON или в виде сериализованного Хранилища Значения. Это позволяет реализовать концепцию NoSQL прямо в реляционной СУБД силами платформы 1С.

Рассмотрим пример функций для сериализации и десериализации параметров расчета строки начисления:


// Преобразует параметры расчета в JSON-строку для хранения в ТЧ документа
Функция УпаковатьПараметрыВJSON(НачальноеПоказание, КонечноеПоказание, Норматив) Экспорт

    СтруктураПараметров = Новый Структура;
    СтруктураПараметров.Вставить("ПоказаниеНачальное", НачальноеПоказание);
    СтруктураПараметров.Вставить("ПоказаниеКонечное", КонечноеПоказание);
    СтруктураПараметров.Вставить("НормативПотребления", Норматив);

    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();
    ЗаписатьJSON(ЗаписьJSON, СтруктураПараметров);
    
    Возврат ЗаписьJSON.Закрыть();

КонецФункции

// Восстанавливает структуру параметров из JSON-строки для анализа или вывода на форму
Функция РаспаковатьПараметрыИзJSON(СтрокаJSON) Экспорт

    Если ПустаяСтрока(СтрокаJSON) Тогда
        Возврат Новый Структура;
    КонецЕсли;

    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
    Результат = ПрочитатьJSON(ЧтениеJSON);
    
    Возврат Результат;

КонецФункции

Преимущества:

Недостатки:

---

Вариант 4. Промышленный паттерн: Разделение ввода данных и расчета (АРМ + Регистры Сведений)

Анализ архитектуры крупных промышленных решений для ЖКХ (таких как системы от «Инфокрафт» или «1С-Рарус») показывает, что наиболее жизнеспособным является паттерн жесткого разделения ввода первичных данных и самого расчета. Главная ошибка начинающих разработчиков — попытка заставить пользователя вводить показания счетчиков непосредственно в форму документа начисления.

Проанализируем правильный жизненный цикл данных при начислении коммунальных услуг:

  1. Ввод показаний счетчиков: Для этого создается отдельный сверхпростой документ (например, ВводПоказанийСчетчиков). Его задача — зафиксировать дату, лицевой счет, прибор учета и текущее показание. При проведении этот документ делает запись в периодический регистр сведений ПоказанияПриборовУчета. Он не занимается расчетами стоимости.
  2. Ввод параметров лицевых счетов: Площади квартир, количество зарегистрированных и проживающих жильцов, наличие льгот хранятся в соответствующих независимых периодических регистрах сведений (ХарактеристикиЛицевыхСчетов, СоставПроживающих).
  3. Начисление услуг: Создается один документ НачислениеУслугЖКХ. Его табличная часть чрезвычайно проста и плоская: ЛицевойСчет, Услуга, Объем, Тариф, Сумма. Заполнение и расчет этого документа происходят полностью автоматически по нажатию кнопки на форме или через специализированное автоматизированное рабочее место (АРМ).

Рассмотрим пример кода расчетного механизма в общем модуле или модуле менеджера документа начисления, который собирает все данные из регистров сведений и производит расчет:


// Расчет объема и стоимости услуг на основании данных из регистров
Процедура РассчитатьНачисленияПоЛицевымСчетам(ДокументСсылка, ПериодРегистрации) Экспорт

    ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
    ДокументОбъект.Начисления.Очистить();

    // Шаг 1. Получаем показания приборов учета за расчетный период
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ПоказанияСрезПоследних.ПриборУчета КАК ПриборУчета,
        |   ПоказанияСрезПоследних.ПриборУчета.ЛицевойСчет КАК ЛицевойСчет,
        |   ПоказанияСрезПоследних.ПриборУчета.Услуга КАК Услуга,
        |   ПоказанияСрезПоследних.Показание КАК ПоказаниеКонечное,
        |   ЕСТЬNULL(ПоказанияПредыдущие.Показание, 0) КАК ПоказаниеНачальное,
        |   ЕСТЬNULL(Тарифы.Цена, 0) КАК Тариф
        |ИЗ
        |   РегистрСведений.ПоказанияПриборовУчета.СрезПоследних(&КонецПериода, ) КАК ПоказанияСрезПоследних
        |       ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПоказанияПриборовУчета.СрезПоследних(&НачалоПериода, ) КАК ПоказанияПредыдущие
        |       ПО ПоказанияСрезПоследних.ПриборУчета = ПоказанияПредыдущие.ПриборУчета
        |       ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ТарифыУслуг.СрезПоследних(&КонецПериода, ) КАК Тарифы
        |       ПО ПоказанияСрезПоследних.ПриборУчета.Услуга = Тарифы.Услуга";
        
    Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ПериодРегистрации));
    Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ПериодРегистрации));
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Вычисляем объем потребления по разности показаний счетчика
        Расход = Выборка.ПоказаниеКонечное - Выборка.ПоказаниеНачальное;
        
        Если Расход > 0 Тогда
            НоваяСтрока = ДокументОбъект.Начисления.Добавить();
            НоваяСтрока.ЛицевойСчет = Выборка.ЛицевойСчет;
            НоваяСтрока.Услуга = Выборка.Услуга;
            НоваяСтрока.Объем = Расход;
            НоваяСтрока.Тариф = Выборка.Тариф;
            НоваяСтрока.Сумма = Расход * Выборка.Тариф;
        КонецЕсли;
        
    КонецЦикла;
    
    ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);

КонецПроцедуры

---

Визуализация данных на форме: Кросс-таблицы (Динамические колонки)

Даже если в базе данных табличная часть хранится в строго нормализованном («вертикальном») виде (одна строка — одна услуга), пользователю для удобства анализа часто требуется видеть начисления в «горизонтальном» виде, где лицевые счета идут по строкам, а услуги — колонками.

Для решения этой задачи в управляемом приложении 1С используется механизм динамического конструирования колонок формы при ее создании или обновлении. Нам не нужно добавлять реквизиты в метаданные документа. Мы программно генерируем элементы интерфейса на клиенте на основе кросс-таблицы, полученной с сервера.

Проанализируем пример кода для программного добавления колонок услуг на форму документа начисления:


&НаСервере
Процедура СформироватьДинамическуюТаблицуНаФорме()

    // 1. Получаем список уникальных услуг, рассчитанных в документе
    УслугиДокумента = Объект.Начисления.ВыгрузитьКолонку("Услуга");
    СвернутьМассив(УслугиДокумента); // Оставляем только уникальные ссылки

    // 2. Добавляем колонку ЛицевойСчет, если она еще не создана
    ИмяРеквизитаТЗ = "ВременнаяТаблицаНачислений";
    
    МассивРеквизитов = Новый Массив;
    МассивРеквизитов.Добавить(Новый РеквизитФормы(ИмяРеквизитаТЗ, Новый ОписаниеТипов("ТаблицаЗначений")));
    МассивРеквизитов.Добавить(Новый РеквизитФормы("ЛицевойСчет", Новый ОписаниеТипов("СправочникСсылка.ЛицевыеСчета"), ИмяРеквизитаТЗ));
    
    // Создаем реквизиты под каждую услугу динамически
    Для Каждого ТекущаяУслуга Из УслугиДокумента Цикл
        ИмяКолонки = "Услуга_" + СтрЗаменить(Строка(ТекущаяУслуга.УникальныйИдентификатор()), "-", "_");
        МассивРеквизитов.Добавить(Новый РеквизитФормы(ИмяКолонки, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)), ИмяРеквизитаТЗ));
    КонецЦикла;
    
    ИзменитьРеквизиты(МассивРеквизитов);
    
    // 3. Создаем элементы управления на форме для отображения колонок
    ТаблицаФормы = Элементы.Добавить(ИмяРеквизитаТЗ, Тип("ТаблицаФормы"));
    ТаблицаФормы.ПутьКДанным = ИмяРеквизитаТЗ;
    
    КолонкаЛС = Элементы.Добавить("ТаблицаЛС", Тип("ПолеФормы"), ТаблицаФормы);
    КолонкаЛС.Вид = ВидПоляФормы.ПолеВвода;
    КолонкаЛС.ПутьКДанным = ИмяРеквизитаТЗ + ".ЛицевойСчет";
    
    Для Каждого ТекущаяУслуга Из УслугиДокумента Цикл
        ИмяКолонки = "Услуга_" + СтрЗаменить(Строка(ТекущаяУслуга.УникальныйИдентификатор()), "-", "_");
        НоваяКолонка = Элементы.Добавить(ИмяКолонки, Тип("ПолеФормы"), ТаблицаФормы);
        НоваяКолонка.Вид = ВидПоляФормы.ПолеВвода;
        НоваяКолонка.ПутьКДанным = ИмяРеквизитаТЗ + "." + ИмяКолонки;
        НоваяКолонка.Заголовок = Строка(ТекущаяУслуга);
    КонецЦикла;

КонецПроцедуры

// Вспомогательная процедура очистки массива от дублей
Процедура СвернутьМассив(Массив)
    Соответствие = Новый Соответствие;
    Для Каждого Элемент Из Массив Цикл
        Соответствие.Вставить(Элемент, Истина);
    КонецЦикла;
    Массив.Очистить();
    Для Каждого КлючИЗначение Из Соответствие Цикл
        Массив.Добавить(КлючИЗначение.Ключ);
    КонецЦикла;
КонецПроцедуры

---

Резюме и рекомендации для архитектора

Для создания надежной, быстродействующей и легко поддерживаемой биллинговой системы расчета ЖКХ в среде «1С:Предприятие 8» рекомендуем придерживаться следующих правил:

  1. Откажитесь от идеи создания «универсального комбайна» с ручным вводом всех показателей. Это приведет к усложнению кода формы, замедлению работы интерфейса и сделает невозможным масштабирование системы.
  2. Разделяйте ввод первичных данных и начисление стоимости. Ввод показаний приборов учета, площадей и льгот должен осуществляться в отдельные документы и регистры сведений. Документ начисления должен только считывать эти данные и рассчитывать итоговую стоимость.
  3. Табличная часть документа начисления должна быть максимально простой и плоской. Оставляйте в ней только универсальные учетные реквизиты: контрагент/лицевой счет, услуга, объем, тариф и сумма. Это упростит проведение документа, написание отчетов и выгрузку данных.
  4. Для удобства пользователя используйте динамическое отображение данных. Сворачивайте и разворачивайте плоские строки табличной части в удобные горизонтальные кросс-таблицы программно на форме управляемого приложения, не перегружая при этом физическую структуру СУБД.
← На главную