ZKTeco и 1С: загрузка отметок учёта рабочего времени

Дата: 08.10.2026 17:54:25
ZKTeco
zkemkeeper
СКУД
Учет рабочего времени
COM-объект
Регистр сведений
Регламентное задание
DllSurrogate
Комментарий:
0
Платформа:
8.3
Конфигурация:
Любая
Формы:
Управляемые

ZKTeco и 1С связывают через SDK zkemkeeper — COM-библиотеку ZKTeco для разработчиков. Через неё 1С читает с биометрического терминала (отпечаток, лицо, карта) журнал отметок, пользователей и время, и покупной модуль не нужен. Ниже — регистрация SDK из практики, обход 32-битности на 64-битном сервере 1С, общий модуль чтения терминала, свой регистр сведений, загрузка по расписанию и запрос «опоздания / отработано».

Где встречается задача

  • Терминал учёта рабочего времени стоит на проходной, отметки смотрят в программе производителя, а табель и премии считают в 1С.
  • Нужен учёт рабочего времени в 1С по терминалу: опоздания и часы без ручного переноса.
  • Код уже написан, но на сервере Новый COMОбъект("zkemkeeper.ZKEM") падает с «Класс не зарегистрирован».
  • Терминалов несколько, и данные СКУД в 1С нужны каждые 10–15 минут.

ZKTeco и 1С: где создать COM-объект SDK

zkemkeeper.dll — 32-битная внутрипроцессная COM-библиотека: у сборок 6.2.5.5 и 6.3.1.38 из комплекта автора заголовок PE — x86. Загрузить её может только 32-битный процесс, поэтому схема зависит от того, кто создаёт объект.

