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

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

При работе с остатками товаров, особенно когда речь идет о скоропортящейся продукции или товарах со сроками годности, часто возникает задача распределить требуемое количество по имеющимся партиям, соблюдая определенные правила. Одно из таких правил – приоритет выбора товаров с ближайшим истекающим сроком годности (принцип FEFO – First Expired, First Out) или просто с наиболее ранними датами поступления (FIFO – First In, First Out). Мы разберем, как решить эту задачу в запросе 1С, когда необходимо "разбить" одно требуемое количество на несколько строк, каждая из которых соответствует определенной партии со своим сроком годности и остатком.

Понимание задачи: Распределение по срокам годности

Представим ситуацию, когда по заказу клиента требуется 4 штуки определенной номенклатуры. На складе же имеются две партии этого товара с разными сроками годности и остатками, например:

  1. Партия 1: срок годности 01.01.2022, остаток 3 шт.
  2. Партия 2: срок годности 02.01.2022, остаток 3 шт.

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

В нашем примере, для 4 требуемых штук, нам необходимо получить две строки в результате:

  1. 3 шт. со сроком годности 01.01.2022.
  2. 1 шт. со сроком годности 02.01.2022 (чтобы в сумме получить 4 шт.).

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

Шаг 1: Подготовка исходных данных

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

Сначала выберем товары из документа "Заказ клиента", которые нам необходимо распределить. Это будет наша первая временная таблица ВтПодбор:


ВЫБРАТЬ
    ЗаказКлиентаТовары.НоменклатураНабора КАК НоменклатураНабора,
    ЗаказКлиентаТовары.ХарактеристикаНабора КАК ХарактеристикаНабора,
    ЗаказКлиентаТовары.Номенклатура КАК Номенклатура,
    ЗаказКлиентаТовары.Характеристика КАК Характеристика,
    ЗаказКлиентаТовары.ВидЦены КАК ВидЦены,
    ЗаказКлиентаТовары.Цена КАК Цена,
    ЗаказКлиентаТовары.КоличествоУпаковок КАК КоличествоУпаковок,
    ЗаказКлиентаТовары.ПроцентРучнойСкидки КАК ПроцентРучнойСкидки,
    ЗаказКлиентаТовары.Склад КАК Склад,
    ЗаказКлиентаТовары.СрокПоставки КАК СрокПоставки,
    ЗаказКлиентаТовары.Серия КАК Серия
ПОМЕСТИТЬ ВтПодбор
ИЗ
    Документ.ЗаказКлиента.Товары КАК ЗаказКлиентаТовары
ГДЕ
    ЗаказКлиентаТовары.Ссылка = &Ссылка
;

Затем выберем текущие остатки товаров с указанием сроков годности из нашего пользовательского регистра накопления. Здесь важно учитывать, что для решения задачи мы работаем с остатками по конкретному складу и на определенную дату (актуальную на момент формирования перемещения). Результат поместим во временную таблицу ВтСрокиГодности:


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

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


////////////////////////////////////////////////////////////­////////////////////
ВЫБРАТЬ
    ВтПодбор.НоменклатураНабора КАК НоменклатураНабора,
    ВтПодбор.ХарактеристикаНабора КАК ХарактеристикаНабора,
    ВтПодбор.Номенклатура КАК Номенклатура,
    ВтПодбор.Характеристика КАК Характеристика,
    ВтПодбор.ВидЦены КАК ВидЦены,
    ВтПодбор.Цена КАК Цена,
    ВтПодбор.КоличествоУпаковок КАК КоличествоУпаковок,
    ВтПодбор.ПроцентРучнойСкидки КАК ПроцентРучнойСкидки,
    ВтПодбор.Склад КАК Склад,
    ВтПодбор.СрокПоставки КАК СрокПоставки,
    ВтПодбор.Серия КАК Серия,
    ВтСрокиГодности.ДатаОкончанияСрокаГодности КАК ДатаОкончанияСрокаГодности,
    ВтСрокиГодности.КоличествоОстаток КАК КоличествоОстаток
