Приветствуем вас, коллеги! Сегодня мы разберем распространенную проблему, с которой часто сталкиваются начинающие разработчики при создании внешних отчетов в системе 1С:Предприятие (в частности, для конфигурации 1С:Управление нашей фирмой). Ситуация классическая: в унаследованном коде обнаруживается «костыль», когда сумма документа в иностранной валюте пересчитывается в валюту учета путем простого умножения на жестко заданное число (например, 1.21). Разумеется, это в корне неверно, ведь курс валюты постоянно меняется.
Наша задача — разобраться, как правильно, динамически и с соблюдением всех стандартов разработки 1С пересчитать сумму документа по курсу конверсии на дату этого самого документа. Мы рассмотрим четыре практических метода реализации этой задачи: от простого исправления текущего запроса до профессиональной перестройки архитектуры отчета в Системе компоновки данных (СКД).
---
Проанализируем структуру мультивалютных документов в 1С:УНФ (например, таких как РасходнаяНакладная). Любой торговый документ, поддерживающий мультивалютный учет, хранит в своей шапке (в реквизитах ссылки) актуальные на момент проведения документа значения курса и кратности. Это означает, что нам нет необходимости делать сложные запросы к регистру сведений КурсыВалют, если нас устраивает курс, зафиксированный пользователем непосредственно в документе.
Формула пересчета предельно проста: СуммаВВалютеУчета = СуммаДокумента * Курс / Кратность. Однако при написании запроса критически важно предусмотреть защиту от деления на ноль, если реквизит Кратность по какой-то причине окажется незаполненным или равным нулю.
Давайте посмотрим, как будет выглядеть исправленный фрагмент вашего запроса с использованием безопасного выбора:
ВЫБРАТЬ РАЗРЕШЕННЫЕ
Продажи.СуммаДокумента КАК Выручка,
Продажи.Регистратор,
Продажи.Номенклатура,
Продажи.Контрагент,
Продажи.День,
Продажи.Месяц КАК Месяц,
Продажи.СуммаДокументаВВалюте,
Продажи.ВалютаДокумента
ИЗ
(ВЫБРАТЬ
РасходнаяНакладнаяЗапасы.Ссылка КАК Регистратор,
РасходнаяНакладнаяЗапасы.Номенклатура КАК Номенклатура,
РасходнаяНакладнаяЗапасы.Ссылка.Контрагент КАК Контрагент,
ВЫБОР
// Если валюта документа совпадает с валютой учета, берем исходную сумму
КОГДА РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента В
(ВЫБРАТЬ Константы.ВалютаУчета ИЗ Константы КАК Константы)
ТОГДА РасходнаяНакладнаяЗапасы.Сумма
// Иначе динамически рассчитываем сумму по курсу документа с защитой от деления на 0
ИНАЧЕ ВЫБОР
КОГДА ЕСТЬNULL(РасходнаяНакладнаяЗапасы.Ссылка.Кратность, 0) > 0
ТОГДА (РасходнаяНакладнаяЗапасы.Сумма * РасходнаяНакладнаяЗапасы.Ссылка.Курс) / РасходнаяНакладнаяЗапасы.Ссылка.Кратность
ИНАЧЕ РасходнаяНакладнаяЗапасы.Сумма
КОНЕЦ
КОНЕЦ КАК СуммаДокумента,
РасходнаяНакладнаяЗапасы.Сумма КАК СуммаДокументаВВалюте,
РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК ВалютаДокумента,
НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) КАК День,
НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, МЕСЯЦ) КАК Месяц
ИЗ
Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы) КАК Продажи
Обратите внимание, что мы также оптимизировали получение контрагента. Вместо обращения к отдельной таблице Контрагенты мы напрямую пишем РасходнаяНакладнаяЗапасы.Ссылка.Контрагент, так как этот реквизит присутствует в шапке документа.
---
Что делать, если курс в самом документе может быть не заполнен, изменен вручную некорректно или нам принципиально нужно получать официальный курс из регистра сведений КурсыВалют на дату каждого конкретного документа? Соединять таблицы в запросе по условию «ближайшей даты» (срез последних на каждую дату) — задача трудоемкая и непроизводительная.
Здесь нам на помощь приходит мощный встроенный механизм СКД — Связи наборов данных. Рассмотрим по шагам, как настроить такую схему:
Ссылка.Дата (или созданное поле ДатаДокумента) и ВалютаДокумента.
ВЫБРАТЬ
КурсыВалютСрезПоследних.Валюта КАК Валюта,
КурсыВалютСрезПоследних.Курс КАК Курс,
КурсыВалютСрезПоследних.Кратность КАК Кратность
ИЗ
РегистрСведений.КурсыВалют.СрезПоследних(&ПериодКурса, Валюта = &ВалютаКурса) КАК КурсыВалютСрезПоследних
НаборДанных1НаборДанных2ВалютаДокумента = ВалютаПериодКурса -> Выражение источника: ДатаДокументаВалютаКурса -> Выражение источника: ВалютаДокументаВыручкаВВалютеУчета. В поле «Выражение» пишем формулу:Выбор Когда ВалютаДокумента = &ВалютаУчета Тогда СуммаВВалюте Иначе Выбор Когда ЕстьNull(Кратность, 0) > 0 Тогда (СуммаВВалюте * Курс) / Кратность Иначе СуммаВВалюте Конец Конец
Этот способ является наиболее производительным и «чистым» с точки зрения методологии разработки на платформе 1С, поскольку СКД сама оптимизирует запросы к Срезу Последних для каждой строки документов.
---
Если по какой-то причине вам необходимо получить все данные строго в рамках одного классического SQL-запроса (например, отчет строится без использования СКД или логика требует жесткой обработки в коде модуля), нам придется применить технику получения среза последних на каждую дату с использованием временных таблиц.
Проанализируем этот алгоритм шаг за шагом:
// ШАГ 1: Собираем уникальные комбинации Валют и Дат документов во временную таблицу
ВЫБРАТЬ РАЗЛИЧНЫЕ
РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК Валюта,
НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) КАК ПериодДокумента
ПОМЕСТИТЬ ВТ_ВалютыДаты
ИЗ
Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 2: Для каждой даты документа находим реальную дату курса (максимальную дату курса, которая меньше или равна дате документа)
ВЫБРАТЬ
ВТ_ВалютыДаты.Валюта КАК Валюта,
ВТ_ВалютыДаты.ПериодДокумента КАК ПериодДокумента,
МАКСИМУМ(КурсыВалют.Период) КАК ПериодКурса
ПОМЕСТИТЬ ВТ_МаксимальныеДатыКурсов
ИЗ
ВТ_ВалютыДаты КАК ВТ_ВалютыДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
ПО ВТ_ВалютыДаты.Валюта = КурсыВалют.Валюта
И ВТ_ВалютыДаты.ПериодДокумента >= КурсыВалют.Период
ГРУППИРОВАТЬ ПО
ВТ_ВалютыДаты.Валюта,
ВТ_ВалютыДаты.ПериодДокумента
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 3: Получаем значения курса и кратности на найденные даты курса
ВЫБРАТЬ
ВТ_МаксимальныеДатыКурсов.Валюта КАК Валюта,
ВТ_МаксимальныеДатыКурсов.ПериодДокумента КАК ПериодДокумента,
КурсыВалют.Курс КАК Курс,
КурсыВалют.Кратность КАК Кратность
ПОМЕСТИТЬ ВТ_КурсыНаДаты
ИЗ
ВТ_МаксимальныеДатыКурсов КАК ВТ_МаксимальныеДатыКурсов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют
ПО ВТ_МаксимальныеДатыКурсов.Валюта = КурсыВалют.Валюта
И ВТ_МаксимальныеДатыКурсов.ПериодКурса = КурсыВалют.Период
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 4: Соединяем исходные документы с полученной таблицей курсов и рассчитываем сумму
ВЫБРАТЬ
РасходнаяНакладнаяЗапасы.Ссылка КАК Регистратор,
РасходнаяНакладнаяЗапасы.Номенклатура КАК Номенклатура,
РасходнаяНакладнаяЗапасы.Сумма КАК СуммаВВалюте,
РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента КАК ВалютаДокумента,
ЕСТЬNULL(ВТ_КурсыНаДаты.Курс, 1) КАК Курс,
ЕСТЬNULL(ВТ_КурсыНаДаты.Кратность, 1) КАК Кратность,
ВЫБОР
КОГДА РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента В (ВЫБРАТЬ Константы.ВалютаУчета ИЗ Константы)
ТОГДА РасходнаяНакладнаяЗапасы.Сумма
ИНАЧЕ ВЫБОР
КОГДА ЕСТЬNULL(ВТ_КурсыНаДаты.Кратность, 0) > 0
ТОГДА (РасходнаяНакладнаяЗапасы.Сумма * ЕСТЬNULL(ВТ_КурсыНаДаты.Курс, 1)) / ВТ_КурсыНаДаты.Кратность
ИНАЧЕ РасходнаяНакладнаяЗапасы.Сумма
КОНЕЦ
КОНЕЦ КАК СуммаДокумента
ИЗ
Документ.РасходнаяНакладная.Запасы КАК РасходнаяНакладнаяЗапасы
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_КурсыНаДаты КАК ВТ_КурсыНаДаты
ПО РасходнаяНакладнаяЗапасы.Ссылка.ВалютаДокумента = ВТ_КурсыНаДаты.Валюта
И (НАЧАЛОПЕРИОДА(РасходнаяНакладнаяЗапасы.Ссылка.Дата, ДЕНЬ) = ВТ_КурсыНаДаты.ПериодДокумента)
---
Давайте честно признаемся: обращение напрямую к табличным частям документов (таким как Документ.РасходнаяНакладная.Запасы) для построения отчетов по продажам считается плохой практикой проектирования в 1С. Почему?
Проведен = ИСТИНА, ПометкаУдаления = ЛОЖЬ).В конфигурации 1С:УНФ для накопления информации о продажах разработан специализированный регистр накопления — Продажи. При проведении документов система автоматически рассчитывает все курсы и записывает в этот регистр готовые данные:
Сумма — сумма продажи, пересчитанная в валюту управленческого (национального) учета по актуальному курсу на момент проведения.СуммаВВалюте — сумма продажи в валюте документа (сделки).Валюта — валюта, в которой была совершена сделка.Посмотрим, как изящно и просто решается ваша задача, если мы обратимся к виртуальной таблице оборотов этого регистра:
ВЫБРАТЬ РАЗРЕШЕННЫЕ
ПродажиОбороты.СуммаОборот КАК Выручка, // Это готовая сумма в валюте учета!
ПродажиОбороты.СуммаВВалютеОборот КАК ВыручкаВВалюте, // Сумма в валюте документа
ПродажиОбороты.Регистратор КАК Регистратор,
ПродажиОбороты.Номенклатура КАК Номенклатура,
ПродажиОбороты.Контрагент КАК Контрагент,
ПродажиОбороты.Валюта КАК ВалютаДокумента,
НАЧАЛОПЕРИОДА(ПродажиОбороты.Период, ДЕНЬ) КАК День,
НАЧАЛОПЕРИОДА(ПродажиОбороты.Период, МЕСЯЦ) КАК Месяц
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, Авто, ) КАК ПродажиОбороты
Использование регистра оборотов Продажи.Обороты полностью избавляет вас от необходимости рассчитывать курсы валют в запросе вручную. Платформа делает это автоматически в момент проведения накладной. Более того, ваш отчет станет работать в разы быстрее, код запроса сократится в несколько раз, а риск допустить ошибку при делении на нулевую кратность сведется к нулю.
Выбирайте наиболее подходящий для вашей конфигурации и задачи метод и делайте ваши отчеты быстрыми и надежными! Удачи в разработке!