mbu-dev-shared-components 1.2.1__tar.gz → 2.0.0__tar.gz
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.
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/PKG-INFO +1 -1
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/__init__.py +32 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/__init__.py +1 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/app_handler.py +134 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/appointment.py +334 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/base_ui.py +100 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/clinic.py +75 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/document.py +399 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/edi_portal.py +132 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/event.py +51 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/exceptions.py +20 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/patient.py +264 -0
- mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/database/__init__.py +1 -0
- {mbu_dev_shared_components-1.2.1/mbu_dev_shared_components/solteqtand → mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/database}/db_handler.py +38 -6
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components.egg-info/PKG-INFO +1 -1
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components.egg-info/SOURCES.txt +12 -3
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/pyproject.toml +1 -1
- mbu_dev_shared_components-1.2.1/mbu_dev_shared_components/solteqtand/app_handler.py +0 -1668
- mbu_dev_shared_components-1.2.1/mbu_dev_shared_components/solteqtand/helper_functions.py +0 -107
- mbu_dev_shared_components-1.2.1/mbu_dev_shared_components/utils/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/LICENSE +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/README.md +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/database/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/database/constants.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/database/logging.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/database/utility.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/auth.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/cases.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/contacts.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/documents.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/getorganized/objects.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/google/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/google/api/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/google/api/auth.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/google/workspace/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/google/workspace/alerts.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/msoffice365/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/msoffice365/excel/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/msoffice365/excel/excel_reader.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/msoffice365/sharepoint_api/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/msoffice365/sharepoint_api/files.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/os2forms/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/os2forms/documents.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/os2forms/forms.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/romexis/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/romexis/db_handler.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/romexis/helper_functions.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/sap/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/sap/create_invoice.py +0 -0
- {mbu_dev_shared_components-1.2.1/mbu_dev_shared_components/solteqtand → mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/utils}/__init__.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/utils/db_stored_procedure_executor.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/utils/fernet_encryptor.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/utils/file_handler.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components/utils/json_handler.py +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components.egg-info/dependency_links.txt +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components.egg-info/requires.txt +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/mbu_dev_shared_components.egg-info/top_level.txt +0 -0
- {mbu_dev_shared_components-1.2.1 → mbu_dev_shared_components-2.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the main entry point for the Solteq Tand application.
|
|
3
|
+
"""
|
|
4
|
+
from .application import (
|
|
5
|
+
app_handler,
|
|
6
|
+
appointment,
|
|
7
|
+
base_ui,
|
|
8
|
+
clinic,
|
|
9
|
+
document,
|
|
10
|
+
edi_portal,
|
|
11
|
+
event,
|
|
12
|
+
patient,
|
|
13
|
+
exceptions,
|
|
14
|
+
helper_functions,
|
|
15
|
+
SolteqTandApp
|
|
16
|
+
)
|
|
17
|
+
from .database import SolteqTandDatabase
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"app_handler",
|
|
21
|
+
"appointment",
|
|
22
|
+
"base_ui",
|
|
23
|
+
"clinic",
|
|
24
|
+
"document",
|
|
25
|
+
"edi_portal",
|
|
26
|
+
"event",
|
|
27
|
+
"patient",
|
|
28
|
+
"exceptions",
|
|
29
|
+
"helper_functions",
|
|
30
|
+
"SolteqTandApp",
|
|
31
|
+
"SolteqTandDatabase"
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .app_handler import SolteqTandApp
|
mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/app_handler.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uiautomation as auto
|
|
5
|
+
|
|
6
|
+
from .base_ui import BaseUI
|
|
7
|
+
from .patient import PatientHandler
|
|
8
|
+
from .document import DocumentHandler
|
|
9
|
+
from .appointment import AppointmentHandler
|
|
10
|
+
from .edi_portal import EDIHandler
|
|
11
|
+
from .clinic import ClinicHandler
|
|
12
|
+
from .event import EventHandler
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
ManualProcessingRequiredError,
|
|
15
|
+
NotMatchingError,
|
|
16
|
+
PatientNotFoundError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SolteqTandApp(
|
|
21
|
+
BaseUI,
|
|
22
|
+
PatientHandler,
|
|
23
|
+
DocumentHandler,
|
|
24
|
+
AppointmentHandler,
|
|
25
|
+
EDIHandler,
|
|
26
|
+
ClinicHandler,
|
|
27
|
+
EventHandler
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Main application handler for Solteq Tand, integrating various components.
|
|
31
|
+
|
|
32
|
+
Inherits from:
|
|
33
|
+
BaseUI: Provides basic UI interaction methods.
|
|
34
|
+
PatientHandler: Handles patient-related operations.
|
|
35
|
+
DocumentHandler: Manages document operations.
|
|
36
|
+
AppointmentHandler: Manages appointment operations.
|
|
37
|
+
EDIHandler: Handles EDI portal interactions.
|
|
38
|
+
ClinicHandler: Manages clinic-related operations.
|
|
39
|
+
EventHandler: Processes events in the application.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, app_path: str, username: str = None, password: str = None):
|
|
43
|
+
"""
|
|
44
|
+
Initializes the Solteq Tand application handler.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
app_path (str): Path to the Solteq Tand application executable.
|
|
48
|
+
"""
|
|
49
|
+
self.app_path = app_path
|
|
50
|
+
self.username = username
|
|
51
|
+
self.password = password
|
|
52
|
+
self.app_window = None
|
|
53
|
+
|
|
54
|
+
def start_application(self):
|
|
55
|
+
"""
|
|
56
|
+
Starts the application using the specified path.
|
|
57
|
+
"""
|
|
58
|
+
os.startfile(self.app_path)
|
|
59
|
+
|
|
60
|
+
def login(self):
|
|
61
|
+
"""
|
|
62
|
+
Logs into the application by entering the username and password.
|
|
63
|
+
Checks if the login window is open and ready.
|
|
64
|
+
Checks if the main window is opened and ready.
|
|
65
|
+
"""
|
|
66
|
+
self.app_window = self.wait_for_control(
|
|
67
|
+
auto.WindowControl,
|
|
68
|
+
{'AutomationId': 'FormLogin'},
|
|
69
|
+
search_depth=3,
|
|
70
|
+
timeout=60
|
|
71
|
+
)
|
|
72
|
+
self.app_window.SetFocus()
|
|
73
|
+
|
|
74
|
+
username_box = self.app_window.EditControl(AutomationId="TextLogin")
|
|
75
|
+
username_box.SendKeys(text=self.username)
|
|
76
|
+
|
|
77
|
+
password_box = self.app_window.EditControl(AutomationId="TextPwd")
|
|
78
|
+
password_box.SendKeys(text=self.password)
|
|
79
|
+
|
|
80
|
+
login_button = self.app_window.PaneControl(AutomationId="ButtonLogin")
|
|
81
|
+
login_button.SetFocus()
|
|
82
|
+
login_button.SendKeys('{ENTER}')
|
|
83
|
+
|
|
84
|
+
self.app_window = self.wait_for_control(
|
|
85
|
+
auto.WindowControl,
|
|
86
|
+
{'AutomationId': 'FormFront'},
|
|
87
|
+
search_depth=2,
|
|
88
|
+
timeout=60
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def open_sub_tab(self, sub_tab_name: str):
|
|
92
|
+
"""
|
|
93
|
+
Opens a specific sub-tab in the patient's main card.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
sub_tab_name (str): The name of the sub-tab to open (e.g., "Dokumenter").
|
|
97
|
+
"""
|
|
98
|
+
sub_tab_button = self.app_window.TabItemControl(Name=sub_tab_name)
|
|
99
|
+
is_sub_tab_selected = sub_tab_button.GetPattern(10010).IsSelected
|
|
100
|
+
|
|
101
|
+
if not is_sub_tab_selected:
|
|
102
|
+
sub_tab_button.SetFocus()
|
|
103
|
+
sub_tab_button.SendKeys('{ENTER}')
|
|
104
|
+
|
|
105
|
+
def open_tab(self, tab_name: str):
|
|
106
|
+
"""
|
|
107
|
+
Opens a specific tab in the patient's main card.
|
|
108
|
+
Poosibly functionality on other parts of Solteq with tabs as well.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
tab_name (str): The name of the tab to open (e.g., "Frit valg").
|
|
112
|
+
"""
|
|
113
|
+
match tab_name:
|
|
114
|
+
case "Stamkort":
|
|
115
|
+
tab_name_modified = "S&tamkort"
|
|
116
|
+
case "Fritvalg":
|
|
117
|
+
tab_name_modified = "F&ritvalg"
|
|
118
|
+
case "Journal":
|
|
119
|
+
tab_name_modified = "&Journal"
|
|
120
|
+
case "Oversigt":
|
|
121
|
+
tab_name_modified = "O&versigt"
|
|
122
|
+
case _:
|
|
123
|
+
tab_name_modified = tab_name
|
|
124
|
+
|
|
125
|
+
tab_button = self.find_element_by_property(
|
|
126
|
+
control=self.app_window,
|
|
127
|
+
control_type=auto.ControlType.TabItemControl,
|
|
128
|
+
name=tab_name_modified
|
|
129
|
+
)
|
|
130
|
+
is_tab_selected = tab_button.GetPattern(10010).IsSelected
|
|
131
|
+
|
|
132
|
+
if not is_tab_selected:
|
|
133
|
+
tab_button.SetFocus()
|
|
134
|
+
tab_button.SendKeys('{ENTER}')
|
mbu_dev_shared_components-2.0.0/mbu_dev_shared_components/solteqtand/application/appointment.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import uiautomation as auto
|
|
2
|
+
|
|
3
|
+
from .base_ui import BaseUI
|
|
4
|
+
from .exceptions import ManualProcessingRequiredError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AppointmentHandler(BaseUI):
|
|
8
|
+
"""
|
|
9
|
+
Handles appointment-related UI interactions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def get_list_of_appointments(self) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Gets list of appointments as found in patient window
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
booking_list_dict (dict): Dictionary with appointments and informations
|
|
18
|
+
booking_list_ctrls (list): List with the control related to each appointment
|
|
19
|
+
|
|
20
|
+
Todo: Assure that view is on patient
|
|
21
|
+
"""
|
|
22
|
+
# Open "Stamkort"
|
|
23
|
+
self.open_tab("Stamkort")
|
|
24
|
+
|
|
25
|
+
# Read elements in list and check that expected element exists
|
|
26
|
+
# First get the list of appointments
|
|
27
|
+
list_parent = self.find_element_by_property(
|
|
28
|
+
control=self.app_window,
|
|
29
|
+
automation_id='ControlBookingDay'
|
|
30
|
+
)
|
|
31
|
+
booking_list_ctrl = self.find_element_by_property(
|
|
32
|
+
control=list_parent,
|
|
33
|
+
control_type=50008
|
|
34
|
+
)
|
|
35
|
+
# Initiate dictionary for list elements
|
|
36
|
+
booking_list = {'controls': []}
|
|
37
|
+
# Initiate list to hold headers
|
|
38
|
+
booking_list_keys = []
|
|
39
|
+
rowcount = 0
|
|
40
|
+
|
|
41
|
+
# Check for header
|
|
42
|
+
if booking_list_ctrl.GetFirstChildControl().ControlType == 50034:
|
|
43
|
+
# Loop through all elements in list
|
|
44
|
+
for elem in booking_list_ctrl.GetChildren():
|
|
45
|
+
# If header, then add each item to list of headers
|
|
46
|
+
if elem.ControlType == 50034:
|
|
47
|
+
for colname in elem.GetChildren():
|
|
48
|
+
booking_list_keys.append(colname.Name)
|
|
49
|
+
booking_list[colname.Name] = []
|
|
50
|
+
# If listitem, then add each item to dict
|
|
51
|
+
if elem.ControlType == 50007:
|
|
52
|
+
booking_list['controls'].append(elem) # Adds the control to accessed later
|
|
53
|
+
vals = elem.GetChildren() # Extracts all information from control
|
|
54
|
+
|
|
55
|
+
for headercount, val in enumerate(vals):
|
|
56
|
+
booking_list[booking_list_keys[headercount]].append(val.Name)
|
|
57
|
+
rowcount += 1
|
|
58
|
+
|
|
59
|
+
return booking_list
|
|
60
|
+
|
|
61
|
+
def change_appointment_status(
|
|
62
|
+
self,
|
|
63
|
+
appointment_control: auto.ControlType,
|
|
64
|
+
set_status: str,
|
|
65
|
+
send_msg: bool = False
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Changes status of appointment and optionally sends message
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
appointment_control (Control): Control element that identifies the appointment to be changed
|
|
72
|
+
set_status (str): The status which the appointment should be changed to.
|
|
73
|
+
send_msg (bool, optional): Indicates whether message should be sent when status is changed.
|
|
74
|
+
"""
|
|
75
|
+
appointment_control.GetInvokePattern().Invoke()
|
|
76
|
+
|
|
77
|
+
# Find booking control
|
|
78
|
+
booking_control = self.wait_for_control(
|
|
79
|
+
control_type=auto.PaneControl,
|
|
80
|
+
search_params={
|
|
81
|
+
'AutomationId': 'ManageBookingControl'
|
|
82
|
+
},
|
|
83
|
+
search_depth=3
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Find appointment status dropdown
|
|
87
|
+
status_control = self.find_element_by_property(
|
|
88
|
+
control=booking_control,
|
|
89
|
+
control_type=50003,
|
|
90
|
+
name='Status'
|
|
91
|
+
)
|
|
92
|
+
# Get current status to reset if warning on save
|
|
93
|
+
current_status = status_control.GetValuePattern().Value
|
|
94
|
+
|
|
95
|
+
# Open dropdown
|
|
96
|
+
self.find_element_by_property(
|
|
97
|
+
control=status_control,
|
|
98
|
+
control_type=50000
|
|
99
|
+
).GetInvokePattern().Invoke()
|
|
100
|
+
|
|
101
|
+
# Get list control for all status options
|
|
102
|
+
status_list_ctrl = self.wait_for_control(
|
|
103
|
+
control_type=auto.ListControl,
|
|
104
|
+
search_params={
|
|
105
|
+
'ClassName': 'ComboLBox'
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
# Load status options into dict with controls, names and lowercase names
|
|
109
|
+
status_dict = {
|
|
110
|
+
'ctrls' : [elem for elem in status_list_ctrl.GetChildren() if elem.ControlType == 50007],
|
|
111
|
+
'names' : [elem.Name for elem in status_list_ctrl.GetChildren() if elem.ControlType == 50007],
|
|
112
|
+
'names_lo': [elem.Name.lower() for elem in status_list_ctrl.GetChildren() if elem.ControlType == 50007]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Set new status if valid, otherwise return error
|
|
116
|
+
if set_status.lower() in status_dict['names_lo']:
|
|
117
|
+
list_no = status_dict['names_lo'].index(set_status.lower())
|
|
118
|
+
status_dict['ctrls'][list_no].GetInvokePattern().Invoke()
|
|
119
|
+
# Click "Gem og udsend"
|
|
120
|
+
self.app_window = booking_control
|
|
121
|
+
if send_msg:
|
|
122
|
+
save_button = self.find_element_by_property(
|
|
123
|
+
control=booking_control,
|
|
124
|
+
automation_id = "ButtonSavePrint"
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
save_button = self.find_element_by_property(
|
|
128
|
+
control=booking_control,
|
|
129
|
+
automation_id = "ButtonOk"
|
|
130
|
+
)
|
|
131
|
+
save_button.SendKeys('{ENTER}')
|
|
132
|
+
# Check for notification window pop up
|
|
133
|
+
try:
|
|
134
|
+
notification_ctrl = self.wait_for_control(
|
|
135
|
+
control_type=auto.PaneControl,
|
|
136
|
+
search_params={
|
|
137
|
+
'AutomationId': 'BookingNotificationsControl'
|
|
138
|
+
},
|
|
139
|
+
search_depth=3,
|
|
140
|
+
timeout=5
|
|
141
|
+
)
|
|
142
|
+
close_button = self.find_element_by_property(
|
|
143
|
+
control=notification_ctrl,
|
|
144
|
+
automation_id="ButtonCancel"
|
|
145
|
+
)
|
|
146
|
+
close_button.SendKeys('{ENTER}')
|
|
147
|
+
return
|
|
148
|
+
except TimeoutError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# Check for warning window pop up
|
|
152
|
+
try:
|
|
153
|
+
self.handle_error_on_booking_save(slct_button="ButtonChangeManual")
|
|
154
|
+
# Wait for status list to reappear
|
|
155
|
+
booking_control = self.wait_for_control(
|
|
156
|
+
control_type=auto.PaneControl,
|
|
157
|
+
search_params={
|
|
158
|
+
'AutomationId': 'ManageBookingControl'
|
|
159
|
+
},
|
|
160
|
+
search_depth=3
|
|
161
|
+
)
|
|
162
|
+
# Open dropdown
|
|
163
|
+
self.find_element_by_property(
|
|
164
|
+
control=status_control,
|
|
165
|
+
control_type=50000
|
|
166
|
+
).GetInvokePattern().Invoke()
|
|
167
|
+
# Reset to original value
|
|
168
|
+
list_no = status_dict['names_lo'].index(current_status.lower())
|
|
169
|
+
status_dict['ctrls'][list_no].GetInvokePattern().Invoke()
|
|
170
|
+
# Save original status
|
|
171
|
+
save_button = self.find_element_by_property(
|
|
172
|
+
control=booking_control,
|
|
173
|
+
automation_id = "ButtonOk"
|
|
174
|
+
)
|
|
175
|
+
save_button.SendKeys('{ENTER}')
|
|
176
|
+
# Accept despite warning
|
|
177
|
+
self.handle_error_on_booking_save(slct_button="ButtonOk")
|
|
178
|
+
|
|
179
|
+
raise ManualProcessingRequiredError
|
|
180
|
+
except TimeoutError:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
# If warning when sending: press "ret manuelt" -> "annuler" -> return warning error
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
else:
|
|
187
|
+
print(f"{set_status} not in list. Possible status choices are: {', '.join(status_dict['names'])}")
|
|
188
|
+
raise Exception
|
|
189
|
+
|
|
190
|
+
def handle_error_on_booking_save(self, slct_button: str):
|
|
191
|
+
"""Handle error window when saving booking. Select button to press"""
|
|
192
|
+
buttons = [
|
|
193
|
+
"ButtonFindNewTimeSlot",
|
|
194
|
+
"ButtonOk",
|
|
195
|
+
"ButtonChangeManual"
|
|
196
|
+
]
|
|
197
|
+
if slct_button not in buttons:
|
|
198
|
+
print(f"{slct_button} not in buttons. Available buttons are {' '.join(buttons)}")
|
|
199
|
+
raise ValueError
|
|
200
|
+
warning_window = self.wait_for_control(
|
|
201
|
+
control_type=auto.WindowControl,
|
|
202
|
+
search_params={
|
|
203
|
+
"AutomationId": "FormBookingWarnings"
|
|
204
|
+
},
|
|
205
|
+
search_depth=5
|
|
206
|
+
)
|
|
207
|
+
button = self.find_element_by_property(
|
|
208
|
+
control=warning_window,
|
|
209
|
+
control_type=50033,
|
|
210
|
+
automation_id=slct_button
|
|
211
|
+
)
|
|
212
|
+
button.SendKeys("{ENTER}")
|
|
213
|
+
|
|
214
|
+
def get_appointments_aftalebog(
|
|
215
|
+
self,
|
|
216
|
+
close_after: bool = False,
|
|
217
|
+
headers_to_keep: list | None = None
|
|
218
|
+
) -> dict:
|
|
219
|
+
"""Function to retrive data on appointments in view in aftalebog"""
|
|
220
|
+
|
|
221
|
+
# Get list control
|
|
222
|
+
list_box = self.wait_for_control(
|
|
223
|
+
control_type=auto.GroupControl,
|
|
224
|
+
search_params={
|
|
225
|
+
"AutomationId": "GroupBoxView"
|
|
226
|
+
},
|
|
227
|
+
search_depth=5
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
appointment_list = self.find_element_by_property(
|
|
231
|
+
control=list_box,
|
|
232
|
+
control_type=50008
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Extract headers
|
|
236
|
+
appointment_headers = [
|
|
237
|
+
header.Name
|
|
238
|
+
for header in appointment_list.GetFirstChildControl().GetChildren()
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Extract ListItem controls
|
|
242
|
+
appointment_ctrls = [
|
|
243
|
+
ctrl
|
|
244
|
+
for ctrl in appointment_list.GetChildren()
|
|
245
|
+
if ctrl.ControlType == 50007
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
# Package data in dictionary
|
|
249
|
+
# Keep only selected headers if any selected.
|
|
250
|
+
if not headers_to_keep:
|
|
251
|
+
headers_to_keep = appointment_headers
|
|
252
|
+
|
|
253
|
+
appointment_data = {
|
|
254
|
+
j: {
|
|
255
|
+
k: v.Name
|
|
256
|
+
for k, v in zip(appointment_headers, ctrl.GetChildren())
|
|
257
|
+
if k in headers_to_keep
|
|
258
|
+
}
|
|
259
|
+
for j, ctrl in enumerate(appointment_ctrls)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if close_after:
|
|
263
|
+
# Should maybe be in a method of its own?
|
|
264
|
+
list_box.SendKeys('{Control}{F4}')
|
|
265
|
+
self.wait_for_control_to_disappear(
|
|
266
|
+
control_type=auto.WindowControl,
|
|
267
|
+
search_params={
|
|
268
|
+
"AutomationId": "FormBooking"
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return appointment_data
|
|
273
|
+
|
|
274
|
+
def create_booking_reminder(self, booking_reminder_data: dict):
|
|
275
|
+
"""
|
|
276
|
+
Creates a booking reminder for the patient.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
self.open_tab("Stamkort")
|
|
280
|
+
|
|
281
|
+
create_booking_button = self.wait_for_control(
|
|
282
|
+
auto.PaneControl,
|
|
283
|
+
{"AutomationId": "ButtonBookingNew"},
|
|
284
|
+
search_depth=14
|
|
285
|
+
)
|
|
286
|
+
create_booking_button.GetLegacyIAccessiblePattern().DoDefaultAction()
|
|
287
|
+
|
|
288
|
+
booking_window = self.wait_for_control(
|
|
289
|
+
auto.WindowControl,
|
|
290
|
+
{"AutomationId": "MainFrame"},
|
|
291
|
+
search_depth=2
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Fill out ressourcer group
|
|
295
|
+
manage_booking = booking_window.PaneControl(AutomationId="viewPortPanel").PaneControl(AutomationId="ManageBookingControl")
|
|
296
|
+
resources_group = manage_booking.GroupControl(AutomationId="Ressourcer")
|
|
297
|
+
|
|
298
|
+
for child in resources_group.GetChildren():
|
|
299
|
+
if child.ControlTypeName == "ComboBoxControl":
|
|
300
|
+
match child.Name:
|
|
301
|
+
case "Aftaletype":
|
|
302
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["comboBoxBookingType"])
|
|
303
|
+
case "Behandler":
|
|
304
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["comboBoxDentist"])
|
|
305
|
+
case "Stol":
|
|
306
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["comboBoxChair"])
|
|
307
|
+
|
|
308
|
+
# Fill out date and time
|
|
309
|
+
date_and_time_group = manage_booking.GroupControl(AutomationId="GroupBox4")
|
|
310
|
+
|
|
311
|
+
for child in date_and_time_group.GetChildren():
|
|
312
|
+
match child.AutomationId:
|
|
313
|
+
case "DateTimePickerStartTime":
|
|
314
|
+
child.SendKeys(booking_reminder_data["dateTimePickerStartTime"])
|
|
315
|
+
case "TextBoxDuration":
|
|
316
|
+
if child.GetPattern(auto.PatternId.ValuePattern).Value != booking_reminder_data["textBoxDuration"]:
|
|
317
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["textBoxDuration"])
|
|
318
|
+
case "ComboBoxStatus":
|
|
319
|
+
if child.GetPattern(auto.PatternId.ValuePattern).Value != booking_reminder_data["comboBoxStatus"]:
|
|
320
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["comboBoxStatus"])
|
|
321
|
+
case "DateTimePickerDate":
|
|
322
|
+
child.SendKeys(booking_reminder_data["futureDate"])
|
|
323
|
+
|
|
324
|
+
manage_booking.PaneControl(AutomationId="ButtonOk").Click(simulateMove=True, waitTime=0)
|
|
325
|
+
|
|
326
|
+
booking_window_warning = self.wait_for_control(
|
|
327
|
+
auto.WindowControl,
|
|
328
|
+
{"AutomationId": "FormBookingWarnings"},
|
|
329
|
+
search_depth=4
|
|
330
|
+
)
|
|
331
|
+
booking_window_warning.PaneControl(AutomationId="ButtonOk").Click(simulateMove=True, waitTime=0)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print(f"Error while creating booking reminder: {e}")
|
|
334
|
+
raise
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Base UI-Automation helper methods for SolteqTand project."""
|
|
2
|
+
import time
|
|
3
|
+
import uiautomation as auto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseUI:
|
|
7
|
+
"""Base UI-Automation helper methods."""
|
|
8
|
+
|
|
9
|
+
def find_element_by_property(self, control, control_type=None, automation_id=None, name=None, class_name=None) -> auto.Control:
|
|
10
|
+
"""
|
|
11
|
+
Uses GetChildren to traverse through controls and find an element based on the specified properties.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
control (Control): The root control to search from (e.g., main window or pane).
|
|
15
|
+
control_type (ControlType, optional): ControlType to search for.
|
|
16
|
+
automation_id (str, optional): AutomationId of the target element.
|
|
17
|
+
name (str, optional): Name of the target element.
|
|
18
|
+
class_name (str, optional): ClassName of the target element.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Control: The found element or None if no match is found.
|
|
22
|
+
"""
|
|
23
|
+
children = control.GetChildren()
|
|
24
|
+
|
|
25
|
+
for child in children:
|
|
26
|
+
if (control_type is None or child.ControlType == control_type) and \
|
|
27
|
+
(automation_id is None or child.AutomationId == automation_id) and \
|
|
28
|
+
(name is None or child.Name == name) and \
|
|
29
|
+
(class_name is None or child.ClassName == class_name):
|
|
30
|
+
return child
|
|
31
|
+
|
|
32
|
+
found = self.find_element_by_property(child, control_type, automation_id, name, class_name)
|
|
33
|
+
if found:
|
|
34
|
+
return found
|
|
35
|
+
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def wait_for_control(self, control_type, search_params, search_depth=1, timeout=30, retry_interval=0.5):
|
|
39
|
+
"""
|
|
40
|
+
Waits for a given control type to become available with the specified search parameters.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
control_type: The type of control, e.g., auto.WindowControl, auto.ButtonControl, etc.
|
|
44
|
+
search_params (dict): Search parameters used to identify the control.
|
|
45
|
+
The keys must match the properties used in the control type, e.g., 'AutomationId', 'Name'.
|
|
46
|
+
search_depth (int): How deep to search in the user interface.
|
|
47
|
+
timeout (int): Maximum time to wait for the control, in seconds.
|
|
48
|
+
retry_interval (float): Time to wait between retries, in seconds.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Control: The control object if found, otherwise raises TimeoutError.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
TimeoutError: If the control is not found within the timeout period.
|
|
55
|
+
"""
|
|
56
|
+
end_time = time.time() + timeout
|
|
57
|
+
while time.time() < end_time:
|
|
58
|
+
try:
|
|
59
|
+
control = control_type(searchDepth=search_depth, **search_params)
|
|
60
|
+
if control.Exists(0, 0):
|
|
61
|
+
return control
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error while searching for control: {e}")
|
|
64
|
+
|
|
65
|
+
time.sleep(retry_interval)
|
|
66
|
+
print(f"Retrying to find control: {search_params}...")
|
|
67
|
+
|
|
68
|
+
raise TimeoutError(f"Control with parameters {search_params} was not found within the {timeout} second timeout.")
|
|
69
|
+
|
|
70
|
+
def wait_for_control_to_disappear(self, control_type, search_params, search_depth=1, timeout=30):
|
|
71
|
+
"""
|
|
72
|
+
Waits for a given control type to disappear with the specified search parameters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
control_type: The type of control, e.g., auto.WindowControl, auto.ButtonControl, etc.
|
|
76
|
+
search_params (dict): Search parameters used to identify the control.
|
|
77
|
+
The keys must match the properties used in the control type, e.g., 'AutomationId', 'Name'.
|
|
78
|
+
search_depth (int): How deep to search in the user interface.
|
|
79
|
+
timeout (int): How long to wait, in seconds.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
bool: True if the control disappeared within the timeout period, otherwise False.
|
|
83
|
+
"""
|
|
84
|
+
end_time = time.time() + timeout
|
|
85
|
+
while time.time() < end_time:
|
|
86
|
+
try:
|
|
87
|
+
control = control_type(searchDepth=search_depth, **search_params)
|
|
88
|
+
if not control.Exists(0, 0):
|
|
89
|
+
return True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Error while searching for control: {e}")
|
|
92
|
+
|
|
93
|
+
time.sleep(0.5)
|
|
94
|
+
print(f"Retrying to find control: {search_params}...")
|
|
95
|
+
|
|
96
|
+
raise TimeoutError(f"Control with parameters {search_params} did not disappear within the timeout period.")
|
|
97
|
+
|
|
98
|
+
def close_window(self, window_to_close: auto.WindowControl) -> None:
|
|
99
|
+
"""Closes specified window by sending CTRL+F4 keystroke."""
|
|
100
|
+
window_to_close.SendKeys(text="^({F4})")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import uiautomation as auto
|
|
2
|
+
|
|
3
|
+
from .base_ui import BaseUI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClinicHandler(BaseUI):
|
|
7
|
+
"""
|
|
8
|
+
Handles changing the primart clinic for a patient (if it's not "Tandplejen Aarhus").
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def change_primary_clinic(self, current_primary_clinic: str, is_field_locked: bool):
|
|
12
|
+
"""
|
|
13
|
+
Changes the primary clinic for the patient.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
self.open_tab("Stamkort")
|
|
17
|
+
|
|
18
|
+
if current_primary_clinic != "Tandplejen Aarhus":
|
|
19
|
+
if is_field_locked:
|
|
20
|
+
locked_field = self.wait_for_control(
|
|
21
|
+
auto.CheckBoxControl,
|
|
22
|
+
{'AutomationId': 'CheckPatientClinicRegLocked'},
|
|
23
|
+
search_depth=9
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if locked_field.GetPattern(auto.PatternId.TogglePattern).ToggleState:
|
|
27
|
+
locked_field.GetPattern(auto.PatternId.TogglePattern).Toggle()
|
|
28
|
+
locked_field.SendKeys('{Ctrl}s', waitTime=0)
|
|
29
|
+
|
|
30
|
+
box_clinic_parent = self.wait_for_control(
|
|
31
|
+
auto.GroupControl,
|
|
32
|
+
{'AutomationId': 'GroupBoxPatientDentalInfo'},
|
|
33
|
+
search_depth=8
|
|
34
|
+
)
|
|
35
|
+
box_clinic = box_clinic_parent.PaneControl(
|
|
36
|
+
searchDepth=2,
|
|
37
|
+
AutomationId="ControlClinicSelectorPatientClinicReg"
|
|
38
|
+
).PaneControl(
|
|
39
|
+
searchDepth=2,
|
|
40
|
+
AutomationId="PictureBoxClinic"
|
|
41
|
+
)
|
|
42
|
+
box_clinic.Click(simulateMove=False, waitTime=0)
|
|
43
|
+
|
|
44
|
+
clinic_list = self.wait_for_control(
|
|
45
|
+
auto.WindowControl,
|
|
46
|
+
{'AutomationId': 'FormFindClinics'},
|
|
47
|
+
search_depth=2
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
clinic_list_items = clinic_list.ListControl(AutomationId="ListClinics").ListItemControl(Name="Tandplejen Aarhus")
|
|
51
|
+
clinic_list_items.GetPattern(10017).ScrollIntoView()
|
|
52
|
+
clinic_list_items.SetFocus()
|
|
53
|
+
clinic_list_items.DoubleClick(simulateMove=False, waitTime=0)
|
|
54
|
+
|
|
55
|
+
locked_field = self.wait_for_control(
|
|
56
|
+
auto.CheckBoxControl,
|
|
57
|
+
{'AutomationId': 'CheckPatientClinicRegLocked'},
|
|
58
|
+
search_depth=9
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if locked_field.GetPattern(auto.PatternId.TogglePattern).ToggleState == 0:
|
|
62
|
+
locked_field.GetPattern(auto.PatternId.TogglePattern).Toggle()
|
|
63
|
+
locked_field.SendKeys('{Ctrl}s', waitTime=0)
|
|
64
|
+
|
|
65
|
+
self.wait_for_control(
|
|
66
|
+
auto.TextControl,
|
|
67
|
+
{'Name': 'Patient er gemt.'},
|
|
68
|
+
search_depth=3
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print("Primary clinic changed successfully.")
|
|
72
|
+
print("Patient already has the primary clinic set to 'Tandplejen Aarhus'")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"Error while changing primary clinic: {e}")
|
|
75
|
+
raise
|