Initial import: Carteasy Shop (Dev-Stand)

This commit is contained in:
Thomas Bartelt
2026-04-20 01:03:43 +02:00
commit d504f8a05d
3845 changed files with 761050 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
Chart Generation Service with Plotly
Generates modern, interactive charts from statistical data
"""
import json
import sys
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.colors as pc
def generate_chart(data, chart_config):
"""
Generate Plotly chart from statistical data
Args:
data: List of lists - first row is headers, rest is data
chart_config: Dict with chart configuration (title, type, etc.)
Returns:
HTML string with embedded Plotly chart
"""
if not data or len(data) < 2:
return "<div>Keine Daten verfügbar</div>"
headers = data[0]
rows = data[1:]
# Extract x-axis labels (first column)
x_labels = [str(row[0]) for row in rows]
# Prepare traces for each data series
fig = go.Figure()
# Color palette - modern and professional
colors = pc.qualitative.Plotly
# Create a trace for each column (except first which is x-axis)
for i in range(1, len(headers)):
y_values = [row[i] if i < len(row) else 0 for row in rows]
fig.add_trace(go.Bar(
name=headers[i],
x=x_labels,
y=y_values,
marker_color=colors[i % len(colors)],
hovertemplate='<b>%{x}</b><br>%{fullData.name}: %{y}<extra></extra>'
))
# Update layout with modern styling
fig.update_layout(
title={
'text': chart_config.get('title', 'Statistik'),
'x': 0.5,
'xanchor': 'center',
'font': {'size': 20, 'color': '#333'}
},
barmode='group',
plot_bgcolor='rgba(240, 240, 240, 0.5)',
paper_bgcolor='white',
font={'family': 'Arial, sans-serif', 'size': 12, 'color': '#333'},
hovermode='x unified',
legend={
'orientation': 'h',
'yanchor': 'bottom',
'y': -0.2,
'xanchor': 'center',
'x': 0.5,
'bgcolor': 'rgba(255, 255, 255, 0.8)',
'bordercolor': '#ccc',
'borderwidth': 1
},
xaxis={
'title': headers[0] if headers else '',
'showgrid': False,
'showline': True,
'linecolor': '#ccc'
},
yaxis={
'title': 'Anzahl',
'showgrid': True,
'gridcolor': 'rgba(200, 200, 200, 0.3)',
'showline': True,
'linecolor': '#ccc'
},
margin={'l': 60, 'r': 40, 't': 80, 'b': 100},
height=500
)
# Generate HTML with Plotly CDN
html = fig.to_html(
include_plotlyjs='cdn',
div_id='plotly-chart',
config={
'displayModeBar': True,
'displaylogo': False,
'modeBarButtonsToRemove': ['pan2d', 'lasso2d', 'select2d'],
'responsive': True
}
)
return html
def generate_line_chart(data, chart_config):
"""
Generate Plotly line chart for time series data
"""
if not data or len(data) < 2:
return "<div>Keine Daten verfügbar</div>"
headers = data[0]
rows = data[1:]
x_labels = [str(row[0]) for row in rows]
fig = go.Figure()
colors = pc.qualitative.Plotly
# Create line traces
for i in range(1, len(headers)):
y_values = [row[i] if i < len(row) else 0 for row in rows]
fig.add_trace(go.Scatter(
name=headers[i],
x=x_labels,
y=y_values,
mode='lines+markers',
line={'color': colors[i % len(colors)], 'width': 3},
marker={'size': 8},
hovertemplate='<b>%{x}</b><br>%{fullData.name}: %{y}<extra></extra>'
))
# Update layout - NO fixed y-axis minimum for line charts
fig.update_layout(
title={
'text': chart_config.get('title', 'Statistik Verlauf'),
'x': 0.5,
'xanchor': 'center',
'font': {'size': 20, 'color': '#333'}
},
plot_bgcolor='rgba(240, 240, 240, 0.5)',
paper_bgcolor='white',
font={'family': 'Arial, sans-serif', 'size': 12, 'color': '#333'},
hovermode='x unified',
legend={
'orientation': 'h',
'yanchor': 'bottom',
'y': -0.2,
'xanchor': 'center',
'x': 0.5,
'bgcolor': 'rgba(255, 255, 255, 0.8)',
'bordercolor': '#ccc',
'borderwidth': 1
},
xaxis={
'title': headers[0] if headers else '',
'showgrid': False,
'showline': True,
'linecolor': '#ccc'
},
yaxis={
'title': 'Anzahl',
'showgrid': True,
'gridcolor': 'rgba(200, 200, 200, 0.3)',
'showline': True,
'linecolor': '#ccc',
'rangemode': 'normal', # Auto-scale, don't force to 0
'autorange': True
},
margin={'l': 60, 'r': 40, 't': 80, 'b': 100},
height=500
)
html = fig.to_html(
include_plotlyjs='cdn',
div_id='plotly-line-chart',
config={
'displayModeBar': True,
'displaylogo': False,
'modeBarButtonsToRemove': ['pan2d', 'lasso2d', 'select2d'],
'responsive': True
}
)
return html
def main():
"""
Main entry point - expects JSON input via stdin
Expected format:
{
"data": [[headers], [row1], [row2], ...],
"config": {
"title": "Chart Title",
"type": "bar" or "line",
"period": "year", "month", or "day"
}
}
"""
try:
# Read JSON from stdin
input_data = json.load(sys.stdin)
data = input_data.get('data', [])
config = input_data.get('config', {})
# Generate chart based on type
chart_type = config.get('type', 'bar')
if chart_type == 'line':
html = generate_line_chart(data, config)
else:
html = generate_chart(data, config)
# Output HTML
print(html)
except Exception as e:
error_html = f"""
<div style="padding: 20px; background: #fee; border: 1px solid #fcc; border-radius: 4px;">
<h3 style="color: #c33; margin: 0 0 10px 0;">Chart-Generierung fehlgeschlagen</h3>
<p style="margin: 0; color: #666;">Fehler: {str(e)}</p>
</div>
"""
print(error_html)
sys.exit(1)
if __name__ == '__main__':
main()
+6
View File
@@ -0,0 +1,6 @@
[database]
host = localhost
user = tbapy
password = 9%%0H32ryj_N9%%0H32ryj
database = webshop-sql
autocommit = true
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Image Processing Service für Intelectra WebShop
EINFACH & SCHNELL - erstellt nur Thumbnails!
Kompatibel mit Legacy PHP item.class.php
"""
import json
import sys
import os
from PIL import Image
import logging
# Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/www/vhosts/intelectra.de/httpdocs/logs/image_processing.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger('image_processing')
class ImageProcessingService:
"""Einfacher Thumbnail-Generator - keine DB, keine Komplexität!"""
def __init__(self, config):
"""
config = {
'source_file': '/full/path/to/original.jpg',
'watermark': 0 oder 1,
'shop_system': 'intelectra_shop',
'document_root': '/var/www/...',
'image_sizes': [['thumb_', 90], ['overview_', 150], ...] # Von PHP übergeben!
}
"""
self.source_file = config.get('source_file')
self.watermark_enabled = config.get('watermark', 0) == 1
self.document_root = config.get('document_root', '')
# Bildgrößen von PHP übernehmen (aus DB-Konfiguration!)
# PHP übergibt: [['overview_', 150], ['thumb_', 90], ['details_', 330], ['detaildefault_', 600]]
php_image_sizes = config.get('image_sizes', [])
# In Python-Format konvertieren: [('prefix', size), ...]
self.image_sizes = []
# Original resized (falls konfiguriert)
self.image_sizes.append(('', 800))
# Thumbnails von PHP config
for img_config in php_image_sizes:
if isinstance(img_config, (list, tuple)) and len(img_config) >= 2:
prefix = img_config[0]
size = int(img_config[1])
self.image_sizes.append((prefix, size))
logger.info(f"Service initialized for: {self.source_file}")
logger.info(f"Image sizes: {self.image_sizes}")
def process(self):
"""Hauptfunktion: Erstellt alle Thumbnails"""
try:
if not os.path.exists(self.source_file):
logger.error(f"Source file not found: {self.source_file}")
return False
# Original-Bild öffnen
with Image.open(self.source_file) as img:
# Original Format beibehalten (JPG/PNG/GIF)
img_format = img.format
# Alle Größen generieren
for prefix, size in self.image_sizes:
self.create_thumbnail(img, prefix, size, img_format)
logger.info(f"✅ Successfully processed: {self.source_file}")
return True
except Exception as e:
logger.error(f"❌ Error processing {self.source_file}: {str(e)}")
return False
def create_thumbnail(self, img, prefix, max_size, img_format):
"""Erstellt ein Thumbnail mit gegebener Größe"""
try:
# Dateiname mit Präfix
source_dir = os.path.dirname(self.source_file)
source_filename = os.path.basename(self.source_file)
target_filename = f"{prefix}{source_filename}"
target_path = os.path.join(source_dir, target_filename)
# Kopie erstellen für Resize
thumb = img.copy()
# Proportional auf max_size verkleinern (behält Aspect Ratio!)
thumb.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Speichern im Original-Format
if img_format == 'JPEG':
thumb.save(target_path, 'JPEG', quality=85, optimize=True)
elif img_format == 'PNG':
thumb.save(target_path, 'PNG', optimize=True)
elif img_format == 'GIF':
thumb.save(target_path, 'GIF')
else:
thumb.save(target_path)
logger.debug(f"Created: {target_filename} ({max_size}px)")
except Exception as e:
logger.warning(f"Could not create {prefix} thumbnail: {str(e)}")
def main():
"""Entry Point - wird von PHP aufgerufen"""
try:
if len(sys.argv) < 2:
logger.error("No config provided!")
sys.exit(1)
# JSON Config von PHP
config_json = sys.argv[1]
config = json.loads(config_json)
logger.info("=" * 60)
logger.info("🚀 Image Processing Service STARTED")
logger.info(f"Source: {config.get('source_file', 'unknown')}")
# Service starten
service = ImageProcessingService(config)
success = service.process()
if success:
logger.info("✅ Processing completed successfully")
sys.exit(0)
else:
logger.error("❌ Processing failed")
sys.exit(1)
except Exception as e:
logger.error(f"FATAL ERROR: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
+433
View File
@@ -0,0 +1,433 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Import-Skript für Intelectra Webshop
Dieses Skript wird remote per SSH von der GUI aufgerufen.
Es erwartet eine CSV-Datei als erstes Argument.
Mit --passendwie wird die krempl_passendwie Tabelle aktualisiert.
"""
import csv
import pymysql
import os
import sys
import argparse
from datetime import datetime
from configparser import ConfigParser
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'db_config.ini')
REQUIRED_COLUMNS = {
'id', 'altenr', 'originalnummer', 'navisionid', 'zolltarifnummer',
'marke', 'ersatzteilvertreiber', 'ean', 'ersatzteilvorwaerts',
'ersatzteilvorwaertsnavisionid'
}
REQUIRED_COLUMNS_PASSENDWIE = {
'id', 'navisionid', 'vertreiberid', 'vertreiber', 'bestellcode'
}
REQUIRED_COLUMNS_GERAETE = {
'id', 'nr', 'marke', 'typ', 'zusatzNummer', 'modellBezeichnung', 'produktionsstart', 'produktionsende',
'wgtext1', 'wgtext2', 'zusatz', 'bezeichnungoriginal', 'typDE', 'typFR'
}
REQUIRED_COLUMNS_MAPPING = {
'ersatzteil', 'geraet'
}
def load_db_config():
parser = ConfigParser()
parser.read(CONFIG_PATH)
config = parser['database']
return {
'host': config.get('host', 'localhost'),
'user': config.get('user'),
'password': config.get('password', ''),
'database': config.get('database', 'webshop-sql'),
'autocommit': True
}
def connect_to_db():
return pymysql.connect(**load_db_config())
def validate_headers(headers):
return REQUIRED_COLUMNS.issubset(set(headers))
def validate_geraete_headers(headers):
return REQUIRED_COLUMNS_GERAETE.issubset(set(headers))
def validate_mapping_headers(headers):
return REQUIRED_COLUMNS_MAPPING.issubset(set(headers))
def run_import(csv_path):
if not os.path.exists(csv_path):
print(f"[ERROR] Datei nicht gefunden: {csv_path}")
return
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f, delimiter=';')
if not validate_headers(reader.fieldnames):
print(f"[ERROR] CSV-Header ungültig: {reader.fieldnames}")
return
conn = connect_to_db()
cursor = conn.cursor()
backup_table(cursor)
cursor.execute("TRUNCATE TABLE krempl_art_22")
insert_sql = """
INSERT INTO krempl_art_22
(id, altenr, originalnummer, navisionid, zolltarifnummer, marke,
ersatzteilvertreiber, ean, ersatzteilvorwaerts, ersatzteilvorwaertsnavisionid)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
total = 0
batch = []
for row in reader:
try:
values = (
int(row['id']) if row['id'] else None,
row['altenr'],
row['originalnummer'],
int(row['navisionid']) if row['navisionid'] else None,
row['zolltarifnummer'],
row['marke'],
row['ersatzteilvertreiber'],
row['ean'],
row['ersatzteilvorwaerts'],
int(row['ersatzteilvorwaertsnavisionid']) if row['ersatzteilvorwaertsnavisionid'] else None
)
batch.append(values)
if len(batch) >= 500:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
batch.clear()
except Exception as e:
print(f"[WARN] Zeile übersprungen wegen Fehler: {e}")
continue
if batch:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
print(f"[OK] Import abgeschlossen. {total} Datensätze importiert.")
# Tabellen-Statistiken aktualisieren für bessere Query-Performance
print("[INFO] Aktualisiere Tabellen-Statistiken...")
cursor.execute("ANALYZE TABLE krempl_art_22")
print("[OK] Statistiken aktualisiert.")
cursor.close()
conn.close()
def backup_table(cursor):
name = f"krempl_art_22_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
cursor.execute(f"CREATE TABLE {name} AS SELECT * FROM krempl_art_22")
print(f"[INFO] Backup-Tabelle erstellt: {name}")
except Exception as e:
print(f"[WARN] Backup fehlgeschlagen: {e}")
def run_passendwie_import(csv_path):
if not os.path.exists(csv_path):
print(f"[ERROR] Datei nicht gefunden: {csv_path}")
return
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f, delimiter=';')
if not REQUIRED_COLUMNS_PASSENDWIE.issubset(set(reader.fieldnames)):
print(f"[ERROR] CSV-Header ungültig: {reader.fieldnames}")
return
conn = connect_to_db()
cursor = conn.cursor()
backup_passendwie_table(cursor)
insert_sql = """
INSERT INTO krempl_passendwie
(id, navisionid, vertreiberid, vertreiber, bestellcode)
VALUES (%s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
navisionid = VALUES(navisionid),
vertreiberid = VALUES(vertreiberid),
vertreiber = VALUES(vertreiber),
bestellcode = VALUES(bestellcode)
"""
total = 0
batch = []
for row in reader:
try:
values = (
int(row['id']) if row['id'] else None,
int(row['navisionid']) if row['navisionid'] else None,
row['vertreiberid'],
row['vertreiber'],
row['bestellcode']
)
batch.append(values)
if len(batch) >= 500:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
batch.clear()
except Exception as e:
print(f"[WARN] Zeile übersprungen wegen Fehler: {e}")
continue
if batch:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
print(f"[OK] PassendWie-Import abgeschlossen. {total} Datensätze importiert.")
# Tabellen-Statistiken aktualisieren für bessere Query-Performance
print("[INFO] Aktualisiere Tabellen-Statistiken...")
cursor.execute("ANALYZE TABLE krempl_passendwie")
print("[OK] Statistiken aktualisiert.")
cursor.close()
conn.close()
def backup_passendwie_table(cursor):
# 1. Alle bisherigen Backups suchen
cursor.execute("SHOW TABLES LIKE 'krempl_passendwie_backup_%'")
backups = [row[0] for row in cursor.fetchall()]
# 2. Nach Datum sortieren (Tabellenname enthält Zeitstempel)
backups.sort()
# 3. Wenn mehr als 2 Backups existieren, die ältesten löschen
while len(backups) >= 3:
to_delete = backups.pop(0)
try:
cursor.execute(f"DROP TABLE `{to_delete}`")
print(f"[INFO] Altes Backup gelöscht: {to_delete}")
except Exception as e:
print(f"[WARN] Konnte Backup nicht löschen: {e}")
# 4. Neues Backup anlegen
name = f"krempl_passendwie_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
cursor.execute(f"CREATE TABLE `{name}` AS SELECT * FROM krempl_passendwie")
print(f"[INFO] Backup-Tabelle erstellt: {name}")
except Exception as e:
print(f"[WARN] Backup fehlgeschlagen: {e}")
def run_geraete_import(csv_path):
if not os.path.exists(csv_path):
print(f"[ERROR] Datei nicht gefunden: {csv_path}")
return
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f, delimiter=';')
if not validate_geraete_headers(reader.fieldnames):
print(f"[ERROR] CSV-Header ungültig: {reader.fieldnames}")
return
conn = connect_to_db()
cursor = conn.cursor()
backup_geraete_table(cursor)
insert_sql = """
INSERT INTO krempl_geraete_22
(id, nr, marke, typ, zusatZNummer, modellBezeichnung, produktionsstart, produktionsende, wgtext1, wgtext2, zusatz, bezeichnungoriginal, typDE, typFR)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
nr = VALUES(nr),
marke = VALUES(marke),
typ = VALUES(typ),
zusatZNummer = VALUES(zusatZNummer),
modellBezeichnung = VALUES(modellBezeichnung),
produktionsstart = VALUES(produktionsstart),
produktionsende = VALUES(produktionsende),
wgtext1 = VALUES(wgtext1),
wgtext2 = VALUES(wgtext2),
zusatz = VALUES(zusatz),
bezeichnungoriginal = VALUES(bezeichnungoriginal),
typDE = VALUES(typDE),
typFR = VALUES(typFR)
"""
total = 0
batch = []
for row in reader:
try:
values = (
int(row['id']) if row['id'] else None,
row['nr'],
row['marke'],
row['typ'],
row['zusatzNummer'],
row['modellBezeichnung'],
row['produktionsstart'],
row['produktionsende'],
row['wgtext1'],
row['wgtext2'],
row['zusatz'],
row['bezeichnungoriginal'],
row['typDE'],
row['typFR']
)
batch.append(values)
if len(batch) >= 500:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
batch.clear()
except Exception as e:
print(f"[WARN] Zeile übersprungen wegen Fehler: {e}")
continue
if batch:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
print(f"[OK] ZusatzDatenOriginalteil-Import abgeschlossen. {total} Datensätze importiert.")
# Tabellen-Statistiken aktualisieren für bessere Query-Performance
print("[INFO] Aktualisiere Tabellen-Statistiken...")
cursor.execute("ANALYZE TABLE krempl_geraete_22")
print("[OK] Statistiken aktualisiert.")
cursor.close()
conn.close()
def backup_geraete_table(cursor):
# 1. Alle bisherigen Backups suchen
cursor.execute("SHOW TABLES LIKE 'krempl_geraete_22_backup_%'")
backups = [row[0] for row in cursor.fetchall()]
# 2. Nach Datum sortieren (Tabellenname enthält Zeitstempel)
backups.sort()
# 3. Wenn mehr als 2 Backups existieren, die ältesten löschen
while len(backups) >= 3:
to_delete = backups.pop(0)
try:
cursor.execute(f"DROP TABLE `{to_delete}`")
print(f"[INFO] Altes Backup gelöscht: {to_delete}")
except Exception as e:
print(f"[WARN] Konnte Backup nicht löschen: {e}")
# 4. Neues Backup anlegen
name = f"krempl_geraete_22_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
cursor.execute(f"CREATE TABLE `{name}` AS SELECT * FROM krempl_geraete_22")
print(f"[INFO] Backup-Tabelle erstellt: {name}")
except Exception as e:
print(f"[WARN] Backup fehlgeschlagen: {e}")
def run_mapping_import(csv_path):
if not os.path.exists(csv_path):
print(f"[ERROR] Datei nicht gefunden: {csv_path}")
return
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f, delimiter=';')
if not validate_mapping_headers(reader.fieldnames):
print(f"[ERROR] CSV-Header ungültig: {reader.fieldnames}")
return
conn = connect_to_db()
cursor = conn.cursor()
backup_mapping_table(cursor)
insert_sql = """
INSERT INTO krempl_artikelgeraet_22
(ersatzteil, geraet)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE
geraet = VALUES(geraet)
"""
total = 0
batch = []
for row in reader:
try:
values = (
int(row['ersatzteil']) if row['ersatzteil'] else None,
int(row['geraet']) if row['geraet'] else None
)
batch.append(values)
if len(batch) >= 500:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
batch.clear()
except Exception as e:
print(f"[WARN] Zeile übersprungen wegen Fehler: {e}")
continue
if batch:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
print(f"[OK] KremplMapping-Import abgeschlossen. {total} Datensätze importiert.")
# Tabellen-Statistiken aktualisieren für bessere Query-Performance
print("[INFO] Aktualisiere Tabellen-Statistiken...")
cursor.execute("ANALYZE TABLE krempl_artikelgeraet_22")
print("[OK] Statistiken aktualisiert.")
cursor.close()
conn.close()
def backup_mapping_table(cursor):
# 1. Alle bisherigen Backups suchen
cursor.execute("SHOW TABLES LIKE 'krempl_artikelgeraet_22_backup_%'")
backups = [row[0] for row in cursor.fetchall()]
# 2. Nach Datum sortieren (Tabellenname enthält Zeitstempel)
backups.sort()
# 3. Wenn mehr als 2 Backups existieren, die ältesten löschen
while len(backups) >= 3:
to_delete = backups.pop(0)
try:
cursor.execute(f"DROP TABLE `{to_delete}`")
print(f"[INFO] Altes Backup gelöscht: {to_delete}")
except Exception as e:
print(f"[WARN] Konnte Backup nicht löschen: {e}")
# 4. Neues Backup anlegen
name = f"krempl_artikelgeraet_22_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
cursor.execute(f"CREATE TABLE `{name}` AS SELECT * FROM krempl_artikelgeraet_22")
print(f"[INFO] Backup-Tabelle erstellt: {name}")
except Exception as e:
print(f"[WARN] Backup fehlgeschlagen: {e}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Import-Skript für Intelectra Webshop')
parser.add_argument('csv_file', help='Pfad zur CSV-Datei')
parser.add_argument('--passendwie', action='store_true', help='Import in krempl_passendwie Tabelle')
parser.add_argument('--geraete', action='store_true', help='Import in krempl_geraete_22 Tabelle')
parser.add_argument('--mapping', action='store_true', help='Import in krempl_artikelgeraet_22 Tabelle')
args = parser.parse_args()
print(f"[INFO] Starte Import für Datei: {args.csv_file}")
if args.mapping:
run_mapping_import(args.csv_file)
elif args.geraete:
run_geraete_import(args.csv_file)
elif args.passendwie:
run_passendwie_import(args.csv_file)
else:
run_import(args.csv_file)
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Smart Save Service für Intelectra WebShop
Speichert nur echte Änderungen - keine DELETE * mehr!
"""
import json
import sys
import pymysql
import logging
from configparser import ConfigParser
import os
from datetime import datetime
# Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/www/vhosts/intelectra.de/httpdocs/logs/item_save_service.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger('item_save_service')
class ItemSaveService:
"""Intelligenter Save-Service mit Change Detection"""
def __init__(self):
self.conn = self._connect_db()
self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)
def _connect_db(self):
"""DB-Connection aus config.ini"""
config_file = os.path.join(os.path.dirname(__file__), 'db_config.ini')
config = ConfigParser()
config.read(config_file)
return pymysql.connect(
host=config.get('database', 'host', fallback='localhost'),
user=config.get('database', 'user', fallback='tbapy'),
password=config.get('database', 'password', fallback='9%%0H32ryj_N9%%0H32ryj'),
database=config.get('database', 'database', fallback='webshop-sql'),
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def save_alternative_items(self, item_id, new_items):
"""
Speichert Alternativartikel - NUR Änderungen!
Type 3 = Alternativartikel
"""
try:
# 1. Aktuelle Items laden
self.cursor.execute(
"SELECT item_child_id FROM item_item_assign WHERE item_parent_id = %s AND type = 3 ORDER BY position",
(item_id,)
)
current_items = [row['item_child_id'] for row in self.cursor.fetchall()]
# 2. Änderungen erkennen
new_items = [int(x) for x in new_items if x] # Clean input
to_delete = set(current_items) - set(new_items)
to_add = set(new_items) - set(current_items)
# Reihenfolge geändert?
order_changed = (current_items != new_items) and not to_delete and not to_add
changes_made = False
# 3. Nur löschen was weg muss
if to_delete:
placeholders = ','.join(['%s'] * len(to_delete))
self.cursor.execute(
f"DELETE FROM item_item_assign WHERE item_parent_id = %s AND item_child_id IN ({placeholders}) AND type = 3",
[item_id] + list(to_delete)
)
logger.info(f"Deleted {len(to_delete)} alternative items from item {item_id}")
changes_made = True
# 4. Nur hinzufügen was neu ist
if to_add:
# Position berechnen - bei erster Zuweisung start bei 1
if not current_items:
start_pos = 1
else:
start_pos = len(current_items) - len(to_delete) + 1
values = []
for idx, child_id in enumerate(to_add):
values.append((item_id, child_id, 3, start_pos + idx))
self.cursor.executemany(
"INSERT INTO item_item_assign (item_parent_id, item_child_id, type, position) VALUES (%s, %s, %s, %s)",
values
)
logger.info(f"Added {len(to_add)} alternative items to item {item_id}")
changes_made = True
# 5. Reihenfolge updaten wenn nötig
if order_changed or to_add or to_delete:
for idx, child_id in enumerate(new_items):
self.cursor.execute(
"UPDATE item_item_assign SET position = %s WHERE item_parent_id = %s AND item_child_id = %s AND type = 3",
(idx + 1, item_id, child_id)
)
logger.info(f"Updated positions for item {item_id}")
changes_made = True
self.conn.commit()
return {
'success': True,
'changes': {
'deleted': len(to_delete),
'added': len(to_add),
'reordered': order_changed
},
'message': 'Changes saved' if changes_made else 'No changes detected'
}
except Exception as e:
self.conn.rollback()
logger.error(f"Error saving alternative items: {str(e)}")
return {
'success': False,
'error': str(e)
}
def save_accessory_items(self, item_id, new_items):
"""Type 1 = Zubehör - gleiche Logik"""
return self._save_related_items(item_id, new_items, 1)
def save_spare_parts(self, item_id, new_items):
"""Type 2 = Ersatzteile - gleiche Logik"""
return self._save_related_items(item_id, new_items, 2)
def _save_related_items(self, item_id, new_items, item_type):
"""Generische Methode für alle Verknüpfungstypen"""
type_names = {1: 'accessory', 2: 'spare_part', 3: 'alternative'}
try:
# Gleiche Logik wie save_alternative_items
self.cursor.execute(
"SELECT item_child_id FROM item_item_assign WHERE item_parent_id = %s AND type = %s ORDER BY position",
(item_id, item_type)
)
current_items = [row['item_child_id'] for row in self.cursor.fetchall()]
new_items = [int(x) for x in new_items if x]
to_delete = set(current_items) - set(new_items)
to_add = set(new_items) - set(current_items)
if to_delete:
placeholders = ','.join(['%s'] * len(to_delete))
self.cursor.execute(
f"DELETE FROM item_item_assign WHERE item_parent_id = %s AND item_child_id IN ({placeholders}) AND type = %s",
[item_id] + list(to_delete) + [item_type]
)
if to_add:
# Position berechnen - bei erster Zuweisung start bei 1
if not current_items:
start_pos = 1
else:
start_pos = len(current_items) - len(to_delete) + 1
values = [(item_id, child_id, item_type, start_pos + idx)
for idx, child_id in enumerate(to_add)]
self.cursor.executemany(
"INSERT INTO item_item_assign (item_parent_id, item_child_id, type, position) VALUES (%s, %s, %s, %s)",
values
)
# Update positions
for idx, child_id in enumerate(new_items):
self.cursor.execute(
"UPDATE item_item_assign SET position = %s WHERE item_parent_id = %s AND item_child_id = %s AND type = %s",
(idx + 1, item_id, child_id, item_type)
)
self.conn.commit()
logger.info(f"Saved {type_names[item_type]} items: {len(to_delete)} deleted, {len(to_add)} added")
return {
'success': True,
'type': type_names[item_type],
'changes': {
'deleted': len(to_delete),
'added': len(to_add)
}
}
except Exception as e:
self.conn.rollback()
logger.error(f"Error saving {type_names[item_type]} items: {str(e)}")
return {'success': False, 'error': str(e)}
def close(self):
"""Cleanup"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
def main():
"""CLI Entry Point für PHP shell_exec()"""
if len(sys.argv) < 2:
print(json.dumps({'error': 'No data provided'}))
sys.exit(1)
try:
# Parse input
data = json.loads(sys.argv[1])
item_id = data.get('item_id')
action = data.get('action', 'save_all')
service = ItemSaveService()
if action == 'save_alternatives':
items = data.get('items', [])
result = service.save_alternative_items(item_id, items)
elif action == 'save_accessories':
items = data.get('items', [])
result = service.save_accessory_items(item_id, items)
elif action == 'save_spare_parts':
items = data.get('items', [])
result = service.save_spare_parts(item_id, items)
elif action == 'save_all':
# Alle 3 Typen speichern
results = {}
if 'alternatives' in data:
results['alternatives'] = service.save_alternative_items(item_id, data['alternatives'])
if 'accessories' in data:
results['accessories'] = service.save_accessory_items(item_id, data['accessories'])
if 'spare_parts' in data:
results['spare_parts'] = service.save_spare_parts(item_id, data['spare_parts'])
result = {'success': True, 'results': results}
else:
result = {'error': f'Unknown action: {action}'}
service.close()
# Return JSON
print(json.dumps(result))
except Exception as e:
logger.error(f"Main error: {str(e)}")
print(json.dumps({'error': str(e)}))
sys.exit(1)
if __name__ == '__main__':
main()
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
KremplImport GUI - Desktop CSV Import Tool für Intelectra
Version: Full-Tunnel-Mode™
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import csv
import os
from dotenv import load_dotenv
from datetime import datetime
import paramiko
from scp import SCPClient
import threading
load_dotenv()
# DB-Konfiguration aus .env-Datei
DB_CONFIG = {
'host': os.getenv('DB_HOST', 'localhost'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', ''),
'database': os.getenv('DB_NAME', 'webshop-sql'),
'autocommit': True
}
SSH_CONFIG = {
'ssh_host': os.getenv('SSH_HOST', 'remotehost'),
'ssh_user': os.getenv('SSH_USER', 'user'),
'ssh_password': os.getenv('SSH_PASSWORD', 'password'),
'remote_path': os.getenv('REMOTE_PATH', '/tmp/artikel_Export.csv'),
'remote_script': os.getenv('REMOTE_SCRIPT', '/opt/import/import_script.py')
}
REQUIRED_COLUMNS = {
'id', 'altenr', 'originalnummer', 'navisionid', 'zolltarifnummer',
'marke', 'ersatzteilvertreiber', 'ean', 'ersatzteilvorwaerts',
'ersatzteilvorwaertsnavisionid'
}
class KremplImporterApp:
def __init__(self, master):
self.master = master
self.master.title("KremplImport GUI (Full-Tunnel-Mode™)")
self.master.geometry("700x550")
self.csv_path = tk.StringVar()
tk.Label(master, text="1. CSV-Datei auswählen:").pack(pady=(10, 0))
tk.Entry(master, textvariable=self.csv_path, width=80).pack(padx=10)
tk.Button(master, text="Durchsuchen", command=self.select_file).pack(pady=5)
tk.Button(master, text="CSV validieren", command=self.validate_csv).pack(pady=10)
tk.Button(master, text="Hochladen & Import starten (Remote)", command=self.upload_and_import).pack(pady=5)
tk.Label(master, text="Protokoll:").pack(pady=(10, 0))
self.log_output = scrolledtext.ScrolledText(master, height=20)
self.log_output.pack(padx=10, fill='both', expand=True)
def log(self, msg):
self.log_output.insert(tk.END, msg + '\n')
self.log_output.see(tk.END)
def select_file(self):
file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
if file_path:
self.csv_path.set(file_path)
self.log(f"Datei ausgewählt: {file_path}")
def validate_csv(self):
path = self.csv_path.get()
if not os.path.exists(path):
messagebox.showerror("Fehler", "Datei nicht gefunden!")
return False
try:
with open(path, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter=';')
headers = next(reader)
missing = REQUIRED_COLUMNS - set(headers)
if missing:
self.log(f"FEHLENDE SPALTEN: {', '.join(missing)}")
messagebox.showerror("CSV ungültig", "Spalten fehlen!")
return False
self.log(f"CSV validiert. Spalten ok: {', '.join(headers)}")
return True
except Exception as e:
self.log(f"Fehler beim Validieren: {e}")
return False
def upload_and_import(self):
if not self.validate_csv():
return
self.log("[🔄] Starte Tunnel-Upload-Prozess...")
threading.Thread(target=self.run_remote_process).start()
def run_remote_process(self):
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname=SSH_CONFIG['ssh_host'],
username=SSH_CONFIG['ssh_user'],
password=SSH_CONFIG['ssh_password']
)
with SCPClient(ssh.get_transport()) as scp:
scp.put(self.csv_path.get(), SSH_CONFIG['remote_path'])
self.log(f"[📤] Datei hochgeladen nach {SSH_CONFIG['remote_path']}")
cmd = f"python3 {SSH_CONFIG['remote_script']} {SSH_CONFIG['remote_path']}"
self.log(f"[🚀] Starte Remote-Skript: {cmd}")
stdin, stdout, stderr = ssh.exec_command(cmd)
output = stdout.read().decode()
error = stderr.read().decode()
if output:
self.log(f"[✅ OUTPUT]:\n{output}")
if error:
self.log(f"[⚠️ ERROR]:\n{error}")
ssh.close()
except Exception as e:
self.log(f"[❌] Fehler beim Remote-Vorgang: {e}")
messagebox.showerror("Fehler", str(e))
if __name__ == '__main__':
root = tk.Tk()
app = KremplImporterApp(root)
root.mainloop()
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Preisupdate-Skript für Intelectra Webshop
Dieses Skript importiert Preisdaten aus einer CSV-Datei in die Datenbank.
Es erstellt automatisch Backups der bestehenden Preisdaten vor dem Update.
Verwendung:
python3 preisupdate.py /pfad/zur/csv_datei.csv
CSV-Format:
Artikelnummer;VK1;VK3;VK100;VK_Endkunde
Beispiel: 12345;10,50;9,50;8,50;11,50
"""
import csv
import pymysql
import datetime
import os
import sys
import logging
from configparser import ConfigParser
# Logging-Konfiguration
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('preisupdate.log')
]
)
logger = logging.getLogger('preisupdate')
def load_config():
"""
Lädt die Konfiguration aus einer INI-Datei oder Umgebungsvariablen.
Falls keine Konfigurationsdatei gefunden wird, werden Standardwerte verwendet.
"""
config = {
'host': 'localhost',
'user': 'tbapy',
'password': '9%%0H32ryj_N9%%0H32ryj',
'database': 'webshop-sql',
'autocommit': True
}
# Versuche, Konfiguration aus Datei zu laden
config_file = os.path.join(os.path.dirname(__file__), 'db_config.ini')
if os.path.exists(config_file):
parser = ConfigParser()
parser.read(config_file)
if 'database' in parser:
config.update(parser['database'])
logger.info("Konfiguration aus Datei geladen")
# Umgebungsvariablen haben Vorrang
if 'DB_HOST' in os.environ:
config['host'] = os.environ['DB_HOST']
if 'DB_USER' in os.environ:
config['user'] = os.environ['DB_USER']
if 'DB_PASSWORD' in os.environ:
config['password'] = os.environ['DB_PASSWORD']
if 'DB_NAME' in os.environ:
config['database'] = os.environ['DB_NAME']
return config
def connect_db():
"""
Stellt eine Verbindung zur Datenbank her.
Returns:
pymysql.Connection: Die Datenbankverbindung
"""
config = load_config()
try:
conn = pymysql.connect(
host=config['host'],
user=config['user'],
password=config['password'],
database=config['database'],
autocommit=config['autocommit']
)
logger.info(f"Verbindung zur Datenbank {config['database']} hergestellt")
return conn
except Exception as e:
logger.error(f"Fehler beim Verbinden zur Datenbank: {e}")
raise
def cleanup_old_backups(cursor, keep_count=3):
"""
Löscht alte Backup-Tabellen, behält aber die neuesten 'keep_count' Backups.
Args:
cursor: Der Datenbank-Cursor
keep_count: Anzahl der zu behaltenden Backups
"""
try:
# Finde alle Backup-Tabellen
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_name LIKE 'item_prices_backup_%'
ORDER BY table_name DESC
""")
backups = cursor.fetchall()
# Wenn mehr als keep_count Backups existieren, lösche die ältesten
if len(backups) > keep_count:
for backup in backups[keep_count:]:
logger.info(f"Lösche altes Backup: {backup[0]}")
cursor.execute(f"DROP TABLE {backup[0]}")
logger.info(f"Backup-Bereinigung abgeschlossen. {len(backups)-keep_count} alte Backups entfernt.")
except Exception as e:
logger.error(f"Fehler bei der Backup-Bereinigung: {e}")
raise
def backup_and_clear_table(cursor):
"""
Erstellt ein Backup der aktuellen Preisdaten und leert die Update-Tabelle.
Args:
cursor: Der Datenbank-Cursor
"""
try:
# Erstelle ein Backup mit aktuellem Datum
backup_table_name = f"item_prices_backup_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
logger.info(f"Backup-Tabelle: {backup_table_name}")
cursor.execute(f"CREATE TABLE {backup_table_name} AS SELECT * FROM item_prices;")
logger.info("Backup der Echtpreise erstellt.")
# Bereinige alte Backups
cleanup_old_backups(cursor)
# Truncate der item_preise_update-Tabelle
cursor.execute("TRUNCATE TABLE item_preise_update;")
logger.info("item_preise_update geleert.")
except Exception as e:
logger.error(f"Fehler beim Backup und Leeren der Tabelle: {e}")
raise
def get_item_id(cursor, article_number):
"""
Ermittelt die ID eines Artikels anhand seiner Artikelnummer.
Args:
cursor: Der Datenbank-Cursor
article_number: Die Artikelnummer
Returns:
int: Die Artikel-ID oder None, wenn nicht gefunden
"""
try:
cursor.execute("SELECT id FROM items WHERE number = %s", (article_number,))
result = cursor.fetchone()
return result[0] if result else None
except Exception as e:
logger.error(f"Fehler beim Abrufen der Artikel-ID für {article_number}: {e}")
return None
def update_item_prices(cursor):
"""
Aktualisiert die Haupttabelle item_prices mit den Daten aus item_preise_update.
Args:
cursor: Der Datenbank-Cursor
Returns:
int: Anzahl der aktualisierten Datensätze
"""
try:
logger.info("Aktualisiere item_prices aus item_preise_update...")
cursor.execute("""
UPDATE item_prices ip
JOIN item_preise_update ipu
ON ip.item_id = ipu.item_id
AND ip.customergroup_id = ipu.customergroup_id
SET
ip.quantity_1 = ipu.quantity_1,
ip.price_1 = ipu.price_1,
ip.quantity_2 = ipu.quantity_2,
ip.price_2 = ipu.price_2,
ip.quantity_3 = ipu.quantity_3,
ip.price_3 = ipu.price_3;
""")
# Hole die Anzahl der aktualisierten Zeilen
rows_affected = cursor.rowcount
logger.info(f"Aktualisierte Datensätze: {rows_affected}")
logger.info("Update von item_prices abgeschlossen.")
return rows_affected
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Preise: {e}")
raise
def import_csv_to_db(csv_file):
"""
Importiert Preisdaten aus einer CSV-Datei in die Datenbank.
Args:
csv_file: Pfad zur CSV-Datei
Returns:
tuple: (Anzahl der verarbeiteten Artikel, Anzahl der aktualisierten Datensätze)
"""
conn = connect_db()
cursor = conn.cursor()
updated_count = 0
processed_items = set() # Für die Zählung eindeutiger Artikel
try:
# Backup & Truncate durchführen
backup_and_clear_table(cursor)
# Prüfe, ob die Datei existiert
if not os.path.exists(csv_file):
raise FileNotFoundError(f"Die Datei {csv_file} wurde nicht gefunden")
# Prüfe, ob die Datei leer ist
if os.path.getsize(csv_file) == 0:
raise ValueError(f"Die Datei {csv_file} ist leer")
with open(csv_file, "r", encoding="utf-8") as file:
reader = csv.reader(file, delimiter=';')
# Prüfe, ob die Datei eine Kopfzeile hat
try:
header = next(reader)
if len(header) < 5:
raise ValueError(f"Ungültiges CSV-Format: Erwartet mindestens 5 Spalten, gefunden {len(header)}")
logger.info(f"CSV-Header: {header}")
except StopIteration:
raise ValueError(f"Die Datei {csv_file} enthält keine Daten")
for row_num, row in enumerate(reader, start=2): # Start bei 2, da Zeile 1 der Header ist
# Prüfe, ob die Zeile genügend Spalten hat
if len(row) < 5:
logger.warning(f"Zeile {row_num}: Ungültiges Format, überspringe Zeile")
continue
article_number, vk1, vk3, vk100, vk_endkunde = row[:5]
# Überspringe leere Zeilen
if not article_number.strip():
continue
item_id = get_item_id(cursor, article_number)
# Ersetze Komma durch Punkt für float-Umwandlung
try:
vk1 = float(vk1.replace(",", "."))
vk3 = float(vk3.replace(",", "."))
vk100 = float(vk100.replace(",", "."))
vk_endkunde = float(vk_endkunde.replace(",", "."))
except ValueError as e:
logger.warning(f"Zeile {row_num}: Ungültiger Preiswert, überspringe Zeile: {e}")
continue
if item_id:
processed_items.add(item_id) # Merke uns den Artikel
# Sicherstellen, dass beide Einträge für customergroup_id 101 und 1 vorhanden sind
cursor.execute("""
INSERT INTO item_preise_update (item_id, customergroup_id, quantity_1, price_1)
VALUES (%s, 101, 1, %s)
ON DUPLICATE KEY UPDATE price_1 = VALUES(price_1)
""", (item_id, vk1))
cursor.execute("""
INSERT INTO item_preise_update (item_id, customergroup_id, quantity_1, price_1)
VALUES (%s, 1, 1, %s)
ON DUPLICATE KEY UPDATE price_1 = VALUES(price_1)
""", (item_id, vk_endkunde))
# VK3 nur wenn > 0 für customergroup_id 101
if vk3 > 0:
cursor.execute("""
UPDATE item_preise_update SET quantity_2 = 3, price_2 = %s
WHERE item_id = %s AND customergroup_id = 101
""", (vk3, item_id))
# VK100 nur wenn > 0 für customergroup_id 101
if vk100 > 0:
cursor.execute("""
UPDATE item_preise_update SET quantity_3 = 100, price_3 = %s
WHERE item_id = %s AND customergroup_id = 101
""", (vk100, item_id))
else:
logger.warning(f"Artikelnummer {article_number} nicht gefunden!")
# Update der Haupttabelle item_prices
updated_count = update_item_prices(cursor)
logger.info(f"Insgesamt {len(processed_items)} eindeutige Artikel verarbeitet")
return len(processed_items), updated_count
except Exception as e:
logger.error(f"Fehler beim Import: {e}")
raise
finally:
cursor.close()
conn.close()
logger.info("CSV-Import und Datenabgleich abgeschlossen!")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Bitte CSV-Datei als Parameter angeben!")
print("Verwendung: python3 preisupdate.py /pfad/zur/csv_datei.csv")
sys.exit(1)
try:
processed_count, updated_count = import_csv_to_db(sys.argv[1])
print(f"Insgesamt {processed_count} eindeutige Artikel verarbeitet")
print(f"Insgesamt {updated_count} Datensätze aktualisiert")
except Exception as e:
logger.error(f"Fehler: {e}")
sys.exit(1)
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Smart Price Update Service für Intelectra WebShop
Verhindert 50+ REPLACE INTO Abfragen pro Preisänderung!
"""
import json
import sys
import pymysql
import logging
from configparser import ConfigParser
import os
from datetime import datetime
from decimal import Decimal
# Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/www/vhosts/intelectra.de/httpdocs/logs/price_update_service.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger('price_update_service')
class PriceUpdateService:
"""Intelligenter Price-Update Service mit Change Detection"""
def __init__(self):
self.conn = self._connect_db()
self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)
def _connect_db(self):
"""DB-Connection aus config.ini"""
config_file = os.path.join(os.path.dirname(__file__), 'db_config.ini')
config = ConfigParser()
config.read(config_file)
return pymysql.connect(
host=config.get('database', 'host', fallback='localhost'),
user=config.get('database', 'user', fallback='tbapy'),
password=config.get('database', 'password', fallback='9%%0H32ryj_N9%%0H32ryj'),
database=config.get('database', 'database', fallback='webshop-sql'),
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def update_prices_smart(self, item_id, price_data):
"""
Smart Price Update - NUR Änderungen!
Verhindert 50+ REPLACE INTO Abfragen
"""
try:
changes_made = False
total_changes = 0
# 1. Aktuelle Preise laden
self.cursor.execute(
"SELECT customergroup_id, price_1, price_2, price_3, quantity_2, quantity_3, bargain_price_1, bargain_price_2, bargain_price_3 FROM item_prices WHERE item_id = %s",
(item_id,)
)
current_prices = {row['customergroup_id']: row for row in self.cursor.fetchall()}
logger.info(f"Loaded {len(current_prices)} existing price records for item {item_id}")
# 2. Preisänderungen verarbeiten
for group_id, group_prices in price_data.items():
if not group_id.startswith('group_'):
continue
customer_group_id = int(group_id.replace('group_', ''))
current_price = current_prices.get(customer_group_id, {})
# 3. Change Detection für diese Kundengruppe
needs_update = self._price_changed(current_price, group_prices)
if needs_update:
if current_price:
# UPDATE bestehenden Record
self._update_existing_price(item_id, customer_group_id, group_prices)
logger.info(f"Updated prices for group {customer_group_id}")
else:
# INSERT neuen Record
self._insert_new_price(item_id, customer_group_id, group_prices)
logger.info(f"Inserted new prices for group {customer_group_id}")
changes_made = True
total_changes += 1
else:
logger.debug(f"No price changes for group {customer_group_id}")
self.conn.commit()
return {
'success': True,
'item_id': item_id,
'changes_made': changes_made,
'groups_updated': total_changes,
'message': f'Updated {total_changes} price groups' if changes_made else 'No price changes detected'
}
except Exception as e:
self.conn.rollback()
logger.error(f"Error updating prices for item {item_id}: {str(e)}")
return {
'success': False,
'error': str(e),
'item_id': item_id
}
def _safe_float(self, value):
"""Konvertiert Wert sicher zu float - behandelt leere Strings und None"""
if value is None or value == '' or value == 'null':
return 0.0
try:
return float(value)
except (ValueError, TypeError):
return 0.0
def _price_changed(self, current_price, new_prices):
"""Erkennt ob sich Preise geändert haben"""
if not current_price: # Neue Kundengruppe
return True
# Alle relevanten Preisfelder prüfen
price_fields = [
'price_1', 'price_2', 'price_3',
'quantity_2', 'quantity_3',
'bargain_price_1', 'bargain_price_2', 'bargain_price_3'
]
for field in price_fields:
if field in new_prices:
current_val = self._safe_float(current_price.get(field, 0))
new_val = self._safe_float(new_prices[field])
# Dezimal-Vergleich mit Toleranz (0.01)
if abs(current_val - new_val) > 0.001:
logger.debug(f"Price change detected in {field}: {current_val} -> {new_val}")
return True
return False
def _update_existing_price(self, item_id, customer_group_id, group_prices):
"""Update bestehenden Preis-Record"""
set_clauses = []
values = []
price_fields = [
'price_1', 'price_2', 'price_3',
'quantity_2', 'quantity_3',
'bargain_price_1', 'bargain_price_2', 'bargain_price_3'
]
for field in price_fields:
if field in group_prices:
set_clauses.append(f"{field} = %s")
values.append(self._safe_float(group_prices[field]))
if set_clauses:
values.extend([item_id, customer_group_id])
sql = f"UPDATE item_prices SET {', '.join(set_clauses)} WHERE item_id = %s AND customergroup_id = %s"
self.cursor.execute(sql, values)
def _insert_new_price(self, item_id, customer_group_id, group_prices):
"""Insert neuen Preis-Record"""
fields = ['item_id', 'customergroup_id']
values = [item_id, customer_group_id]
price_fields = [
'price_1', 'price_2', 'price_3',
'quantity_2', 'quantity_3',
'bargain_price_1', 'bargain_price_2', 'bargain_price_3'
]
for field in price_fields:
if field in group_prices:
fields.append(field)
values.append(self._safe_float(group_prices[field]))
placeholders = ', '.join(['%s'] * len(values))
sql = f"INSERT INTO item_prices ({', '.join(fields)}) VALUES ({placeholders})"
self.cursor.execute(sql, values)
def get_price_statistics(self, item_id):
"""Debug: Preisstatistiken für ein Item"""
try:
self.cursor.execute("""
SELECT
COUNT(*) as total_groups,
MIN(price_1) as min_price,
MAX(price_1) as max_price,
AVG(price_1) as avg_price
FROM item_prices
WHERE item_id = %s AND price_1 > 0
""", (item_id,))
stats = self.cursor.fetchone()
return {
'success': True,
'item_id': item_id,
'statistics': stats
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def close(self):
"""Cleanup"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
def main():
"""CLI Entry Point für PHP shell_exec()"""
if len(sys.argv) < 2:
print(json.dumps({'error': 'No data provided'}))
sys.exit(1)
try:
# Parse input
data = json.loads(sys.argv[1])
item_id = data.get('item_id')
action = data.get('action', 'update_prices')
service = PriceUpdateService()
if action == 'update_prices':
price_data = data.get('prices', {})
result = service.update_prices_smart(item_id, price_data)
elif action == 'get_statistics':
result = service.get_price_statistics(item_id)
else:
result = {'error': f'Unknown action: {action}'}
service.close()
# Return JSON
print(json.dumps(result))
except Exception as e:
logger.error(f"Main error: {str(e)}")
print(json.dumps({'error': str(e)}))
sys.exit(1)
if __name__ == '__main__':
main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Command-line search script - called by PHP
Usage: python3 python_search.py "search_term"
Output: JSON array of items
"""
import json
import sys
import pymysql
import os
from configparser import ConfigParser
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'db_config.ini')
def load_db_config():
"""Load database config from ini file"""
parser = ConfigParser()
parser.read(CONFIG_PATH)
config = parser['database']
return {
'host': config.get('host', 'localhost'),
'user': config.get('user'),
'password': config.get('password'),
'database': config.get('database'),
'charset': 'utf8mb4',
'autocommit': True
}
def search_items(query):
"""Search items in database"""
print(f"# DEBUG: search_items called with '{query}'", file=sys.stderr)
if len(query.strip()) < 2:
print("# DEBUG: Query too short", file=sys.stderr)
return []
try:
print("# DEBUG: Loading config...", file=sys.stderr)
config = load_db_config()
print(f"# DEBUG: Connecting to DB: {config['host']}", file=sys.stderr)
conn = pymysql.connect(**config)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("# DEBUG: Connected successfully", file=sys.stderr)
sql = """
SELECT id, number, name, active
FROM items
WHERE (number LIKE %s OR name LIKE %s)
AND active = 1
ORDER BY
CASE
WHEN number LIKE %s THEN 1
WHEN name LIKE %s THEN 2
ELSE 3
END,
name ASC, number ASC
LIMIT 20
"""
search_param = f"%{query}%"
search_param_start = f"{query}%"
print(f"# DEBUG: SQL params: {search_param}", file=sys.stderr)
cursor.execute(sql, (search_param, search_param, search_param_start, search_param_start))
results = cursor.fetchall()
print(f"# DEBUG: Raw SQL results: {len(results)} rows", file=sys.stderr)
# Test: try without active filter
test_sql = "SELECT COUNT(*) as total FROM items WHERE (number LIKE %s OR name LIKE %s)"
cursor.execute(test_sql, (search_param, search_param))
total = cursor.fetchone()
print(f"# DEBUG: Total matches (any active): {total}", file=sys.stderr)
items = []
for row in results:
status = ' || aktiv' if row['active'] else ' || inaktiv'
items.append({
'id': row['id'],
'value': row['number'],
'label': f"{row['name']} ({row['number']}{status})",
'number': row['number'],
'name': row['name'],
'active': row['active']
})
cursor.close()
conn.close()
return items
except Exception as e:
# Return empty array on error - let PHP fallback handle it
return []
if __name__ == '__main__':
print("# DEBUG: Script started", file=sys.stderr)
print(f"# DEBUG: Args: {sys.argv}", file=sys.stderr)
if len(sys.argv) != 2:
print("# DEBUG: Wrong number of args", file=sys.stderr)
print("[]")
sys.exit(1)
search_query = sys.argv[1]
print(f"# DEBUG: Searching for '{search_query}'", file=sys.stderr)
try:
results = search_items(search_query)
print(f"# DEBUG: Got results: {results}", file=sys.stderr)
print(json.dumps(results))
except Exception as e:
print(f"# DEBUG: Exception: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
print("[]")
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sicherheitsdaten-Import-Skript für Intelectra Webshop
Dieses Skript importiert Sicherheitsdaten aus einer CSV-Datei in die Datenbank.
Verwendung:
python3 sdbimport.py /pfad/zur/csv_datei.csv
CSV-Format:
article_id;navision_id;warning_ids;Bedienungsanleitung;EU_Datenblatt;Energielabel;Produktdatenblatt;Sicherheitsdatenblatt;Zertifikat;null_feld
Beispiel: 12345;67890;1;2;3;http://example.com/ba.pdf;http://example.com/eu.pdf;http://example.com/el.pdf;http://example.com/pd.pdf;http://example.com/sdb.pdf;http://example.com/cert.pdf;
"""
import csv
import pymysql
import sys
import os
import logging
from configparser import ConfigParser
# Logging-Konfiguration
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('sdbimport.log')
]
)
logger = logging.getLogger('sdbimport')
def load_config():
"""
Lädt die Konfiguration aus einer INI-Datei oder Umgebungsvariablen.
Falls keine Konfigurationsdatei gefunden wird, werden Standardwerte verwendet.
"""
config = {
'host': 'localhost',
'user': 'tbapy',
'password': '9%%0H32ryj_N9%%0H32ryj',
'database': 'webshop-sql',
'autocommit': True
}
# Versuche, Konfiguration aus Datei zu laden
config_file = os.path.join(os.path.dirname(__file__), 'db_config.ini')
if os.path.exists(config_file):
parser = ConfigParser()
parser.read(config_file)
if 'database' in parser:
config.update(parser['database'])
logger.info("Konfiguration aus Datei geladen")
# Umgebungsvariablen haben Vorrang
if 'DB_HOST' in os.environ:
config['host'] = os.environ['DB_HOST']
if 'DB_USER' in os.environ:
config['user'] = os.environ['DB_USER']
if 'DB_PASSWORD' in os.environ:
config['password'] = os.environ['DB_PASSWORD']
if 'DB_NAME' in os.environ:
config['database'] = os.environ['DB_NAME']
return config
def connect_db():
"""
Stellt eine Verbindung zur Datenbank her.
Returns:
pymysql.Connection: Die Datenbankverbindung
"""
config = load_config()
try:
conn = pymysql.connect(
host=config['host'],
user=config['user'],
password=config['password'],
database=config['database'],
autocommit=config['autocommit']
)
logger.info(f"Verbindung zur Datenbank {config['database']} hergestellt")
return conn
except Exception as e:
logger.error(f"Fehler beim Verbinden zur Datenbank: {e}")
raise
def get_item_id(cursor, article_id, navision_id):
"""
Ermittelt die item_id basierend auf verschiedenen Kriterien.
Args:
cursor: Datenbankverbindung
article_id: Die Artikel-ID aus der CSV
navision_id: Die Navision-ID aus der CSV
Returns:
int: Die gefundene item_id oder None wenn nicht gefunden
"""
# Versuche zuerst über navision_id und attribute_7 zu finden
if navision_id:
sql = """
SELECT id
FROM items
WHERE navision_id = %s
AND attribute_7 IS NOT NULL
LIMIT 1
"""
cursor.execute(sql, (navision_id,))
result = cursor.fetchone()
if result:
return result[0]
# Wenn nicht gefunden, versuche über items.number = article_id
sql = """
SELECT id
FROM items
WHERE number = %s
LIMIT 1
"""
cursor.execute(sql, (article_id,))
result = cursor.fetchone()
return result[0] if result else None
def import_sicherheitsdaten(csv_file):
"""
Importiert Sicherheitsdaten aus einer CSV-Datei in die Datenbank.
Args:
csv_file: Pfad zur CSV-Datei
Returns:
int: Anzahl der verarbeiteten Artikel
"""
conn = connect_db()
cursor = conn.cursor()
processed_items = 0
skipped_items = 0
try:
# Prüfe, ob die Datei existiert
if not os.path.exists(csv_file):
raise FileNotFoundError(f"Die Datei {csv_file} wurde nicht gefunden")
# Prüfe, ob die Datei leer ist
if os.path.getsize(csv_file) == 0:
raise ValueError(f"Die Datei {csv_file} ist leer")
with open(csv_file, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f, delimiter=';')
# Prüfe, ob die Datei die erforderlichen Spalten enthält
required_fields = ['article_id', 'warning_ids']
missing_fields = [field for field in required_fields if field not in reader.fieldnames]
if missing_fields:
raise ValueError(f"Die CSV-Datei fehlt die erforderlichen Spalten: {', '.join(missing_fields)}")
logger.info(f"CSV-Header: {reader.fieldnames}")
for row_num, row in enumerate(reader, start=2):
try:
article_id = row['article_id']
navision_id = row.get('navision_id')
if not article_id:
logger.warning(f"Zeile {row_num}: Keine article_id angegeben, überspringe Zeile")
skipped_items += 1
continue
# Ermittle item_id basierend auf den Kriterien
item_id = get_item_id(cursor, article_id, navision_id)
if not item_id:
logger.warning(f"Zeile {row_num}: Kein passender Artikel gefunden für article_id={article_id}, navision_id={navision_id}")
skipped_items += 1
continue
warning_ids = row.get('warning_ids') or ''
ba = row.get('Bedienungsanleitung') or ''
eu = row.get('EU_Datenblatt') or ''
el = row.get('Energielabel') or ''
pd = row.get('Produktdatenblatt') or ''
sdb = row.get('Sicherheitsdatenblatt') or ''
cert = row.get('Zertifikat') or ''
null_feld = row.get('null_feld') or ''
sql = """
INSERT INTO sicherheitshinweis_item
(article_id, navision_id, warning_ids, Bedienungsanleitung, EU_Datenblatt, Energielabel,
Produktdatenblatt, Sicherheitsdatenblatt, Zertifikat, null_feld)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
navision_id = VALUES(navision_id),
warning_ids = VALUES(warning_ids),
Bedienungsanleitung = VALUES(Bedienungsanleitung),
EU_Datenblatt = VALUES(EU_Datenblatt),
Energielabel = VALUES(Energielabel),
Produktdatenblatt = VALUES(Produktdatenblatt),
Sicherheitsdatenblatt = VALUES(Sicherheitsdatenblatt),
Zertifikat = VALUES(Zertifikat),
null_feld = VALUES(null_feld)
"""
cursor.execute(sql, (
item_id, # Verwende die gefundene item_id statt article_id
navision_id,
warning_ids,
ba, eu, el, pd, sdb, cert, null_feld
))
processed_items += 1
# Logge Fortschritt alle 100 Einträge
if processed_items % 100 == 0:
logger.info(f"{processed_items} Artikel verarbeitet")
except Exception as e:
logger.error(f"Fehler bei Zeile {row_num} (article_id: {article_id}): {e}")
skipped_items += 1
continue
logger.info(f"Import abgeschlossen: {processed_items} Artikel verarbeitet, {skipped_items} übersprungen")
return processed_items
except Exception as e:
logger.error(f"Fehler beim Sicherheitsdaten-Import: {e}")
raise
finally:
cursor.close()
conn.close()
logger.info("Sicherheitsdaten-Import abgeschlossen")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Bitte CSV-Datei als Parameter angeben!")
print("Verwendung: python3 sdbimport.py /pfad/zur/csv_datei.csv")
sys.exit(1)
try:
processed_count = import_sicherheitsdaten(sys.argv[1])
print(f"Insgesamt {processed_count} eindeutige Artikel verarbeitet")
except Exception as e:
logger.error(f"Fehler: {e}")
sys.exit(1)