Почему не работает контроль остатков при проведении документов по складам и как исправить «схлопывание» данных

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

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

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

Выясняем причину: почему «схлопываются» остатки при проведении?

Рассмотрим ситуацию, с которой столкнулся автор темы. При переносе логики контроля остатков из документа «Реализация товаров со склада» в документ «Перемещение товаров» система начала некорректно рассчитывать списываемое количество. Вместо проведения конкретного документа программа суммировала показатели и выдавала гигантские дефициты.

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


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

Если внимательно посмотреть на этот запрос, становится очевидна грубейшая логическая ошибка: в секции фильтрации отсутствует условие отбора по конкретному документу. Запрос обращается к табличной части Документ.ПеремещениеТоваров.Товары напрямую, но не ограничивает выборку текущим объектом.

В результате СУБД выбирает и суммирует строки табличных частей абсолютно всех документов данного типа, когда-либо созданных в базе данных! Именно это разработчики называют «схлопыванием остатков по черному» — система пытается списать совокупный объем товаров по всем историческим документам сразу, что приводит к гарантированной ошибке проведения.

Для устранения этой проблемы в запрос обязательно нужно передавать параметр ссылки на проводимый документ:


ГДЕ 
    ПеремещениеТоваровТовары.Ссылка = &Ссылка

---

Разбор по шагам: проектирование контроля остатков по новой методике

Давайте разберем правильный алгоритм построения контроля остатков для документа «Перемещение товаров». Данный документ интересен тем, что делает сразу два вида движений по регистру ТоварыНаСкладах:

  1. Расход — товар списывается со склада-отправителя. На этом этапе контроль остатков критически необходим, так как мы не можем переместить то, чего у нас нет.
  2. Приход — товар поступает на склад-получатель. Здесь контроль остатков бессмысленен, поскольку приходная операция по определению увеличивает остаток и не может увести регистр в минус.

Посмотрим, как правильно выстроить весь процесс в модуле объекта документа в обработчике события ОбработкаПроведения.

Шаг 1. Подготовка и предварительная очистка движений

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


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

Шаг 2. Запись предварительных движений с блокировкой

В новой методике проведения мы сначала записываем расходные движения в базу данных, но делаем это с установкой специального флага БлокироватьДляИзменения. Это позволяет платформе на уровне СУБД наложить управляемую блокировку на те позиции номенклатуры и склады, по которым совершается расход. Другие пользователи не смогут прочитать или изменить эти остатки до завершения нашей транзакции, что полностью защищает систему от «эффекта гонки» (Race Condition).


// Включаем запись движений и устанавливаем управляемую блокировку данных
Движения.ТоварыНаСкладах.Записывать = Истина;
Движения.ТоварыНаСкладах.БлокироватьДляИзменения = Истина;

// Формируем движения по расходу (Склад-отправитель)
Для Каждого ТекСтрока Из Товары Цикл
    Движение = Движения.ТоварыНаСкладах.ДобавитьРасход();
    Движение.Период = Дата;
    Движение.Склад = СкладОтправитель;
    Движение.Номенклатура = ТекСтрока.Номенклатура;
    Движение.Количество = ТекСтрока.Количество;
КонецЦикла;

// Записываем расход в БД для последующего анализа остатков
Движения.ТоварыНаСкладах.Записать();

Шаг 3. Анализ остатков и выявление дефицита через пакетный запрос

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

Обратите внимание на то, как определяется момент времени для контроля остатков. При оперативном проведении проверку остатков рекомендуется выполнять на «бесконечность» (то есть вообще не передавать параметр момента времени в виртуальную таблицу остатков или передавать значение Неопределено). Это гарантирует, что товар есть в наличии не только в секунду проведения документа, но и не уйдет в минус из-за документов, проведенных более поздним временем.

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


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

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

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

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

РезультатПроверки = ЗапросКонтроль.Выполнить();

Шаг 4. Обработка ошибок и отмена проведения

Если пакетный запрос вернул записи, это означает, что после записи нашего расхода остаток товара на складе-отправителе стал отрицательным. Нам необходимо сообщить об этом пользователю, установить флаг Отказ = Истина и прервать выполнение обработчика. Транзакция автоматически откатится платформой, и движения не сохранятся в базе данных.


Если Не РезультатПроверки.Пустой() Тогда
    
    Отказ = Истина;
    ВыборкаОшибок = РезультатПроверки.Выбрать();
    
    Пока ВыборкаОшибок.Следующий() Цикл
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = СтрШаблон("Недостаточно товара '%1' на складе '%2'. Требуется еще %3 шт.", 
                            ВыборкаОшибок.Номенклатура, СкладОтправитель, ВыборкаОшибок.Дефицит);
        // Указываем конкретное поле табличной части, где допущена ошибка
        Сообщение.Поле = "Товары[" + (ВыборкаОшибок.НомерСтроки - 1) + "].Количество";
        Сообщение.УстановитьДанные(ЭтотОбъект);
        Сообщение.Сообщить();
    КонецЦикла;
    
    // Прерываем проведение, так как обнаружен дефицит
    Возврат;
    
КонецЕсли;

Шаг 5. Формирование прихода на склад-получатель

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


// Если ошибок нет, добавляем приходные движения на склад-получатель
Для Каждого ТекСтрока Из Товары Цикл
    Движение = Движения.ТоварыНаСкладах.ДобавитьПриход();
    Движение.Период = Дата;
    Движение.Склад = СкладПолучатель;
    Движение.Номенклатура = ТекСтрока.Номенклатура;
    Движение.Количество = ТекСтрока.Количество;
КонецЦикла;

// Финальная запись всех движений документа в БД
Движения.ТоварыНаСкладах.Записать();

---

Ключевые выводы для разработчика

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

← На главную