ОкружениеГде работает SDKРасписание
Файловая база, 32-битный клиентВезде: серверный код файловой базы выполняется в процессе клиентаСеанс 32-битного клиента
32-битный сервер 1СНа сервереРегламентное задание
64-битный сервер 1СТолько в клиентском коде 32-битного клиентаКлиент, запущенный Планировщиком Windows
64-битный сервер + DllSurrogateНа сервере: объект живёт в 32-битном dllhost.exeРегламентное задание
Прослойка32-битная служба (C#, Python) отдаёт отметки файлом или в HTTP-сервисСвоё у службы

32-битный тонкий клиент запускают из C:\Program Files (x86)\1cv8\<версия>\bin\1cv8c.exe или в окне запуска задают в свойствах базы разрядность «32 (x86)» (параметр AppArch в ibases.v8i). В веб-клиенте COMОбъект по справке поддерживается только в Internet Explorer — этот вариант не рассматриваем. Прослойку стоит писать, если реестр сервера трогать нельзя, а держать открытый клиент негде.

Регистрация zkemkeeper в SysWOW64 и в своей папке

Из практики автора: SDK лежит в отдельной папке и регистрируется 32-битным regsvr32 — на 64-битной Windows это копия из SysWOW64, а не из System32. Сначала проверьте, не зарегистрировала ли библиотеку другая программа ZKTeco: новая регистрация перепишет путь к DLL.

:: Регистрация zkemkeeper на 64-битной Windows. Командная строка от имени администратора.
:: 1. Нет ли уже регистрации от другой программы ZKTeco: CLSID и путь к её DLL
reg query "HKCR\zkemkeeper.ZKEM\CLSID"
reg query "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{00853A19-BD51-419B-9269-2DABE57EB61F}\InprocServer32"

:: 2. Если есть - копия ветки: новая регистрация перепишет путь, вернуть можно через reg import
reg export "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{00853A19-BD51-419B-9269-2DABE57EB61F}" C:\zkemkeeper\clsid-backup.reg

:: 3. Из практики: 32-битный regsvr32, в C:\zkemkeeper - весь комплект DLL из SDK
C:\Windows\SysWOW64\regsvr32 "C:\zkemkeeper\zkemkeeper.dll"

:: Отмена регистрации - тем же regsvr32:
:: C:\Windows\SysWOW64\regsvr32 /u "C:\zkemkeeper\zkemkeeper.dll"
  • Регистрируют на каждом компьютере, где создаётся объект: на сервере 1С для серверной схемы, на рабочих местах — для клиентской.
  • Командная строка — от имени администратора, иначе DllRegisterServer завершится ошибкой 0x80070005 или 0x8002801c. Сообщение regsvr32 «Возможно, модуль … не совместим с версией Windows» обычно значит, что разрядность DLL и regsvr32 не совпала.
  • В папке нужен весь комплект: zkemkeeper.dll импортирует zkemsdk.dll и plcommpro.dll, а zkemsdk.dll во время работы подгружает tcpcomm.dll, usbcomm.dll и другие. Путь к DLL записан в реестре — папку после регистрации не переносят.
  • Скрипты SDK (Register_SDK.bat, SDKinstall32.bat) на 64-битной Windows копируют *.dll в %windir%\SysWOW64 и регистрируют оттуда, второй — с ключом /s, без окон: о сбое вы не узнаете. copy в bat-файле перезаписывает одноимённые файлы без вопроса, и DLL другой версии от программы ZKTeco будут заменены. Своя папка безопаснее. Но если объект создаётся, а Connect_Net при живой сети возвращает Ложь, проверьте, находит ли процесс остальные DLL комплекта.

Если сервер 1С 64-битный: DllSurrogate

Чтобы регламентное задание на 64-битном сервере работало с SDK, zkemkeeper запускают вне процесса, в системном суррогате dllhost.exe. По документации Microsoft для этого у AppID класса задают значение DllSurrogate; пустая строка — системный суррогат. Ветка CLSID 32-битной регистрации лежит в WOW6432Node, ветка AppID общая для 32 и 64 бит. CLSID zkemkeeper.ZKEM в обеих сборках у автора — {00853A19-BD51-419B-9269-2DABE57EB61F}, у своей сверьте первой командой предыдущего листинга.

:: DllSurrogate для 64-битного сервера 1С. Командная строка от имени администратора на сервере.
set CLSID={00853A19-BD51-419B-9269-2DABE57EB61F}

:: 1. Копия ветки класса до правки
reg export "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" C:\zkemkeeper\clsid-before-surrogate.reg

:: 2. Есть ли у класса AppID: у zkemkeeper он может быть уже прописан
reg query "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID

:: 3. AppID есть - подставьте его значение из шага 2:
set APPID={GUID-из-шага-2}
:: AppID нет - назначьте классу его же GUID (вместо строки выше):
:: reg add "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID /t REG_SZ /d %CLSID%
:: set APPID=%CLSID%

:: 4. Пустой DllSurrogate - системный суррогат dllhost.exe. Ветка AppID общая для 32 и 64 бит
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /t REG_SZ /d ""

:: Откат: reg delete "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /f
:: и, если AppID назначали сами: reg delete "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID /f

Запускать COM-серверы по умолчанию могут только Administrators, SYSTEM и INTERACTIVE, а служба 1С обычно работает от своей учётной записи, например usr1cv8, — без разрешения будет «Отказано в доступе» (0x80070005). В dcomcnfg откройте «Службы компонентов → Компьютеры → Мой компьютер → Настройка DCOM», найдите приложение (zkemkeeper или GUID из AppID) и в «Свойства → Безопасность → Разрешения на запуск и активацию → Настроить → Изменить…» дайте учётной записи службы «Локальный запуск» и «Локальная активация». Вызовы методов теперь идут между процессами и работают медленнее.

Регистры для отметок

ОтметкиСКУД — периодичность «В пределах секунды», режим записи «Независимый». Ключ записи — время, терминал и сотрудник, поэтому повторная загрузка дублей не даёт.

ПолеВидТипЧто хранит
Период—Дата и времяВремя отметки по часам терминала
ТерминалИзмерениеСтрока (30)Серийный номер терминала
КодСотрудникаИзмерениеСтрока (24)ID на терминале, по руководству SDK — до 24 цифр
СпособПроверки, СтатусРесурсыЧисло (5, 0)Способ проверки (0 — пароль, 1 — отпечаток, 2 — карта) и статус, выбранный на терминале
РабочийКодРесурсЧисло (10, 0)Рабочий код (work code)
ЗагруженоРеквизитДата и времяКогда отметка попала в 1С

СотрудникиСКУД — непериодический, независимый: измерение КодСотрудника (Строка 24), ресурсы ИмяНаТерминале (Строка 50) и Сотрудник — СправочникСсылка.Сотрудники в КА 2.5 или СправочникСсылка.ФизическиеЛица в УТ 11. Регистры можно добавить расширением, не снимая типовую с поддержки. Про виды регистров — в статье о регистре сведений.

Загрузка отметок с терминала в 1С: общий модуль

Модуль работает там, где окажется COM-объект. ЗагрузитьОтметки проходит весь цикл: подключение, часы, пользователи, отметки, запись в 1С и по флажку очистка журнала.

// Общий модуль ТерминалZKTeco. Флажки «Клиент (управляемое приложение)» и «Сервер»:
// код работает там, где создан COM-объект, - в 32-битном клиенте или на сервере
// (32-битный сервер 1С, DllSurrogate). Методы SDK - по ZKTeco Standalone SDK Development Manual.

// Полный цикл с одним терминалом: часы, пользователи, отметки, по желанию очистка журнала.
// В базу пишет серверный модуль ОтметкиСКУДСервер. Возвращает итог строкой.
Функция ЗагрузитьОтметки(Знач АдресIP, Знач Очистить = Ложь) Экспорт

	Терминал = Подключиться(АдресIP);
	Попытка
		Серийный = СерийныйНомер(Терминал);
		Расхождение = СинхронизироватьВремя(Терминал, ОтметкиСКУДСервер.ВремяСервера());
		ОтметкиСКУДСервер.ЗаписатьСотрудников(ПрочитатьСотрудников(Терминал));
		Если Очистить Тогда
			// Выключенный терминал не примет отметку, которую очистка удалила бы незагруженной
			Если Не Терминал.EnableDevice(НомерТерминала(), Ложь) Тогда
				ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Выключение терминала'"));
			КонецЕсли;
			Отметки = ПрочитатьОтметки(Терминал);
		Иначе
			Отметки = ПрочитатьОтметки(Терминал, ОтметкиСКУДСервер.ЧитатьНачинаяС(Серийный));
		КонецЕсли;
		Новых = ОтметкиСКУДСервер.ЗаписатьОтметки(Отметки, Серийный);
		Если Очистить Тогда
			ОчиститьЖурнал(Терминал); // всё прочитанное уже в регистре
			Терминал.EnableDevice(НомерТерминала(), Истина);
		КонецЕсли;
	Исключение
		Отключиться(Терминал, Очистить);
		ВызватьИсключение;
	КонецПопытки;
	Отключиться(Терминал);

	Возврат СтрШаблон(НСтр("ru = '%1 (%2): прочитано %3, новых %4, часы расходились на %5 с'"),
		АдресIP, Серийный, Отметки.Количество(), Новых, Расхождение);

КонецФункции

// Подключается к терминалу по сети. Возвращает COM-объект SDK или вызывает исключение.
Функция Подключиться(Знач АдресIP, Знач Порт = 4370) Экспорт

	Попытка
		Терминал = Новый COMОбъект("zkemkeeper.ZKEM");
	Исключение
		// В 64-битном процессе без DllSurrogate здесь будет «Класс не зарегистрирован»
		ВызватьИсключение СтрШаблон(НСтр("ru = 'Не создан объект zkemkeeper.ZKEM: %1'"),
			КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
	КонецПопытки;

	Если Не Терминал.Connect_Net(АдресIP, Порт) Тогда
		ВызватьОшибкуSDK(Терминал, СтрШаблон(НСтр("ru = 'Подключение к %1:%2'"),
			АдресIP, Формат(Порт, "ЧГ=0")), Истина);
	КонецЕсли;
	Возврат Терминал;

КонецФункции

// Закрывает соединение; ВключитьОбратно - если терминал выключали для очистки.
// Ошибки не выпускает: вызывается и из обработчика исключения, где важна исходная ошибка.
Процедура Отключиться(Терминал, Знач ВключитьОбратно = Ложь) Экспорт

	Попытка
		Если ВключитьОбратно Тогда
			Терминал.EnableDevice(НомерТерминала(), Истина);
		КонецЕсли;
		Терминал.Disconnect();
	Исключение
		// связь уже потеряна
	КонецПопытки;

КонецПроцедуры

// Серийный номер - постоянный ключ терминала в регистре: IP-адрес может смениться.
Функция СерийныйНомер(Терминал) Экспорт

	Номер = "";
	Если Не Терминал.GetSerialNumber(НомерТерминала(), Номер) Тогда
		ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Чтение серийного номера'"));
	КонецЕсли;
	Возврат СокрЛП(Номер);

КонецФункции

// Отметки не раньше НачинаяС: массив структур, его можно передать с клиента на сервер.
Функция ПрочитатьОтметки(Терминал, Знач НачинаяС = '00010101') Экспорт

	Отметки = Новый Массив;
	// Журнал целиком копируется в буфер SDK; Ложь с кодом 0 - журнал пуст
	Если Не ДанныеПрочитаны(Терминал, Терминал.ReadGeneralLogData(НомерТерминала()),
			НСтр("ru = 'Чтение журнала отметок'")) Тогда
		Возврат Отметки;
	КонецЕсли;

	// Выходные параметры: переменные заранее получают значения нужного типа
	КодСотрудника = "";
	СпособПроверки = 0;
	Статус = 0;
	РабочийКод = 0;
	ГодОтметки = 0; МесяцОтметки = 0; ДеньОтметки = 0;
	ЧасОтметки = 0; МинутаОтметки = 0; СекундаОтметки = 0;

	// Каждый вызов отдаёт следующую запись буфера, после последней - Ложь
	Пока Терминал.SSR_GetGeneralLogData(НомерТерминала(), КодСотрудника, СпособПроверки, Статус,
			ГодОтметки, МесяцОтметки, ДеньОтметки, ЧасОтметки, МинутаОтметки, СекундаОтметки, РабочийКод) Цикл

		Если ГодОтметки < 2000 Тогда
			Продолжить; // защита от записи с пустой датой: Дата() на ней упадёт
		КонецЕсли;
		Период = Дата(ГодОтметки, МесяцОтметки, ДеньОтметки, ЧасОтметки, МинутаОтметки, СекундаОтметки);
		Если Период >= НачинаяС Тогда
			Отметки.Добавить(Новый Структура("Период, КодСотрудника, СпособПроверки, Статус, РабочийКод",
				Период, СокрЛП(КодСотрудника), СпособПроверки, Статус, РабочийКод));
		КонецЕсли;

	КонецЦикла;

	Возврат Отметки;

КонецФункции

// Пользователи терминала: массив структур (КодСотрудника, Имя, Права, Включен).
// Шаблоны отпечатков и лиц метод не читает; пароль с терминала в 1С не переносим.
Функция ПрочитатьСотрудников(Терминал) Экспорт

	Сотрудники = Новый Массив;
	Если Не ДанныеПрочитаны(Терминал, Терминал.ReadAllUserID(НомерТерминала()),
			НСтр("ru = 'Чтение пользователей терминала'")) Тогда
		Возврат Сотрудники;
	КонецЕсли;

	КодСотрудника = "";
	Имя = "";
	ПарольНаТерминале = "";
	Права = 0; // 0 - пользователь, 1 - регистратор, 2 - администратор, 3 - суперадминистратор
	Включен = Ложь;
	Пока Терминал.SSR_GetAllUserInfo(НомерТерминала(), КодСотрудника, Имя, ПарольНаТерминале, Права, Включен) Цикл
		Сотрудники.Добавить(Новый Структура("КодСотрудника, Имя, Права, Включен",
			СокрЛП(КодСотрудника), СокрЛП(Имя), Права, Включен));
	КонецЦикла;

	Возврат Сотрудники;

КонецФункции

// Ставит на терминале время Эталон, если часы ушли больше чем на Допуск секунд.
// Возвращает расхождение до правки в секундах: плюс - терминал спешил.
Функция СинхронизироватьВремя(Терминал, Знач Эталон, Знач Допуск = 30) Экспорт

	Расхождение = ВремяТерминала(Терминал) - Эталон;
	Если ?(Расхождение < 0, -Расхождение, Расхождение) > Допуск Тогда
		Если Не Терминал.SetDeviceTime2(НомерТерминала(), Год(Эталон), Месяц(Эталон), День(Эталон),
				Час(Эталон), Минута(Эталон), Секунда(Эталон)) Тогда
			ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Установка времени терминала'"));
		КонецЕсли;
	КонецЕсли;
	Возврат Расхождение;

КонецФункции

// Часы терминала как дата 1С.
Функция ВремяТерминала(Терминал) Экспорт

	ГодТерминала = 0; МесяцТерминала = 0; ДеньТерминала = 0;
	ЧасТерминала = 0; МинутаТерминала = 0; СекундаТерминала = 0;
	Если Не Терминал.GetDeviceTime(НомерТерминала(), ГодТерминала, МесяцТерминала, ДеньТерминала,
			ЧасТерминала, МинутаТерминала, СекундаТерминала) Тогда
		ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Чтение времени терминала'"));
	КонецЕсли;
	Возврат Дата(ГодТерминала, МесяцТерминала, ДеньТерминала, ЧасТерминала, МинутаТерминала, СекундаТерминала);

КонецФункции

// Удаляет все отметки на терминале. Только на выключенном терминале
// и только после записи прочитанного в 1С - см. ЗагрузитьОтметки.
Процедура ОчиститьЖурнал(Терминал)

	Если Не Терминал.ClearGLog(НомерТерминала()) Тогда
		ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Очистка журнала отметок'"));
	КонецЕсли;
	Терминал.RefreshData(НомерТерминала());

КонецПроцедуры

// Номер терминала (Device ID) из его настроек связи.
Функция НомерТерминала()
	Возврат 1;
КонецФункции

// Истина - данные в буфере SDK; Ложь - данных нет (код 0); иначе исключение.
Функция ДанныеПрочитаны(Терминал, Знач Успех, Знач Действие)

	Если Успех Тогда
		Возврат Истина;
	КонецЕсли;
	Код = КодОшибкиSDK(Терминал);
	Если Код <> 0 Тогда
		ВызватьОшибкуSDK(Терминал, Действие, Ложь, Код);
	КонецЕсли;
	Возврат Ложь;

КонецФункции

// Код последней ошибки: GetLastError возвращает его в выходном параметре.
Функция КодОшибкиSDK(Терминал)
	Код = 0;
	Терминал.GetLastError(Код);
	Возврат Код;
КонецФункции

Процедура ВызватьОшибкуSDK(Терминал, Знач Действие, Знач ПриПодключении = Ложь, Знач Код = Неопределено)

	Если Код = Неопределено Тогда
		Код = КодОшибкиSDK(Терминал);
	КонецЕсли;
	ВызватьИсключение СтрШаблон(НСтр("ru = '%1: ошибка SDK %2 (%3)'"),
		Действие, Формат(Код, "ЧН=0; ЧГ=0"), ОписаниеОшибкиSDK(Код, ПриПодключении));

КонецПроцедуры

// Расшифровка кодов GetLastError по руководству SDK. При подключении часть кодов значит другое.
Функция ОписаниеОшибкиSDK(Знач Код, Знач ПриПодключении)

	Описания = Новый Соответствие;
	Если ПриПодключении Тогда
		Описания.Вставить(-1, НСтр("ru = 'не удалось вызвать интерфейс'"));
		Описания.Вставить(-2, НСтр("ru = 'ошибка инициализации'"));
		Описания.Вставить(-6, НСтр("ru = 'неверный пароль'"));
		Описания.Вставить(-7, НСтр("ru = 'ошибка ответа'"));
		Описания.Вставить(-8, НСтр("ru = 'истекло время ожидания ответа'"));
		Описания.Вставить(-307, НСтр("ru = 'истекло время подключения'"));
		Описания.Вставить(-12001, НСтр("ru = 'истекло время создания сокета'"));
	Иначе
		Описания.Вставить(-201, НСтр("ru = 'терминал занят'"));
		Описания.Вставить(-100, НСтр("ru = 'не поддерживается или данных нет'"));
		Описания.Вставить(-10, НСтр("ru = 'неверная длина данных'"));
		Описания.Вставить(-4, НСтр("ru = 'не хватает места'"));
		Описания.Вставить(-2, НСтр("ru = 'ошибка чтения или записи файла'"));
		Описания.Вставить(-1, НСтр("ru = 'SDK не инициализирован, нужно переподключиться'"));
		Описания.Вставить(0, НСтр("ru = 'данные не найдены или повторяются'"));
		Описания.Вставить(4, НСтр("ru = 'неверный параметр'"));
	КонецЕсли;
	Описание = Описания.Получить(Код);
	Возврат ?(Описание = Неопределено, НСтр("ru = 'см. GetLastError в руководстве SDK'"), Описание);

КонецФункции

Разбор ключевых строк

  • "zkemkeeper.ZKEM" — ProgID без номера версии. В DLL прописаны zkemkeeper.ZKEM.1 и zkemkeeper.ZKEM.
  • Connect_Net(АдресIP, Порт) — порт по умолчанию 4370. При Ложь код берут из GetLastError; при подключении коды значат другое, поэтому в ОписаниеОшибкиSDK два набора.
  • Выходные параметры. SDK отдаёт данные через параметры-указатели: 1С передаёт переменную по ссылке и после вызова кладёт в неё значение. Поэтому переменные заранее заполнены значениями нужного типа: строкой для BSTR*, числом для LONG*, булевым для VARIANT_BOOL*.
  • ReadGeneralLogData копирует журнал в буфер SDK, SSR_GetGeneralLogData отдаёт по записи, пока не вернёт Ложь. Ложь с кодом 0 у ReadGeneralLogData — «данных нет», а не сбой.
  • SSR_-методы по руководству SDK — для терминалов с цветным экраном. У старых чёрно-белых моделей — GetGeneralLogData: код сотрудника там число, секунд нет.
  • Время отметки идёт с часов терминала, поэтому их сверяют при каждой загрузке: SetDeviceTime2 ставит время сервера 1С, если расхождение больше допуска. SetDeviceTime поставил бы время компьютера, где работает SDK.
  • EnableDevice(..., Ложь) выключает отпечаток, карту и клавиатуру: пока журнал читается, пишется в 1С и очищается, новая отметка не появится и не пропадёт. При ошибке Отключиться включает терминал обратно.

Запись в регистр и загрузка по расписанию

Серверный модуль пишет только новые отметки: прочитанное сравнивается с регистром запросом, а управляемая блокировка по терминалу не даст ручной загрузке и регламентному заданию записать одно и то же дважды. Про наборы записей и менеджер записи — в статье о записи в регистр сведений.

// Общий модуль ОтметкиСКУДСервер. Флажки «Сервер» и «Вызов сервера».

// Пишет в регистр ОтметкиСКУД отметки, которых там ещё нет. Возвращает количество новых.
Функция ЗаписатьОтметки(Знач Отметки, Знач Терминал) Экспорт

	Если Отметки.Количество() = 0 Тогда
		Возврат 0;
	КонецЕсли;

	// Колонкам нужны типы: таблица уходит в запрос параметром
	Таблица = Новый ТаблицаЗначений;
	Таблица.Колонки.Добавить("Период", Новый ОписаниеТипов("Дата", , , Новый КвалификаторыДаты(ЧастиДаты.ДатаВремя)));
	Таблица.Колонки.Добавить("КодСотрудника", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(24)));
	Таблица.Колонки.Добавить("СпособПроверки", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 0)));
	Таблица.Колонки.Добавить("Статус", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 0)));
	Таблица.Колонки.Добавить("РабочийКод", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(10, 0)));
	Для Каждого Отметка Из Отметки Цикл
		ЗаполнитьЗначенияСвойств(Таблица.Добавить(), Отметка);
	КонецЦикла;

	// Только отметки, которых нет в регистре; одна строка на ключ записи
	Запрос = Новый Запрос;
	Запрос.Текст =
	"ВЫБРАТЬ
	|	Т.Период КАК Период,
	|	Т.КодСотрудника КАК КодСотрудника,
	|	Т.СпособПроверки КАК СпособПроверки,
	|	Т.Статус КАК Статус,
	|	Т.РабочийКод КАК РабочийКод
	|ПОМЕСТИТЬ ВТПрочитанные
	|ИЗ
	|	&Таблица КАК Т
	|;
	|
	|////////////////////////////////////////////////////////////////////////////////
	|ВЫБРАТЬ
	|	Прочитанные.Период КАК Период,
	|	Прочитанные.КодСотрудника КАК КодСотрудника,
	|	МАКСИМУМ(Прочитанные.СпособПроверки) КАК СпособПроверки,
	|	МАКСИМУМ(Прочитанные.Статус) КАК Статус,
	|	МАКСИМУМ(Прочитанные.РабочийКод) КАК РабочийКод
	|ИЗ
	|	ВТПрочитанные КАК Прочитанные
	|		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОтметкиСКУД КАК Отметки
	|		ПО (Отметки.Терминал = &Терминал)
	|			И Прочитанные.Период = Отметки.Период
	|			И Прочитанные.КодСотрудника = Отметки.КодСотрудника
	|ГДЕ
	|	Отметки.Период ЕСТЬ NULL
	|
	|СГРУППИРОВАТЬ ПО
	|	Прочитанные.Период,
	|	Прочитанные.КодСотрудника";
	Запрос.УстановитьПараметр("Таблица", Таблица);
	Запрос.УстановитьПараметр("Терминал", Терминал);

	// Ручная загрузка и регламентное задание не пишут один терминал одновременно
	Блокировка = Новый БлокировкаДанных;
	ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ОтметкиСКУД");
	ЭлементБлокировки.УстановитьЗначение("Терминал", Терминал);
	ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

	НачатьТранзакцию();
	Попытка
		Блокировка.Заблокировать();
		Новые = Запрос.Выполнить().Выгрузить();
		Набор = РегистрыСведений.ОтметкиСКУД.СоздатьНаборЗаписей();
		Загружено = ТекущаяДатаСеанса();
		Для Каждого СтрокаНовых Из Новые Цикл
			Запись = Набор.Добавить();
			ЗаполнитьЗначенияСвойств(Запись, СтрокаНовых);
			Запись.Терминал = Терминал;
			Запись.Загружено = Загружено;
		КонецЦикла;
		// Ложь - только добавить: с Истиной набор без отбора заменил бы весь регистр
		Набор.Записать(Ложь);
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		ВызватьИсключение;
	КонецПопытки;

	Возврат Новые.Количество();

