commit 44130aa746b9e21e787657a99326597bf7ac29bd Author: Ilya_Chis Date: Thu Jan 16 13:32:08 2025 +0500 проект HR_bot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08177ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +тест.py \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/Bot_for_clinic.iml b/.idea/Bot_for_clinic.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/Bot_for_clinic.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..891d263 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3baff58 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/botforclinic-436512-0c117dd103a8.json b/botforclinic-436512-0c117dd103a8.json new file mode 100644 index 0000000..1ab70e3 --- /dev/null +++ b/botforclinic-436512-0c117dd103a8.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "botforclinic-436512", + "private_key_id": "0c117dd103a890d20705d9e66bbfde56f9d65a05", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4XOtHBzxPR4lb\nqiX8grd2XBfBxXZLgLKTkz4WRcncgBpACXKms5AtKF707uKCvgUD8SwTpzvfY3oz\nvzLQs0uZoM9RALw5ui9dlimdoYQGsuH5LAUX68w0oPyour36XeBqacxNoEeO5C2F\nbb4+ql4gC2qJT86aBt2kwKpTxnTPZxvYoKZ0TwsmN8gj9vNckONbSSZteMKpQA50\nRqpoLM0EpsB64HRSiu8mH+bkdLK9ZMEZGmOG7pNDijctGkgAy9rK9vurH4PRgIMt\n7j+q/EeBQDuZX2SSVIaMkiFkDhHVGd84CFzcodBIYOSiCDer7aIcv8SrcYGZ0QJD\nnIzTqZMrAgMBAAECggEAW3HHR2dI3D/s2xkVl15U1nHOXM/vIXsX2EgH2y9JdUPb\nAVlOU24sPKHSG6YjltrejPqCNMtV0LM8SU90Sn0sKerP+esl8XIujTpusN+Mp2/x\n0tApRJiyJIaXW6+ojHck2VNZwUUXSUJXa9+npdiGMFnJQYtivBZcuJvRwBTnGnm/\ntuwQUcQREPy8IaaX5j+xPf390KeXqNOopULW7FtItkHthM18fr6706X1GQ77ua23\nzz1DT2PfIxOu2e3uPJ1O1CsQ8UiTIAm3J0nNgbpau1nP2+ea62vCrjmuqcyn5dfb\nhpgGD7Qe8uRvU+EICr67sOW47ikjAqkM38lZFgufuQKBgQDmypfFTYke7oDTZ8GE\nZFHZKn+ri5s94pDzjeeb8HxmeEC85cA6+jOr6f1QncwHON+tD2FRO7HHp/yVVyZY\nU2RSxaLaFleRL4CwlQkiMoqynLFWbqQthqdeJx2onNyGoO8uBDqfMJVBhOjwb8jI\n3SZt1Z6BJ076CMMOiLI7moZsWQKBgQDMgBdWND+uTR0SHXWqF1hj/+UgwHBqQKio\nyWbnIO28uFyMscfBAgh7JJbqcJIh6A+M9NdD4TveqZWoYBaUHkMBg5VGcmWrCH+L\nJOlAVJmhy4GgpE2nkYE0ZgDd0qlViYeTOKaLSKchPrLxRKNI9eeWkKqrk/a7wBHh\nsGJbt8p7IwKBgQCvFYaqAbZCpwFIX/ApNcn6DAmq5FAzVIxOmJrabEiTnCrQoyOA\nGovUocCwxeUgnX7i1UYo0DaAlupBiUnnu+vh91kiRczMsdLZI1c4gEtEUScfwPQA\nLaUPAnG3lrid0b1hlYe7eKnieKgWr1cNOuiKboK+zElX2gnQHtfH6+SKkQKBgDYW\nsxVz4FwDvWqrLBH+9rwMVNgizhjsXS+3hWYgiYDe6mu0IfdkyEV52gjMGXqqXGKV\n59HmYuVzyUetkos+rc5atULawxVHvCRbcO448iGfJ/wLORbXH2tyh0wkifE73QYN\nd89jOgFLCh1SYDXOBKEIRnv3OM1T8ebVtk44AHAtAoGAfOprlkWEDyVnfHQjGxxx\nTrxhEAjw3+ikM+wQJj7vumnpg91eAFNcwRUa/ykbVOmj6z/fBnStAbsG7mMQwa1M\nyIVPTvUogrsTHvf9GaG0IaAVJn/Su2DVNqQeL/ZpDmu2+wC959g8z665BKLzm3S8\nFc0eV1a7FN4GOpKNvXuDMBc=\n-----END PRIVATE KEY-----\n", + "client_email": "clinic-chats-bot@botforclinic-436512.iam.gserviceaccount.com", + "client_id": "105253150604049743778", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/clinic-chats-bot%40botforclinic-436512.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/probnik_Bot_for_HR.py b/probnik_Bot_for_HR.py new file mode 100644 index 0000000..8ac3872 --- /dev/null +++ b/probnik_Bot_for_HR.py @@ -0,0 +1,442 @@ +from googleapiclient.discovery import build +from google.oauth2.service_account import Credentials +import asyncio +from aiogram.types import WebAppInfo +import logging +from aiogram import Bot, Dispatcher, types +from aiogram.utils import executor +from aiogram.types import ChatType +from aiogram.utils.exceptions import BadRequest +from aiogram.types import ReplyKeyboardMarkup, KeyboardButton +from aiogram.utils.exceptions import MessageCantBeDeleted +import gspread +from datetime import datetime +from google.oauth2 import service_account +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo, ReplyKeyboardMarkup, KeyboardButton +import sentry_sdk + + + +sentry_sdk.init( + dsn="https://697d3f2b6cb54a78202eeae0cb8c1f65@sentry.pirogov.ai/15", + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + traces_sample_rate=1.0, +) + + +# Настройки бота +API_TOKEN = '7414018470:AAGYF77aCd1BQpRnf38ys05ijkYJ25sNBuU' +ADMIN_ID = 766945900 # ID администратора, которому будет приходить уведомление +SPREADSHEET_ID = '128R3An6fb2PFGlzYMKb4SvEaSYCWUHHFVvial7llGno' # ID вашей Google Таблицы +RANGE_NAME = 'Лист1!A1:O' # Диапазон данных в таблице +CHAT_IDS = [-1002306913175, -1002266505101] # Замените на ID чатов, где бот администратор + +# Логирование +logging.basicConfig(level=logging.INFO) + +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + +# Настройки для Google Sheets API +SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] +SERVICE_ACCOUNT_FILE = 'botforclinic-436512-0c117dd103a8.json' + +credentials = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES) +service = build('sheets', 'v4', credentials=credentials) + +# Настройка API для работы с документами +SCOPES = ['https://www.googleapis.com/auth/documents', 'https://www.googleapis.com/auth/drive'] +SERVICE_ACCOUNT_FILE = 'botforclinic-436512-0c117dd103a8.json' + +credentials = service_account.Credentials.from_service_account_file( + SERVICE_ACCOUNT_FILE, scopes=SCOPES) + +docs_service = build('docs', 'v1', credentials=credentials) +drive_service = build('drive', 'v3', credentials=credentials) + +# Идентификатор Google Документа (шаблона трудового договора) +TEMPLATE_DOC_ID = '1xi4YiPNBEDvODS0SaVb0wgKZ_b_OBkas' + +# Словарь для хранения сотрудников, которых уже обработали +known_employees = set() + +def create_contract_google(employee_data): + # Копирование шаблона + file_metadata = { + 'name': f"ТД_{employee_data[0]}_{datetime.now().strftime('%Y%m%d%H%M%S')}", + 'mimeType': 'application/vnd.google-apps.document', + 'parents': ['1fyA5btnwCQC-juE40T-uLSVHVIB-I2Ny'] + } + copied_file = drive_service.files().copy( + fileId=TEMPLATE_DOC_ID, body=file_metadata).execute() + document_id = copied_file['id'] + + # Замены текста в шаблоне + replacements = { + "{{ФИО}}": employee_data[0], + "{{Серия паспорта}}": employee_data[3], + "{{Номер паспорта}}": employee_data[4], + "{{Кем выдан}}": employee_data[5], + "{{Дата выдачи}}": employee_data[6], + "{{Адрес}}": employee_data[8], + "{{Дата рождения}}": employee_data[12], + "{{ИНН}}": employee_data[11], + "{{СНИЛС}}": employee_data[10], + } + + requests = [{"replaceAllText": { + "containsText": {"text": key, "matchCase": True}, + "replaceText": value + }} for key, value in replacements.items()] + + docs_service.documents().batchUpdate( + documentId=document_id, body={'requests': requests}).execute() + + return f"https://docs.google.com/document/d/{document_id}/edit" + + + +# Словарь с соответствием специальности и ссылки на чат +specialty_chat_links = { + "Врачи": "https://t.me/botclinic", + "Медсёстры": "https://t.me/+DcZn0yBO0RFjZTY6", + "Санитарки": "https://t.me/joinchat/{chat_id_orderlies}", + "Администраторы": "https://t.me/joinchat/{chat_id_admins}", + "Операторы КЦ": "https://t.me/joinchat/{chat_id_operators}", + "АУП": "https://t.me/joinchat/{chat_id_aap}" +} + + +# Функция для отправки сообщения о новом сотруднике с клавиатурой для выбора специальности +async def notify_admin_about_new_employee(entry): + full_name = entry[0] if entry else "Без имени" + new_employee_id = entry[13] if len(entry) > 13 else None + + if new_employee_id: + # Отправляем уведомление админу о новом сотруднике с клавишами выбора специальности + keyboard = create_employee_keyboard() + + await bot.send_message( + ADMIN_ID, + f"Зарегистрирован новый сотрудник: {full_name}\nTelegram ID сотрудника: {new_employee_id}", + reply_markup=keyboard + ) + else: + logging.error("Не удалось найти Telegram ID сотрудника.") + + +# Функция для проверки новых сотрудников +async def check_for_new_entries(): + sheet = service.spreadsheets() + + while True: + try: + result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME).execute() + values = result.get('values', []) + except Exception as e: + logging.error(f"Ошибка при чтении таблицы: {e}") + await asyncio.sleep(30) # Ждем перед следующей попыткой + continue + + # Проверяем каждую запись на наличие нового сотрудника + for entry in values: + full_name = entry[0] if entry else "Без имени" + new_employee_id = entry[13] if len(entry) > 13 else None # Telegram ID сотрудника + + if full_name and full_name not in known_employees: + known_employees.add(full_name) + + # Генерация трудового договора + try: + contract_url = create_contract_google(entry) + + # Уведомление для администратора + await bot.send_message( + ADMIN_ID, + f"Зарегистрирован новый сотрудник: {full_name}\nТрудовой договор: {contract_url}", + ) + + # Уведомление для сотрудника + if new_employee_id: + welcome_message = f"Большое спасибо! Мы получили данные для заключения трудового договора с вами." + await bot.send_message( + new_employee_id, + welcome_message, + ) + + # Оповещаем админа и отправляем клавиатуру для выбора специальности + await notify_admin_about_new_employee(entry) + + except Exception as e: + logging.error(f"Ошибка при создании трудового договора: {e}") + await bot.send_message(ADMIN_ID, "Произошла ошибка при создании трудового договора.") + + await asyncio.sleep(10) # Проверяем таблицу каждые 10 секунд + + + + + + + + + +# Функция отправки сообщения о прохождении инструктажа +async def send_fire_safety_instructions(employee_id, full_name, employee_data): + message_text = f""" +{full_name}, добрый день. Вы приглашены на корпоративный канал Клиники в Telegram. В закрепленном сообщении на этом канале Вы сможете ознакомиться с такими разделами: + +👉[Наши сотрудники](https://t.me/c/1414064442/5) - актуальный список всех сотрудников Клиники; +👉[Контакты сотрудников](https://t.me/c/1414064442/39) - номера телефонов сотрудников для скачивания в телефонную книгу; +👉[История клиники](https://t.me/c/1414064442/6) +👉[Структура рабочих чатов](https://t.me/c/1414064442/10) +👉[Инструктаж по пожарной безопасности в Клинике ухо, горло, нос](https://disk.yandex.ru/i/i1k41Z2SznfpOA) +👉[Обратная связь](https://forms.gle/Dfjrb8K1NzzjmW5j7) - оставить предложение, пожелание или задать вопрос руководству (анонимно). + +**ВАЖНО:** +В разделе [Инструктаж по пожарной безопасности в Клинике ухо, горло, нос](https://disk.yandex.ru/i/i1k41Z2SznfpOA) Вам необходимо изучить материалы по Пожарной безопасности, уделить особое внимание теме - порядок действий и по каким номерам звонить в случае пожароопасной ситуации. + +После этого пройти тестирование по данной теме (21 вопрос - зачет 70% правильных ответов): +👉[Тестирование по пожарной безопасности](https://forms.gle/VLEx2Gf1h8grpXXu5) + +**Срок обучения и прохождения тестирования** - до начала выполнения трудовой функции! + +Спасибо! + +С уважением, +Жуланова Наталья Александровна, +помощник генерального директора ООО "Клиника ухо, горло, нос им. проф. Е.Н.Оленевой" (г. Пермь) +📞 +7 (902) 64-54-648 +""" + try: + # Отправка сообщения сотруднику + await bot.send_message(employee_id, message_text, parse_mode="Markdown") + logging.info(f"Сообщение о пожарной безопасности отправлено {full_name}.") + + + except Exception as e: + logging.error(f"Ошибка при отправке инструктажа или документа: {e}") + await bot.send_message(ADMIN_ID, "Произошла ошибка при отправке инструктажа или документа.") + + +# Обработчик нажатия на кнопки выбора специальности + +@dp.message_handler(lambda message: message.text in specialty_chat_links.keys(), chat_type=types.ChatType.PRIVATE) +async def handle_specialty_selection(message: types.Message): + selected_specialty = message.text + data = sheet.get_all_values() + last_employee_row = len(data) + + if last_employee_row < 2: + await message.answer("В таблице пока нет сотрудников для назначения специальности.") + return + + new_employee_id = data[last_employee_row - 1][13] + full_name = data[last_employee_row - 1][0] + + if not new_employee_id: + await message.answer("Не удалось найти Telegram ID сотрудника в таблице.") + return + + try: + sheet.update_cell(last_employee_row, 16, selected_specialty) + await message.answer(f"Специальность сотрудника обновлена на: {selected_specialty}") + + chat_invite_link = specialty_chat_links.get(selected_specialty) + if chat_invite_link: + await bot.send_message( + new_employee_id, + f"Поздравляем! Вы были добавлены в нашу команду как {selected_specialty}. Вот ссылка на вашу беседу: {chat_invite_link}" + ) + await message.answer(f"Ссылка на беседу отправлена сотруднику.") + + # Отправка сообщения о прохождении инструктажа + await send_fire_safety_instructions(new_employee_id, full_name, data[last_employee_row - 1]) + + except Exception as e: + await message.answer(f"Ошибка при обновлении таблицы: {e}") + + + +# Функция для генерации клавиатуры с кнопками типа сотрудника +def create_employee_keyboard(): + keyboard = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=False) + button_doctors = KeyboardButton("Врачи") + button_nurses = KeyboardButton("Медсёстры") + button_orderlies = KeyboardButton("Санитарки") + button_admins = KeyboardButton("Администраторы") + button_operators = KeyboardButton("Операторы КЦ") + button_aap = KeyboardButton("АУП") + + keyboard.add(button_doctors, button_nurses) + keyboard.add(button_orderlies, button_admins) + keyboard.add(button_operators, button_aap) + + return keyboard + + +# Обработчик команды /start +@dp.message_handler(commands=['start'], chat_type=types.ChatType.PRIVATE) +async def send_welcome(message: types.Message): + user_id = message.from_user.id + + # Отправка фото + photo_path = r'D:\Users\Maxim\PycharmProjectsbot\Bot_for_clinic\start_foto.png' + await bot.send_photo(user_id, types.InputFile(photo_path)) + + # Отправка текста с кнопкой + await bot.send_message( + user_id, + "Добрый день, этот бот нужен Вам для начала прохождения процедуры трудоустройства в " + "Общество с ограниченной ответственностью «Клиника ухо, горло, нос имени профессора Е.Н. Оленевой».\n" + "Нажмите, пожалуйста, на команду 👉 /registration", + reply_markup=get_registration_button() + ) + + + + +# Обработчик команды регистрации +@dp.message_handler(commands=['registration'], chat_type=types.ChatType.PRIVATE) +async def send_welcome(message: types.Message): + await message.answer( + "▪️В соответствии с требованиями статьи 9 Федерального закона от 27.07.2006 г. «О персональных данных» № 152-ФЗ даю согласие на обработку своих персональных данных.\n" + "▪️Наименование и адрес оператора, получающего согласие субъекта персональных данных: Общество с ограниченной ответственностью «Клиника ухо, горло, нос имени профессора Е.Н. Оленевой».\n" + "▪️Цель обработки персональных данных: организация труда и осуществление иных, связанных с этим мероприятий.", + reply_markup=get_consent_keyboard() + ) + +# Обработчик ответа пользователя +@dp.message_handler(lambda message: message.text in ["✅ Даю согласие на обработку своих персональных данных", "❌ Не даю согласие на обработку своих персональных данных"]) +async def process_consent(message: types.Message): + user_id = message.from_user.id + + if message.text == "✅ Даю согласие на обработку своих персональных данных": + form_url = f"https://tgbotkugn.pirogov.ai/?user_id={user_id}" + form_keyboard = InlineKeyboardMarkup().add( + InlineKeyboardButton( + text="Ввести персональные данные", web_app=WebAppInfo(url=form_url) + ) + ) + await message.answer( + "Спасибо за согласие! Нажмите кнопку ниже, чтобы ввести персональные данные:", + reply_markup=form_keyboard + ) + else: + await message.answer( + "Вы не дали согласие на обработку персональных данных.\n" + "Вы не можете продолжить регистрацию.", + reply_markup=get_consent_keyboard() + ) + +@dp.message_handler(commands=['getchatid'], chat_type=[ChatType.GROUP, ChatType.SUPERGROUP]) +async def get_chat_id(message: types.Message): + # Проверяем статус пользователя в чате + chat_member = await bot.get_chat_member(message.chat.id, message.from_user.id) + if chat_member.status not in ['administrator', 'creator']: + # Отправляем предупреждение о недостатке прав + warning_message = await message.answer("У вас нет прав на выполнение этой команды.") + + # Удаляем команду /getchatid через 1 секунду + await asyncio.sleep(1) + try: + await message.delete() + except MessageCantBeDeleted: + logging.warning("Не удалось удалить сообщение с командой /getchatid") + + # Удаляем предупреждение через 5 секунд + await asyncio.sleep(5) + try: + await warning_message.delete() + except MessageCantBeDeleted: + logging.warning("Не удалось удалить сообщение с предупреждением") + return + + # Отправляем ID и название чата администратору бота + chat_id = message.chat.id + chat_title = message.chat.title or "Без названия" + await bot.send_message(ADMIN_ID, f"ID чата: {chat_id}\nНазвание чата: {chat_title}") + + # Удаляем команду /getchatid через 1 секунду + await asyncio.sleep(1) + try: + await message.delete() + except MessageCantBeDeleted: + logging.warning("Не удалось удалить сообщение с командой /getchatid") + + + +# Инициализация доступа к Google Sheets +gc = gspread.service_account(filename=SERVICE_ACCOUNT_FILE) +sheet = gc.open_by_key(SPREADSHEET_ID).sheet1 # Открываем первую таблицу + + +@dp.message_handler(commands=['kick'], chat_type=types.ChatType.PRIVATE) +async def kick_user(message: types.Message): + if message.from_user.id != ADMIN_ID: + await message.answer("Эта команда доступна только администратору бота.") + return + + # Получаем ФИО из сообщения + full_name = message.get_args().strip() + if not full_name: + await message.answer("Пожалуйста, укажите ФИО пользователя для кика.") + return + + # Поиск ID пользователя по ФИО в Google Таблице + try: + data = sheet.get_all_values() # Получаем все значения в таблице + except Exception as e: + await message.answer("Не удалось получить данные из таблицы.") + return + + user_id = None + for row in data: + if row and row[0] == full_name: # ФИО в первом столбце + user_id = row[13] # ID в 14-м столбце (индекс 13) + break + + if user_id is None: + await message.answer(f"Пользователь с ФИО '{full_name}' не найден в таблице.") + return + + errors = [] # Список для хранения ошибок + for chat_id in CHAT_IDS: + try: + await bot.kick_chat_member(chat_id, user_id) + except BadRequest as e: + errors.append(f"Ошибка в чате {chat_id}: {e}") + + # Отправляем сообщение админу один раз + if errors: + await bot.send_message( + ADMIN_ID, + f"Пользователь с ID {user_id} был исключён из чатов, " + f"но произошли ошибки: {'; '.join(errors)}" + ) + else: + await bot.send_message(ADMIN_ID, f"Пользователь с ID {user_id} был исключён из всех чатов.") + + await message.answer(f"Команда на кик пользователя с ФИО '{full_name}' отправлена во все чаты.") + + +def get_registration_button(): + keyboard = ReplyKeyboardMarkup(resize_keyboard=True) + registration_button = KeyboardButton("/registration") + keyboard.add(registration_button) + return keyboard + + +def get_consent_keyboard(): + keyboard = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) + consent_button = KeyboardButton("✅ Даю согласие на обработку своих персональных данных") + deny_button = KeyboardButton("❌ Не даю согласие на обработку своих персональных данных") + keyboard.add(consent_button, deny_button) + return keyboard + +if __name__ == '__main__': + # Запускаем отслеживание таблицы и бота + loop = asyncio.get_event_loop() + loop.create_task(check_for_new_entries()) + executor.start_polling(dp, skip_updates=True) diff --git a/start_foto.png b/start_foto.png new file mode 100644 index 0000000..9dcb20f Binary files /dev/null and b/start_foto.png differ diff --git a/~$_бланк.docx b/~$_бланк.docx new file mode 100644 index 0000000..8437482 Binary files /dev/null and b/~$_бланк.docx differ diff --git a/ТД_бланк.docx b/ТД_бланк.docx new file mode 100644 index 0000000..cc38019 Binary files /dev/null and b/ТД_бланк.docx differ