Почему не работает контроль остатков в регистре накопления и как это исправить?

Программист 1С v8.3 (Управляемые формы) Управленческий учет
← На главную

При разработке механизмов проведения документов в 1С начинающие программисты часто сталкиваются с ситуацией, когда код, написанный по обучающим видео, отказывается работать корректно. Одной из самых распространенных проблем является ситуация, когда запрос к виртуальной таблице остатков всегда возвращает пустой результат или нулевое значение, даже если данные в системе есть. Рассмотрим подробно, почему это происходит и как построить надежный механизм контроля остатков, соответствующий стандартам разработки 1С.

Проблема типов данных: Строка против Ссылки

Проанализируем исходный код, представленный в вопросе. Мы видим, что соединение в запросе происходит по полям Название и Состояние. Скорее всего, в конфигурации автора эти поля имеют тип Строка. Это критическая архитектурная ошибка.

Выясним причину, почему использование строк в измерениях регистра накопления — это плохая практика:

  1. Чувствительность к символам: Для базы данных значения "Книга" и "Книга " (с пробелом в конце) — это абсолютно разные записи. При ручном вводе пользователь может случайно нажать лишний пробел, сменить регистр или использовать латинскую букву "с" вместо кириллической "с". В итоге визуально данные одинаковые, но ЛЕВОЕ СОЕДИНЕНИЕ не найдет соответствия, и функция ЕСТЬNULL вернет 0.
  2. Производительность: Индексация строковых полей (особенно неограниченной длины) работает значительно медленнее, чем индексация ссылочных типов. При росте базы данных отчеты и проведение документов начнут "тормозить".
  3. Целостность данных: Используя справочник (например, Справочники.Номенклатура), мы гарантируем, что во всех документах и регистрах используется один и тот же объект.

Решение: Нам необходимо изменить тип измерения НаименованиеКниги в регистре накопления Полка на СправочникСсылка.Книги (или аналогичный). То же самое нужно сделать в табличной части документа.

Корректное получение остатков в запросе

Посмотрим на структуру запроса в процедуре ОбработкаПроведения. В примере автора используется вложенный запрос к табличной части документа, который соединяется с виртуальной таблицей остатков. Разберем по шагам, как сделать этот запрос эффективнее.

Главная ошибка — отсутствие параметров в виртуальной таблице Остатки. Без параметров система сначала получит все остатки по всем книгам из регистра, а уже потом начнет отсекать лишнее через СОЕДИНЕНИЕ. Если в регистре будет 100 000 записей, процедура проведения будет работать крайне долго.

Правильный подход заключается в передаче списка товаров прямо в параметры виртуальной таблицы. Рассмотрим пример кода:


// Сначала сформируем список номенклатуры из документа
СписокТоваров = ВыдачаКниги.ВыгрузитьКолонку("Название");

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТоварыДокумента.Номенклатура КАК Номенклатура,
    |	ТоварыДокумента.Количество КАК КоличествоВДокументе,
    |	ЕСТЬNULL(ОстаткиНаСкладе.ШтукиОстаток, 0) КАК ОстатокНаСкладе
    |ИЗ
    |	(ВЫБРАТЬ
    |		Док.Название КАК Номенклатура,
    |		СУММА(Док.Количество) КАК Количество
    |	ИЗ
    |		Документ.Стол.ВыдачаКниги КАК Док
    |	ГДЕ
    |		Док.Ссылка = &Ссылка
    |	СГРУППИРОВАТЬ ПО
    |		Док.Название) КАК ТоварыДокумента
    |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Полка.Остатки(&МоментВремени, НаименованиеКниги В (&СписокТоваров)) КАК ОстаткиНаСкладе
    |		ПО ТоварыДокумента.Номенклатура = ОстаткиНаСкладе.НаименованиеКниги";

Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("СписокТоваров", СписокТоваров);
Запрос.УстановитьПараметр("МоментВремени", МоментВремени());

Обратите внимание: Мы использовали МоментВремени() вместо Дата. Это гарантирует, что при получении остатков будут учтены все документы, записанные в ту же секунду, но стоящие в реестре раньше текущего.

Управляемые блокировки данных

Как справедливо заметили в обсуждении на форуме, представленный автором код не защищен от конкурентного доступа. Проанализируем ситуацию: два пользователя одновременно нажимают кнопку "Провести" на разные документы, в которых указана одна и та же последняя книга. Оба запроса одновременно увидят, что остаток равен 1. Оба документа проведутся, и в итоге остаток станет равен -1. Чтобы этого избежать, нужно использовать объект БлокировкаДанных.

Добавим блокировку перед выполнением запроса:


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.Полка");
ЭлементБлокировки.УстановитьЗначение("НаименованиеКниги", ТекСтрока.Название); // В цикле или через источник данных
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

Это заставит вторую транзакцию ждать, пока первая не завершится (не зафиксирует списание или не отменит его).

Контроль остатков и "чистка" движений

Важно понимать, в какой момент происходит проверка. В 1С 8.3 по умолчанию при перепроведении документа его старые движения в регистрах удаляются автоматически перед входом в процедуру ОбработкаПроведения (если в свойствах документа стоит "Удалять движения автоматически").

Если же вы записываете движения вручную или используете сложную логику, проанализируем следующий алгоритм:

  1. Очистить существующие записи регистра для этого документа (чтобы запрос остатков не видел "самого себя").
  2. Записать пустой набор записей (Движения.Полка.Записать()).
  3. Установить исключительные блокировки на списываемые товары.
  4. Выполнить запрос остатков и проверить, хватает ли количества.
  5. Если хватает — заполнить коллекцию Движения и завершить процедуру.

Использование временных таблиц

Для повышения качества кода и производительности (особенно на СУБД MS SQL или PostgreSQL) рекомендуется использовать временные таблицы вместо вложенных запросов. Это позволяет оптимизатору СУБД лучше построить план выполнения.


// Пример использования временной таблицы
Запрос = Новый Запрос;
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТЧ.Название КАК Книга,
    |	СУММА(ТЧ.Количество) КАК Кол
    |ПОМЕСТИТЬ ВТ_ТоварыДокумента
    |ИЗ
    |	Документ.Стол.ВыдачаКниги КАК ТЧ
    |ГДЕ
    |	ТЧ.Ссылка = &Ссылка
    |СГРУППИРОВАТЬ ПО
    |	ТЧ.Название;
    |
    |ВЫБРАТЬ
    |	ВТ.Книга,
    |	ВТ.Кол,
    |	ЕСТЬNULL(Ост.ШтукиОстаток, 0) КАК Остаток
    |ИЗ
    |	ВТ_ТоварыДокумента КАК ВТ
    |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Полка.Остатки(&МоментВремени, НаименованиеКниги В (ВЫБРАТЬ Книга ИЗ ВТ_ТоварыДокумента)) КАК Ост
    |	ПО ВТ.Книга = Ост.НаименованиеКниги";

Такой подход считается "золотым стандартом" разработки на платформе 1С. Мы сначала изолируем данные документа во временную таблицу, а затем соединяем их с актуальными остатками, фильтруя виртуальную таблицу по списку из этой же временной таблицы.

Подведем итог: чтобы ваш регистр накопления "заработал", первым делом откажитесь от строковых типов в пользу ссылок на справочники. После этого настройте параметры виртуальной таблицы остатков и добавьте механизм блокировок для обеспечения корректности данных в многопользовательском режиме.

← На главную