При разработке отчетов или обработок в 1С часто возникает задача пересчета сумм документов в управленческую или регламентированную валюту на момент совершения операции. Если мы используем виртуальную таблицу СрезПоследних с одним параметром даты, мы получаем курс только на один момент времени. Но что делать, если в выборке сотни документов с разными датами, и для каждого нужно получить свой актуальный курс?
В этой статье мы подробно разберем, как правильно реализовать получение курсов валют на даты документов (реализаций, поступлений) в одном пакетном запросе, избежав дублирования строк и ошибок производительности. Этот подход является альтернативой другим методам оптимизации, таким как пакетная выборка данных для множества разрозненных запросов.
Давайте проанализируем ситуацию, с которой столкнулся автор вопроса. У нас есть таблица продаж (обороты или документы), и мы хотим присоединить к ней регистр сведений КурсыВалют. Первая мысль, которая приходит в голову начинающему разработчику — соединить таблицы по условию, что дата курса меньше или равна дате документа.
Посмотрим на пример "наивного" запроса, который приводит к ошибкам:
ВЫБРАТЬ
Продажи.Ссылка,
Продажи.Дата,
Курсы.Курс
ИЗ
Документ.РеализацияТоваровУслуг КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК Курсы
ПО Курсы.Валюта = Продажи.Валюта
И Курсы.Период <= Продажи.Дата
В чем здесь ошибка?
Проблема заключается в условии Курсы.Период <= Продажи.Дата. Если курсы валют загружаются каждый день, а документ введен сегодня, то под это условие подпадут все записи курсов за всю историю ведения базы до текущего момента. В результате, одна строка документа "размножится" на количество дней, за которые были введены курсы. Вместо одной суммы вы получите колоссальные значения, а отчет будет формироваться вечно.
Второй неверный подход — использовать жесткое равенство Курсы.Период = Продажи.Дата (или начало дня даты документа). Это не сработает, потому что:
NULL.Самый надежный и производительный способ (особенно для больших выборок) — это эмуляция логики "Срез Последних" вручную через временные таблицы. Чтобы отлаживать подобные многоэтапные запросы, удобно использовать специализированные инструменты, например, функциональную консоль запросов, которая полностью поддерживает пакеты и временные таблицы — для этого подойдет функциональная консоль отладки запросов и СКД. Рассмотрим этот алгоритм по шагам.
Суть алгоритма сводится к трем этапам:
Сначала выберем все необходимые нам документы во временную таблицу. На этом этапе полезно привести дату документа к началу дня (если курсы в вашей системе хранятся с точностью до дня, как это обычно бывает в типовых конфигурациях).
ВЫБРАТЬ
Реализация.Ссылка КАК ДокументСсылка,
Реализация.ВалютаДокумента КАК Валюта,
НАЧАЛОПЕРИОДА(Реализация.Дата, ДЕНЬ) КАК ДатаКурса
ПОМЕСТИТЬ ВТ_Документы
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
;
Теперь самое интересное. Мы соединяем нашу таблицу документов с регистром курсов. Условие соединения: дата курса должна быть меньше или равна дате документа. Затем мы группируем результат и выбираем МАКСИМУМ(Период) из регистра курсов.
Таким образом, для каждого документа мы находим самую свежую дату установки курса, доступную на момент документа.
ВЫБРАТЬ
ВТ_Документы.ДокументСсылка,
ВТ_Документы.Валюта,
МАКСИМУМ(КурсыВалют.Период) КАК ПериодКурса
ПОМЕСТИТЬ ВТ_МаксимальныеДатыКурсов
ИЗ
ВТ_Документы КАК ВТ_Документы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
ПО ВТ_Документы.Валюта = КурсыВалют.Валюта
И КурсыВалют.Период <= ВТ_Документы.ДатаКурса
СГРУППИРОВАТЬ ПО
ВТ_Документы.ДокументСсылка,
ВТ_Документы.Валюта
;
Обратите внимание: здесь мы используем ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Если курса для валюты вообще нет в истории (что маловероятно, но возможно), документ отсеется. Если важно сохранить документы без курсов, используйте ЛЕВОЕ СОЕДИНЕНИЕ, но тогда функция МАКСИМУМ может вернуть NULL.
Теперь у нас есть точная дата курса для каждого документа. Осталось соединить таблицу ВТ_МаксимальныеДатыКурсов обратно с регистром КурсыВалют, но теперь уже по точному равенству периодов.
Важно: Никогда не забывайте про поле Кратность. Многие валюты (например, Вьетнамский донг или Японская иена) имеют курс не за 1 единицу, а за 10, 100 или 1000 единиц. Если вы упустите кратность, ваши суммы будут неверны на несколько порядков.
ВЫБРАТЬ
ВТ_МаксимальныеДатыКурсов.ДокументСсылка,
ЕСТЬNULL(КурсыВалют.Курс, 1) КАК Курс,
ЕСТЬNULL(КурсыВалют.Кратность, 1) КАК Кратность
ИЗ
ВТ_МаксимальныеДатыКурсов КАК ВТ_МаксимальныеДатыКурсов
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
ПО ВТ_МаксимальныеДатыКурсов.Валюта = КурсыВалют.Валюта
И ВТ_МаксимальныеДатыКурсов.ПериодКурса = КурсыВалют.Период
Теперь мы можем использовать полученные поля Курс и Кратность для расчетов в основном запросе. Формула пересчета валютной суммы в рубли обычно выглядит так:
СуммаВРублях = СуммаВВалюте * (Курс / Кратность)
Если выборка данных не очень велика, или вы хотите написать более компактный код без трех временных таблиц, можно воспользоваться возможностями языка запросов 1С, позволяющими делать вложенные запросы в списке выборки. Это выглядит элегантнее.
Давайте посмотрим, как это делается:
ВЫБРАТЬ
Реализация.Ссылка,
Реализация.СуммаДокумента,
ЕСТЬNULL(
(ВЫБРАТЬ ПЕРВЫЕ 1
Курсы.Курс
ИЗ
РегистрСведений.КурсыВалют КАК Курсы
ГДЕ
Курсы.Валюта = Реализация.ВалютаДокумента
И Курсы.Период <= Реализация.Дата
УПОРЯДОЧИТЬ ПО
Курсы.Период УБЫВ), 1) КАК АктуальныйКурс,
ЕСТЬNULL(
(ВЫБРАТЬ ПЕРВЫЕ 1
Курсы.Кратность
ИЗ
РегистрСведений.КурсыВалют КАК Курсы
ГДЕ
Курсы.Валюта = Реализация.ВалютаДокумента
И Курсы.Период <= Реализация.Дата
УПОРЯДОЧИТЬ ПО
Курсы.Период УБЫВ), 1) КАК АктуальнаяКратность
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
Преимущества: Код читается легко, все в одном запросе.
Недостатки: Может работать медленнее на больших объемах данных, так как подзапрос выполняется для каждой строки основной таблицы. Кроме того, приходится дублировать подзапрос для Курса и для Кратности.
Если ваша задача — создать отчет, то писать сложные запросы зачастую вообще не нужно. СКД умеет "соединять" наборы данных, автоматически применяя параметры.
Рассмотрим, как это настроить в конструкторе схемы компоновки данных:
ВЫБРАТЬ Курс, Кратность, Валюта, Период ИЗ РегистрСведений.КурсыВалют.СрезПоследних(&Период, )
&Период здесь обязателен.
Валюта = ВалютаПериод в значение поля источника Дата (или Период).При такой настройке СКД сама для каждой строки из набора "Документы" выполнит виртуальное получение среза последних на конкретную дату этой строки. Это очень мощный механизм, который избавляет от написания "многоэтажных" запросов. Для еще более сложных отчетов можно расширить стандартные возможности, используя, например, инструменты для пользовательской настройки СКД или применяя нестандартные приемы, такие как использование двух разных схем в одном отчете.
В завершение проанализируем несколько важных моментов, которые помогут избежать ошибок в будущем:
ВТ_Документы добавьте индекс по (Валюта, ДатаКурса), а в таблице ВТ_МаксимальныеДатыКурсов — по (Валюта, ПериодКурса). Это существенно ускорит выполнение запроса. Для более глубокого анализа узких мест на уровне СУБД можно применять обработки для анализа SQL сервера глазами 1С-ника.НАЧАЛОПЕРИОДА(Док.Дата, ДЕНЬ). Это правильно, если курсы в вашей базе загружаются один раз в день (обычно на 00:00:00). Если же у вас биржевая специфика и курсы меняются внутри дня, то использовать нужно точное время документа.NULL через функцию ЕСТЬNULL. Если курс не найден, логично подставить 1 (для курса) и 1 (для кратности), чтобы при делении и умножении не получить ошибок или пустых сумм, но это зависит от бизнес-логики вашей задачи.РаботаСКурсамиВалют с готовой функцией ПолучитьКурсВалюты(Валюта, Дата), которая уже учитывает все нюансы и кэширование.Используя эти подходы, вы сможете корректно получать курсы валют на любые даты, обеспечивая точность финансового и управленческого учета в ваших решениях на платформе 1С.