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