Commit 8e9dcfe4 authored by Marco Schmiedel's avatar Marco Schmiedel

Backup

parent 5fb2066a
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
"fileId": "24784b38-54dc-4000-9d2a-f59082ebbc1c", "fileId": "24784b38-54dc-4000-9d2a-f59082ebbc1c",
"originalPath": "work/models/base_base.py", "originalPath": "work/models/base_base.py",
"currentPath": "work/models/base_base.py", "currentPath": "work/models/base_base.py",
"hash": "a13647e2879a37dfcb76bf95c143fc42b0da2eac67a471884afd5c314dc46f8f", "hash": "722215d7b7ca6bc285f74f96c5096fdae09a7d3ec477e319031640e3100ad5b4",
"docContent": "<p><br></p>", "docContent": "<p><br></p>",
"checkedStatus": "done", "checkedStatus": "changed",
"comments": [], "comments": [],
"lastCheckedTimestamp": 1745314601207, "lastCheckedTimestamp": 1745314601207,
"lastFileModificationTimestamp": 1745313922231.5488, "lastFileModificationTimestamp": 1746440397236.4856,
"flaggedForCopy": false "flaggedForCopy": false
} }
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
"fileId": "36e791b4-e235-42f6-ac61-8560f1762892", "fileId": "36e791b4-e235-42f6-ac61-8560f1762892",
"originalPath": "work/workbench/Workbench.mwb", "originalPath": "work/workbench/Workbench.mwb",
"currentPath": "work/workbench/Workbench.mwb", "currentPath": "work/workbench/Workbench.mwb",
"hash": "f10bcc0690c342970656e4b4dcaa462b189bc5fa833b9f2bb04f88731782b1f0", "hash": "d53db9e9d211116d4aafc32106a7e0c05a86c062af72f21a37420853a1c4eacc",
"docContent": "<p><br></p>", "docContent": "<p><br></p>",
"checkedStatus": "done", "checkedStatus": "changed",
"comments": [], "comments": [],
"lastCheckedTimestamp": 1744805338829, "lastCheckedTimestamp": 1746433397717,
"lastFileModificationTimestamp": 1744805330071.942 "lastFileModificationTimestamp": 1746440499172.53
} }
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
"fileId": "543b791f-9a45-4b53-906e-c49f79ac95d7", "fileId": "543b791f-9a45-4b53-906e-c49f79ac95d7",
"originalPath": "work/notebooks/ImportCsvToDatabase.ipynb", "originalPath": "work/notebooks/ImportCsvToDatabase.ipynb",
"currentPath": "work/notebooks/ImportCsvToDatabase.ipynb", "currentPath": "work/notebooks/ImportCsvToDatabase.ipynb",
"hash": "c6e6e4c70653fbfc4d43ae89e4f05fc1a8d2804eb2f515ad52c41637bd5b0e14", "hash": "1897435119be0001a90340771be7cde4ca337b9d59653cf0faed8cd52514815c",
"docContent": "<p><br></p>", "docContent": "<p><br></p>",
"checkedStatus": "changed", "checkedStatus": "changed",
"comments": [], "comments": [],
"lastCheckedTimestamp": 1744806594117, "lastCheckedTimestamp": 1744806594117,
"lastFileModificationTimestamp": 1745313435812.8953 "lastFileModificationTimestamp": 1746447349386.795
} }
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
"currentPath": "work/workbench/Documentation.md", "currentPath": "work/workbench/Documentation.md",
"hash": "35d995fb3f8a904ed6f47adbd4da60916c93a29aad08af551b493a32714dcd9c", "hash": "35d995fb3f8a904ed6f47adbd4da60916c93a29aad08af551b493a32714dcd9c",
"docContent": "<p><br></p>", "docContent": "<p><br></p>",
"checkedStatus": "done", "checkedStatus": "todo",
"comments": [], "comments": [],
"lastCheckedTimestamp": 1744805208732, "lastCheckedTimestamp": 1746428387938,
"lastFileModificationTimestamp": 1744805204185.4846 "lastFileModificationTimestamp": 1744805204185.4846
} }
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
"fileId": "f645f6dc-6831-4020-a6ae-8b5a572eed54", "fileId": "f645f6dc-6831-4020-a6ae-8b5a572eed54",
"originalPath": "work/config/OpenAiConfig.py", "originalPath": "work/config/OpenAiConfig.py",
"currentPath": "work/config/OpenAiConfig.py", "currentPath": "work/config/OpenAiConfig.py",
"hash": "3da3805934b36b3ab8c21e10d8babe3cc8b6c9acb7b2b1446f782612b76cf2c4", "hash": "50c0f7d96f9ea76aa069a0a24137e898dbd4fc3c4af867565c90468981bf6ff5",
"docContent": "<p><br></p>", "docContent": "<p><br></p>",
"checkedStatus": "todo", "checkedStatus": "changed",
"comments": [], "comments": [],
"lastCheckedTimestamp": 1745314586062, "lastCheckedTimestamp": 1745314586062,
"lastFileModificationTimestamp": 1745313976355.6653 "lastFileModificationTimestamp": 1746437070245.503
} }
# Wir verwenden Ubuntu als Betriebssystem.
FROM ubuntu:24.04
# Wir deaktivieren das interaktive Frontend.
ENV DEBIAN_FRONTEND=noninteractive
# Zuerst aktualisieren wir die Paketquellen und führen ein Upgrade durch.
RUN apt-get -y update
RUN apt-get -y upgrade
# Anschließend installieren wir systemweite Hilfspakete.
RUN apt-get install -y software-properties-common
# Wir installieren Python3 und pip.
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
# Wir fügen das PPA hinzu, um die deb-basierte Version von Chromium zu erhalten.
RUN add-apt-repository ppa:xtradeb/apps -y
# Wir aktualisieren erneut die Paketquellen (dadurch werden auch die PPA-Pakete verfügbar).
RUN apt-get -y update
# Wir installieren den Browser.
RUN apt-get install -y chromium-browser
RUN apt-get install -y chromium-driver
RUN apt-get install -y firefox-geckodriver
# Wir entfernen snapd, damit keine Snap-Version von Chromium verwendet wird.
RUN apt-get remove -y snapd
# Wir installieren Cron.
RUN apt-get install -y cron
# Wir installieren Vim.
RUN apt-get install -y vim
# Wir installhieren htop.
RUN apt-get install -y htop
# Wir installhieren ffmpeg.
RUN apt-get install -y ffmpeg
# Wir installhieren curl.
RUN apt-get install -y curl
# Wir installieren die Python-Abhängigkeiten via pip.
RUN pip3 install --break-system-packages selenium
RUN pip3 install --break-system-packages requests
RUN pip3 install --break-system-packages sqlalchemy
RUN pip3 install --break-system-packages pymysql
RUN pip3 install --break-system-packages pandas
RUN pip3 install --break-system-packages bs4
RUN pip3 install --break-system-packages feedparser
RUN pip3 install --break-system-packages demjson3
RUN pip3 install --break-system-packages --ignore-installed flask
RUN pip3 install --break-system-packages feedgen
RUN pip3 install --break-system-packages boto3
RUN pip3 install --break-system-packages pydub
RUN pip3 install --break-system-packages json5
RUN pip3 install --break-system-packages pyotp
RUN pip3 install --break-system-packages sshtunnel
RUN pip3 install --break-system-packages pypdf
# Wir kopieren die Cron-Datei in den Container.
COPY config/_CronConfig.txt /etc/cron.d/scrapeNewsCron
RUN chmod 0644 /etc/cron.d/scrapeNewsCron
RUN crontab /etc/cron.d/scrapeNewsCron
COPY cron.sh /maui/cron.sh
RUN chmod +x /maui/cron.sh
# Wir kopieren alle Systemdatein in den Container.
COPY config /maui/config
COPY manager /maui/manager
COPY commands /maui/commands
COPY models /maui/models
COPY boot.sh /maui/boot.sh
RUN chmod +x /maui/boot.sh
# Wir definieren das Startscript des Containers.
CMD ["/maui/boot.sh"]
\ No newline at end of file
#!/bin/bash
set -e
# Dieser Befehl startet den Cron-Service.
service cron start
# Dieser Befehl wechselt in das Arbeitsverzeichnis /obsidian/manager.
cd /maui/manager
# Dieser Befehl setzt die Umgebungsvariable PYTHONPATH auf /obsidian.
export PYTHONPATH=/maui
# Dieser Befehl startet den ApiManager Webserver.
python3 WebManager.py
This diff is collapsed.
This diff is collapsed.
import sys; sys.path.append("..")
import os
import csv
import datetime
import logging
from decimal import Decimal
from collections import defaultdict
from sqlalchemy.dialects.mysql import insert as mysql_insert
from manager.MysqlManager import MysqlManager
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti
#
# Hier wird die Log-Konfiguration festgelegt, damit während des Ablaufs aussagekräftige Zeit- und Fehlermeldungen ausgegeben werden.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s"
)
#
# Dieses Verzeichnis verweist auf den Zwischenspeicher, in dem alle CSV-Dateien abgelegt sind.
CSV_DIR = os.path.join("..", "cache")
#
# Hier werden die einzelnen CSV-Dateien innerhalb des Zwischenspeichers definiert.
csvFileCategories = os.path.join(CSV_DIR, "categorys.csv")
csvFilePlans = os.path.join(CSV_DIR, "plans.csv")
csvFileCampaigns = os.path.join(CSV_DIR, "campaigns.csv")
csvFileOptions = os.path.join(CSV_DIR, "options.csv")
#
# Diese Funktion liest eine CSV-Datei vollständig ein und liefert jede Zeile als Wörterbuch zurück.
def read_csv(path):
with open(path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f, delimiter=";"))
#
# Hier werden sämtliche CSV-Dateien in Listen von Wörterbüchern eingelesen.
cat_rows = read_csv(csvFileCategories)
plan_rows = read_csv(csvFilePlans)
camp_rows = read_csv(csvFileCampaigns)
opt_rows = read_csv(csvFileOptions)
#
# Dieses Wörterbuch ordnet jeder Kategorietabelle den zugehörigen Namen für späteres Nachschlagen zu.
category_name = {r["id"].strip(): r["name"].strip() for r in cat_rows}
#
# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Kampagnenzeilen zu, um schnellen Zugriff zu ermöglichen.
campaigns_by_plan = defaultdict(list)
for c in camp_rows:
campaigns_by_plan[c["plan"].strip()].append(c)
#
# Diese Datenstruktur ordnet jeder Plan-ID alle zugehörigen Optionszeilen zu, um schnellen Zugriff zu ermöglichen.
options_by_plan = defaultdict(list)
for o in opt_rows:
options_by_plan[o["plan"].strip()].append(o)
#
# Hier wird die Verbindung zur Datenbank geöffnet und eine neue Session erzeugt.
mysql = MysqlManager()
session = mysql.getSession()
#
# Dieses Wörterbuch enthält alle bestehenden Basiseinträge, damit später neue Einträge erkannt werden können.
base_db = {(b.provider_base, b.providercode_base): b
for b in session.query(BaseBase).all()}
#
# Diese verschachtelte Struktur hält alle vorhandenen Deals pro Base-ID, wodurch ein schneller Abgleich ermöglicht wird.
deals_db = defaultdict(dict)
for d in session.query(DealDeal).all():
deals_db[d.base_deal][d.providercode_deal] = d
#
# Diese verschachtelte Struktur hält alle vorhandenen Optionen pro Base-ID, um später Stop- und Reaktivierungslogik anzuwenden.
opts_db = defaultdict(dict)
for o in session.query(OptionOpti).all():
opts_db[o.base_opti][o.providercode_opti] = o
#
# Hier wird der aktuelle Zeitpunkt einmalig festgelegt, um ihn konsistent für alle neu erzeugten Datensätze zu verwenden.
now = datetime.datetime.now()
#
# Diese Liste sammelt alle neu anzulegenden Basiseinträge, damit sie in einem Schritt geschrieben werden können.
new_bases = []
for p in plan_rows:
prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"
key = (prov_base, p["id"].strip())
if key not in base_db:
b = BaseBase(
provider_base = prov_base,
providercode_base = p["id"].strip(),
name_base = p["name"].strip(),
created_base = now,
updated_base = now
)
new_bases.append(b)
base_db[key] = b
#
# Hier werden alle neu erkannten Basiseinträge in einem einzigen Datenbankvorgang gespeichert.
if new_bases:
session.add_all(new_bases)
session.flush()
logging.info("Inserted %d new bases", len(new_bases))
#
# Diese verschachtelten Mengen erfassen für jede Base-ID die in diesem Lauf gewünschten Deals und Optionen.
desired_deals = defaultdict(set)
desired_opts = defaultdict(set)
#
# Diese Listen sammeln alle Datensätze, die per INSERT IGNORE neu geschrieben oder aktualisiert werden sollen.
deal_rows_insert = []
opt_rows_insert = []
for p in plan_rows:
prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"
base_obj = base_db[(prov_base, p["id"].strip())]
b_id = base_obj.id_base
price = Decimal(p["price"].strip() or "0.00")
#
# Dieser Block fügt den obligatorischen Standard-Deal ohne Kampagnenkennung hinzu.
desired_deals[b_id].add("")
deal_rows_insert.append({
"provisiongroup_deal": 1,
"base_deal": b_id,
"providercode_deal": "",
"name_deal": "",
"price_deal": price,
"starts_deal": now,
"stops_deal": None,
"created_deal": now,
"updated_deal": now
})
#
# Dieser Block verarbeitet alle Kampagnen zum aktuellen Plan und fügt sie der Wunschliste hinzu.
for c in campaigns_by_plan[p["id"].strip()]:
code = f"A{c['id'].strip()}"
desired_deals[b_id].add(code)
deal_rows_insert.append({
"provisiongroup_deal": 1,
"base_deal": b_id,
"providercode_deal": code,
"name_deal": c["name"].strip(),
"price_deal": price,
"starts_deal": now,
"stops_deal": None,
"created_deal": now,
"updated_deal": now
})
#
# Dieser Block fügt alle Optionen zum aktuellen Plan der Wunschliste hinzu und bereitet die Insert-Zeilen vor.
for o in options_by_plan[p["id"].strip()]:
code_opt = o["id"].strip()
desired_opts[b_id].add(code_opt)
opt_rows_insert.append({
"provisiongroup_opti": 1,
"base_opti": b_id,
"providercode_opti": code_opt,
"providercategory_opti": o["category"].strip(),
"name_opti": f"{category_name.get(o['category'].strip(),'Unbekannt')} | {o['name'].strip()}",
"alias_opti": None,
"price_opti": Decimal(o["price"].strip() or "0.00"),
"starts_opti": now,
"stops_opti": None,
"provision1_opti": Decimal("0.00000"),
"provision2_opti": Decimal("0.00000"),
"provision3_opti": Decimal("0.00000"),
"provision4_opti": Decimal("0.00000"),
"created_opti": now,
"updated_opti": now
})
#
# In diesem Schritt werden doppelte Deal- und Optionszeilen anhand ihrer Schlüsselwerte entfernt.
deal_rows_insert = list({(r["base_deal"], r["providercode_deal"]): r for r in deal_rows_insert}.values())
opt_rows_insert = list({(r["base_opti"], r["providercode_opti"]): r for r in opt_rows_insert }.values())
#
# Diese Listen sammeln Datensätze, deren Status auf gestoppt oder reaktiviert gesetzt werden muss.
stop_deals, react_deals = [], []
stop_opts, react_opts = [], []
for (prov, _), b in base_db.items():
if not prov.startswith("Freenet"):
continue
b_id = b.id_base
wantD = desired_deals.get(b_id, set())
haveD = deals_db.get(b_id, {})
for code, obj in haveD.items():
if code in wantD and obj.stops_deal is not None:
react_deals.append({"id_deal": obj.id_deal,
"stops_deal": None,
"updated_deal": now})
if code not in wantD and obj.stops_deal is None:
stop_deals.append({"id_deal": obj.id_deal,
"stops_deal": now,
"updated_deal": now})
wantO = desired_opts.get(b_id, set())
haveO = opts_db.get(b_id, {})
for code, obj in haveO.items():
if code in wantO and obj.stops_opti is not None:
react_opts.append({"id_opti": obj.id_opti,
"stops_opti": None,
"updated_opti": now})
if code not in wantO and obj.stops_opti is None:
stop_opts.append({"id_opti": obj.id_opti,
"stops_opti": now,
"updated_opti": now})
#
# Dieser Block schreibt alle gewünschten Deals per INSERT IGNORE in die Datenbank.
session.execute(
mysql_insert(DealDeal.__table__).prefix_with("IGNORE"),
deal_rows_insert
)
logging.info("INSERT IGNORE'd %d deals", len(deal_rows_insert))
#
# Dieser Block schreibt alle gewünschten Optionen per INSERT IGNORE in die Datenbank.
session.execute(
mysql_insert(OptionOpti.__table__).prefix_with("IGNORE"),
opt_rows_insert
)
logging.info("INSERT IGNORE'd %d options", len(opt_rows_insert))
#
# Dieser Block aktualisiert alle Deals, die jetzt gestoppt werden müssen.
if stop_deals:
session.bulk_update_mappings(DealDeal, stop_deals)
logging.info("Stopped %d deals", len(stop_deals))
#
# Dieser Block aktualisiert alle Deals, die wieder reaktiviert werden müssen.
if react_deals:
session.bulk_update_mappings(DealDeal, react_deals)
logging.info("Reactivated %d deals", len(react_deals))
#
# Dieser Block aktualisiert alle Optionen, die jetzt gestoppt werden müssen.
if stop_opts:
session.bulk_update_mappings(OptionOpti, stop_opts)
logging.info("Stopped %d options", len(stop_opts))
#
# Dieser Block aktualisiert alle Optionen, die wieder reaktiviert werden müssen.
if react_opts:
session.bulk_update_mappings(OptionOpti, react_opts)
logging.info("Reactivated %d options", len(react_opts))
#
# Hier werden sämtliche Änderungen dauerhaft in der Datenbank gespeichert.
session.commit()
#
# Zum Abschluss wird die Session geschlossen, um Ressourcen freizugeben.
session.close()
logging.info("Import-Lauf abgeschlossen.")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.append("..")
import os
import datetime
from manager.S3Manager import S3Manager
from manager.MysqlManager import MysqlManager
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti
from models.provisiongroup_pgro import ProvisiongroupPgro # zwingend, um Abhängigkeits-Mapping zu initialisieren
#
# Dieses Verzeichnis enthält sämtliche PDF-Dateien für den Upload.
cacheDir = "../cache"
#
# Dieses Objekt übernimmt das Hochladen der Dateien in den S3-Bucket und liefert die endgültige URL.
s3Manager = S3Manager()
#
# Diese Datenbank-Session ermöglicht Abfragen und Aktualisierungen innerhalb der MySQL-Datenbank.
dbSession = MysqlManager().getSession()
#
# Diese Liste sammelt alle PDF-Dateinamen im Cache-Verzeichnis.
pdfFiles = [f for f in os.listdir(cacheDir) if f.lower().endswith(".pdf")]
#
# Diese Menge speichert alle eindeutigen Basis-IDs, die durch Suffix-Prüfung ermittelt wurden.
pdfIdSet = set()
for name in pdfFiles:
stem = name[:-4].lower()
if stem.endswith("_flyer"):
pdfIdSet.add(stem[:-6])
elif stem.endswith("_pib"):
pdfIdSet.add(stem[:-4])
#
# Dieser Block beendet das Skript, wenn keine geeigneten PDF-Dateien vorhanden sind.
if not pdfIdSet:
print(f"INFO: Keine PDF-Paare in '{cacheDir}' gefunden.")
dbSession.close()
sys.exit(0)
#
# Diese Schleife verarbeitet jede erkannte Basis-ID in sortierter Reihenfolge.
for currentId in sorted(pdfIdSet):
print(f"\n--- Verarbeitung ID: {currentId} ---")
#
# Diese Abfrage liefert alle BaseBase-Datensätze, um Mehrfachtreffer sicher zu unterstützen.
baseRecords = dbSession.query(BaseBase).filter_by(providercode_base=currentId).all()
if not baseRecords:
print(f"WARNUNG: Kein BaseBase-Eintrag für providercode_base='{currentId}'.")
continue
#
# Dieser Pfad verweist auf die potenzielle Flyer-Datei der aktuellen Basis-ID.
flyerPath = os.path.join(cacheDir, f"{currentId}_flyer.pdf")
#
# Dieser Pfad verweist auf die potenzielle PIB-Datei der aktuellen Basis-ID.
pibPath = os.path.join(cacheDir, f"{currentId}_pib.pdf")
#
# Diese Variable hält die hochgeladene Flyer-URL oder bleibt None, falls kein Upload erfolgte.
flyerUrl = None
if os.path.exists(flyerPath):
flyerKey = f"flyers/{currentId}_flyer.pdf"
flyerUrl = s3Manager.uploadFile(flyerPath, flyerKey)
if not flyerUrl:
print(f"FEHLER: Flyer-Upload fehlgeschlagen für ID {currentId}")
#
# Diese Variable hält die hochgeladene PIB-URL oder bleibt None, falls kein Upload erfolgte.
pibUrl = None
if os.path.exists(pibPath):
pibKey = f"pibs/{currentId}_pib.pdf"
pibUrl = s3Manager.uploadFile(pibPath, pibKey)
if not pibUrl:
print(f"FEHLER: PIB-Upload fehlgeschlagen für ID {currentId}")
#
# Diese Schleife aktualisiert jede gefundene Base-Zeile, um Flyer- und PIB-URLs konsistent zu setzen.
for base in baseRecords:
if base.flyerurl_base is None and flyerUrl:
base.flyerurl_base = flyerUrl
base.updated_base = datetime.datetime.now()
print(f"INFO: flyerurl_base gesetzt: {flyerUrl}")
if base.piburl_base is None and pibUrl:
base.piburl_base = pibUrl
base.updated_base = datetime.datetime.now()
print(f"INFO: piburl_base gesetzt: {pibUrl}")
#
# Dieser Aufruf speichert alle Änderungen für die aktuelle Basis-ID atomar in der Datenbank.
dbSession.commit()
#
# Hier wird die Session geschlossen, sobald alle Basis-IDs verarbeitet wurden.
dbSession.close()
#
# Diese Meldung bestätigt das erfolgreiche Ende des gesamten Upload-Vorgangs.
print("INFO: Upload-Vorgang abgeschlossen.")
secret = "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" secret = "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"
0 4 * * * /maui/cron.sh downloadDataFromMaui.py
30 5 * * * /maui/cron.sh importCacheToDatabase.py
0 6 * * * /maui/cron.sh uploadCacheToAwsS3.py
0 6 * * * /maui/cron.sh calculateTarifDetailsWithGpt.py
#!/bin/bash
# Dieser Wrapper wechselt ins Verzeichnis /maui/commands und startet das
# gewünschte Python-Skript (mit python3), sofern nicht bereits eine Instanz
# dieses Skripts läuft. Gleichzeitig werden alle Ausgaben in zwei getrennten
# Logfiles im Verzeichnis /maui/logs abgelegt, wobei jedes Skript einen
# eigenen Unterordner erhält (benannt nach dem Skriptnamen ohne Erweiterung)
# und die Logfiles die Namen im Format
# - L_yyyymmdd-hhiiss.txt für die Standardausgabe,
# - E_yyyymmdd-hhiiss.err für die Fehlermeldung
# tragen. Logfiles, die älter als 24 Stunden (1440 Minuten) sind, werden
# automatisch gelöscht.
#--- Parameterprüfung ---
# In dieser Abfrage wird überprüft, ob mindestens ein Parameter übergeben wurde.
if [ "$#" -lt 1 ]; then
# Hier wird ein Hinweis ausgegeben, wie dieses Skript zu nutzen ist, wenn nicht
# genügend Parameter übergeben wurden.
echo "Usage: $0 <jobfilename> [arguments...]"
# Dieser Befehl beendet das Skript mit einem Fehlercode.
exit 1
fi
# Diese Variable speichert den ersten übergebenen Parameter als Namen des
# Python-Skripts.
jobname="$1"
# Dieser Befehl entfernt den ersten Parameter aus der Parameterliste, damit
# weitere Argumente optional weiterverarbeitet werden können.
shift
#--- Arbeitsverzeichnis und Log-Verzeichnis festlegen ---
# Diese Variable legt das Arbeitsverzeichnis fest, in dem sich die
# Python-Skripte befinden.
WORKDIR="/maui/commands"
# Diese Variable legt das Hauptverzeichnis für die Logdateien fest.
LOG_ROOT="/maui/logs"
# Diese Variable ermittelt aus dem übergebenen Skriptnamen
# (z. B. rawFromBloomberg.py) den Basisteil (rawFromBloomberg).
job_base=$(basename "$jobname" .py)
# Diese Variable bildet den Pfad für das individuellen Logverzeichnis, basierend
# auf dem Basisteil des Skriptnamens.
LOG_DIR="$LOG_ROOT/$job_base"
# Dieser Befehl stellt sicher, dass das Haupt-Logverzeichnis und das Verzeichnis
# für das aktuelle Skript existieren, und legt sie gegebenenfalls an.
mkdir -p "$LOG_DIR"
# Dieser Befehl findet und löscht alle Logdateien im spezifischen Verzeichnis,
# die älter als 24 Stunden (1440 Minuten) sind.
find "$LOG_DIR" -type f -mmin +1440 -delete
#--- Prozessüberprüfung ---
# Diese Variable speichert die Prozess-ID des aktuell ausgeführten Skripts,
# damit es sich nicht selbst erkennt.
current_pid=$$
# Diese Variable hält den Namen dieses Wrapperskripts (cron.sh), um ihn ebenfalls
# von der Prozessliste auszuschließen.
wrapper_name=$(basename "$0")
# Diese Variable legt fest, nach welchem exakten Aufrufmuster
# (python3 <jobname>) in der Prozessliste gesucht werden soll.
pattern="python3 $jobname"
# In dieser Variable werden alle zum Muster passenden Prozess-IDs gespeichert,
# wobei Zeilen des Wrappers und greps ausgeschlossen werden.
running=$(ps ax -o pid,cmd | grep "$pattern" | grep -v grep | grep -v "$wrapper_name" | awk '{print $1}')
# Diese Abfrage prüft, ob ein passender Prozess bereits läuft.
if [ -n "$running" ]; then
# Hier wird der Nutzer informiert, dass der entsprechende Job bereits ausgeführt
# wird, und ein erneuter Start verhindert.
echo "Job '$jobname' läuft bereits (PID(s): $running). Abbruch."
# Das Skript wird hier mit Exit-Code 0 (ohne Fehler) beendet, um keine neue
# Instanz zu starten.
exit 0
fi
#--- Logging vorbereiten und Job starten ---
# Diese Variable erzeugt einen Zeitstempel im Format yyyymmdd-hhiiss
# (z. B. 20250413-114530), um eindeutige Logdateien zu erstellen.
timestamp=$(date "+%Y%m%d-%H%M%S")
# Diese Variable bildet den vollständigen Pfad zur Logdatei für die Standardausgabe.
STDOUT_LOG="$LOG_DIR/L_${timestamp}.txt"
# Diese Variable bildet den vollständigen Pfad zur Logdatei für die Fehlermeldungen.
ERROR_LOG="$LOG_DIR/E_${timestamp}.err"
# Dieser Befehl wechselt in das festgelegte Arbeitsverzeichnis oder bricht mit
# Fehlermeldung ab, falls es nicht erreichbar ist.
cd "$WORKDIR" || { echo "Arbeitsverzeichnis $WORKDIR nicht erreichbar." >&2; exit 1; }
# Dieser Befehl führt das Python-Skript aus und leitet stdout in das L_-Logfile
# und stderr in das E_-Logfile um.
python3 "$jobname" "$@" > "$STDOUT_LOG" 2> "$ERROR_LOG"
# Fehler senden bei Inhalt: Jobname, Zeilenumbruch, Fehler
if [ -s "$ERROR_LOG" ]; then
payload="$jobname
$(<"$ERROR_LOG")"
curl -s -X POST https://ntfy.sh/itmaxDebug -d "$payload"
fi
\ No newline at end of file
import sys
sys.path.append("..")
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Datums- und Zeitfunktionen bereit, die für diverse Berechnungen benötigt werden.
from datetime import datetime, date, timedelta, time, timezone
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Funktionen für den Zugriff auf das Betriebssystem bereit.
import os
# Leertaste vor dem Kommentar.
# Dieses Modul dient zur Verarbeitung von XML-Daten.
import xml.etree.ElementTree as ET
# Leertaste vor dem Kommentar.
# Dieses Modul stellt Klassen und Funktionen für eine Flask-Webanwendung bereit.
from flask import Flask, Response, request, jsonify
# Leertaste vor dem Kommentar.
# Dieses Modul erzeugt RSS-Feeds aus Datenstrukturen.
from feedgen.feed import FeedGenerator
# Leertaste vor dem Kommentar.
# Diese Klasse stellt eine Verbindung zur MySQL-Datenbank her und verwaltet Sessions.
from manager.MysqlManager import MysqlManager
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert Rohdaten von News, die aus Obsidian stammen.
from models.rawnewsdata_rane import RawNewsDataRane
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert wirtschaftliche News, die ausgewertet werden.
from models.economicnews_ecne import EconomicNewsEcne
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert einen Wirtschaftsdatenkalender.
from models.economiccalendar_ecca import EconomicCalendarEcca
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert KI-generierte News-Daten.
from models.ainewsdata_aine import AinewsDataAine
# Leertaste vor dem Kommentar.
# Diese Klasse repräsentiert einen Puffer zur Priorisierung von News.
from models.prioritybuffer_prbu import PrioritybufferPrbu
# Leertaste vor dem Kommentar.
# Diese Klasse stellt Reporting-Funktionen zur Verfügung, um HTML-Berichte zu erzeugen.
from manager.ReportingManager import ReportingManager
# Leertaste vor dem Kommentar.
# Diese Funktionen stellen Operatoren bereit, um komplexe SQL-Filter zu bauen.
from sqlalchemy import or_, and_
# Leertaste vor dem Kommentar.
# Diese Variable erzeugt eine neue Flask-Anwendung, die als Webserver agiert.
app = Flask(__name__)
# Leertaste vor dem Kommentar.
# Diese Abfrage startet den Flask-Webserver nur, wenn dieses Skript direkt ausgeführt wird.
if __name__ == '__main__':
# Leertaste vor dem Kommentar.
# Diese Zeile startet die Flask-Anwendung auf allen Schnittstellen und Port 80.
app.run(host='0.0.0.0', port=80)
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from models._system import Base from models._system import Base
from models.basegroup_bgro import BasegroupBgro
from models.option_opti import OptionOpti from models.option_opti import OptionOpti
class BaseBase(Base): class BaseBase(Base):
__tablename__ = 'base_base' __tablename__ = 'base_base'
id_base = Column(Integer, primary_key=True, autoincrement=True) id_base = Column(Integer, primary_key=True, autoincrement=True)
basegroup_base = Column(Integer, ForeignKey("basegroup_bgro.id_bgro"))
provider_base = Column(String(255), nullable=False) provider_base = Column(String(255), nullable=False)
providercode_base= Column(String(255)) providercode_base= Column(String(255))
name_base = Column(String(255), nullable=False) name_base = Column(String(255), nullable=False)
alias_base = Column(String(255)) alias_base = Column(String(255))
network_base = Column(Integer, nullable=False)
type_base = Column(Integer, nullable=False)
flyerurl_base = Column(String(255)) flyerurl_base = Column(String(255))
piburl_base = Column(String(255)) piburl_base = Column(String(255))
details_base = Column(JSON) # enthält das von GPT extrahierte Tarif‑JSON details_base = Column(JSON) # enthält das von GPT extrahierte Tarif‑JSON
created_base = Column(DateTime, nullable=False) created_base = Column(DateTime, nullable=False)
updated_base = Column(DateTime, nullable=False) updated_base = Column(DateTime, nullable=False)
basegroup = relationship("BasegroupBgro", back_populates="bases")
deals = relationship("DealDeal", back_populates="base") deals = relationship("DealDeal", back_populates="base")
options = relationship("OptionOpti", back_populates="base") options = relationship("OptionOpti", back_populates="base")
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from models._system import Base
class BasegroupBgro(Base):
__tablename__ = 'basegroup_bgro'
id_bgro = Column(Integer, primary_key=True, autoincrement=True)
name_bgro = Column(String(255), nullable=False)
created_bgro = Column(DateTime, nullable=False)
updated_bgro = Column(DateTime, nullable=False)
bases = relationship("BaseBase", back_populates="basegroup")
This diff is collapsed.
This diff is collapsed.
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 1,
"id": "531d8b07-8f2f-4ef6-b92a-d4bf3364e166", "id": "531d8b07-8f2f-4ef6-b92a-d4bf3364e166",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"eyJraWQiOiJpZUFmc2p0UDJLdDhVM2F2VHlGVEkiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3R5cGUiOiJhcHBsaWNhdGlvbiIsIm9yZyI6Ik1EIiwiY2xpZW50X2lkIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJjdXN0b21fZGF0YSI6eyJyZXNwb25zaWJsZVRlYW0iOiJCQ1MgTW9iaWxlIn0sImF6cCI6IjhWeWpiUVp5VFZkeDJUMlVPNm1BM1pURWVpb2RIY3AtIiwic2NvcGUiOiJhZ3JlZW1lbnRUZXJtU2hlZXRzOndyaXRlIG1hdWkudnZpOnJlYWQgY3VzdG9tZXJQcm9kdWN0OnJlYWQgcHJvZHVjdE9mZmVyaW5nOnJlYWQgemFwLmNvbnRyYWN0OnJlYWQgemFwLmNvbnRyYWN0OndyaXRlIHN0cyIsImlhdCI6MTc0NTk5OTY0MCwic3ViIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJpc3MiOiJodHRwczovL3N0cy5tZC5kZS92MS9vaWRjLyIsImp0aSI6ImE4bnR5OGlZZEZpRDdfZzEwZnVaTCIsImV4cCI6MTc0NjA4NjA0MH0.z-LLmQHq1Pb2PNFMjLhmxMMhS9PXZoKgfBSWV-brTdIAZs4uYoUHqVTdf2UcQnFWTFuUyguoQG7r3oKANOiGQKGXiMQZrljZvi0Lb_M-ExVjtBxoVXfyea-aSl9I7qAhyq_Ir7x62RyEJzX0A47y3ZxYT8bGsLd6ApE4e1LnvF8f5QEGRNRqhHBdvlGHYAY2NbJxj2TQrzvwdPPsJONiqJWgt7Ab7OErB39qDJY91ZgQYyUQNErAEhqYlJCZu5HgC_AsX-QgH9u4FQbee6LZL-mJ_dHwWbq5c55dLET6cxxBq4h2Ha3E8fOw2tV3wInaTeHOwGmypGGVNog3el7Tgw\n", "eyJraWQiOiJpZUFmc2p0UDJLdDhVM2F2VHlGVEkiLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3R5cGUiOiJhcHBsaWNhdGlvbiIsIm9yZyI6Ik1EIiwiY2xpZW50X2lkIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJjdXN0b21fZGF0YSI6eyJyZXNwb25zaWJsZVRlYW0iOiJCQ1MgTW9iaWxlIn0sImF6cCI6IjhWeWpiUVp5VFZkeDJUMlVPNm1BM1pURWVpb2RIY3AtIiwic2NvcGUiOiJhZ3JlZW1lbnRUZXJtU2hlZXRzOndyaXRlIG1hdWkudnZpOnJlYWQgY3VzdG9tZXJQcm9kdWN0OnJlYWQgcHJvZHVjdE9mZmVyaW5nOnJlYWQgemFwLmNvbnRyYWN0OnJlYWQgemFwLmNvbnRyYWN0OndyaXRlIHN0cyIsImlhdCI6MTc0NjAxOTcwOCwic3ViIjoiOFZ5amJRWnlUVmR4MlQyVU82bUEzWlRFZWlvZEhjcC0iLCJpc3MiOiJodHRwczovL3N0cy5tZC5kZS92MS9vaWRjLyIsImp0aSI6InJ1OF9GaUYtZi1GUS00eFdIV2JuSyIsImV4cCI6MTc0NjEwNjEwOH0.BiISXTvunrU_v6KR98pzQ4GZLpEl2f_fSNfaIYDLWq1BsKTeNS7f4hUsBLK91yLGIvyY1vfOuTOeLJeYodX_5CFpQOvx97nKlg46_4g9uqiHYiyd6ehGyAnEp4W-DT_mwJ8PgIqprFeN9Tw5_a5WYMmCZKfMwPP940werC90r0iabClML_J-56_DT8NWN_bm_EUPNrpyWRUb65WCphmbutqbFMc8wBCF4xtwI5VD9v2Nd9sXbFs8TevvX-Weg8WdIVQeQimiigvP1uEEiVimybcK8_Lav3PcDkVcQJ5YbtAx8INIGImAQckeCR7Bvd8mdwdrqc9YWa_oEPg_X89uoA\n",
"---\n", "---\n",
"{'error': [], 'pciId': 'YLXM-34JW-L1Q4', 'pciPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragsinformationen.pdf', 'pcsPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragszusammenfassung.pdf'}\n", "{'error': [], 'pciId': '0DRR-IY89-V1MT', 'pciPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragsinformationen.pdf', 'pcsPdf': 'https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragszusammenfassung.pdf'}\n",
"---\n", "---\n",
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragsinformationen.pdf\n", "https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragsinformationen.pdf\n",
"https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/YLXM-34JW-L1Q4/095402_vertragszusammenfassung.pdf\n" "https://media.mdm.freenet-group.de/downloads/vorvertragliche-dokumente/0DRR-IY89-V1MT/152831_vertragszusammenfassung.pdf\n"
] ]
} }
], ],
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 2,
"id": "4d796b32-df5d-4ba5-a062-4d96b12db2b3", "id": "4d796b32-df5d-4ba5-a062-4d96b12db2b3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"---\n", "---\n",
"PCS PDF erfolgreich heruntergeladen und gespeichert unter: ../cache/YLXM-34JW-L1Q4_3877325_pcs.pdf\n", "PCS PDF erfolgreich heruntergeladen und gespeichert unter: ../cache/0DRR-IY89-V1MT_3877325_pcs.pdf\n",
"---\n" "---\n"
] ]
} }
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
jupyter lab jupyter lab
docker build --platform linux/amd64 -t obsidian:latest . docker build --platform linux/amd64 -t maui:latest .
# dev # dev
docker run -it -v ./commands:/obsidian/commands -v ./cache:/obsidian/cache -v ./config:/obsidian/config -v ./manager:/obsidian/manager -v ./models:/obsidian/models -v ./files:/obsidian/files -p 80:80 obsidian:latest /bin/bash docker run -it -v ./commands:/maui/commands -v ./cache:/maui/cache -v ./config:/maui/config -v ./manager:/maui/manager -v ./models:/maui/models -p 80:80 maui:latest /bin/bash
# detached
docker run -it -d obsidian:latest
# ecr # ecr
aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 181802255479.dkr.ecr.eu-central-1.amazonaws.com aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 181802255479.dkr.ecr.eu-central-1.amazonaws.com
......
# Documentation
## Base (base_base)
In dieser Tabelle sind die grundlegenden Tarife gespeichert, die als Ausgangsbasis für das gesamte Datenbankschema dienen. Die hinterlegten Tarife bilden das Fundament, auf dem alle weiteren Strukturen aufbauen.
Beispielhafte Tarife:
```
Green Allnet Flat
Green Allnet Flat mit Handy 5
Green Allnet Flat mit Handy 10
```
### id_base
Dieser Primärschlüssel identifiziert jeden Tarif eindeutig in der Tabelle.
### basegroup_base
Dieses optionale Feld dient als Fremdschlüssel und verweist auf die Tabelle **Basegroup (basegroup_bgro)**, wodurch eine thematische Gruppierung der Tarife ermöglicht wird.
### provider_base
Hier wird der Name des Providers gespeichert, beispielsweise "Freenet" oder "Klarmobil".
### providercode_base
Ein optionales Feld, in dem ein zusätzlicher Provider-Code (Fremdschlüssel) hinterlegt werden kann.
### name_base
Dieses Feld enthält den offiziellen Tarifnamen, wie er vom Provider vorgegeben wird.
### alias_base
Hier kann ein alternativer Name definiert werden, der den offiziellen Tarifnamen ersetzt.
### network_base
In diesem Feld wird das verwendete Funknetz definiert.
**Mögliche Werte:**
- **1**: D1/Telekom
- **2**: D2/Vodafone
- **4**: O2/Telefonica
### type_base
Dieses Feld legt den Tariftyp fest.
**Mögliche Werte:**
- **0**: Unbekannt
- **1**: Voice
- **2**: Daten
### flyerurl_base
Dies ist die URL zum Tarifflyer.
### piburl_base
Dies ist die URL zum Produktinformationsblatt.
### details_base
Dies sind alle Tarifdetails die vom Provider vorgegeben werden (als JSON).
### created_base
Der Zeitstempel, der angibt, wann der Datensatz erstellt wurde.
### updated_base
Der Zeitstempel, an dem der Datensatz zuletzt aktualisiert wurde.
---
## Basegroup (basegroup_bgro)
Diese Tabelle fasst Tarife in Gruppen zusammen, um eine übersichtliche Struktur zu ermöglichen. Die Gruppierung hilft dabei, ähnliche Tarife gemeinsam zu verwalten.
Beispielhafte Tarifgruppen:
```
Red Allnet Flat Tarifgruppe
Green Allnet Flat Tarifgruppe
Blue Allnet Flat Tarifgruppe
```
### id_bgro
Der Primärschlüssel der Tabelle, der jede Tarifgruppe eindeutig identifiziert.
### name_bgro
Hier wird der Name der Tarifgruppe festgelegt.
### created_bgro
Der Zeitpunkt, zu dem die Tarifgruppe erstellt wurde.
### updated_bgro
Der Zeitpunkt, zu dem die Tarifgruppe zuletzt aktualisiert wurde.
---
## Deal (deal_deal)
In dieser Tabelle werden zeitlich befristete Aktionen erfasst. Jede Aktion, jeder Deal verknüpft einen Basis-Tarif aus der Tabelle **Base (base_base)** mit einer Provisiongruppe und definiert spezielle Konditionen.
### id_deal
Der Primärschlüssel der Tabelle, der jede Aktion eindeutig identifiziert.
### base_deal
Dieses Feld ist ein Fremdschlüssel, der auf einen Tarif in der Tabelle **Base (base_base)** verweist.
### provisiongroup_deal
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_deal
Ein optionales Feld, das einen zusätzlichen Provider-Code (Fremdschlüssel) speichern kann.
### alias_deal
Ein optionaler, alternativer Name des Deals.
### price_deal
In diesem Feld wird der Preis des Deals als Dezimalwert festgehalten (Netto).
### starts_deal
Das Datum und die Uhrzeit, ab wann der Deal aktiv wird.
### stops_deal
Ein optionales Datum und eine Uhrzeit, bis zu denen der Deal gültig ist.
### provision1_deal bis provision4_deal
Diese Felder enthalten verschiedene Provisionswerte für für die Aktion.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_deal
Der Zeitpunkt, zu dem der Deal-Datensatz erstellt wurde.
### updated_deal
Der Zeitpunkt, zu dem der Deal zuletzt aktualisiert wurde.
---
## Option (option_opti)
Diese Tabelle erfasst spezifische Optionen zu Tarifen, die zusätzliche Leistungen oder Konfigurationen darstellen. Optionen ergänzen die Basistarife und ermöglichen erweiterte Angebotsvarianten.
### id_opti
Der Primärschlüssel der Tabelle, der jede Option eindeutig identifiziert.
### base_opti
Dieses Feld fungiert als Fremdschlüssel und verweist auf den zugehörigen Tarif in der Tabelle **Base (base_base)**.
### provisiongroup_opti
Dieses Feld definiert, unter welcher Provisionsgruppe dieser Tarif verwaltet wird.
### providercode_opti
Ein optionales Feld zur Speicherung eines zusätzlichen Provider-Codes.
### providercategory_opti
Dieses optionale Feld dient der Kategorisierung der Option (Providerkategorie).
### name_opti
Das Feld enthält den offiziellen Namen der Option, wie er vom Provider vorgegeben wird.
### alias_opti
Ein optionaler Alias, der den offiziellen Namen ergänzen oder ersetzen kann.
### price_opti
In diesem Feld wird der Preis der Option als Dezimalwert festgehalten (Netto).
### starts_opti
Das Datum und die Uhrzeit, ab wann die Option aktiv wird.
### stops_opti
Ein optionales Datum und eine Uhrzeit, bis zu denen die Option gültig ist.
### provision1_opti bis provision4_opti
Diese Felder enthalten verschiedene Provisionswerte für für die Option.
Alle Provisionen zusammengerechnet ergeben die Gesamtprovision.
### created_opti
Der Zeitpunkt, zu dem der Options-Datensatz erstellt wurde.
### updated_opti
Der Zeitpunkt, zu dem der Options-Datensatz zuletzt aktualisiert wurde.
---
## Provisiongroup (provisiongroup_pgro)
In dieser Tabelle werden Provisiongruppen verwaltet, die sowohl in Deals als auch in Optionen Anwendung finden. Eine Provisiongruppe legt fest, wie viel Provision prozentual für den Vertrieb freigegeben wird.
### id_pgro
Der Primärschlüssel der Tabelle, der jede Provisiongruppe eindeutig identifiziert.
### name_pgro
Hier wird der Name der Provisiongruppe gespeichert.
### percent_pgro
Dieser Dezimalwert legt den Provisionsprozentsatz fest. Standardmäßig beträgt dieser 0.00.
### created_pgro
Der Zeitpunkt, zu dem der Datensatz der Provisiongruppe erstellt wurde.
### updated_pgro
Ein optionales Feld, das den Zeitpunkt der letzten Aktualisierung der Provisiongruppe festhält.
No preview for this file type
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment