diff --git a/Gui/AddContact.qml b/Gui/AddContact.qml index c904cf1..78f78d7 100644 --- a/Gui/AddContact.qml +++ b/Gui/AddContact.qml @@ -143,9 +143,7 @@ Frame var bd = birthday.text if (len === 2 || len === 5) birthday.text = bd + "." } - } - } Label diff --git a/Gui/AddNewObject.qml b/Gui/AddNewObject.qml new file mode 100644 index 0000000..e48fbd8 --- /dev/null +++ b/Gui/AddNewObject.qml @@ -0,0 +1,277 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +GridLayout +{ + id: newObject + + columns: 4 + Layout.fillWidth: true + Layout.fillHeight: true + rowSpacing: 9 + + Label + { + text: qsTr("Firma") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + ComboBox + { + property string name: "business" + id: business + editable: true + Layout.fillWidth: true + Layout.columnSpan: 3 + } + + //// New grid row + + Label + { + text: qsTr("Straße") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + TextField + { + property string name: "street" + id: street + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onTextChanged: checkFields() + placeholderText: "Pflichtfeld" + placeholderTextColor: "red" + } + + Label + { + text: qsTr("Nr.") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + TextField + { + property string name: "houseno" + id: houseno + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onTextChanged: checkFields() + placeholderText: "Pflichtfeld" + placeholderTextColor: "red" + } + + // New grid row + Label + { + text: qsTr("PLZ") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + ComboBox + { + property string name: "postcode" + id: postcode + Layout.fillWidth: true + } + + Label + { + text: qsTr("Ort") + Layout.alignment: Qt.AlignRight + + } + + ComboBox + { + property string name: "city" + id: city + Layout.fillWidth: true + editable: true + onEditTextChanged: checkFields() + onCurrentTextChanged: checkFields() + model: address_model + textRole: "city" + popup.height: 300 + popup.y: postcode.y + 5 - (postcode.height * 2) + currentIndex: -1 + } + + // New grid row + Label + { + text: qsTr("Parteien") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + SpinBox + { + id: parteien + Layout.fillWidth: true + from: 1 + to: 100 + value: 1 + } + + Label + { + text: qsTr("Stockwerke") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + SpinBox + { + id: floors + Layout.fillWidth: true + from: 1 + to: 100 + value: 1 + } + + // New grid row + Label + { + text: qsTr("Zwischenetage") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + ComboBox + { + property string name: "mezzanin" + id: mezzanin + Layout.fillWidth: true + editable: false + model: [qsTr("Jööö"), qsTr("Nöööööööööööööööööööööööööö")] + } + + Label + { + text: qsTr("Aufzug") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + ComboBox + { + property string name: "lift" + id: lift + Layout.fillWidth: true + editable: false + model: [qsTr("Jööö"), qsTr("Nöööööööööööööööööööööööööö")] + } + + //New grid row + + Label + { + text: qsTr("Fenster") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + ComboBox + { + property string name: "windows" + id: windows + Layout.fillWidth: true + editable: false + model: [qsTr("Jööö"), qsTr("Nöööööööööööööööööööööööööö")] + onCurrentIndexChanged: nrWindows.enabled = (windows.currentIndex === 0)? true: false + } + + Label + { + text: qsTr("Anzahl") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + SpinBox + { + id: nrWindows + Layout.fillWidth: true + from: 0 + to: 100 + value: 0 + } + + // New grid row + CheckBox + { + id: ladder + text: qsTr("Leiter") + Layout.alignment: Qt.AlignRight + checked: false + onCheckStateChanged: + { + //checkFields() + } + } + + CheckBox + { + id: accessible + text: qsTr("Erreichbar") + Layout.alignment: Qt.AlignRight + checked: false + onCheckStateChanged: + { + //checkFields() + } + } + + Label + { + text: qsTr("Besonderheiten") + Layout.alignment: Qt.AlignRight + } + ComboBox + { + property string name: "remarks" + id: remarks + Layout.fillWidth: true + editable: false + textRole: "display" + } + + //// New grid row + Label + { + text: qsTr("kontaktdaten") + Layout.alignment: Qt.AlignRight | Qt.AlignTop + } + + ComboBox + { + property string name: "contact" + id: contact + Layout.fillWidth: true + editable: false + model: [qsTr("Beirat"), qsTr("Hausmeister")] + } + + Label + { + text: qsTr("Reingunsmittel wo?") + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + TextField + { + property string name: "cleansing" + id: cleamsing + Layout.fillWidth: true + placeholderText: "Pflichtfeld" + placeholderTextColor: "red" + } + Item + { + Layout.fillHeight: true + } +} + + + + + + + + + diff --git a/Gui/AddObject.qml b/Gui/AddObject.qml index d75845f..630b3b8 100644 --- a/Gui/AddObject.qml +++ b/Gui/AddObject.qml @@ -41,9 +41,9 @@ ColumnLayout { Layout.alignment: Qt.AlignTop Layout.fillWidth: true - ObjectView + AddNewObject { - id: objectView + id: newObject width: parent.width } } diff --git a/Gui/ApplicantPersonalData.qml b/Gui/ApplicantPersonalData.qml index 6f54a56..df19d65 100644 --- a/Gui/ApplicantPersonalData.qml +++ b/Gui/ApplicantPersonalData.qml @@ -182,10 +182,18 @@ GridLayout Layout.columnSpan: 3 visible: radio.children[1].checked validator: RegularExpressionValidator - { regularExpression: /((^|)(0[1-9]{1}|[1-2]{1}[0-9]{1}|3[0-1]))\.((^|)(0[1-9]{1}|1[0-2]{1}))\.((^|)(196[0-9]{1}|19[7-9]{1}[0-9]{1}|20[0-9]{2}))/ } + Keys.onPressed: (event)=> + { + if (event.key !== Qt.Key_Backspace) + { + var len = birthday.length + var bd = birthday.text + if (len === 2 || len === 5) birthday.text = bd + "." + } + } } Label diff --git a/Gui/CustomerContactDetails.qml b/Gui/CustomerContactDetails.qml new file mode 100644 index 0000000..6c66c9a --- /dev/null +++ b/Gui/CustomerContactDetails.qml @@ -0,0 +1,174 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +GridLayout +{ + columns: 2 + rowSpacing: 25 + Layout.leftMargin: 7 + + // Grid row + ColumnLayout + { + Layout.columnSpan: 2 + Label + { + id: contactLabel + color: "darksalmon" + font.bold: true + text: qsTr("Ansprechpartner") + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['salute'] + " " + contact['contact']['fname'] + " " + contact['contact']['lname']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Geburtsdatum") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['birthday']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("E-Mail") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['email']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Position") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['position']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Priorität") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['priority']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Telefon") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['phone']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Handy") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['cell']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Abrechnung") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['invoice']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Mahnung") + font.bold: true + } + + Label + { + color: "goldenrod" + text: contact? contact['contact']['reminder']: "" + } + } + + // Grid row + Item + { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + Component.onCompleted: + { + if (contact && contact['contact']['salute'] === "Frau") + contactLabel.text = qsTr("Ansprechpartnerin") + } +} diff --git a/Gui/CustomerDetails.qml b/Gui/CustomerDetails.qml index 9860d47..fbd4e54 100644 --- a/Gui/CustomerDetails.qml +++ b/Gui/CustomerDetails.qml @@ -2,26 +2,61 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -Item +ColumnLayout { property int selectedClient: -1 + property var client: null + property var contact: null id: clDet - ColumnLayout + + Button { - Label + text: qsTr("Zurück") + //Layout.columnSpan: 2 + onClicked: customersStack.pop() + } + + SplitView + { + id: clDetView + Layout.fillHeight: true + Layout.fillWidth: true + leftPadding: 9 + rightPadding: 9 + + CustomerDetailsView { - text: qsTr("Ausgewählter Kunde " + selectedClient) + id: customerDetails } - Button + CustomerContactDetails { - text: qsTr("Kunden zeigen") - onClicked: customersStack.pop() + id: contactDetails + visible: false } + + NoCustomerContact + { + id: noCustomerContact + visible: false + } + } + + Item + { + //Layout.columnSpan: 2 + Layout.fillHeight: true } Component.onCompleted: { - business_model.onRowClicked(selectedClient) + //business_model.onRowClicked(selectedClient) + client = business_model.getClientDetails() + if (client['business']['contactid'] > 0) + { + contact = contact_model.getContactDetails(client['business']['contactid']) + contactDetails.visible = true + } + else noCustomerContact.visible = true } } diff --git a/Gui/CustomerDetailsView.qml b/Gui/CustomerDetailsView.qml new file mode 100644 index 0000000..c69ac2b --- /dev/null +++ b/Gui/CustomerDetailsView.qml @@ -0,0 +1,225 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +GridLayout +{ + columns: 2 + rowSpacing: 25 + SplitView.preferredWidth: clDetView.width / 3 * 1.8 + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Steuer-ID") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['tax']? client['business']['tax']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Anmerkungen") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['info']? client['business']['info']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Kundenname") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['company'] + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("CEO") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['ceo'] + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Telefon") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['phone']? client['business']['phone']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Handy") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['cell']? client['business']['cell']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Webseite") + font.bold: true + } + + Label + { + id: clientWebsite + color: "goldenrod" + font.underline: false + text: client['business']['website']? '' + client['business']['website'] + '': "" + onLinkActivated: + { + var web_protocol = /^((http|https):\/\/)/; + var client_website = !web_protocol.test(client['business']['website'])? "https://" + client['business']['website']: client['business']['website']; + Qt.openUrlExternally(client_website) + } + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("E-Mail") + font.bold: true + } + + Label + { + id: clientEmail + color: "goldenrod" + text: client['business']['email']? '' + client['business']['email'] + '': "" + onLinkActivated: Qt.openUrlExternally('mailto:' + client['business']['email']) + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Straße") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['street']? client['business']['tax']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Haus-Nr.") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['house']? client['business']['house']: "" + } + } + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("PLZ") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['zip']? client['business']['zip']: "" + } + } + + ColumnLayout + { + Label + { + color: "darksalmon" + text: qsTr("Stadt") + font.bold: true + } + + Label + { + color: "goldenrod" + text: client['business']['city']? client['business']['city']: "" + } + } + + // Grid row + // Item + // { + // Layout.columnSpan: 2 + // Layout.fillHeight: true + // } +} diff --git a/Gui/CustomersTable.qml b/Gui/CustomersTable.qml index 70895e2..3746ced 100644 --- a/Gui/CustomersTable.qml +++ b/Gui/CustomersTable.qml @@ -145,6 +145,7 @@ Item hoverEnabled: true onDoubleClicked: { + business_model.onRowClicked(row) customersStack.push("CustomerDetails.qml", {selectedClient: row}); } diff --git a/Gui/LoginScreen.qml b/Gui/LoginScreen.qml index ff032b2..9cf01a6 100644 --- a/Gui/LoginScreen.qml +++ b/Gui/LoginScreen.qml @@ -83,6 +83,13 @@ Item placeholderText: qsTr ("Benutzernamen eingeben") implicitWidth: 300 font: hussarPrint.font + focus: true + onAccepted: + { + if (benutzerName.text.trim() && passwort.text.trim()) + loggedin_user.login(benutzerName.text.trim(), passwort.text) + else if(benutzerName.text.trim()) passwort.forceActiveFocus() + } } } @@ -110,6 +117,12 @@ Item implicitWidth: 300 font: hussarPrint.font echoMode: TextInput.Password + onAccepted: + { + if (benutzerName.text.trim() && passwort.text.trim()) + loggedin_user.login(benutzerName.text.trim(), passwort.text) + else if(passwort.text.trim()) benutzerName.forceActiveFocus() + } } } @@ -193,6 +206,7 @@ Item config.invalidEncryptionKey.connect(getEncryptionKey) config.checkEncryptionKey() loggedin_user.noDbConnection.connect(dbConnectionFailed) + benutzerName.forceActiveFocus() } function loggedin() diff --git a/Gui/NoCustomerContact.qml b/Gui/NoCustomerContact.qml new file mode 100644 index 0000000..eb0434c --- /dev/null +++ b/Gui/NoCustomerContact.qml @@ -0,0 +1,33 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +GridLayout +{ + columns: 2 + rowSpacing: 25 + + // Grid row + ColumnLayout + { + Label + { + color: "darksalmon" + font.bold: true + text: qsTr("Kein Ansprechpartner gefunden") + } + + Label + { + color: "goldenrod" + text: qsTr("Was willst du tun?") + } + } + + // Grid row + Item + { + Layout.columnSpan: 2 + Layout.fillHeight: true + } +} diff --git a/Gui/PrinterDialog.qml b/Gui/PrinterDialog.qml new file mode 100644 index 0000000..d0ba6a7 --- /dev/null +++ b/Gui/PrinterDialog.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Window +{ + property alias printerDialog: printDialog + property var printers: null + + id: printDialog + title: qsTr("PYQCRM - Drucker") + color: palette.base + minimumWidth: 300 + maximumWidth: 600 + minimumHeight: 150 + maximumHeight: 150 + ColumnLayout + { + spacing: 9 + y: 15 + implicitWidth: parent.width + RowLayout + { + Layout.fillWidth: true + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Label + { + id: printersLabel + Layout.alignment: Qt.AlignRight + text: qsTr("Drucker") + } + + ComboBox + { + id: allPrinters + model: printers + Layout.fillWidth: true + } + } + + RowLayout + { + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Layout.fillWidth: true + + Label + { + Layout.minimumWidth: printersLabel.width + Layout.alignment: Qt.AlignRight + text: qsTr("Kopie") + } + + SpinBox + { + id: copiesSpinBox + from: 1 + to: 10 + value: 1 + } + + Item + { + Layout.fillWidth: true + } + } + + RowLayout + { + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Layout.fillWidth: true + CheckBox + { + id: colorPrint + text: qsTr("Farbe") + Layout.minimumWidth: printersLabel.width + } + + Item + { + Layout.fillWidth: true + } + } + + RowLayout + { + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Layout.fillWidth: true + Item + { + Layout.fillWidth: true + } + + Button + { + id: printButton + text: qsTr("Drucken") + onClicked: + { + var copies = copiesSpinBox.value > 1? copiesSpinBox.value + " copies": "one copy" + console.log("Printing ", copies, " using ", allPrinters.currentText); + printDialog.close(); + } + } + + Button + { + text: qsTr("Ablehnen") + onClicked: printDialog.close(); + } + } + + Item + { + Layout.fillHeight: true + } + } + + onVisibleChanged: + { + copiesSpinBox.value = 1 + colorPrint.checked = true + } + + Component.onCompleted: + { + printers = sys_printers.getPrinters() + if (sys_printers.getDefaultPrinter()) + allPrinters.currentIndex = allPrinters.indexOfValue(sys_printers.getDefaultPrinter()) + } +} diff --git a/Gui/ReadMe.qml b/Gui/ReadMe.qml new file mode 100644 index 0000000..cd9d1eb --- /dev/null +++ b/Gui/ReadMe.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Window +{ + property alias readMeWin: readMeWin + id: readMeWin + width: 400 + height: 300 + title: "PYQCRM - README" + color: palette.base + + ScrollView + { + anchors.fill: parent + TextArea + { + id: readMe + anchors.fill: parent + readOnly: true + wrapMode: TextArea.Wrap + color: "darksalmon" + + Component.onCompleted: + { + var filePath = "qrc:/README"; + var xhr = new XMLHttpRequest(); + xhr.open("GET", filePath, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) + { + if (xhr.status === 200) + { + readMe.text = xhr.responseText; + } + else + { + readMe.text = qsTr("Datei nicht gefunden!"); + } + } + }; + xhr.send(); + } + } + } +} diff --git a/Gui/TopBar.qml b/Gui/TopBar.qml index 0ec4e88..758dfb0 100644 --- a/Gui/TopBar.qml +++ b/Gui/TopBar.qml @@ -172,7 +172,32 @@ RowLayout icon.color: "red" flat: true Layout.rightMargin: 9 - } + onClicked: mainMenu.open() + Menu { + id: mainMenu + MenuItem + { + text: qsTr("Benutzer-Verwaltung") + onTriggered: appLoader.source = "UsersPage.qml" + } + MenuSeparator {} + MenuItem { text: qsTr("Als PDF exportieren") } + MenuSeparator {} + MenuItem { text: qsTr("Drucken") } + MenuItem + { + text: qsTr("Erweiterter Druck") + onTriggered: printerDialog.show() + } + MenuSeparator {} + MenuItem + { + text: qsTr("Über PYQCRM") + onTriggered: readMeWin.show() + } + } + } } + diff --git a/Gui/UsersPage.qml b/Gui/UsersPage.qml new file mode 100644 index 0000000..31d8c59 --- /dev/null +++ b/Gui/UsersPage.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Item +{ + anchors.fill: parent + + Label + { + text: qsTr("Benutzer-Verwaltung") + anchors.centerIn: parent + font.pixelSize: 57 + font.bold: true + } +} diff --git a/Gui/main.qml b/Gui/main.qml index 1ae8060..bd3f55e 100644 --- a/Gui/main.qml +++ b/Gui/main.qml @@ -26,6 +26,16 @@ ApplicationWindow visible: bad_config || !db_con ? false: true } + PrinterDialog + { + id: printerDialog + } + + ReadMe + { + id: readMeWin + } + Item { id: mainView diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6562a0e --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Nicht zu verwenden ohne Zahlung! diff --git a/README b/README new file mode 100644 index 0000000..fe19178 --- /dev/null +++ b/README @@ -0,0 +1,54 @@ + PYQCRM - Python QtQuick CRM System + +Einleitung: +---------------- + +PYQCRM entstand als Teil einer Abschlusspraxis und dem Bedarf der TERO GmbH an einem Managementprogramm für den internen Gebrauch. + + +Erstellung: +---------------- + +Das Program wurde mit den folgenen Techologien entwickelt: + +- IDE: Qt-Creator 6.8.x +- Framework: QtQuick - QML 6.8.x +- Sprache: Python 3.13.x +- Datenbank: MariaDB 10.11.x +- Sonstiges: JavaScript + + +Installation: +------------------ + +Möglicherweise ist für deine Plattform ein Installationspaket verfügbar. Prüfe es also zuerst. + +Es gibt zwei möglichkeiten das Program zu verwenden: + +Möglichkeit I: + +1. Datenbank einrichten (MaraiDB SQL-Schema verfügbar) +2. Python installiert und eingerichtet +3. Das Program in einen bevorzugten Pfad kopieren. +4. Erstelle einen Link zum main.py-Skript und stelle ihn so ein, dass er mit deinem Python-Interpreter ausgeführt wird + +Möglichkeit II: + +1. Datenbank einrichten (MaraiDB SQL-Schema verfügbar) +2. Die ausführbare Version des Programs in einen bevorzugten Pfad kopieren +3. Erstelle einen Link zur ausführbaren Datei, die für dein System erstellt wurde (pyqcrm/pyqcrm.exe) + + +Kudos: +---------- + +Wenn dir dieses Kunstwerk gefällt, kannst du uns deine Wertschätzung einfach dadurch zeigen, dass du jedem von uns einen Bungalow, einen 760IL BMW, lebenslange Flugtickets inklusive Begleitung kaufst und als Geschenk ein paar Millionen Euro auf ein Schweizer Bankkonto überweist. + + +Team: +--------- + +Marcopolo +Schnacke +Renegade + diff --git a/doc/Tero_CRM_Reinigungsunternehmen.txt b/doc/Tero_CRM_Reinigungsunternehmen.txt index edb6049..b33e0fe 100644 --- a/doc/Tero_CRM_Reinigungsunternehmen.txt +++ b/doc/Tero_CRM_Reinigungsunternehmen.txt @@ -1,29 +1,88 @@ -CRM für Objektbetreuung Reinigungsservice - -Aktueller Stand CRM Sebastian - -Umsetzung mit Python + QML - -- Kunden anlegen -- Abrufen und anzeigen vorhandender Daten aus der Datenbank -- GUI vorhanden - -Minimal Voraussetzung: - -- Kunden anlegen (Hausverwaltung Krefeld) -- Objekt anlegen -- Ansprechpartner anlegen -- Arbeitnehmer anlegen -- Arbeitsauftrag anlegen -- Rechnung schreiben (Zugferd Modus) -- Dokumenten Managment System (DMS) -- Arbeitszeitdokumentation -- Urlaubsplaner - - -Nice To Have: -- WhatsApp -- Telefon -- Social Media Uploader - - +CRM für Objektbetreuung Reinigungsservice + + +Minimal Voraussetzung: + + +Mitarbeiter +- Dokumenten Management System (DMS) +- Arbeitszeitdokumentation +- Urlaubsplaner +- Arbeitnehmer anlegen + +Nice To Have: +- WhatsApp +- Telefon +- Social Media Uploader +Dokumentenvorlagen + + + +Kunden +- Kunden anlegen (Hausverwaltung Krefeld) +- Dokumenten Management System (DMS) +- Ansprechpartner anlegen +RG auch in CC Funktion + +Nice To Have: +Dokumentenvorlagen + + + + +Dashboard + + + +Abrechnung +- Rechnung schreiben (Zugferd Modus) +- Fixkostenerfassung +Rechnungsarchiv +Mahnfunktion + + +Objekt +- Objekt anlegen +- Ansprechpartner anlegen +- Dokumenten Management System (DMS) +Str. (Pflicht) +Hausnummer (Pflicht) +PLZ +Ort (Pflicht) +Parteien (Anzahl) +Stockwerke (Anzahl) +Zwischenetage (ja/nein) +Aufzug (ja/nein) +Fenster (ja/nein) anschließend Anzahl + ohne Leiter erreichbar (ja/nein) +Besonderheiten (Infofeld) +Kontaktdaten Hausmeister / Beirat +Reinigungsmittel zu finden? ( Wo ist es im Kellerraum, Dachgeschoss, 2 Tür von links?) +Foto Upload wäre toll ;-) + +Leistungen +Treppenhausreinigung +Garten +siehe Homepage !!! + + + + +Angebot + + +Auftrag +- Arbeitsauftrag anlegen +Pflegehinweise zum Objekt +BG Sicherheitsdatenblätter +Reinigungsmittel erhalten am ? +Preis ab Datum +Objektkontrolle (Wann wurde das Objekt das letzte Mal kontrolliert?) Ähnlich wie Preis mit Datum +Preise +Schlüssel + + +Auswertung +Stundenkalender + + + diff --git a/lib/ConfigLoader.py b/lib/ConfigLoader.py index f26ed9a..a3bda34 100644 --- a/lib/ConfigLoader.py +++ b/lib/ConfigLoader.py @@ -41,7 +41,7 @@ class ConfigLoader(QObject): else: config_dir.mkdir(0o750, True, True) - @Slot(dict, result= bool) + @Slot(dict, result = bool) def setConfig(self, app_config): # print(f"In {__file__} file, setConfig()") if not self.__config: diff --git a/lib/DB/BusinessDAO.py b/lib/DB/BusinessDAO.py index 62fa609..5dbf439 100644 --- a/lib/DB/BusinessDAO.py +++ b/lib/DB/BusinessDAO.py @@ -27,6 +27,17 @@ class BusinessDAO(QObject): except mariadb.Error as e: print(str(e)) + def getOneBusiness(self, business_id, enc_key = None): + try: + if self.__cur: + self.__cur.callproc("getCustomer", (business_id, enc_key,)) + #self.__all_cols = [desc[0] for desc in self.__cur.description] + return self.__cur.fetchall() #, self.__all_cols + else: + return None #, None + except mariadb.Error as e: + print(str(e)) + def addBusiness(self, data, contact_id): try: if self.__cur: diff --git a/lib/DB/BusinessModel.py b/lib/DB/BusinessModel.py index 120fd70..da793b0 100644 --- a/lib/DB/BusinessModel.py +++ b/lib/DB/BusinessModel.py @@ -65,6 +65,8 @@ class BusinessModel(QAbstractTableModel): __visible_index = {} __col_name = "" __business_dao = None + __business = None + __business_dict = {'business':{}} #,'contact':{}} def __init__(self): super().__init__() @@ -80,6 +82,22 @@ class BusinessModel(QAbstractTableModel): self.__data = rows self.endResetModel() + def __getBusinessInfo(self): + self.__business_dict['business']['id'] = self.__business[0][0] + self.__business_dict['business']['contactid'] = self.__business[0][1] + self.__business_dict['business']['company'] = self.__business[0][2] + self.__business_dict['business']['phone'] = self.__business[0][3] + self.__business_dict['business']['cell'] = self.__business[0][4] + self.__business_dict['business']['email'] = self.__business[0][5] + self.__business_dict['business']['website'] = self.__business[0][6] + self.__business_dict['business']['ceo'] = self.__business[0][7] + self.__business_dict['business']['info'] = self.__business[0][8] + self.__business_dict['business']['tax'] = self.__business[0][9] + self.__business_dict['business']['street'] = self.__business[0][10] + self.__business_dict['business']['house'] = self.__business[0][11] + self.__business_dict['business']['zip'] = self.__business[0][12] + self.__business_dict['business']['city'] = self.__business[0][13] + def rowCount(self, parent= QModelIndex()): return len (self.__data) @@ -113,7 +131,17 @@ class BusinessModel(QAbstractTableModel): @Slot(int) def onRowClicked(self, row): - print(f"Selected table row: {row}, corresponding DB ID: {self.__data[row][0]}") + #print(f"Selected table row: {row}, corresponding DB ID: {self.__data[row][0]}") + if not self.__business_dict['business'] or self.__data[row][0] != self.__business_dict['business']['id']: + self.__business = self.__business_dao.getOneBusiness(self.__data[row][0], self.__key) + #print(self.__business) + self.__getBusinessInfo() + # self.__getContactInfo() + # print(self.__business_dict) + + @Slot(result = dict) + def getClientDetails(self): + return self.__business_dict @Slot(str) def viewCriterion(self, criterion): @@ -134,5 +162,3 @@ class BusinessModel(QAbstractTableModel): def updateTable(self): pass - - diff --git a/lib/DB/ContactDAO.py b/lib/DB/ContactDAO.py index 71ffecf..a5ba4df 100644 --- a/lib/DB/ContactDAO.py +++ b/lib/DB/ContactDAO.py @@ -30,4 +30,15 @@ class ContactDAO: except Exception as e: print("PYT: " + str(e)) + def getContact(self, contact_id, enc_key = None): + try: + if self.__cur: + self.__cur.callproc("getCustomerContact", (contact_id, enc_key,)) + #self.__all_cols = [desc[0] for desc in self.__cur.description] + return self.__cur.fetchall() #, self.__all_cols + else: + return None #, None + except mariadb.Error as e: + print(str(e)) + diff --git a/lib/DB/ContactModel.py b/lib/DB/ContactModel.py index 0cc833c..513b3b3 100644 --- a/lib/DB/ContactModel.py +++ b/lib/DB/ContactModel.py @@ -5,6 +5,10 @@ import logging class ContactModel(QObject): contactIdReady = Signal(int) + + __contact = None + __contact_dict = {'contact':{}} + def __init__(self): super().__init__() # print(f"*** File: {__file__}, __init__()") @@ -27,5 +31,29 @@ class ContactModel(QObject): i = ContactDAO().addContact(contact, self.__key) self.contactIdReady.emit(i) + def __getContact(self, contact): + self.__contact = ContactDAO().getContact(contact, self.__key) + self.__getContactInfo() + + @Slot(int, result = dict) + def getContactDetails(self, contact): + self.__getContact(contact) + #print(self.__contact_dict) + return self.__contact_dict + + def __getContactInfo(self): + self.__contact_dict['contact']['id'] = self.__contact[0][0] + self.__contact_dict['contact']['salute'] = self.__contact[0][1] + self.__contact_dict['contact']['fname'] = self.__contact[0][2].decode("utf-8") + self.__contact_dict['contact']['lname'] = self.__contact[0][3].decode("utf-8") + self.__contact_dict['contact']['phone'] = self.__contact[0][4].decode("utf-8") + self.__contact_dict['contact']['cell'] = self.__contact[0][5].decode("utf-8") + self.__contact_dict['contact']['position'] = self.__contact[0][6] + self.__contact_dict['contact']['email'] = self.__contact[0][7].decode("utf-8") + self.__contact_dict['contact']['birthday'] = self.__contact[0][8].decode("utf-8") + self.__contact_dict['contact']['priority'] = "Ja" if self.__contact[0][9] else "Nein" + self.__contact_dict['contact']['invoice'] = "Ja" if self.__contact[0][10] else "Nein" + self.__contact_dict['contact']['reminder'] = "Ja" if self.__contact[0][11] else "Nein" + diff --git a/lib/DB/UserManager.py b/lib/DB/UserManager.py index 83da450..dd8b5f6 100644 --- a/lib/DB/UserManager.py +++ b/lib/DB/UserManager.py @@ -1,9 +1,13 @@ from .DbManager import DbManager from ..PyqcrmFlags import PyqcrmFlags from ..Vermasseln import Vermasseln -from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput +#from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput : Not working well with Nuitka +import soundfile as sf +import sounddevice as sd from .UserDAO import UserDAO -from PySide6.QtCore import Slot, QObject, Signal, QUrl +from PySide6.QtCore import Slot, QObject, Signal, QUrl, QFile +import tempfile + class UserManager(QObject): @@ -61,12 +65,24 @@ class UserManager(QObject): if user: self.__checkPassword(password, user[2]) else: - player = QMediaPlayer(self) - audioOutput = QAudioOutput(self) - player.setAudioOutput(audioOutput) - player.setSource(QUrl("qrc:/sounds/fail2c.ogg")) - audioOutput.setVolume(150) - player.play() + fail_src = ":/sounds/fail2c.ogg" + with tempfile.NamedTemporaryFile(suffix='.ogg') as ogg_file: + failure_sound = QFile(fail_src) + if not failure_sound.open(QFile.ReadOnly): + print(f"Failed to open resource file: {fail_src}") + else: + ogg_file.write(failure_sound.readAll()) + ogg_path = ogg_file.name + fail, samplerate = sf.read(ogg_path) + sd.play(fail, samplerate) + + ### Not working with Nuitka + # player = QMediaPlayer(self) + # audioOutput = QAudioOutput(self) + # player.setAudioOutput(audioOutput) + # player.setSource(QUrl("qrc:/sounds/fail2c.ogg")) + # audioOutput.setVolume(150) + # player.play() def __checkPassword(self, password, hash_password): pw_list = hash_password.split("$") diff --git a/lib/Printers.py b/lib/Printers.py new file mode 100644 index 0000000..c93c7f4 --- /dev/null +++ b/lib/Printers.py @@ -0,0 +1,23 @@ +from PySide6.QtCore import QObject, Slot +from PySide6.QtPrintSupport import QPrinterInfo + +class Printers(QObject): + __printers = None + __default_printer = None + __default_printer_name = None + __available_printers = [] + + def __init__(self): + super().__init__() + self.__printers = QPrinterInfo.availablePrinters() + self.__available_printers = QPrinterInfo.availablePrinterNames() + self.__default_printer = QPrinterInfo.defaultPrinter() + self.__default_printer_name = QPrinterInfo.defaultPrinterName() + + @Slot(result = list) + def getPrinters(self): + return self.__available_printers + + @Slot(result = str) + def getDefaultPrinter(self): + return self.__default_printer_name diff --git a/lib/PyqcrmPDF.py b/lib/PyqcrmPDF.py new file mode 100644 index 0000000..c9286e6 --- /dev/null +++ b/lib/PyqcrmPDF.py @@ -0,0 +1,81 @@ +from reportlab.lib.pagesizes import A4 #, letter...etc +from reportlab.pdfgen import canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + + +''' + Need to check for sender and receiver sections + Other alternatives can be pdf-gen, pdf-generator, pdf-play, pdf34, abdul-987-pdf +''' + + +class PyqcrmPDF: + __pyq_doc = None + __pyq_pdf = None + __pyq_usable_width = 0 + __pyq_usable_height = 0 + __pyq_lr_margin = 20 + __pyq_tb_margin = 25 + __line = 0 + __line_height = 14 + __doc_title = "PYQCRM PDF Document" + __doc_author = "The PYQCRM Team" + __doc_subject = "PYQCRM Document" + + def __init__(self, doc_title, doc_subject, pdf_file): + self.__pyq_usable_width = A4[0] - self.__pyq_lr_margin - self.__pyq_lr_margin + self.__pyq_usable_height = A4[1] - self.__pyq_tb_margin - self.__pyq_tb_margin + self.__line = self.__pyq_usable_height + self.__pyq_tb_margin + self.__pyq_pdf = canvas.Canvas(pdf_file, pagesize=A4) + self.__pyq_pdf.setAuthor(self.__doc_author) + self.__pyq_pdf.setTitle(doc_title if doc_title else self.__doc_title) + self.__pyq_pdf.setSubject(doc_subject if doc_subject else self.__doc_subject) + self.__pyq_doc = self.__pyq_pdf.beginText(self.__pyq_lr_margin, self.__line) + self.__pyq_doc.setFont("Helvetica", 12) + + def addLine(self, line): + #self.__pyq_pdf.drawString(self.__pyq_lr_margin, self.__line, line) : Need to check if it does the trick! + # print(f"Line No.: {self.__line}") + # print(f"Line: {line}") + if self.__pyq_doc.getY() < self.__pyq_tb_margin: + # print("creating a new page...") + self.__pyq_pdf.drawText(self.__pyq_doc) + self.__pyq_pdf.showPage() + self.__line = self.__pyq_usable_height + self.__pyq_tb_margin + # print(f"Line No.: {self.__line}") + self.__pyq_doc = self.__pyq_pdf.beginText(self.__pyq_lr_margin, self.__line) + self.__pyq_doc.setFont("Helvetica", 12) + + if pdfmetrics.stringWidth(line, "Helvetica", 12) > self.__pyq_usable_width: + # print(f"Line width: {pdfmetrics.stringWidth(line, 'Helvetica', 12)}, Available width: {self.__pyq_usable_width}") + words = line.split(' ') + line = '' + for word in words: + # print(f"Line: {line}") + if self.__pyq_doc.getY() < self.__pyq_tb_margin: + # print("creating a new page...") + self.__pyq_pdf.drawText(self.__pyq_doc) + self.__pyq_pdf.showPage() + self.__line = self.__pyq_usable_height + self.__pyq_tb_margin + # print(f"Line No.: {self.__line}") + self.__pyq_doc = self.__pyq_pdf.beginText(self.__pyq_lr_margin, self.__line) + self.__pyq_doc.setFont("Helvetica", 12) + + if pdfmetrics.stringWidth(line + word + ' ', "Helvetica", 12) <= self.__pyq_usable_width: + line = line + word + ' ' + else: + self.__pyq_doc.textLine(line) + line = word + ' ' + # print(f"Last line: {line}") + + if line: + # print(f"Available line: {line}") + self.__pyq_doc.textLine(line) + + self.__line = self.__line + self.__line_height + + def saveDoc(self): + self.__pyq_pdf.drawText(self.__pyq_doc) + self.__pyq_pdf.showPage() + self.__pyq_pdf.save() diff --git a/main.py b/main.py index f146afa..d4f396f 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ # # !/home/linuxero/proj/tero/pyqcrm/.qtcreator/venv-3.13.1/bin/python3 +import os import sys import logging from PySide6.QtGui import QGuiApplication @@ -13,9 +14,10 @@ from lib.DB.UserManager import UserManager from lib.DB.AddressModel import AddressModel from lib.DB.BTypeModel import BTypeModel from lib.DB.ContactModel import ContactModel +from lib.Printers import Printers - +os.environ['QML_XHR_ALLOW_FILE_READ'] = '1' # [pyqcrm] # program-name="" @@ -35,14 +37,16 @@ address_model = None business_model = None business_type = None contact_model = None +printers = None user = None def initializeProgram(): #print(f"In {__file__} file, initializeProgram()") - global address_model, bad_config, business_model, user, business_type, contact_model, db_con + global address_model, bad_config, business_model, user, business_type, contact_model, db_con, printers if not bad_config: dbconf = config.getConfig()['database'] DbManager(dbconf) + printers = Printers() if DbManager().getConnection(): db_con = True user = UserManager() @@ -55,7 +59,7 @@ def initializeProgram(): def publishContext(): # print(f"In {__file__} file, publishContext()") - global engine, address_model, bad_config, business_model, user, business_type, contact_model + global engine, address_model, bad_config, business_model, user, business_type, contact_model, printers engine.rootContext().setContextProperty("loggedin_user", user) engine.rootContext().setContextProperty("business_model", business_model) engine.rootContext().setContextProperty("address_model", address_model) @@ -76,14 +80,13 @@ if __name__ == "__main__": config = ConfigLoader() - if not config.getConfig(): bad_config = True config.configurationReady.connect(initializeProgram) else: initializeProgram() - + engine.rootContext().setContextProperty("sys_printers", printers) engine.rootContext().setContextProperty("bad_config", bad_config) # print(f"Fehler: {i}") engine.rootContext().setContextProperty("db_con", db_con) engine.rootContext().setContextProperty("config", config) diff --git a/pyqcrm.pyproject b/pyqcrm.pyproject index 1d5ad15..aea1a65 100644 --- a/pyqcrm.pyproject +++ b/pyqcrm.pyproject @@ -20,6 +20,8 @@ "lib/DB/ContactDAO.py", "lib/DB/ContactModel.py", "lib/DB/EmployeeModel.py", - "lib/DB/EmployeeDAO.py" + "lib/DB/EmployeeDAO.py", + "lib/Printers.py", + "lib/PyqcrmPDF.py" ] } diff --git a/pyqcrm.qrc b/pyqcrm.qrc index ce334d8..6fd41f3 100644 --- a/pyqcrm.qrc +++ b/pyqcrm.qrc @@ -18,5 +18,7 @@ fonts/LittleBirdsRegular.ttf fonts/ReginaldScript.ttf images/account.svg + README + LICENSE diff --git a/qml.qrc b/qml.qrc index fd7dfe1..1e6ca28 100644 --- a/qml.qrc +++ b/qml.qrc @@ -35,5 +35,12 @@ Gui/EmployeesTable.qml Gui/EmployeeDetails.qml Gui/ObjectDetails.qml + Gui/AddNewObject.qml + Gui/PrinterDialog.qml + Gui/CustomerContactDetails.qml + Gui/NoCustomerContact.qml + Gui/CustomerDetailsView.qml + Gui/ReadMe.qml + Gui/UsersPage.qml diff --git a/requirements.txt b/requirements.txt index b95e0fa..1a4ee44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,10 @@ pycryptodome psutil toml mariadb +soundfile +sounddevice +reportlab +