Почему запрос в 1С не находит товары, которых не хватает на складе при проведении документа "Реализация товаров и услуг"?

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

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

Анализ исходного запроса и выявление первоначальной проблемы

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


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

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

Основная проблема, с которой столкнулся разработчик, заключалась в том, что переменная РезультатЗапроса.Пустой() практически всегда возвращала значение Истина, даже в тех случаях, когда по логике вещей должен был быть обнаружен дефицит товаров. Мы выяснили, что причина такого поведения кроется в месте выполнения запроса: он находится в процедуре ОбработкаПроведения документа, но выполняется до того, как движения текущего документа были фактически записаны в регистр накопления. То есть, к моменту выполнения запроса, списание товаров из-за проведения текущего документа еще не произошло в данных регистра, и запрос "не видит" эти изменения.

Ключевое решение: Обязательная запись движений перед проверкой остатков

Виртуальные таблицы регистров накопления, такие как РегистрНакопления.ТоварыНаСкладах.Остатки(), формируют свои данные на основе уже записанных движений. Это означает, что для получения актуальной информации об остатках, которая учитывает списание товаров текущим документом, нам необходимо явно убедиться, что эти движения уже существуют в регистре. Для этой цели в 1С используется метод Движения.Записать().

Разместив вызов метода Движения.Записать() непосредственно перед выполнением запроса на проверку остатков, мы даем системе указание зафиксировать все движения, которые были сформированы текущим документом. Эти движения временно записываются в регистр (до окончательного подтверждения транзакции проведения документа). Только после этой операции виртуальная таблица Остатки() сможет корректно отразить все изменения, внесенные текущим документом, включая потенциальное образование дефицита.

Давайте подробно рассмотрим, как будет выглядеть доработанный фрагмент кода, включающий этот критически важный шаг:


// ... Ваш код инициализации запроса и установки параметров ...
    
// Критически важный шаг: записываем движения документа, чтобы они были видны запросу
Движения.Записать(); 
        
РезультатЗапроса = Запрос.Выполнить();
    
Если Не РезультатЗапроса.Пустой() Тогда
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
        
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        Сообщить("Недостаточно Товара " + ВыборкаДетальныеЗаписи.Номенклатура + " в количестве " + ВыборкаДетальныеЗаписи.Количество);
    КонецЦикла;
        
    // Если обнаружен дефицит, мы должны отменить проведение документа
    Отказ = Истина;
КонецЕсли;

В этом доработанном варианте, после вызова Движения.Записать(), запрос на остатки будет работать уже с актуальными данными, полностью учитывающими эффект списания товаров текущим документом. Если после выполнения запроса переменная РезультатЗапроса.Пустой() вернет значение Ложь, это будет четким сигналом об обнаружении дефицита. В таком случае мы можем принять соответствующие меры: вывести информационное сообщение пользователю и, что крайне важно, установить флаг Отказ = Истина;, чтобы предотвратить некорректное проведение документа с недостаточным количеством товаров.

Важность параметра "МоментВремени" для корректного расчета остатков

Помимо явной записи движений, существует еще один критически важный аспект, который необходимо учитывать при работе с виртуальными таблицами остатков – это правильное использование параметра "МоментВремени". Если этот параметр не задан при обращении к виртуальной таблице Остатки(), система по умолчанию рассчитывает остатки на "бесконечно удаленную" дату. Это означает, что в расчет будут включены все движения по регистру до самого конца данных в информационной базе, что в реальных условиях эксплуатации может привести к абсолютно некорректным результатам, особенно если в системе присутствуют документы с будущими датами (например, запланированные поставки или отгрузки).

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

Давайте подробно рассмотрим, как правильно инициализировать и использовать этот параметр:


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

В приведенном примере мы сначала создаем объект Граница, который точно определяет момент времени для расчета остатков. МоментВремени() возвращает объект, который включает в себя дату и время текущего документа, а ВидГраницы.Включая указывает, что движения, происходящие точно в этот момент времени, также должны быть учтены. Это гарантирует, что мы проверяем остатки именно на момент проведения документа, получая наиболее точную и релевантную информацию.

Оптимизация запросов: Временные таблицы против вложенных запросов

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

Временные таблицы создаются в запросе с помощью конструкции ПОМЕСТИТЬ ИмяВременнойТаблицы. Они позволяют нам сначала подготовить необходимый набор данных в отдельной, промежуточной таблице, а затем использовать ее в последующих частях запроса как обычную таблицу. Этот подход может быть особенно полезен, если один и тот же набор данных требуется использовать несколько раз в разных частях сложного запроса, или если объем данных, возвращаемых вложенным запросом, достаточно велик.

Рассмотрим ключевые преимущества временных таблиц:

  1. Улучшенная читаемость: Запрос становится более структурированным и понятным, поскольку сложная логика разбивается на отдельные, логически завершенные этапы. Каждый этап создания временной таблицы выполняет конкретную задачу, что упрощает отладку и понимание общего потока данных.
  2. Повышенная производительность: В определенных сценариях СУБД может более эффективно оптимизировать запросы, использующие временные таблицы. Это особенно актуально, если данные во временной таблице сильно сокращаются на первом этапе (например, мы отбираем только нужные позиции из большой таблицы), или если на эту временную таблицу будут наложены индексы (что происходит автоматически, если платформа сочтет это целесообразным).
  3. Повторное использование данных: После того как данные помещены во временную таблицу, их можно легко использовать в нескольких последующих запросах в рамках того же пакета запросов без необходимости повторного извлечения из исходных источников, что экономит ресурсы.

