Получение остатков и оборотов по каждому товару на каждый день запросом

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

Вся соль задачи состоит в том, что обращения к виртуальной таблице остатков и оборотов в запросе не достаточно, так как эта виртуальная таблица содержит периоды, по которым были движения, если так можно выразиться (см. рисунок 1). Иными словами, если Товар №1 поступил в магазин в начале недели (в понедельник) в количестве 5ти штук и за неделю не было ни одной продажи, то виртуальная таблица по этому товару за эту неделю выдаст лишь одну запись за понедельник, которая будет отражать остатки на начало и конец дня, приход (+5), расход (0) и оборот (0).

Данную задачу я решил разделить на несколько подзадач, ведь слона легче есть кусками, чем сразу целиком. Получились такие подзадачи:

  1. Получить таблицу с датами на каждый день в пределах задаваемого периода
  2. Получить таблицу остатков и оборотов по каждому товару за период с периодичностью ДЕНЬ
  3. Как-то соединить полученные таблицы в первых двух подзадачах для получения желаемого результата

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

  
    ДатыНаКаждыйДень = Новый ТаблицаЗначений;
    ДатыНаКаждыйДень.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата", , , Новый КвалификаторыДаты(ЧастиДаты.Дата)));
  
    НачальнаяДата = Объект.НачалоПериода;
    Пока НачальнаяДата <= Объект.КонецПериода Цикл 
	
      НоваяСтрока = ДатыНаКаждыйДень.Добавить();
      НоваяСтрока.Дата = НачальнаяДата;
    
      НачальнаяДата = НачальнаяДата + 86400;
    
    КонецЦикла;
  

Получившуюся таблицу значений помещаем во временную таблицу запроса с последующим выводом содержимого временной таблицы через основной запрос.

  
    Запрос = Новый Запрос;
    Запрос.Текст = 
      "ВЫБРАТЬ
      |  ДатыНаКаждыйДень.Дата КАК Дата
      |ПОМЕСТИТЬ ВТ_ДатыНаКаждыйДень
      |ИЗ
      |  &ДатыНаКаждыйДень КАК ДатыНаКаждыйДень
      |;
      |
      |////////////////////////////////////////////////////////////////////////////////
      |ВЫБРАТЬ
      |  ВТ_ДатыНаКаждыйДень.Дата
      |ИЗ
      |  ВТ_ДатыНаКаждыйДень КАК ВТ_ДатыНаКаждыйДень";
  
    Запрос.УстановитьПараметр("ДатыНаКаждыйДень", ДатыНаКаждыйДень);
  

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

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

  
    Запрос = Новый Запрос;
    Запрос.Текст = 
      "ВЫБРАТЬ
      |  Товары.Ссылка КАК Товар,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.Период, &НачалоПериода) КАК Период,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоНачальныйОстаток, 0) КАК КоличествоНачальныйОстаток,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоКонечныйОстаток, 0) КАК КоличествоКонечныйОстаток,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоОборот, 0) КАК КоличествоОборот,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоПриход, 0) КАК КоличествоПриход,
      |  ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоРасход, 0) КАК КоличествоРасход
      |ИЗ
      |  Справочник.Номенклатура КАК Товары
      |    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, ДвиженияИГраницыПериода, ) КАК ОстаткиТоваровОстаткиИОбороты
      |      ПО Товары.Ссылка = ОстаткиТоваровОстаткиИОбороты.Номенклатура
      |ГДЕ
      |  НЕ Товары.ЭтоГруппа
      |
      |УПОРЯДОЧИТЬ ПО
      |  Товары.Наименование,
      |  Период";
  
    Запрос.УстановитьПараметр("НачалоПериода", Объект.НачалоПериода);
    Запрос.УстановитьПараметр("КонецПериода", КонецДня(Объект.КонецПериода));
  

Немножко пояснений. Допустим, пользователь указал период 01.01.2020 - 31.12.2020 в форме обработки, то есть осуществляется ввод дат без времени. В системе 1С такие даты будут храниться как 01.01.2020 00:00 и 31.12.2020 00:00, время присутствует и оно нулевое, соответственно остатки и обороты мы получим за период 01.01.2020 00:00 - 31.12.2020 00:00, что неправильно, т.к. данные за 31 декабря не попадут в результат запроса. Для этого к концу периода применяется функция КонецДня(), что бы получить остатки и обороты за период 01.01.2020 00:00 - 31.12.2020 23:59, или можно воспользоваться встроенной функцией КОНЕЦПЕРИОДА() языка запросов. Для параметра Периодичность виртуальной таблицы указана периодичность День, что и требуется в задаче, для параметра Метод дополнения периодов указано значение ДвиженияИГраницыПериода, что бы в результате запроса всегда присутствовали периоды на начало и конец заданного периода. Результат запроса представлен на рисунке 1.

Рисунок 1. Остатки и обороты по товару "Стол дуб." за 2015 год

А вот и самое интересное, решение третьей подзадачи. Соединяем таблицы первой и второй подзадачи таким образом, что бы в результате каждому дню соответствовал необходимый период остатков и оборотов, результат помещаем во временную таблицу (см. рисунок 2).

  
    |ВЫБРАТЬ
    |   ВТ_ДатыНаКаждыйДень.Дата КАК День,
    |   ВТ_ОстаткиИОбороты.Товар КАК Товар,
    |   МАКСИМУМ(ВТ_ОстаткиИОбороты.Период) КАК ПериодОстатковИОборотов
    |ПОМЕСТИТЬ ВТ_СоответствиеДатыПериоду
    |ИЗ
    |   ВТ_ДатыНаКаждыйДень КАК ВТ_ДатыНаКаждыйДень
    |     ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиИОбороты КАК ВТ_ОстаткиИОбороты
    |     ПО (ВТ_ОстаткиИОбороты.Период <= ВТ_ДатыНаКаждыйДень.Дата)
    |
    |СГРУППИРОВАТЬ ПО
    |   ВТ_ДатыНаКаждыйДень.Дата,
    |   ВТ_ОстаткиИОбороты.Товар
  
Рисунок 2. Промежуточная таблица с ключом соединения по товару и периоду

Полученную временную таблицу соединяем с таблицей второй подзадачи по составному ключу {Товар : ПериодОстатковИОборотов}, на поля выборки НачальныйОстаток, КонечныйОстаток, Приход, Расход и Оборот накладываем условие по периоду остатков и оборотов для правильного расчета значений (см. последний запрос в пакете запросов), в итоге получаем остатки и обороты на каждый день.

