Как исправить ошибку контроля остатков и резервов при проведении документов с упаковками в 1С

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

При разработке собственных механизмов проведения документов в конфигурациях 1С (особенно в УПП 1.3, КА 1.1 или УТ 10.3) разработчики часто сталкиваются с ситуацией, когда система контроля остатков ведет себя непредсказуемо. Документ может выдавать противоречивые сообщения: например, одновременно сообщать, что товара «слишком много по заказу покупателя» и что его же «недостаточно в резерве». При этом пользователи утверждают, что в самом документе и в справочниках все коэффициенты упаковок заполнены абсолютно корректно.

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

Рассмотрим по шагам основные причины сбоя системы контроля проведения и способы их устранения.

---

Причина 1. Математическая ошибка пересчета упаковок (сбой коэффициентов)

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

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

Если единица хранения остатков в карточке товара отличается от базовой единицы, то простое умножение Количество * Коэффициент вернет количество в базовых единицах, а регистр вернет остаток в единицах хранения остатков. Возникает конфликт масштабов.

Пример несоответствия:

  1. Базовая единица товара — «Грамм» (коэффициент = 1).
  2. Единица хранения остатков — «Килограмм» (коэффициент = 1000).
  3. В документе указано количество 1 (в килограммах, коэффициент = 1000).
  4. По формуле Количество * Коэффициент запрос рассчитывает: 1 * 1000 = 1000.
  5. Запрос пытается сопоставить эту тысячу с остатком в регистре резервов. А в регистре лежит ровно 1 кг (значение в базе равно 1).
  6. Система вычисляет разность: 1 (остаток из регистра) - 1000 (расход из формулы) = -999.
  7. Программа выдает ошибку: «Недостаточно товара в резерве», хотя фактически резерв полностью покрывает потребность.

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

РасходВЕдиницахХранения = (Док.Количество * Док.Коэффициент) / Док.Номенклатура.ЕдиницаХраненияОстатков.Коэффициент

---

Причина 2. Прямое соединение физических таблиц с виртуальными (Отсутствие свертки табличной части)

Вторым критическим уязвимым местом является архитектура самого запроса. Посмотрим на классический пример неудачного запроса:


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

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

Правильное решение: Перед соединением с остатками необходимо выбрать данные из табличной части во временную таблицу, сгруппировать (свернуть) строки по уникальным ключевым полям (Номенклатура, Характеристика, Серия, Сделка) и рассчитать суммарный расход. И только эту свернутую временную таблицу соединять с остатками.

---

Причина 3. Неправильный выбор момента времени (Параметр &Период)

Передача в качестве параметра виртуальной таблицы остатков чистой даты документа (Ссылка.Дата) — частая ошибка. При проведении документа в транзакции СУБД должна анализировать остатки строго на момент времени перед проведением текущего документа, чтобы его собственные движения не искажали картину.

Если передать просто дату, то документы, записанные в ту же секунду, или собственные движения перепроводимого документа могут быть учтены некорректно. Чтобы избежать этого, в качестве параметра периода необходимо использовать объект Граница с исключением момента времени документа.

Пример кода на встроенном языке:


ГраницаКонтроля = Новый Граница(Ссылка.МоментВремени(), ВидГраницы.Исключая);
Запрос.УстановитьПараметр("Период", ГраницаКонтроля);

---

Причина 4. Ошибка фильтрации по сделке в виртуальной таблице

В более сложных проверках резервов разработчики часто ставят жесткий фильтр в параметры виртуальной таблицы остатков резервов:

РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(&Период, ДокументРезерва = &Сделка)

Где параметр &Сделка заполняется из реквизита шапки документа (например, Ссылка.Сделка или Ссылка.ЗаказПокупателя).

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

---

Комплексное решение: Создание правильного алгоритма контроля остатков и резервов

Объединим все наши выводы в один эффективный и отказоустойчивый алгоритм. Напишем оптимизированную процедуру проверки наличия резервов под документ реализации.

Разберем по шагам структуру нового запроса:

  1. ВТ_ТоварыДокумента: Выбираем номенклатуру, характеристики, серии и заказы из табличной части. Группируем их. Применяем правильную формулу пересчета количества в единицы хранения остатков.
  2. ВТ_УникальныеЗаказы: Формируем список уникальных заказов для передачи в параметры виртуальной таблицы остатков в качестве фильтра.
  3. ВТ_Резервы: Запрашиваем остатки резервов из регистра накопления только по тем номенклатурным позициям и заказам, которые фигурируют в нашем документе.
  4. Анализ: Соединяем сгруппированные товары документа с остатками резервов и проверяем наличие дефицита.

Посмотрим на пример реализации этой процедуры:


