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

Публикация № 1070851

Программирование - Практика программирования

Рабочие дни запрос календарь график производственный добавить дате разность дат

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

Работа с производственными календарями, рабочими графиками часто встречается в практике разработки. Большинство задач можно свести к двум: 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 дней.

 

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

3

Скачать файлы

Наименование Файл Версия Размер
Еще раз о рабочих днях. Быстрый способ расчета в запросах.:
.dt 316,74Kb
25.06.19
0
.dt 316,74Kb Скачать

См. также

Специальные предложения

Комментарии
Избранное Подписка Сортировка: Древо
1. VmvLer 20.06.19 13:42 Сейчас в теме
Решение поставленной задачи неожиданно получилось довольно простым.

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

Мне кажется, что не может быть простым решение для которого требуется добавлять в конфигурацию таблицу. Ведь эту таблицу необходимо обслуживать самостоятельно.
2. Alxby 451 20.06.19 15:39 Сейчас в теме
(1)В ваших словах есть доля истины - при обслуживании конфигураций, стоящих на поддержке, придется провести дополнительные работы. Если же абстрагироваться от типовых конфигураций и рассматривать разработку "с нуля", то решение действительно простое - один регистр и несложные алгоритмы расчета. Конечно же, каждый разработчик, принимая решение о внедрении в свою систему какого-либо механизма, должен соотнести трудозатраты с выгодой от такого внедрения. В статье я привел пример задачи, для которой, на мой взгляд, плюсы от его использования с лихвой перекрывают затраты на доработки. В приложенном файле есть пример заполнения регистра - дополнительной таблицы, эту функцию несложно доработать при встраивании в типовую конфигурацию.
3. Alxby 451 25.06.19 10:23 Сейчас в теме
Update: Добавлено заполнение информации на основе данных из типовых объектов БСП
Оставьте свое сообщение