ИЗ
    ВтПодбор КАК ВтПодбор
        ЛЕВОЕ СОЕДИНЕНИЕ ВтСрокиГодности КАК ВтСрокиГодности
        ПО ВтПодбор.Номенклатура = ВтСрокиГодности.Номенклатура

В этом запросе мы соединяем требуемые позиции из заказа с доступными остатками по срокам годности. Теперь у нас есть база для расчета распределения.

Шаг 2: Расчет нарастающего итога (кумулятивной суммы)

Ключевым моментом в решении нашей задачи является вычисление нарастающего итога (или кумулятивной суммы) для количества остатков по срокам годности. Это позволяет нам для каждой партии товара знать, сколько всего товара доступно, если брать партии по порядку их сроков годности (от самых ранних к самым поздним). Несмотря на отсутствие прямых оконных функций типа `SUM() OVER (ORDER BY ...)` в стандартном языке запросов 1С, мы можем эмулировать их функционал с помощью самосоединения временной таблицы.

Мы будем использовать временную таблицу, содержащую данные по остаткам и срокам годности, и соединим ее саму с собой. Важно отсортировать данные по `ДатаОкончанияСрокаГодности` в возрастающем порядке, чтобы обеспечить выборку товаров с ближайшими сроками годности в первую очередь.

Рассмотрим следующий запрос, который рассчитывает нарастающий итог и помещает его во временную таблицу Пром:


ВЫБРАТЬ
    Таб.СрокГодности,
    Таб.Товар,
    Таб.Количество,
    Сумма(Таб2.Количество) КАК КолНИ
ПОМЕСТИТЬ Пром
ИЗ
    Таб КАК Таб
        ЛЕВОЕ СОЕДИНЕНИЕ Таб КАК Таб2
        ПО Таб.СрокГодности >= Таб2.СрокГодности
            И Таб.Товар = Таб2.Товар
ГДЕ
    Таб2.Количество > 0 // Важно: учитываем только положительные остатки!
СГРУППИРОВАТЬ ПО
    Таб.СрокГодности,
    Таб.Товар,
    Таб.Количество
;

Давайте подробно разберем этот запрос:

  1. Исходная таблица Таб: Предположим, что Таб – это наша временная таблица ВтСрокиГодности или результат ее соединения с ВтПодбор, содержащая поля СрокГодности (аналог `ДатаОкончанияСрокаГодности`), Товар (аналог `Номенклатура`) и Количество (аналог `КоличествоОстаток`). Для примера, возьмем упрощенную структуру:
    
    выбрать ДатаВремя(2022,1,1) как СрокГодности, 2 как Количество, "Товар А" как Товар поместить Таб объединить все
    выбрать ДатаВремя(2022,1,2), 3, "Товар А" объединить все
    выбрать ДатаВремя(2022,1,3), 4, "Товар А"
    ;
    

    Для "Товар А" у нас есть три партии.

  2. Самосоединение Таб КАК Таб с Таб КАК Таб2: Мы соединяем таблицу с самой собой. Это ключевой момент для расчета нарастающего итога.
    • Условие Таб.СрокГодности >= Таб2.СрокГодности: Для каждой строки из "левой" таблицы (Таб) мы находим все строки из "правой" таблицы (Таб2), у которых срок годности меньше или равен сроку годности текущей строки. Это гарантирует, что при группировке мы будем суммировать количества всех предыдущих и текущей партии.
    • Условие И Таб.Товар = Таб2.Товар: Обязательно добавляем условие для номенклатуры, чтобы расчет нарастающего итога происходил только в рамках одной номенклатуры.
  3. Где Таб2.Количество > 0: Это очень важное уточнение, предложенное в ходе обсуждения. Оно позволяет исключить из расчета нарастающего итога партии с отрицательными или нулевыми остатками, что предотвращает некорректное распределение. Мы заинтересованы только в реально доступном количестве.
  4. Функция Сумма(Таб2.Количество) КАК КолНИ: Мы суммируем количества из "правой" таблицы (Таб2), которая, благодаря условию соединения, содержит все предыдущие и текущие количества. Это и есть наш нарастающий итог.

    Важно: как было верно замечено, здесь следует использовать Сумма(Таб2.Количество), а не Сумма(Таб.Количество). Если бы мы использовали Сумма(Таб.Количество), то нарастающий итог для каждой строки был бы равен количеству * этой же * строки (если не было других условий), а не сумме по предыдущим. Таб2.Количество гарантирует, что мы суммируем по всем *подходящим* строкам из Таб2.

  5. Группировка: Мы группируем по полям СрокГодности, Товар и Количество из основной таблицы (Таб), чтобы для каждой уникальной партии получить один рассчитанный нарастающий итог.

