При автоматизации загрузки табелей учета рабочего времени из внешних таблиц Excel (например, в транспортных компаниях, где водитель в течение месяца может работать на нескольких автомобилях или совмещать рейсы с ремонтом) разработчики часто сталкиваются с типовой проблемой: в исходном файле на одного сотрудника приходится несколько строк. Если загрузить эти данные напрямую в документ Табель конфигурации «Зарплата и управление персоналом 3.1», программа регистрирует факт выхода на работу по каждой строке независимо. В результате количество дней явки за месяц превышает календарное число дней, а расчет зарплаты становится некорректным.
Разберем причины такого поведения системы, проанализируем архитектурные ограничения типового документа Табель и реализуем правильный алгоритм предобработки и сворачивания данных рабочего времени на встроенном языке 1С.
Проанализируем структуру документа Табель. В типовой конфигурации табличная часть рассчитана строго на одну строку по каждому сотруднику (в разрезе его основного места работы или конкретного трудового договора). Внутренние механизмы документа считают отработанные дни по каждой строке независимо. Если сотрудник Иванов фигурирует в табличной части три раза (например, по трем разным машинам), то отметка «Явка» («Я»), проставленная в один и тот же день в двух строках, будет посчитана системой как два отдельных выхода. В итоге при норме в 22 рабочих дня табель может показать 30 и более дней явки.
Кроме того, действуют следующие жесткие правила конфигурации:
Исходная таблица из Excel обычно представлена «в ширину»: сотрудник и 31 колонка с днями месяца, где каждая ячейка содержит вид времени и часы (день/ночь). Попытка применить метод Свернуть() напрямую к такой горизонтальной таблице значений не дает результата. Поскольку строки водителя различаются (в строке первой машины заполнено 1-е число, во второй строке — 2-е число, а в строке ремонта — 5-е число), строки таблицы остаются уникальными и не схлопываются платформенным методом.
Посмотрим на пример громоздкого обхода через циклы и проверку 124 реквизитов (день, ночь, вид времени на каждый из 31 дня):
// Сложный и медленный алгоритм попарного сравнения колонок
Для Счетчик = 1 По 31 Цикл
ВидДень = "ВидВремениСтрока" + Счетчик;
ЧасДень = "ЧасовСтрока" + Счетчик;
Если ТекущаяСтрока[ВидДень] = СледующаяСтрока[ВидДень] Тогда
ТекущаяСтрока[ЧасДень] = ТекущаяСтрока[ЧасДень] + СледующаяСтрока[ЧасДень];
СледующаяСтрока[ВидДень] = "";
СледующаяСтрока[ЧасДень] = 0;
КонецЕсли;
КонецЦикла;
Такой подход порождает трудночитаемый код, чувствителен к пустым значениям и крайне сложен в отладке. Рассмотрим правильный архитектурный способ решения задачи.
Вместо мучительной работы с десятками колонок приведем данные к вертикальной (нормализованной) структуре. Разберем этот процесс по шагам:
Сотрудник, Дата, ВидВремени, Часы.Свернуть(): стандартная свертка мгновенно суммирует одинаковые виды времени за один и тот же день по одному человеку.Табель с контролем заполнения.Рассмотрим реализацию промежуточной плоской таблицы на встроенном языке:
// 1. Создаем нормализованную плоскую таблицу значений
ТЗ_Нормализованная = Новый ТаблицаЗначений;
ТЗ_Нормализованная.Колонки.Добавить("Сотрудник", Новый ОписаниеТипов("СправочникСсылка.Сотрудники"));
ТЗ_Нормализованная.Колонки.Добавить("ДатаРаботы", Новый ОписаниеТипов("Дата", , Новый КвалификаторыДаты(ЧастиДаты.Дата)));
ТЗ_Нормализованная.Колонки.Добавить("ВидВремени", Новый ОписаниеТипов("СправочникСсылка.ВидыИспользованияРабочегоВремени"));
ТЗ_Нормализованная.Колонки.Добавить("Часы", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 2, ДопустимыйЗнак.Неотрицательный)));
// 2. В процессе чтения строк Excel добавляем данные построчно (разворачиваем дни в строки)
// Предположим, что мы обходим дни месяца для текущей строки сотрудника:
Для ДеньМесяца = 1 По КоличествоДнейВМесяце Цикл
ТекущаяДатаДня = Дата(Год, Месяц, ДеньМесяца);
// Если есть дневные часы по текущей строке (машине)
Если ЧасыДневные > 0 Тогда
НоваяЗапись = ТЗ_Нормализованная.Добавить();
НоваяЗапись.Сотрудник = СсылкаНаСотрудника;
НоваяЗапись.ДатаРаботы = ТекущаяДатаДня;
НоваяЗапись.ВидВремени = ВидВремениДневной; // Например, Явка или Ремонт
НоваяЗапись.Часы = ЧасыДневные;
КонецЕсли;
// Если есть ночные часы
Если ЧасыНочные > 0 Тогда
НоваяЗапись = ТЗ_Нормализованная.Добавить();
НоваяЗапись.Сотрудник = СсылкаНаСотрудника;
НоваяЗапись.ДатаРаботы = ТекущаяДатаДня;
НоваяЗапись.ВидВремени = Справочники.ВидыИспользованияРабочегоВремени.НочныеЧасы;
НоваяЗапись.Часы = ЧасыНочные;
КонецЕсли;
КонецЦикла;
// 3. Выполняем типовую свертку
// Одинаковые виды времени за один день сложатся, разные (Явка и Ремонт) останутся отдельными строками
ТЗ_Нормализованная.Свернуть("Сотрудник, ДатаРаботы, ВидВремени", "Часы");
Обратим внимание на ключевое преимущество: если водитель отработал 4 часа на первой машине («Я») и 6 часов на второй машине («Я»), метод Свернуть("Сотрудник, ДатаРаботы, ВидВремени", "Часы") превратит эти две записи в одну: «Я» — 10 часов. Если же в этот день был еще и ремонт на 3 часа («Р»), то строка ремонта останется отдельной записью за эту дату, так как у нее отличается значение в колонке ВидВремени.
После свертки плоской таблицы необходимо перенести показатели в табличную часть документа, сгруппировав их так, чтобы на одного сотрудника приходилась ровно одна строка документа.
// Сортируем свернутые данные для последовательного обхода
ТЗ_Нормализованная.Сортировать("Сотрудник Возр, ДатаРаботы Возр");
ТекущийСотрудник = Неопределено;
СтрокаТабеля = Неопределено;
Для Каждого Запись Из ТЗ_Нормализованная Цикл
Если Запись.Сотрудник <> ТекущийСотрудник Тогда
// Создаем строго одну строку табеля на сотрудника
ТекущийСотрудник = Запись.Сотрудник;
СтрокаТабеля = ДокументОбъект.ВременнаяТабличнаяЧасть.Добавить();
СтрокаТабеля.Сотрудник = ТекущийСотрудник;
КонецЕсли;
День = День(Запись.ДатаРаботы);
// Заполняем свободный слот вида времени для конкретного дня
// Например, если первый слот свободен — пишем в него, если занят другим видом времени — во второй
Если Не ЗначениеЗаполнено(СтрокаТабеля["ВидВремени" + День + "_1"]) Тогда
СтрокаТабеля["ВидВремени" + День + "_1"] = Запись.ВидВремени;
СтрокаТабеля["Часов" + День + "_1"] = Запись.Часы;
ИначеЕсли Не ЗначениеЗаполнено(СтрокаТабеля["ВидВремени" + День + "_2"]) Тогда
СтрокаТабеля["ВидВремени" + День + "_2"] = Запись.ВидВремени;
СтрокаТабеля["Часов" + День + "_2"] = Запись.Часы;
КонецЕсли;
КонецЦикла;
Чтобы документ Табель с несколькими видами времени за одни сутки корректно провелся, проверим настройки базы данных:
Проанализируем методическую сторону задачи: если учет в разрезе конкретных машин или ремонта необходим бухгалтерии исключительно для расчета сдельного заработка или начисления доплат по разным часовым ставкам, перегружать типовой табель не рекомендуется. Документ Табель предназначен в первую очередь для учета нормы и отклонений от нормы времени.
В ЗУП 3.1 для распределения часов по объектам (машинам, нарядам) эффективнее настроить шаблон документа Данные для расчета зарплаты. В него загружаются фактически отработанные часы с указанием конкретного гаражного номера автомобиля через дополнительный реквизит, а сам табель формируется автоматически типовым способом по основному графику и фактически отработанному времени.