From 64c7793b4e0ae8b2d92ed5ab221998c64330a60c Mon Sep 17 00:00:00 2001 From: "sebastian.serfling" Date: Wed, 21 Aug 2024 17:35:08 +0200 Subject: [PATCH] add Ticket Export as APP --- .env | 10 +- .streamlit/config.toml | 4 +- app.py | 19 ++- apps/ticket_export/.env | 13 ++ apps/ticket_export/main.py | 278 +++++++++++++++++++++++++++++++ apps/ticket_export/template.docx | Bin 0 -> 37296 bytes sites/lastrun.py | 94 +++++++++++ sites/server.py | 1 - sites/tickets.py | 140 ++++++++++++++++ sites/userlist.py | 10 +- 10 files changed, 557 insertions(+), 12 deletions(-) create mode 100644 apps/ticket_export/.env create mode 100644 apps/ticket_export/main.py create mode 100644 apps/ticket_export/template.docx create mode 100644 sites/lastrun.py create mode 100644 sites/tickets.py diff --git a/.env b/.env index 28c0ea8..5948ca5 100644 --- a/.env +++ b/.env @@ -2,4 +2,12 @@ MYSQL_HOST="172.17.1.21" MYSQL_USER="root" MYSQL_PASSWORD="N53yBCswuawzBzS445VNAhWVMs3N59Gb9szEsrzXRBzarDqpdETpQeyt5v5CGe" MYSQL_DATABASE="Kunden" -MYSQL_AUTH='mysql_native_password' \ No newline at end of file +MYSQL_AUTH='mysql_native_password' + +## Ticketsystem +ZAMMAD_URL = "https://ticket.stines.de/api/v1" +ZAMMAD_API_TOKEN ="1v4XGY7cZpBXSfb4s_tIBbywQjcaDV6q65IXQyVXrrBDqVtmAtLxM5tOqIAp0VXZ" + +## API-Server +API_SERVER = "http://api.stines.de:8001" +API_TOKEN = "^YWUbG7yX*V!tV^KBSd*2c&vdN3wV9a2i7f3hfGFMBYFxi6#mMiJGiaA5KEHE%B*miK%qb7rQ67gmcYP@gqmux8" \ No newline at end of file diff --git a/.streamlit/config.toml b/.streamlit/config.toml index fe7c2dc..6bb3aa9 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -4,4 +4,6 @@ primaryColor="#1abc9c" backgroundColor="#2c3e50" secondaryBackgroundColor="#34495e" textColor="#ffffff" -font="sans serif" \ No newline at end of file +font="sans serif" + +layout="wide" \ No newline at end of file diff --git a/app.py b/app.py index 2d9e41d..429cc27 100644 --- a/app.py +++ b/app.py @@ -2,9 +2,10 @@ import streamlit as st import sites.services_reporting as sr import sites.userlist as us import sites.server as s - +import sites.tickets as ti +import sites.lastrun as lr # Page Settings -st.set_page_config(page_title="Reporting") +st.set_page_config(page_title="Reporting",layout="wide") # Load custom CSS def load_css(file_name): @@ -27,17 +28,25 @@ if st.sidebar.button('Home'): st.session_state.page = 'Home' if st.sidebar.button('Services Reporting'): st.session_state.page = 'Services Reporting' -if st.sidebar.button('User'): - st.session_state.page = 'User' +if st.sidebar.button('User Filter'): + st.session_state.page = 'User Filter' if st.sidebar.button('Server'): st.session_state.page = 'Server' +if st.sidebar.button('Tickets'): + st.session_state.page = 'Tickets' +if st.sidebar.button('Last-Run'): + st.session_state.page = 'Last-Run' # Page display logic if st.session_state.page == 'Home': home() elif st.session_state.page == 'Services Reporting': sr.services_reporting() -elif st.session_state.page == 'User': +elif st.session_state.page == 'User Filter': us.user_filter() elif st.session_state.page == 'Server': s.server_filter() +elif st.session_state.page == 'Tickets': + ti.ticket_filter() +elif st.session_state.page == 'Last-Run': + lr.user_filter() \ No newline at end of file diff --git a/apps/ticket_export/.env b/apps/ticket_export/.env new file mode 100644 index 0000000..5948ca5 --- /dev/null +++ b/apps/ticket_export/.env @@ -0,0 +1,13 @@ +MYSQL_HOST="172.17.1.21" +MYSQL_USER="root" +MYSQL_PASSWORD="N53yBCswuawzBzS445VNAhWVMs3N59Gb9szEsrzXRBzarDqpdETpQeyt5v5CGe" +MYSQL_DATABASE="Kunden" +MYSQL_AUTH='mysql_native_password' + +## Ticketsystem +ZAMMAD_URL = "https://ticket.stines.de/api/v1" +ZAMMAD_API_TOKEN ="1v4XGY7cZpBXSfb4s_tIBbywQjcaDV6q65IXQyVXrrBDqVtmAtLxM5tOqIAp0VXZ" + +## API-Server +API_SERVER = "http://api.stines.de:8001" +API_TOKEN = "^YWUbG7yX*V!tV^KBSd*2c&vdN3wV9a2i7f3hfGFMBYFxi6#mMiJGiaA5KEHE%B*miK%qb7rQ67gmcYP@gqmux8" \ No newline at end of file diff --git a/apps/ticket_export/main.py b/apps/ticket_export/main.py new file mode 100644 index 0000000..83a0a43 --- /dev/null +++ b/apps/ticket_export/main.py @@ -0,0 +1,278 @@ +import mysql.connector +from docx import Document +from datetime import datetime +from docx.shared import Pt +from docx.oxml.ns import qn +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK +from docx.enum.table import WD_ALIGN_VERTICAL +from docx.oxml import OxmlElement +import os +from datetime import datetime, timedelta +from dotenv import load_dotenv +from dateutil.relativedelta import relativedelta + +load_dotenv() + +def fetch_tickets_from_database(): + # Verbindung zur Datenbank herstellen + mydb = mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE") + ) + cursor = mydb.cursor() + + # Tickets abrufen, inklusive customer_ID + cursor.execute(""" + SELECT t.`number`, t.title, t.createdate, t.`type`, t.customer_ID, tct.firstdate, t.time, s.price, t.service_ID + FROM Kunden.tickets t + JOIN Kunden.`tickets.customer.timerange` tct ON t.customer_ID = tct.customer_ID + JOIN Kunden.services s ON s.service_ID = t.service_ID + ORDER by t.createdate ASC + """) + tickets = cursor.fetchall() + mydb.close() + return tickets + +def fetch_customer_data(customer_id): + # Verbindung zur Datenbank herstellen + mydb = mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE") + ) + cursor = mydb.cursor() + + # Kundendaten basierend auf customer_ID abrufen + cursor.execute("""SELECT co.companyname, co.street,co.housenumber, co.postcode, co.city, c.customer FROM company co + JOIN customers c ON c.customer_ID = co.customer_ID + WHERE co.customer_id = %s""", (customer_id,)) + customer_data = cursor.fetchone() + + mydb.close() + return customer_data + +def fetch_customer_servicetime(customer_id): + # Verbindung zur Datenbank herstellen + mydb = mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE") + ) + cursor = mydb.cursor() + + # Kundendaten basierend auf customer_ID abrufen + cursor.execute("""SELECT servicetime FROM `tickets.customers.servicetime` + WHERE customer_id = %s""", (customer_id,)) + customer_servicetime = cursor.fetchone() + + mydb.close() + return customer_servicetime + + +def fetch_customer_price(price, customer_ID, service_ID): + mydb = mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE") + ) + cursor = mydb.cursor() + + # Prozentwert aus der Datenbank abrufen + cursor.execute(f"""SELECT percent FROM `customers.pricechange` + WHERE customer_ID = {customer_ID} and service_ID = {service_ID}""") + customer_price_percent = cursor.fetchone() + mydb.close() + + # Preis in Minuten umrechnen (da der aktuelle Preis in Stunden angegeben ist) + price_per_minute = price / 60 # Umrechnung von Stunden auf Minuten + + # Falls ein Prozentwert existiert, diesen auf den Minutenpreis anwenden + if customer_price_percent: + price_per_minute += price_per_minute * (customer_price_percent[0] / 100) + # print(f"Preis pro Minute nach Aufschlag von {customer_price_percent[0]}%: {price_per_minute:.2f} €") + else: + print(f"Standard Preis pro Minute: {price_per_minute:.2f} €") + + return price_per_minute + +def replace_text_in_run_with_format(run, key, value): + if f"{{{key}}}" in run.text: + run.text = run.text.replace(f"{{{key}}}", str(value)) + run.font.name = 'Verdana' + run.font.size = Pt(10) + run.font.bold = False + run.font.italic = False + +def insert_page_break(paragraph): + run = paragraph.add_run() + run.add_break(WD_BREAK.PAGE) + +def replace_text_in_paragraph(paragraph, key, value): + for run in paragraph.runs: + replace_text_in_run_with_format(run, key, value) + +def set_cell_border(cell, **kwargs): + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + + borders = OxmlElement('w:tcBorders') + + for border_name in ["top", "left", "bottom", "right"]: + border = kwargs.get(border_name, None) + if border: + element = OxmlElement(f"w:{border_name}") + element.set(qn("w:val"), border.get("val", "single")) + element.set(qn("w:sz"), border.get("sz", "4")) + element.set(qn("w:space"), border.get("space", "0")) + element.set(qn("w:color"), border.get("color", "000000")) + borders.append(element) + + tcPr.append(borders) + +def fill_template(doc_path, output_path, data, tickets): + doc = Document(doc_path) + + # Platzhalter in normalem Text ersetzen + for paragraph in doc.paragraphs: + for key, value in data.items(): + if f"{{{key}}}" in paragraph.text: + replace_text_in_paragraph(paragraph, key, value) + if "{page_breaker}" in paragraph.text: + if len(tickets) >= 7: + paragraph.text = paragraph.text.replace("{page_breaker}", "") + insert_page_break(paragraph) + else: + paragraph.text = paragraph.text.replace("{page_breaker}", "") + + # Platzhalter in der ersten Tabelle ersetzen + first_table = doc.tables[0] + for row in first_table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + for key, value in data.items(): + replace_text_in_paragraph(paragraph, key, value) + + # Bearbeiten der zweiten Tabelle für Ticketdaten + second_table = doc.tables[1] + for ticket in tickets: + row = second_table.add_row() + row.cells[0].text = ticket[0] # Ticketnummer + row.cells[1].text = ticket[1] # Tickettitel + row.cells[2].text = ticket[2].strftime("%d.%m.%Y") # createdate als String formatieren + row.cells[3].text = str(ticket[6]) # timerange (time_unit) + + # Formatierung und Ausrichtung für jede Zelle der neuen Zeile setzen + for idx, cell in enumerate(row.cells): + for paragraph in cell.paragraphs: + if idx == 1: + paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT + else: + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + + for run in paragraph.runs: + run.font.name = 'Verdana' + run.font.size = Pt(10) + run.font.bold = False + cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER + set_cell_border(cell, top={"val": "single", "sz": "4", "color": "000000"}, + bottom={"val": "single", "sz": "4", "color": "000000"}, + left={"val": "single", "sz": "4", "color": "000000"}, + right={"val": "single", "sz": "4", "color": "000000"}) + + # Berechnungen (z.B. Gesamtzeit, Preise) + gesamt_time_unit = sum(int(ticket[6]) for ticket in tickets) + data['gesamt_time_unit'] = gesamt_time_unit + + # Zeit größer als Servicetime + if gesamt_time_unit >= customer_servicetime[0]: + data['sl_time_unit'] = gesamt_time_unit + customer_servicetime[0] + data['sl_minus_unit'] = gesamt_time_unit - customer_servicetime[0] + data['time_unit_sum'] = gesamt_time_unit + data['zl_time_unit'] + data['price_per_minute'] = customer_price + + data['price_ex_mwst'] = gesamt_time_unit * data['price_per_minute'] + data['mwst_set'] = data['price_ex_mwst'] * 0.19 + data['sum'] = data['price_ex_mwst'] + data['mwst_set'] + + data['price_ex_mwst'] = f"{data['price_ex_mwst']:.2f}".replace(".",",") + data['mwst_set'] = f"{data['mwst_set']:.2f}".replace(".",",") + data['sum'] = f"{data['sum']:.2f}".replace(".",",") + + + # Platzhalter in der dritten Tabelle ersetzen + third_table = doc.tables[2] + for row in third_table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + for key, value in data.items(): + if key == "sum": + # Setze den Text und mache ihn fett + for run in paragraph.runs: + if f"{{{key}}}" in run.text: + run.text = run.text.replace(f"{{{key}}}", str(value)) + run.font.bold = True # Fett setzen + else: + replace_text_in_paragraph(paragraph, key, value) + + doc.save(output_path) + +# Hauptprogramm +if __name__ == "__main__": + tickets = fetch_tickets_from_database() + service_ID = next(iter(set(ticket[8] for ticket in tickets))) + price = next(iter(set(ticket[7] for ticket in tickets))) + customer_ids = set(ticket[4] for ticket in tickets) + + for customer_id in customer_ids: + customer_data = fetch_customer_data(customer_id) + customer_servicetime = fetch_customer_servicetime(customer_id) + customer_price = fetch_customer_price(price, customer_id, service_ID) + customer_tickets = [ticket for ticket in tickets if ticket[4] == customer_id] + + if not customer_data: + print(f"Keine Kundendaten für Kunden-ID {customer_id} gefunden!") + continue + + day = customer_tickets[0][5] + now = datetime.now() + if now.month == 1: + last_month = 12 + year = now.year - 1 + else: + last_month = now.month - 1 + year = now.year + startdate = datetime(year, last_month, day) + enddate = startdate + relativedelta(months=1) - timedelta(days=1) + + data = { + "name": customer_data[0], + "street": customer_data[1], + "housenumber": customer_data[2], + "postcode": customer_data[3], + "city": customer_data[4], + "customernumber": customer_data[5], + "year": datetime.now().year, + "ordernumber": "0001", + "startdate": startdate.strftime("%d.%m.%Y"), + "enddate": enddate.strftime("%d.%m.%Y"), + "today": datetime.now().strftime("%d.%m.%Y"), + "price_per_minute": customer_price, + "servicetime": customer_servicetime[0], + "gesamt_time_unit": 0, + "price_ex_mwst": 0, + "sl_time_unit": 0, + "sl_minus_unit": 0, + "zl_time_unit": 0, + "time_unit_sum": 0, + "mwst_set": 0, + "sum": 0, + } + + output_path = f"apps/ticket_export/exports/RE2024.{customer_data[5]}.{data['ordernumber']}.docx" + fill_template('apps/ticket_export/template.docx', output_path, data, customer_tickets) +print("True") diff --git a/apps/ticket_export/template.docx b/apps/ticket_export/template.docx new file mode 100644 index 0000000000000000000000000000000000000000..18a9b430f98406fc0173e7b24c9038805d485a72 GIT binary patch literal 37296 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') + + return users + +def get_initial_data(): + mydb = mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE") + ) + # Fetch unique service IDs and names + service_id_query = """ + SELECT DISTINCT s.service_ID, s.name + FROM Kunden.services s + """ + service_ids = pd.read_sql_query(service_id_query, mydb) + + # Fetch customer information + customer_query = """ + SELECT DISTINCT c.customer_ID, c.customer, co.companyname + FROM Kunden.company co + JOIN Kunden.customers c ON co.customer_ID = c.customer_ID + """ + customers = pd.read_sql_query(customer_query, mydb) + + mydb.close() + return service_ids, customers + +def run_script_and_list_documents(): + output_area = st.empty() # Platzhalter für die Ausgabe + document_paths = [] # Liste der erstellten Dokumente + + # Verzeichnis, in dem das Skript ausgeführt werden soll + working_directory = "apps/ticket_export/exports" + + # Führe das Skript in dem angegebenen Verzeichnis aus + result = subprocess.run( + ["python3", "apps/ticket_export/main.py"], + capture_output=True, + text=True + ) + + # Suche nach den erstellten Dokumenten im Arbeitsverzeichnis + for filename in os.listdir("apps/ticket_export/exports"): + if filename.endswith(".docx"): + full_path = os.path.join(working_directory, filename) + document_paths.append(full_path) + + # Zeige die Liste der erstellten Dokumente an und biete sie zum Download an + if document_paths: + st.write("Erstellte Dokumente:") + for doc_path in document_paths: + with open(doc_path, "rb") as file: + st.download_button( + label=f"Download {os.path.basename(doc_path)}", + data=file, + file_name=os.path.basename(doc_path), + mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + else: + st.write("Keine Dokumente gefunden.") + +def ticket_filter(): + st.title("Ticket Filter :mag_right:") + # Get initial data for widgets + initial_service_ids, customers = get_initial_data() + + # Add selection widget for customer ID + selected_customer = st.selectbox( + 'Select Customer', + ["All"] + customers.apply(lambda row: f"{row['customer_ID']} - {row['companyname']} - {row['customer']}", + axis=1).tolist() + ) + # Extract customer_ID from selected option + selected_customer_id = None if selected_customer == "All" else int(selected_customer.split(' - ')[0]) + + if st.button("Rechnung erstellen"): + st.write("Das Skript wird ausgeführt...") + run_script_and_list_documents() + + # Add a button to apply filters + if st.button('Apply Filters'): + # Fetch filtered data from the database + filtered_data = get_filtered_users(selected_customer_id) + if not filtered_data.empty: + # st.dataframe(filtered_data,hide_index=True) + st.markdown(filtered_data.to_html(escape=False), unsafe_allow_html=True) + # Convert DataFrame to CSV + csv = filtered_data.drop(columns=['Link']).to_csv(index=False) + + st.write(f"Total Time Unit: {filtered_data['time'].sum()}") + + # Create a download button with a custom file name + st.download_button( + label="Download CSV", + data=csv, + file_name=f"filtered_data_{selected_customer}.csv", # Custom file name + mime='text/csv', + ) + + else: + st.write("No data available for the selected filters.") \ No newline at end of file diff --git a/sites/userlist.py b/sites/userlist.py index ea7e495..c1b6278 100644 --- a/sites/userlist.py +++ b/sites/userlist.py @@ -18,7 +18,7 @@ def get_filtered_users(customer_id, service_id, service_status): # Prepare the base query query = f""" - SELECT us.user_id, u.username, s.service_ID, uss.status, uss.timestamp + SELECT us.user_id, u.username, s.service_ID, uss.status, uss.timestamp, c.companyname FROM Kunden.`users.status` us JOIN ( SELECT user_id, MAX(timestamp) AS latest_timestamp @@ -28,14 +28,16 @@ def get_filtered_users(customer_id, service_id, service_status): JOIN Kunden.users u ON u.user_ID = us.user_id JOIN Kunden.`users.services` uss ON us.user_id = uss.user_id JOIN Kunden.services s ON uss.service_ID = s.service_ID - WHERE us.customer_ID = {customer_id} and us.status = {service_status} + JOIN Kunden.company c ON c.customer_ID = us.customer_ID + WHERE 1=1 """ - + if customer_id: + query += f"AND us.customer_ID = {customer_id}" if service_id: query += f" AND s.service_ID = {service_id}" if service_status: query += f" AND uss.status = {service_status}" - + query += " ORDER BY uss.status DESC" users = pd.read_sql_query(query, mydb) mydb.close() return users