Разработка подсистемы расчета начислений для сферы ЖКХ в среде «1С:Предприятие 8» — одна из сложнейших архитектурных задач. Нам необходимо учесть огромное разнообразие услуг (отопление, электроэнергия, водоснабжение, водоотведение, вывоз ТКО, интернет, телефония), каждая из которых рассчитывается по своим уникальным алгоритмам (по индивидуальным приборам учета, общедомовым нуждам, нормативам потребления, площади помещения, количеству проживающих или по фиксированным тарифам). Прямолинейное решение — создать один универсальный документ и вывести все возможные реквизиты в одну табличную часть — неизбежно приводит к созданию интерфейсного «монстра» с десятками колонок, который невозможно поддерживать, заполнять и тестировать.
В этой статье мы подробно проанализируем различные архитектурные паттерны проектирования документов расчета услуг ЖКХ, взвесим их плюсы и минусы, разберем опыт тиражных решений и спроектируем оптимальную схему данных с примерами программного кода.
---
Самое очевидное решение, которое часто выбирают на начальном этапе разработки, — создать в конфигураторе отдельные документы: НачислениеЭлектроэнергии, НачислениеВодоснабжения, НачислениеОтопления и так далее. На первый взгляд это кажется логичным, поскольку интерфейс каждого документа получается простым и содержит только специфические для данной услуги реквизиты.
Преимущества:
Недостатки:
---
Этот паттерн активно применяется в сложных типовых конфигурациях фирмы 1С (например, в «1С:Зарплата и управление персоналом»). Суть подхода заключается в разделении информации на две или более табличные части в рамках одного универсального документа:
Начисления): содержит только универсальные поля, необходимые для финансового учета и формирования движений по регистрам. Это лицевой счет, услуга, объем, тариф и итоговая сумма.ПараметрыРасчета): содержит показатели, которые использовались для вычисления объема и суммы (показания приборов учета, нормативы, площади, формулы расчета). Записи в этой ТЧ связываются со строками основной ТЧ по уникальному ключу связи (GUID).Разберем программную реализацию такого механизма. Ниже представлен пример заполнения связанных табличных частей в модуле объекта документа:
// Процедура заполняет основную и подчиненную табличные части документа
Процедура РассчитатьУслугиЖКХ() Экспорт
ЭтотОбъект.Начисления.Очистить();
ЭтотОбъект.ПараметрыРасчета.Очистить();
// Получим выборку лицевых счетов и актуальных тарифов (упрощенный пример)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЛицевыеСчета.Ссылка КАК ЛицевойСчет,
| ЛицевыеСчета.Площадь КАК Площадь,
| ТарифыУслугСрезПоследних.Услуга КАК Услуга,
| ТарифыУслугСрезПоследних.Цена КАК Тариф
|ИЗ
| Справочник.ЛицевыеСчета КАК ЛицевыеСчета
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ТарифыУслуг.СрезПоследних КАК ТарифыУслугСрезПоследних
| ПО (ИСТИНА)";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// 1. Создаем строку в основной табличной части
СтрокаНачисления = ЭтотОбъект.Начисления.Добавить();
СтрокаНачисления.КлючСвязи = Новый УникальныйИдентификатор();
СтрокаНачисления.ЛицевойСчет = Выборка.ЛицевойСчет;
СтрокаНачисления.Услуга = Выборка.Услуга;
СтрокаНачисления.Тариф = Выборка.Тариф;
// Пример расчета услуги по площади помещения
Если Выборка.Услуга = Справочники.УслугиЖКХ.Отопление Тогда
СтрокаНачисления.Объем = Выборка.Площадь;
СтрокаНачисления.Сумма = СтрокаНачисления.Объем * СтрокаНачисления.Тариф;
// 2. Записываем расчетный показатель во вспомогательную ТЧ
СтрокаПараметра = ЭтотОбъект.ПараметрыРасчета.Добавить();
СтрокаПараметра.КлючСвязи = СтрокаНачисления.КлючСвязи;
СтрокаПараметра.ИмяПараметра = "ПлощадьПомещения";
СтрокаПараметра.ЗначениеПараметра = Выборка.Площадь;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Преимущества:
Недостатки:
---
Интересный альтернативный подход — хранить всю специфическую детализацию расчета непосредственно в основной табличной части, но не в отдельных колонках, а упакованной в одно строковое поле неограниченной длины в формате JSON или в виде сериализованного Хранилища Значения. Это позволяет реализовать концепцию NoSQL прямо в реляционной СУБД силами платформы 1С.
Рассмотрим пример функций для сериализации и десериализации параметров расчета строки начисления:
// Преобразует параметры расчета в JSON-строку для хранения в ТЧ документа
Функция УпаковатьПараметрыВJSON(НачальноеПоказание, КонечноеПоказание, Норматив) Экспорт
СтруктураПараметров = Новый Структура;
СтруктураПараметров.Вставить("ПоказаниеНачальное", НачальноеПоказание);
СтруктураПараметров.Вставить("ПоказаниеКонечное", КонечноеПоказание);
СтруктураПараметров.Вставить("НормативПотребления", Норматив);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, СтруктураПараметров);
Возврат ЗаписьJSON.Закрыть();
КонецФункции
// Восстанавливает структуру параметров из JSON-строки для анализа или вывода на форму
Функция РаспаковатьПараметрыИзJSON(СтрокаJSON) Экспорт
Если ПустаяСтрока(СтрокаJSON) Тогда
Возврат Новый Структура;
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
Результат = ПрочитатьJSON(ЧтениеJSON);
Возврат Результат;
КонецФункции
Преимущества:
Недостатки:
---
Анализ архитектуры крупных промышленных решений для ЖКХ (таких как системы от «Инфокрафт» или «1С-Рарус») показывает, что наиболее жизнеспособным является паттерн жесткого разделения ввода первичных данных и самого расчета. Главная ошибка начинающих разработчиков — попытка заставить пользователя вводить показания счетчиков непосредственно в форму документа начисления.
Проанализируем правильный жизненный цикл данных при начислении коммунальных услуг:
ВводПоказанийСчетчиков). Его задача — зафиксировать дату, лицевой счет, прибор учета и текущее показание. При проведении этот документ делает запись в периодический регистр сведений ПоказанияПриборовУчета. Он не занимается расчетами стоимости.ХарактеристикиЛицевыхСчетов, СоставПроживающих).НачислениеУслугЖКХ. Его табличная часть чрезвычайно проста и плоская: ЛицевойСчет, Услуга, Объем, Тариф, Сумма. Заполнение и расчет этого документа происходят полностью автоматически по нажатию кнопки на форме или через специализированное автоматизированное рабочее место (АРМ).Рассмотрим пример кода расчетного механизма в общем модуле или модуле менеджера документа начисления, который собирает все данные из регистров сведений и производит расчет:
// Расчет объема и стоимости услуг на основании данных из регистров
Процедура РассчитатьНачисленияПоЛицевымСчетам(ДокументСсылка, ПериодРегистрации) Экспорт
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.Начисления.Очистить();
// Шаг 1. Получаем показания приборов учета за расчетный период
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПоказанияСрезПоследних.ПриборУчета КАК ПриборУчета,
| ПоказанияСрезПоследних.ПриборУчета.ЛицевойСчет КАК ЛицевойСчет,
| ПоказанияСрезПоследних.ПриборУчета.Услуга КАК Услуга,
| ПоказанияСрезПоследних.Показание КАК ПоказаниеКонечное,
| ЕСТЬNULL(ПоказанияПредыдущие.Показание, 0) КАК ПоказаниеНачальное,
| ЕСТЬNULL(Тарифы.Цена, 0) КАК Тариф
|ИЗ
| РегистрСведений.ПоказанияПриборовУчета.СрезПоследних(&КонецПериода, ) КАК ПоказанияСрезПоследних
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПоказанияПриборовУчета.СрезПоследних(&НачалоПериода, ) КАК ПоказанияПредыдущие
| ПО ПоказанияСрезПоследних.ПриборУчета = ПоказанияПредыдущие.ПриборУчета
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ТарифыУслуг.СрезПоследних(&КонецПериода, ) КАК Тарифы
| ПО ПоказанияСрезПоследних.ПриборУчета.Услуга = Тарифы.Услуга";
Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ПериодРегистрации));
Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ПериодРегистрации));
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Вычисляем объем потребления по разности показаний счетчика
Расход = Выборка.ПоказаниеКонечное - Выборка.ПоказаниеНачальное;
Если Расход > 0 Тогда
НоваяСтрока = ДокументОбъект.Начисления.Добавить();
НоваяСтрока.ЛицевойСчет = Выборка.ЛицевойСчет;
НоваяСтрока.Услуга = Выборка.Услуга;
НоваяСтрока.Объем = Расход;
НоваяСтрока.Тариф = Выборка.Тариф;
НоваяСтрока.Сумма = Расход * Выборка.Тариф;
КонецЕсли;
КонецЦикла;
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецПроцедуры
---
Даже если в базе данных табличная часть хранится в строго нормализованном («вертикальном») виде (одна строка — одна услуга), пользователю для удобства анализа часто требуется видеть начисления в «горизонтальном» виде, где лицевые счета идут по строкам, а услуги — колонками.
Для решения этой задачи в управляемом приложении 1С используется механизм динамического конструирования колонок формы при ее создании или обновлении. Нам не нужно добавлять реквизиты в метаданные документа. Мы программно генерируем элементы интерфейса на клиенте на основе кросс-таблицы, полученной с сервера.
Проанализируем пример кода для программного добавления колонок услуг на форму документа начисления:
&НаСервере
Процедура СформироватьДинамическуюТаблицуНаФорме()
// 1. Получаем список уникальных услуг, рассчитанных в документе
УслугиДокумента = Объект.Начисления.ВыгрузитьКолонку("Услуга");
СвернутьМассив(УслугиДокумента); // Оставляем только уникальные ссылки
// 2. Добавляем колонку ЛицевойСчет, если она еще не создана
ИмяРеквизитаТЗ = "ВременнаяТаблицаНачислений";
МассивРеквизитов = Новый Массив;
МассивРеквизитов.Добавить(Новый РеквизитФормы(ИмяРеквизитаТЗ, Новый ОписаниеТипов("ТаблицаЗначений")));
МассивРеквизитов.Добавить(Новый РеквизитФормы("ЛицевойСчет", Новый ОписаниеТипов("СправочникСсылка.ЛицевыеСчета"), ИмяРеквизитаТЗ));
// Создаем реквизиты под каждую услугу динамически
Для Каждого ТекущаяУслуга Из УслугиДокумента Цикл
ИмяКолонки = "Услуга_" + СтрЗаменить(Строка(ТекущаяУслуга.УникальныйИдентификатор()), "-", "_");
МассивРеквизитов.Добавить(Новый РеквизитФормы(ИмяКолонки, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)), ИмяРеквизитаТЗ));
КонецЦикла;
ИзменитьРеквизиты(МассивРеквизитов);
// 3. Создаем элементы управления на форме для отображения колонок
ТаблицаФормы = Элементы.Добавить(ИмяРеквизитаТЗ, Тип("ТаблицаФормы"));
ТаблицаФормы.ПутьКДанным = ИмяРеквизитаТЗ;
КолонкаЛС = Элементы.Добавить("ТаблицаЛС", Тип("ПолеФормы"), ТаблицаФормы);
КолонкаЛС.Вид = ВидПоляФормы.ПолеВвода;
КолонкаЛС.ПутьКДанным = ИмяРеквизитаТЗ + ".ЛицевойСчет";
Для Каждого ТекущаяУслуга Из УслугиДокумента Цикл
ИмяКолонки = "Услуга_" + СтрЗаменить(Строка(ТекущаяУслуга.УникальныйИдентификатор()), "-", "_");
НоваяКолонка = Элементы.Добавить(ИмяКолонки, Тип("ПолеФормы"), ТаблицаФормы);
НоваяКолонка.Вид = ВидПоляФормы.ПолеВвода;
НоваяКолонка.ПутьКДанным = ИмяРеквизитаТЗ + "." + ИмяКолонки;
НоваяКолонка.Заголовок = Строка(ТекущаяУслуга);
КонецЦикла;
КонецПроцедуры
// Вспомогательная процедура очистки массива от дублей
Процедура СвернутьМассив(Массив)
Соответствие = Новый Соответствие;
Для Каждого Элемент Из Массив Цикл
Соответствие.Вставить(Элемент, Истина);
КонецЦикла;
Массив.Очистить();
Для Каждого КлючИЗначение Из Соответствие Цикл
Массив.Добавить(КлючИЗначение.Ключ);
КонецЦикла;
КонецПроцедуры
---
Для создания надежной, быстродействующей и легко поддерживаемой биллинговой системы расчета ЖКХ в среде «1С:Предприятие 8» рекомендуем придерживаться следующих правил: