При работе с отчетами в 1С, особенно теми, что используют Систему Компоновки Данных (СКД) и детальный учет по сериям товаров, мы часто сталкиваемся с необходимостью точно отображать данные. Одна из распространенных задач — показать обороты по сериям товаров, но с учетом их текущего наличия. То есть, если товар определенной серии продан, мы хотим видеть его оборот как ноль, а не исходное количество, и при этом избежать появления «старых» серий в отчете, когда поступает новый товар с похожим названием или сроком годности.
Давайте вместе разберем эту ситуацию и предложим эффективное решение.
Автор столкнулся с ситуацией, когда отчет, построенный на регистре накопления ДвиженияСерийТоваров.Обороты, показывал неверные данные для проданных серий товаров. Даже если товар определенной серии был полностью реализован и его физически не осталось на складе, отчет продолжал отображать по этой серии обороты, а иногда и вовсе дублировал ее, показывая старую серию с количеством из нового поступления.
Ключевые моменты проблемы:
КоличествоОборот отображалось как ноль.Предоставленный автором запрос СКД является достаточно сложным, но мы сосредоточимся на тех его частях, которые касаются получения данных по остаткам и оборотам серий. Запрос объединяет данные из нескольких источников:
Справочник.НоменклатураРегистрНакопления.ТоварыНаСкладах.ОстаткиРегистрСведений.ШтрихкодыРегистрСведений.ЦеныНоменклатуры.СрезПоследнихРегистрНакопления.ДвиженияСерийТоваров.ОборотыДавайте посмотрим на основные моменты, связанные с остатками и оборотами:
ВЫБРАТЬ РАЗРЕШЕННЫЕ РАЗЛИЧНЫЕ
...
ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) КАК Остаток1,
ЕСТЬNULL(ТоварыНаСкладахОстатки.РезервОстаток, 0) КАК Резерв,
ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) - ЕСТЬNULL(ТоварыНаСкладахОстатки.РезервОстаток, 0) КАК СвободныйОстаток,
ВЫРАЗИТЬ(NULL КАК ЧИСЛО(15, 2)) КАК Остаток,
ТоварыНаСкладахОстатки.Склад КАК Склад,
...
ДвиженияСерийТоваровОбороты.Серия КАК Серия,
ДвиженияСерийТоваровОбороты.Номенклатура КАК Номенклатура1,
ДвиженияСерийТоваровОбороты.Характеристика КАК Характеристика1,
ДвиженияСерийТоваровОбороты.Документ КАК Документ,
ДвиженияСерийТоваровОбороты.Магазин КАК Магазин,
ДвиженияСерийТоваровОбороты.СкладскаяОперация КАК СкладскаяОперация,
МАКСИМУМ(ВЫБОР КОГДА ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеИзлишков) ИЛИ ... ТОГДА ДвиженияСерийТоваровОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК Приемка,
ВЫБОР КОГДА ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаВРозницу) ИЛИ ... ТОГДА ДвиженияСерийТоваровОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ КАК Отгрузка,
...
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
...
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияСерийТоваров.Обороты КАК ДвиженияСерийТоваровОбороты ПО (ДвиженияСерийТоваровОбороты.Номенклатура = СправочникНоменклатура.Ссылка)
...
Ключевые наблюдения:
ТоварыНаСкладахОстатки: Для получения остатков используется виртуальная таблица ТоварыНаСкладах.Остатки. Этот регистр обычно хранит общие остатки по номенклатуре (и, возможно, характеристике/складу), но не на уровне конкретной серии. Соответственно, он не может дать точную информацию о наличии *конкретной серии*.ДвиженияСерийТоваров.Обороты: Таблица ДвиженияСерийТоваров.Обороты соединяется с СправочникНоменклатура (через ЛЕВОЕ СОЕДИНЕНИЕ), но напрямую не соединяется с таблицей остатков *по сериям*. Это означает, что для каждой номенклатуры мы получаем все ее обороты по сериям за период, независимо от того, есть ли эти серии сейчас в наличии.ДвиженияСерийТоваровОбороты, такие как Серия, Характеристика, Документ, Магазин, СкладскаяОперация, а также агрегатные функции МАКСИМУМ для расчета Приемка и Отгрузка. Эта группировка, в сочетании с РАЗРЕШЕННЫЕ РАЗЛИЧНЫЕ, может приводить к тому, что по одной и той же номенклатуре, но разным оборотам (например, приход и расход одной и той же серии в разные моменты времени) будут формироваться отдельные строки, даже если серия уже продана.Причина проблемы с дублированием:
Основная проблема заключается в том, что ДвиженияСерийТоваровОбороты просто фиксирует факт движения товара по серии. Если серия А была принята (оборот прихода) и затем продана (оборот расхода), обе записи остаются в таблице оборотов за соответствующий период. Если позже товар с *тем же идентификатором серии* (например, тот же срок годности, который является частью наименования серии) поступает вновь, то отчет, построенный только на оборотах, будет показывать все движения по этой "серии", не различая старые и новые поступления, если не использовать актуальные остатки по сериям. Это создает иллюзию, что "старая" серия все еще присутствует или что ее количество изменилось на количество из нового поступления.
Поле Серия в регистре ДвиженияСерийТоваров предназначено для учета конкретных партий, серийных номеров, сроков годности. Если "Серия" не является абсолютно уникальным идентификатором для каждой физической партии товара, а, например, просто хранит срок годности (который может быть одинаковым у разных партий одного и того же товара, но разных по дате производства), то мы столкнемся с проблемой, когда одна и та же "серия" относится к разным физическим товарам.
В данном случае, если "старый срок годности" означает значение поля Серия, то при поступлении нового товара с аналогичным значением Серия, все обороты по этому значению серии будут агрегироваться, что и приводит к некорректным данным.
Для корректного решения проблемы нам необходимо использовать не только обороты, но и текущие остатки по каждой конкретной серии. Виртуальная таблица РегистрНакопления.ДвиженияСерийТоваров.Остатки предоставляет именно такую информацию – актуальный остаток для каждой уникальной комбинации "Номенклатура", "Характеристика" (если используется) и "Серия" на заданный момент времени (конец периода отчета).
Мы предлагаем следующий подход:
РегистрНакопления.ДвиженияСерийТоваров.Обороты. Это гарантирует, что мы увидим все движения по всем сериям, которые были в обороте за выбранный период.ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияСерийТоваров.Обороты с РегистрНакопления.ДвиженияСерийТоваров.Остатки. Это позволит для каждого оборота по серии получить ее текущий остаток.Номенклатура, Характеристика (если ведется) и Серия. Именно эти поля формируют уникальность записи в регистре остатков по сериям.КоличествоОборот. Если остаток по серии равен нулю, то оборот будем показывать как ноль.Давайте модифицируем ваш запрос, чтобы он учитывал остатки по сериям. Мы сосредоточимся на ключевых таблицах ДвиженияСерийТоваров.Обороты и ДвиженияСерийТоваров.Остатки.
Для начала, нам нужно добавить эту таблицу в секцию ИЗ вашего запроса и корректно ее соединить.
ИЗ
Справочник.Номенклатура КАК СправочникНоменклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
ПО (ТоварыНаСкладахОстатки.Номенклатура = СправочникНоменклатура.Ссылка)
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Штрихкоды КАК Штрихкоды
ПО (Штрихкоды.Владелец = СправочникНоменклатура.Ссылка)
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних
ПО (ЦеныНоменклатурыСрезПоследних.Номенклатура = СправочникНоменклатура.Ссылка)
// Исходное соединение с оборотами, мы его немного переделаем
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияСерийТоваров.Обороты КАК ДвиженияСерийТоваровОбороты
ПО (ДвиженияСерийТоваровОбороты.Номенклатура = СправочникНоменклатура.Ссылка)
// **** ДОБАВЛЯЕМ НОВОЕ СОЕДИНЕНИЕ С ОСТАТКАМИ ПО СЕРИЯМ ****
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияСерийТоваров.Остатки КАК ДвиженияСерийТоваровОстатки
ПО (ДвиженияСерийТоваровОстатки.Номенклатура = ДвиженияСерийТоваровОбороты.Номенклатура
И ДвиженияСерийТоваровОстатки.Характеристика = ДвиженияСерийТоваровОбороты.Характеристика
И ДвиженияСерийТоваровОстатки.Серия = ДвиженияСерийТоваровОбороты.Серия)
Важные моменты соединения:
ДвиженияСерийТоваровОбороты (как основной источник данных по движениям) с ДвиженияСерийТоваровОстатки.Номенклатура, Характеристика (если используется) и Серия. Это гарантирует, что мы получим остаток именно для той конкретной серии, о которой идет речь в строке оборота.ЛЕВОЕ СОЕДИНЕНИЕ, чтобы даже те обороты по сериям, которые уже не имеют остатка (полностью проданы), все равно присутствовали в выборке (но их остаток будет NULL).Теперь, когда у нас есть доступ к актуальному остатку по серии, мы можем изменить поля Приемка и Отгрузка, а также добавить новое поле, которое будет показывать общее количество оборота, обнуляя его при отсутствии остатка.
В секции ВЫБРАТЬ вашего запроса:
ТекущийОстатокСерии из новой таблицы ДвиженияСерийТоваровОстатки.Приемка и Отгрузка, чтобы они учитывали текущий остаток, если это необходимо по логике отчета. Если мы хотим показывать обороты только для тех серий, у которых есть остаток, тогда необходимо добавить соответствующее условие.КоличествоОборотКорректированное, которое будет показывать 0, если ТекущийОстатокСерии равен 0.
ВЫБРАТЬ РАЗРЕШЕННЫЕ РАЗЛИЧНЫЕ
...
// Старые поля остатков по номенклатуре, которые могут быть полезны, но не для детализации по сериям
ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) КАК ОбщийОстатокНоменклатуры,
ЕСТЬNULL(ТоварыНаСкладахОстатки.РезервОстаток, 0) КАК ОбщийРезервНоменклатуры,
ЕСТЬNULL(ТоварыНаСкладахОстатки.КоличествоОстаток, 0) - ЕСТЬNULL(ТоварыНаСкладахОстатки.РезервОстаток, 0) КАК ОбщийСвободныйОстатокНоменклатуры,
...
ДвиженияСерийТоваровОбороты.Серия КАК Серия,
ДвиженияСерийТоваровОбороты.Номенклатура КАК Номенклатура1,
ДвиженияСерийТоваровОбороты.Характеристика КАК Характеристика1,
...
ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) КАК ТекущийОстатокСерии, // Вот оно! Актуальный остаток по конкретной серии
// Модифицированное поле Приемка: показываем оборот только если есть остаток по серии
ВЫБОР
КОГДА ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) > 0 И (
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеИзлишков) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаКомплектующихПослеРазборки) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаОтПоставщика) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаПоВозвратуОтКлиента) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаПоПеремещению) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаПоПрочемуОприходованию) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ПриемкаСобранныхКомплектов) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ВводОстатков) И ДвиженияСерийТоваровОбороты.КоличествоОборот > 0 ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеРезультатовПересчетов) И ДвиженияСерийТоваровОбороты.КоличествоОборот > 0 ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.Пересчет) И ДвиженияСерийТоваровОбороты.КоличествоОборот > 0
) ТОГДА ДвиженияСерийТоваровОбороты.КоличествоОборот
ИНАЧЕ 0
КОНЕЦ КАК Приемка,
// Модифицированное поле Отгрузка: показываем оборот только если есть остаток по серии
ВЫБОР
КОГДА ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) > 0 И (
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаВРозницу) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаКлиенту) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаКомплектовДляРазборки) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаКомплектующихДляСборки) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаНаВнутренниеНужды) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаПоВозвратуПоставщику) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтгрузкаПоПеремещению) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеНедостач) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеПорчи) ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ВводОстатков) И ДвиженияСерийТоваровОбороты.КоличествоОборот < 0 ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.ОтражениеРезультатовПересчетов) И ДвиженияСерийТоваровОбороты.КоличествоОборот < 0 ИЛИ
ДвиженияСерийТоваровОбороты.СкладскаяОперация = ЗНАЧЕНИЕ(Перечисление.СкладскиеОперации.Пересчет) И ДвиженияСерийТоваровОбороты.КоличествоОборот < 0
) ТОГДА -ДвиженияСерийТоваровОбороты.КоличествоОборот // Отгрузка обычно отрицательна, поэтому берем с обратным знаком
ИНАЧЕ 0
КОНЕЦ КАК Отгрузка,
// Новое поле для общего количества оборота, показываем 0, если нет остатка по серии
ВЫБОР
КОГДА ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) = 0
ТОГДА 0
ИНАЧЕ ДвиженияСерийТоваровОбороты.КоличествоОборот
КОНЕЦ КАК КоличествоОборотКорректированное,
// Можем также показать СвободныйОстаток по серии, если РезервОстаток тоже нужен по серии
ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) - ЕСТЬNULL(ДвиженияСерийТоваровОстатки.РезервОстаток, 0) КАК СвободныйОстатокСерии
...
Важно:
ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0), чтобы в случае отсутствия остатка (из-за ЛЕВОГО СОЕДИНЕНИЯ) поле принимало значение 0, а не NULL.Приемка и Отгрузка мы добавили условие ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) > 0, чтобы показывать обороты только для тех серий, у которых есть остаток. Если вы хотите видеть все обороты, но с пометкой, что серия продана, это условие можно убрать, но тогда поле КоличествоОборотКорректированное будет более информативным.Отгрузка мы берем -ДвиженияСерийТоваровОбороты.КоличествоОборот, так как обороты расхода в регистрах часто записываются с отрицательным знаком.После добавления нового поля ТекущийОстатокСерии и его использования в вычисляемых полях, необходимо убедиться, что оно также включено в секцию СГРУППИРОВАТЬ ПО, если вы используете РАЗРЕШЕННЫЕ РАЗЛИЧНЫЕ или агрегатные функции, и это поле не является результатом агрегации.
Ваша секция СГРУППИРОВАТЬ ПО должна включать все поля, которые не являются агрегатными функциями. Убедитесь, что ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) и другие новые поля присутствуют в группировке.
СГРУППИРОВАТЬ ПО
СправочникНоменклатура.Ссылка,
...
ДвиженияСерийТоваровОбороты.Серия,
ДвиженияСерийТоваровОбороты.Характеристика,
ДвиженияСерийТоваровОбороты.Документ,
ДвиженияСерийТоваровОбороты.Магазин,
ДвиженияСерийТоваровОбороты.СкладскаяОперация,
ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0), // Добавляем в группировку!
ЕСТЬNULL(ДвиженияСерийТоваровОстатки.РезервОстаток, 0),
...
Если вы хотите агрегировать данные таким образом, чтобы каждая строка отчета соответствовала уникальной серии, а не каждому обороту, вам, возможно, потребуется более тщательно настроить группировки и агрегатные функции. Например, если вы хотите суммировать все обороты по приемке и отгрузке для конкретной серии, вам понадобится использовать СУММА() для полей Приемка и Отгрузка и группировать только по Номенклатура, Характеристика, Серия и ТекущийОстатокСерии.
После изменения запроса в СКД:
ТекущийОстатокСерии, КоличествоОборотКорректированное) стали доступны.Приемка, Отгрузка, КоличествоОборотКорректированное укажите тип агрегатной функции СУММА.Номенклатура, затем по Серия.ТекущийОстатокСерии = 0, покрасить строку серым цветом).Причина, по которой "старый срок годности с новым поступлением по количеству" может появляться, связана с тем, что идентификатор Серия в регистре может быть не уникальным для *каждого физического поступления*, а скорее для *вида серии* (например, просто срок годности). Если в систему поступает две разные партии одного и того же товара, но с одинаковым сроком годности, то обе эти партии будут иметь одинаковое значение в поле Серия.
Чтобы избежать такой путаницы, вы можете рассмотреть следующие подходы:
ДокументПрихода (если вы можете его получить из оборотов или связать с ним). Тогда в группировке СКД вы сможете сгруппировать сначала по Номенклатура, затем по Серия, а затем по ДокументПрихода. Это поможет различать, откуда пришла та или иная партия, даже если ее серия визуально совпадает с предыдущей.В контексте предоставленного запроса, если проблема с дублированием связана именно с тем, что одно и то же значение Серия используется для разных физических партий, то наше предложенное решение с ДвиженияСерийТоваровОстатки частично поможет, показывая 0, если остатка нет. Однако, если есть несколько партий с одинаковой "серией" и *разными* остатками, то объединение полей Номенклатура, Характеристика, Серия все равно может схлопнуть их в одну строку. В этом случае вам нужно будет добавить дополнительное уникальное измерение в регистр (например, "ИдентификаторПартии") или использовать более сложный анализ оборотов.
Давайте подытожим, что мы изменили в запросе:
ЛЕВОЕ СОЕДИНЕНИЕ с РегистрНакопления.ДвиженияСерийТоваров.Остатки КАК ДвиженияСерийТоваровОстатки по ключевым полям Номенклатура, Характеристика, Серия.ВЫБРАТЬ поле ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) КАК ТекущийОстатокСерии.Приемка и Отгрузка, чтобы они включали проверку ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) > 0, если вам нужно показывать обороты только для тех серий, у которых есть текущий остаток.ВЫБОР КОГДА ЕСТЬNULL(ДвиженияСерийТоваровОстатки.КоличествоОстаток, 0) = 0 ТОГДА 0 ИНАЧЕ ДвиженияСерийТоваровОбороты.КоличествоОборот КОНЕЦ КАК КоличествоОборотКорректированное, которое решает основную задачу — показывать нулевой оборот для проданных серий.СГРУППИРОВАТЬ ПО.Тщательная проверка результатов выполнения запроса в консоли запросов поможет вам убедиться в корректности внесенных изменений и отладить логику формирования отчета.