При автоматизации складского учета, производственных процессов или фасовки продукции разработчики часто сталкиваются с задачей расчета нарастающего итога или вычисления разницы между смежными строками в табличной части (ТЧ). Классический пример — непрерывное взвешивание емкости, в которую постоянно добавляют жидкость. Нам известны только текущие показания весов на каждом шаге, а в документе необходимо зафиксировать чистый вес каждого отдельного подливания.
В этой статье мы подробно разберем, как получить данные из предыдущей строки табличной части в управляемом приложении 1С 8.3, проанализируем возможные подводные камни (такие как интерактивная сортировка или редактирование строк «задним числом») и реализуем отказоустойчивый алгоритм расчета.
Самый очевидный способ, который первым приходит на ум — это обращение к коллекции строк табличной части по индексу текущей строки за вычетом единицы. Давайте рассмотрим, как выглядит такая реализация на встроенном языке 1С.
Для реализации этого метода мы привязываемся к событию изменения колонки, в которую пользователь или внешнее оборудование записывает показания весов. Предположим, что событие называется ПоказаниеВесовПриИзменении.
&НаКлиенте
Процедура ТЧПоказанияПоказаниеВесовПриИзменении(Элемент)
СтрокаТЧ = Элементы.ТЧПоказания.ТекущиеДанные;
Если СтрокаТЧ = Неопределено Тогда
Возврат;
КонецЕсли;
// Получаем индекс текущей строки в коллекции
ТекИндекс = Объект.ТЧПоказания.Индекс(СтрокаТЧ);
// Если это самая первая строка, разницу вычислять не с чем
Если ТекИндекс = 0 Тогда
СтрокаТЧ.Вес = СтрокаТЧ.ПоказаниеВесов;
Возврат;
КонецЕсли;
// Получаем непосредственно предыдущую строку по индексу
ПредСтрока = Объект.ТЧПоказания.Получить(ТекИндекс - 1);
// Вычисляем разницу
СтрокаТЧ.Вес = СтрокаТЧ.ПоказаниеВесов - ПредСтрока.ПоказаниеВесов;
КонецПроцедуры
В чем опасность этого подхода? Физический индекс строки в коллекции объекта Объект.ТЧПоказания жестко привязан к порядку добавления строк. Если пользователь кликнет по заголовку любой колонки на форме, платформа выполнит визуальную сортировку строк. При этом визуальный порядок на экране изменится, а физические индексы строк в коллекции останутся прежними! В результате выражение ТекИндекс - 1 вернет совершенно другую строку, которая визуально может находиться в конце или середине таблицы, что приведет к грубейшей ошибке в расчетах.
Если мы решаем использовать данный метод, нам необходимо строго заблокировать для пользователя возможность изменения порядка строк и сортировки таблицы. Для этого у элемента формы «Таблица» установим следующие свойства:
РазвешитьСортировку — снимаем флаг (устанавливаем в значение Ложь).РазрешитьИзменениеПорядкаСтрок — снимаем флаг (устанавливаем в значение Ложь).Еще один популярный способ — сохранение предыдущего показания в переменную модуля формы при добавлении или изменении строк. Мы можем объявить переменную на уровне модуля формы и последовательно обновлять её значение при переходе от строки к строке.
&НаКлиенте
Перем ПредыдущееПоказаниеВесов;
&НаКлиенте
Процедура ТЧПоказанияПоказаниеВесовПриИзменении(Элемент)
СтрокаТЧ = Элементы.ТЧПоказания.ТекущиеДанные;
Если СтрокаТЧ = Неопределено Тогда
Возврат;
КонецЕсли;
Если ПредыдущееПоказаниеВесов = Неопределено Тогда
// Инициализируем начальное значение для первой сессии ввода
ПредыдущееПоказаниеВесов = 0;
КонецЕсли;
СтрокаТЧ.Вес = СтрокаТЧ.ПоказаниеВесов - ПредыдущееПоказаниеВесов;
// Перезаписываем переменную для следующей строки
ПредыдущееПоказаниеВесов = СтрокаТЧ.ПоказаниеВесов;
КонецПроцедуры
Этот способ отлично показывает себя при быстром потоковом вводе данных сверху вниз (например, при непосредственном получении веса с COM-порта весов). Однако у него есть критический недостаток. Если пользователь решит отредактировать уже заполненную ранее таблицу или вернется в середину списка, чтобы исправить опечатку, значение переменной ПредыдущееПоказаниеВесов окажется некорректным или пустым. Вся цепочка последующих расчетов собьется.
Для того чтобы система работала стабильно вне зависимости от того, в каком порядке пользователь вводит данные, редактирует ли он старые строки или добавляет новые в середину таблицы, мы должны использовать каскадный пересчет всей табличной части в цикле.
Поскольку коллекция табличной части на клиенте обычно невелика (в пределах нескольких десятков или сотен строк), полный обход таблицы в цикле на клиенте выполняется мгновенно и абсолютно незаметно для пользователя. Напишем универсальную процедуру пересчета, которую будем вызывать при любом изменении ключевых полей.
&НаКлиенте
Процедура ТЧПоказанияПоказаниеВесовПриИзменении(Элемент)
ПересчитатьВесаВТаблице();
КонецПроцедуры
&НаКлиенте
Процедура ПересчитатьВесаВТаблице()
НакопленныйВес = 0;
Для Каждого СтрокаТЧ Из Объект.ТЧПоказания Цикл
Если СтрокаТЧ.НомерСтроки = 1 Тогда
// Для первой строки вес равен показанию (или за вычетом тары)
СтрокаТЧ.Вес = СтрокаТЧ.ПоказаниеВесов;
Иначе
// Для последующих строк вычитаем предыдущее накопленное значение
СтрокаТЧ.Вес = СтрокаТЧ.ПоказаниеВесов - НакопленныйВес;
КонецЕсли;
// Запоминаем текущее показание как базовое для следующего шага
НакопленныйВес = СтрокаТЧ.ПоказаниеВесов;
КонецЦикла;
КонецПроцедуры
Разберем преимущества каскадного метода:
Если вам все же требуется точечно получить исключительно предыдущую строку относительно выбранной, не прибегая к полному пересчету и индексам коллекции, в Управляемых Формах 1С можно использовать механизм уникальных идентификаторов строк. Элемент Элементы.ТЧПоказания.ТекущаяСтрока возвращает числовой идентификатор строки на форме.
Давайте посмотрим на пример точечного получения предыдущей строки по её идентификатору:
&НаКлиенте
Процедура РассчитатьРазницуДляТекущейСтроки()
ТекИдентификатор = Элементы.ТЧПоказания.ТекущаяСтрока;
Если ТекИдентификатор <> Неопределено И ТекИдентификатор > 0 Тогда
ТекущиеДанные = Объект.ТЧПоказания.НайтиПоИдентификатору(ТекИдентификатор);
// Пытаемся получить строку, созданную непосредственно перед текущей
ПредыдущиеДанные = Объект.ТЧПоказания.НайтиПоИдентификатору(ТекИдентификатор - 1);
Если ПредыдущиеДанные <> Неопределено Тогда
ТекущиеДанные.Вес = ТекущиеДанные.ПоказаниеВесов - ПредыдущиеДанные.ПоказаниеВесов;
Иначе
// Предыдущая строка не найдена (например, текущая строка первая в списке)
ТекущиеДанные.Вес = ТекущиеДанные.ПоказаниеВесов;
EndIf;
КонецЕсли;
КонецПроцедуры
Этот способ более технологичен, но все еще имеет ограничения при ручной сортировке строк пользователем, поэтому для учетных задач взвешивания и нарастающих итогов мы настоятельно рекомендуем использовать Вариант №3 (Каскадный пересчет).
При проектировании подобных систем взвешивания важно заложить архитектуру, которая предусматривает как автоматический режим работы, так и ручной ввод (так называемый «План Б» на случай выхода оборудования из строя):
Объект.ТЧПоказания.Добавить(), записываем в поле ПоказаниеВесов считанные данные и сразу же вызываем процедуру каскадного пересчета веса.ПриНачалеРедактирования, автоматически подставляя в новую строку показание весов из предыдущей строки в качестве значения по умолчанию. Это сбережет время оператора — ему останется только внести небольшую корректировку.ПередЗаписью(). Это застрахует систему от случайной записи некорректных данных при сбоях на клиенте.