Для примера возьмем таблицу документов "Заявка" с реквизитами Дата, Сумма, Валюта, и представим, что нам нужно вывести список заявок с суммами, пересчитанными в рубли по курсу на дату каждой заявки.
У проблемы есть четыре решения:
1. Предварительный расчет
---------------------------------
В документ "Заявка" добавляется реквизит "СуммаРуб", которая расчитывается и заполняется при записи заявки.
Такой вариант вполне применим и даже предпочтителен для ряда частных случаев, но универсальным не является. Кроме того, если курс по каким-либо причинам менялся после записи документа (например, документ имеет плановый характер и введен будущей датой), необходимо будет перезаписать документ для обновления рублевой суммы.
2. Административные методы
---------------------------------
Необходимо обеспечить, чтобы в регистр курсов курсы заносились на каждый календарный день. То есть курс вводится в регистр не только на дату, когда он установлен, но и на все последующие даты, для которых он действует.
Тогда решение задачи будет очень простым:
ВЫБРАТЬ Дата, Сумма, Заявки.Валюта, Сумма*Курс КАК СуммаРуб ИЗ Документ.Заявка КАК Заявки ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК Курсы ПО (Заявки.Валюта = Курсы.Валюта) И (Заявки.Дата = Курсы.Период)
Однако, как и все административные методы, этот чувствителен к человеческому фактору и соблюдению регламента. Также, он не решает вопрос, если сушествуют документы, введенные будущими датами (например, плановые заявки).
3. Хитрый запрос
---------------------------------
Используем двойное соединение с регистром курсов для получения действующего курса на каждую дату:
ВЫБРАТЬ Дата, Сумма, ЗаявкиСДатамиКурсов.Валюта, Сумма*Курс КАК СуммаРуб ИЗ (ВЫБРАТЬ Ссылка, Дата, Сумма, Заявки.Валюта, МАКСИМУМ(Курсы1.Период) КАК ДатаКурса ИЗ Документ.Заявка КАК Заявки ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК Курсы1 ПО (Заявки.Валюта = Курсы1.Валюта) И (Заявки.Дата >= Курсы1.Период) СГРУППИРОВАТЬ ПО Ссылка, Дата, Сумма, Заявки.Валюта) КАК ЗаявкиСДатамиКурсов ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК Курсы2 ПО (ЗаявкиСДатамиКурсов.Валюта = Курсы2.Валюта) И (ЗаявкиСДатамиКурсов.ДатаКурса = Курсы2.Период)
Этот теоретически работающий запрос может выполняться очень долго.
4. Доработка регистра курсов
---------------------------------
Именно этому варианту посвящена данная статья. Доработка регистра заключается в добавлении индексированного реквизита "СледующийПериод", в котором для каждой записи будет храниться дата следующего курса (то есть дата, начиная с которой текущая запись перестает действовать), а также добавление кода в модуль регистра курсов валют для автоматического заполнения нового реквизита.
Решение задачи получается почти таким же простым и быстрым, как и в варианте 2:
ВЫБРАТЬ Дата, Сумма, Заявки.Валюта, Сумма*Курс КАК СуммаРуб ИЗ Документ.Заявка КАК Заявки ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК Курсы ПО (Заявки.Валюта = Курсы.Валюта) И (Заявки.Дата >= Курсы.Период) И (Заявки.Дата < Курсы.СледующийПериод)
Наибольшие сложности этого метода связаны с доработкой модуля регистра курсов валют для автоматической поддержки актуальности служебного реквизита "СледующийПериод". Когда в регистр добавляется новая запись следует:
1. Определить СледующийПериод для добавляемой записи.
2. Скорректировать СледующийПериод для уже существующей записи, стоящей по хронологии перед добавляемой записью.
Cледующий код решает эту задачу (его необходимо добавить в процедуру "ПриЗаписи" модуля регистра курсов):
// Подготовимся к заполнению поля "Следующий период": определим область пространства измерений, //которые подвергаются изменениям ОтборДатаМин = Неопределено; ОтборДатаМакс = Неопределено; ОтборВалюта = Неопределено; Если не ОбменДанными.Загрузка Тогда Если Замещение Тогда ОтборДатаМин = ?(Отбор.Период.Использование, Отбор.Период.Значение, '00010101'); ОтборДатаМакс = ?(Отбор.Период.Использование, Отбор.Период.Значение, '39991231'); ОтборВалюта = ?(Отбор.Валюта.Использование, Отбор.Валюта.Значение, Неопределено); ИначеЕсли Количество() > 0 Тогда ТЗ = Выгрузить(); ТЗ.Сортировать("Период"); ОтборДатаМин = ТЗ[0].Период; ОтборДатаМакс = ТЗ[ТЗ.Количество()-1].Период; ТЗ.Свернуть("Валюта"); ОтборВалюта = ТЗ.ВыгрузитьКолонку("Валюта"); КонецЕсли; КонецЕсли; // Заполнение поля "Следующий период": находим все записи, попавшие в изменяемый период, // и для каждой определяем дату следующего курса Если ОтборДатаМин <> Неопределено Тогда Записи = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей(); Записи.ОбменДанными.Загрузка = Истина; РЗ = оВыполнитьЗапрос("ВЫБРАТЬ | Период, Валюта, Курс, Кратность, СледующийПериод |ИЗ | РегистрСведений.КурсыВалют КАК КурсыВалют |ГДЕ | СледующийПериод = &ПустаяДата | ИЛИ (Период <= &ДатаМакс И (Период >= &ДатаМин ИЛИ СледующийПериод >= &ДатаМин) | " + ?(ОтборВалюта = Неопределено, "", ?(ТипЗнч(ОтборВалюта) = Тип("Массив"), "И Валюта В (&Валюта)", "И Валюта = &Валюта")) + ")", Новый Структура("ДатаМин, ДатаМакс, Валюта, ПустаяДата", ОтборДатаМин, ОтборДатаМакс, ОтборВалюта, '00010101')); Выборка = РЗ.Выбрать(); Пока Выборка.Следующий() Цикл Зн = оВыполнитьЗапросВСкаляр("ВЫБРАТЬ ПЕРВЫЕ 1 | Период КАК Период |ИЗ | РегистрСведений.КурсыВалют КАК КурсыВалют |ГДЕ | Валюта = &Валюта И Период > &Период |УПОРЯДОЧИТЬ ПО | Период", Новый Структура("Валюта, Период", Выборка.Валюта, Выборка.Период)); Зн = ?(Зн = NULL, '39991231', Зн); Если Зн <> Выборка.СледующийПериод Тогда Записи.Очистить(); Записи.Отбор.Период.Установить(Выборка.Период); Записи.Отбор.Валюта.Установить(Выборка.Валюта); Запись = Записи.Добавить(); Запись.Период = Выборка.Период; Запись.Валюта = Выборка.Валюта; Запись.Курс = Выборка.Курс; Запись.Кратность = Выборка.Кратность; Запись.СледующийПериод = Зн; Записи.Записать(Истина); КонецЕсли; КонецЦикла; КонецЕсли;
P.S. В коде используются две вспомогательные функции (оВыполнитьЗапрос и оВыполнитьЗапросВСкаляр):
// Функция выполняет произвольный запрос // // Параметры // ТекстЗапроса – Строка // Параметры – Структура – параметры запроса // // Возвращаемое значение: // РезультатЗапроса // Функция оВыполнитьЗапрос(ТекстЗапроса, Параметры = Неопределено) Экспорт Перем Запрос, Зн; Запрос = Новый Запрос(ТекстЗапроса); Если Параметры <> Неопределено Тогда Для каждого Зн из Параметры Цикл Запрос.УстановитьПараметр(Зн.Ключ, Зн.Значение) КонецЦикла; КонецЕсли; Возврат Запрос.Выполнить() КонецФункции // Функция выполняет произвольный запрос и возвращает значение из // первой строки и первой колонки результата. // Имеет смысл, применяя эту функцию, использовать в тексте запроса конструкцию // "ВЫБРАТЬ ПЕРВЫЕ 1" // // Параметры // ТекстЗапроса – Строка // Параметры – Структура – параметры запроса // // Возвращаемое значение: // Значение из первой строки и первой колонки результата запроса. // Если результат запроса пустой, возвращается NULL // Функция оВыполнитьЗапросВСкаляр(ТекстЗапроса, Параметры = Неопределено) Экспорт Перем Выборка; Выборка = оВыполнитьЗапрос(ТекстЗапроса, Параметры).Выбрать(ОбходРезультатаЗапроса.Прямой); Возврат ?(Выборка.Следующий(), Выборка[0], NULL); КонецФункции // Функция выполняет произвольный запрос и выгружает его результат в таблицу значений // // Параметры // ТекстЗапроса – Строка // Параметры – Структура – параметры запроса // // Возвращаемое значение: // ТаблицаЗначений // Функция оВыполнитьЗапросВТаблицу(ТекстЗапроса, Параметры = Неопределено) Экспорт Возврат оВыполнитьЗапрос(ТекстЗапроса, Параметры).Выгрузить(ОбходРезультатаЗапроса.Прямой) КонецФункции // Функция выполняет произвольный запрос и выгружает первую колонку его результата // в массив // // Параметры // ТекстЗапроса – Строка // Параметры – Структура – параметры запроса // // Возвращаемое значение: // Массив // Функция оВыполнитьЗапросВМассив(ТекстЗапроса, Параметры = Неопределено) Экспорт Возврат оВыполнитьЗапросВТаблицу(ТекстЗапроса, Параметры).ВыгрузитьКолонку(0) КонецФункции // Функция выполняет произвольный запрос и выгружает первую колонку его результата // в список значений // // Параметры // ТекстЗапроса – Строка // Параметры – Структура – параметры запроса // // Возвращаемое значение: // Список значений // Функция оВыполнитьЗапросВСписок(ТекстЗапроса, Параметры = Неопределено) Экспорт Перем Рез; Рез = Новый СписокЗначений; Рез.ЗагрузитьЗначения(оВыполнитьЗапросВМассив(ТекстЗапроса, Параметры)); Возврат Рез КонецФункции