Как правильно пересчитать сумму документа по курсу в запросе СКД для 1С:УНФ

Программист 1С v8.3 (Управляемые формы) 1С:Управление нашей фирмой Управленческий учет Торговля и дистрибуция
← На главную

Приветствуем вас, коллеги! Сегодня мы разберем распространенную проблему, с которой часто сталкиваются начинающие разработчики при создании внешних отчетов в системе 1С:Предприятие (в частности, для конфигурации 1С:Управление нашей фирмой). Ситуация классическая: в унаследованном коде обнаруживается «костыль», когда сумма документа в иностранной валюте пересчитывается в валюту учета путем простого умножения на жестко заданное число (например, 1.21). Разумеется, это в корне неверно, ведь курс валюты постоянно меняется.

Наша задача — разобраться, как правильно, динамически и с соблюдением всех стандартов разработки 1С пересчитать сумму документа по курсу конверсии на дату этого самого документа. Мы рассмотрим четыре практических метода реализации этой задачи: от простого исправления текущего запроса до профессиональной перестройки архитектуры отчета в Системе компоновки данных (СКД).

---

Метод №1. Использование реквизитов шапки документа (Самый быстрый способ)

Проанализируем структуру мультивалютных документов в 1С:УНФ (например, таких как РасходнаяНакладная). Любой торговый документ, поддерживающий мультивалютный учет, хранит в своей шапке (в реквизитах ссылки) актуальные на момент проведения документа значения курса и кратности. Это означает, что нам нет необходимости делать сложные запросы к регистру сведений КурсыВалют, если нас устраивает курс, зафиксированный пользователем непосредственно в документе.

Формула пересчета предельно проста: СуммаВВалютеУчета = СуммаДокумента * Курс / Кратность. Однако при написании запроса критически важно предусмотреть защиту от деления на ноль, если реквизит Кратность по какой-то причине окажется незаполненным или равным нулю.

Давайте посмотрим, как будет выглядеть исправленный фрагмент вашего запроса с использованием безопасного выбора:


ВЫБРАТЬ РАЗРЕШЕННЫЕ
    Продажи.СуммаДокумента КАК Выручка,
    Продажи.Регистратор,
    Продажи.Номенклатура,
    Продажи.Контрагент,
    Продажи.День,
    Продажи.Месяц КАК Месяц,
    Продажи.СуммаДокументаВВалюте,
    Продажи.ВалютаДокумента
ИЗ
    (ВЫБРАТЬ
        РасходнаяНакладнаяЗапасы.Ссылка КАК Регистратор,
        РасходнаяНакладнаяЗапасы.Номенклатура КАК Номенклатура,
        РасходнаяНакладнаяЗапасы.Ссылка.Контрагент КАК Контрагент,
        ВЫБОР
            // Если валюта документа совпадает с валютой учета, берем исходную сумму
            КОГДА РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента В
                    (ВЫБРАТЬ Константы.ВалютаУчета ИЗ Константы КАК Константы)
                ТОГДА РасходнаяНакладнаяЗапасы.Сумма
            
            // Иначе динамически рассчитываем сумму по курсу документа с защитой от деления на 0
            ИНАЧЕ ВЫБОР
                КОГДА ЕСТЬNULL(РасходнаяНакладнаяЗапасы.Ссылка.Кратность, 0) > 0
                    ТОГДА (РасходнаяНакладнаяЗапасы.Сумма * РасходнаяНакладнаяЗапасы.Ссылка.Курс) / РасходнаяНакладнаяЗапасы.Ссылка.Кратность
                ИНАЧЕ РасходнаяНакладнаяЗапасы.Сумма
            КОНЕЦ
        КОНЕЦ КАК СуммаДокумента,
        РасходнаяНакладнаяЗапасы.Сумма КАК СуммаДокументаВВалюте,
        РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК ВалютаДокумента,
        НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) КАК День,
        НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, МЕСЯЦ) КАК Месяц
    ИЗ
        Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы) КАК Продажи

Обратите внимание, что мы также оптимизировали получение контрагента. Вместо обращения к отдельной таблице Контрагенты мы напрямую пишем РасходнаяНакладнаяЗапасы.Ссылка.Контрагент, так как этот реквизит присутствует в шапке документа.