В результате выполнения этого запроса Пром будет содержать следующие данные (для &РаспределяемоеКол = 4):

СрокГодности Товар Количество (ТК) КолНИ (Нарастающий Итог)
01.01.2022 Товар А 2 2
02.01.2022 Товар А 3 5
03.01.2022 Товар А 4 9

Шаг 3: Распределение требуемого количества по партиям

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

Для этого мы используем конструкцию ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ. В качестве параметра запроса мы будем передавать &РаспределяемоеКол – то самое требуемое количество из заказа клиента.


ВЫБРАТЬ
    Пром.СрокГодности,
    Пром.Товар,
    ВЫБОР
        КОГДА Пром.КолНИ - Пром.Количество >= &РаспределяемоеКол
            ТОГДА 0
        КОГДА Пром.КолНИ < &РаспределяемоеКол
            ТОГДА Пром.Количество
        ИНАЧЕ &РаспределяемоеКол - (Пром.КолНИ - Пром.Количество)
    КОНЕЦ КАК КоличествоКОтгрузке
ИЗ
    Пром КАК Пром
ГДЕ
    ВЫБОР
        КОГДА Пром.КолНИ - Пром.Количество >= &РаспределяемоеКол
            ТОГДА 0
        КОГДА Пром.КолНИ < &РаспределяемоеКол
            ТОГДА Пром.Количество
        ИНАЧЕ &РаспределяемоеКол - (Пром.КолНИ - Пром.Количество)
    КОНЕЦ > 0
УПОРЯДОЧИТЬ ПО
    Пром.СрокГодности
;

Давайте подробно проанализируем логику поля КоличествоКОтгрузке:

Для лучшего понимания введем переменные:

Теперь разберем каждое условие:

  1. КОГДА НИ_Пред >= РК ТОГДА 0:
    • Что это значит: Если требуемое количество РК уже полностью покрыто или превышено количеством из предыдущих партий (НИ_Пред), то из текущей партии нам ничего брать не нужно.
    • Пример: Если нам нужно 4 шт., а из предыдущих партий уже набрано 5 шт., то из текущей партии мы берем 0.
  2. КОГДА НИ < РК ТОГДА ТК:
    • Что это значит: Если даже с учетом количества из текущей партии (т.е. НИ), общее набранное количество все еще меньше требуемого РК, это означает, что нам нужно взять всё количество ТК из текущей партии, потому что его недостаточно для полного покрытия потребности, и мы продолжим набирать из следующих партий.
    • Пример: Если нам нужно 4 шт., а НИ для текущей партии составляет 2 шт., то мы берем все 2 шт. из этой партии, так как 2 < 4.
  3. ИНАЧЕ РК - НИ_Пред:
    • Что это значит: Это "промежуточный" случай. Он возникает, когда НИ_Пред < РК (то есть предыдущие партии не покрыли всю потребность, нам еще нужно) И НИ >= РК (то есть текущая партия либо полностью закрывает оставшуюся потребность, либо ее часть). В этом случае мы берем ровно столько, сколько осталось от требуемого количества после учета всех предыдущих партий. Это рассчитывается как РК - НИ_Пред.
    • Пример: Если нам нужно 4 шт., НИ_Пред = 2 (предыдущие дали 2 шт.), а НИ = 5 (текущая партия 3 шт., итого 2+3=5). Мы видим, что НИ_Пред (2) < РК (4), но НИ (5) >= РК (4). Значит, из текущей партии нужно взять 4 - 2 = 2 шт.

