Как реализовать перемещение товаров между складами с контролем остатков в 1С 8.3

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

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

Подготовка структуры метаданных

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

  1. Справочник Номенклатура: Здесь хранится перечень наших товаров.
  2. Справочник Склады: Места хранения.
  3. Регистр накопления "Товары на складах": Вид регистра — Остатки. Измерения: Склад, Номенклатура. Ресурс: Количество.
  4. Документ "Перемещение товаров":
    • Реквизиты шапки: СкладОтправитель, СкладПолучатель.
    • Табличная часть "СписокНоменклатуры": Номенклатура, Количество.

Важно убедиться, что в свойствах документа на закладке "Движения" отмечен наш регистр ТоварыНаСкладах, а стратегия удаления движений установлена в значение "Удалять автоматически при перепроведении".

Реализация движений документа (Приход и Расход)

Рассмотрим логику проведения. Нам нужно забрать товар с одного склада и положить на другой. В терминах регистров накопления это означает формирование двух видов движений: Расход для склада-отправителя и Приход для склада-получателя. Проанализируем, как это реализовать максимально эффективно с помощью одного запроса.

Откроем модуль документа ПеремещениеТоваров и создадим процедуру ОбработкаПроведения. Мы будем использовать пакетный запрос или объединение, чтобы получить данные для движений. Рассмотрим пример кода, который подготавливает данные:


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

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

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

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

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

Контроль отрицательных остатков: "Новая методика"

Проанализируем ситуацию: почему мы записываем движения до того, как проверили наличие товара? Это и есть суть "Новой методики" проведения. Мы сначала фиксируем намерение (записываем расход), а затем проверяем, не ушел ли остаток по регистру в "минус". Если ушел — отменяем транзакцию установкой Отказ = Истина.

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


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

Профессиональный подход: Управляемые блокировки

В многопользовательской среде может возникнуть проблема: два кладовщика одновременно проводят перемещение одного и того же товара, которого на складе всего 1 штука. Без блокировок оба запроса к остаткам могут вернуть "1 шт.", оба документа проведутся, и остаток станет "-1".

Чтобы этого избежать, проанализируем механизм БлокировкаДанных. Нам нужно заблокировать остатки по складу-отправителю и списку номенклатуры перед выполнением запроса.

Рассмотрим шаги настройки блокировки:

  1. Создаем объект БлокировкаДанных.
  2. Добавляем элемент блокировки для регистра ТоварыНаСкладах.
  3. Устанавливаем режим Исключительный.
  4. Передаем источник данных (табличную часть документа).

Пример кода блокировки:


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

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

Типичные ошибки при реализации

Проанализируем основные "грабли", на которые наступают начинающие разработчики, судя по обсуждениям на форумах:

Заключение

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

← На главную