added functionality for consulting session saving, updating and soft deleting

This commit is contained in:
2026-07-11 10:02:58 +02:00
parent 6eb358e0aa
commit 5563e5a6b9
3 changed files with 162 additions and 105 deletions
+26 -6
View File
@@ -244,13 +244,16 @@ def page_consulting_to_db(
with db.ENGINE.begin() as conn: with db.ENGINE.begin() as conn:
if consultation_data.vorgang_id is None: if consultation_data.vorgang_id is None:
# insert new "Vorgang" # insert new "Vorgang"
insert_data = consultation_data.model_dump(exclude={"vorgang_id", "beratungen"}) insert_data_process = consultation_data.model_dump(
exclude={"vorgang_id", "beratungen"}
)
logger.debug( logger.debug(
"[Consulting Page] Call insert 'Vorgang' with data:\n%s", pformat(insert_data) "[Consulting Page] Call insert 'Vorgang' with data:\n%s",
pformat(insert_data_process),
) )
stmt = db.beratung_vorgang.insert() stmt = db.beratung_vorgang.insert()
ret = conn.execute(stmt, insert_data) ret = conn.execute(stmt, insert_data_process)
if ret.rowcount == 0: if ret.rowcount == 0:
raise IOError("Entry was not inserted correctly") raise IOError("Entry was not inserted correctly")
prim_keys = ret.inserted_primary_key prim_keys = ret.inserted_primary_key
@@ -261,9 +264,14 @@ def page_consulting_to_db(
else: else:
logger.debug( logger.debug(
"[Consulting Page] VorgangID already set. ID: %d", "[Consulting Page] VorgangID already set. ID: %d. Update...",
consultation_data.vorgang_id, consultation_data.vorgang_id,
) )
update_data_process = consultation_data.model_dump(exclude={"beratungen"})
stmt = db.beratung_vorgang.update().where(
db.beratung_vorgang.c.vorgang_id == consultation_data.vorgang_id
)
conn.execute(stmt, update_data_process)
rows_for_db_insert: list[dict[str, Any]] = [] rows_for_db_insert: list[dict[str, Any]] = []
rows_for_db_update: list[dict[str, Any]] = [] rows_for_db_update: list[dict[str, Any]] = []
@@ -337,12 +345,24 @@ def page_consulting_from_db(
beratungs_typ=consultation_data_db["beratungs_typ"], beratungs_typ=consultation_data_db["beratungs_typ"],
erstellt=consultation_data_db["erstellt"], erstellt=consultation_data_db["erstellt"],
aktualisiert=consultation_data_db["aktualisiert"], aktualisiert=consultation_data_db["aktualisiert"],
geloescht=consultation_data_db["geloescht"],
beratungen=[], beratungen=[],
) )
# get all consultation sessions of this process if consultation_data.geloescht is not None:
logger.info(
(
"[Consulting Page] Obtained entry for ID=%d. This entry was marked "
"as deleted."
),
cons_id,
)
# get all consultation sessions of this process, only the ones
# which are not marked as deleted
stmt = db.beratung_einzelberatung.select().where( stmt = db.beratung_einzelberatung.select().where(
db.beratung_einzelberatung.c.vorgang_id == cons_id db.beratung_einzelberatung.c.vorgang_id == cons_id,
db.beratung_einzelberatung.c.geloescht.is_(None),
) )
ret = conn.execute(stmt) ret = conn.execute(stmt)
# empty results possible # empty results possible
+10 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import enum
import json import json
from typing import Annotated, Any, Final, Literal from typing import Annotated, Any, Final, Literal
@@ -38,7 +39,7 @@ def translate_pydantic_errors(
err_type = error.get("type", None) err_type = error.get("type", None)
if err_type is None or err_type not in GERMAN_ERROR_MESSAGES: if err_type is None or err_type not in GERMAN_ERROR_MESSAGES:
error["msg"] = "[fehleden Übersetzung, nur Englisch]: " + error["msg"] error["msg"] = "[fehlende Übersetzung, nur Englisch]: " + error["msg"]
translated_errors.append(error) translated_errors.append(error)
continue continue
@@ -346,6 +347,11 @@ class Grunderfassung_Sprachen(BaseModel):
# ** Consulting # ** Consulting
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
class Beratungsgespraech_Vorgang(BaseModel): class Beratungsgespraech_Vorgang(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True) model_config = ConfigDict(str_strip_whitespace=True)
@@ -353,11 +359,12 @@ class Beratungsgespraech_Vorgang(BaseModel):
un_id: int | None un_id: int | None
pers_id: int | None pers_id: int | None
titel: str titel: str
beratungs_typ: Literal["pauschal", "individuell"] beratungs_typ: ConsultingType
# default in SQLAlchemy with lambda and timezone-aware datetime # default in SQLAlchemy with lambda and timezone-aware datetime
erstellt: AwareDatetime | None = Field(default=None, exclude=True) erstellt: AwareDatetime | None = Field(default=None, exclude=True)
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
geloescht: AwareDatetime | None = None
beratungen: list[Beratungsgespraech_Einzelgespraech] beratungen: list[Beratungsgespraech_Einzelgespraech]
@@ -379,3 +386,4 @@ class Beratungsgespraech_Einzelgespraech(BaseModel):
# default in SQLAlchemy with lambda and timezone-aware datetime # default in SQLAlchemy with lambda and timezone-aware datetime
erstellt: AwareDatetime | None = Field(default=None, exclude=True) erstellt: AwareDatetime | None = Field(default=None, exclude=True)
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
geloescht: AwareDatetime | None = None
+126 -97
View File
@@ -3,10 +3,8 @@ from __future__ import annotations
import copy import copy
import dataclasses as dc import dataclasses as dc
import datetime import datetime
import enum
import re import re
import sys import sys
import time
import traceback import traceback
from collections import defaultdict from collections import defaultdict
from collections.abc import Container, Iterable, Sequence from collections.abc import Container, Iterable, Sequence
@@ -33,10 +31,13 @@ from PySide6.QtCore import (
QDate, QDate,
QDateTime, QDateTime,
QEvent, QEvent,
QLibraryInfo,
QLocale,
QObject, QObject,
Qt, Qt,
QTimer, QTimer,
QtMsgType, QtMsgType,
QTranslator,
Signal, Signal,
qInstallMessageHandler, qInstallMessageHandler,
) )
@@ -81,6 +82,7 @@ from wce_crm.data_models import (
COLUMN_SEP, COLUMN_SEP,
Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Einzelgespraech,
Beratungsgespraech_Vorgang, Beratungsgespraech_Vorgang,
ConsultingType,
FlatBaseModel, FlatBaseModel,
Grunderfassung_Personen, Grunderfassung_Personen,
Grunderfassung_Unternehmen, Grunderfassung_Unternehmen,
@@ -344,11 +346,6 @@ class Page_InitRecPerson_State(PageState[Module]):
pers_id: RecId | None = None pers_id: RecId | None = None
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
@dc.dataclass(slots=True, kw_only=True) @dc.dataclass(slots=True, kw_only=True)
class Page_Consulting_State(PageState[Module]): class Page_Consulting_State(PageState[Module]):
session: Session session: Session
@@ -360,6 +357,7 @@ class Page_Consulting_State(PageState[Module]):
cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field( cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field(
default_factory=list default_factory=list
) )
ist_geloescht: bool = False
@dc.dataclass(slots=True, kw_only=True) @dc.dataclass(slots=True, kw_only=True)
@@ -377,6 +375,7 @@ class Page_Consulting_ConsultingSession_State(PageState[Module]):
) )
anmerkungen: str | None = None anmerkungen: str | None = None
rueckmeldung: str | None = None rueckmeldung: str | None = None
ist_geloescht: bool = False
@dc.dataclass(slots=True, kw_only=True) @dc.dataclass(slots=True, kw_only=True)
@@ -845,29 +844,6 @@ def get_form_data(
else: else:
value = get_widget_value(widget) value = get_widget_value(widget)
# elif isinstance(widget, QDateEdit):
# qt_date = widget.date()
# value = qt_date.toPython()
# elif isinstance(widget, FlexibleDateInput):
# value = widget.get_pydate()
# elif isinstance(widget, FlexibleDateTimeInput):
# value = widget.get_pydatetime()
# elif isinstance(widget, QComboBox):
# value = widget.currentData()
# elif isinstance(
# widget,
# (
# DynamicListWidget,
# DynamicDropdownWidgetNumeric,
# DynamicDropdownWidgetOption,
# CustomWidget,
# ),
# ):
# # this is a special data structure with some assumptions of the widget's internals
# # DynamicListWidget: this is a list -> each dynamic list contains a list
# # of such dictionaries (handled below)
# value = widget.get_form_data()
if isinstance(value, dict): if isinstance(value, dict):
raw_data.update(value) raw_data.update(value)
else: else:
@@ -1854,9 +1830,11 @@ class FlexibleDateInput(QWidget):
# valid or invalid QDate # valid or invalid QDate
return QDate.fromString(self.line_edit.text(), "dd.MM.yyyy") return QDate.fromString(self.line_edit.text(), "dd.MM.yyyy")
def get_pydate(self) -> datetime.date: def get_pydate(self) -> datetime.date | None:
# valid or invalid QDate # valid or invalid QDate
date = self.get_date() date = self.get_date()
if not date.isValid():
return None
py_date = cast(datetime.date, date.toPython()) py_date = cast(datetime.date, date.toPython())
return py_date return py_date
@@ -2065,7 +2043,7 @@ class FlexibleDateTimeInput(QWidget):
# valid or invalid QDateTime # valid or invalid QDateTime
return QDateTime.fromString(self.line_edit.text(), "dd.MM.yyyy hh:mm") return QDateTime.fromString(self.line_edit.text(), "dd.MM.yyyy hh:mm")
def get_pydatetime(self) -> datetime.datetime: def get_pydatetime(self) -> datetime.datetime | None:
"""returns current value of the field as UTC datetime """returns current value of the field as UTC datetime
Returns Returns
@@ -2075,6 +2053,8 @@ class FlexibleDateTimeInput(QWidget):
""" """
# valid or invalid datetime # valid or invalid datetime
dt = self.get_datetime() dt = self.get_datetime()
if not dt.isValid():
return None
py_dt = cast(datetime.datetime, dt.toPython()) py_dt = cast(datetime.datetime, dt.toPython())
py_dt = py_dt.replace(tzinfo=TIMEZONE_CEST) py_dt = py_dt.replace(tzinfo=TIMEZONE_CEST)
@@ -2488,7 +2468,6 @@ class DynamicDropdownWidgetOption(QWidget):
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form) sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
form_field_def = copy.copy(self.assigned_form_fields) form_field_def = copy.copy(self.assigned_form_fields)
# form_field_def.label = form_field_def.enhanced_label(f"{number_form}")
sub_form.full_keys = _build_ui_recursively( sub_form.full_keys = _build_ui_recursively(
schema=form_field_def, schema=form_field_def,
@@ -2746,6 +2725,7 @@ class Page_NewInitRec(QWidget):
self, self,
new_state: Page_NewInitRec_State, new_state: Page_NewInitRec_State,
) -> None: ) -> None:
set_page_state(self.STATE, new_state)
self._sync_state_to_GUI() self._sync_state_to_GUI()
def get_state(self) -> Page_NewInitRec_State: def get_state(self) -> Page_NewInitRec_State:
@@ -3371,42 +3351,6 @@ class Page_CompanyProfile(QWidget):
txt = f"Unternehmen: {name}" txt = f"Unternehmen: {name}"
self.title_company_name.setText(txt) self.title_company_name.setText(txt)
# def load_from_id(
# self,
# id_: InitRecId,
# ) -> None:
# assert id_ is not None, "tried to load with InitRecId None"
# assert id_, "tried to load with invalid InitRecId"
# logger_gui.debug(f"Called function to load with ID: {id_}")
# if id_ < 0:
# # just clear and do nothing else
# logger_gui.debug("just reset with ID < 0")
# self.table_master_data.reset()
# self.set_company_name("")
# self.current_id = id_
# return
# if id_ == self.current_id:
# logger_gui.debug("ID did not change. Do nothing")
# self.current_id = id_
# return
# init_rec = backend.initrec_company_get_initial_recording(id_)
# ma_id = cast(ExtMaId, init_rec["Partnersuche__un_suche"])
# assert ma_id is not None, "no ma_id obtained"
# comp_info = backend.initrec_comp_search_get_info(ma_id)
# data: dict[str, Any] = {}
# for feature, value in comp_info.items():
# if feature in self.COMPANY_FEATURES_MASTER_DATA:
# data[self.COMPANY_FEATURES_MASTER_DATA[feature]] = value
# self.table_master_data.load_data(data)
# self.set_company_name(data["Name:"])
# self.current_id = id_
class MasterDataTable(QWidget): class MasterDataTable(QWidget):
def __init__(self): def __init__(self):
@@ -3668,13 +3612,6 @@ class Page_Consulting(QWidget):
separator1.setFrameShadow(QFrame.Shadow.Sunken) separator1.setFrameShadow(QFrame.Shadow.Sunken)
container_layout.addWidget(separator1) container_layout.addWidget(separator1)
# layout_change_title = QHBoxLayout()
# self.change_title_text = QLineEdit(placeholderText="Unternehmensname eingeben...")
# layout_change_title.addWidget(self.change_title_text)
# change_title_btn = QPushButton("ändern")
# layout_change_title.addWidget(change_title_btn)
# change_title_btn.clicked.connect(self.set_company_name)
# container_layout.addLayout(layout_change_title)
btn_get_data_table = QPushButton("Debug call 'get_state' from table and page") btn_get_data_table = QPushButton("Debug call 'get_state' from table and page")
btn_get_data_table.clicked.connect(self._debug_get_state) btn_get_data_table.clicked.connect(self._debug_get_state)
container_layout.addWidget(btn_get_data_table) container_layout.addWidget(btn_get_data_table)
@@ -3937,13 +3874,23 @@ class Page_Consulting(QWidget):
self.STATE.un_id = data.un_id self.STATE.un_id = data.un_id
self.STATE.pers_id = data.pers_id self.STATE.pers_id = data.pers_id
self.STATE.titel = data.titel self.STATE.titel = data.titel
self.STATE.ist_geloescht = data.geloescht is not None
if update_only: if update_only:
for gui_state, pydantic_state in zip(self.STATE.cons_sessions, data.beratungen): relevant_gui_states = [s for s in self.STATE.cons_sessions if not s.ist_geloescht]
rel_cons_session_states = [s for s in data.beratungen if s.geloescht is None]
print(f">>>>>>> {pformat(relevant_gui_states)}")
assert len(relevant_gui_states) == len(rel_cons_session_states)
for gui_state, pydantic_state in zip(
relevant_gui_states, rel_cons_session_states
):
gui_state.beratung_id = pydantic_state.beratung_id gui_state.beratung_id = pydantic_state.beratung_id
else: else:
self.STATE.cons_sessions.clear() self.STATE.cons_sessions.clear()
for pydantic_state in data.beratungen: for pydantic_state in data.beratungen:
# Deleted sessions are not part of the retrieved data from the backend if this
# page is initialised from a fresh backend call to get data. (Updates do
# indeed have data which must be filtered -- see above)
cons_session_state = Page_Consulting_ConsultingSession_State( cons_session_state = Page_Consulting_ConsultingSession_State(
session=self.STATE.session, session=self.STATE.session,
nutzer_id=pydantic_state.nutzer_id, nutzer_id=pydantic_state.nutzer_id,
@@ -3989,10 +3936,32 @@ class Page_Consulting(QWidget):
pydantic_sessions: list[Beratungsgespraech_Einzelgespraech] = [] pydantic_sessions: list[Beratungsgespraech_Einzelgespraech] = []
timestamp_deleted: datetime.datetime | None = None
if self.STATE.ist_geloescht:
timestamp_deleted = datetime.datetime.now(datetime.UTC)
for row in self.STATE.cons_sessions: for row in self.STATE.cons_sessions:
logger_page_consulting.debug("[Consulting Page] Row state:\n%s", row)
if row.beratung_id is None and row.ist_geloescht:
logger_page_consulting.debug(
(
"[Consulting Page] Session which was never saved in the database "
"and is marked for deletion - ignore."
)
)
continue
timestamp_deleted_session: datetime.datetime | None = None
if timestamp_deleted is not None:
# if process is deleted, all sessions must be marked as deleted
timestamp_deleted_session = timestamp_deleted
elif row.ist_geloescht:
timestamp_deleted_session = datetime.datetime.now(datetime.UTC)
cons_session_dict = dc.asdict(row) cons_session_dict = dc.asdict(row)
cons_session = Beratungsgespraech_Einzelgespraech( cons_session = Beratungsgespraech_Einzelgespraech(
vorgang_id=self.STATE.vorgang_id, vorgang_id=self.STATE.vorgang_id,
geloescht=timestamp_deleted_session,
**cons_session_dict, **cons_session_dict,
) )
pydantic_sessions.append(cons_session) pydantic_sessions.append(cons_session)
@@ -4003,6 +3972,7 @@ class Page_Consulting(QWidget):
pers_id=self.STATE.pers_id, pers_id=self.STATE.pers_id,
titel=self.STATE.titel, titel=self.STATE.titel,
beratungs_typ=self.STATE.beratungs_typ, # type: ignore beratungs_typ=self.STATE.beratungs_typ, # type: ignore
geloescht=timestamp_deleted,
beratungen=pydantic_sessions, beratungen=pydantic_sessions,
) )
@@ -4016,6 +3986,10 @@ class Page_Consulting(QWidget):
if DEBUG_NO_DATABASE: if DEBUG_NO_DATABASE:
return return
logger_page_consulting.debug("[Consulting Page] Validated Pydantic successfully.") logger_page_consulting.debug("[Consulting Page] Validated Pydantic successfully.")
logger_page_consulting.debug(
"[Consulting Page] Pydantic model:\n%s", consulting_process
)
# !! should be placed in a try block to handle errors # !! should be placed in a try block to handle errors
res = backend.page_consulting_to_db(consulting_process) res = backend.page_consulting_to_db(consulting_process)
if res.status != STATUS_HANDLER.SUCCESS: if res.status != STATUS_HANDLER.SUCCESS:
@@ -4042,21 +4016,20 @@ class Page_Consulting(QWidget):
msg_box.exec() msg_box.exec()
return return
updated_data = res.unwrap() updated_consulting_process = res.unwrap()
logger_page_consulting.info( logger_page_consulting.info(
"[Consulting Page] Load state from modified Pydantic model" "[Consulting Page] Load state from modified Pydantic model"
) )
self._load_from_pydantic(updated_data, update_only=True) self._load_from_pydantic(updated_consulting_process, update_only=True)
logger_page_consulting.info("[Consulting Page] Data saved successfully") logger_page_consulting.info("[Consulting Page] Data saved successfully")
# TODO status message successful to user in GUI
msg_box = get_message_box( msg_box = get_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Speichern erfolgreich", "Speichern erfolgreich",
"Die Daten wurden erfolgreich gespeichert", "Die Daten wurden erfolgreich gespeichert",
) )
msg_box.exec() msg_box.exec()
# TODO check triggers for rest of application
# self.update_triggered.emit() # self.update_triggered.emit()
# self._activate_delete() # self._activate_delete()
finally: finally:
@@ -4173,7 +4146,7 @@ class Page_Consulting_ConsultingSession(QWidget):
self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_delete.setStyleSheet("color: #cc0000; font-weight: bold;") self.btn_delete.setStyleSheet("color: #cc0000; font-weight: bold;")
self.btn_delete.clicked.connect(lambda: self.delete_request.emit(self)) self.btn_delete.clicked.connect(self._delete_request)
self.btn_delete.setFixedWidth(30) self.btn_delete.setFixedWidth(30)
layout.addWidget(self.btn_delete) layout.addWidget(self.btn_delete)
@@ -4206,7 +4179,17 @@ class Page_Consulting_ConsultingSession(QWidget):
layout.setStretch(i, stretch) layout.setStretch(i, stretch)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self._sync_state_to_GUI() # ?? only needed if entry is from database? self._sync_state_to_GUI()
def _delete_request(self) -> None:
confirm = get_user_confirmation(
self,
"Löschen bestätigen",
"Soll der Eintrag wirklich gelöscht werden?",
)
if confirm:
self.STATE.ist_geloescht = True
self.delete_request.emit(self)
def _sync_state_to_GUI(self) -> None: def _sync_state_to_GUI(self) -> None:
logger_page_consulting.debug( logger_page_consulting.debug(
@@ -4276,7 +4259,7 @@ class Page_Consulting_Table(QWidget):
self.btn_add.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_add.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_add.setObjectName("BlueAddButton") # ID vergeben self.btn_add.setObjectName("BlueAddButton") # ID vergeben
main_layout.addWidget(self.btn_add) main_layout.addWidget(self.btn_add)
self.btn_add.clicked.connect(lambda: self.add_row(None)) self.btn_add.clicked.connect(lambda: self.add_row(None, False))
self.btn_add.setStyleSheet(""" self.btn_add.setStyleSheet("""
#BlueAddButton { #BlueAddButton {
@@ -4417,7 +4400,8 @@ class Page_Consulting_Table(QWidget):
def add_row( def add_row(
self, self,
row_state: Page_Consulting_ConsultingSession_State | None = None, row_state: Page_Consulting_ConsultingSession_State | None,
state_exists: bool,
) -> None: ) -> None:
if row_state is None: if row_state is None:
row_state = Page_Consulting_ConsultingSession_State( row_state = Page_Consulting_ConsultingSession_State(
@@ -4429,10 +4413,13 @@ class Page_Consulting_Table(QWidget):
row = Page_Consulting_ConsultingSession(state=row_state) row = Page_Consulting_ConsultingSession(state=row_state)
row.delete_request.connect(self.remove_row) row.delete_request.connect(self.remove_row)
# sys.exit(0)
index = self.container_layout.count() - 1 index = self.container_layout.count() - 1
self.container_layout.insertWidget(index, row) self.container_layout.insertWidget(index, row)
self.STATE.child_modules.append(row) self.STATE.child_modules.append(row)
if not state_exists:
self.STATE.row_states.append(row.get_state())
def remove_row( def remove_row(
self, self,
@@ -4440,36 +4427,49 @@ class Page_Consulting_Table(QWidget):
) -> None: ) -> None:
self.container_layout.removeWidget(table_row_widget) self.container_layout.removeWidget(table_row_widget)
self.STATE.child_modules.remove(table_row_widget) self.STATE.child_modules.remove(table_row_widget)
# do not remove the corresponding state since it is tracked
# to be post-processed when the database saving operation is triggered
table_row_widget.deleteLater() table_row_widget.deleteLater()
def reset(self) -> None: def remove_all_rows(self) -> None:
while self.STATE.child_modules: while self.STATE.child_modules:
self.remove_row(self.STATE.child_modules[0]) self.remove_row(self.STATE.child_modules[0])
def _sync_state_to_GUI(self) -> None: def _sync_state_to_GUI(self) -> None:
self.setUpdatesEnabled(False) self.setUpdatesEnabled(False)
self.reset() self.remove_all_rows()
# states_to_show = [s for s in self.STATE.row_states if not s.ist_geloescht]
# for row_state in states_to_show:
for row_state in self.STATE.row_states: for row_state in self.STATE.row_states:
self.add_row(row_state) if not row_state.ist_geloescht:
self.add_row(row_state, True)
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
def _sync_GUI_to_state(self) -> None: def _sync_GUI_to_state(self) -> None:
logger_page_consulting.debug("[Page -- Consulting] Table: Call syncing GUI to state") logger_page_consulting.debug("[Page -- Consulting] Table: Call syncing GUI to state")
row_data: list[Page_Consulting_ConsultingSession_State] = [] logger_page_consulting.debug(
(
"[Page -- Consulting] Table: Row data is managed when entries are added. "
"There is no action needed. All states are obtained by reference and set "
"by their row components/widgets. Therefore, this method calls the "
"method `get_state` of all ACTIVE components to update their respective "
"states with the current user inputs."
)
)
logger_page_consulting.debug( logger_page_consulting.debug(
"[Page -- Consulting] Table - Child modules: %s", "[Page -- Consulting] Table - Child modules: %s",
pformat(self.STATE.child_modules), pformat(self.STATE.child_modules),
) )
# Child modules are ACTIVE elements, viewable and changeable by the user.
# The states are already part of this widget and linked by reference, but
# the current user input must be synced with these states. Therefore, the
# `get_state` method of these children is called, which implicitly updates
# their state. The returned state reference is not needed.
for row in self.STATE.child_modules: for row in self.STATE.child_modules:
row_data.append(row.get_state()) _ = row.get_state()
assert len(row_data) == len(self.STATE.child_modules), (
"mismatch in row count and data length"
)
self.STATE.row_states = row_data
def get_state(self) -> Page_Consulting_Table_State: def get_state(self) -> Page_Consulting_Table_State:
logger_page_consulting.debug("[Page Consulting -- Table] Syncing GUI to state...") logger_page_consulting.debug("[Page Consulting -- Table] Syncing GUI to state...")
@@ -4536,7 +4536,6 @@ def clear_layout(
if layout is None: if layout is None:
return return
# for idx in range(start_idx, layout.count()):
while layout.count(): while layout.count():
child = layout.takeAt(0) child = layout.takeAt(0)
if child is None: if child is None:
@@ -4864,6 +4863,25 @@ def get_message_box(
return msg_box return msg_box
def get_user_confirmation(
parent_widget: QWidget,
title: str,
message: str,
) -> bool:
response = QMessageBox.question(
parent_widget,
title,
message,
buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No),
defaultButton=QMessageBox.StandardButton.No,
)
if response == QMessageBox.StandardButton.Yes:
return True
else:
return False
def pydantic_validation_error_handling( def pydantic_validation_error_handling(
val_error: ValidationError, val_error: ValidationError,
) -> None: ) -> None:
@@ -4959,15 +4977,26 @@ if TYPE_CHECKING:
_w1: type[WrapperModule] = Page_InitRecCompany _w1: type[WrapperModule] = Page_InitRecCompany
_w2: type[WrapperModule] = Page_InitRecPerson _w2: type[WrapperModule] = Page_InitRecPerson
if __name__ == "__main__":
def main() -> None:
sys.excepthook = global_exception_handler sys.excepthook = global_exception_handler
qInstallMessageHandler(qt_message_handler) qInstallMessageHandler(qt_message_handler)
try: try:
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyleSheet(QSS) app.setStyleSheet(QSS)
translator = QTranslator()
translation_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
if translator.load(QLocale.system(), "qtbase", "_", translation_path):
app.installTranslator(translator)
window = MainWindow() window = MainWindow()
window.show() window.show()
sys.exit(app.exec()) sys.exit(app.exec())
except Exception as err: except Exception as err:
logger_gui.critical("Fehler beim Starten der Anwendung:\n%s", str(err), exc_info=True) logger_gui.critical("Fehler beim Starten der Anwendung:\n%s", str(err), exc_info=True)
if __name__ == "__main__":
main()