Условие ГДЕ ВЫБОР ... КОНЕЦ > 0 в конце запроса фильтрует строки, в которых КоличествоКОтгрузке оказалось равно 0. Это позволяет получать только те партии, из которых фактически что-то берется.

УПОРЯДОЧИТЬ ПО Пром.СрокГодности гарантирует, что строки в результате будут отсортированы по срокам годности, что удобно для восприятия и дальнейшей обработки.

Пример работы запроса

Рассмотрим, как этот запрос работает на нашем примере с требуемым количеством `&РаспределяемоеКол = 4`:

СрокГодности Товар Количество (ТК) КолНИ (НИ) НИ_Пред (НИ - ТК) РаспределяемоеКол (РК) КоличествоКОтгрузке (вычисление) Результат
01.01.2022 Товар А 2 2 0 4 КОГДА НИ_Пред (0) >= РК (4) => Ложь.
КОГДА НИ (2) < РК (4) => Истина.
ТОГДА ТК (2)
2
02.01.2022 Товар А 3 5 2 4 КОГДА НИ_Пред (2) >= РК (4) => Ложь.
КОГДА НИ (5) < РК (4) => Ложь.
ИНАЧЕ РК (4) - НИ_Пред (2)
2
03.01.2022 Товар А 4 9 5 4 КОГДА НИ_Пред (5) >= РК (4) => Истина.
ТОГДА 0
0

Финальный результат (после фильтрации по КоличествоКОтгрузке > 0) будет:

СрокГодности Товар КоличествоКОтгрузке
01.01.2022 Товар А 2
02.01.2022 Товар А 2

В сумме мы получаем 4 штуки, распределенные по двум партиям согласно нашим требованиям.

Давайте рассмотрим еще один пример с требуемым количеством &РаспределяемоеКол = 6, используя те же исходные данные (2, 3, 4 штуки по срокам 01.01, 02.01, 03.01):

СрокГодности Товар Количество (ТК) КолНИ (НИ) НИ_Пред (НИ - ТК) РаспределяемоеКол (РК) КоличествоКОтгрузке (вычисление) Результат
01.01.2022 Товар А 2 2 0 6 КОГДА НИ_Пред (0) >= РК (6) => Ложь.
КОГДА НИ (2) < РК (6) => Истина.
ТОГДА ТК (2)
2
02.01.2022 Товар А 3 5 2 6 КОГДА НИ_Пред (2) >= РК (6) => Ложь.
КОГДА НИ (5) < РК (6) => Истина.
ТОГДА ТК (3)
3
03.01.2022 Товар А 4 9 5 6 КОГДА НИ_Пред (5) >= РК (6) => Ложь.
КОГДА НИ (9) < РК (6) => Ложь.
ИНАЧЕ РК (6) - НИ_Пред (5)
1

Финальный результат (после фильтрации по КоличествоКОтгрузке > 0) будет:

СрокГодности Товар КоличествоКОтгрузке
01.01.2022 Товар А 2
02.01.2022 Товар А 3
03.01.2022 Товар А 1

В этом случае, для требуемых 6 штук, мы получили 3 строки, распределенные по самым ранним срокам годности.

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

Хотя запрос с нарастающим итогом является мощным инструментом, его написание и отладка могут быть достаточно сложными, особенно для программистов, не очень хорошо знакомых с тонкими нюансами SQL-подобных запросов в 1С. В некоторых случаях, особенно при очень большом количестве строк остатков, производительность такого запроса может быть неоптимальной, хотя чаще всего он работает достаточно быстро.

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

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

Преимущества постобработки кодом:

Недостатки постобработки кодом:

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

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

Заключение:

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

← На главную