Как рассчитать разность дат без учета выходных и праздников в отчетах СКД?

Программист 1С v8.3 (Управляемые формы)
← На главную

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

Ограничения использования функций общих модулей в пользовательских полях СКД

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

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

Решение 1: Использование функций общего модуля через конфигуратор

Если нам все же необходимо задействовать логику, реализованную в функции общего модуля, то это возможно, но потребует доработки отчета в конфигураторе. Мы должны определить вычисляемое поле в схеме компоновки данных самого отчета.

Для того чтобы СКД мог вызывать функции из общих модулей, необходимо выполнить несколько условий:

  1. Сама функция должна быть определена как экспортная. То есть, к ее объявлению добавляется ключевое слово Экспорт. Это позволяет вызывать функцию из внешнего контекста.
  2. Функция должна находиться в общем модуле, который доступен для вызова из серверного контекста. Например, это может быть общий модуль с параметром "Сервер" или "СерверВызовКлиента".
  3. Самое главное: для процессора компоновки данных (объекта ПроцессорКомпоновкиДанных) необходимо программно установить параметр ВозможностьИспользованияВнешнихФункций = Истина. Этот параметр обычно устанавливается при формировании отчета, например, в обработчике события ПриКомпоновкеРезультата или в функции, ответственной за исполнение СКД. Без этого параметра СКД не позволит вызывать внешние функции из выражений.

После выполнения этих условий мы можем использовать вызов нашей функции в выражении вычисляемого поля, определенного в конфигураторе. Например, если у нас есть функция РазностьДатБезВыходных в модуле МойОбщийМодуль, вычисляемое поле будет выглядеть так:


// Пример вычисляемого поля в СКД
// Выражение: МойОбщийМодуль.РазностьДатБезВыходных(НачальнаяДата, КонечнаяДата)

Однако, даже при таком подходе важно учитывать производительность. Если функция будет вызываться для каждой строки отчета, содержащего большое количество данных, это может значительно замедлить формирование отчета, поскольку каждый вызов функции – это дополнительные вычисления на сервере. Важно оптимизировать такие функции или рассмотреть альтернативные подходы.

Решение 2: Расчет разности дат с учетом календаря непосредственно в запросе СКД

Наиболее производительным и гибким способом расчета разности дат без учета выходных и праздников является выполнение логики непосредственно в запросе, который формирует данные для СКД. Этот подход позволяет передать основную вычислительную нагрузку на СУБД, которая, как правило, оптимизирована для работы с большими объемами данных и обработкой наборов записей. Для разработки и проверки таких запросов отчетов СКД можно использовать инженерную консоль запросов с поддержкой схем компоновки данных.

Стандартная функция СКД РазностьДат(Выражение1, Выражение2, ТипРазности) позволяет получить разницу между датами в различных единицах (секундах, минутах, часах, днях и т.д.), но она не учитывает производственный календарь, выходные или праздничные дни. Поэтому для нашей задачи она подходит только как отправная точка для получения общей разницы в днях, из которой затем будут вычитаться нерабочие дни.

Для получения точной разницы рабочих дней нам необходимо взаимодействовать с производственным календарем. Рассмотрим общий алгоритм:

  1. Получаем общую разность дат в днях. Используем РАЗНОСТЬДАТ(НачальнаяДата, КонечнаяДата, ДЕНЬ) в запросе. Это даст нам общее количество календарных дней в периоде.
  2. Определяем количество выходных и праздничных дней в интервале. Это ключевой момент. В типовых конфигурациях 1С для этих целей используются регистры сведений, хранящие производственные календари (например, РегистрСведений.ПроизводственныйКалендарь или РегистрСведений.ГрафикиРаботы). Мы должны выбрать из этих регистров все нерабочие дни, попадающие в наш период.
  3. Вычитаем количество выходных/праздничных дней из общей разности. Полученное значение и будет искомой разностью рабочих дней.

