gifts2017

Улучшение регистра курсов валют в v8

Опубликовал Сергей Барышников (clappa) в раздел Программирование - Практика программирования

Небольшая доработка регистра сведений "Курсы валют" для более удобного использования его в запросах.
Регистр курсов валют в стандартных конфигурациях 1С прост и понятен любому программисту. Но он имеет одно существенное неудобство - его сложно использовать в запросах в соединении с другими таблицами из-за того, что в общем случае курс валюты устанавливается не на каждый календарный день. Если на какую-то дату курс не установлен, должен браться курс на предыдущую дату, что на языке запросов громоздко выглядит и долго исполняется.

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

У проблемы есть четыре решения:

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)
КонецФункции

// Функция выполняет произвольный запрос и выгружает первую колонку его результата
// в список значений
//
// Параметры
//  ТекстЗапроса – Строка
//  Параметры    – Структура – параметры запроса
//
// Возвращаемое значение:
//  Список значений
//
Функция оВыполнитьЗапросВСписок(ТекстЗапроса, Параметры = Неопределено) Экспорт
  Перем Рез;
  Рез = Новый СписокЗначений;
  Рез.ЗагрузитьЗначения(оВыполнитьЗапросВМассив(ТекстЗапроса, Параметры));
  Возврат Рез
КонецФункции




См. также

Подписаться Добавить вознаграждение

Комментарии

1. Сергей Старых (tormozit) 18.11.07 21:43
2. Gorky (Gorky) 19.11.07 07:29
Да. Мне тоже такой метод посоветовал приятель, который сейчас работает в Канаде. Общий смысл такой, что надо кроме ДатаНачала в регистре сведений ставить ДатаКонца. Если бы это было реализовано на уровне платформы, то вообще проблем бы с запросами не было. Тем более, что делается это элементарно.
3. Алексей (a.v.petuhov) 19.11.07 11:10
Но он имеет одно существенное неудобство - его сложно использовать в запросах в соединении с другими таблицами из-за того, что в общем случае курс валюты устанавливается не на каждый календарный день. Если на какую-то дату курс не установлен, должен браться курс на предыдущую дату, что на языке запросов громоздко выглядит и долго исполняется.

Может я совсем ничего не понял, но есть ведь виртуальная таблица "СрезПоследних". Она и возвращеает ближайший курс на указанную дату. Зачем тогда все ЭТО?
4. Владимир (Skylark) 19.11.07 15:55
(3) +1
Я тоже впомнил про "срез последних". Или мы чего-то не догоняем - или автор осрамился.
5. _Reset (_Reset) 19.11.07 18:00
пример просто приведен не совсем "наглядный".
проблемы обычно возникают когда надо вывести список заявок за разные даты и курсы валют.
тогда срезпоследних не покатит
6. Сергей Барышников (clappa) 20.11.07 00:56
(3,4) Не, ребята, вы не поняли. СрезПоследних возвращает курс на одну конкретную дату, переданную в качестве параметра. А если нужно получить курсы на каждую дату в запросе, тогда он вам не поможет.
7. Buran_ (Yasen) 03.01.08 20:36
Решение интересное, спасибо
8. Роман (PRoman) 07.09.10 09:24
Удивительное рядом. Спасибо! :)
9. Il Il (Il) 03.05.12 07:07
Хорошее решение! Пока база небольшая не заметно было. Спасибо за идею
10. Алекс zhu4 (Arxxximed) 04.08.16 12:27
Интересно, а нафига тогда после этой статьи, все равно слишком популярны статьи типа срез последних на каждую дату? И про пункт 3.Хитрый запрос: я насколько понял при срезе последних, как раз платформа доработатывает запрос к базе вот таким хитрым запросом, или что то типо того
Для написания сообщения необходимо авторизоваться
Прикрепить файл
Дополнительные параметры ответа