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