КонецФункции

// С какого момента читать журнал: последняя загруженная отметка терминала минус сутки.
// Запас - на случай, если часы терминала переводили назад; повторы отсеет ЗаписатьОтметки.
Функция ЧитатьНачинаяС(Знач Терминал) Экспорт

	Запрос = Новый Запрос(
	"ВЫБРАТЬ
	|	МАКСИМУМ(Отметки.Период) КАК Период
	|ИЗ
	|	РегистрСведений.ОтметкиСКУД КАК Отметки
	|ГДЕ
	|	Отметки.Терминал = &Терминал");
	Запрос.УстановитьПараметр("Терминал", Терминал);
	Выборка = Запрос.Выполнить().Выбрать();
	Если Выборка.Следующий() И ЗначениеЗаполнено(Выборка.Период) Тогда
		Возврат Выборка.Период - 86400;
	КонецЕсли;
	Возврат '00010101';

КонецФункции

// Имена с терминала - в регистр СотрудникиСКУД. Сотрудника 1С в нём
// сопоставляют вручную, загрузка это поле не трогает.
Процедура ЗаписатьСотрудников(Знач Сотрудники) Экспорт

	Для Каждого Сотрудник Из Сотрудники Цикл
		Менеджер = РегистрыСведений.СотрудникиСКУД.СоздатьМенеджерЗаписи();
		Менеджер.КодСотрудника = Сотрудник.КодСотрудника;
		Менеджер.Прочитать();
		Если Менеджер.Выбран() И Менеджер.ИмяНаТерминале = Сотрудник.Имя Тогда
			Продолжить; // имя не менялось
		КонецЕсли;
		Менеджер.КодСотрудника = Сотрудник.КодСотрудника; // ключ ещё раз: записи могло не быть
		Менеджер.ИмяНаТерминале = Сотрудник.Имя;
		Менеджер.Записать();
	КонецЦикла;

КонецПроцедуры

// Эталон для часов терминала - время сервера 1С.
Функция ВремяСервера() Экспорт
	Возврат ТекущаяДатаСеанса();
КонецФункции

// Обработчик регламентного задания «Загрузка отметок СКУД»: раз в 10-15 минут.
// Работает, если zkemkeeper создаётся на сервере: 32-битный сервер 1С или DllSurrogate.
Процедура ЗагрузкаОтметокПоРасписанию() Экспорт

	Для Каждого АдресIP Из АдресаТерминалов() Цикл
		Попытка
			Итог = ТерминалZKTeco.ЗагрузитьОтметки(АдресIP);
			ЗаписьЖурналаРегистрации(ИмяСобытия(), УровеньЖурналаРегистрации.Информация, , , Итог);
		Исключение
			// Недоступный терминал не мешает остальным
			ЗаписьЖурналаРегистрации(ИмяСобытия(), УровеньЖурналаРегистрации.Ошибка, , ,
				АдресIP + ": " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		КонецПопытки;
	КонецЦикла;

КонецПроцедуры

// Адреса терминалов. Для примера - строкой; в рабочей базе держите их в константе или справочнике.
Функция АдресаТерминалов()
	Возврат СтрРазделить("192.168.1.10,192.168.1.11", ",", Ложь);
КонецФункции

Функция ИмяСобытия()
	Возврат НСтр("ru = 'СКУД.Загрузка отметок'");
КонецФункции
  • Набор.Записать(Ложь) — только добавление. У набора нет отбора, и с Истина он заменил бы весь регистр.
  • СГРУППИРОВАТЬ ПО ключу записи: две отметки сотрудника в одну секунду не уронят запись набора.
  • ЧитатьНачинаяС отдаёт последнюю загруженную отметку минус сутки: запас на случай, если часы терминала переводили назад.

Регламентное задание с методом ОтметкиСКУДСервер.ЗагрузкаОтметокПоРасписанию добавьте в конфигурацию или расширение, расписание — в статье о регламентном задании. Оно работает, где SDK создаётся на сервере. На 64-битном сервере без DllSurrogate Планировщик Windows запускает 32-битный клиент с ключом /Execute (разбор схемы): форма обработки вызывает ТерминалZKTeco.ЗагрузитьОтметки и закрывает сеанс.

Кнопка в 32-битном клиенте

// Модуль формы обработки «Загрузка отметок ZKTeco». Открывать в 32-битном тонком клиенте:
// SDK создаётся на клиенте, сервер 1С при этом может быть 64-битным.
// Реквизиты формы: АдресIP (Строка), ОчиститьЖурнал (Булево); команда Загрузить.

&НаКлиенте
Процедура Загрузить(Команда)

	Если ПустаяСтрока(АдресIP) Тогда
		ПоказатьПредупреждение(, НСтр("ru = 'Укажите IP-адрес терминала'"));
		Возврат;
	КонецЕсли;

	Если ОчиститьЖурнал Тогда
		ПоказатьВопрос(Новый ОписаниеОповещения("ЗагрузитьПослеВопроса", ЭтотОбъект),
			НСтр("ru = 'После загрузки журнал на терминале будет очищен. Продолжить?'"),
			РежимДиалогаВопрос.ДаНет);
	Иначе
		ВыполнитьЗагрузку();
	КонецЕсли;

КонецПроцедуры

&НаКлиенте
Процедура ЗагрузитьПослеВопроса(Ответ, ДополнительныеПараметры) Экспорт

	Если Ответ = КодВозвратаДиалога.Да Тогда
		ВыполнитьЗагрузку();
	КонецЕсли;

КонецПроцедуры

&НаКлиенте
Процедура ВыполнитьЗагрузку()

	Попытка
		Итог = ТерминалZKTeco.ЗагрузитьОтметки(СокрЛП(АдресIP), ОчиститьЖурнал);
	Исключение
		ПоказатьПредупреждение(, КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
		Возврат;
	КонецПопытки;
	ПоказатьОповещениеПользователя(НСтр("ru = 'Отметки загружены'"), , Итог);

КонецПроцедуры

Отчёт «опоздания / отработано»

Первая отметка за день — приход, последняя — уход. Статус с терминала отчёт не учитывает: его забывают выбирать. Код сотрудника делайте одинаковым на всех терминалах, тогда отметки с проходной и склада сойдутся. Запрос проверьте в консоли запросов, затем перенесите в схему компоновки.

// Консоль запросов или отчёт на СКД. Параметры: НачалоПериода, КонецПериода (дата и время),
// НачалоСмены - начало рабочего дня в секундах от полуночи (9:00 = 32400).
ВЫБРАТЬ
	Отметки.КодСотрудника КАК КодСотрудника,
	НАЧАЛОПЕРИОДА(Отметки.Период, ДЕНЬ) КАК День,
	МИНИМУМ(Отметки.Период) КАК Приход,
	МАКСИМУМ(Отметки.Период) КАК Уход,
	КОЛИЧЕСТВО(*) КАК ЧислоОтметок
ПОМЕСТИТЬ ВТДни
ИЗ
	РегистрСведений.ОтметкиСКУД КАК Отметки
ГДЕ
	Отметки.Период МЕЖДУ &НачалоПериода И &КонецПериода

СГРУППИРОВАТЬ ПО
	Отметки.КодСотрудника,
	НАЧАЛОПЕРИОДА(Отметки.Период, ДЕНЬ)
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	Дни.День КАК День,
	Сотрудники.Сотрудник КАК Сотрудник,
	ЕСТЬNULL(Сотрудники.ИмяНаТерминале, "") КАК ИмяНаТерминале,
	Дни.КодСотрудника КАК КодСотрудника,
	Дни.Приход КАК Приход,
	Дни.Уход КАК Уход,
	ВЫБОР
		КОГДА Дни.Приход > ДОБАВИТЬКДАТЕ(Дни.День, СЕКУНДА, &НачалоСмены)
			ТОГДА РАЗНОСТЬДАТ(ДОБАВИТЬКДАТЕ(Дни.День, СЕКУНДА, &НачалоСмены), Дни.Приход, МИНУТА)
		ИНАЧЕ 0
	КОНЕЦ КАК ОпозданиеМинут,
	ВЫРАЗИТЬ(РАЗНОСТЬДАТ(Дни.Приход, Дни.Уход, МИНУТА) / 60 КАК ЧИСЛО(6, 2)) КАК ОтработаноЧасов,
	ВЫБОР
		КОГДА Дни.ЧислоОтметок = 1
			ТОГДА ИСТИНА
		ИНАЧЕ ЛОЖЬ
	КОНЕЦ КАК ОднаОтметка
ИЗ
	ВТДни КАК Дни
		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СотрудникиСКУД КАК Сотрудники
		ПО Дни.КодСотрудника = Сотрудники.КодСотрудника

УПОРЯДОЧИТЬ ПО
	День,
	КодСотрудника

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

Типичные ошибки

  • «Ошибка при вызове конструктора (COMОбъект)» с «Класс не зарегистрирован» (0x80040154). Объект создаётся в 64-битном процессе (сервер 1С, 64-битный клиент): ProgID общий для 32 и 64 бит, а класс зарегистрирован только в 32-битной ветке.
  • «Недопустимая строка с указанием класса» (0x800401F3). ProgID нет в реестре: регистрация не прошла или код выполняется на другом компьютере.
  • «Отказано в доступе» (0x80070005) после настройки DllSurrogate. Учётной записи службы 1С не дали запуск и активацию в dcomcnfg.
  • Connect_Net возвращает Ложь с кодом −307 или −12001. Терминал недоступен: IP-адрес, порт 4370, межсетевой экран, подсеть. Код −6 — на терминале задан пароль связи; в SDK для него есть SetCommPassword, по практике его вызывают до подключения.
  • «Метод объекта не обнаружен (ReadTimeGLogData)». Старая сборка SDK: в 6.2.5.5 нет ни этого метода, ни DeleteAttlogBetweenTheDate, в 6.3.1.38 оба есть.
  • Терминал перестал принимать отметки. Его выключили EnableDevice(1, Ложь), а процесс прервался до включения. Подключитесь и вызовите EnableDevice(1, Истина) или перезагрузите терминал.
  • Время отметок сдвинуто. Ушли часы терминала: синхронизация поправит новые отметки, загруженные останутся как были.

Частые вопросы

Можно ли читать не весь журнал, а только новые отметки?

В SDK есть ReadTimeGLogData(1, "2026-10-01 00:00:00", "2026-10-08 23:59:59") — интервал с точностью до секунды — и ReadNewGLogData(1). По руководству SDK оба работают только на прошивках новой архитектуры. Универсальный путь — читать всё и отсекать старое по дате, как в модуле выше.

Можно ли получать отметку сразу, как сотрудник приложил палец?

SDK умеет события (RegEvent, OnAttTransactionEx), но для них процесс с объектом должен работать постоянно, а для 1С это неудобно. Учёту рабочего времени хватает загрузки раз в 10–15 минут.

Терминал подключён по USB или RS485, а не по сети?

Вместо Connect_Net — Connect_Com(ComPort, MachineNumber, BaudRate); для USB по руководству SDK нужен драйвер виртуального COM-порта. НомерТерминала() должен совпасть с номером устройства на линии.

Как связать отметки с табелем?

Сначала сопоставьте коды терминала с сотрудниками в регистре СотрудникиСКУД. В КА 2.5 есть документ «Табель учета рабочего времени», но заполнять его из отметок — отдельная доработка. Своя обёртка над SDK вместо COM — тема статьи о внешней компоненте.

Когда проще заказать

Если терминалов несколько, сервер 64-битный и правку реестра надо согласовать с администратором, а отчёт должен учитывать графики смен, быстрее отдать задачу программисту: он подберёт схему и проверит загрузку на копии базы. Разместите задачу — оценка бесплатная, ответ в течение рабочего дня. Отдельно отчёт на СКД — в разделе отчёты и обработки 1С.

По теме

Не хотите разбираться сами?

Адаптируем это решение под вашу базу. Оценка бесплатно.

Оставить задачу

Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.


1С, 1С:Предприятие — товарные знаки ООО «1С». Сайт не является официальным представительством фирмы «1С» и не аффилирован с ней.