mbu-dev-shared-components 2.3.0__tar.gz → 2.4.3__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-2.3.0 → mbu_dev_shared_components-2.4.3}/PKG-INFO +6 -2
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/msoffice365/sharepoint_api/files.py +273 -4
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/appointment.py +9 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/base_ui.py +22 -6
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/document.py +3 -1
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/edi_portal.py +17 -3
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/patient.py +5 -3
- mbu_dev_shared_components-2.4.3/mbu_dev_shared_components/tests/go_tests/go_integration_tests.py +278 -0
- mbu_dev_shared_components-2.4.3/mbu_dev_shared_components/tests/go_tests/objects_tests.py +124 -0
- mbu_dev_shared_components-2.4.3/mbu_dev_shared_components/tests/msoffice_tests/msoffice_integration_tests.py +188 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components.egg-info/PKG-INFO +6 -2
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components.egg-info/SOURCES.txt +3 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components.egg-info/requires.txt +5 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/pyproject.toml +13 -2
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/LICENSE +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/README.md +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/database/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/database/constants.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/database/logging.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/database/utility.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/auth.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/cases.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/contacts.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/documents.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/getorganized/objects.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/google/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/google/api/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/google/api/auth.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/google/workspace/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/google/workspace/alerts.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/msoffice365/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/msoffice365/excel/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/msoffice365/excel/excel_reader.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/msoffice365/sharepoint_api/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/os2forms/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/os2forms/documents.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/os2forms/forms.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/romexis/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/romexis/db_handler.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/romexis/helper_functions.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/sap/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/sap/create_invoice.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/app_handler.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/clinic.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/event.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/exceptions.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/handler_base.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/application/journal_note.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/database/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/solteqtand/database/db_handler.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/utils/__init__.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/utils/db_stored_procedure_executor.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/utils/fernet_encryptor.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/utils/file_handler.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components/utils/json_handler.py +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components.egg-info/dependency_links.txt +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/mbu_dev_shared_components.egg-info/top_level.txt +0 -0
- {mbu_dev_shared_components-2.3.0 → mbu_dev_shared_components-2.4.3}/setup.cfg +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mbu_dev_shared_components
|
|
3
|
-
Version: 2.3
|
|
3
|
+
Version: 2.4.3
|
|
4
4
|
Summary: Shared components to use in RPA projects
|
|
5
5
|
Author-email: MBU <rpa@mbu.aarhus.dk>
|
|
6
|
+
License-Expression: MIT
|
|
6
7
|
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: Microsoft :: Windows
|
|
9
9
|
Requires-Python: >=3.7
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
@@ -19,7 +19,11 @@ Requires-Dist: uiautomation
|
|
|
19
19
|
Requires-Dist: pillow
|
|
20
20
|
Requires-Dist: psutil
|
|
21
21
|
Requires-Dist: docx2pdf
|
|
22
|
+
Requires-Dist: pandas>=2.2.3
|
|
22
23
|
Requires-Dist: rawpy
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-dependency>=0.5.1; extra == "dev"
|
|
23
27
|
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|
# MBU Dev Shared Components
|
|
@@ -29,12 +29,25 @@ Example:
|
|
|
29
29
|
sp.download_files("FolderName", "C:\\LocalPath")
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
from pathlib import PurePath
|
|
33
|
-
from typing import Optional, List
|
|
34
32
|
import os
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
|
|
36
|
+
import traceback
|
|
37
|
+
|
|
38
|
+
from pathlib import PurePath
|
|
39
|
+
|
|
40
|
+
from io import BytesIO
|
|
41
|
+
|
|
42
|
+
from typing import Optional, List, Dict, Any
|
|
43
|
+
|
|
44
|
+
from openpyxl.styles import Font, Alignment
|
|
45
|
+
from openpyxl import load_workbook
|
|
46
|
+
|
|
47
|
+
import pandas as pd
|
|
48
|
+
|
|
35
49
|
from office365.runtime.auth.user_credential import UserCredential
|
|
36
50
|
from office365.sharepoint.client_context import ClientContext
|
|
37
|
-
|
|
38
51
|
from office365.sharepoint.files.file import File
|
|
39
52
|
|
|
40
53
|
|
|
@@ -136,7 +149,6 @@ class Sharepoint:
|
|
|
136
149
|
file_content = File.open_binary(self.ctx, file_url)
|
|
137
150
|
return file_content.content
|
|
138
151
|
except Exception:
|
|
139
|
-
import traceback
|
|
140
152
|
print("Failed to download file:")
|
|
141
153
|
traceback.print_exc()
|
|
142
154
|
return None
|
|
@@ -249,3 +261,260 @@ class Sharepoint:
|
|
|
249
261
|
print(f"File '{file_name}' uploaded successfully to '{folder_url}'.")
|
|
250
262
|
except Exception as e:
|
|
251
263
|
print(f"Failed to upload file '{file_name}': {e}")
|
|
264
|
+
|
|
265
|
+
def append_row_to_sharepoint_excel(
|
|
266
|
+
self,
|
|
267
|
+
required_headers: Optional[List[str]] = None,
|
|
268
|
+
folder_name: str = "",
|
|
269
|
+
excel_file_name: str = "",
|
|
270
|
+
sheet_name: str = "",
|
|
271
|
+
new_row: Dict = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
• Appends a row to an existing Excel file.
|
|
275
|
+
• Sorts and formats based on provided parameters.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# 1. Pull file
|
|
279
|
+
binary_file = self.fetch_file_using_open_binary(excel_file_name, folder_name)
|
|
280
|
+
|
|
281
|
+
if binary_file is None:
|
|
282
|
+
raise FileNotFoundError(f"File '{excel_file_name}' not found in folder '{folder_name}'.")
|
|
283
|
+
|
|
284
|
+
wb = load_workbook(BytesIO(binary_file))
|
|
285
|
+
|
|
286
|
+
if sheet_name not in wb.sheetnames:
|
|
287
|
+
raise ValueError(f"Sheet '{sheet_name}' not found in '{excel_file_name}'")
|
|
288
|
+
|
|
289
|
+
ws = wb[sheet_name]
|
|
290
|
+
|
|
291
|
+
# 2. Validate headers
|
|
292
|
+
if required_headers:
|
|
293
|
+
current_headers = [cell.value for cell in ws[1]]
|
|
294
|
+
|
|
295
|
+
if current_headers != required_headers:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"Header mismatch in sheet '{sheet_name}'!\n"
|
|
298
|
+
f"Expected: {required_headers}\n"
|
|
299
|
+
f"Found: {current_headers}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# 2.5 Clean up empty rows before appending
|
|
303
|
+
for row_idx in range(ws.max_row, 1, -1): # Start from bottom, skip header
|
|
304
|
+
row_values = [cell.value for cell in ws[row_idx]]
|
|
305
|
+
|
|
306
|
+
if all(cell is None for cell in row_values):
|
|
307
|
+
ws.delete_rows(row_idx)
|
|
308
|
+
|
|
309
|
+
# 3. Append new row to sheet
|
|
310
|
+
ws.append([new_row.get(header.value, "") for header in ws[1]])
|
|
311
|
+
|
|
312
|
+
# 4. Save and upload
|
|
313
|
+
temp_stream = BytesIO()
|
|
314
|
+
|
|
315
|
+
wb.save(temp_stream)
|
|
316
|
+
|
|
317
|
+
temp_stream.seek(0)
|
|
318
|
+
|
|
319
|
+
self.upload_file_from_bytes(temp_stream.getvalue(), excel_file_name, folder_name)
|
|
320
|
+
|
|
321
|
+
print(f"✔ Added row + sorted '{sheet_name}' in '{excel_file_name}'.")
|
|
322
|
+
|
|
323
|
+
def format_and_sort_excel_file(
|
|
324
|
+
self,
|
|
325
|
+
folder_name: str,
|
|
326
|
+
excel_file_name: str,
|
|
327
|
+
sheet_name: str,
|
|
328
|
+
sorting_keys: Optional[List[Dict[str, Any]]] = None,
|
|
329
|
+
font_config: Optional[Dict[int, Dict[str, Any]]] = None,
|
|
330
|
+
bold_rows: Optional[List[int]] = None,
|
|
331
|
+
italic_rows: Optional[List[int]] = None,
|
|
332
|
+
align_horizontal: str = "center",
|
|
333
|
+
align_vertical: str = "center",
|
|
334
|
+
column_widths: Any = "auto",
|
|
335
|
+
freeze_panes: Optional[str] = None,
|
|
336
|
+
):
|
|
337
|
+
"""
|
|
338
|
+
Sorts and formats an Excel worksheet based on provided styling and sorting rules.
|
|
339
|
+
|
|
340
|
+
Params:
|
|
341
|
+
folder_name: Name of the folder where the file resides
|
|
342
|
+
excel_file_name: Name of the excel file
|
|
343
|
+
sheet_name: Name of the sheet that will be sorted
|
|
344
|
+
sorting_keys: List of dicts like [{"key": "A", "ascending": True, "type": "datetime"}]
|
|
345
|
+
bold_rows: List of row numbers to bold (defaults to [1])
|
|
346
|
+
italic_rows: List of row numbers to italicize
|
|
347
|
+
font_config: Dict of row -> font config (overrides bold/italic)
|
|
348
|
+
align_horizontal: Horizontal text alignment
|
|
349
|
+
align_vertical: Vertical text alignment
|
|
350
|
+
column_widths: "auto" or an int to represent a pixel value
|
|
351
|
+
freeze_panes: E.g., "A2" to freeze header row
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Modified worksheet
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
# Step 1 - Fetch the file to update from SharePoint and load it as a workbook
|
|
358
|
+
# This ensures we don't override any other sheets in the excel file
|
|
359
|
+
binary_file = self.fetch_file_using_open_binary(excel_file_name, folder_name)
|
|
360
|
+
if binary_file is None:
|
|
361
|
+
raise FileNotFoundError(f"File '{excel_file_name}' not found in folder '{folder_name}'.")
|
|
362
|
+
|
|
363
|
+
wb = load_workbook(BytesIO(binary_file))
|
|
364
|
+
if sheet_name not in wb.sheetnames:
|
|
365
|
+
raise ValueError(f"Sheet '{sheet_name}' not found in '{excel_file_name}'")
|
|
366
|
+
|
|
367
|
+
ws = wb[sheet_name]
|
|
368
|
+
|
|
369
|
+
# Step 2 - Read data into DataFrame
|
|
370
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
371
|
+
header, *data_rows = rows
|
|
372
|
+
df = pd.DataFrame(data_rows, columns=header)
|
|
373
|
+
|
|
374
|
+
# Step 3 – Prepare sorting logic
|
|
375
|
+
# For each sorting instruction, we:
|
|
376
|
+
# - Extract the column to sort by (using letter, index, or name)
|
|
377
|
+
# - Convert the column values to the desired data type if specified (str, int, float, datetime)
|
|
378
|
+
# - Track which columns to sort and in which order (ascending or descending)
|
|
379
|
+
#
|
|
380
|
+
# This ensures the DataFrame is sorted correctly, even when types like dates or numbers need conversion.
|
|
381
|
+
if sorting_keys:
|
|
382
|
+
sort_columns = []
|
|
383
|
+
ascending_flags = []
|
|
384
|
+
|
|
385
|
+
for item in sorting_keys:
|
|
386
|
+
key = item.get("key")
|
|
387
|
+
ascending = item.get("ascending", True)
|
|
388
|
+
dtype = item.get("type")
|
|
389
|
+
|
|
390
|
+
if isinstance(key, int):
|
|
391
|
+
col_name = header[key]
|
|
392
|
+
|
|
393
|
+
elif isinstance(key, str) and key.isalpha():
|
|
394
|
+
col_name = header[ord(key.upper()) - ord("A")]
|
|
395
|
+
|
|
396
|
+
else:
|
|
397
|
+
col_name = key
|
|
398
|
+
|
|
399
|
+
sort_columns.append(col_name)
|
|
400
|
+
ascending_flags.append(ascending)
|
|
401
|
+
|
|
402
|
+
if dtype == "datetime":
|
|
403
|
+
df[col_name] = pd.to_datetime(df[col_name], dayfirst=True, errors="coerce")
|
|
404
|
+
|
|
405
|
+
elif dtype == "int":
|
|
406
|
+
df[col_name] = pd.to_numeric(df[col_name], errors="coerce", downcast="integer")
|
|
407
|
+
|
|
408
|
+
elif dtype == "float":
|
|
409
|
+
df[col_name] = pd.to_numeric(df[col_name], errors="coerce", downcast="float")
|
|
410
|
+
|
|
411
|
+
elif dtype == "str":
|
|
412
|
+
df[col_name] = df[col_name].astype(str)
|
|
413
|
+
|
|
414
|
+
# Step 4 – Sort
|
|
415
|
+
df.sort_values(by=sort_columns, ascending=ascending_flags, inplace=True)
|
|
416
|
+
|
|
417
|
+
# Step 5 - Overwrite worksheet
|
|
418
|
+
ws.delete_rows(1, ws.max_row)
|
|
419
|
+
|
|
420
|
+
ws.append(header)
|
|
421
|
+
|
|
422
|
+
for _, row in df.iterrows():
|
|
423
|
+
ws.append(list(row))
|
|
424
|
+
|
|
425
|
+
# Step 6 – Adjust column widths and apply wrapping if needed
|
|
426
|
+
#
|
|
427
|
+
# If column_widths is "auto":
|
|
428
|
+
# - Calculate the max content length in each column and set the column width accordingly (+2 for padding)
|
|
429
|
+
#
|
|
430
|
+
# If column_widths is a single int:
|
|
431
|
+
# - Use it as a global max width across all columns
|
|
432
|
+
# - If content fits, set width based on actual content length
|
|
433
|
+
# - If content exceeds the max width clamp column width and enable wrap_text for that column's cells
|
|
434
|
+
#
|
|
435
|
+
# Then, for wrapped cells, auto-adjust the row height:
|
|
436
|
+
# - Estimate how many lines the wrapped text would occupy and set row height accordingly to ensure all content is visible
|
|
437
|
+
if column_widths in (None, "auto"):
|
|
438
|
+
for col in ws.columns:
|
|
439
|
+
max_len = max(len(str(cell.value or "")) for cell in col)
|
|
440
|
+
|
|
441
|
+
ws.column_dimensions[col[0].column_letter].width = max_len + 2
|
|
442
|
+
|
|
443
|
+
elif isinstance(column_widths, int):
|
|
444
|
+
for col in ws.columns:
|
|
445
|
+
col_letter = col[0].column_letter
|
|
446
|
+
|
|
447
|
+
max_len = max(len(str(cell.value or "")) for cell in col)
|
|
448
|
+
|
|
449
|
+
# If content fits, auto-size
|
|
450
|
+
if max_len + 2 <= column_widths:
|
|
451
|
+
ws.column_dimensions[col_letter].width = max_len + 2
|
|
452
|
+
|
|
453
|
+
# Else, cap width and enable wrap
|
|
454
|
+
else:
|
|
455
|
+
ws.column_dimensions[col_letter].width = column_widths
|
|
456
|
+
|
|
457
|
+
for cell in col:
|
|
458
|
+
cell.alignment = Alignment(wrap_text=True)
|
|
459
|
+
|
|
460
|
+
# Here we handle row height
|
|
461
|
+
for row in ws.iter_rows():
|
|
462
|
+
max_line_count = 1
|
|
463
|
+
|
|
464
|
+
for cell in row:
|
|
465
|
+
if cell.value and cell.alignment and cell.alignment.wrap_text:
|
|
466
|
+
col_letter = cell.column_letter
|
|
467
|
+
col_width = ws.column_dimensions[col_letter].width or 10
|
|
468
|
+
chars_per_line = col_width * 1.2
|
|
469
|
+
lines = str(cell.value).split("\n")
|
|
470
|
+
line_count = sum(math.ceil(len(line) / chars_per_line) for line in lines)
|
|
471
|
+
max_line_count = max(max_line_count, line_count)
|
|
472
|
+
|
|
473
|
+
ws.row_dimensions[row[0].row].height = max_line_count * 20
|
|
474
|
+
|
|
475
|
+
else:
|
|
476
|
+
raise ValueError(f"Column width provided with incorrect datatype - datatype int expected, instead column width is of datatype {type(column_widths)}")
|
|
477
|
+
|
|
478
|
+
# Step 7 - Freeze panes if needed
|
|
479
|
+
if freeze_panes:
|
|
480
|
+
ws.freeze_panes = freeze_panes
|
|
481
|
+
|
|
482
|
+
# Step 8 – Apply base formatting
|
|
483
|
+
# For each cell in the worksheet:
|
|
484
|
+
# - Apply font styling based on either a custom `font_config` (row-specific) or default to bold/italic based on row number (e.g., header rows)
|
|
485
|
+
# - Set horizontal and vertical alignment for consistent layout
|
|
486
|
+
# - Disable text wrapping by default (wrapping will be handled later if needed)
|
|
487
|
+
#
|
|
488
|
+
# This ensures a clean, uniform look across the sheet while allowing for custom styling where defined.
|
|
489
|
+
for row_idx, row in enumerate(ws.iter_rows(), start=1):
|
|
490
|
+
for cell in row:
|
|
491
|
+
if font_config and row_idx in font_config:
|
|
492
|
+
config = font_config[row_idx]
|
|
493
|
+
|
|
494
|
+
cell.font = Font(
|
|
495
|
+
name=config.get("name", "Calibri"),
|
|
496
|
+
size=config.get("size", 11),
|
|
497
|
+
bold=config.get("bold", False),
|
|
498
|
+
italic=config.get("italic", False),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
else:
|
|
502
|
+
cell.font = Font(
|
|
503
|
+
bold=row_idx in bold_rows if bold_rows else False,
|
|
504
|
+
italic=row_idx in italic_rows if italic_rows else False,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
cell.alignment = Alignment(
|
|
508
|
+
horizontal=align_horizontal,
|
|
509
|
+
vertical=align_vertical,
|
|
510
|
+
wrap_text=cell.alignment.wrap_text
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Step 9 - Save and re-upload
|
|
514
|
+
temp_stream = BytesIO()
|
|
515
|
+
|
|
516
|
+
wb.save(temp_stream)
|
|
517
|
+
|
|
518
|
+
temp_stream.seek(0)
|
|
519
|
+
|
|
520
|
+
self.upload_file_from_bytes(temp_stream.getvalue(), excel_file_name, folder_name)
|
|
@@ -276,6 +276,7 @@ class AppointmentHandler(HandlerBase):
|
|
|
276
276
|
"""
|
|
277
277
|
try:
|
|
278
278
|
self.open_tab("Stamkort")
|
|
279
|
+
self.open_sub_tab("Behandlingsstatus")
|
|
279
280
|
|
|
280
281
|
create_booking_button = self.wait_for_control(
|
|
281
282
|
auto.PaneControl,
|
|
@@ -304,6 +305,14 @@ class AppointmentHandler(HandlerBase):
|
|
|
304
305
|
case "Stol":
|
|
305
306
|
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["comboBoxChair"])
|
|
306
307
|
|
|
308
|
+
# Fill out text booking field
|
|
309
|
+
text_booking_field_group = manage_booking.GroupControl(AutomationId="GroupBox5")
|
|
310
|
+
for child in text_booking_field_group.GetChildren():
|
|
311
|
+
match child.AutomationId:
|
|
312
|
+
case "TextBoxBookingText":
|
|
313
|
+
if child.GetPattern(auto.PatternId.ValuePattern).Value != booking_reminder_data["textBoxBookingText"]:
|
|
314
|
+
child.GetPattern(auto.PatternId.ValuePattern).SetValue(booking_reminder_data["textBoxBookingText"])
|
|
315
|
+
|
|
307
316
|
# Fill out date and time
|
|
308
317
|
date_and_time_group = manage_booking.GroupControl(AutomationId="GroupBox4")
|
|
309
318
|
|
|
@@ -23,10 +23,12 @@ class BaseUI:
|
|
|
23
23
|
children = control.GetChildren()
|
|
24
24
|
|
|
25
25
|
for child in children:
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (
|
|
27
|
+
(control_type is None or child.ControlType == control_type) and
|
|
28
|
+
(automation_id is None or child.AutomationId == automation_id) and
|
|
29
|
+
(name is None or child.Name == name) and
|
|
30
|
+
(class_name is None or child.ClassName == class_name)
|
|
31
|
+
):
|
|
30
32
|
return child
|
|
31
33
|
|
|
32
34
|
found = self.find_element_by_property(child, control_type, automation_id, name, class_name)
|
|
@@ -96,5 +98,19 @@ class BaseUI:
|
|
|
96
98
|
raise TimeoutError(f"Control with parameters {search_params} did not disappear within the timeout period.")
|
|
97
99
|
|
|
98
100
|
def close_window(self, window_to_close: auto.WindowControl) -> None:
|
|
99
|
-
"""Closes specified window
|
|
100
|
-
window_to_close.
|
|
101
|
+
"""Closes specified window."""
|
|
102
|
+
window_name = window_to_close.Name
|
|
103
|
+
window_to_close.SetFocus()
|
|
104
|
+
window_to_close.GetWindowPattern().Close()
|
|
105
|
+
|
|
106
|
+
# Handle popup when closin main window
|
|
107
|
+
if window_name.lower().startswith("hovedvindue"):
|
|
108
|
+
|
|
109
|
+
pop_up_window = window_to_close.WindowControl(Name="TMT - Afslut")
|
|
110
|
+
pop_up_window.SetFocus()
|
|
111
|
+
pop_up_window.ButtonControl(Name="Ja").Click(simulateMove=False, waitTime=0)
|
|
112
|
+
|
|
113
|
+
time.sleep(2)
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
self.app_window = self.wait_for_control(search_params={'AutomationId': 'FormFront'}, control_type=auto.WindowControl)
|
|
@@ -5,6 +5,7 @@ import time
|
|
|
5
5
|
import psutil
|
|
6
6
|
from docx2pdf import convert
|
|
7
7
|
import uiautomation as auto
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
|
|
9
10
|
from .handler_base import HandlerBase
|
|
10
11
|
|
|
@@ -96,6 +97,8 @@ class DocumentHandler(HandlerBase):
|
|
|
96
97
|
Under “Print/Flet patienter” → select template → merge → wait for Word to open,
|
|
97
98
|
convert to PDF, kill WINWORD.EXE, then create_document() with the new PDF.
|
|
98
99
|
"""
|
|
100
|
+
folder_path = rf"{os.environ.get('USERPROFILE')}\AppData\Local\Temp\Care\TMTand"
|
|
101
|
+
|
|
99
102
|
try:
|
|
100
103
|
self.open_tab("Stamkort")
|
|
101
104
|
|
|
@@ -168,7 +171,6 @@ class DocumentHandler(HandlerBase):
|
|
|
168
171
|
if new_value != metadata['templateName']:
|
|
169
172
|
raise ValueError(f"Failed to set the correct status. Expected '{metadata['templateName']}', but got '{new_value}'.")
|
|
170
173
|
|
|
171
|
-
folder_path = rf"{os.environ.get('USERPROFILE')}\AppData\Local\Temp\Care\TMTand"
|
|
172
174
|
shutil.rmtree(folder_path, ignore_errors=True)
|
|
173
175
|
|
|
174
176
|
form_mail_merge.PaneControl(AutomationId="ButtonMerge").GetLegacyIAccessiblePattern().DoDefaultAction()
|
|
@@ -28,6 +28,8 @@ class EDIHandler(HandlerBase):
|
|
|
28
28
|
)
|
|
29
29
|
journalforsendelse_button.Click(simulateMove=False, waitTime=0)
|
|
30
30
|
|
|
31
|
+
time.sleep(5)
|
|
32
|
+
|
|
31
33
|
except Exception as e:
|
|
32
34
|
print(f"Error while opening EDI Portal: {e}")
|
|
33
35
|
|
|
@@ -70,7 +72,7 @@ class EDIHandler(HandlerBase):
|
|
|
70
72
|
if not next_button:
|
|
71
73
|
try:
|
|
72
74
|
next_button = self.wait_for_control(
|
|
73
|
-
auto.ButtonControl, {"
|
|
75
|
+
auto.ButtonControl, {"AutomationId": "patientInformationNextButton"},
|
|
74
76
|
search_depth=50,
|
|
75
77
|
timeout=5
|
|
76
78
|
)
|
|
@@ -97,6 +99,14 @@ class EDIHandler(HandlerBase):
|
|
|
97
99
|
dict: A dictionary containing the row count and whether the phone number matches.
|
|
98
100
|
"""
|
|
99
101
|
try:
|
|
102
|
+
# Handle Hasle Torv Clinic special case
|
|
103
|
+
if extern_clinic_data[0]["contractorId"] == "477052" or extern_clinic_data[0]["contractorId"] == "470678":
|
|
104
|
+
contractor_id = "485055"
|
|
105
|
+
clinic_phone_number = "86135240"
|
|
106
|
+
else:
|
|
107
|
+
contractor_id = extern_clinic_data[0]["contractorId"]
|
|
108
|
+
clinic_phone_number = extern_clinic_data[0]["phoneNumber"]
|
|
109
|
+
|
|
100
110
|
self.edi_portal_click_next_button(sleep_time=2)
|
|
101
111
|
|
|
102
112
|
class_options = [
|
|
@@ -120,7 +130,7 @@ class EDIHandler(HandlerBase):
|
|
|
120
130
|
raise RuntimeError("Search box not found")
|
|
121
131
|
search_box.SetFocus()
|
|
122
132
|
search_box_value_pattern = search_box.GetPattern(auto.PatternId.ValuePattern)
|
|
123
|
-
search_box_value_pattern.SetValue(
|
|
133
|
+
search_box_value_pattern.SetValue(contractor_id)
|
|
124
134
|
search_box.SendKeys("{ENTER}")
|
|
125
135
|
|
|
126
136
|
time.sleep(sleep_time)
|
|
@@ -134,10 +144,14 @@ class EDIHandler(HandlerBase):
|
|
|
134
144
|
row_count = grid_pattern.RowCount
|
|
135
145
|
|
|
136
146
|
is_phone_number_match = False
|
|
147
|
+
|
|
148
|
+
if grid_pattern.GetItem(1, 0).Name == "Ingen data i tabellen":
|
|
149
|
+
return {"rowCount": 0, "isPhoneNumberMatch": False}
|
|
150
|
+
|
|
137
151
|
if row_count > 0:
|
|
138
152
|
for row in range(row_count):
|
|
139
153
|
phone_number = grid_pattern.GetItem(row, 4).Name
|
|
140
|
-
if phone_number ==
|
|
154
|
+
if phone_number == clinic_phone_number:
|
|
141
155
|
is_phone_number_match = True
|
|
142
156
|
break
|
|
143
157
|
return {"rowCount": row_count, "isPhoneNumberMatch": is_phone_number_match}
|
|
@@ -112,13 +112,14 @@ class PatientHandler(HandlerBase):
|
|
|
112
112
|
TimeoutError: If the patient window does not close within the expected time.
|
|
113
113
|
"""
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
self.app_window.SetFocus()
|
|
116
|
+
self.app_window.GetWindowPattern().Close()
|
|
117
117
|
|
|
118
118
|
self.app_window = self.wait_for_control_to_disappear(
|
|
119
119
|
auto.WindowControl,
|
|
120
120
|
{'AutomationId': 'FormPatient'},
|
|
121
|
-
search_depth=2
|
|
121
|
+
search_depth=2,
|
|
122
|
+
timeout=30
|
|
122
123
|
)
|
|
123
124
|
|
|
124
125
|
self.app_window = self.wait_for_control(
|
|
@@ -127,6 +128,7 @@ class PatientHandler(HandlerBase):
|
|
|
127
128
|
search_depth=2,
|
|
128
129
|
timeout=5
|
|
129
130
|
)
|
|
131
|
+
self.app_window.SetFocus()
|
|
130
132
|
|
|
131
133
|
def change_status(self, status: str) -> None:
|
|
132
134
|
"""
|