generated from dopt-python/py311
prepare document selection widget
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
Ext_Kontaktliste.Master
|
Ext_Kontaktliste.Master
|
||||||
ma_id int pk
|
ma_id int pk
|
||||||
|
|
||||||
|
|
||||||
Ext_Kontaktliste.Ansprechpartner
|
Ext_Kontaktliste.Ansprechpartner
|
||||||
an_id int pk
|
an_id int pk
|
||||||
ma_id int -> Ext_Kontaktliste.Master(ma_id)
|
ma_id int -> Ext_Kontaktliste.Master(ma_id)
|
||||||
|
|
||||||
|
|
||||||
Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'.
|
Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'.
|
||||||
un_id int pk -> Ext_Kontaktliste.Master(ma_id)
|
un_id int pk -> Ext_Kontaktliste.Master(ma_id)
|
||||||
erstellt "timestamp with time zone"
|
erstellt "timestamp with time zone"
|
||||||
geloescht "timestamp with time zone" nullable
|
geloescht "timestamp with time zone" nullable
|
||||||
|
|
||||||
|
|
||||||
Personen_Unternehmen_Zuordnung
|
Personen_Unternehmen_Zuordnung
|
||||||
id int pk {autoIncrement}
|
id int pk {autoIncrement}
|
||||||
pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung
|
pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung
|
||||||
@@ -20,6 +23,7 @@ Personen_Unternehmen_Zuordnung
|
|||||||
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`}
|
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`}
|
||||||
geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL)
|
geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL)
|
||||||
|
|
||||||
|
|
||||||
Grunderfassung_Personen
|
Grunderfassung_Personen
|
||||||
pers_id int pk {autoIncrement}
|
pers_id int pk {autoIncrement}
|
||||||
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
||||||
@@ -72,16 +76,19 @@ Grunderfassung_Personen
|
|||||||
WeitereInfos__WI_deutsch_sprache text nullable
|
WeitereInfos__WI_deutsch_sprache text nullable
|
||||||
WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable
|
WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable
|
||||||
WeitereInfos__WI_meldung_institution text nullable
|
WeitereInfos__WI_meldung_institution text nullable
|
||||||
# Metadaten_erstellung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable
|
|
||||||
# Metadaten_aktualisierung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`}
|
|
||||||
|
|
||||||
|
|
||||||
# Zuordnung_Personen_Unternehmen | Eine Person kann zu mehreren Unternehmen gehören, ein Unternehmen kann mehrere Personen betreuen.
|
Grunderfassung_Dokumente
|
||||||
# id int pk {autoIncrement}
|
id int pk {autoIncrement}
|
||||||
# un_id int -> Grunderfassung_Unternehmen(un_id)
|
pers_id int -> Grunderfassung_Personen(pers_id)
|
||||||
# pers_id int -> Grunderfassung_Personen(pers_id)
|
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
||||||
# gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`}
|
||||||
# gueltig_bis "timestamp with time zone" nullable
|
geloescht "timestamp with time zone" nullable
|
||||||
|
speicher_pfad text
|
||||||
|
dateiname_original text
|
||||||
|
dateigroesse int nullable
|
||||||
|
mime_type text
|
||||||
|
anmerkung text
|
||||||
|
|
||||||
|
|
||||||
Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein (pauschal) oder beide (individual) gebunden sein
|
Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein (pauschal) oder beide (individual) gebunden sein
|
||||||
@@ -93,7 +100,7 @@ Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein
|
|||||||
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)`
|
||||||
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`}
|
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`}
|
||||||
geloescht "timestamp with time zone" nullable
|
geloescht "timestamp with time zone" nullable
|
||||||
# temporaer bool | Gibt an, ob der Vorgang losgelöst von der Grunderfassung gestartet wurde (Daten müssen nachträglich zugeordnet werden); theoretisch auch erfüllt, wenn `un_id` und `pers_id` beide NULL sind
|
|
||||||
|
|
||||||
Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden
|
Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden
|
||||||
beratung_id int pk {autoIncrement}
|
beratung_id int pk {autoIncrement}
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
QFileInfo,
|
||||||
|
QMimeDatabase,
|
||||||
|
Qt,
|
||||||
|
QUrl,
|
||||||
|
Signal,
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
|
QFileDialog,
|
||||||
|
QFileIconProvider,
|
||||||
|
QFrame,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMainWindow,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from wce_crm.constants import TIMEZONE_CEST
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wce_crm.data_models import Module
|
||||||
|
|
||||||
|
|
||||||
|
def get_mime_type(
|
||||||
|
file_path: Path,
|
||||||
|
) -> str:
|
||||||
|
"""gets the MIME type of a file via PySide6 / QMimeDatabase"""
|
||||||
|
db = QMimeDatabase()
|
||||||
|
mime_type = db.mimeTypeForFile(str(file_path))
|
||||||
|
|
||||||
|
# .name() -> for example "application/pdf" or "image/png"
|
||||||
|
return mime_type.name()
|
||||||
|
|
||||||
|
|
||||||
|
def _format_file_size(size_in_bytes):
|
||||||
|
"""converts bytes in readable string, e.g. KB or MB"""
|
||||||
|
if size_in_bytes < 1024:
|
||||||
|
return f"{size_in_bytes} B"
|
||||||
|
elif size_in_bytes < 1024 * 1024:
|
||||||
|
return f"{size_in_bytes / 1024:.1f} KB"
|
||||||
|
else:
|
||||||
|
return f"{size_in_bytes / (1024 * 1024):.1f} MB"
|
||||||
|
|
||||||
|
|
||||||
|
class _DropZoneWidget(QFrame):
|
||||||
|
files_dropped = Signal(list) # sends list of pathlib.Path objects
|
||||||
|
|
||||||
|
STYLE_NORMAL = """
|
||||||
|
_DropZoneWidget {
|
||||||
|
border: 2px dashed #B0B0B0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #F9F9F9;
|
||||||
|
}
|
||||||
|
_DropZoneWidget:hover {
|
||||||
|
border-color: #0078D4;
|
||||||
|
background-color: #F0F6FF;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
STYLE_DRAG_ACTIVE = """
|
||||||
|
_DropZoneWidget {
|
||||||
|
border: 2px dashed #0078D4;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #E1EFFA;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
|
self.setStyleSheet(self.STYLE_NORMAL)
|
||||||
|
self.setFixedHeight(60)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.label = QLabel(
|
||||||
|
"📁 <b>Dateien hierher ziehen</b> oder <u>klicken</u> zum Auswählen"
|
||||||
|
)
|
||||||
|
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
def dragEnterEvent(
|
||||||
|
self,
|
||||||
|
event: QDragEnterEvent,
|
||||||
|
):
|
||||||
|
if event.mimeData().hasUrls():
|
||||||
|
# check if MDB/explorer gives at least one real file (no folder)
|
||||||
|
has_files = False
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
pfad = url.toLocalFile()
|
||||||
|
if pfad and os.path.isfile(pfad):
|
||||||
|
has_files = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_files:
|
||||||
|
event.acceptProposedAction()
|
||||||
|
self.setStyleSheet(self.STYLE_DRAG_ACTIVE)
|
||||||
|
|
||||||
|
def dragLeaveEvent(self, event):
|
||||||
|
self.setStyleSheet(self.STYLE_NORMAL)
|
||||||
|
|
||||||
|
def dropEvent(self, event: QDropEvent):
|
||||||
|
self.dragLeaveEvent(None)
|
||||||
|
|
||||||
|
valid_files: list[Path] = []
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
if url.isLocalFile():
|
||||||
|
path = Path(url.toLocalFile()).resolve()
|
||||||
|
# only add if it is a file
|
||||||
|
if path.is_file():
|
||||||
|
valid_files.append(path)
|
||||||
|
|
||||||
|
if valid_files:
|
||||||
|
self.files_dropped.emit(valid_files)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen")
|
||||||
|
if files:
|
||||||
|
files_pathlib = [Path(file).resolve() for file in files]
|
||||||
|
self.files_dropped.emit(files_pathlib)
|
||||||
|
|
||||||
|
|
||||||
|
class _FileItemWidget(QFrame):
|
||||||
|
request_remove = Signal(QWidget) # signal to parent to remove entry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
file_path: Path,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.file_path = file_path
|
||||||
|
self._created_at_UTC = datetime.datetime.now(tz=datetime.UTC)
|
||||||
|
self.created_at_string = self._created_at_UTC.astimezone(TIMEZONE_CEST).strftime(
|
||||||
|
"%d.%m.%Y %H:%M"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setStyleSheet("""
|
||||||
|
_FileItemWidget {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
_FileItemWidget:hover {
|
||||||
|
border-color: #B0B0B0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout = QHBoxLayout(self)
|
||||||
|
layout.setContentsMargins(8, 6, 8, 6)
|
||||||
|
|
||||||
|
# file icon: use dummy to avoid icons with checkmarks (synchronised folder)
|
||||||
|
icon_label = QLabel()
|
||||||
|
provider = QFileIconProvider()
|
||||||
|
file_icon = provider.icon(QFileInfo(f"dummy{file_path.suffix}"))
|
||||||
|
icon_label.setPixmap(file_icon.pixmap(28, 28))
|
||||||
|
layout.addWidget(icon_label)
|
||||||
|
|
||||||
|
# file info: name, size, date, mime type
|
||||||
|
info_vbox = QVBoxLayout()
|
||||||
|
info_vbox.setSpacing(2)
|
||||||
|
|
||||||
|
self.original_name = file_path.name
|
||||||
|
self.mime_type = get_mime_type(file_path)
|
||||||
|
self._raw_file_size: int | None = None
|
||||||
|
self.file_size_display: str
|
||||||
|
try:
|
||||||
|
self._raw_file_size = file_path.stat().st_size
|
||||||
|
self.file_size_display = _format_file_size(self._raw_file_size)
|
||||||
|
except OSError:
|
||||||
|
self.file_size_display = "Unbekannt"
|
||||||
|
|
||||||
|
name_label = QLabel(f"<b>{self.original_name}</b>")
|
||||||
|
meta_label = QLabel(
|
||||||
|
(
|
||||||
|
f"<span style='color: #666666;'>{self.file_size_display} • Hinzugefügt: "
|
||||||
|
f"{self.created_at_string}</span>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
meta_label.setStyleSheet("font-size: 11px;")
|
||||||
|
|
||||||
|
# additional note field for more comprehensible file descriptions
|
||||||
|
self.note_edit = QLineEdit()
|
||||||
|
self.note_edit.setPlaceholderText("Eigene Anmerkung / Bez. eingeben (optional)...")
|
||||||
|
self.note_edit.setStyleSheet("""
|
||||||
|
QLineEdit {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
min-height: 22px; /* avoid vertical cropping of text */
|
||||||
|
border: 1px solid #CCCCCC;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
QLineEdit:focus {
|
||||||
|
border-color: #0078D4; /* blue frame on click */
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
info_vbox.addWidget(name_label)
|
||||||
|
info_vbox.addWidget(meta_label)
|
||||||
|
info_vbox.addWidget(self.note_edit)
|
||||||
|
layout.addLayout(info_vbox, stretch=2)
|
||||||
|
|
||||||
|
# category dropdown: not needed by WCE, but still included to re-enable later
|
||||||
|
# TODO maybe add later
|
||||||
|
self.category_combo = QComboBox()
|
||||||
|
self.category_combo.addItems(
|
||||||
|
[
|
||||||
|
"Allgemein / Unsortiert",
|
||||||
|
"Identitätsnachweis",
|
||||||
|
"Vertrag",
|
||||||
|
"Gewerbeanmeldung",
|
||||||
|
"Sonstiges",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.category_combo.setFixedWidth(160)
|
||||||
|
layout.addWidget(self.category_combo)
|
||||||
|
self.category_combo.setVisible(False)
|
||||||
|
|
||||||
|
# action buttons: view and delete
|
||||||
|
self.btn_preview = QPushButton("👁️")
|
||||||
|
self.btn_preview.setToolTip("Datei im Standardprogramm öffnen")
|
||||||
|
self.btn_preview.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.btn_preview.setFixedWidth(36)
|
||||||
|
self.btn_preview.clicked.connect(self._open_preview)
|
||||||
|
|
||||||
|
self.btn_delete = QPushButton("🗑️")
|
||||||
|
self.btn_delete.setToolTip("Datei entfernen")
|
||||||
|
self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.btn_delete.setFixedWidth(36)
|
||||||
|
self.btn_delete.clicked.connect(lambda: self.request_remove.emit(self))
|
||||||
|
|
||||||
|
layout.addWidget(self.btn_preview)
|
||||||
|
layout.addWidget(self.btn_delete)
|
||||||
|
|
||||||
|
def _open_preview(self):
|
||||||
|
QDesktopServices.openUrl(QUrl.fromLocalFile(self.file_path))
|
||||||
|
|
||||||
|
# TODO change to protocol/interface with state
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
"file_path": self.file_path,
|
||||||
|
"created_at": self._created_at_UTC,
|
||||||
|
"category": None,
|
||||||
|
"note": self.note_edit.text().strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentSectionWidget(QWidget):
|
||||||
|
"""main widget for file selection"""
|
||||||
|
|
||||||
|
count_changed = Signal(int) # sends current number of files contained by the widget
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Dropzone at the top
|
||||||
|
self.drop_zone = _DropZoneWidget()
|
||||||
|
self.drop_zone.files_dropped.connect(self._add_files)
|
||||||
|
main_layout.addWidget(self.drop_zone)
|
||||||
|
|
||||||
|
# list containing the file items
|
||||||
|
self.file_list_widget = QListWidget()
|
||||||
|
self.file_list_widget.setStyleSheet("""
|
||||||
|
QListWidget {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px 0px 6px 0px; /* 6px Abstand zwischen den einzelnen Datei-Zeilen */
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
main_layout.addWidget(self.file_list_widget)
|
||||||
|
|
||||||
|
def _add_files(
|
||||||
|
self,
|
||||||
|
filepaths: Iterable[Path],
|
||||||
|
):
|
||||||
|
for path in filepaths:
|
||||||
|
# create custom row widget
|
||||||
|
item_widget = _FileItemWidget(path)
|
||||||
|
item_widget.request_remove.connect(self._remove_file_item)
|
||||||
|
|
||||||
|
# embed in QListWidget
|
||||||
|
list_item = QListWidgetItem(self.file_list_widget)
|
||||||
|
# min size to avoid cropping of text
|
||||||
|
size_hint = item_widget.sizeHint()
|
||||||
|
size_hint.setHeight(size_hint.height() + 4)
|
||||||
|
list_item.setSizeHint(size_hint)
|
||||||
|
|
||||||
|
self.file_list_widget.addItem(list_item)
|
||||||
|
self.file_list_widget.setItemWidget(list_item, item_widget)
|
||||||
|
|
||||||
|
self.count_changed.emit(self.file_list_widget.count())
|
||||||
|
|
||||||
|
def _remove_file_item(
|
||||||
|
self,
|
||||||
|
widget: QWidget,
|
||||||
|
):
|
||||||
|
# search and delete row
|
||||||
|
for i in range(self.file_list_widget.count()):
|
||||||
|
item = self.file_list_widget.item(i)
|
||||||
|
if self.file_list_widget.itemWidget(item) == widget:
|
||||||
|
self.file_list_widget.takeItem(i)
|
||||||
|
break
|
||||||
|
self.count_changed.emit(self.file_list_widget.count())
|
||||||
|
|
||||||
|
# TODO change to protocol/interface with state
|
||||||
|
def get_all_documents_data(self):
|
||||||
|
"""Methode zum Auslesen aller angehängten Dateien inkl. Metadaten für das Speichern."""
|
||||||
|
documents = []
|
||||||
|
for i in range(self.file_list_widget.count()):
|
||||||
|
item = self.file_list_widget.item(i)
|
||||||
|
widget = self.file_list_widget.itemWidget(item)
|
||||||
|
if isinstance(widget, _FileItemWidget):
|
||||||
|
documents.append(widget.get_data())
|
||||||
|
return documents
|
||||||
|
|
||||||
|
|
||||||
|
class _MainWindow(QMainWindow):
|
||||||
|
"""main window for internal test only, not the this module's component"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Prototyp: Dokumente & Anhänge")
|
||||||
|
self.resize(650, 500)
|
||||||
|
|
||||||
|
central_widget = QWidget()
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# simulate custom widget in form
|
||||||
|
self.header_label = QLabel("<b>📎 Dokumente & Anhänge (0)</b>")
|
||||||
|
self.header_label.setStyleSheet("font-size: 14px; color: #333333;")
|
||||||
|
layout.addWidget(self.header_label)
|
||||||
|
|
||||||
|
# custom widget
|
||||||
|
self.doc_section = DocumentSectionWidget()
|
||||||
|
self.doc_section.count_changed.connect(self.update_header)
|
||||||
|
layout.addWidget(self.doc_section)
|
||||||
|
|
||||||
|
# test button to trigger data gathering (simulation for saving procedure and
|
||||||
|
# backend interaction)
|
||||||
|
self.btn_save = QPushButton("Erfassung speichern (Daten auslesen)")
|
||||||
|
self.btn_save.setStyleSheet(
|
||||||
|
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||||
|
)
|
||||||
|
self.btn_save.clicked.connect(self.print_backend_data)
|
||||||
|
layout.addWidget(self.btn_save)
|
||||||
|
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
def update_header(self, count):
|
||||||
|
"""update counter in corresponding label, must be mapped to section heading in form"""
|
||||||
|
self.header_label.setText(f"<b>📎 Dokumente & Anhänge ({count})</b>")
|
||||||
|
|
||||||
|
def print_backend_data(self):
|
||||||
|
data = self.doc_section.get_all_documents_data()
|
||||||
|
print("\n--- DATEN FÜR SPEICHERVORGANG BEREIT ---")
|
||||||
|
for i, doc in enumerate(data, 1):
|
||||||
|
print(f"Datei {i}:")
|
||||||
|
print(f" Pfad: {doc['file_path']}")
|
||||||
|
print(f" Datum: {doc['created_at']}")
|
||||||
|
print(f" Kategorie: {doc['category']}")
|
||||||
|
print(f" Anmerkung: {doc['note']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = _MainWindow()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# enable static type checking for the protocol
|
||||||
|
_m1: type[Module] = DocumentSectionWidget
|
||||||
|
_m2: type[Module] = _FileItemWidget
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+1
-1
@@ -275,7 +275,7 @@ print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}")
|
|||||||
|
|
||||||
# %%
|
# %%
|
||||||
db_path = constants.Config.DB_PATH_MAIN
|
db_path = constants.Config.DB_PATH_MAIN
|
||||||
crm_path = constants.Config.DB_PATH_CRM
|
crm_path = constants.Config.DB_PATH_KONTAKTLISTE
|
||||||
engine = sql.create_engine(f"sqlite:///{str(db_path)}")
|
engine = sql.create_engine(f"sqlite:///{str(db_path)}")
|
||||||
engine_crm = sql.create_engine(f"sqlite:///{str(crm_path)}")
|
engine_crm = sql.create_engine(f"sqlite:///{str(crm_path)}")
|
||||||
# %%
|
# %%
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ assert BASE_PATH
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None))
|
DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None))
|
||||||
DB_PATH_CRM: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
DB_PATH_KONTAKTLISTE: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
||||||
DB_PATH_MAIN: Path = BASE_PATH / os.getenv(
|
DB_PATH_MAIN: Path = BASE_PATH / os.getenv(
|
||||||
"DOPT_DB_MAIN", "data/db/wce_grunderfassung.db"
|
"DOPT_DB_MAIN", "data/db/wce_grunderfassung.db"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dataclasses as dc
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar
|
from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar
|
||||||
|
|
||||||
from dopt_basics.datastructures import DualDict
|
from dopt_basics.datastructures import DualDict
|
||||||
@@ -265,6 +266,34 @@ class Page_InitRec_Form_State(PageState[Module]):
|
|||||||
geloescht: bool = False
|
geloescht: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass(slots=True, kw_only=True)
|
||||||
|
class Page_InitRec_DocumentSection_State:
|
||||||
|
session: Session
|
||||||
|
pers_id: int | None
|
||||||
|
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
||||||
|
locked: bool
|
||||||
|
geloescht: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def doc_count(self) -> int:
|
||||||
|
return len(self.docs)
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass(slots=True, kw_only=True)
|
||||||
|
class Page_InitRec_DocumentSection_Doc_State:
|
||||||
|
id: int | None
|
||||||
|
pers_id: int | None
|
||||||
|
datum_erstellt_utc: datetime.datetime = dc.field(
|
||||||
|
default_factory=lambda: datetime.datetime.now(datetime.UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
speicher_pfad: Path
|
||||||
|
dateiname_original: str
|
||||||
|
dateigroesse: int | None
|
||||||
|
locked: bool
|
||||||
|
geloescht: bool = False
|
||||||
|
|
||||||
|
|
||||||
@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
|
||||||
|
|||||||
+19
-8
@@ -45,29 +45,40 @@ class UTCDateTime(TypeDecorator):
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
# Ensure it's converted to UTC before saving
|
# Ensure it's converted to UTC before saving
|
||||||
if value.tzinfo is None:
|
if value.tzinfo is None:
|
||||||
value = value.replace(tzinfo=datetime.timezone.utc)
|
value = value.replace(tzinfo=datetime.UTC)
|
||||||
else:
|
else:
|
||||||
value = value.astimezone(datetime.timezone.utc)
|
value = value.astimezone(datetime.UTC)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
def process_result_value(self, value, dialect):
|
||||||
"""Runs when fetching from the database."""
|
"""Runs when fetching from the database."""
|
||||||
if value is not None and value.tzinfo is None:
|
if value is not None and value.tzinfo is None:
|
||||||
# Explicitly tell Python this data *is* UTC
|
# Explicitly tell Python this data *is* UTC
|
||||||
value = value.replace(tzinfo=datetime.timezone.utc)
|
value = value.replace(tzinfo=datetime.UTC)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
MD_CRM = sa.MetaData()
|
MD_CRM = sa.MetaData()
|
||||||
MD_MAIN = sa.MetaData()
|
MD_MAIN = sa.MetaData()
|
||||||
ENGINE = sa.create_engine(f"sqlite:///{constants.Config.DB_PATH_MAIN}")
|
URL_MAIN = sa.engine.URL.create(
|
||||||
|
"sqlite+pysqlite",
|
||||||
|
database=str(constants.Config.DB_PATH_MAIN),
|
||||||
|
)
|
||||||
|
ENGINE = sa.create_engine(
|
||||||
|
URL_MAIN,
|
||||||
|
connect_args={"uri": True, "timeout": 0.5},
|
||||||
|
)
|
||||||
|
|
||||||
|
KONTAKTLISTE_URI = constants.Config.DB_PATH_KONTAKTLISTE.as_uri() + "?mode=ro"
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(ENGINE, "connect")
|
@event.listens_for(ENGINE, "connect")
|
||||||
def attach_second_db(dbapi_connection, connection_record):
|
def _db_conn_event(dbapi_connection, connection_record):
|
||||||
cursor = dbapi_connection.cursor()
|
cursor = dbapi_connection.cursor()
|
||||||
# 'kontaktliste' is the alias for the second DB
|
# 'kontaktliste' is the alias for the second DB
|
||||||
cursor.execute(f"ATTACH DATABASE '{constants.Config.DB_PATH_CRM}' AS kontaktliste")
|
cursor.execute("ATTACH DATABASE ? AS kontaktliste", (KONTAKTLISTE_URI,))
|
||||||
|
# enable foreign keys
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +157,7 @@ def get_ext_crm_master(
|
|||||||
return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema)
|
return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema)
|
||||||
|
|
||||||
|
|
||||||
DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_CRM)
|
DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_KONTAKTLISTE)
|
||||||
|
|
||||||
ext_crm_nutzer: Table = Table(
|
ext_crm_nutzer: Table = Table(
|
||||||
"Nutzer",
|
"Nutzer",
|
||||||
@@ -253,7 +264,7 @@ def get_ext_crm_contact_person(
|
|||||||
|
|
||||||
|
|
||||||
# df_contact_person = get_ext_crm_contact_person(None)
|
# df_contact_person = get_ext_crm_contact_person(None)
|
||||||
DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_CRM)
|
DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_KONTAKTLISTE)
|
||||||
|
|
||||||
t_ext_crm_master_attach: Table = Table(
|
t_ext_crm_master_attach: Table = Table(
|
||||||
"Master",
|
"Master",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class CountryList:
|
|||||||
|
|
||||||
|
|
||||||
def get_country_list_german() -> CountryList:
|
def get_country_list_german() -> CountryList:
|
||||||
locale = babel.Locale("de", "DE")
|
locale = babel.Locale("en", "GB")
|
||||||
countries: list[tuple[str, str]] = []
|
countries: list[tuple[str, str]] = []
|
||||||
iso_to_country: dict[str, str] = {}
|
iso_to_country: dict[str, str] = {}
|
||||||
|
|
||||||
@@ -938,7 +938,7 @@ INITREC_PERSON = [
|
|||||||
trigger_value="ja",
|
trigger_value="ja",
|
||||||
children=[
|
children=[
|
||||||
FormField(
|
FormField(
|
||||||
"Relevanz",
|
"Relevant perspektivERZ",
|
||||||
FormFieldType.DROPDOWN,
|
FormFieldType.DROPDOWN,
|
||||||
required=True,
|
required=True,
|
||||||
options=[("ja", None), ("nein", None)],
|
options=[("ja", None), ("nein", None)],
|
||||||
|
|||||||
+21
-6
@@ -3256,7 +3256,7 @@ class Page_InitRec(QWidget):
|
|||||||
self.modify_btn.modify_activated.connect(self.unlock)
|
self.modify_btn.modify_activated.connect(self.unlock)
|
||||||
self.modify_btn.modify_deactivated.connect(self.lock)
|
self.modify_btn.modify_deactivated.connect(self.lock)
|
||||||
self.save_btn.clicked.connect(self.save_data)
|
self.save_btn.clicked.connect(self.save_data)
|
||||||
self.reset_btn.clicked.connect(self._reset)
|
self.reset_btn.clicked.connect(self._trigger_reset_btn)
|
||||||
self.delete_btn.clicked.connect(self._delete_data)
|
self.delete_btn.clicked.connect(self._delete_data)
|
||||||
header_layout.addSpacing(5)
|
header_layout.addSpacing(5)
|
||||||
header_layout.addWidget(self.edit_buttons)
|
header_layout.addWidget(self.edit_buttons)
|
||||||
@@ -3477,6 +3477,21 @@ class Page_InitRec(QWidget):
|
|||||||
)
|
)
|
||||||
return all(valid_id_states)
|
return all(valid_id_states)
|
||||||
|
|
||||||
|
def _trigger_reset_btn(self) -> None:
|
||||||
|
assert not self.STATE.locked, "reset initialised, even though form is locked"
|
||||||
|
confirm = get_user_confirmation(
|
||||||
|
self,
|
||||||
|
"Zurücksetzen bestätigen",
|
||||||
|
(
|
||||||
|
"Soll der Eintrag wirklich zurückgesetzt werden? Alle nicht "
|
||||||
|
"gespeicherten Änderungen gehen verloren."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not confirm:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._sync_state_to_GUI(None)
|
||||||
|
|
||||||
def _delete_data(self) -> None:
|
def _delete_data(self) -> None:
|
||||||
assert self._deletion_valid(), "deletion initialised despite IDs are not properly set"
|
assert self._deletion_valid(), "deletion initialised despite IDs are not properly set"
|
||||||
confirm = get_user_confirmation(
|
confirm = get_user_confirmation(
|
||||||
@@ -3525,7 +3540,6 @@ class Page_InitRec(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# TODO add loading logic with backend call
|
# TODO add loading logic with backend call
|
||||||
# !! just a dummy
|
|
||||||
initrec = backend.page_initrec_from_db(ids)
|
initrec = backend.page_initrec_from_db(ids)
|
||||||
|
|
||||||
# TODO re-enable later
|
# TODO re-enable later
|
||||||
@@ -4589,6 +4603,7 @@ class Page_Consulting(QWidget):
|
|||||||
container_layout.addWidget(self.info_banners_container)
|
container_layout.addWidget(self.info_banners_container)
|
||||||
|
|
||||||
# --- BUTTONS ---
|
# --- BUTTONS ---
|
||||||
|
# TODO check if no reset button is correct
|
||||||
self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=False)
|
self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=False)
|
||||||
modify_btn = self.edit_buttons.modify_button
|
modify_btn = self.edit_buttons.modify_button
|
||||||
assert modify_btn
|
assert modify_btn
|
||||||
@@ -6165,7 +6180,7 @@ class EditButtons(QWidget):
|
|||||||
self.save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
self.save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
layout_btn.addWidget(self.save_btn)
|
layout_btn.addWidget(self.save_btn)
|
||||||
# reset
|
# reset
|
||||||
self.reset_button: QPushButton | None = None
|
self.reset_btn: QPushButton | None = None
|
||||||
if add_reset_btn:
|
if add_reset_btn:
|
||||||
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
|
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
|
||||||
self.reset_btn.setShortcut("Ctrl+Z")
|
self.reset_btn.setShortcut("Ctrl+Z")
|
||||||
@@ -6185,12 +6200,15 @@ class EditButtons(QWidget):
|
|||||||
self,
|
self,
|
||||||
enable_modification: bool,
|
enable_modification: bool,
|
||||||
set_delete_btn: bool = True,
|
set_delete_btn: bool = True,
|
||||||
|
set_reset_btn: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.modify_button is not None:
|
if self.modify_button is not None:
|
||||||
self.modify_button.set_modify_state(enable_modification)
|
self.modify_button.set_modify_state(enable_modification)
|
||||||
self.save_btn.setEnabled(enable_modification)
|
self.save_btn.setEnabled(enable_modification)
|
||||||
if set_delete_btn:
|
if set_delete_btn:
|
||||||
self.delete_btn.setEnabled(enable_modification)
|
self.delete_btn.setEnabled(enable_modification)
|
||||||
|
if self.reset_btn is not None and set_reset_btn:
|
||||||
|
self.reset_btn.setEnabled(enable_modification)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -6220,8 +6238,6 @@ class MainWindow(QMainWindow):
|
|||||||
new_initrec_select_state = Page_NewEntry_State(session=self.STATE.session)
|
new_initrec_select_state = Page_NewEntry_State(session=self.STATE.session)
|
||||||
self.new_entry_select = Page_NewEntry(new_initrec_select_state)
|
self.new_entry_select = Page_NewEntry(new_initrec_select_state)
|
||||||
self.new_entry_select.back_requested.connect(self.show_main_page)
|
self.new_entry_select.back_requested.connect(self.show_main_page)
|
||||||
# self.new_initrec_select.company_requested.connect(self.show_page_initrec_company)
|
|
||||||
# self.new_initrec_select.person_requested.connect(self.show_page_initrec_person)
|
|
||||||
self.new_entry_select.new_consulting_requested.connect(self.show_page_consulting)
|
self.new_entry_select.new_consulting_requested.connect(self.show_page_consulting)
|
||||||
self.new_entry_select.new_initrec_requested.connect(self.show_page_initrec)
|
self.new_entry_select.new_initrec_requested.connect(self.show_page_initrec)
|
||||||
self.stack.addWidget(self.new_entry_select)
|
self.stack.addWidget(self.new_entry_select)
|
||||||
@@ -6504,7 +6520,6 @@ def qt_message_handler(mode, context, message):
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# TODO add all relevant modules
|
|
||||||
# enable static type checking for the protocol
|
# enable static type checking for the protocol
|
||||||
_m1: type[Module] = Page_Consulting
|
_m1: type[Module] = Page_Consulting
|
||||||
_m2: type[Module] = Page_Consulting_Table
|
_m2: type[Module] = Page_Consulting_Table
|
||||||
|
|||||||
Reference in New Issue
Block a user