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