Когда предпочтительны вложенные запросы:

Несмотря на преимущества временных таблиц, вложенные запросы также имеют свое место. Их предпочтительно использовать в следующих случаях:

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


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

В этом примере мы сначала формируем список номенклатуры из табличной части текущего документа и помещаем его во временную таблицу СписокНоменклатуры. Затем, во втором запросе, мы выполняем внутреннее соединение виртуальной таблицы остатков с этой временной таблицей. Такой подход обеспечивает более ясную структуру и часто является более гибким и производительным при обработке данных.

Рекомендации по именованию полей и объектов

Хотя этот момент не влияет напрямую на работоспособность запроса, он критически важен для качества кода, его читаемости и поддержки. Мы обратили внимание на опечатки в исходном запросе, такие как Номенкулатура (вместо Номенклатура) и КолличествоОстаток (вместо КоличествоОстаток).

Использование корректных и общепринятых наименований способствует:

Мы настоятельно рекомендуем всегда перепроверять наименования метаданных (справочников, документов, регистров, их реквизитов и табличных частей) непосредственно в конфигураторе 1С. Это простой, но очень эффективный способ избежать подобных ошибок и повысить общее качество вашего кода.

Итоговый доработанный код проверки остатков

Объединив все рассмотренные рекомендации и исправления, мы получим надежный, эффективный и корректный запрос для проверки остатков товаров на складе. Этот подход учитывает все нюансы работы с регистрами накопления и обеспечит стабильное функционирование вашего документа. Давайте посмотрим на полностью доработанный фрагмент кода:


// Определяем момент времени для расчета остатков.
// Это гарантирует, что мы проверяем остатки именно на момент проведения документа.
ГраницаКонтроля = Новый Граница(МоментВремени(), ВидГраницы.Включая);
    
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    РеализацияТоваровИУслугТовары.Номенклатура КАК Номенклатура // Исправлена опечатка 'Номенкулатура'
    |ПОМЕСТИТЬ СписокНоменклатуры
    |ИЗ
    |    Документ.РеализацияТоваровИУслуг.Товары КАК РеализацияТоваровИУслугТовары
    |ГДЕ
    |    РеализацияТоваровИУслугТовары.Ссылка = &Ссылка
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
    |    ТоварыНаСкладахОстатки.Склад КАК Склад,
    |    ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество // Исправлена опечатка 'КолличествоОстаток'
    |ИЗ
    |    РегистрНакопления.ТоварыНаСкладах.Остатки(&МоментВремени, Склад = &Склад) КАК ТоварыНаСкладахОстатки
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ СписокНоменклатуры КАК СписокНоменклатуры
    |        ПО ТоварыНаСкладахОстатки.Номенклатура = СписокНоменклатуры.Номенклатура
    |ГДЕ
    |    ТоварыНаСкладахОстатки.КоличествоОстаток < 0";
        
// Устанавливаем все необходимые параметры запроса
Запрос.УстановитьПараметр("Склад", Склад);    
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("МоментВремени", ГраницаКонтроля);
    
// Критически важный шаг: записываем движения документа, чтобы они были видны запросу.
// Это позволяет виртуальной таблице Остатки() корректно отразить списание товаров текущим документом.
Движения.Записать(); 
        
РезультатЗапроса = Запрос.Выполнить();
    
Если Не РезультатЗапроса.Пустой() Тогда
    // Если запрос вернул данные, значит, обнаружен дефицит товаров
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
        
    Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
        // Выводим сообщение пользователю о недостатке конкретного товара
        Сообщить("Недостаточно Товара " + ВыборкаДетальныеЗаписи.Номенклатура + " в количестве " + ВыборкаДетальныеЗаписи.Количество);
    КонецЦикла;
        
    // Устанавливаем флаг Отказ в Истина, чтобы отменить проведение документа
    // Это предотвращает проведение документа при наличии дефицита
    Отказ = Истина;
КонецЕсли;

Этот код выполняет следующие важные шаги:

  1. Мы устанавливаем момент времени для расчета остатков, используя Граница(МоментВремени(), ВидГраницы.Включая). Это обеспечивает точность данных на момент проведения документа.
  2. Мы создаем временную таблицу СписокНоменклатуры. В нее помещаются все позиции номенклатуры из табличной части текущего документа. Это улучшает структуру запроса и потенциально его производительность.
  3. Мы выполняем критически важный шаг: Движения.Записать(). Этот вызов гарантирует, что все движения, сформированные текущим документом, будут доступны виртуальной таблице остатков для корректного расчета.
  4. Затем мы запрашиваем остатки товаров, используя параметр МоментВремени и соединение с нашей временной таблицей СписокНоменклатуры.
  5. Мы фильтруем результаты запроса, отбирая только те записи, где КоличествоОстаток (исправлено с КолличествоОстаток) меньше нуля, что указывает на дефицит.
  6. Наконец, если запрос обнаруживает дефицит (т.е., РезультатЗапроса.Пустой() возвращает Ложь), мы выводим информационные сообщения для пользователя о недостающих товарах и, что крайне важно, устанавливаем флаг Отказ = Истина;. Это предотвращает проведение документа и сохраняет целостность данных о складских остатках.

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

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

← На главную