Процедура ПроверитьНаличиеРезервов(Ссылка, Отказ) Экспорт
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	РеализацияТовары.Номенклатура КАК Номенклатура,
    |	РеализацияТовары.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    |	РеализацияТовары.СерияНоменклатуры КАК СерияНоменклатуры,
    |	РеализацияТовары.ЗаказПокупателя КАК ЗаказПокупателя,
    |	СУММА(РеализацияТовары.Количество * РеализацияТовары.Коэффициент / РеализацияТовары.Номенклатура.ЕдиницаХраненияОстатков.Коэффициент) КАК КоличествоДокумента
    |ПОМЕСТИТЬ ВТ_ТоварыДокумента
    |ИЗ
    |	Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
    |ГДЕ
    |	РеализацияТовары.Ссылка = &Ссылка
    |	И РеализацияТовары.СпособСписанияОстаткаТоваров = ЗНАЧЕНИЕ(Перечисление.СпособыСписанияОстаткаТоваров.ИзРезерва)
    |
    |СГРУППИРОВАТЬ ПО
    |	РеализацияТовары.Номенклатура,
    |	РеализацияТовары.ХарактеристикаНоменклатуры,
    |	РеализацияТовары.СерияНоменклатуры,
    |	РеализацияТовары.ЗаказПокупателя
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ВТ_ТоварыДокумента.ЗаказПокупателя КАК ЗаказПокупателя
    |ПОМЕСТИТЬ ВТ_УникальныеЗаказы
    |ИЗ
    |	ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ТоварыВРезервеОстатки.Номенклатура КАК Номенклатура,
    |	ТоварыВРезервеОстатки.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    |	ТоварыВРезервеОстатки.СерияНоменклатуры КАК СерияНоменклатуры,
    |	ТоварыВРезервеОстатки.ДокументРезерва КАК ЗаказПокупателя,
    |	СУММА(ТоварыВРезервеОстатки.КоличествоОстаток) КАК КоличествоОстаток
    |ПОМЕСТИТЬ ВТ_Резервы
    |ИЗ
    |	РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(
    |			&Период,
    |			ДокументРезерва В (ВЫБРАТЬ ВТ_УникальныеЗаказы.ЗаказПокупателя ИЗ ВТ_УникальныеЗаказы)
    |				И Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ ВТ_ТоварыДокумента.Номенклатура ИЗ ВТ_ТоварыДокумента)) КАК ТоварыВРезервеОстатки
    |
    |СГРУППИРОВАТЬ ПО
    |	ТоварыВРезервеОстатки.Номенклатура,
    |	ТоварыВРезервеОстатки.ХарактеристикаНоменклатуры,
    |	ТоварыВРезервеОстатки.СерияНоменклатуры,
    |	ТоварыВРезервеОстатки.ДокументРезерва
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ТоварыДокумента.Номенклатура КАК Номенклатура,
    |	ВТ_ТоварыДокумента.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    |	ВТ_ТоварыДокумента.СерияНоменклатуры КАК СерияНоменклатуры,
    |	ВТ_ТоварыДокумента.ЗаказПокупателя КАК ЗаказПокупателя,
    |	ВТ_ТоварыДокумента.КоличествоДокумента КАК КоличествоДокумента,
    |	ЕСТЬNULL(ВТ_Резервы.КоличествоОстаток, 0) КАК КоличествоОстаток,
    |	ЕСТЬNULL(ВТ_Резервы.КоличествоОстаток, 0) - ВТ_ТоварыДокумента.КоличествоДокумента КАК Нехватка
    |ИЗ
    |	ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
    |		ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Резервы КАК ВТ_Резервы
    |		ПО ВТ_ТоварыДокумента.Номенклатура = ВТ_Резервы.Номенклатура
    |			И ВТ_ТоварыДокумента.ХарактеристикаНоменклатуры = ВТ_Резервы.ХарактеристикаНоменклатуры
    |			И ВТ_ТоварыДокумента.СерияНоменклатуры = ВТ_Резервы.СерияНоменклатуры
    |			И ВТ_ТоварыДокумента.ЗаказПокупателя = ВТ_Резервы.ЗаказПокупателя
    |ГДЕ
    |	ЕСТЬNULL(ВТ_Резервы.КоличествоОстаток, 0) - ВТ_ТоварыДокумента.КоличествоДокумента < 0";
    
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    
    // Устанавливаем границу времени ИСКЛЮЧАЯ движения текущего документа
    ГраницаПроверки = Новый Граница(Ссылка.МоментВремени(), ВидГраницы.Исключая);
    Запрос.УстановитьПараметр("Период", ГраницаПроверки);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если Не РезультатЗапроса.Пустой() Тогда
        
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            
            ШаблонСообщения = "Недостаточно товара в резерве по заказу %1! Товар: %2, Требуется: %3, В резерве: %4 (Нехватка: %5)";
            ТекстСообщения = СтрШаблон(ШаблонСообщения, 
                СокрЛП(Выборка.ЗаказПокупателя), 
                СокрЛП(Выборка.Номенклатура), 
                Выборка.КоличествоДокумента, 
                Выборка.КоличествоОстаток, 
                Abs(Выборка.Нехватка)
            );
            
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю(ТекстСообщения, , , , Отказ);
            Отказ = Истина;
            
        КонецЦикла;
        
    КонецЕсли;

КонецПроцедуры

---

Преимущества разработанного решения:

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

← На главную