---

Метод №2. Использование возможностей СКД: Связи наборов данных

Что делать, если курс в самом документе может быть не заполнен, изменен вручную некорректно или нам принципиально нужно получать официальный курс из регистра сведений КурсыВалют на дату каждого конкретного документа? Соединять таблицы в запросе по условию «ближайшей даты» (срез последних на каждую дату) — задача трудоемкая и непроизводительная.

Здесь нам на помощь приходит мощный встроенный механизм СКД — Связи наборов данных. Рассмотрим по шагам, как настроить такую схему:

  1. Создаем НаборДанных1 (запрос). В нем мы выбираем основные данные по документам продажи. Нам обязательно понадобятся поля Ссылка.Дата (или созданное поле ДатаДокумента) и ВалютаДокумента.
  2. Создаем НаборДанных2 (запрос) для получения курсов валют. Его текст будет выглядеть следующим образом:

ВЫБРАТЬ
    КурсыВалютСрезПоследних.Валюта КАК Валюта,
    КурсыВалютСрезПоследних.Курс КАК Курс,
    КурсыВалютСрезПоследних.Кратность КАК Кратность
ИЗ
    РегистрСведений.КурсыВалют.СрезПоследних(&ПериодКурса, Валюта = &ВалютаКурса) КАК КурсыВалютСрезПоследних
  1. Переходим на вкладку Связи наборов данных в конструкторе схемы компоновки данных и добавляем новую строку связи со следующими параметрами:
    • Источник связи: НаборДанных1
    • Приемник связи: НаборДанных2
    • Условие связи: ВалютаДокумента = Валюта
    • Параметры:
      • Имя параметра: ПериодКурса -> Выражение источника: ДатаДокумента
      • Имя параметра: ВалютаКурса -> Выражение источника: ВалютаДокумента
  2. Теперь переходим на вкладку Вычисляемые поля и добавляем новое вычисляемое поле, например, ВыручкаВВалютеУчета. В поле «Выражение» пишем формулу:

Выбор Когда ВалютаДокумента = &ВалютаУчета Тогда СуммаВВалюте Иначе Выбор Когда ЕстьNull(Кратность, 0) > 0 Тогда (СуммаВВалюте * Курс) / Кратность Иначе СуммаВВалюте Конец Конец

Этот способ является наиболее производительным и «чистым» с точки зрения методологии разработки на платформе 1С, поскольку СКД сама оптимизирует запросы к Срезу Последних для каждой строки документов.

---

Метод №3. Классический пакетный запрос для среза последних на каждую дату

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

Проанализируем этот алгоритм шаг за шагом:


// ШАГ 1: Собираем уникальные комбинации Валют и Дат документов во временную таблицу
ВЫБРАТЬ РАЗЛИЧНЫЕ
    РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК Валюта,
    НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) КАК ПериодДокумента
ПОМЕСТИТЬ ВТ_ВалютыДаты
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы
;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 2: Для каждой даты документа находим реальную дату курса (максимальную дату курса, которая меньше или равна дате документа)
ВЫБРАТЬ
    ВТ_ВалютыДаты.Валюта КАК Валюта,
    ВТ_ВалютыДаты.ПериодДокумента КАК ПериодДокумента,
    МАКСИМУМ(КурсыВалют.Период) КАК ПериодКурса
ПОМЕСТИТЬ ВТ_МаксимальныеДатыКурсов
ИЗ
    ВТ_ВалютыДаты КАК ВТ_ВалютыДаты
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
        ПО ВТ_ВалютыДаты.Валюта = КурсыВалют.Валюта
            И ВТ_ВалютыДаты.ПериодДокумента >= КурсыВалют.Период
ГРУППИРОВАТЬ ПО
    ВТ_ВалютыДаты.Валюта,
    ВТ_ВалютыДаты.ПериодДокумента
;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 3: Получаем значения курса и кратности на найденные даты курса
ВЫБРАТЬ
    ВТ_МаксимальныеДатыКурсов.Валюта КАК Валюта,
    ВТ_МаксимальныеДатыКурсов.ПериодДокумента КАК ПериодДокумента,
    КурсыВалют.Курс КАК Курс,
    КурсыВалют.Кратность КАК Кратность
