← Назад к вопросам

Как организовать контроль остатков при проведении документа?

2.0 Middle🔥 201 комментариев
#Стандарты разработки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Контроль остатков при проведении документа

Это важнейшая задача в учёте: убедиться, что при проведении документа (например, отпуска товара) остатки не уйдут в минус.

Архитектура решения

1. Регистр остатков

// РегистрНакопления.ОстаткиТоваров
// ОСТАТКИ (не ОСТАТКИ И ОБОРОТЫ)
// 
// Измерения:
//   - Товар
//   - Склад
// 
// Ресурсы:
//   - КоличествоОстаток (число)

2. Проверка перед проведением документа

// Документ.РасходнаяНакладная

&НаСервере
Процедура ПроверитьОстаткиТоваров()
    
    // Проходим по каждой строке документа
    Для Каждого СтрокаТовара Из Товары Цикл
        
        // Получаем текущий остаток на складе
        Остаток = ПолучитьОстатокТовара(
            СтрокаТовара.Товар,
            СтрокаТовара.Склад
        );
        
        // Проверяем: достаточно ли товара
        Если Остаток < СтрокаТовара.Количество Тогда
            ВызватьИсключение(СтрокаТовара.Товар.Наименование + 
                " на складе " + СтрокаТовара.Склад.Наименование +
                ": остаток только " + Остаток + 
                " шт., а вы хотите отпустить " + СтрокаТовара.Количество + " шт.");
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

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

Проверка в процедуре проведения

&НаСервере
Процедура ПроведениеПроведение(Отказ, Режим)
    
    НачатьТранзакцию();
    Попытка
        
        // ШАГ 1: Проверяем остатки
        ПроверитьОстаткиТоваров();
        
        // ШАГ 2: Создаём движения по регистру
        СоздатьДвиженияРегистров();
        
        // ШАГ 3: Записываем изменения
        ЗаписьДвижений();
        
        ЗафиксироватьТранзакцию();
        
    Исключение
        
        ОтменитьТранзакцию();
        Отказ = Истина;
        
        // Показываем пользователю, в чём проблема
        Сообщить("Ошибка при проведении: " + ОписаниеОшибки());
        
    КонецПопытки;
    
КонецПроцедуры

Использование блокировок для параллелизма

Если несколько пользователей одновременно проводят документы, может быть проблема race condition. Решение — блокировки:

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

Что делает FOR UPDATE:

  • Получает блокировку на таблице товаров
  • Другие транзакции ждут, пока эта завершится
  • Предотвращает одновременные изменения
  • Гарантирует консистентность данных

Создание движений регистра

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

Альтернатива: свойство "Контролировать остатки"

В 1С есть встроенный механизм в РегистрНакопления:

// При создании РегистрНакопления в конфигурации:
// Свойство "Контролировать остатки" = Истина
// 
// Тогда 1С автоматически будет проверять,
// что остаток НЕ уходит в минус при проведении

// Но это работает ТОЛЬКО если:
// - Регистр с типом "ОСТАТКИ"
// - Свойство включено
// - Документ использует встроенные движения

Обработка ошибок

// Что сделать, если остатков недостаточно?

Процедура ПроверитьОстаткиТоваровСОбработкой()
    
    ТоварыСПроблемами = Новый Массив();
    
    Для Каждого СтрокаТовара Из Товары Цикл
        
        Остаток = ПолучитьОстатокТовара(СтрокаТовара.Товар, СтрокаТовара.Склад);
        
        Если Остаток < СтрокаТовара.Количество Тогда
            
            // Вариант 1: Строгая проверка — ошибка
            Если НЕ РазрешитьОтрицательныеОстатки Тогда
                ВызватьИсключение "Недостаточно остатков";
            КонецЕсли;
            
            // Вариант 2: Предупреждение
            ТоварыСПроблемами.Добавить(СтрокаТовара);
            
        КонецЕсли;
        
    КонецЦикла;
    
    Если ТоварыСПроблемами.Количество() > 0 Тогда
        
        // Спрашиваем пользователя
        Ответ = Вопрос(
            "Недостаточно остатков для некоторых товаров. Продолжить?",
            РежимДиалогаВопрос.ДаНет
        );
        
        Если Ответ = НЕ ДаНет.Да Тогда
            ВызватьИсключение "Проведение отменено пользователем";
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

Полный пример: РасходнаяНакладная

// Документ.РасходнаяНакладная

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

Ключевые моменты

  1. Всегда проверяй остатки перед проведением
  2. Используй транзакции для атомарности
  3. Блокируй данные чтобы избежать race conditions
  4. Создавай движения со знаком минус для расхода
  5. Обрабатывай ошибки информативно
  6. Логируй проблемы для аудита

Это гарантирует, что ваша система всегда содержит корректные данные об остатках!