diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..9540b3e --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + mariadb + true + org.mariadb.jdbc.Driver + jdbc:mariadb://bearybot.selfhost.co:8080/pyqcrm + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 104102c..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 449e696..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/Gui/Employees/AddApplicant.qml b/Gui/Employees/AddApplicant.qml index 630fa6b..ed967fe 100644 --- a/Gui/Employees/AddApplicant.qml +++ b/Gui/Employees/AddApplicant.qml @@ -6,13 +6,6 @@ ColumnLayout { anchors.fill: parent spacing: Dimensions.l - Component.onCompleted: { - employee_model.addedNewEmployee.connect(successful => { - if (successful) - contentStack.pop(); - }); - } - ApplicantForm { id: applicantForm @@ -36,7 +29,8 @@ ColumnLayout { text: qsTr("Speichern") onClicked: { - employee_model.addApplicant(applicantForm.value); + applicantModel.createApplicant(applicantForm.value); + contentStack.pop(); } } } diff --git a/Gui/Employees/AddEmployee.qml b/Gui/Employees/AddEmployee.qml index f05c3b7..1c2a220 100644 --- a/Gui/Employees/AddEmployee.qml +++ b/Gui/Employees/AddEmployee.qml @@ -46,7 +46,7 @@ ColumnLayout { Layout.fillWidth: true Layout.horizontalStretchFactor: 1 - ApplicantPersonalData { + EmployeePersonalData { id: personalData implicitWidth: parent.width @@ -61,14 +61,14 @@ ColumnLayout { Layout.alignment: Qt.AlignTop implicitWidth: parent.width - ApplicantBankData { + EmployeeBankData { id: bankAccount } - ApplicantNationalInsurance { + EmployeeNationalInsurance { id: nationalInsurance } - ApplicantVarious { + EmployeeVarious { id: applicantVarious } diff --git a/Gui/Employees/ApplicantForm.qml b/Gui/Employees/ApplicantForm.qml index 257dead..66bdd7d 100644 --- a/Gui/Employees/ApplicantForm.qml +++ b/Gui/Employees/ApplicantForm.qml @@ -6,19 +6,31 @@ import TeroStyle ColumnLayout { readonly property int fieldM: 235 readonly property int fieldS: 110 - readonly property bool valid: city.acceptableInput && email.acceptableInput && firstname.acceptableInput && lastname.acceptableInput && mobile.acceptableInput && phone.acceptableInput && postcode.acceptableInput && formofaddress.acceptableInput && title.acceptableInput + readonly property bool valid: emailAddress.acceptableInput && firstName.acceptableInput && houseNumber.acceptableInput && lastName.acceptableInput && mobileNumber.acceptableInput && phoneNumber.acceptableInput && salutation.acceptableInput&& street.acceptableInput && title.acceptableInput && zipCode.acceptableInput readonly property var value: QtObject { - readonly property string city: (city.editText ? city.editText : city.currentText) ?? "" - readonly property string email: email.text - readonly property string firstname: firstname.text - readonly property string formofaddress: formofaddress.currentText ?? "" - readonly property string houseno: houseno.text ?? "" - readonly property string lastname: lastname.text - readonly property string mobile: mobile.text - readonly property string phone: phone.text - readonly property string postcode: (postcode.editText ? postcode.editText : postcode.currentText) ?? "" - readonly property string street: (street.editText ? street.editText : street.currentText) ?? "" + readonly property string emailAddress: emailAddress.text ?? "" + readonly property string firstName: firstName.text ?? "" + readonly property string houseNumber: houseNumber.text ?? "" + readonly property string lastName: lastName.text ?? "" + readonly property string mobileNumber: mobileNumber.text ?? "" + readonly property string phoneNumber: phoneNumber.text ?? "" + readonly property string salutation: salutation.text ?? "" + readonly property string street: street.text ?? "" readonly property string title: title.currentText + readonly property int zipCode: zipCode.currentIndex + } + + function setValue(value) { + title.currentIndex = value.title ?? 0; + firstName.text = value.firstName ?? ""; + lastName.text = value.lastName ?? ""; + street.text = value.street ?? ""; + houseNumber.text = value.houseNumber ?? ""; + zipCode.currentIndex = value.zipCode ?? -1; + phoneNumber.text = value.phoneNumber ?? ""; + mobileNumber.text = value.mobileNumber ?? ""; + emailAddress.text = value.emailAddress ?? ""; + salutation.text = value.salutation ?? 0; } spacing: Dimensions.l @@ -48,13 +60,13 @@ ColumnLayout { onCurrentTextChanged: { switch (title.currentIndex) { case 1: - formofaddress.text = "Sehr geehrter Herr "; + salutation.text = "Sehr geehrter Herr "; break; case 2: - formofaddress.text = "Sehr geehrte Frau "; + salutation.text = "Sehr geehrte Frau "; break; default: - formofaddress.text = "Guten Tag "; + salutation.text = "Guten Tag "; } } } @@ -64,7 +76,7 @@ ColumnLayout { mandatory: true TextField { - id: firstname + id: firstName implicitWidth: fieldM placeholderText: qsTr("Max") @@ -78,7 +90,7 @@ ColumnLayout { mandatory: true TextField { - id: lastname + id: lastName implicitWidth: fieldM placeholderText: qsTr("Mustermann") @@ -92,39 +104,30 @@ ColumnLayout { spacing: Dimensions.m Field { - id: street - label: qsTr("Straße") - mandatory: true TextField { + id: street + implicitWidth: fieldM placeholderText: qsTr("Musterstraße") - - validator: NotEmptyValidator { - } } } Field { - id: houseno - mandatory: true - label: qsTr("Hausnummer") TextField { + id: houseNumber + implicitWidth: fieldS placeholderText: qsTr("1a") - - validator: NotEmptyValidator { - } } } Field { label: qsTr("PLZ") - mandatory: true ComboBox { - id: postcode + id: zipCode currentIndex: -1 editable: true @@ -133,14 +136,11 @@ ColumnLayout { textRole: "display" onActivated: currentValue - onCurrentIndexChanged: city.currentIndex = postcode.currentIndex - - validator: NotEmptyValidator {} + onCurrentIndexChanged: city.currentIndex = zipCode.currentIndex } } Field { label: qsTr("Ort") - mandatory: true ComboBox { id: city @@ -150,9 +150,6 @@ ColumnLayout { implicitWidth: fieldM model: address_model textRole: "city" - - validator: NotEmptyValidator { - } } } } @@ -173,7 +170,7 @@ ColumnLayout { label: qsTr("Telefonnummer") TextField { - id: phone + id: phoneNumber implicitWidth: fieldM placeholderText: "+49 1234 567890" @@ -186,7 +183,7 @@ ColumnLayout { label: qsTr("Mobil") TextField { - id: mobile + id: mobileNumber implicitWidth: fieldM placeholderText: "+49 123 4567891011" @@ -199,7 +196,7 @@ ColumnLayout { label: qsTr("E-Mail Adresse") TextField { - id: email + id: emailAddress implicitWidth: fieldM placeholderText: "tero@example.org" @@ -212,7 +209,7 @@ ColumnLayout { label: qsTr("Briefanrede") TextField { - id: formofaddress + id: salutation implicitWidth: fieldM } diff --git a/Gui/Employees/ApplicantBankData.qml b/Gui/Employees/EmployeeBankData.qml similarity index 100% rename from Gui/Employees/ApplicantBankData.qml rename to Gui/Employees/EmployeeBankData.qml diff --git a/Gui/Employees/EmployeeDetails.qml b/Gui/Employees/EmployeeDetails.qml index 8e340aa..8675011 100644 --- a/Gui/Employees/EmployeeDetails.qml +++ b/Gui/Employees/EmployeeDetails.qml @@ -1,26 +1,34 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import TeroStyle -Item { - property var employee - property int row +ColumnLayout { + property int row: -1 + + anchors.fill: parent + spacing: Dimensions.l onRowChanged: { - employee = employee_model.fetchApplicant(row); + if (row !== -1) { + applicantForm.setValue(applicantModel.applicant(row)) + } } - ColumnLayout { - Label { - text: qsTr("Ausgewählter Mitarbeiter ") + row - } - Label { - text: employee.postcode ?? "" - } - Button { - text: qsTr("Mitarbeiter zeigen") + ApplicantForm { + id: applicantForm - onClicked: contentStack.pop() + Layout.alignment: Qt.AlignTop + Layout.fillHeight: true + Layout.verticalStretchFactor: 1 + } + + RowLayout { + spacing: Dimensions.l + + Button { + text: qsTr("Als Mitarbeiter:in Einstellen") + icon.source: "qrc:/images/InboxArrowDown.svg" } } } diff --git a/Gui/Employees/ApplicantNationalInsurance.qml b/Gui/Employees/EmployeeNationalInsurance.qml similarity index 100% rename from Gui/Employees/ApplicantNationalInsurance.qml rename to Gui/Employees/EmployeeNationalInsurance.qml diff --git a/Gui/Employees/ApplicantPersonalData.qml b/Gui/Employees/EmployeePersonalData.qml similarity index 100% rename from Gui/Employees/ApplicantPersonalData.qml rename to Gui/Employees/EmployeePersonalData.qml diff --git a/Gui/Employees/ApplicantVarious.qml b/Gui/Employees/EmployeeVarious.qml similarity index 100% rename from Gui/Employees/ApplicantVarious.qml rename to Gui/Employees/EmployeeVarious.qml diff --git a/Gui/Employees/EmployeesTable.qml b/Gui/Employees/EmployeesTable.qml index ccdd8ed..dc1236d 100644 --- a/Gui/Employees/EmployeesTable.qml +++ b/Gui/Employees/EmployeesTable.qml @@ -92,7 +92,7 @@ ColumnLayout { Layout.fillHeight: true Layout.fillWidth: true columnSpacing: 2 - model: employee_model + model: applicantModel resizableColumns: true rowSpacing: 2 selectionBehavior: TableView.SelectRows diff --git a/images/InboxArrowDown.svg b/images/InboxArrowDown.svg new file mode 100644 index 0000000..e62eafc --- /dev/null +++ b/images/InboxArrowDown.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/lib/ConfigLoader.py b/lib/ConfigLoader.py index 3413470..8809bc1 100644 --- a/lib/ConfigLoader.py +++ b/lib/ConfigLoader.py @@ -1,19 +1,18 @@ # This Python file uses the following encoding: utf-8 -import toml -from platformdirs import user_config_dir -from pathlib import Path -from PySide6.QtCore import QObject, Slot, Signal -from .Vermasseln import Vermasseln -import shutil -from urllib.parse import urlparse -from .DB.DbManager import DbManager import os -from Crypto.Random import get_random_bytes from base64 import b64encode +from pathlib import Path +from urllib.parse import urlparse + +import toml +from Crypto.Random import get_random_bytes +from PySide6.QtCore import QObject, Slot, Signal +from platformdirs import user_config_dir + from .DB.UserManager import UserManager from .PyqcrmFlags import PyqcrmFlags - - +from .Vermasseln import Vermasseln +from .domain.BaseModel import database class ConfigLoader(QObject): @@ -21,7 +20,6 @@ class ConfigLoader(QObject): __version = "0.1-alpha" __check_enc_key = True - dbConnectionError = Signal(str, bool) adminUserError = Signal(str, bool) adminNotAsvailable = Signal() @@ -31,7 +29,6 @@ class ConfigLoader(QObject): def __init__(self): super().__init__() - # print(f"In {__file__} file, __init__()") self.config_dir = user_config_dir() + '/pyqcrm' config_dir = Path(self.config_dir) if config_dir.exists(): @@ -41,15 +38,13 @@ 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: base_conf = self.__initializeConfig() conf = self.__checkDbConnection(app_config) app_config = toml.dumps(app_config) - if conf: + if conf: app_config = base_conf + app_config self.__config = toml.loads(app_config) self.__saveConfig() @@ -65,45 +60,41 @@ class ConfigLoader(QObject): conf = conf + f"ENCRYPTION_KEY = \"{self.__encrypt_key}\"\n\n" return conf - def __checkDbConnection(self, db_config): - # print(f"In {__file__} file, __checkDbConnection()") - con = DbManager(db_config['database']).getConnection() - if con: + def __checkDbConnection(self): + if database.is_closed(): self.dbConnectionError.emit("Connection OK", True) return True else: self.dbConnectionError.emit("Connection fehlgeschlagen", False) return False - def __saveConfig(self): # print(f"In {__file__} file, saveConfig()") try: - with open (self.config_dir + '/pyqcrm.toml', 'w') as f: + with open(self.config_dir + '/pyqcrm.toml', 'w') as f: # print(self.__config) config = Vermasseln().oscarVermasseln(toml.dumps(self.__config)) f.write(config) except FileNotFoundError: print("Konnte die Konfiguration nicht speichern.") - def __checkAdminUser(self): # print(f"In {__file__} file, __checkAdminUser()") result = UserManager().checkAdmin() if not result: - #if not result[0][0] == 1: + # if not result[0][0] == 1: self.adminUserError.emit("Kein Admin vorhanden", False) return False else: self.adminUserError.emit("Admin vorhanden", True) return True - @Slot(dict, result= bool) + @Slot(dict, result=bool) def addAdminUser(self, user_config): # print(f"In {__file__} file, addAdminUser()") admin = UserManager(user_config["user"], PyqcrmFlags.ADMIN).createUser() if not admin: - #self.adminNotAvailable.emit() + # self.adminNotAvailable.emit() self.adminUserError.emit("Benutzername nich verfügbar", False) else: self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] = 'Yes' @@ -111,7 +102,6 @@ class ConfigLoader(QObject): self.backupEncryptionKey.emit() return admin - @Slot(str, str) def __saveData(self, recovery_file, recovery_password, data): # print(f"In {__file__} file, __saveData()") @@ -121,7 +111,7 @@ class ConfigLoader(QObject): rf = Vermasseln().oscarVermasseln(rf, local) rec_file = urlparse(recovery_file) if os.name == "nt": - rec_file = rec_file [1:] + rec_file = rec_file[1:] else: rec_file = rec_file.path + ".pyqrec" try: @@ -136,7 +126,7 @@ class ConfigLoader(QObject): rec_file = urlparse(recovery_file) rec_file = rec_file.path if os.name == "nt": - rec_file = rec_file [1:] + rec_file = rec_file[1:] try: ek = self.__parseImport(rec_file, recovery_password) @@ -165,7 +155,6 @@ class ConfigLoader(QObject): else: return None - def __invalidateEncryptionKey(self): # print(f"In {__file__} file, __invalidateEncryptionKey()") self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] = 'No' @@ -182,7 +171,7 @@ class ConfigLoader(QObject): rp = self.__setRecoveryPassword(recovery_password, salt) return rp[1] == password - @Slot(str, str) # todo: non local encryption + @Slot(str, str) # todo: non local encryption def importConfig(self, confile, password): confile = urlparse(confile) confile = confile.path @@ -200,15 +189,10 @@ class ConfigLoader(QObject): except Exception as e: print(str(e)) - - - - - def __configLoad(self): # print(f"In {__file__} file, __configLoad()") try: - with open (self.config_dir + '/pyqcrm.toml', 'r') as f: + with open(self.config_dir + '/pyqcrm.toml', 'r') as f: config = f.read() self.__config = toml.loads(Vermasseln().entschluesseln(config)) self.configurationReady.emit() @@ -219,13 +203,12 @@ class ConfigLoader(QObject): except Exception as e: print(str(e)) - def getConfig(self): # print(f"In {__file__} file, getConfig()") # print(self.__config) return self.__config - def __setRecoveryPassword(self, key, salt = None): + def __setRecoveryPassword(self, key, salt=None): # print(f"In {__file__} file, __setRecoveryPassword()") key = Vermasseln.userPasswordHash(key, salt) return key.split("$") @@ -242,13 +225,12 @@ class ConfigLoader(QObject): conf_file = toml.dumps(self.getConfig()) self.__saveData(filename, password, conf_file) - @Slot(dict) - def saveDbConf(self, db = None): + def saveDbConf(self, db=None): self.__config.update(db) self.__saveConfig() - @Slot(result = dict) + @Slot(result=dict) def getDbConf(self): try: return self.__config['database'] @@ -257,11 +239,11 @@ class ConfigLoader(QObject): return None @Slot(dict) - def saveCompanyInfo(self, company = None): + def saveCompanyInfo(self, company=None): self.__config.update(company) self.__saveConfig() - @Slot(result = dict) + @Slot(result=dict) def getCompanyInfo(self): try: return self.__config['company'] @@ -270,11 +252,11 @@ class ConfigLoader(QObject): return None @Slot(dict) - def saveMiscConf(self, misc_conf = None): + def saveMiscConf(self, misc_conf=None): self.__config.update(misc_conf) self.__saveConfig() - @Slot(result = bool) + @Slot(result=bool) def systray(self): try: return self.__config['misc']['SYSTRAY'] @@ -282,9 +264,7 @@ class ConfigLoader(QObject): print(f"Missing configuration: {ex}") return False - @Slot(str, str) def backupEncryptkey(self, filename, password): encrypt_key = self.__config['pyqcrm']['ENCRYPTION_KEY'] self.__saveData(filename, password, encrypt_key) - diff --git a/lib/DB/AddressDAO.py b/lib/DB/AddressDAO.py index b2150bd..4df355b 100644 --- a/lib/DB/AddressDAO.py +++ b/lib/DB/AddressDAO.py @@ -1,27 +1,26 @@ -from .DbManager import DbManager -import mariadb import json +import mariadb + +from lib.domain.BaseModel import database + class AddressDAO: __cur = None + def __init__(self): - #print(f"*** File: {__file__}, init()") - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() - - - def __importPlz(self): with open("pfad zur datei", "r") as plz: postcodes = json.load(plz) irgendwas = "" try: for i in postcodes: - test =i["plz_name"].split(",") + test = i["plz_name"].split(",") for town in test: if "u.a" in town: town = town[:-4] @@ -29,12 +28,12 @@ class AddressDAO: if town: print(f"PROCESSING {i['name']} {town}") self.__cur.callproc("addZipCodes", (i["name"], town, irgendwas,)) - #self.__cur.callproc("addZipCodes", ("56271", "Kleinmaischeid", irgendwas,)) + # self.__cur.callproc("addZipCodes", ("56271", "Kleinmaischeid", irgendwas,)) except mariadb.OperationalError as e: print(f"Database Error: {e}") finally: self.__con.commit() - print("FINISHED")# + print("FINISHED") # def __importCountry(self): with open("pfad zur datei", "r") as country: @@ -55,18 +54,17 @@ class AddressDAO: print(i[4], i[3], i[2], i[8], i[7]) - self.__cur.execute("INSERT INTO country (country, countryshort, nationality, iso2, iso3) VALUES (%s, %s, %s, %s, %s)", (i[4], i[3], i[2], i[8], i[7])) + self.__cur.execute( + "INSERT INTO country (country, countryshort, nationality, iso2, iso3) VALUES (%s, %s, %s, %s, %s)", + (i[4], i[3], i[2], i[8], i[7])) old = i[4] except mariadb.OperationalError as e: print(f"Database Error: {e}") finally: self.__con.commit() - print("FINISHED")# + print("FINISHED") # - - - - def getAddressData(self, all = True, zipcode = None): + def getAddressData(self, all=True, zipcode=None): try: if self.__cur: self.__cur.callproc("getAddress", (all, zipcode,)) @@ -76,5 +74,3 @@ class AddressDAO: return None except mariadb.Error as e: print(str(e)) - - diff --git a/lib/DB/AddressModel.py b/lib/DB/AddressModel.py index aecd12f..357e8bd 100644 --- a/lib/DB/AddressModel.py +++ b/lib/DB/AddressModel.py @@ -2,18 +2,19 @@ from PySide6.QtCore import QAbstractListModel, Qt, Slot, QModelIndex from .AddressDAO import AddressDAO from ..PyqcrmDataRoles import PyqcrmDataRoles + class AddressModel(QAbstractListModel): def __init__(self): super().__init__() self.__address_data = AddressDAO().getAddressData() - def rowCount(self, parent = QModelIndex()): + def rowCount(self, parent=QModelIndex()): return len(self.__address_data) - def data(self, index, role = Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: data = self.__address_data[row][2] return data elif role == PyqcrmDataRoles.CITY_ROLE: @@ -23,7 +24,7 @@ class AddressModel(QAbstractListModel): def roleNames(self): return { - Qt.DisplayRole: b"display", + Qt.ItemDataRole.DisplayRole: b"display", PyqcrmDataRoles.CITY_ROLE: b"city", } @@ -34,9 +35,3 @@ class AddressModel(QAbstractListModel): def getAddresses(self, all, zipcode): data = AddressDAO().getAddressData(all, zipcode) return data - - - - - - diff --git a/lib/DB/ApplicantModel.py b/lib/DB/ApplicantModel.py new file mode 100644 index 0000000..48745a5 --- /dev/null +++ b/lib/DB/ApplicantModel.py @@ -0,0 +1,76 @@ +import uuid +from typing import List, Callable, Any + +from PySide6.QtCore import QModelIndex, Qt, QAbstractTableModel, Slot +from PySide6.QtQml import QJSValue +from peewee import Select + +from lib.domain.Applicant import Applicant + +COLUMNS: list[Callable[[Applicant], Any]] = [ + lambda applicant: applicant.first_name, + lambda applicant: applicant.last_name, + lambda applicant: applicant.zip_code.zip_code or None, + lambda applicant: applicant.zip_code.town.town if applicant.zip_code.id is not None else None +] + +COLUMN_NAMES = ["Vorname", "Nachname", "PLZ", "Ort"] + + +class ApplicantModel(QAbstractTableModel): + _applicants: Select + + def __init__(self) -> None: + super().__init__() + self._applicants = Applicant.select_table_data() + + def rowCount(self, /, parent=...): + return len(self._applicants) + + def columnCount(self, /, parent=...): + return len(COLUMNS) + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): + if role == Qt.ItemDataRole.DisplayRole: + applicant = self._applicants[index.row()] + return COLUMNS[index.column()](applicant) + return None + + @Slot(int, result=dict) + def applicant(self, row) -> dict: + applicant = Applicant.get_by_id(self._applicants[row].id) + return { + 'title': applicant.title, + "firstName": applicant.first_name, + "lastName": applicant.last_name, + "street": applicant.street, + "houseNumber": applicant.house_number, + "zipCode": applicant.zip_code_id, + "phoneNumber": applicant.phone_number, + "mobileNumber": applicant.mobile_number, + "emailAddress": applicant.email_address, + "salutation": applicant.salutation + } + + @Slot(QJSValue) + def createApplicant(self, values: QJSValue): + applicant = Applicant() + applicant.id = uuid.uuid4() + applicant.title = values.property("title").toInt() + applicant.first_name = values.property("firstName").toString() + applicant.last_name = values.property("lastName").toString() + applicant.street = values.property("street").toString() or None + applicant.house_number = values.property("houseNumber").toString() or None + if values.property("zipCode").toInt() != -1: + applicant.zip_code = values.property("zipCode").toInt() + applicant.phone_number = values.property("phoneNumber").toString() or None + applicant.mobile_number = values.property("mobileNumber").toString() or None + applicant.email_address = values.property("emailAddress").toString() or None + applicant.salutation = values.property("salutation").toString() or None + applicant.save(force_insert=True) + self._applicants = Applicant.select_table_data() + + def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole): + if role == Qt.ItemDataRole.DisplayRole: + return COLUMN_NAMES[section] + return None diff --git a/lib/DB/BTypeDAO.py b/lib/DB/BTypeDAO.py index 6e82db5..d43039b 100644 --- a/lib/DB/BTypeDAO.py +++ b/lib/DB/BTypeDAO.py @@ -1,20 +1,20 @@ -from .DbManager import DbManager +from lib.domain.BaseModel import database class BTypeDAO: __cur = None + def __init__(self): - #print(f"*** File: {__file__}, init()") - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() def getBType(self): try: if self.__cur: - self.__cur.callproc("getBtype", (None, None, )) + self.__cur.callproc("getBtype", (None, None,)) data = self.__cur.fetchall() - return(data) + return data else: return None except mariadb.Error as e: diff --git a/lib/DB/BusinessDAO.py b/lib/DB/BusinessDAO.py index d9009c1..557fa24 100644 --- a/lib/DB/BusinessDAO.py +++ b/lib/DB/BusinessDAO.py @@ -1,7 +1,7 @@ -from .DbManager import DbManager import json import mariadb from PySide6.QtCore import QObject, Signal +from lib.domain.BaseModel import database class BusinessDAO(QObject): @@ -12,11 +12,11 @@ class BusinessDAO(QObject): def __init__(self): super().__init__() - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() - def getBusiness(self, enc_key, criterion = "Alle"): + def getBusiness(self, enc_key, criterion="Alle"): try: if self.__cur: self.__cur.callproc("getCustomerView", (enc_key, criterion,)) @@ -27,14 +27,14 @@ class BusinessDAO(QObject): except mariadb.Error as e: print(str(e)) - def getOneBusiness(self, business_id, enc_key = None): + 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 + # self.__all_cols = [desc[0] for desc in self.__cur.description] + return self.__cur.fetchall() # , self.__all_cols else: - return None #, None + return None # , None except mariadb.Error as e: print(str(e)) @@ -47,10 +47,3 @@ class BusinessDAO(QObject): except mariadb.Error as e: print(str(e)) - - - - - - - diff --git a/lib/DB/ContactDAO.py b/lib/DB/ContactDAO.py index 47ce47c..9acc907 100644 --- a/lib/DB/ContactDAO.py +++ b/lib/DB/ContactDAO.py @@ -1,7 +1,9 @@ -from .DbManager import DbManager -from PySide6.QtCore import QObject, Signal import json + import mariadb +from PySide6.QtCore import QObject, Signal + +from lib.domain.BaseModel import database class ContactDAO(QObject): @@ -9,8 +11,7 @@ class ContactDAO(QObject): def __init__(self): super().__init__() - #print(f"*** File: {__file__}, __init__()") - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() diff --git a/lib/DB/DbManager.py b/lib/DB/DbManager.py deleted file mode 100644 index c46c6c5..0000000 --- a/lib/DB/DbManager.py +++ /dev/null @@ -1,46 +0,0 @@ -import mariadb - -class DbManager(): - __connection = None - __con_param = None - __dbmanager = None - - def __new__(cls, dbconf = None): - - if cls.__dbmanager is None: - cls.__dbmanager = super(DbManager, cls).__new__(cls) - cls.__dbmanager.__initializeConfig(dbconf) - - return cls.__dbmanager - - def getConnection(cls): - #print(f"DB Manager: {cls.__dbmanager}") - #print(f"DB Connection: {cls.__connection}") - try: - if not cls.__connection or not cls.__connection.ping(): - cls.__failure_notified = False - cls.__connection = mariadb.connect(**cls.__con_param) - except mariadb.InterfaceError as e: - cls.__connection = mariadb.connect(**cls.__con_param) - print(f"DbManager Connection (INTERFACE ERROR): {e}..reconnecting...") - except mariadb.Error as e: - if '(110)' in str(e): - print(f"File: {__file__}\n Database connection timed out (Check connection parameters or server running): {e}") - elif '(138)' in str(e): - print(f"File: {__file__}\n Database connection timed out (Check connection parameters or server running - initial handshake): {e}") - else: - print(f"File: {__file__}\n Database connection error: {e}") - cls.__connection = None - - return cls.__connection - - def __initializeConfig(cls, dbconf): - cls.__con_param = { 'user': dbconf['DB_USER'], 'password': dbconf['DB_PASS'], - 'port': int (dbconf['DB_PORT']), 'host': dbconf['DB_HOST'], - 'database': dbconf['DB_NAME'], 'connect_timeout': 5, 'autocommit': True, - } - - - - - diff --git a/lib/DB/EmployeeDAO.py b/lib/DB/EmployeeDAO.py index b7955a7..d689bd4 100644 --- a/lib/DB/EmployeeDAO.py +++ b/lib/DB/EmployeeDAO.py @@ -1,35 +1,41 @@ -from .DbManager import DbManager import json -import mariadb + from PySide6.QtCore import QObject, Signal +from lib.domain.BaseModel import database + class EmployeeDAO(QObject): newEmployeeAdded = Signal(bool) - __all_cols = None def __init__(self): super().__init__() - self.__con = DbManager().getConnection() + self._connection = database.connection() def getEmployees(self, enc_key, criterion="Alle", processed=False, fired=False, every_state=True): - cursor = self.__con.cursor() - cursor.callproc("getEmployeeTable", (criterion, processed, fired, every_state, enc_key,)) - self.__all_cols = [desc[0] for desc in cursor.description] - result = cursor.fetchall(), self.__all_cols - cursor.close() - return result + cursor = self._connection.cursor() + try: + cursor.callproc("getEmployeeTable", (criterion, processed, fired, every_state, enc_key,)) + all_cols = [desc[0] for desc in cursor.description] + result = cursor.fetchall(), all_cols + return result + finally: + cursor.close() def fetchApplicant(self, employee_id, enc_key=None) -> dict: - cursor = self.__con.cursor(dictionary=True) - cursor.callproc("getApplicant", (employee_id, enc_key)) - it = cursor.fetchone() - cursor.close() - return it + cursor = self._connection.cursor(dictionary=True) + try: + cursor.callproc("getApplicant", (employee_id, enc_key)) + it = cursor.fetchone() + return it + finally: + cursor.close() - def addEmployee(self, data, enc_key, applicant=True): - cursor = self.__con.cursor() - cursor.callproc("addApplicant", (json.dumps(data), applicant, enc_key,)) - self.__con.commit() - cursor.close() - self.newEmployeeAdded.emit(True) + def addApplicant(self, data, enc_key, applicant=True): + cursor = self._connection.cursor() + try: + cursor.callproc("addApplicant", (json.dumps(data), applicant, enc_key,)) + self._connection.commit() + self.newEmployeeAdded.emit(True) + finally: + cursor.close() diff --git a/lib/DB/EmployeeModel.py b/lib/DB/EmployeeModel.py index 00e85af..ae60437 100644 --- a/lib/DB/EmployeeModel.py +++ b/lib/DB/EmployeeModel.py @@ -10,7 +10,6 @@ class EmployeeModel(QAbstractTableModel): addedNewEmployee = Signal(bool) __data = None __employee_dao = None - __visible_index = None __visible_columns = None __col_name = "" __col_skip = 2 @@ -18,7 +17,7 @@ class EmployeeModel(QAbstractTableModel): def __init__(self): super().__init__() - self.__employee_dao = EmployeeDAO() + self.__employee_dao = EmployeeDAO() self.__employee_dao.newEmployeeAdded.connect(self.__refreshView) self.__conf = ConfigLoader().getConfig() self.__key = self.__conf['pyqcrm']['ENCRYPTION_KEY'] @@ -29,11 +28,11 @@ class EmployeeModel(QAbstractTableModel): if 'worklicense' in new_employee: new_employee['worklicense'] = int(new_employee['worklicense']) new_employee['residencetype'] = int(new_employee['residencetype']) - self.__employee_dao.addEmployee(new_employee, self.__key, False) + self.__employee_dao.addApplicant(new_employee, self.__key, False) @Slot(QJSValue) def addApplicant(self, applicant: QJSValue): - self.__employee_dao.addEmployee({ + self.__employee_dao.addApplicant({ "city": applicant.property("city").toString(), "email": applicant.property("email").toString(), "firstname": applicant.property("firstname").toString(), @@ -72,29 +71,21 @@ class EmployeeModel(QAbstractTableModel): self.__col_skip = 2 self.__getData(criterion, criterion == 'Erledigt', False, criterion == 'Alle') - def data(self, index, role=Qt.DisplayRole): - if role == Qt.DisplayRole: + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if role == Qt.ItemDataRole.DisplayRole: row = self.__data[index.row()] applicant_col = index.column() + self.__col_skip - tr = row[ - applicant_col] # if type(row[index.column() + 2]) is str else str(row[index.column() + 2], "utf-8") + tr = row[applicant_col] if applicant_col == 2 and self.__everyone: tr = 'Ja' if tr == 1 else 'Nein' else: if tr: tr = re.sub("Keine Angabe ", "", tr) - # print(f"Data: {tr}") - # return row[index.column() + 2] return tr return None - def headerData(self, section, orientation, role=Qt.DisplayRole): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: self.__col_name = self.__visible_columns[section + self.__col_skip] return self.__col_name return super().headerData(section, orientation, role) - - @Slot(int, result=dict) - def fetchApplicant(self, row) -> dict: - employee_id = self.__data[row][0] - return self.__employee_dao.fetchApplicant(employee_id, self.__key) diff --git a/lib/DB/ObjectDAO.py b/lib/DB/ObjectDAO.py index dea84c1..114c24b 100644 --- a/lib/DB/ObjectDAO.py +++ b/lib/DB/ObjectDAO.py @@ -1,17 +1,14 @@ -from .DbManager import DbManager import json import mariadb from PySide6.QtCore import QObject, Signal -# from ..PyqcrmFlags import PyqcrmAppliEmpyFlags - +from lib.domain.BaseModel import database class ObjectDAO(QObject): newObjectAdded = Signal(bool, int) def __init__(self): super().__init__() - #print(f"*** File: {__file__}, __init__()") - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() diff --git a/lib/DB/UserDAO.py b/lib/DB/UserDAO.py index aedb459..6058338 100644 --- a/lib/DB/UserDAO.py +++ b/lib/DB/UserDAO.py @@ -1,20 +1,21 @@ # This Python file uses the following encoding: utf-8 -from .DbManager import DbManager from ..PyqcrmFlags import PyqcrmFlags import mariadb from PySide6.QtCore import QObject, Signal +from lib.domain.BaseModel import database + class UserDAO(QObject): noDbConnection = Signal(str) __cursor = None + def __init__(self): - #print(f"*** File: {__file__}, init()") super().__init__() - self.__con = DbManager().getConnection() + self.__con = database.connection() if self.__con: self.__cur = self.__con.cursor() - def createUser(self, username, password, info, role= PyqcrmFlags.USER): + def createUser(self, username, password, info, role=PyqcrmFlags.USER): user_created = False try: if self.__cur: @@ -39,6 +40,3 @@ class UserDAO(QObject): except mariadb.Error as e: print(str(e)) self.noDbConnection.emit(str(e)) - - - diff --git a/lib/DB/UserManager.py b/lib/DB/UserManager.py index dd8b5f6..37f1f6d 100644 --- a/lib/DB/UserManager.py +++ b/lib/DB/UserManager.py @@ -1,35 +1,28 @@ -from .DbManager import DbManager +from PySide6.QtCore import Slot, QObject, Signal + +from lib.domain.BaseModel import database +from .UserDAO import UserDAO from ..PyqcrmFlags import PyqcrmFlags from ..Vermasseln import Vermasseln -#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, QFile -import tempfile - class UserManager(QObject): - loginOkay = Signal() noDbConnection = Signal(str) - def __init__(self, user_config = None, role = None): + def __init__(self, user_config=None, role=None): super().__init__() - self.__con = DbManager().getConnection() + self.__con = database.connection() self.__user_dao = UserDAO() self.__user_dao.noDbConnection.connect(self.noDbConnection) if self.__con: self.__cur = self.__con.cursor() if user_config and role: - self.__username = user_config["PYQCRM_USER"] self.__password = user_config["PYQCRM_USER_PASS"] self.__info = user_config["PYQCRM_USER_INFO"] self.__role = role if role == PyqcrmFlags.ADMIN else 0 - def createUser(self): self.__hashPassword() user_created = self.__user_dao.createUser(self.__username, self.__password, self.__info, self.__role) @@ -64,25 +57,6 @@ class UserManager(QObject): user = self.__user_dao.getUser(username) if user: self.__checkPassword(password, user[2]) - else: - 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("$") @@ -90,6 +64,3 @@ class UserManager(QObject): hash_pw = Vermasseln.userPasswordHash(password, pw_list[0]) if hash_password == hash_pw: self.loginOkay.emit() - - - diff --git a/lib/domain/Applicant.py b/lib/domain/Applicant.py new file mode 100644 index 0000000..0fa4425 --- /dev/null +++ b/lib/domain/Applicant.py @@ -0,0 +1,31 @@ +from peewee import CharField, UUIDField, SmallIntegerField, TextField, ForeignKeyField, JOIN + +from lib.domain.Town import Town +from lib.domain.BaseModel import BaseModel +from lib.domain.ZipCode import ZipCode + + +class Applicant(BaseModel): + class Meta: + table_name = "applicants" + + id = UUIDField(primary_key=True) + title = SmallIntegerField(default=0) + first_name = CharField(null=False) + last_name = CharField(null=False) + street = CharField() + house_number = CharField() + zip_code = ForeignKeyField(ZipCode, column_name="zip_code", null=True) + phone_number = CharField() + mobile_number = CharField() + email_address = CharField() + salutation = TextField(null=False) + + @classmethod + def select_table_data(cls): + return ( + Applicant + .select(Applicant.id, Applicant.first_name, Applicant.last_name, ZipCode.id, ZipCode.zip_code, Town.town) + .join(ZipCode, join_type="LEFT JOIN") + .join(Town, join_type="LEFT JOIN") + ) diff --git a/lib/domain/BaseModel.py b/lib/domain/BaseModel.py new file mode 100644 index 0000000..56bac14 --- /dev/null +++ b/lib/domain/BaseModel.py @@ -0,0 +1,8 @@ +from peewee import Model, MySQLDatabase + +database = MySQLDatabase(None) + + +class BaseModel(Model): + class Meta: + database = database diff --git a/lib/domain/Country.py b/lib/domain/Country.py new file mode 100644 index 0000000..79a068a --- /dev/null +++ b/lib/domain/Country.py @@ -0,0 +1,14 @@ +from peewee import AutoField, CharField + +from lib.domain.BaseModel import BaseModel + + +class Country(BaseModel): + class Meta: + table_name = "country" + id = AutoField(column_name="countryid") + name = CharField(max_length=200, unique=True) + name_short = CharField(max_length=100, column_name="countryshort") + nationality = CharField(max_length=100) + iso2 = CharField(max_length=2, unique=True) + iso3 = CharField(max_length=3, unique=True) diff --git a/lib/domain/Town.py b/lib/domain/Town.py new file mode 100644 index 0000000..44bdda9 --- /dev/null +++ b/lib/domain/Town.py @@ -0,0 +1,12 @@ +from peewee import AutoField, CharField, ForeignKeyField + +from lib.domain.BaseModel import BaseModel +from lib.domain.Country import Country + + +class Town(BaseModel): + class Meta: + table_name = "address" + id = AutoField(column_name="addressid") + town = CharField(max_length=50, column_name="city") + country = ForeignKeyField(Country, column_name="countryid", backref="towns") diff --git a/lib/domain/ZipCode.py b/lib/domain/ZipCode.py new file mode 100644 index 0000000..0a0e26c --- /dev/null +++ b/lib/domain/ZipCode.py @@ -0,0 +1,13 @@ +from peewee import AutoField, CharField, ForeignKeyField + +from lib.domain.BaseModel import BaseModel +from lib.domain.Town import Town + + +class ZipCode(BaseModel): + class Meta: + table_name = "postcode" + + id = AutoField(column_name="postcodeid") + zip_code = CharField(max_length=15, column_name="postcode") + town = ForeignKeyField(Town, backref="zip_codes", column_name="addressid") diff --git a/main.py b/main.py index 7fa4103..c16c89f 100644 --- a/main.py +++ b/main.py @@ -1,44 +1,35 @@ # # !/home/linuxero/proj/tero/pyqcrm/.qtcreator/venv-3.13.1/bin/python3 import os import sys -import logging -from PySide6.QtNetwork import QLocalServer, QLocalSocket -from PySide6.QtWidgets import QSystemTrayIcon -from PySide6.QtGui import QGuiApplication, QIcon -from PySide6.QtQml import QQmlApplicationEngine -from PySide6.QtCore import QIODevice -from lib.ConfigLoader import ConfigLoader -from lib.DB.BusinessModel import BusinessModel +# noinspection PyUnresolvedReferences import rc_pyqcrm +# noinspection PyUnresolvedReferences import rc_qml -from lib.DB.DbManager import DbManager -from lib.DB.UserManager import UserManager + +from PySide6.QtCore import QIODevice +from PySide6.QtGui import QGuiApplication, QIcon +from PySide6.QtNetwork import QLocalServer, QLocalSocket +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtWidgets import QSystemTrayIcon + +from lib.ConfigLoader import ConfigLoader from lib.DB.AddressModel import AddressModel +from lib.DB.ApplicantModel import ApplicantModel from lib.DB.BTypeModel import BTypeModel +from lib.DB.BusinessModel import BusinessModel from lib.DB.ContactModel import ContactModel from lib.DB.EmployeeModel import EmployeeModel from lib.DB.ObjectModel import ObjectModel +from lib.DB.UserManager import UserManager from lib.Printers import Printers +from lib.domain.BaseModel import database os.environ['QML_XHR_ALLOW_FILE_READ'] = '1' - -# [pyqcrm] -# program-name="" -# version= - -# [database] -# server="" -# port= -# user="" -# password="" -# name="" -# type="" - - bad_config = False db_con = False address_model = None +applicant_model = None business_model = None business_type = None contact_model = None @@ -49,17 +40,26 @@ user = None def initializeProgram(): - print(f"In {__file__} file, initializeProgram()") - global address_model, bad_config, business_model, user, business_type, contact_model, employee_model, object_model, db_con, printers + global address_model, applicant_model, bad_config, business_model, user, business_type, contact_model, employee_model, object_model, db_con, printers if not bad_config: dbconf = config.getConfig()['database'] - DbManager(dbconf) + database.init( + host=dbconf['DB_HOST'], + user=dbconf['DB_USER'], + password=dbconf['DB_PASS'], + database=dbconf['DB_NAME'], + port=int(dbconf['DB_PORT']), + connect_timeout=5, + ) + database.connect() + printers = Printers() - if DbManager().getConnection(): + if not database.is_closed(): db_con = True user = UserManager() business_model = BusinessModel() address_model = AddressModel() + applicant_model = ApplicantModel() business_type = BTypeModel() contact_model = ContactModel() employee_model = EmployeeModel() @@ -74,11 +74,11 @@ def configReady(): def publishContext(): - # print(f"In {__file__} file, publishContext()") - global engine, address_model, bad_config, business_model, user, business_type, contact_model, object_model, employee_model, printers + global engine, address_model, applicant_model, bad_config, business_model, user, business_type, contact_model, object_model, employee_model, printers engine.rootContext().setContextProperty("loggedin_user", user) engine.rootContext().setContextProperty("business_model", business_model) engine.rootContext().setContextProperty("address_model", address_model) + engine.rootContext().setContextProperty("applicantModel", applicant_model) engine.rootContext().setContextProperty("business_type", business_type) engine.rootContext().setContextProperty("contact_model", contact_model) engine.rootContext().setContextProperty("employee_model", employee_model) @@ -105,7 +105,7 @@ if __name__ == "__main__": qml_file = "qrc:/Gui/main.qml" - icon = QIcon(":/images/tero.jpg") + icon = QIcon("qrc:/images/tero.jpg") app.setWindowIcon(icon) tray = QSystemTrayIcon() diff --git a/pyqcrm.pyproject b/pyqcrm.pyproject index 7db28ab..9f3ccb6 100644 --- a/pyqcrm.pyproject +++ b/pyqcrm.pyproject @@ -7,7 +7,6 @@ "lib/DB/BusinessModel.py", "pyqcrm.qrc", "qml.qrc", - "lib/DB/DbManager.py", "lib/DB/UserManager.py", "lib/PyqcrmFlags.py", "lib/DB/UserDAO.py", diff --git a/pyqcrm.qrc b/pyqcrm.qrc index 1927922..b3e221d 100644 --- a/pyqcrm.qrc +++ b/pyqcrm.qrc @@ -9,6 +9,7 @@ images/ChevronDown.svg images/Funnel.svg images/Identification-Outline.svg + images/InboxArrowDown.svg images/MagnifyingGlass.svg images/Newspaper-Outline.svg images/Phone.svg @@ -19,14 +20,8 @@ images/UserCircle.svg images/UserGroup-Outline.svg images/Wallet-Outline.svg - sounds/error.ogg - sounds/fail2c.ogg - sounds/puzzerr.ogg - sounds/sysnotify.ogg - sounds/wrong.ogg fonts/RobotoCondensed.otf README LICENSE - images/tero.jpg diff --git a/qml.qrc b/qml.qrc index 9d1b12c..b078986 100644 --- a/qml.qrc +++ b/qml.qrc @@ -39,11 +39,11 @@ Gui/OffersTable.qml Gui/Employees/AddApplicant.qml Gui/Employees/AddEmployee.qml - Gui/Employees/ApplicantPersonalData.qml - Gui/Employees/ApplicantBankData.qml Gui/Employees/ApplicantForm.qml - Gui/Employees/ApplicantNationalInsurance.qml - Gui/Employees/ApplicantVarious.qml + Gui/Employees/EmployeePersonalData.qml + Gui/Employees/EmployeeBankData.qml + Gui/Employees/EmployeeNationalInsurance.qml + Gui/Employees/EmployeeVarious.qml Gui/Employees/EmployeeDetails.qml Gui/Employees/EmployeesTable.qml Gui/Employees/qmldir diff --git a/requirements.txt b/requirements.txt index 7d26b55..f301c49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,8 @@ platformdirs pycryptodome psutil toml -mariadb soundfile sounddevice reportlab +peewee +pymysql \ No newline at end of file diff --git a/sounds/error.aac b/sounds/error.aac deleted file mode 100644 index 1a1fdc7..0000000 Binary files a/sounds/error.aac and /dev/null differ diff --git a/sounds/error.ac3 b/sounds/error.ac3 deleted file mode 100644 index 5f7f39b..0000000 Binary files a/sounds/error.ac3 and /dev/null differ diff --git a/sounds/error.mp3 b/sounds/error.mp3 deleted file mode 100644 index ad527eb..0000000 Binary files a/sounds/error.mp3 and /dev/null differ diff --git a/sounds/error.ogg b/sounds/error.ogg deleted file mode 100644 index a622fb5..0000000 Binary files a/sounds/error.ogg and /dev/null differ diff --git a/sounds/error.wav b/sounds/error.wav deleted file mode 100644 index a184896..0000000 Binary files a/sounds/error.wav and /dev/null differ diff --git a/sounds/fail2c.ogg b/sounds/fail2c.ogg deleted file mode 100644 index c18884c..0000000 Binary files a/sounds/fail2c.ogg and /dev/null differ diff --git a/sounds/puzzerr.ogg b/sounds/puzzerr.ogg deleted file mode 100644 index fce3b92..0000000 Binary files a/sounds/puzzerr.ogg and /dev/null differ diff --git a/sounds/sysnotify.ogg b/sounds/sysnotify.ogg deleted file mode 100644 index 7cc1475..0000000 Binary files a/sounds/sysnotify.ogg and /dev/null differ diff --git a/sounds/wrong.ogg b/sounds/wrong.ogg deleted file mode 100644 index ec56a55..0000000 Binary files a/sounds/wrong.ogg and /dev/null differ