Еще раз о рабочих днях. Быстрый способ расчета в запросах

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

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

На мой взгляд, предлагаемые решения обладают теми или иными недостатками. В их числе:

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

Предлагаю свой вариант решения.

Постановка задачи:

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

В чем могут быть "подводные камни" при решении?  Например токарь работает по стандартному рабочему графику — пятидневке. 01 апреля 2024 он начинает изготавливать деталь №1, тратит на ее изготовление 5 дней, и начинает изготавливать следующую деталь №2. Когда он закончит изготовление детали №1? Когда начнет изготавливать деталь №2? Казалось бы в обоих случаях ответ: через 5 рабочих дней после 01 апреля, т.е. к 01.04.2024 надо прибавить 5 рабочих дней. Но в первом случае ответ — 05.04.2024, а во втором — 08.04.2024.

Решение:

Решение поставленной задачи неожиданно получилось довольно простым.

Предлагается следующее:

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

РабочийГрафик — ссылка на справочник "РабочиеГрафики" — если на предприятии используется несколько графиков (пятидневка, пятидневка с праздниками, семидневка и т.п.)

Дата — дата графика (без времени)

ЭтоРабочийДень — флаг рабочий/нерабочий день

КолВоДнейСНачалаПериода — Число рабочих дней, прошедших до начала даты записи, начиная с определенной, наперед заданной даты. В моем примере используется 01.01.2000.

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

Пример содержимого:

 

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

// Возвращает разность в днях между двумя датами (Дата2-Дата1) с учетом рабочего графика
// Даты до полудня округляются вниз, после - вверх
// Параметры:
//  Дата1  - Дата -  Начальная дата
//  Дата2  - Дата -  Конечная дата
//  РабочийГрафик  - СправочникСсылка.РабочиеГрафики - Рабочий график
// Возвращаемое значение:
//  Число    - разность дат
Функция РазностьДат(Знач Дата1, Знач Дата2, Знач РабочийГрафик)Экспорт

СекундВ12Часах = 12 * 60 * 60;
Дата1 = НачалоДня(Дата1 + СекундВ12Часах);
Дата2 = НачалоДня(Дата2 + СекундВ12Часах);

Запр = Новый Запрос;
Текст = "ВЫБРАТЬ
| РабочиеДни2.КолВоДнейСНачалаПериода - РабочиеДни1.КолВоДнейСНачалаПериода КАК КолВоДней
|ИЗ
| РегистрСведений.РабочиеДни КАК РабочиеДни1
|  ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РабочиеДни КАК РабочиеДни2
|  ПО (РабочиеДни2.РабочийГрафик = &РабочийГрафик)
|   И (РабочиеДни2.Дата = &Дата2)
|ГДЕ
| РабочиеДни1.РабочийГрафик = &РабочийГрафик
| И РабочиеДни1.Дата = &Дата1";
Запр.Текст = Текст;
Запр.УстановитьПараметр("Дата1", Дата1);
Запр.УстановитьПараметр("Дата2", Дата2);
Запр.УстановитьПараметр("РабочийГрафик", РабочийГрафик);

РезЗапроса = Запр.Выполнить();
Если НЕ РезЗапроса.Пустой() Тогда
Выб = РезЗапроса.Выбрать(ОбходРезультатаЗапроса.Прямой);
Выб.Следующий();
Результат = Выб.КолВоДней;
Иначе
КонецЕсли;
Возврат Результат;
КонецФункции //


// Добавляет к дате заданное количество дней с учетом рабочего графика
// Параметры:
//  Дата  - Дата -  Дата.  Даты до полудня округляются вниз, после - вверх
//  КолВоДней  - Число - количество дней, любое целое
//  РабочийГрафик  - СправочникСсылка.РабочиеГрафики - РабочийГрафик
//  РезультатНачалоДня - Булево - Результат должен быть начало дня
// Возвращаемое значение:
//  Дата    -  Рассчитанная дата
Функция ДобавитьКДате(Знач Дата, Знач КолВоДней, Знач РабочийГрафик, РезультатНачалоДня) Экспорт

