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