258 lines
9.8 KiB
Python
Executable File
258 lines
9.8 KiB
Python
Executable File
#!/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() |