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