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