diff --git a/.gitignore b/.gitignore index 8c72241..5f2bf0a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ reports/ CREDENTIALS* *.pkl *.pickle +# docs +*.pdf # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 83f30a6..925ae5c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ -# WCE: NAFKA --- GUI +# WCE/NAFKA: CRM GUI -... +## Allgemeine Hinweise + +Die vorliegende Anwendung befindet sich nach aktuellem Stand (Mai 2026) noch in einem sehr frühen Teststadium. Weder das Programmverhalten noch das Erscheinungsbild sind repräsentativ für das angestrebte Endergebnis. Es kann beim Testen zu Fehlern und Abstürzen kommen. Die Anwendung schreibt zwar selbstständig Fehlerlogs, dennoch ist es in Absturzfällen hilfreich, wenn hierzu ein kurzer Fehlerbericht erstellt wird. Dieser sollte die Schritte beinhalten, die durch den Nutzer erfolgt sind, damit der Fehler reproduziert und schlussendlich behoben werden kann. + +## Aktuelle Einschränkungen + +Dem Programm liegt aktuell ein hitorische Kopie der Kontaktliste vor, die für die Verknüpfung von Unternehmensdaten genutzt wird. Das bedeutet, dass sich Änderungen an der tatsächlichen, aktuell verwendeten Kontaktliste *nicht* in der Anwendung widerspiegeln. Die Anbindung an die in Betrieb befindliche Kontaktliste wird noch vorgenommen. + +Eine Nutzerverwatung gibt es aktuell noch nicht. Dementsprechend wird in der Anwendung auch nicht gespeichert, von wem die Änderungen zuletzt durchgeführt wurden. Ein Nutzerfeld existiert in der Oberfläche zwar, wird aber nicht gefüllt und bleibt aktuell noch stets leer. + +## Start der Anwendung + +Die Anwendung besteht aus einem Ordner, der alle relevanten Dateien zu ihrer Ausführung beinhaltet. Wenn der Ordner geöffnet wird, befinden sich darin die benötigten Unterordner und eine *Batch-Datei* (Endung ".bat"). Mit einem Doppelklick auf diese Datei, wird die Anwendung gestartet. Dies kann einige Sekunden in Anspruch nehmen. Sollte die Anwendung nach mehr als zehn Sekunden nicht gestartet sein, so wird vermutlich die Ausführung blockiert. In diesem Fall sind Ausführungsrichtlinien und Anti-Virus-Software durch die Administration zu prüfen. Sollte die Anwendung auch nach Tätigkeit der Administration nicht starten, so ist bitte *Kontakt zu d-opt* aufzunehmen, damit das Problem behoben werden kann. + +## Verwendung + +In der aktuellen Version ist die Grunderfassung für Unternehmen implementiert. Formularfelder und grundlegende Optik orientieren sich an den bereitgestellten Anforderungsdokumenten inkl. etwaiger Mock-Ups. Zur Anlage eines neuen Eintrags ist in der Übersicht oben die Schaltfläche "Neu" zu wählen. Anschließend kann die Grunderfassung für Unternehmen gewählt werden. Daraufhin öffnet sich eine Formularübersicht. Pflichtfelder sind mit einem Sternchen "*" gekennzeichnet. Sollten Pflichtfelder beim Speichern fehlen, so werden diese in einem Hinweisfenster benannt und im Formular selbst rot hervorgehoben. Das Formular kann mit den Knöpfen ganz unten gespeichert oder zurückgesetzt werden. Darüber hinaus sind Tastenkürzel für beide Aktionen hinterlegt: "Strg + s" für das Speichern und "Strg + z" für das Zurücksetzen. + +Sind alle Felder gefüllt, werden im Hintergrund die Daten nach dem Auslösen des Speichervorgangs nochmals validiert. Beispielsweise werden erfasste E-Mail-Adressen auf Gültigkeit geprüft. Etwaige Fehler werden dem Nutzer ebenfalls angezeigt. + +Nach der erfolgreichen Speicherung kann über die Schaltfläche im oberen Bereich "zurück zur Übersicht" auf die Hauptseite gewechselt werden. Dort sollte nun der neu angelegte Eintrag sichtbar sein. Die Übersicht ist absteigend nach dem Datum der letzten Änderung sortiert. Ein eben gespeicherter Eintrag sollte sich demzufolge ganz oben in der Übersicht befinden. Mit einem Klick auf einen Eintrag in der Übersicht wird erneut das Formular zur Grunderfassung mit den hinterlegten Daten geladen und angezeigt. Es können Änderungen vorgenommen und gespeichert werden. + +*Die Grunderfassung für Individualpersonen ist aktuell nur als Dummy verfügbar. Bei einem Klick auf die entsprechende Schaltfläche passiert nichts.* diff --git a/docs/Versionshistorie.md b/docs/Versionshistorie.md new file mode 100644 index 0000000..dfe1622 --- /dev/null +++ b/docs/Versionshistorie.md @@ -0,0 +1,5 @@ +# Versionshistorie (Changelog) + +## 29.05.2026 (Version: v0.1.1dev) (Tag: ExtTest-20260529) + +- initiale Version für erste Feedback-Schleife diff --git a/pdm.lock b/pdm.lock index 46bbfb0..cd1f265 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "lint", "nb", "tests"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:4b3f779c4a10819bdc84e071dbf4016edd7429afeb4697b5ac7e622b3c870d87" +content_hash = "sha256:25812ee6ba42033e1341c1799716828d8582092826c1d0889ea775a0f5c548a2" [[metadata.targets]] requires_python = ">=3.11,<3.14" @@ -975,7 +975,7 @@ files = [ [[package]] name = "dopt-basics" -version = "0.2.5" +version = "0.2.6" requires_python = ">=3.11" summary = "basic cross-project tools for Python-based d-opt projects" groups = ["default"] @@ -983,8 +983,8 @@ dependencies = [ "tzdata>=2025.1", ] files = [ - {file = "dopt_basics-0.2.5-py3-none-any.whl", hash = "sha256:e5a920054463af782ff05526d2ae726d2d3533d7a6419a0f1edeee43b0118ca5"}, - {file = "dopt_basics-0.2.5.tar.gz", hash = "sha256:3f26a1c2d9557c41ec9b6452ee0e2cfd256e64160386708a2f69b2e36b403ad3"}, + {file = "dopt_basics-0.2.6-py3-none-any.whl", hash = "sha256:f0818e2f83e91fb7d398bcabfc6c420159757d7d093b20574b88a3abc24e3eab"}, + {file = "dopt_basics-0.2.6.tar.gz", hash = "sha256:0e90d0d7a711e0dee9f898574683442644d3145ac8905d38ea23775f62aa5d2b"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 877c05b..5f1caf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "wce-crm" -version = "0.1.1dev4" +version = "0.1.1dev7" description = "GUI for CRM of NAFKA project with WCE" authors = [ {name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"}, ] -dependencies = ["pyside6>=6.11.0", "sqlalchemy[asyncio]>=2.0.50", "polars>=1.40.1", "dopt-basics>=0.2.5", "pydantic[email]>=2.13.4", "babel>=2.18.0", "python-dotenv>=1.2.2"] +dependencies = ["pyside6>=6.11.0", "sqlalchemy[asyncio]>=2.0.50", "polars>=1.40.1", "dopt-basics>=0.2.6", "pydantic[email]>=2.13.4", "babel>=2.18.0", "python-dotenv>=1.2.2"] requires-python = "<3.14,>=3.11" readme = "README.md" license = {text = "LicenseRef-Proprietary"} @@ -71,7 +71,7 @@ directory = "reports/coverage" [tool.bumpversion] -current_version = "0.1.1dev4" +current_version = "0.1.1dev7" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. diff --git a/scripts/build.ps1 b/scripts/build.ps1 index b66bc4e..844618f 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -8,6 +8,7 @@ $ENV_PATH = 'B:\deployments\WCE-NAFKA\dopt_nafka_wce-crm' $PY_PATH = Join-Path -Path $ENV_PATH -ChildPath 'python' $SRC_PATH = (Get-Location).Path + Write-Output "Build Pipeline for d-opt WCE/NAFKA project" Write-Output "Delete existing artifacts..." @@ -41,23 +42,23 @@ if ($? -eq $false){ } Write-Output "Built package successfully" -# manual -# Write-Output "Generate manual..." -# .\scripts\cvt_manual.ps1 -# if ($? -eq $false){ -# Write-Output "[PWSH] Exiting script because there errors while generating the README file" -# Exit -# } -# Write-Output "Generated manual file successfully" -# Write-Output "Copying manual file..." -# $readme_src_path = Join-Path -Path $SRC_PATH -ChildPath 'docs\manual.pdf' -# $readme_dest_path = Join-Path -Path $ENV_PATH -ChildPath 'manual' -# Copy-Item -Path $readme_src_path -Destination $readme_dest_path -Force -# if ($? -eq $false){ -# Write-Output "[PWSH] Exiting script because there were errors while copying the manual file" -# Exit -# } -# Write-Output "Copied manual file successfully" +# documentation +Write-Output "Generate docs..." +# $doc_build_script = Join-Path -Path $SRC_PATH -ChildPath "scripts\cvt_docs.ps1" -Resolve +.\scripts\cvt_docs.ps1 +if ($? -eq $false){ + Write-Output "[PWSH] Exiting script because there errors while generating the doc files" + Exit +} +Write-Output "Generated doc files successfully" +Write-Output "Copying doc files..." +$docs_src_path = Join-Path -Path $SRC_PATH -ChildPath 'docs\*.pdf' +Copy-Item -Path $docs_src_path -Destination $ENV_PATH -Force +if ($? -eq $false){ + Write-Output "[PWSH] Exiting script because there were errors while copying the doc files" + Exit +} +Write-Output "Copied doc files successfully" Write-Output "Go into env directory..." diff --git a/scripts/cvt_docs.ps1 b/scripts/cvt_docs.ps1 new file mode 100644 index 0000000..9f41dd9 --- /dev/null +++ b/scripts/cvt_docs.ps1 @@ -0,0 +1,3 @@ +# convert README Markdown file to PDF as a manual +pandoc .\README.md -o .\docs\01_Kurzanleitung.pdf -V geometry:"a4paper, margin=2.5cm" -V header-includes="\usepackage[none]{hyphenat}" +pandoc .\docs\Versionshistorie.md -o .\docs\02_Versionshistorie.pdf -V geometry:"a4paper, margin=2.5cm" -V header-includes="\usepackage[none]{hyphenat}" diff --git a/src/wce_crm/README.md b/src/wce_crm/README.md new file mode 100644 index 0000000..4ea9ef6 --- /dev/null +++ b/src/wce_crm/README.md @@ -0,0 +1,7 @@ +# List of environment variables + +- DOPT_DEVELOPMENT: flag which signals that the current environment is in development mode +- DOPT_STOP_FOLDER_NAME: stop folder to find base path +- DOPT_DB_CRM: path to CRM database, relative to base path +- DOPT_DB_MAIN: path to main database, relative to base path +- DOPT_PATH_LOGGING: path to logging folder, relative to base path diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index a2c5605..a6e669e 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -8,6 +8,7 @@ import polars as pl import sqlalchemy as sql from wce_crm import db +from wce_crm.logging import logger_back as logger class CompanyInfo(TypedDict): @@ -83,6 +84,7 @@ def _transform_for_gui_output( def comp_search_choices() -> tuple[tuple[str, int], ...]: # TODO no reload functionality + logger.debug("[Call backend] comp_search_choices") q = db.DF_CRM_MASTER.lazy() counter = pl.int_range(0, pl.len()).over(pl.col.ma_unternehmensname) q = q.with_columns( @@ -98,6 +100,7 @@ def comp_search_choices() -> tuple[tuple[str, int], ...]: def comp_search_get_info( ma_id: int, ) -> CompanyInfo: + logger.debug("[Call backend] comp_search_get_info") df = db.DF_CRM_MASTER.filter(pl.col.ma_id == ma_id) if df.height > 1 or df.height == 0: raise ValueError(f"Größe des zurückgelieferten Datenpakets ungültig: {df.height}") @@ -111,6 +114,7 @@ def contact_person_search_choices( use_both_names: bool, ) -> tuple[tuple[str, int], ...]: # TODO no reload functionality + logger.debug("[Call backend] contact_person_search_choices") q = db.DF_CONTACT_PERSON.lazy() if ma_id is not None: q = q.filter(pl.col.ma_id == ma_id) @@ -136,6 +140,7 @@ def contact_person_search_choices( def contact_person_search_get_info( an_id: int, ) -> ContactPersonInfo: + logger.debug("[Call backend] contact_person_search_get_info") df = db.DF_CONTACT_PERSON.filter(pl.col.an_id == an_id) if df.height > 1 or df.height == 0: raise ValueError(f"Größe des zurückgelieferten Datenpakets ungültig: {df.height}") @@ -147,6 +152,7 @@ def contact_person_search_get_info( def insert_initial_recording( data: dict[str, Any], ) -> None: + logger.debug("[Call backend] insert_initial_recording") stmt = db.grunderfassung_unternehmen.insert().values(data) with db.ENGINE.begin() as conn: conn.execute(stmt) @@ -156,6 +162,7 @@ def update_initial_recording( id_: int, data: dict[str, Any], ) -> None: + logger.debug("[Call backend] update_initial_recording") stmt = ( db.grunderfassung_unternehmen.update() .where(db.grunderfassung_unternehmen.c.erfassung_id == id_) @@ -168,6 +175,7 @@ def update_initial_recording( def get_initial_recording( id_: int, ) -> dict[str, Any]: + logger.debug("[Call backend] get_initial_recording") stmt = db.grunderfassung_unternehmen.select().where( db.grunderfassung_unternehmen.c.erfassung_id == id_ ) @@ -190,6 +198,7 @@ class FrontpageCompany: def get_company_list() -> list[FrontpageCompany]: + logger.debug("[Call backend] get_company_list") stmt = sql.select( db.grunderfassung_unternehmen.c.erfassung_id, db.grunderfassung_unternehmen.c.Partnersuche__un_suche, diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 4d5cfd5..5cc67cb 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -357,7 +357,9 @@ grunderfassung_unternehmen: sql.Table = Table( Column("Kontaktperson__KP_funktion_beziehung", sql.Text, nullable=True), Column("Kontaktperson__KP_mobilfunknummer", sql.Text, nullable=True), Column("Kontaktperson__KP_name", sql.Text, nullable=True), - Column("Kontaktperson__KP_name_partner", sql.Text, nullable=True), + Column( + "Kontaktperson__KP_name_partner", sql.Text, nullable=True + ), # TODO: check if needed when set by trigger Column("Kontaktperson__KP_titel", sql.Text, nullable=True), Column("Kontaktperson__KP_vorname", sql.Text, nullable=True), Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True), diff --git a/src/wce_crm/env_vars.txt b/src/wce_crm/env_vars.txt deleted file mode 100644 index 00c2f3e..0000000 --- a/src/wce_crm/env_vars.txt +++ /dev/null @@ -1,5 +0,0 @@ -DOPT_DEVELOPMENT: flag which signals that the current environment is in development mode -DOPT_STOP_FOLDER_NAME: stop folder to find base path -DOPT_DB_CRM: path to CRM database, relative to base path -DOPT_DB_MAIN: path to main database, relative to base path -DOPT_PATH_LOGGING: path to logging folder, relative to base path \ No newline at end of file diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 0cae7d7..f7f96a7 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -8,6 +8,7 @@ import json import pickle import re import sys +import traceback import uuid from collections import defaultdict from collections.abc import Container, Iterable, Sequence @@ -33,7 +34,9 @@ from PySide6.QtCore import ( QObject, Qt, QTimer, + QtMsgType, Signal, + qInstallMessageHandler, ) from PySide6.QtGui import QAction from PySide6.QtWidgets import ( @@ -364,14 +367,26 @@ def get_leaf_dicts(data): def pprint_registry(widget_registry: WidgetRegistry) -> None: - print("---\n\n>>> Widget registry:") + print("\n\n>>> Widget registry:") for key, entry in widget_registry.items(): print(f"Key: {key}") - print(f"\twidget: {entry['widget']}") - print(f"\tfield key: {entry['form_field'].key}") + print(f"\twidget: {entry['widget']}") + print(f"\tfield key: {entry['form_field'].key}") print(f"\tfield type: {entry['form_field'].type}") +def pformat_registry(widget_registry: WidgetRegistry) -> str: + lines: list[str] = [] + lines.append("\n\n>>> Widget registry:") + for key, entry in widget_registry.items(): + lines.append(f"Key: {key}") + lines.append(f"\twidget: {entry['widget']}") + lines.append(f"\tfield key: {entry['form_field'].key}") + lines.append(f"\tfield type: {entry['form_field'].type}") + + return "\n".join(lines) + + class FormFieldType(enum.StrEnum): GROUP = enum.auto() TEXT = enum.auto() @@ -1073,11 +1088,11 @@ def set_form_data( value = data else: if key not in data: - logger_gui.debug("---- Key not in data: %s") - logger_gui.debug("-------- Data: %s", pformat(data)) + logger_gui.error("Key not in data: %s", key) + logger_gui.error("Data:\n%s", pformat(data)) value = data[key] - logger_gui.debug("---- Key set: %s", key) + logger_gui.debug("Key set: %s", key) set_widget_value(widget, value) @@ -1716,6 +1731,11 @@ class AutoForm(QWidget): self.current_id: int = -1 + logger_auto_form.debug( + "Initialised Auto Form Widget. Registry:%s", + pformat_registry(self.widget_registry), + ) + def _print_registry(self) -> None: pprint_registry(self.widget_registry) @@ -2472,7 +2492,9 @@ class NewEntrySelect_view(QWidget): btn_person.setFixedWidth(300) btn_person.setFixedHeight(40) btn_person.clicked.connect(lambda: self.person_requested.emit()) - btn_person.clicked.connect(lambda: print("Person gewählt")) + btn_person.clicked.connect( + lambda: logger_gui.info("[Grunderfassung: Dummy Individualperson] Person gewählt") + ) layout.addWidget(back_btn) layout.addSpacing(15) @@ -2631,7 +2653,7 @@ FORM_FIELDS_SEARCH_HEAD = [ FORM_FIELDS_CONTACT_PERSON = [ FormField( - "Name Unternehmen/Netzwerkpartner (pre-filled von Suche)", + "Name Unternehmen/Netzwerkpartner (vorausgefüllt von Suche)", FormFieldType.TEXT, key="KP_name_partner", required=False, @@ -3308,7 +3330,6 @@ CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig( ) -# TODO clean code class PageFormCompany(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button @@ -3381,7 +3402,23 @@ class PageFormCompany(QWidget): container_layout.addSpacing(15) # --- CUSTOM LOGIC --- - # 'Bundesland' only if 'Inland' selected in 'Stammdaten' + # ** fill 'Kontaktperson -> Namen Unternehmen' + search_res = search_widgets_by_key(self.auto_form.widget_registry, "Partnersuche") + assert len(search_res) == 1 + search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) + self.search_widget_trigger = cast( + QLineEdit, search_widget.company_widgets["ma_unternehmensname"] + ) + text_widget_set = search_widgets_by_key( + self.auto_form.widget_registry, f"Kontaktperson{COLUMN_SEP}KP_name_partner" + ) + assert len(text_widget_set) == 1 + self.text_widget_set = cast(QLineEdit, text_widget_set[0]["widget"]) + self.search_widget_trigger.textChanged.connect( + self._custom_set_company_name_contact_person + ) + + # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' person_location = search_widgets_by_key( self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" ) @@ -3399,6 +3436,9 @@ class PageFormCompany(QWidget): self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) + def _custom_set_company_name_contact_person(self, value: str) -> None: + self.text_widget_set.setText(str(value)) + def _custom_county_selection(self, idx: int) -> None: value = self.person_location.itemData(idx) if value == "Inland": @@ -3634,9 +3674,51 @@ class MainWindow(QMainWindow): help_menu.addAction(about_action) +# ** global exception handling +def global_exception_handler(exc_type, exc_value, exc_traceback): + """catches all unhandled errors""" + + # format error + error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) + logger_gui.critical(f"UNEXPECTED ERROR:\n{error_msg}") + + # message to user + # check if QApplication exists otherwise crashing of crash handler possible) + if QApplication.instance(): + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Icon.Critical) + msg_box.setWindowTitle("Kritischer Fehler") + msg_box.setText( + "Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert." + ) + # details for user or screenshots + msg_box.setDetailedText(error_msg) + msg_box.exec() + + sys.exit(1) + + +def qt_message_handler(mode, context, message): + """forwards internal Qt C++ warnings to Python loggers""" + if mode == QtMsgType.QtInfoMsg: + logger_gui.info(message) + elif mode == QtMsgType.QtWarningMsg: + logger_gui.warning(message) + elif mode == QtMsgType.QtCriticalMsg: + logger_gui.error(message) + elif mode == QtMsgType.QtFatalMsg: + logger_gui.critical(message) + + if __name__ == "__main__": - app = QApplication(sys.argv) - app.setStyleSheet(QSS) - window = MainWindow() - window.show() - sys.exit(app.exec()) + sys.excepthook = global_exception_handler + qInstallMessageHandler(qt_message_handler) + + try: + app = QApplication(sys.argv) + app.setStyleSheet(QSS) + window = MainWindow() + window.show() + sys.exit(app.exec()) + except Exception as err: + logger_gui.critical("Fehler beim Starten der Anwendung:\n%s", str(err), exc_info=True) diff --git a/src/wce_crm/logging.py b/src/wce_crm/logging.py index 5f6e855..c8afd46 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -1,6 +1,6 @@ import logging -from dopt_basics.logging import BASE_LOGGER, setup_logging +from dopt_basics.logging import BASE_LOGGER, LoggingConfig, setup_logging from wce_crm.constants import Config @@ -14,12 +14,16 @@ if Config.DEVELOPMENT_STATE: if not Config.PATH_LOGGING.exists(): Config.PATH_LOGGING.mkdir() -setup_logging( +LOGGING_CFG: LoggingConfig = LoggingConfig( enable_stderr=enable_stderr, enable_file=enable_file, logging_dir=Config.PATH_LOGGING, log_filename=Config.LOG_FILENAME, + file_max_bytes=10_485_760, + file_backup_count=2, ) + +setup_logging(LOGGING_CFG) logger_base = BASE_LOGGER.getChild("wce_crm") # ** GUI @@ -31,3 +35,7 @@ logger_get_data = logger_gui.getChild("get_data") logger_get_data.setLevel(logging.DEBUG) logger_auto_form = logger_gui.getChild("get_data_auto_form") logger_auto_form.setLevel(logging.DEBUG) + +# ** Backend +logger_back = logger_base.getChild("backend") +logger_back.setLevel(logging.DEBUG)