Разработано/протестировано на:
Версия конфигурации: Зарплата и управление персоналом, редакция 3.1 (3.1.11.106)
Версия платформы: 1С: Предприятие 8.3 (8.3.12.1790)
Освещены вопросы:
- Получения отработанного времени по сотруднику;
- Работа с процедурой "ПоказатьВопрос" в обработчике "ПередЗаписью";
- Работа с оповещениями пользователя при помощи БСП 3.0;
Описание задачи.
На предприятии ряду сотрудников аванс выплачивается фиксированной суммой, необходимо учитывать отработанное время за первую половину месяца и корректировать аванс согласно формуле:
([Сумма фиксированного аванса] / [Количество плановых дней по графику сотрудника]) * [Количество отработанных дней]
Если существуют отклонения по отработанному от планового времени, необходимо вывести предупреждение пользователю.
Командировка, как вид времени тоже включен в плановое отработанное время.
Теория.
В ЗУП 3.1 начисление аванса происходит тремя способами:
- Расчетом за первую половину месяца;
- Фиксированной суммой;
- Процентом от тарифа;
При выплате фиксированной суммой – расчет производится без учета отработанного времени. Пользователю необходимо вручную выполнять корректировку суммы аванса.
Задать сумму фиксированного аванса можно в документе «Прием на работу», закладка «Оплата труда». Изменить сумму фиксированного аванса, а так же способ начисления можно в документах: «Кадровый перевод», «Кадровый перевод списком», «Изменение оплаты труда», «Изменение аванса». Более подробно можно прочитать здесь.
По логике работы системы, гораздо правильней для учета отработанного времени использовать вид начисления «Расчетом за первую половину месяца», но исторически сложилось, что сумма выделенная сотруднику на аванс, рассчитывается каждому сотруднику индивидуально и не привязана к плановым начислениям сотрудника. Все изменения будем производить в документе «ВедомостьНаВыплатуЗарплатыВБанк».
Решение задачи.
Этап 1. Определим инструмент решения задачи и взаимодействие с пользователем.
На мой взгляд, для достижения поставленных целей можно воспользоваться следующими методами:
- Внесение изменения непосредственно в конфигурацию;
- Использовать механизм расширений;
- Использовать внешнюю обработку заполнения табличных частей;
Внесение изменения непосредственно в конфигурацию, метод практически сразу отпал поскольку, конфигурация находится на поддержке и обновления в данной ситуации, будут вызывать, если не трудности, то дополнительные затраты времени;
Использовать механизм расширений, метод более гибкий, но к сожалению при наследовании объектов в расширении, придется заимствовать «ФормуДокумента», поскольку в работе необходимо будет совершать интерактивные действия с интерфейсом.
Использовать внешнюю обработку заполнения табличных частей, метод хорош тем, что нет необходимости вносить изменений ни в конфигурацию, ни в расширение, но пользователю, для запуска проверки, необходимо будет совершать дополнительные действия, т.е. нажать на кнопку с вероятностью в 99%, это когда-нибудь будет не сделано.
В данном случае задачу проще решить, используя механизм расширений. Возникает сразу ряд проблем…
Этап 2. Определим дополнительные ограничения на механизм доработки.
Первое что приходит в голову. Это момент проведения документа, поскольку вся проверка на корректность сумм аванса будет, происходит в момент записи/проведения документа, и в случае если будет выдано предупреждение, оно будет выведено в не модальном режиме, документ запишется в не зависимости от того, что ответит пользователь.
Подобные проблемы решаются различными способами, добавлением доп. реквизитов на форму, для хранения текущего флага записи документа и пр.
Но в данном случае лучше всего подойдет использование механизма общих настроек, конечно есть нюанс, связанный с тем что, для получения значений настроек необходимо обращаться к серверу, но поскольку по логике работы вся процедура проверки будет, происходит при записи документа, т.е. вся проверка происходит на сервере, а конечный список предупреждений будет просто передан клиенту, это не сильная нагрузка на систему, и конечно поддерживаются вне контекстные вызовы получение настроек из хранилища.
Схема работы следующая:
- Создаем две общих настройки: ЗаписьДокументаПриНаличииОшибокПлановогоВремени и НеобходимаПроверкаПлановогоВремени, все они будут иметь тип Булево.
- При создании документа на сервере, устанавливаем следующие значения настроек
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "ЗаписьДокументаПриНаличииОшибокПлановогоВремени", Ложь);
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "НеобходимаПроверкаПлановогоВремени", Истина);
- Создаем процедуру возвращающую результат сравнения двух настроек:
ЗаписьДокументаПриНаличииОшибокПлановогоВремени ИЛИ НеобходимаПроверкаПлановогоВремени
Она будет возвращать значение в переменную Отказ, метода «ПередЗаписью» формы документа, две настройки и сравнение на ИЛИ, необходимы для тех случаев, если в документе не выбрана формы выплаты Аванс или если, к примеру, пользователь получил предупреждение о несоответствии сумм, поправил несколько строк и решил провести документ как есть.
- Переопределяем процедуру модуля «ПередЗаписью», в ней мы проверяем, необходима ли проверка на плановое время через функцию описанную выше. Если проверка необходима, то выполняем функции сверки.
Этап 3. Основные задачи по получению и обработки данных.
Первое что придется решить, это получение планового и отработанного времени сотрудника. Как всегда нюанс). Ведется ли табельный учет через документ «ТабельУчетаРабочегоВремени» или учет ведется методом отклонений от рабочего времени. В зависимости от методов ведения учета придется обращаться к разным таблицам системы. В рассматриваемой задаче учет ведется методом отклонений.
Для решения будем пользоваться методами общего модуля «УчетРабочегоВремениРасширенный», заполняем параметры и временные таблицы запроса используемого для получения отработанного и планового времени, но данный метод возвращает временную таблицу в целом по периоду, и по дням, соответственно к полученным таблицам необходимо применить сворачивания и отборы.
Второе и более тривиальное это интерактивная работа с пользователем вывод сообщений об ошибках и проведение/запись документа.
Для получения обратной связи от пользователя будем использовать функцию ПоказатьВопрос с обработкой оповещения.
Для вывода ошибок воспользуемся методами БСП 3.0, из общего модуля «ОбщегоНазначенияКлиентСервер», «ДобавитьОшибкуПользователю», «СообщитьОшибкиПользователю». Весь синтаксис методов далее будет понятен по коду…
Код, код, код…
Процедуры и функции постарался расположить в порядке вызова и логики работы.
Заимствуем в расширение форму документа «ВедомостьНаВыплатуЗарплатыВБанк» и приступаем… Далее по тексту распишу только основные функции для сокращения статьи, более подробно можно просмотреть в расширении.
Переопределим процедуру формы документа «ПриСозданииНаСервере» и будем выполнять ее перед вызовом аналогичной процедуры конфигурации:
&НаСервере
Процедура Расш1_ПриСозданииНаСервереПеред(Отказ, СтандартнаяОбработка)
//установим первоначальные значения
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "ЗаписьДокументаПриНаличииОшибокПлановогоВремени", Ложь);
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "НеобходимаПроверкаПлановогоВремени", Истина);
КонецПроцедуры
Затем переопределяем процедуру «ПередЗаписью» и будем выполнять ее перед основной:
&НаКлиенте
Процедура Расш1_ПередЗаписьюПеред(Отказ, ПараметрыЗаписи)
Если ПолучитьЗначениеЗаписиДокумента() Тогда
ПроверитьОплатуПоНормамВремениСотрудниковНаКлиенте();
Отказ = ПолучитьЗначениеЗаписиДокумента();
КонецЕсли;
КонецПроцедуры
Распишем функцию ПолучитьЗначениеЗаписиДокумента:
&НаСервереБезКонтекста
Функция ПолучитьЗначениеЗаписиДокумента()
//прочитаем значения
ЗаписьДокументаПриНаличииОшибокПлановогоВремени = ХранилищеОбщихНастроек.Загрузить("Расш1_ПроверкаПлановогоВремени", "ЗаписьДокументаПриНаличииОшибокПлановогоВремени");
НеобходимаПроверкаПлановогоВремени = ХранилищеОбщихНастроек.Загрузить("Расш1_ПроверкаПлановогоВремени", "НеобходимаПроверкаПлановогоВремени");
Возврат ЗаписьДокументаПриНаличииОшибокПлановогоВремени ИЛИ НеобходимаПроверкаПлановогоВремени;
КонецФункции // ПолучитьЗначениеЗаписиДокумента()
Здесь все просто получаем значение из хранилища настроек и возвращаем результат сравнения.
Перейдем к основной процедуре. ПроверитьОплатуПоНормамВремениСотрудниковНаКлиенте.
&НаКлиенте
Процедура ПроверитьОплатуПоНормамВремениСотрудниковНаКлиенте()
Если СпособВыплаты.Наименование = "Аванс" Тогда
МассивПредупреждений = ПроверитьНормыРабочегоВремениПоСотрудникам();
Если МассивПредупреждений.Количество() > 0 Тогда
//добавим свои кнопки
СписокКнопок = Новый СписокЗначений;
СписокКнопок.Добавить("ПровестиИЗакрыть", "Провести и закрыть");
СписокКнопок.Добавить("Записать", "Записать");
СписокКнопок.Добавить("Нет", "Нет");
Оповещение = Новый ОписаниеОповещения("ПослеОтветаНаВопрос", ЭтотОбъект);
ПоказатьВопрос(Оповещение, "Обнаружены расхождения в подсчете сумм фиксированного аванса, связанных с отработанным временем. Записать документ?", СписокКнопок,
0, , "Обратите внимание!");
Ошибки = Неопределено;
Для Каждого СтрокаПредупреждение Из МассивПредупреждений Цикл
//найдем индекс строкис ошибкой
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("ИдентификаторСтроки", СтрокаПредупреждение.ИдентификаторСтроки);
НайденныеСтрокиСостава = Объект.Состав.НайтиСтроки(ПараметрыОтбора);
Для Каждого НайденнаяСтрока Из НайденныеСтрокиСостава Цикл
ФизЛицо = НайденнаяСтрока.ФизическоеЛицо.Наименование + ". ";
СтрокаОтработано = "Отраб. Д: " + СтрокаПредупреждение.ОтработаноДней + ", Ч: " + СтрокаПредупреждение.ОтработаноЧасов + ". ";
СтрокаПлан = "План. Д: " + СтрокаПредупреждение.НормаПоГрафикуДней + ", Ч: " + СтрокаПредупреждение.НормаПоГрафикуЧасов + ". ";
СтрокаФормулаРасчета = "(" + СтрокаПредупреждение.Аванс + " / " + СтрокаПредупреждение.НормаПоГрафикуДней + ") * " + СтрокаПредупреждение.ОтработаноДней + " = " + Окр(СтрокаПредупреждение.РасчетнаяСуммаАванса,2);
СтрокаОписанияОшибки = Физлицо + СтрокаОтработано + СтрокаПлан + СтрокаФормулаРасчета;
ОбщегоНазначенияКлиентСервер.ДобавитьОшибкуПользователю(Ошибки, "Объект.Состав[%1].КВыплатеСумма", СтрокаОписанияОшибки,, НайденнаяСтрока.НомерСтроки - 1);
КонецЦикла;
КонецЦикла;
Иначе
УстановитьЗначениеЗаписиДокумента(Ложь, Ложь);
КонецЕсли;
ОбщегоНазначенияКлиентСервер.СообщитьОшибкиПользователю(Ошибки);
Иначе
УстановитьЗначениеЗаписиДокумента(Ложь, Ложь);
КонецЕсли;
КонецПроцедуры // ПроверитьОплатуПоНормамВремениСотрудниковНаКлиенте()
Кратко поясним. Процедура проверяет выбран ли вид оплаты «Аванс», если выбран то вызывает функцию ПроверитьНормыРабочегоВремениПоСотрудникам, которая возвращает массив предупреждений, если массив предупреждений не пуст, то все ошибки выводятся на форму а пользователю интерактивно предлагается действие.
Сама процедура ПроверитьНормыРабочегоВремениПоСотрудникам.
&НаСервере
Функция ПроверитьНормыРабочегоВремениПоСотрудникам()
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "НеобходимаПроверкаПлановогоВремени", Ложь);
//получим таблицу значений с индентификатором строки и сотрудников
ДокОбъект = РеквизитФормыВЗначение("Объект");
ТаблицаСотрудников = ДокОбъект.Зарплата.Выгрузить(,"ИдентификаторСтроки, Сотрудник, КВыплате");
//Добавим к таблице дополнительные колонки по авансу
ТаблицаСотрудников.Колонки.Добавить("СпособРасчетаАванса", Новый ОписаниеТипов("ПеречислениеСсылка.СпособыРасчетаАванса"));
ТаблицаСотрудников.Колонки.Добавить("Аванс", Новый ОписаниеТипов("Число"));
ДополнитьТаблицуПлановымиАвансами(ТаблицаСотрудников);
ОтобратьТаблицуПоОтбору(ТаблицаСотрудников, ВидСравнения.Равно, "СпособРасчетаАванса", Перечисления.СпособыРасчетаАванса.ФиксированнойСуммой);
//Выборка по рабочего времени за весь период
ДатаНачала = НачалоМесяца(Объект.Дата);
ДатаОкончания = КонецМесяца(Объект.Дата);
МассивСотрудников = ТаблицаСотрудников.ВыгрузитьКолонку("Сотрудник");
МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
СоздатьВТСотрудники(МенеджерВременныхТаблиц, МассивСотрудников, ДатаНачала);
ПараметрыПолученияДанных = УчетРабочегоВремениРасширенный.ПараметрыДляЗапросВТДанныеУчетаВремениИСостоянийСотрудников();
ПараметрыПолученияДанных.ДатаНачала = ДатаНачала;
ПараметрыПолученияДанных.ДатаОкончания = КонецМесяца(ДатаНачала);
ПараметрыПолученияДанных.МесяцДатаНачала = ДатаНачала;
ПараметрыПолученияДанных.МесяцДатаОкончания = КонецМесяца(ДатаНачала);
ПараметрыПолученияДанных.ДатаАктуальности = ТекущаяДата();
УчетРабочегоВремениРасширенный.СоздатьВТДанныеУчетаВремениИСостоянийСотрудников(МенеджерВременныхТаблиц, Истина, ПараметрыПолученияДанных);
ДанныеТабеляТ13 = МенеджерВременныхТаблиц.Таблицы.Найти("ВТДанныеУчетаВремениИСостоянийСотрудников").ПолучитьДанные().Выгрузить();
ДанныеТабеляТ13.Сортировать("Дата");
ОтобратьТаблицуПоОтбору(ДанныеТабеляТ13, ВидСравнения.МеньшеИлиРавно, "Дата", ДатаНачала +(86400*14));
ДанныеТабеляТ13.Свернуть("Сотрудник, ВидУчетаВремени", "Дни, Часы");
ОтобратьТаблицуПоОтбору(ДанныеТабеляТ13, ВидСравнения.ВСписке, "ВидУчетаВремени", ПолучитьРабочиеВидыВремени());
//получим общее количество отработанных часов
ДанныеТабеляТ13.Свернуть("Сотрудник", "Дни, Часы");
НормаПоГрафику = МенеджерВременныхТаблиц.Таблицы.Найти("ВТНормаВремени").ПолучитьДанные().Выгрузить();
ОтобратьТаблицуПоОтбору(НормаПоГрафику, ВидСравнения.МеньшеИлиРавно, "Дата", ДатаНачала +(86400*14));
//ОтобратьДанныеПоПериоду(НормаПоГрафику, ДатаНачала);
НормаПоГрафику.Свернуть("Сотрудник","ДниНорма, ЧасыНорма");
ИтоговаяТаблица = ПолучитьОбщуюТаблицуСОтработаннымИНормированнымВременем(ДанныеТабеляТ13, НормаПоГрафику, ТаблицаСотрудников);
МассивПредупреждений = Новый Массив;
Для Каждого СтрокаПредупреждение Из ИтоговаяТаблица Цикл
СтруктураПредупреждения = Новый Структура;
СтруктураПредупреждения.Вставить("Сотрудник", СтрокаПредупреждение.Сотрудник);
СтруктураПредупреждения.Вставить("ОтработаноДней", СтрокаПредупреждение.ОтработаноДней);
СтруктураПредупреждения.Вставить("ОтработаноЧасов", СтрокаПредупреждение.ОтработаноЧасов);
СтруктураПредупреждения.Вставить("НормаПоГрафикуДней", СтрокаПредупреждение.НормаПоГрафикуДней);
СтруктураПредупреждения.Вставить("НормаПоГрафикуЧасов", СтрокаПредупреждение.НормаПоГрафикуЧасов);
СтруктураПредупреждения.Вставить("ИдентификаторСтроки", СтрокаПредупреждение.ИдентификаторСтроки);
СтруктураПредупреждения.Вставить("КВыплате", СтрокаПредупреждение.КВыплате);
СтруктураПредупреждения.Вставить("СпособРасчетаАванса", СтрокаПредупреждение.СпособРасчетаАванса);
СтруктураПредупреждения.Вставить("Аванс", СтрокаПредупреждение.Аванс);
РасчетнаяСуммаАванса = (СтрокаПредупреждение.Аванс / СтрокаПредупреждение.НормаПоГрафикуДней) * СтрокаПредупреждение.ОтработаноДней;
СтруктураПредупреждения.Вставить("РасчетнаяСуммаАванса", РасчетнаяСуммаАванса);
Если Цел(РасчетнаяСуммаАванса) <> Цел(СтрокаПредупреждение.КВыплате) Тогда
ХранилищеОбщихНастроек.Сохранить("Расш1_ПроверкаПлановогоВремени", "НеобходимаПроверкаПлановогоВремени", Истина);
МассивПредупреждений.Добавить(СтруктураПредупреждения);
КонецЕсли;
КонецЦикла;
Возврат МассивПредупреждений;
КонецФункции // ПроверитьНормыРабочегоВремениПоСотрудникам()
Так же дополнительно процедура для заполнения временной таблицы сотрудников. СоздатьВТСотрудники.
&НаСервереБезКонтекста
Процедура СоздатьВТСотрудники(МенеджерВременныхТаблиц, МассивСотрудников, Месяц, ДатаАктуальности = Неопределено, ДатаНачала = Неопределено, ДатаОкончания = Неопределено)
ТаблицаСотрудники = Новый ТаблицаЗначений;
ТаблицаСотрудники.Колонки.Добавить("Сотрудник", Новый ОписаниеТипов("СправочникСсылка.Сотрудники"));
ТаблицаСотрудники.Колонки.Добавить("Месяц", Новый ОписаниеТипов("Дата"));
ТаблицаСотрудники.Колонки.Добавить("ДатаАктуальности", Новый ОписаниеТипов("Дата"));
ТаблицаСотрудники.Колонки.Добавить("ДатаНачала", Новый ОписаниеТипов("Дата"));
ТаблицаСотрудники.Колонки.Добавить("ДатаОкончания", Новый ОписаниеТипов("Дата"));
Для Каждого Сотрудник Из МассивСотрудников Цикл
СтрокаСотрудники = ТаблицаСотрудники.Добавить();
СтрокаСотрудники.Сотрудник = Сотрудник;
КонецЦикла;
ТаблицаСотрудники.ЗаполнитьЗначения(Месяц, "Месяц");
ТаблицаСотрудники.ЗаполнитьЗначения(?(ДатаАктуальности = Неопределено, ТекущаяДата(), ДатаАктуальности), "ДатаАктуальности");
ТаблицаСотрудники.ЗаполнитьЗначения(?(ДатаНачала = Неопределено, Месяц, ДатаНачала), "ДатаНачала");
ТаблицаСотрудники.ЗаполнитьЗначения(?(ДатаОкончания = Неопределено, КонецМесяца(Месяц), ДатаОкончания), "ДатаОкончания");
Запрос = Новый Запрос(
"ВЫБРАТЬ
| ТаблицаСотрудники.Сотрудник,
| ТаблицаСотрудники.Месяц,
| ТаблицаСотрудники.ДатаАктуальности,
| ТаблицаСотрудники.ДатаНачала,
| ТаблицаСотрудники.ДатаОкончания
|ПОМЕСТИТЬ ВТСотрудники
|ИЗ
| &ТаблицаСотрудники КАК ТаблицаСотрудники");
Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("ТаблицаСотрудники", ТаблицаСотрудники);
Запрос.Выполнить();
КонецПроцедуры
Кратко поясним. Механизм работы.
Получаем таблицу сотрудников из документа, нам будет необходим Сотрудник, ИдентификаторСтроки для подвязки предупреждения к строке табличной части с сотрудниками и КВыплате в ней содержится сумма аванса. Для дополнительного отбора по сотрудникам, чей аванс указан фиксированной суммой, дополним таблицу двумя колонками СпособРасчетаАванса и Аванс (сумма назначенная сотруднику в системе), заполняем данные колонки запросом к регистру сведений «ПлановыеАвансы».
Оставляем в таблице только тех сотрудников у которых аванс рассчитывается фиксированной суммой.
Получаем период аванса, в данном случае берем дату документа и получаем начало и конец месяца. Передаем список сотрудников и периоды отбора в запрос общего модуля УчетРабочегоВремениРасширенный, выгружаем две временные таблицы: ВТДанныеУчетаВремениИСостоянийСотрудников и ВТНормаВремени, в них хранятся данные по отработанному и по нормам времени на сотрудника.
Далее выполняем отбор по датам только за первую половину месяца т.е. по 15 число. Сворачиваем таблицы по сотрудникам и суммируем отработанные дни и часы.
При помощи запроса формируем одну общую таблицу с сотрудниками отработанным/нормативным временем и фактической суммой выплаты и выплаты зарегистрированной в системе как плановой, если присутствуют расхождения устанавливаем флаг в общих настройках о том что необходима проверка планового времени, данные из таблицы передаем в массив структур и возвращаем на клиент.
На клиенте фомируем список ошибок пользователей и показываем их на форме. Далее обрабатываем результат ответа пользователя либо проводим, как есть либо отказываемся от записи, и он корректирует результат.
Статья не претендует на универсальное решение проблемы. Но надеюсь поможет.
----
Мой блог с заметками здесь.
Написать мне telegram можно здесь.