При разработке и доработке зарплатных или кадровых блоков часто возникает необходимость рассчитать количество праздничных дней, которые выпадают на период отпуска сотрудника. Рассмотрим типичную ситуацию: у нас есть табличная часть документа с периодами отпуска (даты начала и окончания), и нам нужно из общего количества дней вычесть праздники. Разберем по шагам, почему интуитивный подход со вложенным запросом не работает в 1С, и проанализируем правильные способы решения этой задачи.
Многие разработчики, знакомые с классическим SQL (например, T-SQL), пытаются написать запрос со вложенной конструкцией прямо в секции выборки полей. Посмотрим на пример кода, который вызывает ошибку в 1С:
ВЫБРАТЬ
РасчетО.Сотрудник КАК Сотрудник,
ПериодыО.НачалоОтпуска КАК НачалоОтпуска,
ПериодыО.КонецОтпуска КАК КонецОтпуска,
(ВЫБРАТЬ КОЛИЧЕСТВО(*)
ИЗ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК К
ГДЕ К.ДатаКалендаря МЕЖДУ ПериодыО.НачалоОтпуска И ПериодыО.КонецОтпуска
И К.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
) КАК КолВоПД
ИЗ
Документ.РасчетОтпускных.ПериодыОтпуска КАК ПериодыО
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасчетОтпускных КАК РасчетО
ПО ПериодыО.Ссылка = РасчетО.Ссылка
Платформа выдаст ошибку или откажется понимать этот синтаксис. Выясним причину: язык запросов 1С не поддерживает использование подзапросов непосредственно в секции ВЫБРАТЬ. Это связано с тем, что платформа должна транслировать единый код на разные СУБД (PostgreSQL, MS SQL Server, Oracle и др.), используя универсальный механизм построения планов запросов. Конструкция вида SELECT t1.a, (SELECT COUNT(*) FROM t2 WHERE t1.id = t2.id) (коррелирующий подзапрос в списке выборки) в 1С недоступна.
Для реализации подобной логики в 1С применяется механизм соединений и группировок. Разберем правильную структуру запроса. Нам нужно соединить таблицу отпусков с производственным календарем и затем сгруппировать результат, чтобы подсчитать количество праздников.
ЛЕВОЕ СОЕДИНЕНИЕ регистр сведений (в старых конфигурациях это РегламентированныйПроизводственныйКалендарь, в новых — ДанныеПроизводственногоКалендаря).ПО) задаем интервал дат и фильтр по виду дня.СГРУППИРОВАТЬ ПО.Посмотрим на пример правильного подхода:
ВЫБРАТЬ
ПериодыО.Сотрудник КАК Сотрудник,
ПериодыО.НачалоОтпуска КАК НачалоОтпуска,
ПериодыО.КонецОтпуска КАК КонецОтпуска,
КОЛИЧЕСТВО(Календарь.ДатаКалендаря) КАК КолВоПраздников
ИЗ
Документ.РасчетОтпускных.ПериодыОтпуска КАК ПериодыО
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
ПО Календарь.ДатаКалендаря МЕЖДУ ПериодыО.НачалоОтпуска И ПериодыО.КонецОтпуска
И Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Праздник)
СГРУППИРОВАТЬ ПО
ПериодыО.Сотрудник,
ПериодыО.НачалоОтпуска,
ПериодыО.КонецОтпуска
Обратите внимание: мы считаем КОЛИЧЕСТВО(Календарь.ДатаКалендаря). Благодаря левому соединению, если праздников в периоде нет, агрегатная функция вернет 0, а не NULL, что избавит от необходимости использовать функцию ЕСТЬNULL() перед дальнейшими математическими операциями.
Не всегда целесообразно изобретать велосипед. В современных и даже старых типовых конфигурациях существуют готовые функции для работы с календарями. Проанализируем ситуацию с точки зрения готовых механизмов.
Например, в конфигурации УПП существует функция для получения дат по рабочему календарю. Хоть она и решает смежную задачу (сдвиг даты на N рабочих дней), рассмотрим ее код, чтобы понять общую логику работы с производственным календарем:
Функция ОпределитьДату(ДатаНач, ЧислоДней) Экспорт
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаНач", ДатаНач);
Запрос.УстановитьПараметр("ЧислоДней", ЧислоДней);
Запрос.УстановитьПараметр("РабочийДень", Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий);
Запрос.УстановитьПараметр("ПредпраздничныйДень", Перечисления.ВидыДнейПроизводственногоКалендаря.Предпраздничный);
Если ЧислоДней > 0 Тогда
Запрос.Текст = "
|ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ " + ЧислоДней + "
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
|ГДЕ РегламентированныйПроизводственныйКалендарь.ДатаКалендаря > &ДатаНач
| И (РегламентированныйПроизводственныйКалендарь.ВидДня = &РабочийДень
| ИЛИ РегламентированныйПроизводственныйКалендарь.ВидДня = &ПредпраздничныйДень)
|УПОРЯДОЧИТЬ ПО
| ДатаКалендаря ВОЗР";
Иначе
// Обработка отрицательного сдвига (в прошлое)
ЧислоДней = -ЧислоДней;
Запрос.Текст = "
|ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ " + ЧислоДней + "
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря
...
|УПОРЯДОЧИТЬ ПО
| ДатаКалендаря УБЫВ";
КонецЕсли;
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Количество() = ЧислоДней Тогда
Пока Выборка.Следующий() Цикл
ТекДата = Выборка.ДатаКалендаря;
КонецЦикла;
Возврат ТекДата;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Для современных систем на базе Библиотеки стандартных подсистем (БСП) лучше искать процедуры в общих модулях, таких как КалендарныеГрафики. Это особенно актуально, потому что архитектура хранения календарей изменилась.
Рассмотрим предметные особенности учета при подсчете праздников. В современных конфигурациях (например, ЗУП 3.1 или ERP) появилась поддержка региональных производственных календарей. Праздники теперь могут зависеть от региона сотрудника (к примеру, в Татарстане или Башкортостане есть свои нерабочие дни). Данные хранятся в регистре ДанныеПроизводственногоКалендаря, который имеет измерение с указанием конкретного календаря.
Также важно помнить статью 120 Трудового кодекса РФ: нерабочие праздничные дни, приходящиеся на период ежегодного основного или дополнительного оплачиваемого отпуска, в число календарных дней отпуска не включаются. Однако обычные выходные дни (суббота и воскресенье) из отпуска вычитать не нужно! Именно поэтому в условии нашего запроса мы жестко фильтруем виды дней на равенство значению Праздник, игнорируя обычные выходные.