shop-old/modules/import/item_save_service.py
2026-04-20 01:03:43 +02:00

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()