СекундВ12Часах = 12 * 60 * 60;
Дата = НачалоДня(Дата + СекундВ12Часах);
Если РезультатНачалоДня = Ложь Тогда
КолВоДней = КолВоДней - 1;
КонецЕсли;
Запр = Новый Запрос;
Текст = "ВЫБРАТЬ
| РабочиеДни2.Дата КАК Дата
|ИЗ
| РегистрСведений.РабочиеДни КАК РабочиеДни1
|  ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РабочиеДни КАК РабочиеДни2
|  ПО (РабочиеДни2.РабочийГрафик = &РабочийГрафик)
|   И (РабочиеДни2.КолВоДнейСНачалаПериода = РабочиеДни1.КолВоДнейСНачалаПериода + &КолВоДней)
|   И (РабочиеДни2.ЭтоРабочийДень)
|ГДЕ
| РабочиеДни1.РабочийГрафик = &РабочийГрафик
| И РабочиеДни1.Дата = &Дата";
Запр.Текст = Текст;
Запр.УстановитьПараметр("Дата", Дата);
Запр.УстановитьПараметр("КолВоДней", КолВоДней);
Запр.УстановитьПараметр("РабочийГрафик", РабочийГрафик);

РезЗапроса = Запр.Выполнить();
Если НЕ РезЗапроса.Пустой() Тогда
Выб = РезЗапроса.Выбрать(ОбходРезультатаЗапроса.Прямой);
Выб.Следующий();
Результат = Выб.Дата;
Если РезультатНачалоДня = Ложь Тогда
Результат = КонецДня(Результат);
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции //

 

Примеры использования

// Когда завершится работа токаря №1?
ДобавитьКДате('20240401', 5, РабочийГрафик, Ложь);

// Когда токарь начнет работу №2 после завершения пятидневной работы №1?
ДобавитьКДате('20240401', 5, РабочийГрафик, Истина);

// Сколько фактически токарь делал работу - от начала до конца?
РазностьДат(НачалоДня(Дата1), КонецДня(Дата2), РабочийГрафик);

// Сколько рабочих дней токарь прогулял между окончанием работы №1 и началом работы №2?
РазностьДат(КонецДня(Дата1), НачалоДня(Дата2), РабочийГрафик);

// Сколько рабочих дней прошло от окончания работы №1 до сегодняшней вечерней планерки?
РазностьДат(КонецДня(Дата1), КонецДня(ТекущаяДата()), РабочийГрафик);

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

Может возникнуть вопрос: оправдано ли с точки зрения производительности использование дополнительного регистра такой структуры, ведь при изменении флага рабочего/выходного дня надо пересчитывать все записи с большей датой? Я считаю, что вполне. Во-первых, изменение производственного календаря происходит обычно не чаще одного раза в месяц, а полный пересчет и сохранение набора записей за 100 лет(~40000 записей) по выбранному графику занимает считанные секунды. А во-вторых, выгода от использования быстрого массового расчета как правило с лихвой перекроет все время, потраченное на предварительную подготовку.

А что же БСП?

Опытный разработчик, использующий БСП, может сказать: "Так ведь в БСП реализовано почти что то же самое!". Да, действительно в БСП есть аналогичный регистр:

Есть также программный интерфейс модулей "ГрафикиРаботы", "КалендарныеГрафики" с функциями "РазностьДатПоКалендарю", "ДатыПоГрафику" и т.п. Но если присмотреться, то можно увидеть, что в регистре имеется измерение "Год". То есть в этом регистре отсчет количества дней идет с начала каждого года. Когда мы работаем с датами в пределах одного года, то подход при расчете совпадает с рассмотренным. Но если даты попадают в разные года, а особенно если рассматривается промежуток в несколько лет, то алгоритм получается весьма сложным. Все интересующиеся могут самостоятельно сравнить объем программного кода в библиотеке и в предложенном решении. Скорее всего, разработчики БСП стремились к упрощению процедуры заполнения — каждый год рабочего графика заполняется отдельно и не зависит от других. Но в результате мы получаем существенное усложнение алгоритмов при решении практических задач. Я бы рекомендовал использовать регистры БСП как источник для заполнения регистра "РабочиеДни", а все дальнейшие операции производить уже с ним.

UPD 25.06.2024:

Для конфигураций с БСП добавлено заполнение регистра на основе данных из типовых объектов — регистра КалендарныеГрафики и справочника Календари. В процессе обработки заполняется регистр за период с 2000 г. по примерно 2109 г. — 40000 дней.

 

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

3 Comments

  1. VmvLer
    Решение поставленной задачи неожиданно получилось довольно простым.

    и далее идет описание таблицы которую необходимо добавить конфигурацию для …простоты.

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

    Reply
  2. Alxby

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

    Reply
  3. Alxby

    Update: Добавлено заполнение информации на основе данных из типовых объектов БСП

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *