Compare commits

..

21 Commits
main ... main

Author SHA1 Message Date
kinoshitakenta f9792b99e0
chore: add Beerware license 2025-08-18 14:05:10 +08:00
kinoshitakenta 42ca15bfbd
docs: updates README documentation
Updates a README file to describe the project, including instructions on how to configure and run the application, and explains the project's purpose.
2025-08-18 13:41:46 +08:00
kinoshitakenta 82fb61ec26
fix: prompt re-login on expired Selenium session to prevent crash 2025-07-03 17:06:25 +08:00
kinoshitakenta 982e70a4e4
docs: use straight quotes (') instead of typographic apostrophes (’)
Typographic apostrophes can lead to formatting or encoding issues in technical documents.
Straight quotes are preferred in source-controlled files to ensure consistency and portability.
2025-07-03 13:26:29 +08:00
kinoshitakenta e1ec580ce1
fix: missing import `ctypes` library 2025-07-03 10:29:12 +08:00
kinoshitakenta 5b87c86223
fix: skip handling alerts that are already closed
Avoids handling alerts that have already been dismissed at the time of operation.
2025-07-03 10:18:35 +08:00
kinoshitakenta 2c5ca3c3d3
docs: removes outdated `TODO.txt`
Removed TODO.txt as the mentioned feature has been implemented.
2025-06-20 14:50:27 +08:00
kinoshitakenta 86bd790f1c
feat: added the function to open the booking meeting room page 2025-06-20 14:30:33 +08:00
kinoshitakenta ccd5dee807
Merge branch 'env/migrate-to-uv' 2025-06-20 13:52:19 +08:00
kinoshitakenta bee7927ef9
chore: use uv to manage the environment
Refactors the project to use `uv` for dependency management
instead of `poetry` and `requirements.txt`.

Updates `pyproject.toml` to remove unnecessary fields, and adds
`uv.lock` to specify exact versions of dependencies.

Updates the README with `uv` command to execute the application.
2025-06-20 13:37:38 +08:00
kinoshitakenta 8579b135cf
Merge branch 'env/migrate-to-poetry' 2025-06-19 16:52:21 +08:00
kinoshitakenta 07e01554d0
chore: updates author info and dependencies
Updates the author information and adjusts the dependency versions in the project configuration.
2025-06-19 16:47:22 +08:00
kinoshitakenta 953b6ab909
feat: improves element click handling
Adds retry logic to handle `ElementClickInterceptedException`.

This change enhances the robustness of element clicking by implementing a retry mechanism with a timeout.
It addresses the issue where clicks are sometimes intercepted (such as scrolling up and down on the page), preventing the action from succeeding.
2025-06-12 16:29:28 +08:00
kinoshitakenta 77054f1d84
docs: adds documentation to login function
Adds a detailed docstring to the `keep_login_status` function,
clarifying its purpose, arguments, return value, and behavior
during login attempts.
2025-06-12 16:26:41 +08:00
kinoshitakenta 6523fecb5a
feat: improves login status handling
Ability to further handle pop-up windows when login attempts are unsuccessful.

Modified the return value of the `keep_login_status` function to indicate whether the operation was successful or failed.
In case of a failure, no further command actions will be executed.
2025-06-12 16:25:26 +08:00
kinoshitakenta 3c23ee2ce9
feat: improves screen clearing functionality
Updates the screen clearing function to provide a more robust cross-platform solution. It now attempts to reset text attributes on Windows and uses `shutil.which` to locate the `clear` command on Unix-like systems, falling back to ANSI escape codes if necessary.

This change improves the screen-clearing function, ensuring its effectiveness regardless of the operating system or available commands.
2025-05-28 15:57:25 +08:00
kinoshitakenta 0671d49497
feat: validates config path before loading login info
Adds a check to ensure the provided configuration file path exists and is a valid file before attempting to load login information.
2025-05-28 15:45:01 +08:00
kinoshitakenta e32063e2ce
refactor: uses constants for EIP login elements
Refactors the code to use constants for EIP URL and HTML element IDs, improving code maintainability and readability.
This change replaces hardcoded strings with named constants, making it easier to update element identifiers if they change on the EIP website.
2025-05-28 15:33:05 +08:00
kinoshitakenta cfd1e5e61a
feat: validates required fields in login configuration
Adds validation to ensure that all required fields ("lang", "login_ID", "login_passwd", "company_ID") are present in the login_info section of the configuration file.

Raises a ValueError if any required field is missing, improving error handling and preventing potential issues
due to incomplete configuration.
2025-05-28 15:33:03 +08:00
kinoshitakenta ef204f1aae
chore: improved compatibility with Python 3.11 and above
Uses `tomllib` when available for Python 3.11+ and falls back to `tomli` for older versions.

This avoids deprecation warnings and ensures compatibility across different Python versions.
2025-05-28 13:19:31 +08:00
kinoshitakenta 6de63bfdb5
chore: add rich library for better output
Includes `rich` library in requirements.txt to enhance the formatting and presentation of output in the application.
2025-05-20 16:25:57 +08:00
9 changed files with 203 additions and 50 deletions

2
.gitignore vendored
View File

@ -6,4 +6,4 @@ __pycache__/
# config file
config.toml
poetry.lock
uv.lock

8
LICENSE Normal file
View File

@ -0,0 +1,8 @@
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <ybs0306748@gmail.com> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return Kinoshita Kenta
* ----------------------------------------------------------------------------
*/

View File

@ -1,3 +1,50 @@
# auto_login_EIP
`poetry run python -m main --config_path .\config.toml.example`
太常要訂便當了,所以寫個自動登入 EIP 的東西
省的每次都要找 EIP 的網址是什麼
## env
* Python
* uv
* 剩下的 uv 會幫你裝
## repository structure
```text
./auto_login_EIP/
├── main.py # main file
├── config.toml # config file
├── utils/
├── pyproject.toml
├── .gitignore
└── README.md
```
## usage
### 配置
`config.toml` 設定好登入資訊,如果員工 ID 與密碼留空則會在登入時詢問你
專案路徑下有一個 `config.toml.example` 提供作為修改模板
### 執行
```shell
# uv run python main.py
```
或指定 config 檔
```shell
# uv run python main.py --config_path .\YOUR_CONFIG.toml
```
---
如果要酷一點的話串個 Windows 工作排程器或 Linux crontab
這樣每天早上就會自動幫你把 EIP 開起來,就能當個認真的模範打工仔了
我要繼續去 coding ㄌ各位88

View File

@ -1,2 +0,0 @@
1. 過久沒登入,然後重登之後,要操作的頁面被關掉
2. 重新一個新指令時,把其他開啟的頁面全部關掉

42
main.py
View File

@ -4,10 +4,12 @@
__author__ = 'kinoshitakenta'
__email__ = "ybs0306748@gmail.com"
import ctypes
import logging
import os
from pathlib import Path
from selenium.common.exceptions import InvalidSessionIdException
from utils.cli import cli
from utils.driver import get_driver
from utils.EIP_action import ActionType, Action
@ -25,25 +27,50 @@ def get_usage() -> list[tuple]:
return cmd_list
def display_usage():
def clear_screen():
if os.name == 'nt':
# Windows system
try:
# try to reset the text attributes
kernel32 = ctypes.windll.kernel32
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
kernel32.SetConsoleTextAttribute(handle, 7)
except Exception:
pass
os.system('cls')
else:
os.system('clear')
# Unix-like system
clear_cmd = shutil.which('clear')
if clear_cmd:
os.system(clear_cmd)
else:
# backup plan: Use ANSI escape codes (to clear and reset the screen).
print('\033c', end='')
def display_usage():
clear_screen()
for cmd, msg in get_usage():
print(f"{cmd}: {msg}")
print()
def main(opt):
login_info = LoginInfo(Path(opt.config_path))
config_path = Path(opt.config_path)
if not config_path.exists() or not config_path.is_file():
print(f"Error: Config path '{config_path}' does not exist or is not a file.")
return
login_info = LoginInfo(config_path)
driver = get_driver()
keep_login_status(driver, login_info)
action_agent = Action(driver)
display_usage()
keep_login_status(driver, login_info)
try:
while True:
cmd = input("\nInput action code: ").strip()
@ -69,8 +96,11 @@ def main(opt):
continue
# * run command
keep_login_status(driver, login_info)
action_agent.run(action_code)
if keep_login_status(driver, login_info):
action_agent.run(action_code)
except InvalidSessionIdException:
print("Session has expired, please re-login.")
except KeyboardInterrupt:
pass

View File

@ -2,19 +2,19 @@
name = "auto-login-eip"
version = "0.1.0"
description = ""
authors = [
{name = "Your Name",email = "you@example.com"}
]
authors = [{ name = "Kinoshita Kenta", email = "ybs0306748@gmail.com" }]
requires-python = ">=3.9"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"selenium (>=4.32.0,<5.0.0)",
"selenium (>=4.27.1,<5.0.0)",
"tomli (>=2.2.1,<3.0.0)",
"webdriver-manager (>=4.0.2,<5.0.0)",
"rich (>=14.0.0,<15.0.0)"
"rich (>=13.9.4,<15.0.0)",
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.uv]
package = false

View File

@ -1,6 +0,0 @@
# for parsing toml file
tomli
# `webdriver_manager` manages webdriver versions
webdriver_manager
selenium

View File

@ -14,6 +14,7 @@ class ActionType(IntEnum):
請假 = 1
補卡 = 2
訂便當 = 3
訂會議室 = 4
@classmethod
def has_value(cls, value):
@ -25,7 +26,8 @@ class Action():
self.action_list = {ActionType.登入: self.__登入,
ActionType.請假: self.__請假,
ActionType.補卡: self.__補卡,
ActionType.訂便當: self.__訂便當
ActionType.訂便當: self.__訂便當,
ActionType.訂會議室: self.__訂會議室
}
self.driver = driver
@ -125,9 +127,17 @@ class Action():
order_title, img, order_status = a_tag
if order_status.text != "【已訂購】":
order_title.click()
open_page_num += 1
time.sleep(0.05)
for attempt in range(5):
try:
order_title.click()
open_page_num += 1
time.sleep(0.05)
break # 點成功就跳出 retry
except ElementClickInterceptedException:
print("點擊被遮蔽,重試中...")
time.sleep(0.2) # 等一下再重試
else:
print("點擊失敗:可能被遮蔽或其他原因")
if open_page_num > 0:
print(f"已開啟 {open_page_num} 個訂購頁面")
@ -135,3 +145,16 @@ class Action():
print("沒有尚未訂購的團購訂單")
self.driver.switch_to.default_content()
def __訂會議室(self):
"""訂會議室"""
self.driver.switch_to.frame(self.driver.find_element(By.ID, "main"))
all_meeting_room_tag = self.driver.find_element(By.ID, "WPPublicResource_TreeTagt0") # 會議室
ActionChains(self.driver).move_to_element(all_meeting_room_tag).move_to_element(all_meeting_room_tag).click(all_meeting_room_tag).perform()
Zhubei_tag = self.driver.find_element(By.ID, "WPPublicResource_TreeTagt1") # 竹北
ActionChains(self.driver).move_to_element(Zhubei_tag).move_to_element(Zhubei_tag).click(Zhubei_tag).perform()
meeting_room_500_tag = self.driver.find_element(By.ID, "WPPublicResource_TreeTagt2") # 500會議室
ActionChains(self.driver).move_to_element(meeting_room_500_tag).move_to_element(meeting_room_500_tag).click(meeting_room_500_tag).perform()

View File

@ -1,17 +1,35 @@
import getpass
import sys
import time
from pathlib import WindowsPath
import tomli
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from selenium import webdriver
from selenium.common.exceptions import NoAlertPresentException, NoSuchWindowException, UnexpectedAlertPresentException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
# Constants for configuration and element IDs
EIP_URL = "https://eip.techmation.com.tw/MotorWeb/MotorFaceDefaultNew.aspx"
LANG_DROPDOWN_ID = "ddlLang"
LOGIN_ID_INPUT_ID = "txtLoginID"
LOGIN_PWD_INPUT_ID = "txtLoginPwd"
COMPANY_ID_DROPDOWN_ID = "ddlCompanyID"
LOGIN_BUTTON_ID = "btnLogin"
class LoginInfo():
def __init__(self, config_path: WindowsPath):
with open(config_path, mode="rb") as f:
config = tomli.load(f)
config = tomllib.load(f)
required_fields = ["lang", "login_ID", "login_passwd", "company_ID"]
for field in required_fields:
if field not in config.get("login_info", {}):
raise ValueError(f"Missing required field: {field} in login_info section")
self.lang = config["login_info"]["lang"]
self.login_ID = config["login_info"]["login_ID"]
@ -27,8 +45,21 @@ class LoginInfo():
"please enter your passwd: ")
def keep_login_status(driver: webdriver.Chrome, login_info: LoginInfo):
EIP_url = "https://eip.techmation.com.tw/MotorWeb/MotorFaceDefaultNew.aspx"
def keep_login_status(driver: webdriver.Chrome, login_info: LoginInfo) -> bool:
"""
Attempt to log into the CHI MotorWeb ERP system using the provided login information.
This function navigates to the login page, fills in the login form with the user's credentials,
and attempts to log in. It handles unexpected alert pop-ups that indicate login failure,
and manages browser windows to ensure only the relevant page remains open.
Args:
driver (webdriver.Chrome): An instance of Selenium WebDriver controlling a Chrome browser.
login_info (LoginInfo): A data object containing login credentials and preferences.
Returns:
bool: True if login is successful or already logged in; False if login failed (e.g., due to incorrect credentials).
"""
# * Close all windows except the main window.
while len(driver.window_handles) > 1:
@ -38,57 +69,79 @@ def keep_login_status(driver: webdriver.Chrome, login_info: LoginInfo):
driver.switch_to.window(driver.window_handles[0])
top_page = driver.current_window_handle
driver.get(EIP_url)
driver.get(EIP_URL)
time.sleep(0.3)
if driver.title == "CHI MOTOR WEB ERP 登入":
# * Fill in all login information.
dropdown_element = driver.find_element(By.ID, "ddlLang")
dropdown_element = driver.find_element(By.ID, LANG_DROPDOWN_ID)
select = Select(dropdown_element)
select.select_by_value(login_info.lang)
input_text_element = driver.find_element(By.ID, "txtLoginID")
input_text_element = driver.find_element(By.ID, LOGIN_ID_INPUT_ID)
input_text_element.clear()
input_text_element.send_keys(login_info.login_ID)
input_text_element = driver.find_element(By.ID, "txtLoginPwd")
input_text_element = driver.find_element(By.ID, LOGIN_PWD_INPUT_ID)
input_text_element.clear()
input_text_element.send_keys(login_info.login_passwd)
dropdown_element = driver.find_element(By.ID, "ddlCompanyID")
dropdown_element = driver.find_element(By.ID, COMPANY_ID_DROPDOWN_ID)
select = Select(dropdown_element)
select.select_by_value(login_info.company_ID)
# * Press the submit button.
submit_btn = driver.find_element(By.ID, "btnLogin")
submit_btn = driver.find_element(By.ID, LOGIN_BUTTON_ID)
submit_btn.click()
time.sleep(1)
time.sleep(3)
# * Check if login failed (alert popup)
try:
alert = driver.switch_to.alert
print(f"Login error message: {alert.text}")
alert.accept()
return False # Skip remaining logic, login failed
except (NoAlertPresentException, NoSuchWindowException):
pass # No alert, proceed
time.sleep(2)
# * If login has pop a new window, switch main window to the new one.
login_page_handle = ""
main_page_handle = ""
for handle in driver.window_handles:
driver.switch_to.window(handle)
if "CHI MotorWeb - " in driver.title:
try:
title = driver.title
except UnexpectedAlertPresentException:
try:
alert = driver.switch_to.alert
print(f"Unexpected alert: {alert.text}")
alert.accept()
continue
except (NoAlertPresentException, NoSuchWindowException):
continue
if "CHI MotorWeb - " in title:
main_page_handle = handle
elif "CHI MOTOR WEB ERP 登入" in driver.title:
elif "CHI MOTOR WEB ERP 登入" in title:
login_page_handle = handle
# * get the page handle that should be stay
if main_page_handle:
stay_page_handle = main_page_handle
elif login_page_handle:
stay_page_handle = login_page_handle
else:
stay_page_handle = top_page
stay_page_handle = main_page_handle or login_page_handle or top_page
# * close unnecessary pages
for handle in driver.window_handles:
if handle != stay_page_handle:
driver.switch_to.window(handle)
driver.close()
try:
driver.switch_to.window(handle)
driver.close()
except Exception as e:
print(f"Error closing window {handle}: {e}")
driver.switch_to.window(stay_page_handle)
driver.maximize_window()
return True
elif "CHI MotorWeb - " in driver.title:
return True