Как реализовать расчет и вывод периодического графика в 1С:Подробное руководство

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

В практике разработки на платформе «1С:Предприятие» регулярно возникают задачи, связанные с расчетом периодических событий на будущие периоды. Это могут быть графики платежей, регламентные сервисные работы, расписания дежурств или, как в нашем игровом, но очень показательном примере, график полива декоративных растений. У нас есть два объекта: Дерево 1 (требует 3 литра полива каждые 5 дней) и Дерево 2 (требует 2 литра полива каждые 3 дня). Нам необходимо вывести график полива на определенный будущий период, например, с 1 по 10 августа.

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

Архитектурное проектирование: Статический график против динамического («План-Факт»)

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

  1. Статический (жесткий) график. В этом случае все последующие даты рассчитываются строго от первоначальной даты старта, независимо от того, выполнялись ли работы фактически. Если Дерево 1 должны были полить 5 августа, но полили только 7 августа, следующий плановый полив все равно останется на 10 августа. При таком подходе хранить сам график в базе данных не имеет смысла — это создает избыточность. Достаточно хранить параметры в регистре сведений ПараметрыПолива (Периодичность, Дата начала, Объем), а сам график рассчитывать динамически «на лету» при выводе отчета или печатной формы.
  2. Динамический (сдвигающийся) график. В реальной жизни биологические объекты требуют ухода по факту. Если мы пропустили полив и полили дерево на 2 дня позже, то следующий полив должен отсчитываться от даты фактического последнего действия, иначе мы рискуем залить растение. Для реализации такой логики нам понадобится документ ПоливРастений (фиксирующий факт выполнения) и регистр сведений ИсторияПолива. Плановый график в этом случае будет рассчитываться по формуле: ДатаПоследнегоФактическогоПолива + ПериодичностьДней.

Проанализируем обе схемы реализации подробнее.

Вариант 1. Динамический расчет на встроенном языке 1С (Обход в цикле)

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

Рассмотрим пример функции, которая принимает параметры и возвращает заполненную таблицу значений с датами и объемами полива:


Функция СформироватьТаблицуПолива(Знач ДатаНачалаПериода, Знач ДатаКонцаПериода, ПараметрыДеревьев) Экспорт
    
    ТаблицаГрафика = Новый ТаблицаЗначений;
    ТаблицаГрафика.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
    ТаблицаГрафика.Колонки.Добавить("Дерево", Новый ОписаниеТипов("СправочникСсылка.Деревья"));
    ТаблицаГрафика.Колонки.Добавить("ОбъемПолива", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(10, 2)));
    
    // Перебираем каждый день из указанного периода
    ТекущаяДата = НачалоДня(ДатаНачалаПериода);
    КонечнаяДата = НачалоДня(ДатаКонцаПериода);
    
    Пока ТекущаяДата <= КонечнаяДата Цикл
        
        Для Каждого Настройка Из ПараметрыДеревьев Цикл
            
            // Вычисляем разницу в днях между текущим днем и точкой отсчета (датой старта)
            РазностьВДнях = (ТекущаяДата - НачалоДня(Настройка.ДатаСтарта)) / 86400;
            
            Если РазностьВДнях >= 0 Тогда
                // Проверяем кратность периода с помощью оператора остатка от деления %
                Если РазностьВДнях % Настройка.ПериодичностьДней = 0 Тогда
                    НоваяСтрока = ТаблицаГрафика.Добавить();
                    НоваяСтрока.Дата = ТекущаяДата;
                    НоваяСтрока.Дерево = Настройка.Дерево;
                    НоваяСтрока.ОбъемПолива = Настройка.ОбъемЛитров;
                КонецЕсли;
            КонецЕсли;
            
        КонецЦикла;
        
        ТекущаяДата = ТекущаяДата + 86400; // Переходим к следующему дню
    КонецЦикла;
    
    Возврат ТаблицаГрафика;
    
КонецФункции

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

Вариант 2. Математический расчет периодичности в запросах 1С

При попытке перенести этот расчет в запрос мы сталкиваемся с серьезным ограничением: в языке запросов 1С отсутствует оператор получения остатка от деления (аналог % или MOD). Нам придется воссоздать этот математический инструмент вручную.

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

Чтобы получить математически точную целую часть от деления в запросе 1С, мы используем следующую формулу:

ЦелаяЧасть = ВЫРАЗИТЬ(Делимое / Делитель КАК ЧИСЛО(15, 0)) - ВЫБОР КОГДА Делимое / Делитель - ВЫРАЗИТЬ(Делимое / Делитель КАК ЧИСЛО(15, 0)) < 0 ТОГДА 1 ИНАЧЕ 0 КОНЕЦ

Зная целую часть, мы легко можем вычислить остаток от деления по формуле: Остаток = Делимое - (Делитель * ЦелаяЧасть). Если этот остаток равен нулю, значит, день является расчетным.

Посмотрим на пример практической реализации такого запроса. Предположим, что параметры у нас хранятся во временной таблице ВТ_Параметры, а список дат мы получаем из производственного календаря:


ВЫБРАТЬ
    Параметры.Дерево КАК Дерево,
    Параметры.ДатаСтарта КАК ДатаСтарта,
    Параметры.ПериодичностьДней КАК ПериодичностьДней,
    Параметры.ОбъемЛитров КАК ОбъемЛитров,
    Календарь.ДатаКалендаря КАК ДатаКалендаря,
    РАЗНОСТЬДАТ(Параметры.ДатаСтарта, Календарь.ДатаКалендаря, ДЕНЬ) КАК ВсегоДней
ПОМЕСТИТЬ ВТ_ПодготовкаДанных
ИЗ
    ВТ_Параметры КАК Параметры
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
        ПО Календарь.ДатаКалендаря МЕЖДУ &ДатаНачала И &ДатаОкончания
        И Календарь.ДатаКалендаря >= Параметры.ДатаСтарта
;

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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ.Дерево КАК Дерево,
    ВТ.ДатаКалендаря КАК ДатаКалендаря,
    ВТ.ОбъемЛитров КАК ОбъемЛитров
ИЗ
    ВТ_РасчетЦелойЧасти КАК ВТ
ГДЕ
    // Условие на равенство остатка от деления нулю
    (ВТ.ВсегоДней - (ВТ.ПериодичностьДней * ВТ.ЦелаяЧасть)) = 0

Вариант 3. Генерация последовательности дат внутри запроса «на лету»

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

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


ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 9
;

////////////////////////////////////////////////////////////////////////////////
// Генерируем числа от 0 до 999
ВЫБРАТЬ
    Т1.Цифра + Т2.Цифра * 10 + Т3.Цифра * 100 КАК ЧислоДней
ПОМЕСТИТЬ ВТ_ПоследовательностьЧисел
ИЗ
    ВТ_Цифры КАК Т1
    КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т2
    КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т3
;

////////////////////////////////////////////////////////////////////////////////
// Превращаем последовательность чисел в последовательность дат
ВЫБРАТЬ
    ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, ВТ.ЧислоДней) КАК ДатаКалендаря
ПОМЕСТИТЬ ВТ_ГенерируемыйКалендарь
ИЗ
    ВТ_ПоследовательностьЧисел КАК ВТ
ГДЕ
    ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, ВТ.ЧислоДней) <= &ДатаОкончания

Соединяя полученную виртуальную таблицу ВТ_ГенерируемыйКалендарь с нашими параметрами полива, мы получим полностью независимый алгоритм, который будет работать стабильно на любой конфигурации 1С и СУБД.

Рекомендации по выводу через Систему Компоновки Данных (СКД)

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

← На главную