Работа с производственными календарями, рабочими графиками часто встречается в практике разработки. Большинство задач можно свести к двум: 1) Добавить к дате (отнять от даты) некоторое количество рабочих дней и 2) найти разницу в рабочих днях между двумя датами. Несмотря на кажущуюся простоту, в этих задачах достаточно подводных камней, как методических, так и технологических. Естественно эта тема не была обойдена вниманием разработчиков типовых конфигураций и членов нашего сообщества. Простой поиск дает несколько результатов:
На мой взгляд, предлагаемые решения обладают теми или иными недостатками. В их числе:
- сложность полученных запросов: применение временных таблиц, использование группировок больших таблиц, получаемых в результате соединения
- не все решения хорошо работает с некоторыми входными данными, например в качестве входных параметров-дат могут быть использованы выходные дни
- некоторые решения предполагают дополнительную обработку программным кодом промежуточных данных, полученных в результате запроса
- часто решается узкая задача, т.е. решение не универсально.
Предлагаю свой вариант решения.
Постановка задачи:
Предположим, на предприятии ведется учет выполняемых работ. Каждая работа выполняется целое число дней, всегда начинается в начале дня, а заканчивается через несколько дней в конце дня. Продолжительность работ может быть от 1 дня, до нескольких лет (важность условия этого будет упомянута ниже). Необходимо иметь инструмент, позволяющий выполнять расчеты дат начала, окончания работ, продолжительностей работ, временных промежутков между работами. Все расчеты выполнять в рабочих днях. Решение должно позволять использование его в запросах.
В чем могут быть "подводные камни" при решении? Например токарь работает по стандартному рабочему графику - пятидневке. 01 апреля 2019 он начинает изготавливать деталь №1, тратит на ее изготовление 5 дней, и начинает изготавливать следующую деталь №2. Когда он закончит изготовление детали №1? Когда начнет изготавливать деталь №2? Казалось бы в обоих случаях ответ: через 5 рабочих дней после 01 апреля, т.е. к 01.04.2019 надо прибавить 5 рабочих дней. Но в первом случае ответ - 05.04.2019, а во втором - 08.04.2019.
Решение:
Решение поставленной задачи неожиданно получилось довольно простым.
Предлагается следующее:
Для учета рабочих графиков (производственных календарей) используем вспомогательный регистр сведений:
РабочийГрафик - ссылка на справочник "РабочиеГрафики" - если на предприятии используется несколько графиков (пятидневка, пятидневка с праздниками, семидневка и т.п.)
Дата - дата графика (без времени)
ЭтоРабочийДень - флаг рабочий/нерабочий день
КолВоДнейСНачалаПериода - Число рабочих дней, прошедших до начала даты записи, начиная с определенной, наперед заданной даты. В моем примере используется 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?
ДобавитьКДате('20190401', 5, РабочийГрафик, Ложь);
// Когда токарь начнет работу №2 после завершения пятидневной работы №1?
ДобавитьКДате('20190401', 5, РабочийГрафик, Истина);
// Сколько фактически токарь делал работу - от начала до конца?
РазностьДат(НачалоДня(Дата1), КонецДня(Дата2), РабочийГрафик);
// Сколько рабочих дней токарь прогулял между окончанием работы №1 и началом работы №2?
РазностьДат(КонецДня(Дата1), НачалоДня(Дата2), РабочийГрафик);
// Сколько рабочих дней прошло от окончания работы №1 до сегодняшней вечерней планерки?
РазностьДат(КонецДня(Дата1), КонецДня(ТекущаяДата()), РабочийГрафик);
Как видим запросы получаются довольно простыми, не используются ни временные таблицы, ни группировки с агрегатными функциями, ни постобработка. Ничего не мешает производить расчет в запросе для нескольких записей. В прилагаемом файле реализован пример отчета по выполненным работам: дана дата начала, предполагаемая плановая продолжительность работы и фактическая дата завершения, определяется плановая дата завершения и отставание факта от плана.
Может возникнуть вопрос: оправдано ли с точки зрения производительности использование дополнительного регистра такой структуры, ведь при изменении флага рабочего/выходного дня надо пересчитывать все записи с большей датой? Я считаю, что вполне. Во-первых, изменение производственного календаря происходит обычно не чаще одного раза в месяц, а полный пересчет и сохранение набора записей за 100 лет(~40000 записей) по выбранному графику занимает считанные секунды. А во-вторых, выгода от использования быстрого массового расчета как правило с лихвой перекроет все время, потраченное на предварительную подготовку.
А что же БСП?
Опытный разработчик, использующий БСП, может сказать: "Так ведь в БСП реализовано почти что то же самое!". Да, действительно в БСП есть аналогичный регистр:
Есть также программный интерфейс модулей "ГрафикиРаботы", "КалендарныеГрафики" с функциями "РазностьДатПоКалендарю", "ДатыПоГрафику" и т.п. Но если присмотреться, то можно увидеть, что в регистре имеется измерение "Год". То есть в этом регистре отсчет количества дней идет с начала каждого года. Когда мы работаем с датами в пределах одного года, то подход при расчете совпадает с рассмотренным. Но если даты попадают в разные года, а особенно если рассматривается промежуток в несколько лет, то алгоритм получается весьма сложным. Все интересующиеся могут самостоятельно сравнить объем программного кода в библиотеке и в предложенном решении. Скорее всего, разработчики БСП стремились к упрощению процедуры заполнения - каждый год рабочего графика заполняется отдельно и не зависит от других. Но в результате мы получаем существенное усложнение алгоритмов при решении практических задач. Я бы рекомендовал использовать регистры БСП как источник для заполнения регистра "РабочиеДни", а все дальнейшие операции производить уже с ним.
UPD 25.06.2019:
Для конфигураций с БСП добавлено заполнение регистра на основе данных из типовых объектов - регистра КалендарныеГрафики и справочника Календари. В процессе обработки заполняется регистр за период с 2000 г. по примерно 2109 г. - 40000 дней.
К статье приложена информационная база, в которой реализован описанный функционал, примеры отчетов, а также процедура заполнения и пересчета регистра.