tune-dms 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tune_dms/__init__.py +25 -0
- tune_dms/config.py +57 -0
- tune_dms/images/confirm_save_as_dialog.png +0 -0
- tune_dms/images/excel_application_screen.png +0 -0
- tune_dms/images/save_as_window.png +0 -0
- tune_dms/images/tune_admin_purchase_orders_icon_selected.png +0 -0
- tune_dms/images/tune_application_screen.png +0 -0
- tune_dms/images/tune_cancel_button.png +0 -0
- tune_dms/images/tune_cancel_button_2.png +0 -0
- tune_dms/images/tune_cancel_button_c.png +0 -0
- tune_dms/images/tune_exit_button.png +0 -0
- tune_dms/images/tune_login_error.png +0 -0
- tune_dms/images/tune_login_screen.png +0 -0
- tune_dms/images/tune_login_screen_environment.png +0 -0
- tune_dms/images/tune_part_general_enquiry_cap_qty.png +0 -0
- tune_dms/images/tune_part_general_enquiry_dangerous_goods_class.png +0 -0
- tune_dms/images/tune_part_general_enquiry_default_supplier.png +0 -0
- tune_dms/images/tune_part_general_enquiry_other.png +0 -0
- tune_dms/images/tune_part_general_enquiry_pack_size.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part_height.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part_length.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part_volume.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part_weight.png +0 -0
- tune_dms/images/tune_part_general_enquiry_part_width.png +0 -0
- tune_dms/images/tune_part_general_enquiry_unit_of_measure.png +0 -0
- tune_dms/images/tune_previous_button.png +0 -0
- tune_dms/images/tune_report_parts_by_bin_location.png +0 -0
- tune_dms/images/tune_report_parts_price_list.png +0 -0
- tune_dms/images/tune_search_field.png +0 -0
- tune_dms/images/tune_select_franchise.png +0 -0
- tune_dms/images/tune_select_franchise_grey.png +0 -0
- tune_dms/images/tune_select_franchise_toy_grey.png +0 -0
- tune_dms/images/tune_select_franchise_toy_grey_box.png +0 -0
- tune_dms/images/tune_select_franchise_toy_yellow.png +0 -0
- tune_dms/images/tune_select_franchise_toy_yellow_box.png +0 -0
- tune_dms/images/tune_supplier_part_selection.png +0 -0
- tune_dms/images/tune_x_button.png +0 -0
- tune_dms/launcher.py +942 -0
- tune_dms-0.1.0.dist-info/METADATA +24 -0
- tune_dms-0.1.0.dist-info/RECORD +43 -0
- tune_dms-0.1.0.dist-info/WHEEL +5 -0
- tune_dms-0.1.0.dist-info/top_level.txt +1 -0
tune_dms/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TUNE DMS: desktop GUI automation for login and report generation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from tune_dms.config import TuneConfig
|
|
6
|
+
from tune_dms.launcher import (
|
|
7
|
+
main as run_tune_reports,
|
|
8
|
+
TuneReportGenerator,
|
|
9
|
+
PartsPriceListParams,
|
|
10
|
+
PartsByBinLocationParams,
|
|
11
|
+
parts_price_list_report_download,
|
|
12
|
+
parts_by_bin_location_report_download,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"TuneConfig",
|
|
17
|
+
"run_tune_reports",
|
|
18
|
+
"TuneReportGenerator",
|
|
19
|
+
"PartsPriceListParams",
|
|
20
|
+
"PartsByBinLocationParams",
|
|
21
|
+
"parts_price_list_report_download",
|
|
22
|
+
"parts_by_bin_location_report_download",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
tune_dms/config.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for TUNE DMS (desktop GUI automation).
|
|
3
|
+
Users can set settings via environment variables or pass config programmatically.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_dotenv_if_available() -> None:
|
|
12
|
+
try:
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
load_dotenv()
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TuneConfig:
|
|
21
|
+
"""Configuration for TUNE DMS (desktop GUI automation)."""
|
|
22
|
+
|
|
23
|
+
user_id: str
|
|
24
|
+
password: str
|
|
25
|
+
shortcut_path: str = r"C:\Users\Public\Desktop\TUNE.lnk"
|
|
26
|
+
images_dir: Optional[str] = None
|
|
27
|
+
reports_dir: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_env(
|
|
31
|
+
cls,
|
|
32
|
+
*,
|
|
33
|
+
user_id: Optional[str] = None,
|
|
34
|
+
password: Optional[str] = None,
|
|
35
|
+
shortcut_path: Optional[str] = None,
|
|
36
|
+
images_dir: Optional[str] = None,
|
|
37
|
+
reports_dir: Optional[str] = None,
|
|
38
|
+
load_dotenv: bool = True,
|
|
39
|
+
) -> "TuneConfig":
|
|
40
|
+
"""Build config from environment variables. Override any field by passing it explicitly."""
|
|
41
|
+
if load_dotenv:
|
|
42
|
+
_load_dotenv_if_available()
|
|
43
|
+
return cls(
|
|
44
|
+
user_id=user_id or os.getenv("TUNE_USER_ID") or "",
|
|
45
|
+
password=password or os.getenv("TUNE_USER_PASSWORD") or "",
|
|
46
|
+
shortcut_path=shortcut_path or os.getenv("TUNE_SHORTCUT_PATH") or cls.shortcut_path,
|
|
47
|
+
images_dir=images_dir or os.getenv("TUNE_IMAGES_DIR"),
|
|
48
|
+
reports_dir=reports_dir or os.getenv("TUNE_REPORTS_DIR"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def validate(self) -> None:
|
|
52
|
+
"""Raise ValueError if required fields are missing."""
|
|
53
|
+
if not self.user_id or not self.password:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"TUNE_USER_ID and TUNE_USER_PASSWORD must be set "
|
|
56
|
+
"(via TuneConfig.from_env(), environment variables, or constructor)."
|
|
57
|
+
)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
tune_dms/launcher.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import pyautogui
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional, Literal
|
|
8
|
+
import traceback
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from tune_dms.config import TuneConfig
|
|
12
|
+
|
|
13
|
+
# Configure logging
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
17
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
18
|
+
)
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Set by main() so that login_to_tune, launch_tune_application, and waitFor use it
|
|
22
|
+
_config: Optional[TuneConfig] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_images_dir() -> str:
|
|
26
|
+
"""Return the images directory from config or package default (tune_dms/images/)."""
|
|
27
|
+
if _config and _config.images_dir:
|
|
28
|
+
return _config.images_dir
|
|
29
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
30
|
+
return os.path.join(pkg_dir, "images")
|
|
31
|
+
|
|
32
|
+
# Define dataclasses for report parameters
|
|
33
|
+
@dataclass
|
|
34
|
+
class PartsPriceListParams:
|
|
35
|
+
from_department: str
|
|
36
|
+
from_franchise: str
|
|
37
|
+
to_franchise: str = None # Default to same as from_franchise if None
|
|
38
|
+
from_bin: Optional[str] = None
|
|
39
|
+
to_bin: Optional[str] = None
|
|
40
|
+
price_1: Literal["List"] = "List"
|
|
41
|
+
price_2: Literal["Stock"] = "Stock"
|
|
42
|
+
include_gst: bool = True
|
|
43
|
+
output_file_type: Literal["CSV", "Excel"] = "CSV"
|
|
44
|
+
output_file_name: str = None # Will be set in the function
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class PartsByBinLocationParams:
|
|
48
|
+
from_department: str
|
|
49
|
+
to_department: Optional[str] = None # Default to same as from_department if None
|
|
50
|
+
from_franchise: str = "TOY"
|
|
51
|
+
to_franchise: Optional[str] = None # Default to same as from_franchise if None
|
|
52
|
+
from_bin: Optional[str] = None
|
|
53
|
+
to_bin: Optional[str] = None
|
|
54
|
+
from_movement_code: Optional[str] = None
|
|
55
|
+
to_movement_code: Optional[str] = None
|
|
56
|
+
show_stock_as: Literal["Physical Stock", "Available Stock"] = "Physical Stock"
|
|
57
|
+
print_when_stock_not_zero: bool = True
|
|
58
|
+
print_part_when_stock_is_zero: bool = False
|
|
59
|
+
print_part_when_stock_on_order_is_zero: bool = False
|
|
60
|
+
no_primary_bin_location: bool = False
|
|
61
|
+
no_alternate_bin_location: bool = False
|
|
62
|
+
no_primary_bin_but_has_alternate_bin: bool = False
|
|
63
|
+
has_both_primary_and_alternate_bin_location: bool = False
|
|
64
|
+
last_sale_before: Optional[str] = None
|
|
65
|
+
last_receipt_before: Optional[str] = None
|
|
66
|
+
print_average_cost: bool = False
|
|
67
|
+
report_format: Literal["Split departments onto new page", "Print all departments on one page"] = "Split departments onto new page"
|
|
68
|
+
output_file_type: Literal["CSV", "Excel"] = "CSV"
|
|
69
|
+
output_file_name: str = None # Will be set in the function
|
|
70
|
+
|
|
71
|
+
def waitFor(image_name, timeout=10, confidence=0.9):
|
|
72
|
+
"""
|
|
73
|
+
Wait for an image to appear on screen.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
image_name (str): Name of the image file in the images directory
|
|
77
|
+
timeout (int): Maximum time to wait in seconds
|
|
78
|
+
confidence (float): Confidence level for image matching (0-1)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
tuple or None: Position of the image if found, None otherwise
|
|
82
|
+
"""
|
|
83
|
+
image_path = os.path.join(_get_images_dir(), image_name)
|
|
84
|
+
logger.info(f"Waiting for image: {image_path}")
|
|
85
|
+
|
|
86
|
+
if not os.path.exists(image_path):
|
|
87
|
+
logger.error(f"Image file not found: {image_path}")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
|
|
92
|
+
while time.time() - start_time < timeout:
|
|
93
|
+
try:
|
|
94
|
+
position = pyautogui.locateOnScreen(image_path, confidence=confidence)
|
|
95
|
+
if position:
|
|
96
|
+
logger.info(f"Found image at position: {position}")
|
|
97
|
+
return position
|
|
98
|
+
except pyautogui.ImageNotFoundException:
|
|
99
|
+
pass
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error finding image: {e}")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
time.sleep(0.5)
|
|
105
|
+
|
|
106
|
+
logger.warning(f"Image not found after {timeout} seconds: {image_path}")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def open_parts_price_list_report(currently_selected_report: str = None):
|
|
110
|
+
"""
|
|
111
|
+
Navigate to and open the Parts Price List Report in TUNE
|
|
112
|
+
by using keyboard navigation
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
bool: True if the operation was attempted, False otherwise
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
logger.info("Attempting to open Parts Price List Report...")
|
|
119
|
+
|
|
120
|
+
if currently_selected_report == 'Parts By Bin Location':
|
|
121
|
+
for i in range(2):
|
|
122
|
+
pyautogui.press('up')
|
|
123
|
+
# Press Enter to select the menu item
|
|
124
|
+
pyautogui.press('enter')
|
|
125
|
+
logger.info("Opening the report configuration dialog")
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Press Down arrow key 6 times to Parts
|
|
130
|
+
for i in range(6):
|
|
131
|
+
pyautogui.press('down')
|
|
132
|
+
|
|
133
|
+
# Open Parts folder
|
|
134
|
+
pyautogui.press('right')
|
|
135
|
+
|
|
136
|
+
# Press Down arrow key 1 time to Parts
|
|
137
|
+
for i in range(5):
|
|
138
|
+
pyautogui.press('down')
|
|
139
|
+
|
|
140
|
+
# Open reports folder
|
|
141
|
+
pyautogui.press('right')
|
|
142
|
+
|
|
143
|
+
# Open Parts Price List Report
|
|
144
|
+
for i in range(16):
|
|
145
|
+
pyautogui.press('down')
|
|
146
|
+
|
|
147
|
+
# Press Enter to select the menu item
|
|
148
|
+
pyautogui.press('enter')
|
|
149
|
+
logger.info("Opening the report configuration dialog")
|
|
150
|
+
|
|
151
|
+
# Wait for the report configuration dialog to open
|
|
152
|
+
report_configuration_dialog = waitFor('tune_report_parts_price_list.png')
|
|
153
|
+
if not report_configuration_dialog:
|
|
154
|
+
logger.error("Report configuration dialog not found")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
logger.info("Parts Price Pist Peport navigation sequence executed successfully")
|
|
158
|
+
return True
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error while opening Parts Price List Report: {e}")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def parts_price_list_report_download(params: PartsPriceListParams = None, reports_dir: str = None, **kwargs):
|
|
164
|
+
"""
|
|
165
|
+
Download the Parts Price List Report with user-friendly parameter options.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
params: A PartsPriceListParams object containing all parameters
|
|
169
|
+
reports_dir: Directory where reports will be saved
|
|
170
|
+
**kwargs: Individual parameters that override those in params (if provided)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
bool: True if successful, False otherwise
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
# Create reports directory if it doesn't exist
|
|
177
|
+
if reports_dir:
|
|
178
|
+
os.makedirs(reports_dir, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
# Allow creating params from kwargs or updating the provided params
|
|
181
|
+
if params is None:
|
|
182
|
+
params = PartsPriceListParams(**kwargs)
|
|
183
|
+
elif kwargs:
|
|
184
|
+
# Update params with any provided kwargs
|
|
185
|
+
for key, value in kwargs.items():
|
|
186
|
+
if hasattr(params, key):
|
|
187
|
+
setattr(params, key, value)
|
|
188
|
+
|
|
189
|
+
# Set output file name if not provided
|
|
190
|
+
if params.output_file_name is None and reports_dir:
|
|
191
|
+
params.output_file_name = "parts_price_list_report.csv"
|
|
192
|
+
|
|
193
|
+
# Set output file path
|
|
194
|
+
params.output_file_path = os.path.join(reports_dir, params.output_file_name)
|
|
195
|
+
|
|
196
|
+
# Auto-populate to_franchise if not provided
|
|
197
|
+
if params.to_franchise is None:
|
|
198
|
+
params.to_franchise = params.from_franchise
|
|
199
|
+
|
|
200
|
+
logger.info(f"Downloading Parts Price List Report with parameters: {params}")
|
|
201
|
+
|
|
202
|
+
# Start navigating the form
|
|
203
|
+
pyautogui.press('tab')
|
|
204
|
+
pyautogui.write(params.from_department)
|
|
205
|
+
|
|
206
|
+
pyautogui.press('tab')
|
|
207
|
+
pyautogui.write(params.from_franchise)
|
|
208
|
+
|
|
209
|
+
pyautogui.press('tab')
|
|
210
|
+
pyautogui.write(params.to_franchise)
|
|
211
|
+
|
|
212
|
+
pyautogui.press('tab')
|
|
213
|
+
if params.from_bin:
|
|
214
|
+
pyautogui.write(params.from_bin)
|
|
215
|
+
|
|
216
|
+
pyautogui.press('tab')
|
|
217
|
+
if params.to_bin:
|
|
218
|
+
pyautogui.write(params.to_bin)
|
|
219
|
+
|
|
220
|
+
pyautogui.press('tab')
|
|
221
|
+
if params.price_1 == 'List':
|
|
222
|
+
for i in range(2):
|
|
223
|
+
pyautogui.press('down')
|
|
224
|
+
else:
|
|
225
|
+
logger.error(f"Invalid price type: {params.price_1}")
|
|
226
|
+
|
|
227
|
+
pyautogui.press('tab')
|
|
228
|
+
if params.price_2 == 'Stock':
|
|
229
|
+
for i in range(25):
|
|
230
|
+
pyautogui.press('down')
|
|
231
|
+
pyautogui.press('enter')
|
|
232
|
+
else:
|
|
233
|
+
logger.error(f"Invalid price type: {params.price_2}")
|
|
234
|
+
|
|
235
|
+
pyautogui.press('tab')
|
|
236
|
+
if params.include_gst:
|
|
237
|
+
pyautogui.press('space')
|
|
238
|
+
pyautogui.press('tab')
|
|
239
|
+
if params.include_gst:
|
|
240
|
+
pyautogui.press('space')
|
|
241
|
+
|
|
242
|
+
pyautogui.press('o')
|
|
243
|
+
|
|
244
|
+
if params.output_file_type == 'CSV':
|
|
245
|
+
for i in range(4):
|
|
246
|
+
pyautogui.press('down')
|
|
247
|
+
elif params.output_file_type == 'Excel':
|
|
248
|
+
for i in range(5):
|
|
249
|
+
pyautogui.press('down')
|
|
250
|
+
else:
|
|
251
|
+
logger.error(f"Invalid output file type: {params.output_file_type}")
|
|
252
|
+
|
|
253
|
+
time.sleep(0.1)
|
|
254
|
+
|
|
255
|
+
pyautogui.press('o')
|
|
256
|
+
save_as_window = waitFor('save_as_window.png', timeout=15)
|
|
257
|
+
if not save_as_window:
|
|
258
|
+
logger.error("Save as window not found")
|
|
259
|
+
return False
|
|
260
|
+
pyautogui.write(params.output_file_path)
|
|
261
|
+
pyautogui.press('enter')
|
|
262
|
+
|
|
263
|
+
# Overwrite the file if it already exists
|
|
264
|
+
confirm_save_as_dialog = waitFor('confirm_save_as_dialog.png')
|
|
265
|
+
if confirm_save_as_dialog:
|
|
266
|
+
pyautogui.press('left')
|
|
267
|
+
pyautogui.press('enter')
|
|
268
|
+
|
|
269
|
+
# Start timing the report generation
|
|
270
|
+
start_time = time.time()
|
|
271
|
+
logger.info("Starting report generation...")
|
|
272
|
+
# Wait for report to finish
|
|
273
|
+
report_success = waitFor('tune_application_screen.png', timeout=80)
|
|
274
|
+
# Calculate and log the duration
|
|
275
|
+
end_time = time.time()
|
|
276
|
+
duration = round(end_time - start_time, 2)
|
|
277
|
+
if not report_success:
|
|
278
|
+
logger.error(f"Report FAILED after {duration} seconds")
|
|
279
|
+
else:
|
|
280
|
+
logger.info(f"Report generation completed in {duration} seconds")
|
|
281
|
+
|
|
282
|
+
# Verify the file exists and has content
|
|
283
|
+
if not os.path.exists(params.output_file_path):
|
|
284
|
+
logger.error(f"Report file was not created: {params.output_file_path}")
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
# Wait a moment for file to be fully written
|
|
288
|
+
time.sleep(1)
|
|
289
|
+
|
|
290
|
+
# Check file size
|
|
291
|
+
file_size = os.path.getsize(params.output_file_path)
|
|
292
|
+
if file_size == 0:
|
|
293
|
+
logger.error(f"Report file is empty: {params.output_file_path}")
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
logger.info(f"Parts Price List Report downloaded successfully (size: {file_size} bytes)")
|
|
297
|
+
|
|
298
|
+
'''
|
|
299
|
+
# Check if user clicked and report opened
|
|
300
|
+
report_open = waitFor('tune_admin_purchase_orders_icon_selected.png', timeout=15)
|
|
301
|
+
if report_open:
|
|
302
|
+
logger.info("Report is open, closing it now")
|
|
303
|
+
# Find and click on the "Previous" button to close the report
|
|
304
|
+
previous_button = waitFor('tune_work_with_purchase_orders.png')
|
|
305
|
+
if previous_button:
|
|
306
|
+
pyautogui.click(pyautogui.center(previous_button))
|
|
307
|
+
logger.info("Clicked on Previous button")
|
|
308
|
+
time.sleep(0.5)
|
|
309
|
+
logger.info("Report closed")
|
|
310
|
+
'''
|
|
311
|
+
|
|
312
|
+
return True
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Error while downloading Parts Price List Report: {e}")
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def open_parts_by_bin_location_report():
|
|
318
|
+
"""
|
|
319
|
+
Navigate to and open the Parts by Bin Location Report in TUNE
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
logger.info("Attempting to open Parts by Bin Location Report...")
|
|
323
|
+
|
|
324
|
+
# Press Down arrow key 2 times to Parts
|
|
325
|
+
for i in range(2):
|
|
326
|
+
pyautogui.press('down')
|
|
327
|
+
|
|
328
|
+
# Press Enter to select the menu item
|
|
329
|
+
pyautogui.press('enter')
|
|
330
|
+
|
|
331
|
+
# Wait for the report configuration dialog to open
|
|
332
|
+
report_configuration_dialog = waitFor('tune_report_parts_by_bin_location.png')
|
|
333
|
+
if not report_configuration_dialog:
|
|
334
|
+
logger.error("Report configuration dialog not found")
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
logger.info("Parts by Bin Location Report opened successfully")
|
|
338
|
+
return True
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Error while opening Parts by Bin Location Report: {e}")
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
def parts_by_bin_location_report_download(params: PartsByBinLocationParams = None, reports_dir: str = None, **kwargs):
|
|
344
|
+
"""
|
|
345
|
+
Download the Parts by Bin Location Report with user-friendly parameter options.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
params: A PartsByBinLocationParams object containing all parameters
|
|
349
|
+
reports_dir: Directory where reports will be saved
|
|
350
|
+
**kwargs: Individual parameters that override those in params (if provided)
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
bool: True if successful, False otherwise
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
# Create reports directory if it doesn't exist
|
|
357
|
+
if reports_dir:
|
|
358
|
+
os.makedirs(reports_dir, exist_ok=True)
|
|
359
|
+
|
|
360
|
+
# Allow creating params from kwargs or updating the provided params
|
|
361
|
+
if params is None:
|
|
362
|
+
params = PartsByBinLocationParams(**kwargs)
|
|
363
|
+
elif kwargs:
|
|
364
|
+
# Update params with any provided kwargs
|
|
365
|
+
for key, value in kwargs.items():
|
|
366
|
+
if hasattr(params, key):
|
|
367
|
+
setattr(params, key, value)
|
|
368
|
+
|
|
369
|
+
# Set output file name if not provided
|
|
370
|
+
if params.output_file_name is None and reports_dir:
|
|
371
|
+
params.output_file_name = "parts_by_bin_location_report.csv"
|
|
372
|
+
|
|
373
|
+
# Set output file path
|
|
374
|
+
params.output_file_path = os.path.join(reports_dir, params.output_file_name)
|
|
375
|
+
|
|
376
|
+
# Auto-populate defaults if not provided
|
|
377
|
+
if params.to_department is None:
|
|
378
|
+
params.to_department = params.from_department
|
|
379
|
+
|
|
380
|
+
if params.to_franchise is None:
|
|
381
|
+
params.to_franchise = params.from_franchise
|
|
382
|
+
|
|
383
|
+
logger.info(f"Downloading Parts by Bin Location Report with parameters: {params}")
|
|
384
|
+
|
|
385
|
+
# Tab to Department
|
|
386
|
+
pyautogui.write(params.from_department)
|
|
387
|
+
|
|
388
|
+
# Tab to To Department
|
|
389
|
+
pyautogui.press('tab')
|
|
390
|
+
pyautogui.write(params.to_department)
|
|
391
|
+
|
|
392
|
+
# Tab to Franchise
|
|
393
|
+
pyautogui.press('tab')
|
|
394
|
+
pyautogui.write(params.from_franchise)
|
|
395
|
+
|
|
396
|
+
# Tab to To Franchise
|
|
397
|
+
pyautogui.press('tab')
|
|
398
|
+
pyautogui.write(params.to_franchise)
|
|
399
|
+
|
|
400
|
+
# Tab to From Bin
|
|
401
|
+
pyautogui.press('tab')
|
|
402
|
+
if params.from_bin:
|
|
403
|
+
pyautogui.write(params.from_bin)
|
|
404
|
+
|
|
405
|
+
# Tab to To Bin
|
|
406
|
+
pyautogui.press('tab')
|
|
407
|
+
if params.to_bin:
|
|
408
|
+
pyautogui.write(params.to_bin)
|
|
409
|
+
|
|
410
|
+
# Tab to From Movement Code
|
|
411
|
+
pyautogui.press('tab')
|
|
412
|
+
if params.from_movement_code:
|
|
413
|
+
pyautogui.write(params.from_movement_code)
|
|
414
|
+
|
|
415
|
+
# Tab to To Movement Code
|
|
416
|
+
pyautogui.press('tab')
|
|
417
|
+
if params.to_movement_code:
|
|
418
|
+
pyautogui.write(params.to_movement_code)
|
|
419
|
+
|
|
420
|
+
# Tab to Show Stock As
|
|
421
|
+
pyautogui.press('tab')
|
|
422
|
+
if params.show_stock_as == "Physical Stock":
|
|
423
|
+
logger.info("Physical Stock selected")
|
|
424
|
+
elif params.show_stock_as == "Available Stock":
|
|
425
|
+
pyautogui.press('down')
|
|
426
|
+
else:
|
|
427
|
+
logger.error(f"Invalid show stock as option: {params.show_stock_as}")
|
|
428
|
+
|
|
429
|
+
# Tab to Print When Stock Not Zero
|
|
430
|
+
pyautogui.press('tab')
|
|
431
|
+
if params.print_when_stock_not_zero:
|
|
432
|
+
logger.info("Print when stock not zero checked")
|
|
433
|
+
|
|
434
|
+
# Tab to Print part when stock is Zero
|
|
435
|
+
pyautogui.press('tab')
|
|
436
|
+
if params.print_part_when_stock_is_zero:
|
|
437
|
+
logger.info("Print part when stock is Zero checked")
|
|
438
|
+
|
|
439
|
+
# Tab to Print part when STOCK ON ORDER is ZERO
|
|
440
|
+
pyautogui.press('tab')
|
|
441
|
+
if params.print_part_when_stock_on_order_is_zero:
|
|
442
|
+
logger.info("Print part when stock on order is Zero checked")
|
|
443
|
+
|
|
444
|
+
# Tab to NO PRIMARY bin location
|
|
445
|
+
pyautogui.press('tab')
|
|
446
|
+
if params.no_primary_bin_location:
|
|
447
|
+
logger.info("No primary bin location checked")
|
|
448
|
+
|
|
449
|
+
# Tab to NO ALTERNATE bin location
|
|
450
|
+
pyautogui.press('tab')
|
|
451
|
+
if params.no_alternate_bin_location:
|
|
452
|
+
logger.info("No alternate bin location checked")
|
|
453
|
+
|
|
454
|
+
# Tab to NO PRIMARY bin, but has an ALTERNATE bin location
|
|
455
|
+
pyautogui.press('tab')
|
|
456
|
+
if params.no_primary_bin_but_has_alternate_bin:
|
|
457
|
+
logger.info("No primary bin, but has an alternate bin checked")
|
|
458
|
+
|
|
459
|
+
# Tab to has BOTH a PRIMARY and ALTERNATE bin location
|
|
460
|
+
pyautogui.press('tab')
|
|
461
|
+
if params.has_both_primary_and_alternate_bin_location:
|
|
462
|
+
logger.info("Has both a primary and alternate bin location checked")
|
|
463
|
+
|
|
464
|
+
# Tab to Last Sale Before (Date)
|
|
465
|
+
pyautogui.press('tab')
|
|
466
|
+
if params.last_sale_before:
|
|
467
|
+
logger.info("Last sale before checked")
|
|
468
|
+
|
|
469
|
+
# Tab to Last Receipt Before Date
|
|
470
|
+
pyautogui.press('tab')
|
|
471
|
+
if params.last_receipt_before:
|
|
472
|
+
logger.info("Last receipt before checked")
|
|
473
|
+
|
|
474
|
+
# Tab to Print Average Cost
|
|
475
|
+
pyautogui.press('tab')
|
|
476
|
+
if params.print_average_cost:
|
|
477
|
+
pyautogui.press('space')
|
|
478
|
+
logger.info("Print average cost checked")
|
|
479
|
+
|
|
480
|
+
# Tab to Report format
|
|
481
|
+
#pyautogui.press('tab')
|
|
482
|
+
#if params.report_format == 'Split departments onto new page':
|
|
483
|
+
# logger.info("Split departments onto new page")
|
|
484
|
+
#elif params.report_format == 'Print all departments on one page':
|
|
485
|
+
# logger.info("Print all departments on one page")
|
|
486
|
+
#else:
|
|
487
|
+
# logger.error(f"Invalid report format: {params.report_format}")
|
|
488
|
+
|
|
489
|
+
# Tab to OK
|
|
490
|
+
#pyautogui.press('tab')
|
|
491
|
+
#pyautogui.press('enter')
|
|
492
|
+
pyautogui.press('o')
|
|
493
|
+
time.sleep(0.1)
|
|
494
|
+
# Select output type
|
|
495
|
+
if params.output_file_type == 'CSV':
|
|
496
|
+
for i in range(4):
|
|
497
|
+
pyautogui.press('down')
|
|
498
|
+
elif params.output_file_type == 'Excel':
|
|
499
|
+
for i in range(5):
|
|
500
|
+
pyautogui.press('down')
|
|
501
|
+
else:
|
|
502
|
+
logger.error(f"Invalid output file type: {params.output_file_type}")
|
|
503
|
+
|
|
504
|
+
# Select Output
|
|
505
|
+
pyautogui.press('o')
|
|
506
|
+
save_as_window = waitFor('save_as_window.png', timeout=15)
|
|
507
|
+
if not save_as_window:
|
|
508
|
+
logger.error("Save as window not found")
|
|
509
|
+
return False
|
|
510
|
+
pyautogui.write(params.output_file_path)
|
|
511
|
+
pyautogui.press('enter')
|
|
512
|
+
|
|
513
|
+
# Overwrite the file if it already exists
|
|
514
|
+
confirm_save_as_dialog = waitFor('confirm_save_as_dialog.png')
|
|
515
|
+
if confirm_save_as_dialog:
|
|
516
|
+
pyautogui.press('left')
|
|
517
|
+
pyautogui.press('enter')
|
|
518
|
+
|
|
519
|
+
# Start timing the report generation
|
|
520
|
+
start_time = time.time()
|
|
521
|
+
logger.info("Starting report generation...")
|
|
522
|
+
# Wait for report to finish
|
|
523
|
+
report_success = waitFor('tune_application_screen.png', timeout=60)
|
|
524
|
+
# Calculate and log the duration
|
|
525
|
+
end_time = time.time()
|
|
526
|
+
duration = round(end_time - start_time, 2)
|
|
527
|
+
if not report_success:
|
|
528
|
+
logger.error(f"Report generation failed or timed out after {duration} seconds")
|
|
529
|
+
return False
|
|
530
|
+
else:
|
|
531
|
+
logger.info(f"Report generation completed in {duration} seconds")
|
|
532
|
+
|
|
533
|
+
# Verify the file exists and has content
|
|
534
|
+
if not os.path.exists(params.output_file_path):
|
|
535
|
+
logger.error(f"Report file was not created: {params.output_file_path}")
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
# Wait a moment for file to be fully written
|
|
539
|
+
time.sleep(1)
|
|
540
|
+
|
|
541
|
+
# Check file size
|
|
542
|
+
file_size = os.path.getsize(params.output_file_path)
|
|
543
|
+
if file_size == 0:
|
|
544
|
+
logger.error(f"Report file is empty: {params.output_file_path}")
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
logger.info(f"Parts by Bin Location Report downloaded successfully (size: {file_size} bytes)")
|
|
548
|
+
return True
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.error(f"Error while downloading Parts by Bin Location Report: {e}")
|
|
551
|
+
return False
|
|
552
|
+
|
|
553
|
+
def close_tune_application():
|
|
554
|
+
"""
|
|
555
|
+
Close TUNE application using Alt, Down, E keyboard shortcut
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
bool: True if the operation was attempted, False otherwise
|
|
559
|
+
"""
|
|
560
|
+
try:
|
|
561
|
+
logger.info("Attempting to close TUNE application...")
|
|
562
|
+
|
|
563
|
+
# Press Alt key
|
|
564
|
+
pyautogui.press('alt')
|
|
565
|
+
time.sleep(0.3)
|
|
566
|
+
|
|
567
|
+
# Press Down arrow key
|
|
568
|
+
pyautogui.press('down')
|
|
569
|
+
time.sleep(0.3)
|
|
570
|
+
|
|
571
|
+
# Press E key
|
|
572
|
+
pyautogui.press('e')
|
|
573
|
+
|
|
574
|
+
logger.info("TUNE close sequence executed successfully")
|
|
575
|
+
return True
|
|
576
|
+
except Exception as e:
|
|
577
|
+
logger.error(f"Error while closing TUNE: {e}")
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
def launch_tune_application():
|
|
581
|
+
"""Launch the TUNE software application using the shortcut (path from config)."""
|
|
582
|
+
global _config
|
|
583
|
+
if not _config:
|
|
584
|
+
raise RuntimeError("TUNE config not set. Call run_tune_reports(config) first.")
|
|
585
|
+
logger.info("Launching TUNE software using shortcut...")
|
|
586
|
+
shortcut_path = _config.shortcut_path
|
|
587
|
+
try:
|
|
588
|
+
if not os.path.exists(shortcut_path):
|
|
589
|
+
logger.error(f"TUNE shortcut not found at {shortcut_path}")
|
|
590
|
+
raise FileNotFoundError(f"TUNE shortcut not found at {shortcut_path}")
|
|
591
|
+
|
|
592
|
+
# Start the TUNE process using the shortcut
|
|
593
|
+
process = subprocess.Popen(f'start "" "{shortcut_path}"', shell=True)
|
|
594
|
+
logger.info("TUNE process started using shortcut")
|
|
595
|
+
return process
|
|
596
|
+
except Exception as e:
|
|
597
|
+
logger.error(f"Failed to launch TUNE: {e}")
|
|
598
|
+
raise
|
|
599
|
+
|
|
600
|
+
def login_to_tune():
|
|
601
|
+
"""Login to TUNE using credentials from config (set by run_tune_reports)."""
|
|
602
|
+
global _config
|
|
603
|
+
if not _config:
|
|
604
|
+
raise RuntimeError("TUNE config not set. Call run_tune_reports(config) first.")
|
|
605
|
+
logger.info("Attempting to login to TUNE...")
|
|
606
|
+
login_screen = waitFor('tune_login_screen.png')
|
|
607
|
+
if not login_screen:
|
|
608
|
+
logger.error("Login screen not found")
|
|
609
|
+
return False
|
|
610
|
+
pyautogui.click(pyautogui.center(login_screen))
|
|
611
|
+
time.sleep(0.5)
|
|
612
|
+
if waitFor('tune_login_screen_environment.png', timeout=1):
|
|
613
|
+
logger.info("TUNE Environment not selected, selecting")
|
|
614
|
+
time.sleep(0.2)
|
|
615
|
+
pyautogui.press('e')
|
|
616
|
+
time.sleep(0.2)
|
|
617
|
+
pyautogui.press('down')
|
|
618
|
+
time.sleep(0.2)
|
|
619
|
+
pyautogui.press('tab')
|
|
620
|
+
time.sleep(0.5)
|
|
621
|
+
pyautogui.write(_config.user_id)
|
|
622
|
+
logger.info("Entered User ID")
|
|
623
|
+
time.sleep(0.5)
|
|
624
|
+
pyautogui.press('tab')
|
|
625
|
+
time.sleep(0.5)
|
|
626
|
+
pyautogui.write(_config.password)
|
|
627
|
+
logger.info("Entered Password")
|
|
628
|
+
time.sleep(0.5)
|
|
629
|
+
|
|
630
|
+
# Press Enter to submit login
|
|
631
|
+
pyautogui.press('enter')
|
|
632
|
+
logger.info("Submitted login form")
|
|
633
|
+
|
|
634
|
+
# Check if login failed
|
|
635
|
+
login_error = waitFor('tune_login_error.png', timeout=3)
|
|
636
|
+
if login_error:
|
|
637
|
+
logger.error("Login failed")
|
|
638
|
+
time.sleep(2)
|
|
639
|
+
pyautogui.press('space')
|
|
640
|
+
return False
|
|
641
|
+
|
|
642
|
+
# Wait for the application screen to appear
|
|
643
|
+
app_screen = waitFor('tune_application_screen.png', timeout=30) # Longer timeout for application loading
|
|
644
|
+
if not app_screen:
|
|
645
|
+
logger.error("Application screen not found after login")
|
|
646
|
+
return False
|
|
647
|
+
|
|
648
|
+
logger.info("Successfully logged into TUNE")
|
|
649
|
+
return True
|
|
650
|
+
|
|
651
|
+
class TuneReportGenerator:
|
|
652
|
+
"""Generates reports from the TUNE system."""
|
|
653
|
+
|
|
654
|
+
def __init__(self, config: TuneConfig):
|
|
655
|
+
self.config = config
|
|
656
|
+
|
|
657
|
+
def run_reports(self) -> bool:
|
|
658
|
+
"""Run the TUNE launcher to download the required reports."""
|
|
659
|
+
logger.info("Starting TUNE report generation...")
|
|
660
|
+
reports_dir = self.config.reports_dir or os.getcwd()
|
|
661
|
+
try:
|
|
662
|
+
# Launch TUNE
|
|
663
|
+
launch_tune_application()
|
|
664
|
+
time.sleep(3)
|
|
665
|
+
# Login to TUNE
|
|
666
|
+
login_success = login_to_tune()
|
|
667
|
+
if not login_success:
|
|
668
|
+
logger.error("Failed to login to TUNE. Exiting.")
|
|
669
|
+
return False
|
|
670
|
+
time.sleep(2)
|
|
671
|
+
# Select 130 Department - MCT Parts and Accessories
|
|
672
|
+
for i in range(3):
|
|
673
|
+
pyautogui.press('tab')
|
|
674
|
+
time.sleep(0.1)
|
|
675
|
+
for i in range(6):
|
|
676
|
+
pyautogui.press('up')
|
|
677
|
+
time.sleep(0.1)
|
|
678
|
+
|
|
679
|
+
# Move to menu
|
|
680
|
+
for i in range(5):
|
|
681
|
+
pyautogui.press('tab')
|
|
682
|
+
|
|
683
|
+
# Generate Parts Price List Report
|
|
684
|
+
logger.info("Generating Parts Price List Report...")
|
|
685
|
+
report_success = open_parts_price_list_report()
|
|
686
|
+
if report_success:
|
|
687
|
+
# Configure report parameters
|
|
688
|
+
params = PartsPriceListParams(
|
|
689
|
+
from_department='130',
|
|
690
|
+
from_franchise='TOY',
|
|
691
|
+
output_file_name='Parts Price List Report - 130.csv'
|
|
692
|
+
)
|
|
693
|
+
# Download the report
|
|
694
|
+
download_success = parts_price_list_report_download(params, reports_dir=reports_dir)
|
|
695
|
+
if not download_success:
|
|
696
|
+
logger.error("Failed to download Parts Price List Report")
|
|
697
|
+
close_tune_application()
|
|
698
|
+
return False
|
|
699
|
+
logger.info("Parts Price List Report generated successfully")
|
|
700
|
+
else:
|
|
701
|
+
logger.error("Failed to open Parts Price List Report")
|
|
702
|
+
close_tune_application()
|
|
703
|
+
return False
|
|
704
|
+
|
|
705
|
+
# Generate Parts by Bin Location Report
|
|
706
|
+
logger.info("Generating Parts by Bin Location Report...")
|
|
707
|
+
report_success = open_parts_by_bin_location_report()
|
|
708
|
+
if report_success:
|
|
709
|
+
# Configure report parameters
|
|
710
|
+
params = PartsByBinLocationParams(
|
|
711
|
+
from_department='130',
|
|
712
|
+
from_franchise='TOY',
|
|
713
|
+
show_stock_as='Available Stock',
|
|
714
|
+
print_when_stock_not_zero=True,
|
|
715
|
+
output_file_name='Parts by Bin Location Report - 130.csv'
|
|
716
|
+
)
|
|
717
|
+
# Download the report
|
|
718
|
+
download_success = parts_by_bin_location_report_download(params, reports_dir=reports_dir)
|
|
719
|
+
if not download_success:
|
|
720
|
+
logger.error("Failed to download Parts by Bin Location Report")
|
|
721
|
+
close_tune_application()
|
|
722
|
+
return False
|
|
723
|
+
logger.info("Parts by Bin Location Report generated successfully")
|
|
724
|
+
else:
|
|
725
|
+
logger.error("Failed to open Parts by Bin Location Report")
|
|
726
|
+
close_tune_application()
|
|
727
|
+
return False
|
|
728
|
+
|
|
729
|
+
# Select 145 Department - MCT Tyres
|
|
730
|
+
for i in range(5): # Go to department menu
|
|
731
|
+
pyautogui.hotkey('shift', 'tab')
|
|
732
|
+
time.sleep(0.1)
|
|
733
|
+
for i in range(3): # Select 145
|
|
734
|
+
pyautogui.press('down')
|
|
735
|
+
time.sleep(0.1)
|
|
736
|
+
|
|
737
|
+
# Move to menu
|
|
738
|
+
for i in range(5):
|
|
739
|
+
pyautogui.press('tab')
|
|
740
|
+
|
|
741
|
+
# Generate Tyres Price List Report
|
|
742
|
+
logger.info("Generating Tyres Price List Report...")
|
|
743
|
+
report_success = open_parts_price_list_report(currently_selected_report='Parts By Bin Location')
|
|
744
|
+
if report_success:
|
|
745
|
+
# Configure report parameters
|
|
746
|
+
params = PartsPriceListParams(
|
|
747
|
+
from_department='145',
|
|
748
|
+
from_franchise='TOY',
|
|
749
|
+
output_file_name='Parts Price List Report - 145.csv'
|
|
750
|
+
)
|
|
751
|
+
# Download the report
|
|
752
|
+
download_success = parts_price_list_report_download(params, reports_dir=reports_dir)
|
|
753
|
+
if not download_success:
|
|
754
|
+
logger.error("Failed to download Tyres Price List Report")
|
|
755
|
+
close_tune_application()
|
|
756
|
+
return False
|
|
757
|
+
logger.info("Tyres Price List Report generated successfully")
|
|
758
|
+
else:
|
|
759
|
+
logger.error("Failed to open Tyres Price List Report")
|
|
760
|
+
close_tune_application()
|
|
761
|
+
return False
|
|
762
|
+
|
|
763
|
+
# Generate Tyres by Bin Location Report
|
|
764
|
+
logger.info("Generating Tyres by Bin Location Report...")
|
|
765
|
+
report_success = open_parts_by_bin_location_report()
|
|
766
|
+
if report_success:
|
|
767
|
+
# Configure report parameters
|
|
768
|
+
params = PartsByBinLocationParams(
|
|
769
|
+
from_department='145',
|
|
770
|
+
from_franchise='TOY',
|
|
771
|
+
output_file_name='Parts by Bin Location Report - 145.csv'
|
|
772
|
+
)
|
|
773
|
+
# Download the report
|
|
774
|
+
download_success = parts_by_bin_location_report_download(params, reports_dir=reports_dir)
|
|
775
|
+
if not download_success:
|
|
776
|
+
logger.error("Failed to download Tyres by Bin Location Report")
|
|
777
|
+
close_tune_application()
|
|
778
|
+
return False
|
|
779
|
+
logger.info("Tyres by Bin Location Report generated successfully")
|
|
780
|
+
else:
|
|
781
|
+
logger.error("Failed to open Tyres by Bin Location Report")
|
|
782
|
+
close_tune_application()
|
|
783
|
+
return False
|
|
784
|
+
|
|
785
|
+
# Select 330 Department - Ingham Toyota
|
|
786
|
+
for i in range(5): # Go to department menu
|
|
787
|
+
pyautogui.hotkey('shift', 'tab')
|
|
788
|
+
time.sleep(0.1)
|
|
789
|
+
for i in range(7): # Select 330
|
|
790
|
+
pyautogui.press('down')
|
|
791
|
+
time.sleep(0.1)
|
|
792
|
+
|
|
793
|
+
# Move to menu
|
|
794
|
+
for i in range(5):
|
|
795
|
+
pyautogui.press('tab')
|
|
796
|
+
|
|
797
|
+
# Generate Ingham Toyota Parts Price List Report
|
|
798
|
+
logger.info("Generating Ingham Toyota Parts Price List Report...")
|
|
799
|
+
report_success = open_parts_price_list_report(currently_selected_report='Parts By Bin Location')
|
|
800
|
+
if report_success:
|
|
801
|
+
# Configure report parameters
|
|
802
|
+
params = PartsPriceListParams(
|
|
803
|
+
from_department='330',
|
|
804
|
+
from_franchise='TOY',
|
|
805
|
+
output_file_name='Parts Price List Report - 330.csv'
|
|
806
|
+
)
|
|
807
|
+
# Download the report
|
|
808
|
+
download_success = parts_price_list_report_download(params, reports_dir=reports_dir)
|
|
809
|
+
if not download_success:
|
|
810
|
+
logger.error("Failed to download Ingham Toyota Parts Price List Report")
|
|
811
|
+
close_tune_application()
|
|
812
|
+
return False
|
|
813
|
+
logger.info("Ingham Toyota Parts Price List Report generated successfully")
|
|
814
|
+
else:
|
|
815
|
+
logger.error("Failed to open Ingham Toyota Parts Price List Report")
|
|
816
|
+
close_tune_application()
|
|
817
|
+
return False
|
|
818
|
+
|
|
819
|
+
# Generate Ingham Toyota Parts by Bin Location Report
|
|
820
|
+
logger.info("Generating Ingham Toyota Parts by Bin Location Report...")
|
|
821
|
+
report_success = open_parts_by_bin_location_report()
|
|
822
|
+
if report_success:
|
|
823
|
+
# Configure report parameters
|
|
824
|
+
params = PartsByBinLocationParams(
|
|
825
|
+
from_department='330',
|
|
826
|
+
from_franchise='TOY',
|
|
827
|
+
output_file_name='Parts by Bin Location Report - 330.csv'
|
|
828
|
+
)
|
|
829
|
+
# Download the report
|
|
830
|
+
download_success = parts_by_bin_location_report_download(params, reports_dir=reports_dir)
|
|
831
|
+
if not download_success:
|
|
832
|
+
logger.error("Failed to download Ingham Toyota Parts by Bin Location Report")
|
|
833
|
+
close_tune_application()
|
|
834
|
+
return False
|
|
835
|
+
logger.info("Ingham Toyota Parts by Bin Location Report generated successfully")
|
|
836
|
+
else:
|
|
837
|
+
logger.error("Failed to open Ingham Toyota Parts by Bin Location Report")
|
|
838
|
+
close_tune_application()
|
|
839
|
+
|
|
840
|
+
# Select Company 04 - Charters Towers Partnership
|
|
841
|
+
for i in range(7): # Go to company menu
|
|
842
|
+
pyautogui.hotkey('shift', 'tab')
|
|
843
|
+
pyautogui.press('down')
|
|
844
|
+
logger.info("Selected Company 04 - Charters Towers Partnership")
|
|
845
|
+
time.sleep(2) # Wait for 'Updating' to finish
|
|
846
|
+
# Department 430 - Parts & Acc selected by default
|
|
847
|
+
|
|
848
|
+
# Move to menu
|
|
849
|
+
for i in range(7):
|
|
850
|
+
pyautogui.press('tab')
|
|
851
|
+
|
|
852
|
+
# Generate Charters Towers Partnership Parts Price List Report
|
|
853
|
+
logger.info("Generating Charters Towers Partnership Parts Price List Report...")
|
|
854
|
+
report_success = open_parts_price_list_report()
|
|
855
|
+
if report_success:
|
|
856
|
+
# Configure report parameters
|
|
857
|
+
params = PartsPriceListParams(
|
|
858
|
+
from_department='430',
|
|
859
|
+
from_franchise='TOY',
|
|
860
|
+
output_file_name='Parts Price List Report - 430.csv'
|
|
861
|
+
)
|
|
862
|
+
# Download the report
|
|
863
|
+
download_success = parts_price_list_report_download(params, reports_dir=reports_dir)
|
|
864
|
+
if not download_success:
|
|
865
|
+
logger.error("Failed to download Charters Towers Partnership Parts Price List Report")
|
|
866
|
+
close_tune_application()
|
|
867
|
+
return False
|
|
868
|
+
logger.info("Charters Towers Partnership Parts Price List Report generated successfully")
|
|
869
|
+
else:
|
|
870
|
+
logger.error("Failed to open Charters Towers Partnership Parts Price List Report")
|
|
871
|
+
close_tune_application()
|
|
872
|
+
return False
|
|
873
|
+
|
|
874
|
+
# Generate Charters Towers Partnership Parts by Bin Location Report
|
|
875
|
+
|
|
876
|
+
logger.info("Generating Charters Towers Partnership Parts by Bin Location Report...")
|
|
877
|
+
report_success = open_parts_by_bin_location_report()
|
|
878
|
+
if report_success:
|
|
879
|
+
# Configure report parameters
|
|
880
|
+
params = PartsByBinLocationParams(
|
|
881
|
+
from_department='430',
|
|
882
|
+
from_franchise='TOY',
|
|
883
|
+
output_file_name='Parts by Bin Location Report - 430.csv'
|
|
884
|
+
)
|
|
885
|
+
# Download the report
|
|
886
|
+
download_success = parts_by_bin_location_report_download(params, reports_dir=reports_dir)
|
|
887
|
+
if not download_success:
|
|
888
|
+
logger.error("Failed to download Charters Towers Partnership Parts by Bin Location Report")
|
|
889
|
+
close_tune_application()
|
|
890
|
+
return False
|
|
891
|
+
logger.info("Charters Towers Partnership Parts by Bin Location Report generated successfully")
|
|
892
|
+
else:
|
|
893
|
+
logger.error("Failed to open Charters Towers Partnership Parts by Bin Location Report")
|
|
894
|
+
close_tune_application()
|
|
895
|
+
return False
|
|
896
|
+
|
|
897
|
+
# Close TUNE
|
|
898
|
+
close_tune_application()
|
|
899
|
+
logger.info("TUNE reports generation completed successfully")
|
|
900
|
+
return True
|
|
901
|
+
|
|
902
|
+
except Exception as e:
|
|
903
|
+
logger.error(f"Error during TUNE report generation: {e}")
|
|
904
|
+
traceback.print_exc()
|
|
905
|
+
# Try to close TUNE if it's open
|
|
906
|
+
try:
|
|
907
|
+
close_tune_application()
|
|
908
|
+
except:
|
|
909
|
+
pass
|
|
910
|
+
return False
|
|
911
|
+
|
|
912
|
+
def main(config: TuneConfig) -> bool:
|
|
913
|
+
"""Main entry point: run TUNE report generation with the given config."""
|
|
914
|
+
global _config
|
|
915
|
+
_config = config
|
|
916
|
+
try:
|
|
917
|
+
config.validate()
|
|
918
|
+
images_dir = config.images_dir or _get_images_dir()
|
|
919
|
+
login_image = os.path.join(images_dir, 'tune_login_screen.png')
|
|
920
|
+
app_image = os.path.join(images_dir, 'tune_application_screen.png')
|
|
921
|
+
missing_images = []
|
|
922
|
+
if not os.path.exists(login_image):
|
|
923
|
+
missing_images.append('tune_login_screen.png')
|
|
924
|
+
if not os.path.exists(app_image):
|
|
925
|
+
missing_images.append('tune_application_screen.png')
|
|
926
|
+
if missing_images:
|
|
927
|
+
missing_str = ', '.join(missing_images)
|
|
928
|
+
logger.error(f"Missing reference images: {missing_str}")
|
|
929
|
+
return False
|
|
930
|
+
report_generator = TuneReportGenerator(config)
|
|
931
|
+
return report_generator.run_reports()
|
|
932
|
+
except Exception as e:
|
|
933
|
+
logger.error(f"An error occurred: {e}")
|
|
934
|
+
return False
|
|
935
|
+
finally:
|
|
936
|
+
_config = None
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
if __name__ == "__main__":
|
|
940
|
+
config = TuneConfig.from_env()
|
|
941
|
+
success = main(config)
|
|
942
|
+
sys.exit(0 if success else 1)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tune-dms
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TUNE DMS desktop GUI automation: login and report generation
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: tune,dms,gui,automation,reports
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: pyautogui
|
|
16
|
+
|
|
17
|
+
# tune-dms
|
|
18
|
+
|
|
19
|
+
TUNE DMS desktop GUI automation: log in and run reports (e.g. Parts Price List, Parts by Bin Location) via keyboard/screen automation.
|
|
20
|
+
|
|
21
|
+
Install: `pip install tune-dms`
|
|
22
|
+
Or from repo root: `pip install -e ./packages/tune_dms`
|
|
23
|
+
|
|
24
|
+
See the [main repo README](../README.md) for configuration and usage.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
tune_dms/__init__.py,sha256=AqosmWxyT-43xMgfA-JECy3THgoasmH2WNU5lIkhIUo,617
|
|
2
|
+
tune_dms/config.py,sha256=2E6V_T031ZpjsaO-mt1mF2KccBSBhPv9oL-9DJ8W4WU,1935
|
|
3
|
+
tune_dms/launcher.py,sha256=MJaZYnnZ0egdXxa0SegoA81nM37rYLgz91wvogm3I_s,36693
|
|
4
|
+
tune_dms/images/confirm_save_as_dialog.png,sha256=zNTFAaV56FxEXlO8dtgrMmEzGqybIczkRVg3Fa5IFC0,1100
|
|
5
|
+
tune_dms/images/excel_application_screen.png,sha256=vkmYg466hyyRHfvE2mHlaMxZ-Ob7Q-I_kj0_RRc9P0g,527
|
|
6
|
+
tune_dms/images/save_as_window.png,sha256=IJRh7yL6wUSVEaWljJwHEvmFjBwpYNyAqUv9h_hPG0I,1733
|
|
7
|
+
tune_dms/images/tune_admin_purchase_orders_icon_selected.png,sha256=9rzlT5hsQF6OczfxlSAKQ5URGOhV35nf9nZZYN3wK9c,2002
|
|
8
|
+
tune_dms/images/tune_application_screen.png,sha256=1GnO27UzU3jRU9FOGmXCewC2fm8hUqbK2DBYVz39wq4,975
|
|
9
|
+
tune_dms/images/tune_cancel_button.png,sha256=sY2I1s7X94xK6BKDgrdv2STIqELnZyYMiCLKGd8h8oI,508
|
|
10
|
+
tune_dms/images/tune_cancel_button_2.png,sha256=JipzsYqLtrIwyP83DJ1u4EkhjOknyFK11VzFSZp6Qso,503
|
|
11
|
+
tune_dms/images/tune_cancel_button_c.png,sha256=VLMECAoretn-STQQ43msppUTP7r5psc2_XVjafPRcZI,522
|
|
12
|
+
tune_dms/images/tune_exit_button.png,sha256=AlEZamFjISaW7cGy_qPW6RBYGsfER0HJ4Z9iv1-ibAY,433
|
|
13
|
+
tune_dms/images/tune_login_error.png,sha256=kBM8RNt0pHKIHl8cd9R05Z3raC9omui50FgEwfZjSVQ,2256
|
|
14
|
+
tune_dms/images/tune_login_screen.png,sha256=SRxkt013051OOeU87VLQtxQ-CR0AJpfjPpWR7lQHwIo,65866
|
|
15
|
+
tune_dms/images/tune_login_screen_environment.png,sha256=jBJo5XD0klEK7k2vB9Kv33QBNzot0ph4i-ul2YyRdcY,620
|
|
16
|
+
tune_dms/images/tune_part_general_enquiry_cap_qty.png,sha256=nRErc88LyNkZ2IS0myGb2qbBht3IndhFJ3f75m13Z9M,285
|
|
17
|
+
tune_dms/images/tune_part_general_enquiry_dangerous_goods_class.png,sha256=PLlBhUbwbtI942DJhS3nyBRU1zzBeOZ3V0K9KuhPIL8,447
|
|
18
|
+
tune_dms/images/tune_part_general_enquiry_default_supplier.png,sha256=lwBbigDU7oNmDyKBAHPLdRgQX1UV2Hv1U5etmEG5fdw,363
|
|
19
|
+
tune_dms/images/tune_part_general_enquiry_other.png,sha256=7mFLZo8Xora_8CAHl69haRa9MLyQxoZZqiqqvlW56vA,345
|
|
20
|
+
tune_dms/images/tune_part_general_enquiry_pack_size.png,sha256=q5R8JKHK02aYNbeJDxGxUCeMFl_d9lF4SFszATIQglU,302
|
|
21
|
+
tune_dms/images/tune_part_general_enquiry_part.png,sha256=LerhCawK1hG9ZjvNR6lvX1BD9Ejbsmzp8TjYy-qsnhg,239
|
|
22
|
+
tune_dms/images/tune_part_general_enquiry_part_height.png,sha256=ZMbCXiKYcQvkPxE3eezMGTKkFAGIIxNvLlHnMS7pg8c,321
|
|
23
|
+
tune_dms/images/tune_part_general_enquiry_part_length.png,sha256=vRQ5o_Kvwo6XDMYr2c_hl1lnHMXeA7mG8j5A8esCL44,305
|
|
24
|
+
tune_dms/images/tune_part_general_enquiry_part_volume.png,sha256=lTlOzIu34I32WKfvfYanoErViFYcdlfZJo7QJ2PEOUc,294
|
|
25
|
+
tune_dms/images/tune_part_general_enquiry_part_weight.png,sha256=oE2k5m842rlw9sNR8Yxt57oCNJtJeWGJWoLGC7MGvNk,312
|
|
26
|
+
tune_dms/images/tune_part_general_enquiry_part_width.png,sha256=-fr5JvKU4RdzcO9QtxIkzOywVcWLz_16hIK5pDMHKPM,294
|
|
27
|
+
tune_dms/images/tune_part_general_enquiry_unit_of_measure.png,sha256=C7t5dj4y3mm8HJr_6e4mFMKEx0XdFBYJbI4jTmCD4fU,353
|
|
28
|
+
tune_dms/images/tune_previous_button.png,sha256=j55BAvWhnxaPw9IZalAXhgHc6-G0Zd01fqvobEkU1Ok,526
|
|
29
|
+
tune_dms/images/tune_report_parts_by_bin_location.png,sha256=fmVt236qzjUk2mYf0oLMg-VmXmPlqaK1kDJQ94uZrZ8,1684
|
|
30
|
+
tune_dms/images/tune_report_parts_price_list.png,sha256=COjUuiFn4-CEDKeOlOwHoR4Jr0SP1SvQOf093NenZ1o,1136
|
|
31
|
+
tune_dms/images/tune_search_field.png,sha256=uCzFpgrv_71mvqN-3uxkykw_-TKdwYnAXsPvxXZq0ew,1854
|
|
32
|
+
tune_dms/images/tune_select_franchise.png,sha256=hkwCMPZV6F6Yft_x_2Ud1TCnXwwOHRiI9uBV8Kv2pAw,936
|
|
33
|
+
tune_dms/images/tune_select_franchise_grey.png,sha256=0Dp5e0FsmXl3zdLS3oJA8BKmmuutezoC9d3-V2yV4x8,855
|
|
34
|
+
tune_dms/images/tune_select_franchise_toy_grey.png,sha256=lQhOP7lXWhZwLV9l-iOQ3NsW_BaRyxEnhk7fho_fCGE,219
|
|
35
|
+
tune_dms/images/tune_select_franchise_toy_grey_box.png,sha256=rrid7bKxZguvz_lxGkXwiGehHfDPL28rFSaqRefi2iI,344
|
|
36
|
+
tune_dms/images/tune_select_franchise_toy_yellow.png,sha256=QgQom3rtEoJUAcRXYOt4wfaZXKZtGc19BZZDjtPDBBQ,216
|
|
37
|
+
tune_dms/images/tune_select_franchise_toy_yellow_box.png,sha256=hCy0pfVLychIUBqwEz0pg0NDiJWeaIUsppzqXMYoj-U,311
|
|
38
|
+
tune_dms/images/tune_supplier_part_selection.png,sha256=diXleZHo4emsAhblyO8Z_v81uvdACVqUWLbi5hUdxHE,1213
|
|
39
|
+
tune_dms/images/tune_x_button.png,sha256=LncxIEGnSdZpSdsWxQIfQ1oG04AseUgl41H30o6xaAs,313
|
|
40
|
+
tune_dms-0.1.0.dist-info/METADATA,sha256=D3BnGt6-zSgENLcfRHZQ-W6ssoS3shJLZA7GBO42HNs,900
|
|
41
|
+
tune_dms-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
42
|
+
tune_dms-0.1.0.dist-info/top_level.txt,sha256=foSEFiroat3zXAH5sxCZMw7NVXVnFlLH9idIe_QrOOs,9
|
|
43
|
+
tune_dms-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tune_dms
|