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