Конечный текст пакета запросов:

  
      Запрос = Новый Запрос;
      Запрос.Текст = 
        "ВЫБРАТЬ
        |   ДатыНаКаждыйДень.Дата КАК Дата
        |ПОМЕСТИТЬ ВТ_ДатыНаКаждыйДень
        |ИЗ
        |   &ДатыНаКаждыйДень КАК ДатыНаКаждыйДень
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |   Товары.Ссылка КАК Товар,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.Период, &НачалоПериода) КАК Период,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоНачальныйОстаток, 0) КАК КоличествоНачальныйОстаток,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоКонечныйОстаток, 0) КАК КоличествоКонечныйОстаток,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоОборот, 0) КАК КоличествоОборот,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоПриход, 0) КАК КоличествоПриход,
        |   ЕСТЬNULL(ОстаткиТоваровОстаткиИОбороты.КоличествоРасход, 0) КАК КоличествоРасход
        |ПОМЕСТИТЬ ВТ_ОстаткиИОбороты
        |ИЗ
        |   Справочник.Номенклатура КАК Товары
        |     ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, ДвиженияИГраницыПериода, ) КАК ОстаткиТоваровОстаткиИОбороты
        |     ПО Товары.Ссылка = ОстаткиТоваровОстаткиИОбороты.Номенклатура
        |ГДЕ
        |   НЕ Товары.ЭтоГруппа
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |   ВТ_ДатыНаКаждыйДень.Дата КАК День,
        |   ВТ_ОстаткиИОбороты.Товар КАК Товар,
        |   МАКСИМУМ(ВТ_ОстаткиИОбороты.Период) КАК ПериодОстатковИОборотов
        |ПОМЕСТИТЬ ВТ_СоответствиеДатыПериоду
        |ИЗ
        |   ВТ_ДатыНаКаждыйДень КАК ВТ_ДатыНаКаждыйДень
        |     ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиИОбороты КАК ВТ_ОстаткиИОбороты
        |     ПО (ВТ_ОстаткиИОбороты.Период <= ВТ_ДатыНаКаждыйДень.Дата)
        |
        |СГРУППИРОВАТЬ ПО
        |   ВТ_ДатыНаКаждыйДень.Дата,
        |   ВТ_ОстаткиИОбороты.Товар
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |   ВТ_СоответствиеДатыПериоду.День КАК День,
        |   ВТ_ОстаткиИОбороты.Период КАК Период,
        |   ВТ_ОстаткиИОбороты.Товар КАК Товар,
        |   ВЫБОР
        |     КОГДА ВТ_СоответствиеДатыПериоду.День = ВТ_ОстаткиИОбороты.Период
        |       ТОГДА ВТ_ОстаткиИОбороты.КоличествоНачальныйОстаток
        |     ИНАЧЕ ВТ_ОстаткиИОбороты.КоличествоКонечныйОстаток
        |   КОНЕЦ КАК НачальныйОстаток,
        |   ВТ_ОстаткиИОбороты.КоличествоКонечныйОстаток КАК КонечныйОстаток,
        |   ВЫБОР
        |     КОГДА ВТ_СоответствиеДатыПериоду.День = ВТ_ОстаткиИОбороты.Период
        |       ТОГДА ВТ_ОстаткиИОбороты.КоличествоОборот
        |     ИНАЧЕ 0
        |   КОНЕЦ КАК Оборот,
        |   ВЫБОР
        |     КОГДА ВТ_СоответствиеДатыПериоду.День = ВТ_ОстаткиИОбороты.Период
        |       ТОГДА ВТ_ОстаткиИОбороты.КоличествоПриход
        |     ИНАЧЕ 0
        |   КОНЕЦ КАК Приход,
        |   ВЫБОР
        |     КОГДА ВТ_СоответствиеДатыПериоду.День = ВТ_ОстаткиИОбороты.Период
        |       ТОГДА ВТ_ОстаткиИОбороты.КоличествоРасход
        |     ИНАЧЕ 0
        |   КОНЕЦ КАК Расход
        |ИЗ
        |   ВТ_СоответствиеДатыПериоду КАК ВТ_СоответствиеДатыПериоду
        |     ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиИОбороты КАК ВТ_ОстаткиИОбороты
        |     ПО ВТ_СоответствиеДатыПериоду.ПериодОстатковИОборотов = ВТ_ОстаткиИОбороты.Период
        |       И ВТ_СоответствиеДатыПериоду.Товар = ВТ_ОстаткиИОбороты.Товар
        |
        |УПОРЯДОЧИТЬ ПО
        |   ВТ_СоответствиеДатыПериоду.Товар.Наименование,
        |   День";

      Запрос.УстановитьПараметр("ДатыНаКаждыйДень",   ДатыНаКаждыйДень);
      Запрос.УстановитьПараметр("НачалоПериода",    Объект.НачалоПериода);
      Запрос.УстановитьПараметр("КонецПериода",     КонецДня(Объект.КонецПериода));

      РезультатЗапроса = Запрос.Выполнить();
  

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

Комментарии