Реализовать это можно несколькими способами в запросе СКД:

  1. Через временные таблицы (ВТ). Этот подход часто является наиболее понятным и достаточно производительным. Мы можем сначала в одной ВТ получить набор всех дней в нашем интервале (если интервал не очень большой, можно сгенерировать набор дат), затем соединить ее с регистром производственного календаря, чтобы пометить выходные и праздничные дни. После этого посчитать количество таких дней и в конечном запросе вычесть их из общей разницы.
    
    // Запрос 1: Исходные данные и общая разность дней
    ВЫБРАТЬ
        ИсходныеДанные.Ссылка КАК ДокументСсылка,
        ИсходныеДанные.ДатаНачала КАК НачальнаяДата,
        ИсходныеДанные.ДатаОкончания КАК КонечнаяДата,
        РАЗНОСТЬДАТ(ИсходныеДанные.ДатаНачала, ИсходныеДанные.ДатаОкончания, ДЕНЬ) КАК ОбщаяРазностьДней
    ПОМЕСТИТЬ ВТ_ИсходныеДанные
    ИЗ
        Документ.НашДокумент КАК ИсходныеДанные
    ГДЕ
        ИсходныеДанные.Проведен
    ;
    
    // Запрос 2: Получаем нерабочие дни в интервале для каждой строки
    ВЫБРАТЬ
        ИсходныеДанные.ДокументСсылка,
        ИсходныеДанные.НачальнаяДата,
        ИсходныеДанные.КонечнаяДата,
        ИсходныеДанные.ОбщаяРазностьДней,
        СУММА(ВЫБОР
                КОГДА Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисления.ВидыДнейПроизводственногоКалендаря.Выходной)
                    ИЛИ Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисления.ВидыДнейПроизводственногоКалендаря.Праздник)
                ТОГДА 1
                ИНАЧЕ 0
            КОНЕЦ) КАК КоличествоВыходныхИПраздников
    ПОМЕСТИТЬ ВТ_ПромежуточныйРезультат
    ИЗ
        ВТ_ИсходныеДанные КАК ИсходныеДанные
            ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПроизводственныйКалендарь КАК Календарь
                ПО Календарь.Дата МЕЖДУ ИсходныеДанные.НачальнаяДата И ИсходныеДанные.КонечнаяДата
    ГДЕ
        Календарь.Дата МЕЖДУ ИсходныеДанные.НачальнаяДата И ИсходныеДанные.КонечнаяДата
    СГРУППИРОВАТЬ ПО
        ИсходныеДанные.ДокументСсылка,
        ИсходныеДанные.НачальнаяДата,
        ИсходныеДанные.КонечнаяДата,
        ИсходныеДанные.ОбщаяРазностьДней
    ;
    
    // Запрос 3: Конечный расчет рабочих дней
    ВЫБРАТЬ
        ПромежуточныйРезультат.ДокументСсылка,
        ПромежуточныйРезультат.НачальнаяДата,
        ПромежуточныйРезультат.КонечнаяДата,
        ПромежуточныйРезультат.ОбщаяРазностьДней - ПромежуточныйРезультат.КоличествоВыходныхИПраздников КАК РазностьРабочихДней
    ИЗ
        ВТ_ПромежуточныйРезультат КАК ПромежуточныйРезультат
    

    Важно: Приведенный пример — это упрощенный запрос. В реальной СКД для получения всех дней между датами может потребоваться более сложная логика, например, динамическое создание набора дат через виртуальные таблицы или использование функций, генерирующих последовательности, если интервал дат сильно варьируется и простой МЕЖДУ не справляется с задачей перебора всех дней.

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

    Этот метод арифметически менее точен, так как не учитывает праздники, которые могут выпадать на будние дни, но проще в реализации. Он подойдет, если точность по праздникам не критична, и достаточно исключить только субботы и воскресенья. Для более точного подсчета без производственного календаря, но с учетом суббот/воскресений, потребуется итеративный подход или более сложный запрос, генерирующий каждый день в интервале и проверяющий ДЕНЬНЕДЕЛИ().

Решение 3: Использование функций Библиотеки Стандартных Подсистем (БСП)

В типовых конфигурациях 1С, основанных на Библиотеке Стандартных Подсистем (БСП), разработчики предусмотрели специальные функции для работы с календарными графиками. Одна из таких функций, которая идеально подходит для нашей задачи, это РазностьДатПоКалендарю (или аналогичная) из общего модуля КалендарныеГрафики.

Эта функция позволяет определить количество дней в заданном периоде с учетом конкретного графика работы или производственного календаря, принимая на вход начальную и конечную даты, а также, возможно, идентификатор календаря или графика. Использование таких функций, если они доступны в вашей конфигурации, является наиболее удобным, точным и рекомендованным способом, поскольку они уже отлажены и оптимизированы разработчиками 1С, а также централизованно учитывают все особенности производственных календарей.

Чтобы воспользоваться этой функцией, вам все равно потребуется доработка в конфигураторе. Вы сможете вызвать ее в вычисляемом поле СКД, как мы описывали в "Решении 1", после установки ВозможностьИспользованияВнешнихФункций = Истина в обработчике, формирующем СКД. Пример вызова может выглядеть так:


// Пример вызова функции БСП в вычисляемом поле СКД
// Выражение: КалендарныеГрафики.РазностьДатПоКалендарю(НачальнаяДата, КонечнаяДата, "ОсновнойПроизводственныйКалендарь")

Название функции и ее параметры могут немного отличаться в зависимости от версии БСП и конкретной конфигурации, поэтому всегда рекомендуем обращаться к синтакс-помощнику или исходному коду общего модуля КалендарныеГрафики вашей конфигурации для уточнения точного синтаксиса и доступных параметров.

Производительность и рекомендации

При выборе метода всегда анализируйте объем данных и ожидаемую производительность отчета. Неправильно выбранный подход может привести к очень медленной работе отчета.

  1. Наиболее производительным, как правило, является расчет непосредственно в запросе СКД. Особенно это актуально для отчетов с большим количеством строк и для длительных периодов. Механизмы СУБД хорошо справляются с обработкой больших наборов данных и выполнениями агрегирующих операций.
  2. Использование функций общего модуля (включая БСП) в вычисляемых полях СКД (определенных в конфигураторе) удобно с точки зрения инкапсуляции логики и повторного использования кода. Однако, если функция является сложной и вызывается для каждой строки отчета, это может привести к снижению производительности. Для типовых функций БСП, таких как РазностьДатПоКалендарю, разработчики обычно предусматривают оптимизации, поэтому их использование часто является хорошим компромиссом между производительностью и удобством.
  3. В пользовательском режиме без конфигуратора добавить такое поле, которое учитывает календарь и выходные, невозможно из-за ограничений языка выражений СКД, как мы выяснили в начале. Любое сложное вычисление, требующее доступа к серверным данным или функциям встроенного языка, потребует изменений в конфигураторе.

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

← На главную