При работе с остатками товаров, особенно когда речь идет о скоропортящейся продукции или товарах со сроками годности, часто возникает задача распределить требуемое количество по имеющимся партиям, соблюдая определенные правила. Одно из таких правил – приоритет выбора товаров с ближайшим истекающим сроком годности (принцип FEFO – First Expired, First Out) или просто с наиболее ранними датами поступления (FIFO – First In, First Out). Мы разберем, как решить эту задачу в запросе 1С, когда необходимо "разбить" одно требуемое количество на несколько строк, каждая из которых соответствует определенной партии со своим сроком годности и остатком.
Представим ситуацию, когда по заказу клиента требуется 4 штуки определенной номенклатуры. На складе же имеются две партии этого товара с разными сроками годности и остатками, например:
Если мы просто возьмем 4 штуки из первой попавшейся партии, мы можем нарушить принцип ротации запасов. Наша цель – взять сначала все, что есть из партии с самым ранним сроком годности, а затем, если этого недостаточно, добрать остаток из следующей по сроку годности партии.
В нашем примере, для 4 требуемых штук, нам необходимо получить две строки в результате:
Такое распределение необходимо выполнить средствами запроса 1С, что требует использования механизма нарастающего итога (кумулятивной суммы).
Прежде чем приступить к расчету, нам необходимо получить исходные данные: требуемое количество по заказу клиента и информацию об остатках товара по срокам годности. Мы будем использовать временные таблицы для удобства.
Сначала выберем товары из документа "Заказ клиента", которые нам необходимо распределить. Это будет наша первая временная таблица ВтПодбор:
ВЫБРАТЬ
ЗаказКлиентаТовары.НоменклатураНабора КАК НоменклатураНабора,
ЗаказКлиентаТовары.ХарактеристикаНабора КАК ХарактеристикаНабора,
ЗаказКлиентаТовары.Номенклатура КАК Номенклатура,
ЗаказКлиентаТовары.Характеристика КАК Характеристика,
ЗаказКлиентаТовары.ВидЦены КАК ВидЦены,
ЗаказКлиентаТовары.Цена КАК Цена,
ЗаказКлиентаТовары.КоличествоУпаковок КАК КоличествоУпаковок,
ЗаказКлиентаТовары.ПроцентРучнойСкидки КАК ПроцентРучнойСкидки,
ЗаказКлиентаТовары.Склад КАК Склад,
ЗаказКлиентаТовары.СрокПоставки КАК СрокПоставки,
ЗаказКлиентаТовары.Серия КАК Серия
ПОМЕСТИТЬ ВтПодбор
ИЗ
Документ.ЗаказКлиента.Товары КАК ЗаказКлиентаТовары
ГДЕ
ЗаказКлиентаТовары.Ссылка = &Ссылка
;
Затем выберем текущие остатки товаров с указанием сроков годности из нашего пользовательского регистра накопления. Здесь важно учитывать, что для решения задачи мы работаем с остатками по конкретному складу и на определенную дату (актуальную на момент формирования перемещения). Результат поместим во временную таблицу ВтСрокиГодности:
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
КНВ_СрокиГодностиОстатки.Номенклатура КАК Номенклатура,
КНВ_СрокиГодностиОстатки.ДатаОкончанияСрокаГодности КАК ДатаОкончанияСрокаГодности,
КНВ_СрокиГодностиОстатки.КоличествоОстаток КАК КоличествоОстаток
ПОМЕСТИТЬ ВтСрокиГодности
ИЗ
РегистрНакопления.КНВ_СрокиГодности.Остатки(&Дата, Склад = &Склад) КАК КНВ_СрокиГодностиОстатки
;
Наконец, мы объединяем эти две временные таблицы, чтобы получить общую картину по требуемым товарам и их доступным остаткам со сроками годности. Это позволит нам работать с единым набором данных для дальнейших расчетов.
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВтПодбор.НоменклатураНабора КАК НоменклатураНабора,
ВтПодбор.ХарактеристикаНабора КАК ХарактеристикаНабора,
ВтПодбор.Номенклатура КАК Номенклатура,
ВтПодбор.Характеристика КАК Характеристика,
ВтПодбор.ВидЦены КАК ВидЦены,
ВтПодбор.Цена КАК Цена,
ВтПодбор.КоличествоУпаковок КАК КоличествоУпаковок,
ВтПодбор.ПроцентРучнойСкидки КАК ПроцентРучнойСкидки,
ВтПодбор.Склад КАК Склад,
ВтПодбор.СрокПоставки КАК СрокПоставки,
ВтПодбор.Серия КАК Серия,
ВтСрокиГодности.ДатаОкончанияСрокаГодности КАК ДатаОкончанияСрокаГодности,
ВтСрокиГодности.КоличествоОстаток КАК КоличествоОстаток
ИЗ
ВтПодбор КАК ВтПодбор
ЛЕВОЕ СОЕДИНЕНИЕ ВтСрокиГодности КАК ВтСрокиГодности
ПО ВтПодбор.Номенклатура = ВтСрокиГодности.Номенклатура
В этом запросе мы соединяем требуемые позиции из заказа с доступными остатками по срокам годности. Теперь у нас есть база для расчета распределения.
Ключевым моментом в решении нашей задачи является вычисление нарастающего итога (или кумулятивной суммы) для количества остатков по срокам годности. Это позволяет нам для каждой партии товара знать, сколько всего товара доступно, если брать партии по порядку их сроков годности (от самых ранних к самым поздним). Несмотря на отсутствие прямых оконных функций типа `SUM() OVER (ORDER BY ...)` в стандартном языке запросов 1С, мы можем эмулировать их функционал с помощью самосоединения временной таблицы.
Мы будем использовать временную таблицу, содержащую данные по остаткам и срокам годности, и соединим ее саму с собой. Важно отсортировать данные по `ДатаОкончанияСрокаГодности` в возрастающем порядке, чтобы обеспечить выборку товаров с ближайшими сроками годности в первую очередь.
Рассмотрим следующий запрос, который рассчитывает нарастающий итог и помещает его во временную таблицу Пром:
ВЫБРАТЬ
Таб.СрокГодности,
Таб.Товар,
Таб.Количество,
Сумма(Таб2.Количество) КАК КолНИ
ПОМЕСТИТЬ Пром
ИЗ
Таб КАК Таб
ЛЕВОЕ СОЕДИНЕНИЕ Таб КАК Таб2
ПО Таб.СрокГодности >= Таб2.СрокГодности
И Таб.Товар = Таб2.Товар
ГДЕ
Таб2.Количество > 0 // Важно: учитываем только положительные остатки!
СГРУППИРОВАТЬ ПО
Таб.СрокГодности,
Таб.Товар,
Таб.Количество
;
Давайте подробно разберем этот запрос:
Таб: Предположим, что Таб – это наша временная таблица ВтСрокиГодности или результат ее соединения с ВтПодбор, содержащая поля СрокГодности (аналог `ДатаОкончанияСрокаГодности`), Товар (аналог `Номенклатура`) и Количество (аналог `КоличествоОстаток`). Для примера, возьмем упрощенную структуру:
выбрать ДатаВремя(2022,1,1) как СрокГодности, 2 как Количество, "Товар А" как Товар поместить Таб объединить все
выбрать ДатаВремя(2022,1,2), 3, "Товар А" объединить все
выбрать ДатаВремя(2022,1,3), 4, "Товар А"
;
Для "Товар А" у нас есть три партии.
Таб КАК Таб с Таб КАК Таб2: Мы соединяем таблицу с самой собой. Это ключевой момент для расчета нарастающего итога.
Таб.СрокГодности >= Таб2.СрокГодности: Для каждой строки из "левой" таблицы (Таб) мы находим все строки из "правой" таблицы (Таб2), у которых срок годности меньше или равен сроку годности текущей строки. Это гарантирует, что при группировке мы будем суммировать количества всех предыдущих и текущей партии.И Таб.Товар = Таб2.Товар: Обязательно добавляем условие для номенклатуры, чтобы расчет нарастающего итога происходил только в рамках одной номенклатуры.Таб2.Количество > 0: Это очень важное уточнение, предложенное в ходе обсуждения. Оно позволяет исключить из расчета нарастающего итога партии с отрицательными или нулевыми остатками, что предотвращает некорректное распределение. Мы заинтересованы только в реально доступном количестве.Сумма(Таб2.Количество) КАК КолНИ: Мы суммируем количества из "правой" таблицы (Таб2), которая, благодаря условию соединения, содержит все предыдущие и текущие количества. Это и есть наш нарастающий итог.
Важно: как было верно замечено, здесь следует использовать Сумма(Таб2.Количество), а не Сумма(Таб.Количество). Если бы мы использовали Сумма(Таб.Количество), то нарастающий итог для каждой строки был бы равен количеству * этой же * строки (если не было других условий), а не сумме по предыдущим. Таб2.Количество гарантирует, что мы суммируем по всем *подходящим* строкам из Таб2.
СрокГодности, Товар и Количество из основной таблицы (Таб), чтобы для каждой уникальной партии получить один рассчитанный нарастающий итог.В результате выполнения этого запроса Пром будет содержать следующие данные (для &РаспределяемоеКол = 4):
| СрокГодности | Товар | Количество (ТК) | КолНИ (Нарастающий Итог) |
|---|---|---|---|
| 01.01.2022 | Товар А | 2 | 2 |
| 02.01.2022 | Товар А | 3 | 5 |
| 03.01.2022 | Товар А | 4 | 9 |
Имея рассчитанный нарастающий итог, мы можем выполнить финальный шаг – определить, сколько товара необходимо взять из каждой партии, чтобы удовлетворить требуемое количество, соблюдая приоритет по срокам годности.
Для этого мы используем конструкцию ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ. В качестве параметра запроса мы будем передавать &РаспределяемоеКол – то самое требуемое количество из заказа клиента.
ВЫБРАТЬ
Пром.СрокГодности,
Пром.Товар,
ВЫБОР
КОГДА Пром.КолНИ - Пром.Количество >= &РаспределяемоеКол
ТОГДА 0
КОГДА Пром.КолНИ < &РаспределяемоеКол
ТОГДА Пром.Количество
ИНАЧЕ &РаспределяемоеКол - (Пром.КолНИ - Пром.Количество)
КОНЕЦ КАК КоличествоКОтгрузке
ИЗ
Пром КАК Пром
ГДЕ
ВЫБОР
КОГДА Пром.КолНИ - Пром.Количество >= &РаспределяемоеКол
ТОГДА 0
КОГДА Пром.КолНИ < &РаспределяемоеКол
ТОГДА Пром.Количество
ИНАЧЕ &РаспределяемоеКол - (Пром.КолНИ - Пром.Количество)
КОНЕЦ > 0
УПОРЯДОЧИТЬ ПО
Пром.СрокГодности
;
Давайте подробно проанализируем логику поля КоличествоКОтгрузке:
Для лучшего понимания введем переменные:
РК = &РаспределяемоеКол (Требуемое количество)НИ = Пром.КолНИ (Нарастающий итог для текущей партии, включая ее количество)ТК = Пром.Количество (Количество в текущей партии)НИ_Пред = Пром.КолНИ - Пром.Количество (Нарастающий итог до текущей партии, т.е. сумма количеств всех предыдущих партий)Теперь разберем каждое условие:
КОГДА НИ_Пред >= РК ТОГДА 0:
РК уже полностью покрыто или превышено количеством из предыдущих партий (НИ_Пред), то из текущей партии нам ничего брать не нужно.КОГДА НИ < РК ТОГДА ТК:
НИ), общее набранное количество все еще меньше требуемого РК, это означает, что нам нужно взять всё количество ТК из текущей партии, потому что его недостаточно для полного покрытия потребности, и мы продолжим набирать из следующих партий.НИ для текущей партии составляет 2 шт., то мы берем все 2 шт. из этой партии, так как 2 < 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С.
ТекущееТребуемоеКоличество = &РаспределяемоеКол;
РезультатРаспределения = Новый ТаблицаЗначений;
РезультатРаспределения.Колонки.Добавить("СрокГодности");
РезультатРаспределения.Колонки.Добавить("Товар");
РезультатРаспределения.Колонки.Добавить("КоличествоКОтгрузке");
Для Каждого СтрокаОстатка Из ТаблицаОстатковПоСрокамГодности Цикл
Если ТекущееТребуемоеКоличество <= 0 Тогда
Прервать; // Всю потребность удовлетворили
КонецЕсли;
КоличествоИзТекущейПартии = СтрокаОстатка.КоличествоОстаток;
Если КоличествоИзТекущейПартии <= 0 Тогда
Продолжить; // Пропускаем партии без остатка
КонецЕсли;
КоличествоКВзятию = Мин(ТекущееТребуемоеКоличество, КоличествоИзТекущейПартии);
Если КоличествоКВзятию > 0 Тогда
НоваяСтрока = РезультатРаспределения.Добавить();
НоваяСтрока.СрокГодности = СтрокаОстатка.СрокГодности;
НоваяСтрока.Товар = СтрокаОстатка.Товар;
НоваяСтрока.КоличествоКОтгрузке = КоличествоКВзятию;
ТекущееТребуемоеКоличество = ТекущееТребуемоеКоличество - КоличествоКВзятию;
КонецЕсли;
КонецЦикла;
Преимущества постобработки кодом:
Недостатки постобработки кодом:
Выбор между решением в запросе и постобработкой кодом зависит от конкретных условий: объема данных, требуемой производительности, сложности логики и предпочтений разработчика.
В большинстве случаев для подобных задач, где требуется распределение по приоритету (например, по срокам годности), решение с нарастающим итогом в запросе является более элегантным и производительным, так как оно максимально использует возможности СУБД.
Заключение:
Мы рассмотрели детальное решение задачи по распределению требуемого количества товара по партиям с учетом сроков годности в запросе 1С. Использование нарастающего итога, реализованного через самосоединение временных таблиц, является мощным и гибким инструментом для подобных сценариев. Мы проанализировали каждый шаг построения запроса, включая важные корректировки для фильтрации отрицательных остатков и правильного суммирования. Данный подход позволяет получить точный и структурированный результат, который затем можно использовать для формирования документов перемещения или других операций.