ПОМЕСТИТЬ ВТ_КурсыНаДаты
ИЗ
    ВТ_МаксимальныеДатыКурсов КАК ВТ_МаксимальныеДатыКурсов
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
        ПО ВТ_МаксимальныеДатыКурсов.Валюта = КурсыВалют.Валюта
            И ВТ_МаксимальныеДатыКурсов.ПериодКурса = КурсыВалют.Период
;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 4: Соединяем исходные документы с полученной таблицей курсов и рассчитываем сумму
ВЫБРАТЬ
    РасходнаяНакладнаяЗапасы.Ссылка КАК Регистратор,
    РасходнаяНакладнаяЗапасы.Номенклатура КАК Номенклатура,
    РасходнаяНакладнаяЗапасы.Сумма КАК СуммаВВалюте,
    РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК ВалютаДокумента,
    ЕСТЬNULL(ВТ_КурсыНаДаты.Курс, 1) КАК Курс,
    ЕСТЬNULL(ВТ_КурсыНаДаты.Кратность, 1) КАК Кратность,
    ВЫБОР
        КОГДА РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента В (ВЫБРАТЬ Константы.ВалютаУчета ИЗ Константы)
            ТОГДА РасходнаяНакладнаяЗапасы.Сумма
        ИНАЧЕ ВЫБОР
            КОГДА ЕСТЬNULL(ВТ_КурсыНаДаты.Кратность, 0) > 0
                ТОГДА (РасходнаяНакладнаяЗапасы.Сумма * ЕСТЬNULL(ВТ_КурсыНаДаты.Курс, 1)) / ВТ_КурсыНаДаты.Кратность
            ИНАЧЕ РасходнаяНакладнаяЗапасы.Сумма
        КОНЕЦ
    КОНЕЦ КАК СуммаДокумента
ИЗ
    Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_КурсыНаДаты КАК ВТ_КурсыНаДаты
        ПО РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента = ВТ_КурсыНаДаты.Валюта
            И (НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) = ВТ_КурсыНаДаты.ПериодДокумента)

---

Метод №4. Переход на учетные регистры (Рекомендуемый и самый правильный)

Давайте честно признаемся: обращение напрямую к табличным частям документов (таким как Документ.РасходнаяНакладная.Запасы) для построения отчетов по продажам считается плохой практикой проектирования в 1С. Почему?

В конфигурации 1С:УНФ для накопления информации о продажах разработан специализированный регистр накопления — Продажи. При проведении документов система автоматически рассчитывает все курсы и записывает в этот регистр готовые данные:

Посмотрим, как изящно и просто решается ваша задача, если мы обратимся к виртуальной таблице оборотов этого регистра:


ВЫБРАТЬ РАЗРЕШЕННЫЕ
    ПродажиОбороты.СуммаОборот КАК Выручка,              // Это готовая сумма в валюте учета!
    ПродажиОбороты.СуммаВВалютеОборот КАК ВыручкаВВалюте, // Сумма в валюте документа
    ПродажиОбороты.Регистратор КАК Регистратор,
    ПродажиОбороты.Номенклатура КАК Номенклатура,
    ПродажиОбороты.Контрагент КАК Контрагент,
    ПродажиОбороты.Валюта КАК ВалютаДокумента,
    НАЧАЛОПЕРИОДА(ПродажиОбороты.Период, ДЕНЬ) КАК День,
    НАЧАЛОПЕРИОДА(ПродажиОбороты.Период, МЕСЯЦ) КАК Месяц
ИЗ
    РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, Авто, ) КАК ПродажиОбороты

Использование регистра оборотов Продажи.Обороты полностью избавляет вас от необходимости рассчитывать курсы валют в запросе вручную. Платформа делает это автоматически в момент проведения накладной. Более того, ваш отчет станет работать в разы быстрее, код запроса сократится в несколько раз, а риск допустить ошибку при делении на нулевую кратность сведется к нулю.

Выбирайте наиболее подходящий для вашей конфигурации и задачи метод и делайте ваши отчеты быстрыми и надежными! Удачи в разработке!

← На главную