Initial import: Carteasy Shop (Dev-Stand)
This commit is contained in:
Executable
+233
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
[database]
|
||||
host = localhost
|
||||
user = tbapy
|
||||
password = 9%%0H32ryj_N9%%0H32ryj
|
||||
database = webshop-sql
|
||||
autocommit = true
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Executable
+258
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+318
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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("[]")
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user