working example for document management

This commit was merged in pull request #6.
This commit is contained in:
2026-07-31 15:32:47 +02:00
parent 68bd873838
commit c91bc9022a
10 changed files with 525 additions and 137 deletions
+156 -22
View File
@@ -82,17 +82,19 @@ from PySide6.QtWidgets import (
import wce_crm.constants
from wce_crm import types
from wce_crm.backend import backend
from wce_crm.constants import TIMEZONE_CEST
from wce_crm.constants import COLUMN_SEP, TIMEZONE_CEST
from wce_crm.data_models import (
COLUMN_SEP,
Beratungsgespraech_Einzelgespraech,
Beratungsgespraech_Vorgang,
Grunderfassung_Dokument,
InitRec,
Page_CompanyProfile_State,
Page_Consulting_ConsultingSession_State,
Page_Consulting_Linking_State,
Page_Consulting_State,
Page_Consulting_Table_State,
Page_InitRec_DocumentSection_Doc_State,
Page_InitRec_DocumentSection_State,
Page_InitRec_Form_Data,
Page_InitRec_Form_State,
Page_InitRec_State,
@@ -111,6 +113,7 @@ from wce_crm.gui_components.dialogs import (
get_user_confirmation,
pydantic_validation_error_handling,
)
from wce_crm.gui_components.initrec_file_selection import DocumentSectionWidget
from wce_crm.logging import (
logger_custom_fields,
logger_get_data,
@@ -3083,12 +3086,17 @@ CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = {
class CollapsibleBox(QWidget):
def __init__(self, title="", parent=None):
def __init__(
self,
title: str = "",
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
# 1. Header-Button mit Pfeil
self.toggle_button = QToolButton()
self.toggle_button.setText(f" {title}")
self.base_text = f" {title}"
self.toggle_button.setText(self.base_text)
self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
@@ -3108,7 +3116,10 @@ class CollapsibleBox(QWidget):
main_layout.addWidget(self.toggle_button)
main_layout.addWidget(self.content_area)
def _on_toggle(self, checked: bool):
def _on_toggle(
self,
checked: bool,
) -> None:
# Pfeilrichtung ändern & Inhalt ein-/ausblenden
self.toggle_button.setArrowType(
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
@@ -3118,18 +3129,25 @@ class CollapsibleBox(QWidget):
def setContentLayout(
self,
layout: QLayout,
):
) -> None:
self.content_area.setLayout(layout)
def setContentWidget(
self,
widget: QWidget,
):
) -> None:
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder
layout.addWidget(widget)
self.setContentLayout(layout)
def set_additional_text(
self,
text: str,
) -> None:
new_txt = self.base_text + text
self.toggle_button.setText(new_txt)
def set_open(
self,
open: bool,
@@ -3362,13 +3380,30 @@ class Page_InitRec(QWidget):
QLineEdit, search_widget.company_widgets["ma_unternehmensname"]
)
# ** documents
self.docs_box = CollapsibleBox("Dokumente")
docs_widget_state = Page_InitRec_DocumentSection_State(
session=self.STATE.session,
docs=self.STATE.docs,
locked=self.STATE.locked,
geloescht=self.STATE.geloescht,
)
self.docs_widget = DocumentSectionWidget(docs_widget_state)
self.docs_box.setContentWidget(self.docs_widget)
self.container_layout.addWidget(self.docs_box)
self.docs_widget.doc_count_changed.connect(self._trigger_update_docs_box_title)
self.container_layout.addStretch()
self._update_layout()
def _debug_print_state(self) -> None:
logger_initrecform.info(
self._sync_GUI_to_state()
logger_page_initrec.info(
"[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids)
)
logger_page_initrec.info(
"[Page -- InitRec] Printing state Docs:\n%s", pformat(self.STATE.docs)
)
def _request_company_profile(self) -> None:
assert self.STATE.ids.un_id is not None, (
@@ -3378,9 +3413,16 @@ class Page_InitRec(QWidget):
session=self.STATE.session,
un_id=self.STATE.ids.un_id,
)
logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state)
logger_page_initrec.debug("[Page -- InitRec] State to call: %s", req_state)
self.company_profile_requested.emit(req_state)
def _trigger_update_docs_box_title(
self,
count: int,
) -> None:
add_txt = f" ({count})"
self.docs_box.set_additional_text(add_txt)
def _update_layout(self) -> None:
self.initrec_form_metadata_box.set_open(True)
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
@@ -3518,6 +3560,7 @@ class Page_InitRec(QWidget):
def _reset(self) -> None:
for form in self.STATE.child_modules:
form.reset_form()
self.STATE.docs.clear()
self.STATE.ids = EntityIds()
self.STATE.geloescht = False
@@ -3543,7 +3586,6 @@ class Page_InitRec(QWidget):
logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!")
return
# TODO add loading logic with backend call
initrec = backend.page_initrec_from_db(ids)
# TODO re-enable later
@@ -3612,6 +3654,31 @@ class Page_InitRec(QWidget):
case _:
raise RuntimeError(f"Unspecified entity type {ent_type}")
if update_only:
relevant_gui_states = [s for s in self.STATE.docs if not s.geloescht]
rel_doc_states = [s for s in data.docs if s.geloescht is None]
assert len(relevant_gui_states) == len(rel_doc_states)
for gui_state, pydantic_state in zip(relevant_gui_states, rel_doc_states):
gui_state.id = pydantic_state.id
gui_state.speicher_pfad = pydantic_state.speicher_pfad
else:
self.STATE.docs.clear()
for pydantic_state in data.docs:
# 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)
doc_state = Page_InitRec_DocumentSection_Doc_State(
session=self.STATE.session,
id=pydantic_state.id,
speicher_pfad=pydantic_state.speicher_pfad,
dateiname_original=pydantic_state.dateiname_original,
datum_hinzugefuegt=pydantic_state.datum_hinzugefuegt,
mime_type=pydantic_state.mime_type,
anmerkung=pydantic_state.anmerkung,
locked=self.STATE.locked,
)
self.STATE.docs.append(doc_state)
if sync_to_GUI:
self._sync_state_to_GUI(None)
@@ -3630,6 +3697,14 @@ class Page_InitRec(QWidget):
self._reset()
self._update_layout()
doc_widget_state = Page_InitRec_DocumentSection_State(
session=self.STATE.session,
docs=self.STATE.docs,
locked=self.STATE.locked,
geloescht=self.STATE.geloescht,
)
self.docs_widget.load_state(doc_widget_state)
if section is None or section == "banners":
self._clear_info_banners()
if self.STATE.ids.pers_id is None:
@@ -3642,10 +3717,12 @@ class Page_InitRec(QWidget):
if self.STATE.locked:
for form in self.STATE.child_modules:
form.lock()
self.docs_widget.lock()
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
else:
for form in self.STATE.child_modules:
form.unlock()
self.docs_widget.unlock()
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
self._activate_delete()
@@ -3693,6 +3770,9 @@ class Page_InitRec(QWidget):
case _:
raise RuntimeError(f"Unspecified entity type {form_state.ent_type}")
docs_widget_state = self.docs_widget.get_state()
self.STATE.docs = docs_widget_state.docs
logger_page_initrec.debug(
"[Page -- InitRec] Syncing GUI to state: IDs after...\n%s",
pformat(self.STATE.ids),
@@ -3729,6 +3809,8 @@ class Page_InitRec(QWidget):
errors.extend(m.validate())
errors.extend(self.docs_widget.validate())
return errors
def lock(self) -> None:
@@ -3763,13 +3845,35 @@ class Page_InitRec(QWidget):
"[Page -- InitRec] This was saved in the database "
"and is marked for deletion - fast path."
)
deleted_datetime = datetime.datetime.now(datetime.UTC)
ts_deleted = datetime.datetime.now(datetime.UTC)
assert self.STATE.form_data, "cannot delete entry with uninitialised form data"
pydantic_docs: list[Grunderfassung_Dokument] = []
for doc_state in self.STATE.docs:
if doc_state.id is None:
logger_page_initrec.debug(
(
"[Page -- InitRec] State which was never saved in the database "
"and is marked for deletion - ignore."
)
)
continue
doc_state_dict = dc.asdict(doc_state)
del doc_state_dict["geloescht"]
doc = Grunderfassung_Dokument(
geloescht=ts_deleted,
pers_id=self.STATE.ids.pers_id,
**doc_state_dict,
)
pydantic_docs.append(doc)
data_to_db = InitRec(
ids=self.STATE.ids,
recording_type=self.STATE.recording_type,
geloescht=deleted_datetime,
geloescht=ts_deleted,
form_data=db_data,
docs=pydantic_docs,
)
returned = backend.page_initrec_to_db(data_to_db)
self._load_from_pydantic(returned)
@@ -3817,6 +3921,45 @@ class Page_InitRec(QWidget):
case _:
raise RuntimeError(f"Unspecified entity type {ent_type}")
ts_deleted: datetime.datetime | None = None
if self.STATE.geloescht:
ts_deleted = datetime.datetime.now(datetime.UTC)
pydantic_docs: list[Grunderfassung_Dokument] = []
for doc_state in self.STATE.docs:
if doc_state.id is None and doc_state.geloescht:
logger_page_initrec.debug(
(
"[Page -- InitRec] State which was never saved in the database "
"and is marked for deletion - ignore."
)
)
continue
ts_deleted_doc: datetime.datetime | None = None
if ts_deleted is not None:
# if process is deleted, all sessions must be marked as deleted
ts_deleted_doc = ts_deleted
elif doc_state.geloescht:
ts_deleted_doc = datetime.datetime.now(datetime.UTC)
doc_state_dict = dc.asdict(doc_state)
del doc_state_dict["geloescht"]
doc = Grunderfassung_Dokument(
geloescht=ts_deleted_doc,
pers_id=self.STATE.ids.pers_id,
**doc_state_dict,
)
pydantic_docs.append(doc)
data_to_db = InitRec(
ids=self.STATE.ids,
recording_type=self.STATE.recording_type,
geloescht=ts_deleted,
form_data=db_data,
docs=pydantic_docs,
)
except ValidationError as e:
logger_page_initrec.error(
"[Page -- InitRec] Error during Pydantic validation phase:\n%s",
@@ -3834,19 +3977,10 @@ class Page_InitRec(QWidget):
"[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s",
pformat(db_data),
)
deleted_datetime: datetime.datetime | None = None
if self.STATE.geloescht:
deleted_datetime = datetime.datetime.now(datetime.UTC)
try:
# TODO change to result pattern
data_to_db = InitRec(
ids=self.STATE.ids,
recording_type=self.STATE.recording_type,
geloescht=deleted_datetime,
form_data=db_data,
)
returned = backend.page_initrec_to_db(data_to_db)
self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False)
self.update_triggered.emit()
except Exception as err: