Как в 1С добрать товар с других партий до нужного количества при нехватке остатка?

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

Рассмотрим распространенную прикладную задачу складского и партионного учета: клиент оформляет заказ или документ отгрузки на определенное количество товара. В информационной базе учет ведется в регистре накопления в разрезе номенклатуры и партий (документов поступления). На указанной партии нужного остатка нет или не хватает, поэтому программе требуется автоматически «добрать» недостающее количество с других доступных партий этого же товара по методу ФИФО (FIFO).

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

В чем сложность решения задачи «чистым» запросом

Выясним причину, почему распределение по партиям сложно сделать одной SQL-конструкцией в 1С. В классических СУБД для таких сценариев применяются оконные функции (например, SUM(...) OVER (PARTITION BY ... ORDER BY ...)) либо рекурсивные CTE, вычисляющие накопительный итог в один проход. Однако во встроенном языке запросов 1С оконные функции отсутствуют.

Чтобы рассчитать нарастающий итог средствами одного запроса 1С, разработчикам приходится соединять временную таблицу остатков саму с собой по нестрогому неравенству (сравнение дат или моментов времени партий) с последующей группировкой. При большом количестве партий такое соединение создает квадратичную сложность O(N²), перегружает временные таблицы СУБД и приводит к резкой деградации производительности. Поэтому попытка переложить весь «цикл» в запрос часто оказывается менее эффективной, чем правильная комбинация одного запроса и встроенного языка.

Решение 1. Оптимальный гибридный подход: единый запрос остатков и линейное распределение в цикле

Разберем по шагам наиболее производительный стандарт разработки в 1С:Предприятии. Суть подхода заключается в том, что запрос к СУБД выполняется ровно один раз для всех строк документа или заказа. Запрос получает текущие остатки с упорядочиванием по дате партии, а вычисление списываемых объемов производится в оперативной памяти с линейной сложностью O(N).

Посмотрим на пример реализации. Предположим, у нас есть таблица потребностей (строки заказа), где для номенклатуры указано требуемое количество.

  1. Передаем список требуемой номенклатуры в качестве параметра в виртуальную таблицу РегистрНакопления.ТоварыНаСкладах.Остатки.
  2. Сортируем выборку по номенклатуре и возрастанию момента времени документа партии (принцип ФИФО — старые партии списываются первыми).
  3. Обходим выборку или таблицу значений в цикле, последовательно списывая доступное количество партии и уменьшая потребность.

// 1. Формируем единый запрос на получение остатков
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
    |   ТоварыНаСкладахОстатки.Партия КАК Партия,
    |   ТоварыНаСкладахОстатки.ВНаличииОстаток КАК КоличествоОстаток
    |ИЗ
    |   РегистрНакопления.ТоварыНаСкладах.Остатки(
    |       &Период,
    |       Номенклатура В (&МассивНоменклатуры) И Склад = &Склад) КАК ТоварыНаСкладахОстатки
    |
    |УПОРЯДОЧИТЬ ПО
    |   ТоварыНаСкладахОстатки.Номенклатура,
    |   ТоварыНаСкладахОстатки.Партия.МоментВремени ВОЗР";

Запрос.УстановитьПараметр("Период", МоментВремени());
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНужныхТоваров);
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);

ТаблицаОстатков = Запрос.Выполнить().Выгрузить();

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

Такой механизм гарантирует высокое быстродействие: сервер баз данных выполняет один быстрый индексный поиск, а платформа 1С мгновенно рассчитывает партии без нагрузки на сеть и СУБД.

Решение 2. Расчет накопительного итога внутри запроса («ФИФО в запросе»)

Рассмотрим альтернативный способ, когда логику распределения необходимо полностью инкапсулировать в текст запроса. Этот прием используется, если результат требуется передать дальше в другие пакеты запроса или временные таблицы без возврата управления во встроенный язык 1С.

Разберем логику работы алгоритма:

  1. Помещаем во временную таблицу список требуемых списаний из заказа.
  2. Помещаем во вторую временную таблицу актуальные остатки партий.
  3. Соединяем таблицу остатков саму с собой по условию нестрогого неравенства моментов времени для подсчета суммы остатков всех предшествующих партий (включая текущую).
  4. С помощью условной конструкции ВЫБОР КОГДА вычисляем, сколько единиц необходимо взять именно из текущей партии.

ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.Партия КАК Партия,
    Остатки.КоличествоОстаток КАК КоличествоОстаток,
    СУММА(ОстаткиДо.КоличествоОстаток) КАК НакопленныйОстаток
ПОМЕСТИТЬ ВТ_ОстаткиСНакоплением
ИЗ
    ВТ_Остатки КАК Остатки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Остатки КАК ОстаткиДо
        ПО Остатки.Номенклатура = ОстаткиДо.Номенклатура
        И Остатки.Партия.МоментВремени >= ОстаткиДо.Партия.МоментВремени
СГРУППИРОВАТЬ ПО
    Остатки.Номенклатура,
    Остатки.Партия,
    Остатки.КоличествоОстаток
;

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

Обратим внимание: данный вариант корректно работает только при относительно небольшом количестве партий на одну позицию номенклатуры. Если у одной позиции сотни партий, соединение Остатки.Партия.МоментВремени >= ОстаткиДо.Партия.МоментВремени приведет к лавинообразному росту промежуточных записей.

Обеспечение целостности: управляемые блокировки

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

Чтобы избежать коллизий, перед выполнением запроса остатков необходимо заблокировать нужные позиции с помощью объекта БлокировкаДанных в режиме РежимБлокировкиДанных.Исключительный:


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

Установка управляемой блокировки гарантирует, что между моментом чтения остатков партий и записью движений списания другие сеансы не смогут перехватить распределяемые остатки.

← На главную