commit 2d088f693d4e359ddddc71ab644b8bf1c6944c95 Author: tabea k Date: Fri Jul 4 18:25:02 2025 +0200 Initial commit: Django Shop ohne große Dateien diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd6e497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Django static files +staticfiles/ + +# Media files (uploads) +media/ + +# Database files +*.db +*.sqlite3 + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Log files +*.log + +# Environment variables +.env.local +.env.production \ No newline at end of file diff --git a/django_shop/settings.py b/django_shop/settings.py new file mode 100644 index 0000000..07e129c --- /dev/null +++ b/django_shop/settings.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Lade .env Datei +load_dotenv(BASE_DIR / '.env') + +# E-Mail-Einstellungen (temporär Console-Backend) +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'Fursuit Shop ' + +# Admin-E-Mail-Empfänger +ADMINS = [ + ('Shop Admin', 'admin@fursuitshop.com'), +] + +# Lagerbestand-Einstellungen +LOW_STOCK_THRESHOLD = 5 # Schwellenwert für niedrigen Lagerbestand \ No newline at end of file diff --git a/docs/email_system.md b/docs/email_system.md new file mode 100644 index 0000000..d166578 --- /dev/null +++ b/docs/email_system.md @@ -0,0 +1,155 @@ +# E-Mail-System Dokumentation + +## Übersicht + +Das E-Mail-System des Fursuit Shops versendet automatisch Benachrichtigungen an Kunden und Administratoren bei verschiedenen Shop-Ereignissen. Das System ist mehrsprachig (DE/EN) und verwendet responsive HTML-Templates mit Text-Alternativen. + +## Konfiguration + +### E-Mail-Einstellungen + +Die E-Mail-Konfiguration erfolgt über Umgebungsvariablen in der `.env`-Datei: + +```env +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USE_TLS=True +EMAIL_HOST_USER=your-email@gmail.com +EMAIL_HOST_PASSWORD=your-app-specific-password +DEFAULT_FROM_EMAIL=Fursuit Shop +``` + +### Admin-Benachrichtigungen + +Administratoren werden in `settings.py` konfiguriert: + +```python +ADMINS = [ + ('Shop Admin', 'admin@fursuitshop.com'), +] +``` + +### Lagerbestand-Schwellenwert + +```python +LOW_STOCK_THRESHOLD = 5 # Benachrichtigung bei ≤ 5 Artikeln +``` + +## E-Mail-Typen + +### 1. Kundenbenachrichtigungen + +#### Bestellbestätigung +- Gesendet nach erfolgreicher Zahlung +- Enthält Bestelldetails, Produkte und Preise +- Template: `order_confirmation.html/txt` + +#### Status-Updates +- Gesendet bei Statusänderungen der Bestellung +- Kann Fortschrittsbilder und Beschreibungen enthalten +- Template: `order_status_update.html/txt` + +#### Versandbestätigung +- Gesendet wenn Bestellung versendet wurde +- Enthält Tracking-Nummer und Versanddetails +- Template: `shipping_confirmation.html/txt` + +### 2. Admin-Benachrichtigungen + +#### Neue Bestellung +- Bei jeder neuen Bestellung +- Spezielle Markierung für Fursuit-Bestellungen +- Template: `admin_notification.html/txt` + +#### Zahlungsfehler +- Bei fehlgeschlagenen Zahlungen +- Enthält detaillierte Fehlerinformationen +- Template: `admin_notification.html/txt` + +#### Custom Design +- Bei Bestellungen mit Custom Designs +- Enthält Design-Dateien und Notizen +- Template: `admin_notification.html/txt` + +#### Niedriger Lagerbestand +- Bei Unterschreitung des Schwellenwerts +- Enthält Produktdetails und aktuellen Bestand +- Template: `low_stock_notification.html/txt` + +## Signal-Handler + +Das System verwendet Django-Signals für automatische Benachrichtigungen: + +### Order-Signals +```python +@receiver(post_save, sender=Order) +def handle_order_notifications(sender, instance, created, **kwargs): + # Sendet Benachrichtigungen bei neuen Bestellungen und Statusänderungen +``` + +### Payment-Signals +```python +@receiver(post_save, sender=PaymentError) +def handle_payment_error(sender, instance, created, **kwargs): + # Benachrichtigt Admins über Zahlungsfehler +``` + +### Product-Signals +```python +@receiver(pre_save, sender=Product) +def check_stock_level(sender, instance, **kwargs): + # Überprüft Lagerbestand und sendet Warnungen +``` + +## E-Mail-Templates + +Alle E-Mail-Templates: +- Sind vollständig responsive +- Unterstützen HTML und Text-Alternativen +- Sind mehrsprachig (DE/EN) +- Verwenden einheitliches Branding + +### Template-Struktur +``` +shop/templates/shop/emails/ +├── order_confirmation.html +├── order_confirmation.txt +├── order_status_update.html +├── order_status_update.txt +├── shipping_confirmation.html +├── shipping_confirmation.txt +├── admin_notification.html +├── admin_notification.txt +├── low_stock_notification.html +└── low_stock_notification.txt +``` + +## Sicherheit + +- TLS-Verschlüsselung für E-Mail-Versand +- Keine sensiblen Daten in E-Mails +- Sichere Links zu Admin-Bereich +- App-spezifische Passwörter für SMTP + +## Fehlerbehandlung + +- Robuste Exception-Handling +- Logging von Versandfehlern +- Vermeidung von Doppel-Benachrichtigungen +- Fallback auf Text-Version bei HTML-Problemen + +## Wartung + +### Neue E-Mail-Typen hinzufügen + +1. Templates erstellen (HTML und Text) +2. E-Mail-Funktion in `emails.py` hinzufügen +3. Signal-Handler in `signals.py` registrieren +4. Übersetzungen in `.po`-Dateien hinzufügen + +### Template-Anpassung + +- CSS-Styles in Template-Header +- Einheitliche Farbcodes und Abstände +- Bootstrap-kompatible Klassen +- Responsive Breakpoints \ No newline at end of file diff --git a/init_data.py b/init_data.py new file mode 100644 index 0000000..cc9112a --- /dev/null +++ b/init_data.py @@ -0,0 +1,31 @@ +import sqlite3 + +def add_test_data(): + conn = sqlite3.connect('shop.db') + cursor = conn.cursor() + + # Lösche vorhandene Daten + cursor.execute("DELETE FROM products") + + # Testprodukte + products = [ + ("Gaming Maus", "Hochwertige Gaming-Maus mit RGB-Beleuchtung", 49.99, 10), + ("Mechanische Tastatur", "Mechanische Gaming-Tastatur mit blauen Switches", 89.99, 5), + ("Gaming Headset", "7.1 Surround Sound Gaming Headset", 79.99, 8), + ("Mousepad XL", "Extra großes Gaming-Mousepad", 19.99, 15), + ("Webcam HD", "1080p Webcam für Streaming", 59.99, 3) + ] + + # Füge Produkte ein + cursor.executemany( + "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)", + products + ) + + # Speichere Änderungen + conn.commit() + conn.close() + print("Testdaten wurden erfolgreich hinzugefügt!") + +if __name__ == "__main__": + add_test_data() \ No newline at end of file diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..771e7b8 --- /dev/null +++ b/init_db.py @@ -0,0 +1,43 @@ +import sqlite3 +from contextlib import contextmanager + +@contextmanager +def get_db(): + conn = sqlite3.connect('shop.db') + try: + yield conn + finally: + conn.close() + +def init_db(): + with get_db() as conn: + # Tabelle erstellen + conn.execute(""" + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + price REAL NOT NULL, + stock INTEGER NOT NULL + ) + """) + + # Beispieldaten einfügen + products = [ + ("Laptop", "Leistungsstarker Laptop für Arbeit und Gaming", 999.99, 5), + ("Smartphone", "Neuestes Modell mit Top-Kamera", 699.99, 10), + ("Kopfhörer", "Kabellose Kopfhörer mit Noise-Cancelling", 199.99, 15), + ("Tablet", "Perfekt für Unterhaltung und Produktivität", 449.99, 8), + ("Smartwatch", "Fitness-Tracking und Benachrichtigungen", 299.99, 12) + ] + + conn.execute("DELETE FROM products") # Alte Daten löschen + conn.executemany( + "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)", + products + ) + conn.commit() + +if __name__ == "__main__": + init_db() + print("Datenbank wurde erfolgreich initialisiert!") \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..067eeec --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/paypal_integration/admin.py b/paypal_integration/admin.py new file mode 100644 index 0000000..3328bca --- /dev/null +++ b/paypal_integration/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from .models import PayPalConfig + +@admin.register(PayPalConfig) +class PayPalConfigAdmin(admin.ModelAdmin): + list_display = ('get_mode', 'client_id', 'is_sandbox', 'updated_at') + readonly_fields = ('created_at', 'updated_at') + fieldsets = ( + ('API Konfiguration', { + 'fields': ('client_id', 'client_secret', 'is_sandbox') + }), + ('Zeitstempel', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) \ No newline at end of file diff --git a/paypal_integration/models.py b/paypal_integration/models.py new file mode 100644 index 0000000..e27a623 --- /dev/null +++ b/paypal_integration/models.py @@ -0,0 +1,19 @@ +from django.db import models +from django.conf import settings + +class PayPalConfig(models.Model): + client_id = models.CharField(max_length=255) + client_secret = models.CharField(max_length=255) + is_sandbox = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'PayPal Konfiguration' + verbose_name_plural = 'PayPal Konfigurationen' + + def __str__(self): + return f"PayPal Config ({self.get_mode()})" + + def get_mode(self): + return "Sandbox" if self.is_sandbox else "Live" \ No newline at end of file diff --git a/paypal_integration/urls.py b/paypal_integration/urls.py new file mode 100644 index 0000000..d8328ad --- /dev/null +++ b/paypal_integration/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = 'paypal' + +urlpatterns = [ + path('process//', views.process_payment, name='process_payment'), + path('execute//', views.execute_payment, name='execute_payment'), + path('cancel//', views.cancel_payment, name='cancel_payment'), +] \ No newline at end of file diff --git a/paypal_integration/views.py b/paypal_integration/views.py new file mode 100644 index 0000000..211b0ea --- /dev/null +++ b/paypal_integration/views.py @@ -0,0 +1,87 @@ +from django.shortcuts import render, redirect +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.conf import settings +import paypalrestsdk +from .models import PayPalConfig +from products.models import Order + +def setup_paypal(): + config = PayPalConfig.objects.first() + if config: + paypalrestsdk.configure({ + "mode": "sandbox" if config.is_sandbox else "live", + "client_id": config.client_id, + "client_secret": config.client_secret + }) + return True + return False + +@login_required +def process_payment(request, order_id): + if not setup_paypal(): + messages.error(request, 'PayPal ist nicht richtig konfiguriert. Bitte kontaktieren Sie den Administrator.') + return redirect('order_detail', order_id=order_id) + + try: + order = Order.objects.get(id=order_id, user=request.user) + + payment = paypalrestsdk.Payment({ + "intent": "sale", + "payer": { + "payment_method": "paypal" + }, + "redirect_urls": { + "return_url": request.build_absolute_uri(f'/paypal/execute/{order_id}/'), + "cancel_url": request.build_absolute_uri(f'/paypal/cancel/{order_id}/') + }, + "transactions": [{ + "amount": { + "total": str(order.total_amount), + "currency": "EUR" + }, + "description": f"Bestellung #{order.id}" + }] + }) + + if payment.create(): + for link in payment.links: + if link.method == "REDIRECT": + return redirect(link.href) + else: + messages.error(request, 'Fehler bei der Erstellung der PayPal-Zahlung.') + + except Order.DoesNotExist: + messages.error(request, 'Bestellung nicht gefunden.') + except Exception as e: + messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}') + + return redirect('order_detail', order_id=order_id) + +@login_required +def execute_payment(request, order_id): + payment_id = request.GET.get('paymentId') + payer_id = request.GET.get('PayerID') + + try: + payment = paypalrestsdk.Payment.find(payment_id) + if payment.execute({"payer_id": payer_id}): + order = Order.objects.get(id=order_id, user=request.user) + order.status = 'paid' + order.payment_method = 'paypal' + order.payment_id = payment_id + order.save() + + messages.success(request, 'Zahlung erfolgreich durchgeführt!') + return redirect('payment_success', order_id=order_id) + else: + messages.error(request, 'Fehler bei der Ausführung der Zahlung.') + except Exception as e: + messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}') + + return redirect('payment_failed', order_id=order_id) + +@login_required +def cancel_payment(request, order_id): + messages.info(request, 'Die PayPal-Zahlung wurde abgebrochen.') + return redirect('order_detail', order_id=order_id) \ No newline at end of file diff --git a/products/__init__.py b/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/admin.py b/products/admin.py new file mode 100644 index 0000000..9559829 --- /dev/null +++ b/products/admin.py @@ -0,0 +1,90 @@ +from django.contrib import admin +from .models import ( + Product, Cart, CartItem, Order, OrderItem, + Review, UserProfile, FAQ, ContactMessage, + CustomOrder, OrderProgress, GalleryImage +) +from django.utils.safestring import mark_safe + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name', 'price', 'stock', 'fursuit_type', 'style', 'is_featured', 'is_custom_order') + list_filter = ('fursuit_type', 'style', 'is_featured', 'is_custom_order') + search_fields = ('name', 'description') + ordering = ('-created',) + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'full_name', 'total_amount', 'status', 'payment_status', 'created') + list_filter = ('status', 'payment_status', 'payment_method') + search_fields = ('full_name', 'email') + ordering = ('-created',) + +@admin.register(OrderItem) +class OrderItemAdmin(admin.ModelAdmin): + list_display = ('order', 'product_name', 'quantity', 'price') + search_fields = ('product_name',) + +@admin.register(Review) +class ReviewAdmin(admin.ModelAdmin): + list_display = ('product', 'user', 'rating', 'created') + list_filter = ('rating',) + search_fields = ('comment', 'user__username') + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ('id', 'user', 'created') + search_fields = ('user__username',) + +@admin.register(CartItem) +class CartItemAdmin(admin.ModelAdmin): + list_display = ('cart', 'product', 'quantity') + +@admin.register(UserProfile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'phone', 'newsletter') + list_filter = ('newsletter',) + search_fields = ('user__username', 'phone', 'address') + +@admin.register(FAQ) +class FAQAdmin(admin.ModelAdmin): + list_display = ('question', 'category', 'order') + list_filter = ('category',) + search_fields = ('question', 'answer') + ordering = ('category', 'order') + +@admin.register(ContactMessage) +class ContactMessageAdmin(admin.ModelAdmin): + list_display = ('subject', 'name', 'email', 'category', 'status', 'created') + list_filter = ('category', 'status') + search_fields = ('name', 'email', 'subject', 'message') + ordering = ('-created',) + +@admin.register(CustomOrder) +class CustomOrderAdmin(admin.ModelAdmin): + list_display = ('character_name', 'user', 'fursuit_type', 'status', 'created') + list_filter = ('fursuit_type', 'style', 'status') + search_fields = ('character_name', 'user__username', 'character_description') + ordering = ('-created',) + +@admin.register(OrderProgress) +class OrderProgressAdmin(admin.ModelAdmin): + list_display = ('custom_order', 'stage', 'completed', 'created') + list_filter = ('stage', 'completed') + search_fields = ('custom_order__character_name', 'description') + ordering = ('custom_order', 'created') + +@admin.register(GalleryImage) +class GalleryImageAdmin(admin.ModelAdmin): + list_display = ('title', 'fursuit_type', 'style', 'is_featured', 'order', 'created', 'admin_image') + list_filter = ('fursuit_type', 'style', 'is_featured') + search_fields = ('title', 'description') + ordering = ('order', '-created') + list_editable = ('order', 'is_featured') + readonly_fields = ('admin_image',) + + def admin_image(self, obj): + if obj.image: + return mark_safe(f'') + return "Kein Bild" + admin_image.short_description = 'Vorschau' diff --git a/products/apps.py b/products/apps.py new file mode 100644 index 0000000..2282266 --- /dev/null +++ b/products/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'products' diff --git a/products/forms.py b/products/forms.py new file mode 100644 index 0000000..83f4173 --- /dev/null +++ b/products/forms.py @@ -0,0 +1,219 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm +from django.contrib.auth.models import User +from .models import Order, Product, Review, UserProfile, ContactMessage, CustomOrder, OrderProgress + +class OrderForm(forms.ModelForm): + class Meta: + model = Order + fields = ['full_name', 'email', 'address', 'phone'] + widgets = { + 'full_name': forms.TextInput(attrs={'class': 'form-control'}), + 'email': forms.EmailInput(attrs={'class': 'form-control'}), + 'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'phone': forms.TextInput(attrs={'class': 'form-control'}), + } + labels = { + 'full_name': 'Vollständiger Name', + 'email': 'E-Mail-Adresse', + 'address': 'Lieferadresse', + 'phone': 'Telefonnummer', + } + +class CustomUserCreationForm(UserCreationForm): + email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'})) + + class Meta: + model = User + fields = ['username', 'email', 'password1', 'password2'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields: + self.fields[field].widget.attrs['class'] = 'form-control' + self.fields['username'].label = 'Benutzername' + self.fields['password1'].label = 'Passwort' + self.fields['password2'].label = 'Passwort bestätigen' + +class CustomAuthenticationForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields: + self.fields[field].widget.attrs['class'] = 'form-control' + self.fields['username'].label = 'Benutzername' + self.fields['password'].label = 'Passwort' + +class ReviewForm(forms.ModelForm): + class Meta: + model = Review + fields = ['rating', 'comment'] + widgets = { + 'rating': forms.Select(attrs={'class': 'form-control'}), + 'comment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + } + labels = { + 'rating': 'Bewertung', + 'comment': 'Kommentar', + } + +class UserProfileForm(forms.ModelForm): + first_name = forms.CharField(max_length=30, required=False, label='Vorname') + last_name = forms.CharField(max_length=30, required=False, label='Nachname') + email = forms.EmailField(required=True, label='E-Mail-Adresse') + + class Meta: + model = UserProfile + fields = ['phone', 'address', 'default_shipping_address', 'newsletter'] + labels = { + 'phone': 'Telefonnummer', + 'address': 'Adresse', + 'default_shipping_address': 'Standard-Lieferadresse', + 'newsletter': 'Newsletter abonnieren', + } + widgets = { + 'address': forms.Textarea(attrs={'rows': 3}), + 'default_shipping_address': forms.Textarea(attrs={'rows': 3}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.user: + self.fields['first_name'].initial = self.instance.user.first_name + self.fields['last_name'].initial = self.instance.user.last_name + self.fields['email'].initial = self.instance.user.email + for field in self.fields: + self.fields[field].widget.attrs['class'] = 'form-control' + + def save(self, commit=True): + profile = super().save(commit=False) + if commit: + user = profile.user + user.first_name = self.cleaned_data['first_name'] + user.last_name = self.cleaned_data['last_name'] + user.email = self.cleaned_data['email'] + user.save() + profile.save() + return profile + +class ContactForm(forms.ModelForm): + class Meta: + model = ContactMessage + fields = ['name', 'email', 'category', 'order_number', 'subject', 'message'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control'}), + 'email': forms.EmailInput(attrs={'class': 'form-control'}), + 'category': forms.Select(attrs={'class': 'form-control'}), + 'order_number': forms.TextInput(attrs={'class': 'form-control'}), + 'subject': forms.TextInput(attrs={'class': 'form-control'}), + 'message': forms.Textarea(attrs={'class': 'form-control', 'rows': 5}), + } + labels = { + 'name': 'Name', + 'email': 'E-Mail-Adresse', + 'category': 'Kategorie', + 'order_number': 'Bestellnummer (optional)', + 'subject': 'Betreff', + 'message': 'Ihre Nachricht', + } + + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) + if user and user.is_authenticated: + self.fields['name'].initial = user.get_full_name() or user.username + self.fields['email'].initial = user.email + +class CustomOrderForm(forms.ModelForm): + class Meta: + model = CustomOrder + fields = [ + 'fursuit_type', 'style', 'character_name', + 'character_description', 'reference_images', + 'special_requests', 'measurements', + 'color_preferences', 'budget_range', + 'deadline_request' + ] + widgets = { + 'fursuit_type': forms.Select(attrs={'class': 'form-select'}), + 'style': forms.Select(attrs={'class': 'form-select'}), + 'character_name': forms.TextInput(attrs={'class': 'form-control'}), + 'character_description': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 4, + 'placeholder': 'Beschreiben Sie Ihren Character so detailliert wie möglich...' + }), + 'special_requests': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Besondere Wünsche oder Anforderungen...' + }), + 'measurements': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 4, + 'placeholder': 'Bitte geben Sie alle relevanten Maße an (Kopfumfang, Körpergröße, etc.)' + }), + 'color_preferences': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Beschreiben Sie die gewünschten Farben und Farbkombinationen...' + }), + 'budget_range': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'z.B. 2000-3000€' + }), + 'deadline_request': forms.DateInput(attrs={ + 'class': 'form-control', + 'type': 'date' + }), + 'reference_images': forms.FileInput(attrs={ + 'class': 'form-control', + 'accept': 'image/*' + }) + } + labels = { + 'fursuit_type': 'Art des Fursuits', + 'style': 'Gewünschter Stil', + 'character_name': 'Name des Characters', + 'character_description': 'Beschreibung des Characters', + 'reference_images': 'Referenzbilder', + 'special_requests': 'Besondere Wünsche', + 'measurements': 'Maße', + 'color_preferences': 'Farbwünsche', + 'budget_range': 'Budget-Rahmen', + 'deadline_request': 'Gewünschter Fertigstellungstermin' + } + help_texts = { + 'character_description': 'Je detaillierter die Beschreibung, desto besser können wir Ihre Vision umsetzen.', + 'reference_images': 'Laden Sie bis zu 5 Referenzbilder hoch (max. 5MB pro Bild)', + 'measurements': 'Genaue Maße sind wichtig für eine perfekte Passform.', + 'budget_range': 'Geben Sie einen Bereich an, in dem Sie sich preislich bewegen möchten.', + 'deadline_request': 'Optional: Wenn Sie einen bestimmten Termin im Auge haben.' + } + +class OrderProgressForm(forms.ModelForm): + class Meta: + model = OrderProgress + fields = ['stage', 'description', 'image', 'completed'] + widgets = { + 'stage': forms.Select(attrs={'class': 'form-select'}), + 'description': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Beschreiben Sie den aktuellen Fortschritt...' + }), + 'image': forms.FileInput(attrs={ + 'class': 'form-control', + 'accept': 'image/*' + }), + 'completed': forms.CheckboxInput(attrs={'class': 'form-check-input'}) + } + labels = { + 'stage': 'Arbeitsschritt', + 'description': 'Beschreibung des Fortschritts', + 'image': 'Foto des Fortschritts', + 'completed': 'Abgeschlossen' + } + help_texts = { + 'description': 'Beschreiben Sie detailliert, was in diesem Schritt gemacht wurde.', + 'image': 'Fügen Sie ein Foto hinzu, um den Fortschritt zu dokumentieren.', + 'completed': 'Markieren Sie diesen Schritt als abgeschlossen, wenn er fertig ist.' + } \ No newline at end of file diff --git a/products/migrations/0001_initial.py b/products/migrations/0001_initial.py new file mode 100644 index 0000000..c1c92ce --- /dev/null +++ b/products/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('stock', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/products/migrations/0002_cart_cartitem.py b/products/migrations/0002_cart_cartitem.py new file mode 100644 index 0000000..6e5202d --- /dev/null +++ b/products/migrations/0002_cart_cartitem.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_id', models.CharField(blank=True, max_length=100, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')), + ], + options={ + 'unique_together': {('cart', 'product')}, + }, + ), + ] diff --git a/products/migrations/0003_product_category_product_featured_product_image_and_more.py b/products/migrations/0003_product_category_product_featured_product_image_and_more.py new file mode 100644 index 0000000..29390d7 --- /dev/null +++ b/products/migrations/0003_product_category_product_featured_product_image_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0002_cart_cartitem'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='category', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='product', + name='featured', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='product', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='products/'), + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=200)), + ('email', models.EmailField(max_length=254)), + ('address', models.TextField()), + ('phone', models.CharField(max_length=20)), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('status', models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'In Bearbeitung'), ('shipped', 'Versendet'), ('delivered', 'Geliefert'), ('cancelled', 'Storniert')], default='pending', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_name', models.CharField(max_length=200)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('quantity', models.PositiveIntegerField()), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.order')), + ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')), + ], + ), + ] diff --git a/products/migrations/0004_review.py b/products/migrations/0004_review.py new file mode 100644 index 0000000..4fbb6e0 --- /dev/null +++ b/products/migrations/0004_review.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:25 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0003_product_category_product_featured_product_image_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rating', models.IntegerField(choices=[(1, '1 - Sehr schlecht'), (2, '2 - Schlecht'), (3, '3 - Okay'), (4, '4 - Gut'), (5, '5 - Sehr gut')], validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='products.product')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'unique_together': {('product', 'user')}, + }, + ), + ] diff --git a/products/migrations/0005_userprofile_wishlist.py b/products/migrations/0005_userprofile_wishlist.py new file mode 100644 index 0000000..eea57ac --- /dev/null +++ b/products/migrations/0005_userprofile_wishlist.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:50 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0004_review'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', models.CharField(blank=True, max_length=20)), + ('address', models.TextField(blank=True)), + ('default_shipping_address', models.TextField(blank=True)), + ('newsletter', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Wishlist', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('products', models.ManyToManyField(to='products.product')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/products/migrations/0006_faq_contactmessage.py b/products/migrations/0006_faq_contactmessage.py new file mode 100644 index 0000000..c74f395 --- /dev/null +++ b/products/migrations/0006_faq_contactmessage.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.1 on 2025-05-29 14:57 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0005_userprofile_wishlist'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FAQ', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', models.CharField(max_length=255, verbose_name='Frage')), + ('answer', models.TextField(verbose_name='Antwort')), + ('category', models.CharField(max_length=100, verbose_name='Kategorie')), + ('order', models.IntegerField(default=0, verbose_name='Reihenfolge')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'FAQ', + 'verbose_name_plural': 'FAQs', + 'ordering': ['category', 'order'], + }, + ), + migrations.CreateModel( + name='ContactMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('order_number', models.CharField(blank=True, max_length=50, null=True)), + ('category', models.CharField(choices=[('general', 'Allgemeine Anfrage'), ('order', 'Bestellung'), ('return', 'Rückgabe/Umtausch'), ('complaint', 'Beschwerde'), ('technical', 'Technische Frage')], max_length=20)), + ('subject', models.CharField(max_length=200)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('new', 'Neu'), ('in_progress', 'In Bearbeitung'), ('resolved', 'Erledigt'), ('closed', 'Geschlossen')], default='new', max_length=20)), + ('staff_notes', models.TextField(blank=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Kontaktanfrage', + 'verbose_name_plural': 'Kontaktanfragen', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/products/migrations/0007_product_extras_description_product_fursuit_type_and_more.py b/products/migrations/0007_product_extras_description_product_fursuit_type_and_more.py new file mode 100644 index 0000000..521e445 --- /dev/null +++ b/products/migrations/0007_product_extras_description_product_fursuit_type_and_more.py @@ -0,0 +1,87 @@ +# Generated by Django 5.2.1 on 2025-05-29 15:01 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0006_faq_contactmessage'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='extras_description', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='product', + name='fursuit_type', + field=models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], default='head', max_length=20), + ), + migrations.AddField( + model_name='product', + name='includes_extras', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='product', + name='is_custom_order', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='product', + name='production_time_weeks', + field=models.IntegerField(default=8), + ), + migrations.AddField( + model_name='product', + name='style', + field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], default='toony', max_length=20), + ), + migrations.AlterField( + model_name='product', + name='stock', + field=models.IntegerField(default=1), + ), + migrations.CreateModel( + name='CustomOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fursuit_type', models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], max_length=20)), + ('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], max_length=20)), + ('character_name', models.CharField(max_length=100)), + ('character_description', models.TextField()), + ('reference_images', models.FileField(blank=True, null=True, upload_to='references/')), + ('special_requests', models.TextField(blank=True)), + ('measurements', models.TextField()), + ('color_preferences', models.TextField()), + ('budget_range', models.CharField(max_length=100)), + ('deadline_request', models.DateField(blank=True, null=True)), + ('status', models.CharField(choices=[('pending', 'Anfrage eingegangen'), ('quoted', 'Angebot erstellt'), ('approved', 'Angebot akzeptiert'), ('in_progress', 'In Arbeit'), ('ready', 'Fertig zur Abholung'), ('shipped', 'Versendet'), ('completed', 'Abgeschlossen'), ('cancelled', 'Storniert')], default='pending', max_length=20)), + ('quoted_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stage', models.CharField(choices=[('design', 'Design & Planung'), ('base', 'Grundform'), ('fur', 'Fell'), ('details', 'Details'), ('electronics', 'Elektronik (optional)'), ('finishing', 'Finishing')], max_length=20)), + ('description', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='progress/')), + ('completed', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('custom_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='products.customorder')), + ], + options={ + 'ordering': ['created_at'], + }, + ), + ] diff --git a/products/migrations/0008_alter_contactmessage_options_and_more.py b/products/migrations/0008_alter_contactmessage_options_and_more.py new file mode 100644 index 0000000..ec4ba79 --- /dev/null +++ b/products/migrations/0008_alter_contactmessage_options_and_more.py @@ -0,0 +1,145 @@ +# Generated by Django 5.2.1 on 2025-05-29 18:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0007_product_extras_description_product_fursuit_type_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='contactmessage', + options={'ordering': ['-created'], 'verbose_name': 'Kontaktanfrage', 'verbose_name_plural': 'Kontaktanfragen'}, + ), + migrations.AlterModelOptions( + name='customorder', + options={'ordering': ['-created']}, + ), + migrations.AlterModelOptions( + name='order', + options={'ordering': ['-created']}, + ), + migrations.AlterModelOptions( + name='orderprogress', + options={'ordering': ['created']}, + ), + migrations.AlterModelOptions( + name='product', + options={'ordering': ['-created']}, + ), + migrations.AlterModelOptions( + name='review', + options={'ordering': ['-created']}, + ), + migrations.RenameField( + model_name='cart', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='cart', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='cartitem', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='contactmessage', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='customorder', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='customorder', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='faq', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='faq', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='order', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='order', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='orderprogress', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='product', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='product', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='review', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='review', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='userprofile', + old_name='created_at', + new_name='created', + ), + migrations.RenameField( + model_name='userprofile', + old_name='updated_at', + new_name='updated', + ), + migrations.RenameField( + model_name='wishlist', + old_name='created_at', + new_name='created', + ), + migrations.AddField( + model_name='orderprogress', + name='updated', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='cart', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_carts', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='order', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_orders', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/products/migrations/0009_remove_product_category_and_more.py b/products/migrations/0009_remove_product_category_and_more.py new file mode 100644 index 0000000..afa79d7 --- /dev/null +++ b/products/migrations/0009_remove_product_category_and_more.py @@ -0,0 +1,147 @@ +# Generated by Django 5.2.1 on 2025-05-30 07:31 + +import django.core.validators +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0008_alter_contactmessage_options_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='category', + ), + migrations.RemoveField( + model_name='product', + name='extras_description', + ), + migrations.RemoveField( + model_name='product', + name='featured', + ), + migrations.RemoveField( + model_name='product', + name='includes_extras', + ), + migrations.RemoveField( + model_name='product', + name='production_time_weeks', + ), + migrations.RemoveField( + model_name='review', + name='updated', + ), + migrations.AddField( + model_name='order', + name='payment_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='order', + name='payment_method', + field=models.CharField(blank=True, choices=[('card', 'Kreditkarte'), ('sepa', 'SEPA-Lastschrift'), ('giropay', 'Giropay'), ('sofort', 'Sofort'), ('bancontact', 'Bancontact')], max_length=20, null=True), + ), + migrations.AddField( + model_name='order', + name='payment_status', + field=models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'Wird bearbeitet'), ('paid', 'Bezahlt'), ('failed', 'Fehlgeschlagen'), ('refunded', 'Zurückerstattet')], default='pending', max_length=20), + ), + migrations.AddField( + model_name='order', + name='stripe_payment_intent_id', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='order', + name='stripe_payment_method_id', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='product', + name='is_featured', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='product', + name='wishlist_users', + field=models.ManyToManyField(blank=True, related_name='wishlist_products', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='customorder', + name='fursuit_type', + field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20), + ), + migrations.AlterField( + model_name='customorder', + name='style', + field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20), + ), + migrations.AlterField( + model_name='product', + name='fursuit_type', + field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], default='other', max_length=20), + ), + migrations.AlterField( + model_name='product', + name='is_custom_order', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='product', + name='price', + field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='product', + name='stock', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='product', + name='style', + field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], default='toony', max_length=20), + ), + migrations.AlterField( + model_name='review', + name='rating', + field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddIndex( + model_name='order', + index=models.Index(fields=['status'], name='products_or_status_bd22a2_idx'), + ), + migrations.AddIndex( + model_name='order', + index=models.Index(fields=['payment_status'], name='products_or_payment_0d94df_idx'), + ), + migrations.AddIndex( + model_name='order', + index=models.Index(fields=['created'], name='products_or_created_a2e72d_idx'), + ), + migrations.AddIndex( + model_name='product', + index=models.Index(fields=['name'], name='products_pr_name_9ff0a3_idx'), + ), + migrations.AddIndex( + model_name='product', + index=models.Index(fields=['price'], name='products_pr_price_9b1a5f_idx'), + ), + migrations.AddIndex( + model_name='product', + index=models.Index(fields=['created'], name='products_pr_created_9a1943_idx'), + ), + migrations.AddIndex( + model_name='product', + index=models.Index(fields=['fursuit_type'], name='products_pr_fursuit_fde435_idx'), + ), + migrations.AddIndex( + model_name='product', + index=models.Index(fields=['style'], name='products_pr_style_de3c68_idx'), + ), + ] diff --git a/products/migrations/0010_galleryimage.py b/products/migrations/0010_galleryimage.py new file mode 100644 index 0000000..92dbc7d --- /dev/null +++ b/products/migrations/0010_galleryimage.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.1 on 2025-05-30 07:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0009_remove_product_category_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='GalleryImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='Titel')), + ('description', models.TextField(blank=True, verbose_name='Beschreibung')), + ('image', models.ImageField(upload_to='gallery/', verbose_name='Bild')), + ('fursuit_type', models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20, verbose_name='Fursuit-Typ')), + ('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20, verbose_name='Stil')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), + ('is_featured', models.BooleanField(default=False, verbose_name='Hervorgehoben')), + ('order', models.IntegerField(default=0, verbose_name='Reihenfolge')), + ], + options={ + 'verbose_name': 'Galeriebild', + 'verbose_name_plural': 'Galeriebilder', + 'ordering': ['order', '-created'], + }, + ), + ] diff --git a/products/migrations/0011_alter_galleryimage_fursuit_type_and_more.py b/products/migrations/0011_alter_galleryimage_fursuit_type_and_more.py new file mode 100644 index 0000000..1430a65 --- /dev/null +++ b/products/migrations/0011_alter_galleryimage_fursuit_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.1 on 2025-05-30 08:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0010_galleryimage'), + ] + + operations = [ + migrations.AlterField( + model_name='galleryimage', + name='fursuit_type', + field=models.CharField(choices=[('full', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Head Only'), ('other', 'Other')], default='full', max_length=20, verbose_name='Fursuit-Typ'), + ), + migrations.AlterField( + model_name='galleryimage', + name='style', + field=models.CharField(choices=[('toony', 'Toony'), ('semi', 'Semi-Realistic'), ('real', 'Realistic'), ('anime', 'Anime')], default='toony', max_length=20, verbose_name='Stil'), + ), + ] diff --git a/products/migrations/0012_category_product_category.py b/products/migrations/0012_category_product_category.py new file mode 100644 index 0000000..8b2227d --- /dev/null +++ b/products/migrations/0012_category_product_category.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.1 on 2025-05-30 11:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0011_alter_galleryimage_fursuit_type_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ('slug', models.SlugField(unique=True, verbose_name='URL-Slug')), + ('description', models.TextField(blank=True, verbose_name='Beschreibung')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Kategorie', + 'verbose_name_plural': 'Kategorien', + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='product', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='products.category', verbose_name='Kategorie'), + ), + ] diff --git a/products/migrations/0013_alter_product_category_delete_category.py b/products/migrations/0013_alter_product_category_delete_category.py new file mode 100644 index 0000000..ead5174 --- /dev/null +++ b/products/migrations/0013_alter_product_category_delete_category.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.1 on 2025-05-30 11:54 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0012_category_product_category'), + ('shop', '0003_contactmessage'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_items', to='shop.category', verbose_name='Kategorie'), + ), + migrations.DeleteModel( + name='Category', + ), + ] diff --git a/products/migrations/0014_payment.py b/products/migrations/0014_payment.py new file mode 100644 index 0000000..35f2f59 --- /dev/null +++ b/products/migrations/0014_payment.py @@ -0,0 +1,52 @@ +# Generated by Django 5.2.1 on 2025-06-02 06:12 + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0013_alter_product_category_delete_category'), + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('variant', models.CharField(max_length=255)), + ('status', models.CharField(choices=[('waiting', 'Waiting for confirmation'), ('preauth', 'Pre-authorized'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected'), ('refunded', 'Refunded'), ('error', 'Error'), ('input', 'Input')], default='waiting', max_length=10)), + ('fraud_status', models.CharField(choices=[('unknown', 'Unknown'), ('accept', 'Passed'), ('reject', 'Rejected'), ('review', 'Review')], default='unknown', max_length=10, verbose_name='fraud check')), + ('fraud_message', models.TextField(blank=True, default='')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('transaction_id', models.CharField(blank=True, max_length=255)), + ('currency', models.CharField(max_length=10)), + ('total', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)), + ('delivery', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)), + ('tax', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)), + ('description', models.TextField(blank=True, default='')), + ('billing_first_name', models.CharField(blank=True, max_length=256)), + ('billing_last_name', models.CharField(blank=True, max_length=256)), + ('billing_address_1', models.CharField(blank=True, max_length=256)), + ('billing_address_2', models.CharField(blank=True, max_length=256)), + ('billing_city', models.CharField(blank=True, max_length=256)), + ('billing_postcode', models.CharField(blank=True, max_length=256)), + ('billing_country_code', models.CharField(blank=True, max_length=2)), + ('billing_country_area', models.CharField(blank=True, max_length=256)), + ('billing_email', models.EmailField(blank=True, max_length=254)), + ('billing_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None)), + ('customer_ip_address', models.GenericIPAddressField(blank=True, null=True)), + ('extra_data', models.TextField(blank=True, default='')), + ('message', models.TextField(blank=True, default='')), + ('token', models.CharField(blank=True, default='', max_length=36)), + ('captured_amount', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='products.order')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/products/migrations/0015_alter_payment_options_remove_payment_billing_phone_and_more.py b/products/migrations/0015_alter_payment_options_remove_payment_billing_phone_and_more.py new file mode 100644 index 0000000..f311277 --- /dev/null +++ b/products/migrations/0015_alter_payment_options_remove_payment_billing_phone_and_more.py @@ -0,0 +1,112 @@ +# Generated by Django 5.2.1 on 2025-06-02 10:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0014_payment'), + ] + + operations = [ + migrations.AlterModelOptions( + name='payment', + options={'ordering': ['-created']}, + ), + migrations.RemoveField( + model_name='payment', + name='billing_phone', + ), + migrations.AlterField( + model_name='payment', + name='billing_address_1', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_address_2', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_city', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_country_area', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_country_code', + field=models.CharField(blank=True, max_length=2, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_first_name', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_last_name', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='billing_postcode', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='description', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='payment', + name='extra_data', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='payment', + name='fraud_message', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='payment', + name='fraud_status', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AlterField( + model_name='payment', + name='message', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='payment', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.order'), + ), + migrations.AlterField( + model_name='payment', + name='status', + field=models.CharField(max_length=10), + ), + migrations.AlterField( + model_name='payment', + name='token', + field=models.CharField(blank=True, max_length=36, null=True), + ), + migrations.AlterField( + model_name='payment', + name='transaction_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/products/migrations/__init__.py b/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/models.py b/products/models.py new file mode 100644 index 0000000..56481bd --- /dev/null +++ b/products/models.py @@ -0,0 +1,416 @@ +from django.db import models +from django.contrib.auth.models import User +from django.core.validators import MinValueValidator, MaxValueValidator +from django.db.models.signals import post_save +from django.dispatch import receiver +from shop.models import Category # Import Category aus der shop App + +# Create your models here. + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + phone = models.CharField(max_length=20, blank=True) + address = models.TextField(blank=True) + default_shipping_address = models.TextField(blank=True) + newsletter = models.BooleanField(default=False) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Profil von {self.user.username}" + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + instance.userprofile.save() + +class Product(models.Model): + FURSUIT_TYPE_CHOICES = [ + ('partial', 'Partial'), + ('fullsuit', 'Fullsuit'), + ('head', 'Head Only'), + ('paws', 'Paws'), + ('tail', 'Tail'), + ('other', 'Other'), + ] + + STYLE_CHOICES = [ + ('toony', 'Toony'), + ('semi_realistic', 'Semi-Realistic'), + ('realistic', 'Realistic'), + ('anime', 'Anime'), + ('chibi', 'Chibi'), + ] + + name = models.CharField(max_length=200) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)]) + stock = models.IntegerField(default=0) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + image = models.ImageField(upload_to='products/', null=True, blank=True) + + # Neue Felder + fursuit_type = models.CharField(max_length=20, choices=FURSUIT_TYPE_CHOICES, default='other') + style = models.CharField(max_length=20, choices=STYLE_CHOICES, default='toony') + is_featured = models.BooleanField(default=False) + is_custom_order = models.BooleanField(default=False) + + # Beziehungen + category = models.ForeignKey('shop.Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='product_items', verbose_name='Kategorie') + wishlist_users = models.ManyToManyField(User, related_name='wishlist_products', blank=True) + + def __str__(self): + return self.name + + def average_rating(self): + if self.reviews.exists(): + return self.reviews.aggregate(models.Avg('rating'))['rating__avg'] + return 0 + + class Meta: + ordering = ['-created'] + indexes = [ + models.Index(fields=['name']), + models.Index(fields=['price']), + models.Index(fields=['created']), + models.Index(fields=['fursuit_type']), + models.Index(fields=['style']), + ] + +class Review(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews') + user = models.ForeignKey(User, on_delete=models.CASCADE) + rating = models.IntegerField(validators=[MinValueValidator(1)]) + comment = models.TextField() + created = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('product', 'user') + ordering = ['-created'] + + def __str__(self): + return f'Review by {self.user.username} for {self.product.name}' + +class Cart(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='product_carts') + session_id = models.CharField(max_length=100, null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def get_total(self): + return sum(item.get_subtotal() for item in self.items.all()) + + def __str__(self): + return f"Cart {self.id} - {'User: ' + self.user.username if self.user else 'Session: ' + self.session_id}" + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + created = models.DateTimeField(auto_now_add=True) + + def get_subtotal(self): + return self.product.price * self.quantity + + def __str__(self): + return f"{self.quantity}x {self.product.name} in Cart {self.cart.id}" + + class Meta: + unique_together = ('cart', 'product') + +class Order(models.Model): + STATUS_CHOICES = [ + ('pending', 'Ausstehend'), + ('processing', 'In Bearbeitung'), + ('shipped', 'Versendet'), + ('delivered', 'Geliefert'), + ('cancelled', 'Storniert'), + ] + + PAYMENT_STATUS_CHOICES = [ + ('pending', 'Ausstehend'), + ('processing', 'Wird bearbeitet'), + ('paid', 'Bezahlt'), + ('failed', 'Fehlgeschlagen'), + ('refunded', 'Zurückerstattet'), + ] + + PAYMENT_METHOD_CHOICES = [ + ('card', 'Kreditkarte'), + ('sepa', 'SEPA-Lastschrift'), + ('giropay', 'Giropay'), + ('sofort', 'Sofort'), + ('bancontact', 'Bancontact'), + ] + + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='product_orders') + full_name = models.CharField(max_length=200) + email = models.EmailField() + address = models.TextField() + phone = models.CharField(max_length=20) + total_amount = models.DecimalField(max_digits=10, decimal_places=2) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + # Zahlungsinformationen + payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending') + payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES, null=True, blank=True) + stripe_payment_intent_id = models.CharField(max_length=100, null=True, blank=True) + stripe_payment_method_id = models.CharField(max_length=100, null=True, blank=True) + payment_date = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"Bestellung #{self.id} von {self.full_name}" + + class Meta: + ordering = ['-created'] + indexes = [ + models.Index(fields=['status']), + models.Index(fields=['payment_status']), + models.Index(fields=['created']), + ] + + def get_payment_status_display_class(self): + status_classes = { + 'pending': 'warning', + 'processing': 'info', + 'paid': 'success', + 'failed': 'danger', + 'refunded': 'secondary', + } + return status_classes.get(self.payment_status, 'secondary') + + def get_order_status_display_class(self): + status_classes = { + 'pending': 'warning', + 'processing': 'info', + 'shipped': 'primary', + 'delivered': 'success', + 'cancelled': 'danger', + } + return status_classes.get(self.status, 'secondary') + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) + product_name = models.CharField(max_length=200) + price = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField() + + def get_subtotal(self): + return self.price * self.quantity + + def __str__(self): + return f"{self.quantity}x {self.product_name} in Bestellung #{self.order.id}" + +class Wishlist(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + products = models.ManyToManyField(Product) + created = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Wunschliste von {self.user.username}" + +class FAQ(models.Model): + question = models.CharField(max_length=255, verbose_name='Frage') + answer = models.TextField(verbose_name='Antwort') + category = models.CharField(max_length=100, verbose_name='Kategorie') + order = models.IntegerField(default=0, verbose_name='Reihenfolge') + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['category', 'order'] + verbose_name = 'FAQ' + verbose_name_plural = 'FAQs' + + def __str__(self): + return self.question + +class ContactMessage(models.Model): + CATEGORY_CHOICES = [ + ('general', 'Allgemeine Anfrage'), + ('order', 'Bestellung'), + ('return', 'Rückgabe/Umtausch'), + ('complaint', 'Beschwerde'), + ('technical', 'Technische Frage'), + ] + + name = models.CharField(max_length=100) + email = models.EmailField() + order_number = models.CharField(max_length=50, blank=True, null=True) + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + subject = models.CharField(max_length=200) + message = models.TextField() + created = models.DateTimeField(auto_now_add=True) + status = models.CharField( + max_length=20, + choices=[ + ('new', 'Neu'), + ('in_progress', 'In Bearbeitung'), + ('resolved', 'Erledigt'), + ('closed', 'Geschlossen'), + ], + default='new' + ) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + staff_notes = models.TextField(blank=True) + + class Meta: + ordering = ['-created'] + verbose_name = 'Kontaktanfrage' + verbose_name_plural = 'Kontaktanfragen' + + def __str__(self): + return f"{self.category} - {self.subject} ({self.created.strftime('%d.%m.%Y')})" + +class CustomOrder(models.Model): + STATUS_CHOICES = [ + ('pending', 'Anfrage eingegangen'), + ('quoted', 'Angebot erstellt'), + ('approved', 'Angebot akzeptiert'), + ('in_progress', 'In Arbeit'), + ('ready', 'Fertig zur Abholung'), + ('shipped', 'Versendet'), + ('completed', 'Abgeschlossen'), + ('cancelled', 'Storniert'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE) + fursuit_type = models.CharField(max_length=20, choices=Product.FURSUIT_TYPE_CHOICES) + style = models.CharField(max_length=20, choices=Product.STYLE_CHOICES) + character_name = models.CharField(max_length=100) + character_description = models.TextField() + reference_images = models.FileField(upload_to='references/', null=True, blank=True) + special_requests = models.TextField(blank=True) + measurements = models.TextField() + color_preferences = models.TextField() + budget_range = models.CharField(max_length=100) + deadline_request = models.DateField(null=True, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + quoted_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Custom Order #{self.id} - {self.character_name}" + + class Meta: + ordering = ['-created'] + +class OrderProgress(models.Model): + PROGRESS_CHOICES = [ + ('design', 'Design & Planung'), + ('base', 'Grundform'), + ('fur', 'Fell'), + ('details', 'Details'), + ('electronics', 'Elektronik (optional)'), + ('finishing', 'Finishing'), + ] + + custom_order = models.ForeignKey(CustomOrder, on_delete=models.CASCADE, related_name='progress_updates') + stage = models.CharField(max_length=20, choices=PROGRESS_CHOICES) + description = models.TextField() + image = models.ImageField(upload_to='progress/', null=True, blank=True) + completed = models.BooleanField(default=False) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.custom_order.character_name} - {self.get_stage_display()}" + + class Meta: + ordering = ['created'] + +class GalleryImage(models.Model): + FURSUIT_TYPE_CHOICES = [ + ('full', 'Fullsuit'), + ('partial', 'Partial'), + ('head', 'Head Only'), + ('other', 'Other'), + ] + + STYLE_CHOICES = [ + ('toony', 'Toony'), + ('semi', 'Semi-Realistic'), + ('real', 'Realistic'), + ('anime', 'Anime'), + ] + + title = models.CharField(max_length=200, verbose_name='Titel') + description = models.TextField(blank=True, verbose_name='Beschreibung') + image = models.ImageField(upload_to='gallery/', verbose_name='Bild') + fursuit_type = models.CharField( + max_length=20, + choices=FURSUIT_TYPE_CHOICES, + default='full', + verbose_name='Fursuit-Typ' + ) + style = models.CharField( + max_length=20, + choices=STYLE_CHOICES, + default='toony', + verbose_name='Stil' + ) + is_featured = models.BooleanField(default=False, verbose_name='Hervorgehoben') + order = models.IntegerField(default=0, verbose_name='Reihenfolge') + created = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am') + + class Meta: + verbose_name = 'Galeriebild' + verbose_name_plural = 'Galeriebilder' + ordering = ['order', '-created'] + + def __str__(self): + return self.title + +class Payment(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE) + variant = models.CharField(max_length=255) + status = models.CharField(max_length=10) + fraud_status = models.CharField(max_length=10, null=True, blank=True) + fraud_message = models.TextField(null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + transaction_id = models.CharField(max_length=255, null=True, blank=True) + currency = models.CharField(max_length=10) + total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + delivery = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + description = models.TextField(null=True, blank=True) + billing_first_name = models.CharField(max_length=256, null=True, blank=True) + billing_last_name = models.CharField(max_length=256, null=True, blank=True) + billing_address_1 = models.CharField(max_length=256, null=True, blank=True) + billing_address_2 = models.CharField(max_length=256, null=True, blank=True) + billing_city = models.CharField(max_length=256, null=True, blank=True) + billing_postcode = models.CharField(max_length=256, null=True, blank=True) + billing_country_code = models.CharField(max_length=2, null=True, blank=True) + billing_country_area = models.CharField(max_length=256, null=True, blank=True) + billing_email = models.EmailField(null=True, blank=True) + customer_ip_address = models.GenericIPAddressField(null=True, blank=True) + extra_data = models.TextField(null=True, blank=True) + message = models.TextField(null=True, blank=True) + token = models.CharField(max_length=36, null=True, blank=True) + captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + + class Meta: + ordering = ['-created'] + + def __str__(self): + return f"Payment {self.id} for Order {self.order.id}" + + @property + def attrs(self): + return {} + + def get_failure_url(self): + return f'/payment/failed/{self.order.id}/' + + def get_success_url(self): + return f'/payment/success/{self.order.id}/' diff --git a/products/serializers.py b/products/serializers.py new file mode 100644 index 0000000..6afd385 --- /dev/null +++ b/products/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from .models import Product, Review + +class ReviewSerializer(serializers.ModelSerializer): + user = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = Review + fields = ['id', 'user', 'rating', 'comment', 'created_at', 'product'] + read_only_fields = ['created_at'] + +class ProductSerializer(serializers.ModelSerializer): + average_rating = serializers.FloatField(read_only=True) + reviews = ReviewSerializer(many=True, read_only=True) + + class Meta: + model = Product + fields = ['id', 'name', 'description', 'price', 'stock', 'category', + 'featured', 'image', 'created_at', 'average_rating', 'reviews'] + read_only_fields = ['created_at'] \ No newline at end of file diff --git a/products/templates/base.html b/products/templates/base.html new file mode 100644 index 0000000..2624da4 --- /dev/null +++ b/products/templates/base.html @@ -0,0 +1,294 @@ +{% load static %} + + + + + + {% block title %}Unser Shop{% endblock %} + + + + + + {% block extra_css %}{% endblock %} + + + + + + +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% block content %}{% endblock %} +
+ + +
+
+
+
+
Über uns
+

Ihr vertrauenswürdiger Online-Shop für hochwertige Produkte.

+
+
+
Kontakt
+

E-Mail: info@shop.de
+ Tel: +49 123 456789

+
+
+
Links
+ +
+
+
+
+ © {% now "Y" %} Unser Shop. Alle Rechte vorbehalten. +
+
+
+
+ + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/products/templates/products/add_progress.html b/products/templates/products/add_progress.html new file mode 100644 index 0000000..06eb09f --- /dev/null +++ b/products/templates/products/add_progress.html @@ -0,0 +1,99 @@ +{% extends 'base.html' %} + +{% block title %}Fortschritt hinzufügen - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+
+ Fortschritts-Update für {{ order.character_name }} +
+
+
+
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.stage }} + {% if form.stage.errors %} +
+ {% for error in form.stage.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.description }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.image }} + {% if form.image.errors %} +
+ {% for error in form.image.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+
+ {{ form.completed }} + +
+ {% if form.completed.errors %} +
+ {% for error in form.completed.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + Zurück + + +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/cart_detail.html b/products/templates/products/cart_detail.html new file mode 100644 index 0000000..6dbc5ab --- /dev/null +++ b/products/templates/products/cart_detail.html @@ -0,0 +1,85 @@ +{% extends 'base.html' %} + +{% block title %}Warenkorb - {{ block.super }}{% endblock %} + +{% block content %} +

Ihr Warenkorb

+ +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} + +{% if cart.items.all %} +
+
+ + + + + + + + + + + + {% for item in cart.items.all %} + + + + + + + + {% endfor %} + + + + + + + + +
ProduktPreis pro StückMengeSumme
+ {{ item.product.name }} + {{ item.product.price }} € +
+ {% csrf_token %} + + +
+
{{ item.get_subtotal }} € +
+ {% csrf_token %} + +
+
Gesamtsumme:{{ cart.get_total }} €
+
+
+ +
+ + Weiter einkaufen + +
+ {% csrf_token %} + +
+
+{% else %} +
+ Ihr Warenkorb ist leer. Jetzt einkaufen +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/checkout.html b/products/templates/products/checkout.html new file mode 100644 index 0000000..dc4d9b7 --- /dev/null +++ b/products/templates/products/checkout.html @@ -0,0 +1,90 @@ +{% extends 'shop/base.html' %} +{% load static %} + +{% block title %}Kasse - {{ block.super }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+

Checkout

+ +
+
+
+
+
Bestellübersicht
+
+
+ + + + + + + + + + {% for item in order.items.all %} + + + + + + {% endfor %} + + + + + + + +
ArtikelMengePreis
{{ item.product_name }}{{ item.quantity }}{{ item.price }} €
Gesamtsumme:{{ order.total_amount }} €
+
+
+
+ +
+
+
+
Zahlungsmethoden
+
+
+ {% include 'products/payment_button.html' with form=form order=order %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/contact.html b/products/templates/products/contact.html new file mode 100644 index 0000000..39abd90 --- /dev/null +++ b/products/templates/products/contact.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} + +{% block title %}Kontakt - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Kontaktieren Sie uns

+
+
+ +
+
+
E-Mail
+

info@shop.de

+ +
Telefon
+

+49 123 456789

+
+
+
Geschäftszeiten
+

Mo-Fr: 9:00 - 18:00 Uhr
+ Sa: 10:00 - 14:00 Uhr

+
+
+ + +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
+
+ + {{ form.name }} + {% if form.name.errors %} +
+ {% for error in form.name.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ + {{ form.email }} + {% if form.email.errors %} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ +
+
+ + {{ form.category }} + {% if form.category.errors %} +
+ {% for error in form.category.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ + {{ form.order_number }} + {% if form.order_number.errors %} +
+ {% for error in form.order_number.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ +
+ + {{ form.subject }} + {% if form.subject.errors %} +
+ {% for error in form.subject.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.message }} + {% if form.message.errors %} +
+ {% for error in form.message.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+
+
+ + +
+

Haben Sie eine allgemeine Frage?

+ + FAQ ansehen + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/contact_success.html b/products/templates/products/contact_success.html new file mode 100644 index 0000000..9590dd4 --- /dev/null +++ b/products/templates/products/contact_success.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block title %}Nachricht gesendet - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+ +

Vielen Dank für Ihre Nachricht!

+

+ Wir haben Ihre Anfrage erhalten und werden uns schnellstmöglich bei Ihnen melden. +

+

+ Unsere durchschnittliche Antwortzeit beträgt 24-48 Stunden an Werktagen. +

+
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/custom_order.html b/products/templates/products/custom_order.html new file mode 100644 index 0000000..88c952a --- /dev/null +++ b/products/templates/products/custom_order.html @@ -0,0 +1,248 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Custom Fursuit Bestellung - {{ block.super }}{% endblock %} + +{% block content %} +
+ +
+ Kasico Art & Design Logo +

Custom Fursuit Anfrage

+

Lassen Sie Ihren Traum-Fursuit Realität werden

+
+ +
+ +
+
+
+ +
+

Bestellprozess

+
    +
  1. + Formular ausfüllen +
  2. +
  3. + Angebot erhalten +
  4. +
  5. + Anzahlung leisten +
  6. +
  7. + Design-Abstimmung +
  8. +
  9. + Produktion & Updates +
  10. +
  11. + Fertigstellung & Versand +
  12. +
+ +
Wichtige Hinweise
+
    +
  • + Produktionszeit: 8-12 Wochen +
  • +
  • + Anzahlung: 30% +
  • +
  • + Genaue Maße erforderlich +
  • +
+ + +
+
+ + +
+
+
+ {% csrf_token %} + + +
Character Informationen
+
+
+ + +
+
+ + +
+
+ + +
Design Präferenzen
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Zusätzliche Details
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/custom_order_detail.html b/products/templates/products/custom_order_detail.html new file mode 100644 index 0000000..a405bcc --- /dev/null +++ b/products/templates/products/custom_order_detail.html @@ -0,0 +1,179 @@ +{% extends 'base.html' %} + +{% block title %}Custom Order #{{ order.id }} - {{ block.super }}{% endblock %} + +{% block content %} +
+
+ +
+
+
+
Anfrage-Details
+
+
+
    +
  • + Status: + + {{ order.get_status_display }} + +
  • +
  • + Character: + {{ order.character_name }} +
  • +
  • + Typ: + {{ order.get_fursuit_type_display }} +
  • +
  • + Stil: + {{ order.get_style_display }} +
  • +
  • + Budget: + {{ order.budget_range }} +
  • + {% if order.deadline_request %} +
  • + Gewünschter Termin: + {{ order.deadline_request }} +
  • + {% endif %} +
  • + Bestelldatum: + {{ order.created|date:"d.m.Y" }} +
  • +
+ + {% if order.quoted_price %} +
+
Angebot
+

{{ order.quoted_price }} €

+
+ {% endif %} +
+
+ + +
+
+
Character-Details
+
+
+
Beschreibung
+

{{ order.character_description }}

+ + {% if order.reference_images %} +
Referenzbilder
+ Referenzbild + {% endif %} + +
Farbwünsche
+

{{ order.color_preferences }}

+ + {% if order.special_requests %} +
Besondere Wünsche
+

{{ order.special_requests }}

+ {% endif %} +
+
+
+ + +
+
+
+
Fortschritt
+ {% if user.is_staff %} + + Update hinzufügen + + {% endif %} +
+
+ {% if progress_updates %} +
+ {% for update in progress_updates %} +
+
+
+
+ {{ update.get_stage_display }} + {% if update.completed %} + + {% endif %} +
+

{{ update.description }}

+ {% if update.image %} + Fortschrittsbild + {% endif %} + + {{ update.created|date:"d.m.Y H:i" }} + +
+
+ {% endfor %} +
+ {% else %} +
+ +

Noch keine Fortschritts-Updates vorhanden.

+
+ {% endif %} +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/custom_order_success.html b/products/templates/products/custom_order_success.html new file mode 100644 index 0000000..41dd15e --- /dev/null +++ b/products/templates/products/custom_order_success.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} + +{% block title %}Anfrage erfolgreich - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+ +

Ihre Fursuit-Anfrage wurde erfolgreich übermittelt!

+

+ Vielen Dank für Ihr Interesse an einem Custom Fursuit. +

+ +
+
+
Ihre Anfrage-Details
+
    +
  • Anfrage-ID: #{{ order.id }}
  • +
  • Character: {{ order.character_name }}
  • +
  • Typ: {{ order.get_fursuit_type_display }}
  • +
  • Stil: {{ order.get_style_display }}
  • +
  • Status: {{ order.get_status_display }}
  • +
+
+
+ + + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/dashboard.html b/products/templates/products/dashboard.html new file mode 100644 index 0000000..d206b8b --- /dev/null +++ b/products/templates/products/dashboard.html @@ -0,0 +1,370 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Mein Dashboard - {{ block.super }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+
+
+
+

Willkommen, {{ user.get_full_name|default:user.username }}!

+

Hier finden Sie alle wichtigen Informationen zu Ihrem Account.

+
+
+

Mitglied seit: {{ user.date_joined|date:"d.m.Y" }}

+

Letzte Anmeldung: {{ user.last_login|date:"d.m.Y H:i" }}

+
+
+
+
+
+
+ + +
+
+
+
+
Bestellungen
+

{{ stats.total_orders }}

+
+
+
+
+
+
+
Custom Orders
+

{{ stats.total_custom_orders }}

+
+
+
+
+
+
+
Bewertungen
+

{{ stats.total_reviews }}

+
+
+
+
+
+
+
Wunschliste
+

{{ stats.wishlist_count }}

+
+
+
+
+ +
+ +
+ +
+
+
Profil
+ + Bearbeiten + +
+
+
+
+
+ +
+
+
+
{{ user.get_full_name }}
+

{{ user.email }}

+
+
+
+
    +
  • + {{ user_profile.phone|default:"Keine Telefonnummer" }} +
  • +
  • + {{ user_profile.address|default:"Keine Adresse"|linebreaksbr }} +
  • +
  • + Newsletter: + {% if user_profile.newsletter %} + Aktiviert + {% else %} + Deaktiviert + {% endif %} +
  • +
+
+
+ + + + + +
+
+
Meine letzten Bewertungen
+
+
+ {% if reviews %} + {% for review in reviews %} +
+
+
{{ review.product.name }}
+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= review.rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+

{{ review.created|date:"d.m.Y" }}

+

{{ review.comment|truncatechars:100 }}

+
+ {% endfor %} + {% else %} +

Sie haben noch keine Bewertungen abgegeben.

+ {% endif %} +
+
+
+ + +
+ +
+
+
Meine Custom Orders
+ + Neue Anfrage + +
+
+ {% if custom_orders %} +
+ + + + + + + + + + + + + + {% for order in custom_orders %} + + + + + + + + + + {% endfor %} + +
IDCharacterTypStatusFortschrittErstellt am
#{{ order.id }}{{ order.character_name }}{{ order.get_fursuit_type_display }} + + {{ order.get_status_display }} + + +
+
+ {{ order.progress_percentage }}% +
+
+
{{ order.created|date:"d.m.Y" }} + + Details + +
+
+ {% else %} +
+ +

Sie haben noch keine Custom Orders erstellt.

+ + Erste Custom Order erstellen + +
+ {% endif %} +
+
+ + +
+
+
Letzte Bestellungen
+ + Alle anzeigen + +
+
+ {% if recent_orders %} +
+ + + + + + + + + + + + + {% for order in recent_orders %} + + + + + + + + + + +
BestellungArtikelDatumStatusBetrag
#{{ order.id }}{{ order.items_count }}{{ order.created|date:"d.m.Y" }} + + {{ order.get_status_display }} + + {{ order.total_amount }} € + +
+ + + + + + + + + + {% for item in order.items.all %} + + + + + + + {% endfor %} + + + + + + + +
ArtikelAnzahlPreisGesamt
{{ item.product_name }}{{ item.quantity }}{{ item.price }} €{{ item.get_subtotal }} €
Gesamtbetrag:{{ order.total_amount }} €
+
+
+ +
+
+
+ {% endfor %} + + +
+ {% else %} +
+ +

Sie haben noch keine Bestellungen aufgegeben.

+ + Jetzt einkaufen + +
+ {% endif %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/faq.html b/products/templates/products/faq.html new file mode 100644 index 0000000..f02f6af --- /dev/null +++ b/products/templates/products/faq.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} + +{% block title %}FAQ - {{ block.super }}{% endblock %} + +{% block content %} +
+

Häufig gestellte Fragen

+ + +
+
+ + {% for category in categories %} + + {% endfor %} +
+
+ + +
+ {% for faq in faqs %} +
+

+ +

+
+
+ {{ faq.answer|linebreaks }} +
+
+
+ {% empty %} +
+ Aktuell sind keine FAQ-Einträge verfügbar. +
+ {% endfor %} +
+ + +
+

Keine Antwort auf Ihre Frage gefunden?

+ + Kontaktieren Sie uns + +
+
+ +{% block extra_js %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/gallery.html b/products/templates/products/gallery.html new file mode 100644 index 0000000..5dafcfc --- /dev/null +++ b/products/templates/products/gallery.html @@ -0,0 +1,188 @@ +{% extends "shop/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}Galerie - Kasico Art & Design{% endblock %} + +{% block content %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/order_confirmation.html b/products/templates/products/order_confirmation.html new file mode 100644 index 0000000..bb7403a --- /dev/null +++ b/products/templates/products/order_confirmation.html @@ -0,0 +1,59 @@ +{% extends 'base.html' %} + +{% block title %}Bestellung bestätigt - {{ block.super }}{% endblock %} + +{% block content %} +
+
+

Vielen Dank für Ihre Bestellung!

+

Ihre Bestellnummer lautet: #{{ order.id }}

+

Wir haben Ihnen eine Bestätigungs-E-Mail an {{ order.email }} gesendet.

+ +
+

Lieferadresse:

+

+ {{ order.full_name }}
+ {{ order.address|linebreaks }} +

+
+ +
+
+

Bestellte Artikel

+
+
+ + + + + + + + + + + {% for item in order.items.all %} + + + + + + + {% endfor %} + + + + + + + +
ArtikelMengePreisSumme
{{ item.product_name }}{{ item.quantity }}{{ item.price }} €{{ item.get_subtotal }} €
Gesamtsumme:{{ order.total_amount }} €
+
+
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/order_history.html b/products/templates/products/order_history.html new file mode 100644 index 0000000..a5deaeb --- /dev/null +++ b/products/templates/products/order_history.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Bestellhistorie - Fursuit Shop{% endblock %} + +{% block content %} +
+

Meine Bestellungen

+ + {% if orders %} +
+ + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + +
BestellnummerDatumStatusZahlungsstatusGesamtbetragDetails
#{{ order.id }}{{ order.created|date:"d.m.Y H:i" }} + + {{ order.get_status_display }} + + + + {{ order.get_payment_status_display }} + + {{ order.total_amount }} € + +
+ + + + + + + + + + {% for item in order.items.all %} + + + + + + + {% endfor %} + + + + + + + +
ArtikelAnzahlPreisGesamt
{{ item.product_name }}{{ item.quantity }}{{ item.price }} €{{ item.get_subtotal }} €
Gesamtbetrag:{{ order.total_amount }} €
+
+
+ + + + + {% endfor %} + + + + {% else %} +
+ Sie haben noch keine Bestellungen aufgegeben. + Jetzt einkaufen +
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/payment_button.html b/products/templates/products/payment_button.html new file mode 100644 index 0000000..acb898f --- /dev/null +++ b/products/templates/products/payment_button.html @@ -0,0 +1,24 @@ +{% load static %} + +
+ + {{ form.render }} + + + +
+ + \ No newline at end of file diff --git a/products/templates/products/payment_failed.html b/products/templates/products/payment_failed.html new file mode 100644 index 0000000..783e804 --- /dev/null +++ b/products/templates/products/payment_failed.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Zahlung fehlgeschlagen - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Zahlung fehlgeschlagen

+
+
+
+ +
+ +

Bestellung #{{ order.id }}

+

Leider konnte Ihre Zahlung nicht verarbeitet werden.

+ +
+

Mögliche Gründe:

+
    +
  • Unzureichende Deckung auf Ihrem Konto
  • +
  • Falsche oder abgelaufene Kartendaten
  • +
  • Technische Probleme bei der Zahlungsabwicklung
  • +
+
+ +

Sie können die Zahlung erneut versuchen oder eine andere Zahlungsmethode wählen.

+ + + +
+

Bei weiteren Fragen kontaktieren Sie bitte unseren Kundenservice:

+ + Kontakt aufnehmen + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/payment_process.html b/products/templates/products/payment_process.html new file mode 100644 index 0000000..ba3f31d --- /dev/null +++ b/products/templates/products/payment_process.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Zahlung - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Zahlungsprozess

+
+
+

Bestellung #{{ order.id }}

+

Gesamtbetrag: {{ order.total_amount }} €

+ +
+

Zahlungsinformationen

+

Sie werden zu PayPal weitergeleitet, um die Zahlung sicher abzuschließen.

+
+ +
+ {{ form.render }} +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/payment_success.html b/products/templates/products/payment_success.html new file mode 100644 index 0000000..4528f45 --- /dev/null +++ b/products/templates/products/payment_success.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Zahlung erfolgreich - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+
+

Zahlung erfolgreich!

+
+
+
+ +
+ +

Bestellung #{{ order.id }}

+

Vielen Dank für Ihre Bestellung! Ihre Zahlung wurde erfolgreich verarbeitet.

+ +
+

Bestelldetails:

+
    +
  • Bestellnummer: #{{ order.id }}
  • +
  • Gesamtbetrag: {{ order.total_amount }} €
  • +
  • Bestelldatum: {{ order.created|date:"d.m.Y H:i" }}
  • +
  • Status: {{ order.get_status_display }}
  • +
+
+ +

Sie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details zu Ihrer Bestellung.

+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/product_detail.html b/products/templates/products/product_detail.html new file mode 100644 index 0000000..7f3db56 --- /dev/null +++ b/products/templates/products/product_detail.html @@ -0,0 +1,338 @@ +{% extends 'base.html' %} + +{% block title %}{{ product.name }} - {{ block.super }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+
+
+

{{ product.name }}

+ + {% if product.image %} +
+ {{ product.name }} +
+ +
+ × + +
+ {% endif %} + +

{{ product.description }}

+ +
+
+
Preis
+
{{ product.price }} €
+
+
+
Verfügbarkeit
+
{{ product.stock }} Stück
+
+
+
Hinzugefügt am
+
{{ product.created|date:"d.m.Y" }}
+
+
+
Bewertung
+
+ {% with avg_rating=product.average_rating %} + {% if avg_rating > 0 %} + {{ avg_rating|floatformat:1 }} von 5 Sternen + {% else %} + Noch keine Bewertungen + {% endif %} + {% endwith %} +
+
+
+ +
+
+ {% csrf_token %} + + +
+
+
+
+
+ + +
+
+
+

Bewertungen

+ {% if user.is_authenticated and not user_has_reviewed %} + + {% endif %} +
+
+ {% if product.reviews.all %} + {% for review in product.reviews.all %} +
+
+
{{ review.user.username }}
+ {{ review.created|date:"d.m.Y" }} +
+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= review.rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+

{{ review.comment }}

+
+ {% if not forloop.last %}
{% endif %} + {% endfor %} + {% else %} +

Noch keine Bewertungen vorhanden.

+ {% endif %} +
+
+
+
+ + +{% if user.is_authenticated and not user_has_reviewed %} + +{% endif %} +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/product_list.html b/products/templates/products/product_list.html new file mode 100644 index 0000000..3490daa --- /dev/null +++ b/products/templates/products/product_list.html @@ -0,0 +1,253 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Produkte - {{ block.super }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+

Unsere Produkte

+ + +
+
+ +
+ +
+ + +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + Filter zurücksetzen + +
+
+
+ + + {% if featured_products and not request.GET.search %} + + {% endif %} + + + {% if products %} +
+ {% for product in products %} +
+
+ + {{ product.get_fursuit_type_display }} + + {% if product.is_custom_order %} + + Custom Order + + {% endif %} + {% if product.image %} + {{ product.name }} + {% endif %} +
+
{{ product.name }}
+

{{ product.description|truncatewords:20 }}

+
+ {{ product.price }} € +
+ {% if product.average_rating %} + {% for i in "12345"|make_list %} + + {% endfor %} + {% else %} + {% for i in "12345"|make_list %} + + {% endfor %} + {% endif %} + ({{ product.reviews.count|default:"0" }}) +
+
+

+ + + {% if product.stock > 5 %} + Auf Lager + {% elif product.stock > 0 %} + Nur noch {{ product.stock }} verfügbar + {% else %} + Ausverkauft + {% endif %} + +

+
+ +
+
+ {% endfor %} +
+ + + {% if is_paginated %} + + {% endif %} + {% else %} +
+ Keine Produkte gefunden. + {% if request.GET %} + Filter zurücksetzen + {% endif %} +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/profile.html b/products/templates/products/profile.html new file mode 100644 index 0000000..30233e1 --- /dev/null +++ b/products/templates/products/profile.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}Mein Profil - {{ block.super }}{% endblock %} + +{% block content %} +
+
+ + + + +
+
+
+
Profil bearbeiten
+
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% if field.errors %} +
+ {{ field.errors }} +
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/wishlist.html b/products/templates/products/wishlist.html new file mode 100644 index 0000000..cf10f3f --- /dev/null +++ b/products/templates/products/wishlist.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Meine Wunschliste - Fursuit Shop{% endblock %} + +{% block content %} +
+
+ + + + +
+

Meine Wunschliste

+ + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + {% if wishlist.products.all %} +
+ {% for product in wishlist.products.all %} +
+
+ {% if product.image %} + {{ product.name }} + {% else %} + Placeholder + {% endif %} +
+
{{ product.name }}
+

{{ product.description|truncatewords:20 }}

+

+ Preis: {{ product.price }} €
+ Typ: {{ product.get_fursuit_type_display }}
+ Style: {{ product.get_style_display }} +

+
+ +
+
+ {% endfor %} +
+ {% else %} +
+ Ihre Wunschliste ist leer. + Entdecken Sie unsere Produkte +
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/templates/registration/register.html b/products/templates/registration/register.html new file mode 100644 index 0000000..f4957f0 --- /dev/null +++ b/products/templates/registration/register.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% block title %}Registrieren - {{ block.super }}{% endblock %} + +{% block content %} +
+
+
+
+

Registrieren

+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + +
+ +
+
+ +
+

Bereits ein Konto? Jetzt anmelden

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/products/tests.py b/products/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/products/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/products/urls.py b/products/urls.py new file mode 100644 index 0000000..0465ef7 --- /dev/null +++ b/products/urls.py @@ -0,0 +1,47 @@ +from django.urls import path +from .views import ( + ProductListView, ProductDetailView, + add_to_cart, cart_detail, update_cart_item, + remove_from_cart, create_order, checkout, + add_review, profile_view, order_history, + wishlist_view, add_to_wishlist, remove_from_wishlist, + faq_list, contact, contact_success, + custom_order, custom_order_success, + custom_order_detail, gallery, + add_progress_update, + payment_process, + payment_success, + payment_failed, + register +) + +app_name = 'products' + +urlpatterns = [ + path('', ProductListView.as_view(), name='product_list'), + path('product//', ProductDetailView.as_view(), name='product_detail'), + path('cart/add//', add_to_cart, name='add_to_cart'), + path('cart/', cart_detail, name='cart_detail'), + path('cart/update//', update_cart_item, name='update_cart_item'), + path('cart/remove//', remove_from_cart, name='remove_from_cart'), + path('order/create/', create_order, name='create_order'), + path('checkout//', checkout, name='checkout'), + path('product//review/', add_review, name='add_review'), + path('profile/', profile_view, name='profile'), + path('orders/', order_history, name='order_history'), + path('wishlist/', wishlist_view, name='wishlist'), + path('wishlist/add//', add_to_wishlist, name='add_to_wishlist'), + path('wishlist/remove//', remove_from_wishlist, name='remove_from_wishlist'), + path('faq/', faq_list, name='faq'), + path('contact/', contact, name='contact'), + path('contact/success/', contact_success, name='contact_success'), + path('custom-order/', custom_order, name='custom_order'), + path('custom-order/success//', custom_order_success, name='custom_order_success'), + path('custom-orders//', custom_order_detail, name='custom_order_detail'), + path('custom-orders//update/', add_progress_update, name='add_progress_update'), + path('gallery/', gallery, name='gallery'), + path('payment/process//', payment_process, name='payment_process'), + path('payment/success//', payment_success, name='payment_success'), + path('payment/failed//', payment_failed, name='payment_failed'), + path('register/', register, name='register'), +] \ No newline at end of file diff --git a/products/views.py b/products/views.py new file mode 100644 index 0000000..5092dad --- /dev/null +++ b/products/views.py @@ -0,0 +1,808 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.views.generic import ListView, DetailView +from django.contrib import messages +from django.contrib.auth import login +from django.contrib.auth.decorators import login_required +from django.db.models import Q, Avg, Count +from django.db import transaction +# from rest_framework import viewsets # Temporär auskommentiert +# from rest_framework.permissions import IsAuthenticatedOrReadOnly # Temporär auskommentiert +from .models import Product, Cart, CartItem, Order, OrderItem, Review, UserProfile, Wishlist, FAQ, ContactMessage, CustomOrder, OrderProgress, GalleryImage, Category +from .forms import OrderForm, CustomUserCreationForm, CustomAuthenticationForm, ReviewForm, UserProfileForm, ContactForm, CustomOrderForm, OrderProgressForm +# from .serializers import ProductSerializer, ReviewSerializer # Temporär auskommentiert +import stripe +from django.conf import settings +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.utils import timezone +from django.core.mail import send_mail +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +import json +import paypalrestsdk +from payments import get_payment_model, RedirectNeeded +from paypal.standard.ipn.models import PayPalIPN +from django.urls import reverse +from django.http import HttpResponse +from paypal.standard.forms import PayPalPaymentsForm + +# Create your views here. + +class ProductListView(ListView): + model = Product + template_name = 'products/product_list.html' + context_object_name = 'products' + paginate_by = 12 + + def get_queryset(self): + queryset = Product.objects.all().annotate( + average_rating=Avg('reviews__rating'), + review_count=Count('reviews') + ) + + # Suchfilter + search_query = self.request.GET.get('search') + if search_query: + queryset = queryset.filter( + Q(name__icontains=search_query) | + Q(description__icontains=search_query) + ) + + # Preisbereich Filter + min_price = self.request.GET.get('min_price') + max_price = self.request.GET.get('max_price') + if min_price: + queryset = queryset.filter(price__gte=float(min_price)) + if max_price: + queryset = queryset.filter(price__lte=float(max_price)) + + # Fursuit-Typ Filter + fursuit_type = self.request.GET.get('fursuit_type') + if fursuit_type: + queryset = queryset.filter(fursuit_type=fursuit_type) + + # Style Filter + style = self.request.GET.get('style') + if style: + queryset = queryset.filter(style=style) + + # Sortierung + sort = self.request.GET.get('sort', '-created') + if sort == 'price': + queryset = queryset.order_by('price') + elif sort == '-price': + queryset = queryset.order_by('-price') + elif sort == 'name': + queryset = queryset.order_by('name') + elif sort == '-rating': + queryset = queryset.order_by('-average_rating') + else: # Default: Neueste zuerst + queryset = queryset.order_by('-created') + + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Kategorien für Filter + context['categories'] = Category.objects.all() + context['selected_category'] = self.request.GET.get('category') + + # Fursuit-Typen und Styles für Filter + context['fursuit_types'] = Product.FURSUIT_TYPE_CHOICES + context['fursuit_styles'] = Product.STYLE_CHOICES + + # Filter-Werte + context['min_price'] = self.request.GET.get('min_price') + context['max_price'] = self.request.GET.get('max_price') + context['selected_type'] = self.request.GET.get('fursuit_type') + context['selected_style'] = self.request.GET.get('style') + context['search_query'] = self.request.GET.get('search') + context['sort'] = self.request.GET.get('sort', '-created') + + return context + +class ProductDetailView(DetailView): + model = Product + template_name = 'products/product_detail.html' + context_object_name = 'product' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.request.user.is_authenticated: + context['user_has_reviewed'] = Review.objects.filter( + product=self.object, + user=self.request.user + ).exists() + context['review_form'] = ReviewForm() + return context + +def get_or_create_cart(request): + if request.user.is_authenticated: + cart, created = Cart.objects.get_or_create(user=request.user) + else: + session_key = request.session.session_key + if not session_key: + request.session.create() + session_key = request.session.session_key + cart, created = Cart.objects.get_or_create(session_id=session_key) + return cart + +def add_to_cart(request, product_id): + if request.method == 'POST': + product = get_object_or_404(Product, id=product_id) + quantity = int(request.POST.get('quantity', 1)) + + if quantity > product.stock: + messages.error(request, 'Nicht genügend Artikel auf Lager!') + return redirect('products:product_detail', pk=product_id) + + cart = get_or_create_cart(request) + cart_item, created = CartItem.objects.get_or_create( + cart=cart, + product=product, + defaults={'quantity': quantity} + ) + + if not created: + cart_item.quantity += quantity + cart_item.save() + + messages.success(request, f'{quantity}x {product.name} wurde zum Warenkorb hinzugefügt.') + return redirect('products:cart_detail') + +def cart_detail(request): + cart = get_or_create_cart(request) + return render(request, 'products/cart_detail.html', {'cart': cart}) + +def create_order(request): + if request.method == 'POST': + cart = get_or_create_cart(request) + if not cart.items.exists(): + messages.error(request, 'Ihr Warenkorb ist leer.') + return redirect('products:cart_detail') + + # Erstelle eine neue Bestellung + order = Order.objects.create( + user=request.user if request.user.is_authenticated else None, + full_name=f"{request.user.first_name} {request.user.last_name}" if request.user.is_authenticated else "", + email=request.user.email if request.user.is_authenticated else "", + total_amount=cart.get_total() + ) + + # Füge die Artikel aus dem Warenkorb zur Bestellung hinzu + for cart_item in cart.items.all(): + OrderItem.objects.create( + order=order, + product=cart_item.product, + product_name=cart_item.product.name, + quantity=cart_item.quantity, + price=cart_item.product.price + ) + + # Leere den Warenkorb + cart.items.all().delete() + + messages.success(request, 'Ihre Bestellung wurde erfolgreich erstellt.') + return redirect('products:checkout', order_id=order.id) + + return redirect('products:cart_detail') + +@login_required +def checkout(request, order_id): + order = get_object_or_404(Order, id=order_id) + if request.user.is_authenticated and order.user != request.user: + messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.') + return redirect('products:product_list') + + # PayPal Zahlungsformular erstellen + host = request.get_host() + + paypal_dict = { + "business": settings.PAYPAL_RECEIVER_EMAIL, + "amount": str(order.total_amount), + "item_name": f"Bestellung #{order.id}", + "invoice": str(order.id), + "currency_code": "EUR", + "notify_url": request.build_absolute_uri('/paypal/'), + "return_url": request.build_absolute_uri(f'/products/payment/success/{order.id}/'), + "cancel_return": request.build_absolute_uri(f'/products/payment/failed/{order.id}/'), + } + + form = PayPalPaymentsForm(initial=paypal_dict) + return render(request, 'products/checkout.html', { + 'order': order, + 'form': form + }) + +@csrf_exempt +@login_required +def create_paypal_order(request): + if request.method != 'POST': + return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405) + + data = json.loads(request.body) + order = get_object_or_404(Order, id=data['order_id'], user=request.user) + + payment = paypalrestsdk.Payment({ + "intent": "sale", + "payer": { + "payment_method": "paypal" + }, + "transactions": [{ + "amount": { + "total": str(order.total_amount), + "currency": "EUR" + }, + "description": f"Bestellung #{order.id}" + }], + "redirect_urls": { + "return_url": f"{settings.SITE_URL}/products/order/confirmation/{order.id}/", + "cancel_url": f"{settings.SITE_URL}/products/checkout/{order.id}/" + } + }) + + if payment.create(): + return JsonResponse({'paypal_order_id': payment.id}) + else: + return JsonResponse({'error': payment.error}, status=400) + +@csrf_exempt +@login_required +def capture_paypal_order(request): + if request.method != 'POST': + return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405) + + data = json.loads(request.body) + order = get_object_or_404(Order, id=data['order_id'], user=request.user) + payment_id = data['paypal_order_id'] + + payment = paypalrestsdk.Payment.find(payment_id) + + if payment.execute({'payer_id': payment.payer.payer_info.payer_id}): + order.status = 'paid' + order.payment_id = payment_id + order.save() + + # E-Mail-Bestätigung senden + send_payment_confirmation_email(order) + + return JsonResponse({'success': True}) + else: + return JsonResponse({'error': payment.error}, status=400) + +def order_confirmation(request, order_id): + order = get_object_or_404(Order, id=order_id) + if request.user.is_authenticated and order.user != request.user: + messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.') + return redirect('products:product_list') + + return render(request, 'products/order_confirmation.html', {'order': order}) + +def update_cart_item(request, item_id): + if request.method == 'POST': + cart_item = get_object_or_404(CartItem, id=item_id) + quantity = int(request.POST.get('quantity', 1)) + + if quantity > cart_item.product.stock: + messages.error(request, 'Nicht genügend Artikel auf Lager!') + elif quantity > 0: + cart_item.quantity = quantity + cart_item.save() + messages.success(request, 'Warenkorb wurde aktualisiert.') + else: + cart_item.delete() + messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.') + + return redirect('products:cart_detail') + +def remove_from_cart(request, item_id): + if request.method == 'POST': + cart_item = get_object_or_404(CartItem, id=item_id) + cart_item.delete() + messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.') + return redirect('products:cart_detail') + +def register(request): + if request.method == 'POST': + form = CustomUserCreationForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + messages.success(request, 'Registrierung erfolgreich!') + return redirect('products:product_list') + else: + form = CustomUserCreationForm() + return render(request, 'registration/register.html', {'form': form}) + +@login_required +def add_review(request, product_id): + product = get_object_or_404(Product, id=product_id) + if request.method == 'POST': + form = ReviewForm(request.POST) + if form.is_valid(): + review = form.save(commit=False) + review.product = product + review.user = request.user + review.save() + messages.success(request, 'Ihre Bewertung wurde erfolgreich hinzugefügt.') + else: + messages.error(request, 'Es gab einen Fehler beim Hinzufügen Ihrer Bewertung.') + return redirect('product_detail', pk=product_id) + +@login_required +def profile_view(request): + if request.method == 'POST': + form = UserProfileForm(request.POST, instance=request.user.userprofile) + if form.is_valid(): + form.save() + messages.success(request, 'Profil wurde erfolgreich aktualisiert.') + return redirect('products:profile') + else: + form = UserProfileForm(instance=request.user.userprofile) + + return render(request, 'products/profile.html', {'form': form}) + +@login_required +def order_history(request): + orders = Order.objects.filter(user=request.user).order_by('-created') + return render(request, 'products/order_history.html', {'orders': orders}) + +@login_required +def wishlist_view(request): + wishlist, created = Wishlist.objects.get_or_create(user=request.user) + return render(request, 'products/wishlist.html', {'wishlist': wishlist}) + +@login_required +def add_to_wishlist(request, product_id): + product = get_object_or_404(Product, id=product_id) + wishlist, created = Wishlist.objects.get_or_create(user=request.user) + wishlist.products.add(product) + messages.success(request, f'{product.name} wurde zu Ihrer Wunschliste hinzugefügt.') + return redirect('product_detail', product_id=product_id) + +@login_required +def remove_from_wishlist(request, product_id): + product = get_object_or_404(Product, id=product_id) + wishlist = get_object_or_404(Wishlist, user=request.user) + wishlist.products.remove(product) + messages.success(request, f'{product.name} wurde von Ihrer Wunschliste entfernt.') + return redirect('wishlist') + +def faq_list(request): + faqs = FAQ.objects.all() + categories = FAQ.objects.values_list('category', flat=True).distinct() + return render(request, 'products/faq.html', { + 'faqs': faqs, + 'categories': categories + }) + +def contact(request): + if request.method == 'POST': + form = ContactForm(request.POST, user=request.user) + if form.is_valid(): + contact_message = form.save(commit=False) + if request.user.is_authenticated: + contact_message.user = request.user + contact_message.save() + messages.success( + request, + 'Vielen Dank für Ihre Nachricht! Wir werden uns schnellstmöglich bei Ihnen melden.' + ) + return redirect('contact_success') + else: + form = ContactForm(user=request.user) + + return render(request, 'products/contact.html', {'form': form}) + +def contact_success(request): + return render(request, 'products/contact_success.html') + +@login_required +def custom_order(request): + if request.method == 'POST': + form = CustomOrderForm(request.POST, request.FILES) + if form.is_valid(): + custom_order = form.save(commit=False) + custom_order.user = request.user + custom_order.save() + + messages.success( + request, + 'Ihre Anfrage wurde erfolgreich übermittelt. Wir werden uns in Kürze mit einem Angebot bei Ihnen melden.' + ) + return redirect('custom_order_success', order_id=custom_order.id) + else: + form = CustomOrderForm() + + return render(request, 'products/custom_order.html', { + 'form': form + }) + +@login_required +def custom_order_success(request, order_id): + order = get_object_or_404(CustomOrder, id=order_id, user=request.user) + return render(request, 'products/custom_order_success.html', { + 'order': order + }) + +@login_required +def custom_order_detail(request, order_id): + order = get_object_or_404(CustomOrder, id=order_id, user=request.user) + progress_updates = order.progress_updates.all() + return render(request, 'products/custom_order_detail.html', { + 'order': order, + 'progress_updates': progress_updates + }) + +def gallery(request): + # Hole alle Galeriebilder + images = GalleryImage.objects.all().order_by('order', '-created') + + # Filter nach Typ + fursuit_type = request.GET.get('fursuit_type') + if fursuit_type: + images = images.filter(fursuit_type=fursuit_type) + + # Filter nach Stil + style = request.GET.get('style') + if style: + images = images.filter(style=style) + + # Sortierung + sort = request.GET.get('sort', 'order') + if sort == 'newest': + images = images.order_by('-created') + elif sort == 'oldest': + images = images.order_by('created') + + context = { + 'images': images, + 'fursuit_types': GalleryImage.FURSUIT_TYPE_CHOICES, + 'fursuit_styles': GalleryImage.STYLE_CHOICES, + 'current_type': fursuit_type, + 'current_style': style, + 'current_sort': sort + } + + print(f"Anzahl der geladenen Bilder: {images.count()}") # Debug-Ausgabe + for img in images: + print(f"Bild: {img.title}, URL: {img.image.url}") # Debug-Ausgabe + + return render(request, 'products/gallery.html', context) + +@login_required +def add_progress_update(request, order_id): + order = get_object_or_404(CustomOrder, id=order_id) + if not request.user.is_staff: + messages.error(request, 'Sie haben keine Berechtigung für diese Aktion.') + return redirect('custom_order_detail', order_id=order_id) + + if request.method == 'POST': + form = OrderProgressForm(request.POST, request.FILES) + if form.is_valid(): + progress = form.save(commit=False) + progress.custom_order = order + progress.save() + + messages.success(request, 'Fortschritts-Update wurde hinzugefügt.') + + # Wenn alle Schritte abgeschlossen sind, setze den Status auf "ready" + if all(update.completed for update in order.progress_updates.all()): + order.status = 'ready' + order.save() + + return redirect('custom_order_detail', order_id=order_id) + else: + form = OrderProgressForm() + + return render(request, 'products/add_progress.html', { + 'form': form, + 'order': order + }) + +@login_required +def dashboard(request): + # Basis-Informationen + user_profile = request.user.userprofile + wishlist = Wishlist.objects.get_or_create(user=request.user)[0] + cart = get_or_create_cart(request) + + # Custom Orders mit Fortschritt + custom_orders = CustomOrder.objects.filter(user=request.user).order_by('-created') + for order in custom_orders: + order.progress_percentage = calculate_order_progress(order) + + # Letzte Bestellungen mit Details + recent_orders = Order.objects.filter(user=request.user).order_by('-created')[:5] + for order in recent_orders: + order.items_count = order.items.count() + + # Bewertungsübersicht + reviews = Review.objects.filter(user=request.user).select_related('product').order_by('-created')[:5] + + # Statistiken + stats = { + 'total_orders': Order.objects.filter(user=request.user).count(), + 'total_custom_orders': CustomOrder.objects.filter(user=request.user).count(), + 'total_reviews': Review.objects.filter(user=request.user).count(), + 'wishlist_count': wishlist.products.count(), + } + + return render(request, 'products/dashboard.html', { + 'user_profile': user_profile, + 'custom_orders': custom_orders, + 'recent_orders': recent_orders, + 'reviews': reviews, + 'stats': stats, + 'wishlist': wishlist, + 'cart': cart, + }) + +def calculate_order_progress(order): + """Berechne den Fortschritt einer Custom Order in Prozent.""" + if order.status == 'cancelled': + return 0 + elif order.status == 'completed': + return 100 + + # Status-Gewichtung + status_weights = { + 'pending': 10, + 'quoted': 20, + 'approved': 30, + 'in_progress': 50, + 'ready': 90, + 'shipped': 95, + } + + base_progress = status_weights.get(order.status, 0) + + # Zusätzlicher Fortschritt basierend auf OrderProgress + if order.status == 'in_progress': + completed_stages = order.progress_updates.filter(completed=True).count() + total_stages = len(OrderProgress.PROGRESS_CHOICES) + stage_progress = (completed_stages / total_stages) * 40 # 40% für Fortschrittsstufen + return base_progress + stage_progress + + return base_progress + +# API ViewSets - Temporär auskommentiert +# class ProductViewSet(viewsets.ModelViewSet): +# queryset = Product.objects.all() +# serializer_class = ProductSerializer +# permission_classes = [IsAuthenticatedOrReadOnly] + +# class ReviewViewSet(viewsets.ModelViewSet): +# queryset = Review.objects.all() +# serializer_class = ReviewSerializer +# permission_classes = [IsAuthenticatedOrReadOnly] + +# def perform_create(self, serializer): +# serializer.save(user=self.request.user) + +# Stripe-Konfiguration +stripe.api_key = settings.STRIPE_SECRET_KEY + +def create_payment_intent(request, order_id): + """Erstellt einen Stripe Payment Intent für eine Bestellung.""" + try: + order = get_object_or_404(Order, id=order_id) + + # Stripe akzeptiert nur Cent-Beträge + amount = int(order.total_amount * 100) + + # Payment Intent erstellen + intent = stripe.PaymentIntent.create( + amount=amount, + currency='eur', + metadata={ + 'order_id': order.id, + 'user_id': request.user.id if request.user.is_authenticated else None + } + ) + + # Order aktualisieren + order.stripe_payment_intent_id = intent.id + order.save() + + return JsonResponse({ + 'clientSecret': intent.client_secret, + 'amount': amount + }) + except Exception as e: + return JsonResponse({'error': str(e)}, status=400) + +def payment_view(request, order_id): + """Zeigt die Zahlungsseite an.""" + order = get_object_or_404(Order, id=order_id) + + # Überprüfen, ob die Bestellung dem Benutzer gehört + if request.user.is_authenticated and order.user != request.user: + messages.error(request, 'Sie haben keine Berechtigung für diese Bestellung.') + return redirect('dashboard') + + # Überprüfen, ob die Bestellung bereits bezahlt wurde + if order.payment_status == 'paid': + messages.info(request, 'Diese Bestellung wurde bereits bezahlt.') + return redirect('order_detail', order_id=order.id) + + context = { + 'order': order, + 'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY + } + return render(request, 'products/payment.html', context) + +@csrf_exempt +def stripe_webhook(request): + """Webhook für Stripe-Events.""" + payload = request.body + sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, settings.STRIPE_WEBHOOK_SECRET + ) + except ValueError as e: + return JsonResponse({'error': 'Invalid payload'}, status=400) + except stripe.error.SignatureVerificationError as e: + return JsonResponse({'error': 'Invalid signature'}, status=400) + + if event.type == 'payment_intent.succeeded': + payment_intent = event.data.object + handle_successful_payment(payment_intent) + elif event.type == 'payment_intent.payment_failed': + payment_intent = event.data.object + handle_failed_payment(payment_intent) + + return JsonResponse({'status': 'success'}) + +def handle_successful_payment(payment_intent): + """Verarbeitet erfolgreiche Zahlungen.""" + order_id = payment_intent.metadata.get('order_id') + if order_id: + try: + order = Order.objects.get(id=order_id) + order.payment_status = 'paid' + order.payment_date = timezone.now() + order.status = 'processing' + order.stripe_payment_method_id = payment_intent.payment_method + order.save() + + # E-Mail-Benachrichtigung senden + send_payment_confirmation_email(order) + except Order.DoesNotExist: + print(f"Bestellung {order_id} nicht gefunden") + +def handle_failed_payment(payment_intent): + """Verarbeitet fehlgeschlagene Zahlungen.""" + order_id = payment_intent.metadata.get('order_id') + if order_id: + try: + order = Order.objects.get(id=order_id) + order.payment_status = 'failed' + order.save() + + # E-Mail-Benachrichtigung senden + send_payment_failed_email(order) + except Order.DoesNotExist: + print(f"Bestellung {order_id} nicht gefunden") + +@login_required +def payment_success(request, order_id): + """Zeigt die Erfolgsseite nach erfolgreicher Zahlung an.""" + order = get_object_or_404(Order, id=order_id, user=request.user) + messages.success(request, 'Ihre Zahlung wurde erfolgreich verarbeitet!') + return render(request, 'products/payment_success.html', {'order': order}) + +@login_required +def payment_failed(request, order_id): + """Zeigt die Fehlerseite nach fehlgeschlagener Zahlung an.""" + order = get_object_or_404(Order, id=order_id, user=request.user) + messages.error(request, 'Die Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.') + return render(request, 'products/payment_failed.html', {'order': order}) + +# Hilfsfunktionen für E-Mail-Benachrichtigungen +def send_payment_confirmation_email(order): + """Sendet eine Bestätigungs-E-Mail nach erfolgreicher Zahlung.""" + subject = f'Zahlungsbestätigung für Bestellung #{order.id}' + message = f""" + Sehr geehrte(r) {order.full_name}, + + vielen Dank für Ihre Zahlung. Ihre Bestellung #{order.id} wurde erfolgreich bezahlt. + + Bestelldetails: + - Gesamtbetrag: {order.total_amount} € + - Zahlungsmethode: {order.get_payment_method_display()} + - Zahlungsdatum: {order.payment_date.strftime('%d.%m.%Y %H:%M')} + + Wir werden Ihre Bestellung schnellstmöglich bearbeiten. + + Mit freundlichen Grüßen + Ihr Fursuit-Shop Team + """ + + send_mail( + subject, + message, + settings.DEFAULT_FROM_EMAIL, + [order.email], + fail_silently=False, + ) + +def send_payment_failed_email(order): + """Sendet eine Benachrichtigung nach fehlgeschlagener Zahlung.""" + subject = f'Zahlungsproblem bei Bestellung #{order.id}' + message = f""" + Sehr geehrte(r) {order.full_name}, + + leider ist die Zahlung für Ihre Bestellung #{order.id} fehlgeschlagen. + + Sie können die Zahlung unter folgendem Link erneut versuchen: + {settings.SITE_URL}/payment/{order.id}/ + + Falls Sie Fragen haben, kontaktieren Sie uns bitte. + + Mit freundlichen Grüßen + Ihr Fursuit-Shop Team + """ + + send_mail( + subject, + message, + settings.DEFAULT_FROM_EMAIL, + [order.email], + fail_silently=False, + ) + +@login_required +def payment_process(request, order_id): + order = get_object_or_404(Order, id=order_id, user=request.user) + + if request.method == 'POST': + # Initiiere PayPal-Zahlung + paypal_dict = { + "business": settings.PAYPAL_RECEIVER_EMAIL, + "amount": str(order.total_amount), + "item_name": f"Bestellung #{order.id}", + "invoice": str(order.id), + "notify_url": request.build_absolute_uri(reverse('paypal-ipn')), + "return_url": request.build_absolute_uri(reverse('payment_success', args=[order.id])), + "cancel_return": request.build_absolute_uri(reverse('payment_failed', args=[order.id])), + "custom": json.dumps({ + "order_id": order.id, + "user_id": request.user.id, + }) + } + + form = PayPalPaymentsForm(initial=paypal_dict) + return render(request, 'products/payment_process.html', {'order': order, 'form': form}) + + return render(request, 'products/payment_process.html', {'order': order}) + +@csrf_exempt +def paypal_ipn(request): + """PayPal IPN (Instant Payment Notification) Handler""" + if request.method == "POST": + try: + ipn_obj = PayPalIPN(request.POST) + ipn_obj.verify() + + if ipn_obj.payment_status == "Completed": + # Zahlung war erfolgreich + try: + order = Order.objects.get(id=ipn_obj.invoice) + order.status = 'paid' + order.payment_method = 'paypal' + order.payment_id = ipn_obj.txn_id + order.save() + + # Sende Bestätigungs-E-Mail + send_payment_confirmation_email(order) + except Order.DoesNotExist: + pass + + return HttpResponse("OK") + except Exception as e: + return HttpResponse(str(e)) + + return HttpResponse("OK") diff --git a/shop/__init__.py b/shop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/admin.py b/shop/admin.py new file mode 100644 index 0000000..4481a5e --- /dev/null +++ b/shop/admin.py @@ -0,0 +1,123 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from django.utils.html import format_html +from .models import ( + Product, Category, FursuitGallery, GalleryImage, + DesignTemplate, CustomerDesign, Order, OrderProgress, + Cart, CartItem, ShippingAddress, Checkout, + ProductType, ProductImage, ProductVariant, CustomDesign, + PayPalPayment, PaymentError +) + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ['name', 'slug', 'parent'] + list_filter = ['parent'] + search_fields = ['name'] + prepopulated_fields = {'slug': ('name',)} + +@admin.register(ProductType) +class ProductTypeAdmin(admin.ModelAdmin): + list_display = ['name', 'has_sizes', 'has_colors', 'has_custom_design', 'requires_measurements'] + list_filter = ['has_sizes', 'has_colors', 'has_custom_design', 'requires_measurements'] + search_fields = ['name'] + +class ProductImageInline(admin.TabularInline): + model = ProductImage + extra = 1 + +class ProductVariantInline(admin.TabularInline): + model = ProductVariant + extra = 1 + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ['name', 'category', 'price', 'stock', 'available', 'created'] + list_filter = ['available', 'category', 'product_type', 'created'] + search_fields = ['name', 'description'] + prepopulated_fields = {'slug': ('name',)} + inlines = [ProductImageInline, ProductVariantInline] + date_hierarchy = 'created' + +@admin.register(CustomDesign) +class CustomDesignAdmin(admin.ModelAdmin): + list_display = ['product', 'customer', 'status', 'created'] + list_filter = ['status', 'created'] + search_fields = ['product__name', 'customer__username'] + date_hierarchy = 'created' + +@admin.register(DesignTemplate) +class DesignTemplateAdmin(admin.ModelAdmin): + list_display = ['name', 'created_at'] + search_fields = ['name', 'description'] + +@admin.register(CustomerDesign) +class CustomerDesignAdmin(admin.ModelAdmin): + list_display = ['user', 'name', 'created_at'] + search_fields = ['user__username', 'name'] + date_hierarchy = 'created_at' + +class OrderProgressInline(admin.TabularInline): + model = OrderProgress + extra = 1 + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ['id', 'user', 'product', 'status', 'payment_method', 'total_price', 'created_at'] + list_filter = ['status', 'payment_method', 'created_at'] + search_fields = ['user__username', 'product__name'] + inlines = [OrderProgressInline] + date_hierarchy = 'created_at' + +class CartItemInline(admin.TabularInline): + model = CartItem + extra = 1 + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ['user', 'created_at', 'updated_at'] + search_fields = ['user__username'] + inlines = [CartItemInline] + date_hierarchy = 'created_at' + +@admin.register(ShippingAddress) +class ShippingAddressAdmin(admin.ModelAdmin): + list_display = ['user', 'first_name', 'last_name', 'city', 'country'] + list_filter = ['country'] + search_fields = ['user__username', 'first_name', 'last_name', 'city'] + +@admin.register(Checkout) +class CheckoutAdmin(admin.ModelAdmin): + list_display = ['user', 'status', 'payment_method', 'created_at'] + list_filter = ['status', 'payment_method'] + search_fields = ['user__username'] + date_hierarchy = 'created_at' + +@admin.register(PayPalPayment) +class PayPalPaymentAdmin(admin.ModelAdmin): + list_display = ['order', 'payment_id', 'status', 'amount', 'currency', 'created_at'] + list_filter = ['status', 'currency'] + search_fields = ['payment_id', 'order__id'] + date_hierarchy = 'created_at' + +@admin.register(PaymentError) +class PaymentErrorAdmin(admin.ModelAdmin): + list_display = ['order', 'error_code', 'created_at'] + search_fields = ['order__id', 'error_code', 'error_message'] + date_hierarchy = 'created_at' + +@admin.register(FursuitGallery) +class FursuitGalleryAdmin(admin.ModelAdmin): + list_display = ['name', 'created_at'] + search_fields = ['name', 'description'] + prepopulated_fields = {'slug': ('name',)} + +class GalleryImageInline(admin.TabularInline): + model = GalleryImage + extra = 1 + +@admin.register(GalleryImage) +class GalleryImageAdmin(admin.ModelAdmin): + list_display = ['gallery', 'title', 'order'] + list_filter = ['gallery'] + search_fields = ['gallery__name', 'title'] diff --git a/shop/apps.py b/shop/apps.py new file mode 100644 index 0000000..6eb7484 --- /dev/null +++ b/shop/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class ShopConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'shop' + verbose_name = _('Shop') + + def ready(self): + """ + Importiert und registriert die Signal-Handler + """ + import shop.signals diff --git a/shop/asgi.py b/shop/asgi.py new file mode 100644 index 0000000..5bbf803 --- /dev/null +++ b/shop/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for shop project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shop.settings') + +application = get_asgi_application() diff --git a/shop/emails.py b/shop/emails.py new file mode 100644 index 0000000..8257927 --- /dev/null +++ b/shop/emails.py @@ -0,0 +1,206 @@ +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.translation import gettext as _ +from django.conf import settings +from django.urls import reverse +from django.contrib.sites.shortcuts import get_current_site + +def send_order_confirmation(request, order): + """ + Sendet eine Bestellbestätigungs-E-Mail an den Kunden + """ + context = { + 'order': order, + 'logo_url': f"{settings.STATIC_URL}images/logo.png", + 'order_url': request.build_absolute_uri( + reverse('shop:my_orders') + ) + } + + # HTML-Version + html_content = render_to_string( + 'shop/emails/order_confirmation.html', + context + ) + + # Text-Version + text_content = render_to_string( + 'shop/emails/order_confirmation.txt', + context + ) + + subject = _('Order Confirmation - Order #{}').format(order.id) + from_email = settings.DEFAULT_FROM_EMAIL + to_email = order.shipping_address.email + + msg = EmailMultiAlternatives( + subject, + text_content, + from_email, + [to_email] + ) + msg.attach_alternative(html_content, "text/html") + msg.send() + +def send_order_status_update(request, order, update=None): + """ + Sendet eine E-Mail über Statusänderungen der Bestellung + """ + context = { + 'order': order, + 'update': update, + 'logo_url': f"{settings.STATIC_URL}images/logo.png", + 'order_url': request.build_absolute_uri( + reverse('shop:my_orders') + ) + } + + # HTML-Version + html_content = render_to_string( + 'shop/emails/order_status_update.html', + context + ) + + # Text-Version + text_content = render_to_string( + 'shop/emails/order_status_update.txt', + context + ) + + subject = _('Order Status Update - Order #{}').format(order.id) + from_email = settings.DEFAULT_FROM_EMAIL + to_email = order.shipping_address.email + + msg = EmailMultiAlternatives( + subject, + text_content, + from_email, + [to_email] + ) + msg.attach_alternative(html_content, "text/html") + msg.send() + +def send_shipping_confirmation(request, order): + """ + Sendet eine Versandbestätigungs-E-Mail mit Tracking-Nummer + """ + context = { + 'order': order, + 'logo_url': f"{settings.STATIC_URL}images/logo.png", + 'order_url': request.build_absolute_uri( + reverse('shop:my_orders') + ) + } + + # HTML-Version + html_content = render_to_string( + 'shop/emails/shipping_confirmation.html', + context + ) + + # Text-Version + text_content = render_to_string( + 'shop/emails/shipping_confirmation.txt', + context + ) + + subject = _('Your Order Has Been Shipped - Order #{}').format(order.id) + from_email = settings.DEFAULT_FROM_EMAIL + to_email = order.shipping_address.email + + msg = EmailMultiAlternatives( + subject, + text_content, + from_email, + [to_email] + ) + msg.attach_alternative(html_content, "text/html") + msg.send() + +def send_admin_notification(request, order, notification_type, extra_context=None): + """ + Sendet eine Benachrichtigung an den Shop-Administrator + """ + context = { + 'order': order, + 'notification_type': notification_type, + 'logo_url': f"{settings.STATIC_URL}images/logo.png", + 'order_url': request.build_absolute_uri( + reverse('admin:shop_order_change', args=[order.id]) + ) + } + + if extra_context: + context.update(extra_context) + + # HTML-Version + html_content = render_to_string( + 'shop/emails/admin_notification.html', + context + ) + + # Text-Version + text_content = render_to_string( + 'shop/emails/admin_notification.txt', + context + ) + + # Betreff basierend auf Benachrichtigungstyp + subjects = { + 'new_order': _('New Order Received - Order #{}'), + 'payment_failed': _('Payment Failed - Order #{}'), + 'custom_design': _('New Custom Design Order #{}'), + 'fursuit_order': _('New Fursuit Order #{}'), + } + + subject = subjects.get(notification_type, _('Order Notification - Order #{}')).format(order.id) + + # E-Mail an alle konfigurierten Admin-E-Mail-Adressen senden + admin_emails = [email for name, email in settings.ADMINS] + + if admin_emails: + msg = EmailMultiAlternatives( + subject, + text_content, + settings.DEFAULT_FROM_EMAIL, + admin_emails + ) + msg.attach_alternative(html_content, "text/html") + msg.send() + +def send_low_stock_notification(request, product): + """ + Benachrichtigt den Admin über niedrigen Lagerbestand + """ + context = { + 'product': product, + 'logo_url': f"{settings.STATIC_URL}images/logo.png", + 'product_url': request.build_absolute_uri( + reverse('admin:shop_product_change', args=[product.id]) + ) + } + + # HTML-Version + html_content = render_to_string( + 'shop/emails/low_stock_notification.html', + context + ) + + # Text-Version + text_content = render_to_string( + 'shop/emails/low_stock_notification.txt', + context + ) + + subject = _('Low Stock Alert - {}').format(product.name) + admin_emails = [email for name, email in settings.ADMINS] + + if admin_emails: + msg = EmailMultiAlternatives( + subject, + text_content, + settings.DEFAULT_FROM_EMAIL, + admin_emails + ) + msg.attach_alternative(html_content, "text/html") + msg.send() \ No newline at end of file diff --git a/shop/forms.py b/shop/forms.py new file mode 100644 index 0000000..9a342cc --- /dev/null +++ b/shop/forms.py @@ -0,0 +1,23 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ +from .models import ShippingAddress, Order + +class ShippingAddressForm(forms.ModelForm): + class Meta: + model = ShippingAddress + fields = ['first_name', 'last_name', 'email', 'address', 'city', 'zip', 'country'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.widget.attrs['class'] = 'form-control' + +class PaymentMethodForm(forms.Form): + payment_method = forms.ChoiceField( + choices=Order.PAYMENT_METHODS, + widget=forms.HiddenInput(), + required=True, + error_messages={ + 'required': _('Please select a payment method.') + } + ) \ No newline at end of file diff --git a/shop/management/commands/test_email.py b/shop/management/commands/test_email.py new file mode 100644 index 0000000..6821b55 --- /dev/null +++ b/shop/management/commands/test_email.py @@ -0,0 +1,110 @@ +from django.core.management.base import BaseCommand +from django.core.mail import send_mail, get_connection +from django.conf import settings +from django.template.loader import render_to_string +from shop.models import Order, Product +from shop.emails import ( + send_order_confirmation, + send_order_status_update, + send_shipping_confirmation, + send_admin_notification, + send_low_stock_notification +) + +class Command(BaseCommand): + help = 'Testet das E-Mail-System mit verschiedenen E-Mail-Typen' + + def add_arguments(self, parser): + parser.add_argument( + '--email', + type=str, + help='Test-E-Mail-Adresse', + ) + parser.add_argument( + '--type', + type=str, + choices=['all', 'order', 'status', 'shipping', 'admin', 'stock'], + default='all', + help='Art der Test-E-Mail', + ) + + def handle(self, *args, **options): + test_email = options['email'] + if not test_email: + self.stdout.write(self.style.ERROR('Bitte geben Sie eine Test-E-Mail-Adresse an mit --email=ihre@email.de')) + return + + email_type = options['type'] + + self.stdout.write('Starte E-Mail-Test...') + + try: + # Debug-Informationen ausgeben + self.stdout.write(f'Backend: {settings.EMAIL_BACKEND}') + self.stdout.write(f'Host: {settings.EMAIL_HOST}') + self.stdout.write(f'Port: {settings.EMAIL_PORT}') + self.stdout.write(f'TLS: {settings.EMAIL_USE_TLS}') + self.stdout.write(f'Benutzer: {settings.EMAIL_HOST_USER}') + self.stdout.write('Passwort: ***versteckt***') + + # Basis-Test + send_mail( + 'Test E-Mail', + 'Dies ist eine Test-E-Mail vom Fursuit Shop.', + settings.DEFAULT_FROM_EMAIL, + [test_email], + fail_silently=False, + ) + self.stdout.write(self.style.SUCCESS('✓ Basis-E-Mail erfolgreich gesendet')) + + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Basis-E-Mail fehlgeschlagen: {str(e)}')) + return + + if email_type in ['all', 'order']: + try: + # Test-Bestellung erstellen + order = Order.objects.first() + if order: + send_order_confirmation(None, order) + self.stdout.write(self.style.SUCCESS('✓ Bestellbestätigung gesendet')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Bestellbestätigung fehlgeschlagen: {str(e)}')) + + if email_type in ['all', 'status']: + try: + order = Order.objects.first() + if order: + send_order_status_update(None, order) + self.stdout.write(self.style.SUCCESS('✓ Status-Update gesendet')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Status-Update fehlgeschlagen: {str(e)}')) + + if email_type in ['all', 'shipping']: + try: + order = Order.objects.filter(tracking_number__isnull=False).first() + if order: + send_shipping_confirmation(None, order) + self.stdout.write(self.style.SUCCESS('✓ Versandbestätigung gesendet')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Versandbestätigung fehlgeschlagen: {str(e)}')) + + if email_type in ['all', 'admin']: + try: + order = Order.objects.first() + if order: + send_admin_notification(None, order, 'new_order') + self.stdout.write(self.style.SUCCESS('✓ Admin-Benachrichtigung gesendet')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Admin-Benachrichtigung fehlgeschlagen: {str(e)}')) + + if email_type in ['all', 'stock']: + try: + product = Product.objects.first() + if product: + send_low_stock_notification(None, product) + self.stdout.write(self.style.SUCCESS('✓ Lagerbestand-Warnung gesendet')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'✗ Lagerbestand-Warnung fehlgeschlagen: {str(e)}')) + + self.stdout.write(self.style.SUCCESS('\nE-Mail-Test abgeschlossen!')) \ No newline at end of file diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py new file mode 100644 index 0000000..b968510 --- /dev/null +++ b/shop/migrations/0001_initial.py @@ -0,0 +1,138 @@ +# Generated by Django 5.2.1 on 2025-05-29 10:19 + +import django.db.models.deletion +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('name_en', models.CharField(max_length=200, verbose_name='Name (English)')), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name': 'Category', + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='DesignTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('name_en', models.CharField(max_length=200, verbose_name='Name (English)')), + ('description', models.TextField(verbose_name='Description')), + ('description_en', models.TextField(verbose_name='Description (English)')), + ('image', models.ImageField(upload_to='designs/')), + ('model_3d', models.FileField(blank=True, null=True, upload_to='3d_models/', verbose_name='3D Model')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='FursuitGallery', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(verbose_name='Description')), + ('description_en', models.TextField(verbose_name='Description (English)')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Fursuit Gallery', + 'verbose_name_plural': 'Fursuit Galleries', + }, + ), + migrations.CreateModel( + name='CustomerDesign', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Design Name')), + ('design_file', models.FileField(upload_to='customer_designs/')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='GalleryImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='gallery/')), + ('title', models.CharField(blank=True, max_length=200, verbose_name='Title')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('order', models.PositiveIntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('gallery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='shop.fursuitgallery')), + ], + options={ + 'ordering': ['order', 'created_at'], + }, + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)), + ('payment_method', models.CharField(choices=[('paypal', 'PayPal'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer')], max_length=20)), + ('special_instructions', models.TextField(blank=True)), + ('measurements', models.JSONField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('total_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)), + ('customer_design', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.customerdesign')), + ('design_template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.designtemplate')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='Title')), + ('description', models.TextField(verbose_name='Description')), + ('image', models.ImageField(blank=True, null=True, upload_to='progress/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='shop.order')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_type', models.CharField(choices=[('fursuit', 'Fursuit'), ('printed', 'Printed Item')], default='printed', max_length=10, verbose_name='Product Type')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('name_en', models.CharField(max_length=200, verbose_name='Name (English)')), + ('description', models.TextField(verbose_name='Description')), + ('description_en', models.TextField(verbose_name='Description (English)')), + ('base_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='Base Price')), + ('image', models.ImageField(upload_to='products/')), + ('model_3d', models.FileField(blank=True, null=True, upload_to='3d_models/', verbose_name='3D Model')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.category')), + ('gallery', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.fursuitgallery')), + ], + ), + migrations.AddField( + model_name='order', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product'), + ), + ] diff --git a/shop/migrations/0002_producttype_alter_category_options_and_more.py b/shop/migrations/0002_producttype_alter_category_options_and_more.py new file mode 100644 index 0000000..caad6b8 --- /dev/null +++ b/shop/migrations/0002_producttype_alter_category_options_and_more.py @@ -0,0 +1,277 @@ +# Generated by Django 5.2.1 on 2025-05-29 12:47 + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProductType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ('has_sizes', models.BooleanField(default=False, verbose_name='Hat Größen')), + ('has_colors', models.BooleanField(default=False, verbose_name='Hat Farben')), + ('has_custom_design', models.BooleanField(default=False, verbose_name='Erlaubt Custom Design')), + ('requires_measurements', models.BooleanField(default=False, verbose_name='Benötigt Maße')), + ], + options={ + 'verbose_name': 'Produkttyp', + 'verbose_name_plural': 'Produkttypen', + }, + ), + migrations.AlterModelOptions( + name='category', + options={'ordering': ['name'], 'verbose_name': 'Kategorie', 'verbose_name_plural': 'Kategorien'}, + ), + migrations.AlterModelOptions( + name='product', + options={'ordering': ['-created'], 'verbose_name': 'Produkt', 'verbose_name_plural': 'Produkte'}, + ), + migrations.RemoveField( + model_name='category', + name='name_en', + ), + migrations.RemoveField( + model_name='product', + name='base_price', + ), + migrations.RemoveField( + model_name='product', + name='created_at', + ), + migrations.RemoveField( + model_name='product', + name='description_en', + ), + migrations.RemoveField( + model_name='product', + name='gallery', + ), + migrations.RemoveField( + model_name='product', + name='model_3d', + ), + migrations.RemoveField( + model_name='product', + name='name_en', + ), + migrations.RemoveField( + model_name='product', + name='updated_at', + ), + migrations.AddField( + model_name='category', + name='description', + field=models.TextField(blank=True, verbose_name='Beschreibung'), + ), + migrations.AddField( + model_name='category', + name='image', + field=models.ImageField(blank=True, upload_to='categories/', verbose_name='Bild'), + ), + migrations.AddField( + model_name='category', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='shop.category', verbose_name='Übergeordnete Kategorie'), + ), + migrations.AddField( + model_name='product', + name='available', + field=models.BooleanField(default=True, verbose_name='Verfügbar'), + ), + migrations.AddField( + model_name='product', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt'), + ), + migrations.AddField( + model_name='product', + name='price', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='Preis'), + ), + migrations.AddField( + model_name='product', + name='slug', + field=models.SlugField(default='', max_length=200, unique=True, verbose_name='Slug'), + ), + migrations.AddField( + model_name='product', + name='stock', + field=models.PositiveIntegerField(default=0, verbose_name='Lagerbestand'), + ), + migrations.AddField( + model_name='product', + name='updated', + field=models.DateTimeField(auto_now=True, verbose_name='Aktualisiert'), + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='category', + name='slug', + field=models.SlugField(max_length=100, unique=True, verbose_name='Slug'), + ), + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='shop.category', verbose_name='Kategorie'), + ), + migrations.AlterField( + model_name='product', + name='description', + field=models.TextField(verbose_name='Beschreibung'), + ), + migrations.AlterField( + model_name='product', + name='image', + field=models.ImageField(upload_to='products/', verbose_name='Hauptbild'), + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('size', models.CharField(blank=True, max_length=20, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ('custom_design', models.ImageField(blank=True, null=True, upload_to='custom_designs/')), + ('added_at', models.DateTimeField(auto_now_add=True)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shop.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product')), + ], + options={ + 'ordering': ['-added_at'], + }, + ), + migrations.CreateModel( + name='CustomDesign', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('design_file', models.FileField(upload_to='designs/', verbose_name='Design-Datei')), + ('notes', models.TextField(verbose_name='Anmerkungen')), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt')), + ('status', models.CharField(choices=[('pending', 'Ausstehend'), ('approved', 'Genehmigt'), ('rejected', 'Abgelehnt'), ('in_progress', 'In Bearbeitung'), ('completed', 'Abgeschlossen')], default='pending', max_length=20, verbose_name='Status')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Kunde')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_designs', to='shop.product', verbose_name='Produkt')), + ], + options={ + 'verbose_name': 'Custom Design', + 'verbose_name_plural': 'Custom Designs', + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='PaymentError', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('error_code', models.CharField(max_length=100, verbose_name='Error Code')), + ('error_message', models.TextField(verbose_name='Error Message')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_errors', to='shop.order')), + ], + ), + migrations.CreateModel( + name='PayPalPayment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_id', models.CharField(max_length=100, verbose_name='PayPal Payment ID')), + ('payer_id', models.CharField(max_length=100, verbose_name='PayPal Payer ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded')], max_length=20, verbose_name='Payment Status')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')), + ('currency', models.CharField(default='EUR', max_length=3, verbose_name='Currency')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='paypal_payment', to='shop.order')), + ], + ), + migrations.CreateModel( + name='ProductImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='products/', verbose_name='Bild')), + ('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alternativer Text')), + ('is_feature', models.BooleanField(default=False, verbose_name='Ist Hauptbild')), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Erstellt')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='shop.product', verbose_name='Produkt')), + ], + options={ + 'verbose_name': 'Produktbild', + 'verbose_name_plural': 'Produktbilder', + 'ordering': ['-is_feature', '-created'], + }, + ), + migrations.AlterField( + model_name='product', + name='product_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.producttype', verbose_name='Produkttyp'), + ), + migrations.CreateModel( + name='ShippingAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=100, verbose_name='First Name')), + ('last_name', models.CharField(max_length=100, verbose_name='Last Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('address', models.CharField(max_length=200, verbose_name='Address')), + ('city', models.CharField(max_length=100, verbose_name='City')), + ('zip', models.CharField(max_length=10, verbose_name='ZIP Code')), + ('country', models.CharField(choices=[('DE', 'Deutschland'), ('AT', 'Österreich'), ('CH', 'Schweiz')], max_length=2, verbose_name='Country')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Checkout', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_method', models.CharField(choices=[('paypal', 'PayPal'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer')], max_length=20, null=True, verbose_name='Payment Method')), + ('status', models.CharField(choices=[('address', 'Shipping Address'), ('payment', 'Payment Method'), ('confirm', 'Confirmation'), ('completed', 'Completed')], default='address', max_length=20, verbose_name='Status')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('cart', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='shop.cart')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('shipping_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.shippingaddress')), + ], + ), + migrations.CreateModel( + name='ProductVariant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('size', models.CharField(blank=True, choices=[('XS', 'Extra Small'), ('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'Extra Large'), ('XXL', '2X Large'), ('XXXL', '3X Large')], max_length=10, verbose_name='Größe')), + ('color', models.CharField(blank=True, max_length=50, verbose_name='Farbe')), + ('sku', models.CharField(max_length=50, unique=True, verbose_name='Artikelnummer')), + ('price_adjustment', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Preisanpassung')), + ('stock', models.PositiveIntegerField(default=0, verbose_name='Lagerbestand')), + ('image', models.ImageField(blank=True, upload_to='variants/', verbose_name='Variantenbild')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='shop.product', verbose_name='Produkt')), + ], + options={ + 'verbose_name': 'Produktvariante', + 'verbose_name_plural': 'Produktvarianten', + 'unique_together': {('product', 'size', 'color')}, + }, + ), + ] diff --git a/shop/migrations/0003_contactmessage.py b/shop/migrations/0003_contactmessage.py new file mode 100644 index 0000000..f968953 --- /dev/null +++ b/shop/migrations/0003_contactmessage.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.1 on 2025-05-30 10:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0002_producttype_alter_category_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ContactMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ('email', models.EmailField(max_length=254, verbose_name='E-Mail')), + ('subject', models.CharField(max_length=200, verbose_name='Betreff')), + ('message', models.TextField(verbose_name='Nachricht')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')), + ], + options={ + 'verbose_name': 'Kontaktnachricht', + 'verbose_name_plural': 'Kontaktnachrichten', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/shop/migrations/__init__.py b/shop/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/models.py b/shop/models.py new file mode 100644 index 0000000..6aa2b7d --- /dev/null +++ b/shop/models.py @@ -0,0 +1,365 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import User +from django.utils.text import slugify +from decimal import Decimal +from django.core.validators import MinValueValidator +from django.utils import timezone + +# Create your models here. + +class FursuitGallery(models.Model): + name = models.CharField(_('Name'), max_length=200) + slug = models.SlugField(unique=True) + description = models.TextField(_('Description')) + description_en = models.TextField(_('Description (English)')) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + class Meta: + verbose_name = _('Fursuit Gallery') + verbose_name_plural = _('Fursuit Galleries') + + def __str__(self): + return self.name + +class GalleryImage(models.Model): + gallery = models.ForeignKey(FursuitGallery, on_delete=models.CASCADE, related_name='images') + image = models.ImageField(upload_to='gallery/') + title = models.CharField(_('Title'), max_length=200, blank=True) + description = models.TextField(_('Description'), blank=True) + order = models.PositiveIntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['order', 'created_at'] + +class Category(models.Model): + name = models.CharField(_('Name'), max_length=100) + slug = models.SlugField(_('Slug'), max_length=100, unique=True) + description = models.TextField(_('Beschreibung'), blank=True) + image = models.ImageField(_('Bild'), upload_to='categories/', blank=True) + parent = models.ForeignKey('self', verbose_name=_('Übergeordnete Kategorie'), + on_delete=models.CASCADE, null=True, blank=True, + related_name='children') + + class Meta: + verbose_name = _('Kategorie') + verbose_name_plural = _('Kategorien') + ordering = ['name'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + +class ProductType(models.Model): + name = models.CharField(_('Name'), max_length=100) + has_sizes = models.BooleanField(_('Hat Größen'), default=False) + has_colors = models.BooleanField(_('Hat Farben'), default=False) + has_custom_design = models.BooleanField(_('Erlaubt Custom Design'), default=False) + requires_measurements = models.BooleanField(_('Benötigt Maße'), default=False) + + class Meta: + verbose_name = _('Produkttyp') + verbose_name_plural = _('Produkttypen') + + def __str__(self): + return self.name + +class Product(models.Model): + category = models.ForeignKey(Category, verbose_name=_('Kategorie'), + on_delete=models.CASCADE, related_name='products') + product_type = models.ForeignKey(ProductType, verbose_name=_('Produkttyp'), + on_delete=models.CASCADE) + name = models.CharField(_('Name'), max_length=200) + slug = models.SlugField(_('Slug'), max_length=200, unique=True, default='') + description = models.TextField(_('Beschreibung')) + price = models.DecimalField(_('Preis'), max_digits=10, decimal_places=2, + validators=[MinValueValidator(Decimal('0.01'))], + default=Decimal('0.00')) + stock = models.PositiveIntegerField(_('Lagerbestand'), default=0) + available = models.BooleanField(_('Verfügbar'), default=True) + created = models.DateTimeField(_('Erstellt'), default=timezone.now) + updated = models.DateTimeField(_('Aktualisiert'), auto_now=True) + image = models.ImageField(_('Hauptbild'), upload_to='products/') + + class Meta: + verbose_name = _('Produkt') + verbose_name_plural = _('Produkte') + ordering = ['-created'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + +class ProductImage(models.Model): + product = models.ForeignKey(Product, verbose_name=_('Produkt'), + on_delete=models.CASCADE, related_name='images') + image = models.ImageField(_('Bild'), upload_to='products/') + alt_text = models.CharField(_('Alternativer Text'), max_length=200, blank=True) + is_feature = models.BooleanField(_('Ist Hauptbild'), default=False) + created = models.DateTimeField(_('Erstellt'), default=timezone.now) + + class Meta: + verbose_name = _('Produktbild') + verbose_name_plural = _('Produktbilder') + ordering = ['-is_feature', '-created'] + +class ProductVariant(models.Model): + SIZE_CHOICES = [ + ('XS', 'Extra Small'), + ('S', 'Small'), + ('M', 'Medium'), + ('L', 'Large'), + ('XL', 'Extra Large'), + ('XXL', '2X Large'), + ('XXXL', '3X Large'), + ] + + product = models.ForeignKey(Product, verbose_name=_('Produkt'), + on_delete=models.CASCADE, related_name='variants') + size = models.CharField(_('Größe'), max_length=10, choices=SIZE_CHOICES, blank=True) + color = models.CharField(_('Farbe'), max_length=50, blank=True) + sku = models.CharField(_('Artikelnummer'), max_length=50, unique=True) + price_adjustment = models.DecimalField(_('Preisanpassung'), + max_digits=10, decimal_places=2, default=0) + stock = models.PositiveIntegerField(_('Lagerbestand'), default=0) + image = models.ImageField(_('Variantenbild'), upload_to='variants/', blank=True) + + class Meta: + verbose_name = _('Produktvariante') + verbose_name_plural = _('Produktvarianten') + unique_together = ['product', 'size', 'color'] + + def __str__(self): + variant_parts = [] + if self.size: + variant_parts.append(f"Größe: {self.size}") + if self.color: + variant_parts.append(f"Farbe: {self.color}") + return f"{self.product.name} ({', '.join(variant_parts)})" + +class CustomDesign(models.Model): + STATUS_CHOICES = [ + ('pending', _('Ausstehend')), + ('approved', _('Genehmigt')), + ('rejected', _('Abgelehnt')), + ('in_progress', _('In Bearbeitung')), + ('completed', _('Abgeschlossen')), + ] + + product = models.ForeignKey(Product, verbose_name=_('Produkt'), + on_delete=models.CASCADE, related_name='custom_designs') + customer = models.ForeignKey('auth.User', verbose_name=_('Kunde'), + on_delete=models.CASCADE) + design_file = models.FileField(_('Design-Datei'), upload_to='designs/') + notes = models.TextField(_('Anmerkungen')) + created = models.DateTimeField(_('Erstellt'), default=timezone.now) + status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='pending') + + class Meta: + verbose_name = _('Custom Design') + verbose_name_plural = _('Custom Designs') + ordering = ['-created'] + + def __str__(self): + return f"Custom Design für {self.product.name} von {self.customer.username}" + +class DesignTemplate(models.Model): + name = models.CharField(_('Name'), max_length=200) + name_en = models.CharField(_('Name (English)'), max_length=200) + description = models.TextField(_('Description')) + description_en = models.TextField(_('Description (English)')) + image = models.ImageField(upload_to='designs/') + model_3d = models.FileField(_('3D Model'), upload_to='3d_models/', null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + +class CustomerDesign(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(_('Design Name'), max_length=200) + design_file = models.FileField(upload_to='customer_designs/') + notes = models.TextField(_('Notes'), blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.user.username} - {self.name}" + +class Order(models.Model): + STATUS_CHOICES = [ + ('pending', _('Pending')), + ('confirmed', _('Confirmed')), + ('in_progress', _('In Progress')), + ('completed', _('Completed')), + ('cancelled', _('Cancelled')), + ] + + PAYMENT_METHODS = [ + ('paypal', 'PayPal'), + ('credit_card', _('Credit Card')), + ('bank_transfer', _('Bank Transfer')), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + payment_method = models.CharField(max_length=20, choices=PAYMENT_METHODS) + customer_design = models.ForeignKey(CustomerDesign, on_delete=models.SET_NULL, null=True, blank=True) + design_template = models.ForeignKey(DesignTemplate, on_delete=models.SET_NULL, null=True, blank=True) + special_instructions = models.TextField(blank=True) + + # Nur für Fursuits + measurements = models.JSONField(null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + total_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00')) + + def __str__(self): + return f"Order {self.id} - {self.product.name}" + +class OrderProgress(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='progress_updates') + title = models.CharField(_('Title'), max_length=200) + description = models.TextField(_('Description')) + image = models.ImageField(upload_to='progress/', null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"Progress update for Order {self.order.id}" + +class Cart(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def get_total(self): + return sum(item.get_subtotal() for item in self.items.all()) + + def __str__(self): + return f"Cart #{self.id} - {self.user.username}" + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + size = models.CharField(max_length=20, blank=True, null=True) + notes = models.TextField(blank=True, null=True) + custom_design = models.ImageField(upload_to='custom_designs/', blank=True, null=True) + added_at = models.DateTimeField(auto_now_add=True) + + def get_subtotal(self): + return self.product.base_price * self.quantity + + class Meta: + ordering = ['-added_at'] + + def __str__(self): + return f"{self.quantity}x {self.product.name} in Cart #{self.cart.id}" + +class ShippingAddress(models.Model): + COUNTRY_CHOICES = [ + ('DE', 'Deutschland'), + ('AT', 'Österreich'), + ('CH', 'Schweiz'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE) + first_name = models.CharField(_('First Name'), max_length=100) + last_name = models.CharField(_('Last Name'), max_length=100) + email = models.EmailField(_('Email')) + address = models.CharField(_('Address'), max_length=200) + city = models.CharField(_('City'), max_length=100) + zip = models.CharField(_('ZIP Code'), max_length=10) + country = models.CharField(_('Country'), max_length=2, choices=COUNTRY_CHOICES) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.first_name} {self.last_name}, {self.city}" + + def get_full_address(self): + return f"{self.address}, {self.zip} {self.city}, {self.get_country_display()}" + +class Checkout(models.Model): + STATUS_CHOICES = [ + ('address', _('Shipping Address')), + ('payment', _('Payment Method')), + ('confirm', _('Confirmation')), + ('completed', _('Completed')), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE) + cart = models.OneToOneField(Cart, on_delete=models.CASCADE) + shipping_address = models.ForeignKey(ShippingAddress, on_delete=models.SET_NULL, null=True) + payment_method = models.CharField(_('Payment Method'), max_length=20, choices=Order.PAYMENT_METHODS, null=True) + status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='address') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Checkout #{self.id} - {self.user.username}" + + def get_total(self): + cart_total = self.cart.get_total() + if cart_total < 200: + return cart_total + Decimal('5.99') + return cart_total + +class PayPalPayment(models.Model): + order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='paypal_payment') + payment_id = models.CharField(_('PayPal Payment ID'), max_length=100) + payer_id = models.CharField(_('PayPal Payer ID'), max_length=100) + status = models.CharField(_('Payment Status'), max_length=20, choices=[ + ('pending', _('Pending')), + ('completed', _('Completed')), + ('failed', _('Failed')), + ('refunded', _('Refunded')), + ]) + amount = models.DecimalField(_('Amount'), max_digits=10, decimal_places=2) + currency = models.CharField(_('Currency'), max_length=3, default='EUR') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"PayPal Payment {self.payment_id} for Order #{self.order.id}" + +class PaymentError(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='payment_errors') + error_code = models.CharField(_('Error Code'), max_length=100) + error_message = models.TextField(_('Error Message')) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Payment Error for Order #{self.order.id}: {self.error_code}" + +class ContactMessage(models.Model): + name = models.CharField(_('Name'), max_length=100) + email = models.EmailField(_('E-Mail')) + subject = models.CharField(_('Betreff'), max_length=200) + message = models.TextField(_('Nachricht')) + created_at = models.DateTimeField(_('Erstellt am'), auto_now_add=True) + + class Meta: + verbose_name = _('Kontaktnachricht') + verbose_name_plural = _('Kontaktnachrichten') + ordering = ['-created_at'] + + def __str__(self): + return f"{self.subject} - {self.name}" diff --git a/shop/settings.py b/shop/settings.py new file mode 100644 index 0000000..cd0cf65 --- /dev/null +++ b/shop/settings.py @@ -0,0 +1,143 @@ +""" +Django settings for shop project. + +Generated by 'django-admin startproject' using Django 5.2.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-8m^(a*6xx46y=v*z8j*s*f=hup!+cyzghx8e9f^eugbg1)o!1s' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # 'rest_framework', # Temporär auskommentiert + 'products', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'shop.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'shop.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'de' + +TIME_ZONE = 'Europe/Berlin' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +# Media files +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Authentication +LOGIN_REDIRECT_URL = 'product_list' +LOGOUT_REDIRECT_URL = 'product_list' + +# REST Framework - Temporär auskommentiert +# REST_FRAMEWORK = { +# 'DEFAULT_PERMISSION_CLASSES': [ +# 'rest_framework.permissions.IsAuthenticatedOrReadOnly', +# ], +# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', +# 'PAGE_SIZE': 10 +# } diff --git a/shop/signals.py b/shop/signals.py new file mode 100644 index 0000000..3ce9eae --- /dev/null +++ b/shop/signals.py @@ -0,0 +1,69 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver +from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ObjectDoesNotExist +from .models import Order, Product, PaymentError +from .emails import ( + send_order_confirmation, + send_order_status_update, + send_shipping_confirmation, + send_admin_notification, + send_low_stock_notification +) + +@receiver(post_save, sender=Order) +def handle_order_notifications(sender, instance, created, **kwargs): + """ + Sendet E-Mail-Benachrichtigungen basierend auf Bestellstatus + """ + if created: + # Neue Bestellung - Admin benachrichtigen + notification_type = 'fursuit_order' if any(p.product_type == 'fursuit' for p in instance.products.all()) else 'new_order' + if hasattr(instance, 'customer_design') and instance.customer_design: + notification_type = 'custom_design' + + send_admin_notification(None, instance, notification_type) + + else: + # Status-Änderung + try: + old_instance = Order.objects.get(id=instance.id) + if old_instance.status != instance.status: + # Status hat sich geändert - Kunde benachrichtigen + send_order_status_update(None, instance) + + # Bei Versand zusätzlich Versandbestätigung senden + if instance.status == 'completed' and instance.tracking_number: + send_shipping_confirmation(None, instance) + except ObjectDoesNotExist: + pass + +@receiver(post_save, sender=PaymentError) +def handle_payment_error(sender, instance, created, **kwargs): + """ + Benachrichtigt den Admin über Zahlungsfehler + """ + if created: + send_admin_notification(None, instance.order, 'payment_failed', { + 'payment_error': instance + }) + +@receiver(pre_save, sender=Product) +def check_stock_level(sender, instance, **kwargs): + """ + Überprüft den Lagerbestand und sendet Benachrichtigungen bei niedrigem Stand + """ + if not instance.pk: # Neues Produkt + return + + try: + old_instance = Product.objects.get(pk=instance.pk) + + # Wenn der Lagerbestand unter den Schwellenwert fällt + if (old_instance.stock > settings.LOW_STOCK_THRESHOLD and + instance.stock <= settings.LOW_STOCK_THRESHOLD): + send_low_stock_notification(None, instance) + + except ObjectDoesNotExist: + pass \ No newline at end of file diff --git a/shop/templates/shop/base.html b/shop/templates/shop/base.html new file mode 100644 index 0000000..4235e7f --- /dev/null +++ b/shop/templates/shop/base.html @@ -0,0 +1,497 @@ + +{% load i18n %} +{% load static %} + + + + + {% block title %}Kasico Art & Design - Fursuit Shop{% endblock %} + + + + + + + + + + + + + + + + {% csrf_token %} + + + + + +
+ {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} + +
+ {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + +
+
+
+
+
Über uns
+

Kasico Art & Design - Ihre erste Wahl für hochwertige, handgefertigte Fursuits und Custom Designs.

+
+
+
Kontakt
+

Email: info@kasico-art.com

+

Discord: Kasico#1234

+

Telegram: @KasicoArt

+
+
+
Quick Links
+ +
+
+
+
+

© 2025 Kasico Art & Design. Alle Rechte vorbehalten.

+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/shop/templates/shop/cart.html b/shop/templates/shop/cart.html new file mode 100644 index 0000000..bd69ff6 --- /dev/null +++ b/shop/templates/shop/cart.html @@ -0,0 +1,278 @@ +{% extends "shop/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Shopping Cart" %} - Fursuit Shop{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+

Ihr Warenkorb

+ {% if cart_items %} +

{{ cart_items|length }} Artikel in Ihrem Warenkorb

+ {% else %} +

Ihr Warenkorb ist leer

+ {% endif %} +
+ +{% if cart_items %} +
+ +
+
+ {% for item in cart_items %} +
+
+ +
+ {{ item.product.name }} +
+ + +
+

{{ item.product.name }}

+

{{ item.product.short_description|truncatewords:10 }}

+ {% if item.size %} +

Größe: {{ item.size }}

+ {% endif %} +
+ + {% if item.product.on_sale %} + {{ item.product.regular_price }} € + {{ item.product.sale_price }} € + {% else %} + {{ item.product.price }} € + {% endif %} + + x {{ item.quantity }} +
+
+ + +
+
+ + + +
+
+ + +
+

{{ item.total_price }} €

+ +
+
+
+ {% endfor %} +
+
+ + +
+
+

Bestellübersicht

+ +
+ Zwischensumme + {{ subtotal }} € +
+ + {% if shipping_cost %} +
+ Versandkosten + {{ shipping_cost }} € +
+ {% endif %} + + {% if discount %} +
+ Rabatt + -{{ discount }} € +
+ {% endif %} + +
+ +
+ Gesamtsumme + {{ total }} € +
+ + +
+
+ {% csrf_token %} + + +
+
+ + + +
+ + +
+

Versandinformationen

+
    +
  • + + Kostenloser Versand ab 200 € +
  • +
  • + + Sichere Verpackung +
  • +
  • + + 14 Tage Rückgaberecht +
  • +
+
+
+
+ +{% else %} + +
+ +

Ihr Warenkorb ist noch leer

+

Entdecken Sie unsere einzigartigen Fursuits und Accessoires

+ + Zum Shop + +
+{% endif %} + + +
+ +
+ + + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/checkout.html b/shop/templates/shop/checkout.html new file mode 100644 index 0000000..dfd1ea2 --- /dev/null +++ b/shop/templates/shop/checkout.html @@ -0,0 +1,468 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{% trans "Checkout" %} - Fursuit Shop{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
1
+
{% trans "Shipping" %}
+
+
+
2
+
{% trans "Payment" %}
+
+
+
3
+
{% trans "Confirm" %}
+
+
+ +
+
+ + {% if step == 'address' %} +
+
+

{% trans "Shipping Address" %}

+
+ {% csrf_token %} + + +
+
+ + + {% if form.first_name.errors %} +
+ {{ form.first_name.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.last_name.errors %} +
+ {{ form.last_name.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.email.errors %} +
+ {{ form.email.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.address.errors %} +
+ {{ form.address.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.city.errors %} +
+ {{ form.city.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.zip.errors %} +
+ {{ form.zip.errors }} +
+ {% endif %} +
+ +
+ + + {% if form.country.errors %} +
+ {{ form.country.errors }} +
+ {% endif %} +
+
+ +
+ +
+ +
+
+
+
+ + + {% elif step == 'payment' %} +
+
+

{% trans "Payment Method" %}

+
+ {% csrf_token %} + + + +
+ +
+
+
+ +
+
+ +
PayPal
+

+ {% trans "Fast and secure payment with PayPal" %} +

+
+
+
+ + +
+
+
+ +
+
+ +
{% trans "Credit Card" %}
+

+ {% trans "Pay with Visa, Mastercard, or American Express" %} +

+
+
+
+ + +
+
+
+ +
+
+ +
{% trans "Bank Transfer" %}
+

+ {% trans "Pay via bank transfer" %} +

+
+
+
+
+ + {% if form.payment_method.errors %} +
+ {{ form.payment_method.errors }} +
+ {% endif %} + +
+ +
+ + + {% trans "Back to Shipping" %} + + +
+
+
+
+ + + {% elif step == 'confirm' %} +
+
+

{% trans "Order Confirmation" %}

+ + +
+
{% trans "Shipping Address" %}
+

{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}

+

{{ order.shipping_address.address }}

+

{{ order.shipping_address.zip }} {{ order.shipping_address.city }}

+

{{ order.shipping_address.get_country_display }}

+
+ + +
+
{% trans "Payment Method" %}
+

+ {% if order.payment_method == 'paypal' %} + PayPal + {% elif order.payment_method == 'credit_card' %} + {% trans "Credit Card" %} + {% else %} + {% trans "Bank Transfer" %} + {% endif %} +

+
+ + +
+
{% trans "Order Items" %}
+ {% for item in cart.items.all %} +
+
+ {{ item.quantity }}x + {{ item.product.name }} + {% if item.size %} + ({{ item.size }}) + {% endif %} +
+ {{ item.get_subtotal }} € +
+ {% endfor %} +
+ +
+ + +
+
+ {% trans "Subtotal" %} + {{ cart.get_total }} € +
+ {% if cart.get_total < 200 %} +
+ {% trans "Shipping" %} + 5.99 € +
+ {% else %} +
+ {% trans "Shipping" %} + {% trans "FREE" %} +
+ {% endif %} +
+
+ {% trans "Total" %} + + {% if cart.get_total < 200 %} + {{ cart.get_total|add:"5.99" }} € + {% else %} + {{ cart.get_total }} € + {% endif %} + +
+
+ +
+ {% csrf_token %} + + +
+ + + {% trans "Back to Payment" %} + + +
+
+
+
+ {% endif %} +
+ + +
+
+
+
{% trans "Order Summary" %}
+ + {% for item in cart.items.all %} +
+
+ {{ item.quantity }}x + {{ item.product.name }} +
+ {{ item.get_subtotal }} € +
+ {% endfor %} + +
+ +
+ {% trans "Subtotal" %} + {{ cart.get_total }} € +
+ {% if cart.get_total < 200 %} +
+ {% trans "Shipping" %} + 5.99 € +
+ {% else %} +
+ {% trans "Shipping" %} + {% trans "FREE" %} +
+ {% endif %} +
+
+ {% trans "Total" %} + + {% if cart.get_total < 200 %} + {{ cart.get_total|add:"5.99" }} € + {% else %} + {{ cart.get_total }} € + {% endif %} + +
+
+
+
+
+
+{% endblock %} + +{% block extra_js %} +{% if step == 'payment' %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/contact.html b/shop/templates/shop/contact.html new file mode 100644 index 0000000..0f4eaf7 --- /dev/null +++ b/shop/templates/shop/contact.html @@ -0,0 +1,128 @@ +{% extends "shop/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}Kontakt - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+
+

Kontaktieren Sie uns

+
+
+ +
+
+
+ +
+
E-Mail
+

info@kasico-art.de

+
+
+
+ +
+
Telefon
+

+49 123 456789

+
+
+
+
+
+ +
+
Geschäftszeiten
+

Mo-Fr: 9:00 - 18:00 Uhr
Sa: 10:00 - 14:00 Uhr

+
+
+
+
+ +
+ + +
+ {% csrf_token %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+
+ + +
+ Bitte geben Sie Ihren Namen ein. +
+
+
+ + +
+ Bitte geben Sie eine gültige E-Mail-Adresse ein. +
+
+
+ + +
+ Bitte geben Sie einen Betreff ein. +
+
+
+ + +
+ Bitte geben Sie Ihre Nachricht ein. +
+
+
+ +
+
+
+
+
+ + +
+

Haben Sie eine allgemeine Frage?

+ + FAQ ansehen + +
+
+
+
+ +{% block extra_js %} + +{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/emails/admin_notification.html b/shop/templates/shop/emails/admin_notification.html new file mode 100644 index 0000000..9e4bb49 --- /dev/null +++ b/shop/templates/shop/emails/admin_notification.html @@ -0,0 +1,212 @@ + + + + + + + +
+ +

+ {% if notification_type == 'new_order' %} + {% trans "New Order Received" %} + {% elif notification_type == 'payment_failed' %} + {% trans "Payment Failed" %} + {% elif notification_type == 'custom_design' %} + {% trans "New Custom Design Order" %} + {% elif notification_type == 'fursuit_order' %} + {% trans "New Fursuit Order" %} + {% else %} + {% trans "Order Notification" %} + {% endif %} +

+
+ +
+ + {% if notification_type == 'new_order' %} + {% trans "New Order" %} + {% elif notification_type == 'payment_failed' %} + {% trans "Payment Failed" %} + {% elif notification_type == 'custom_design' %} + {% trans "Custom Design" %} + {% elif notification_type == 'fursuit_order' %} + {% trans "Fursuit Order" %} + {% endif %} + + +

{% trans "Order" %} #{{ order.id }}

+ +
+

{% trans "Customer Information" %}

+

+ {% trans "Name" %}: + {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }} +

+

+ {% trans "Email" %}: + {{ order.shipping_address.email }} +

+

+ {% trans "Address" %}:
+ {{ order.shipping_address.address }}
+ {{ order.shipping_address.zip }} {{ order.shipping_address.city }}
+ {{ order.shipping_address.get_country_display }} +

+
+ +

{% trans "Ordered Items" %}

+ {% for product in order.products.all %} +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+

{{ product.name }}

+ {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} +
+
+ {{ product.base_price }} € +
+
+ {% endfor %} + +
+ {% trans "Total" %}: {{ order.total_price }} € +
+ + {% if notification_type == 'payment_failed' and payment_error %} +
+

{% trans "Payment Error Details" %}

+

{% trans "Error Code" %}: {{ payment_error.error_code }}

+

{% trans "Error Message" %}: {{ payment_error.error_message }}

+
+ {% endif %} + + {% if notification_type == 'custom_design' and order.customer_design %} +
+

{% trans "Custom Design Details" %}

+

{% trans "Design Name" %}: {{ order.customer_design.name }}

+ {% if order.customer_design.notes %} +

{% trans "Notes" %}: {{ order.customer_design.notes }}

+ {% endif %} + {% if order.customer_design.design_file %} +

{% trans "Design File" %}: + + {% trans "Download Design File" %} + +

+ {% endif %} +
+ {% endif %} +
+ + + + \ No newline at end of file diff --git a/shop/templates/shop/emails/admin_notification.txt b/shop/templates/shop/emails/admin_notification.txt new file mode 100644 index 0000000..3ea1c75 --- /dev/null +++ b/shop/templates/shop/emails/admin_notification.txt @@ -0,0 +1,51 @@ +{% load i18n %} + +{% if notification_type == 'new_order' %} +{% trans "New Order Received" %} +{% elif notification_type == 'payment_failed' %} +{% trans "Payment Failed" %} +{% elif notification_type == 'custom_design' %} +{% trans "New Custom Design Order" %} +{% elif notification_type == 'fursuit_order' %} +{% trans "New Fursuit Order" %} +{% else %} +{% trans "Order Notification" %} +{% endif %} + +{% trans "Order" %} #{{ order.id }} + +{% trans "Customer Information" %}: +{% trans "Name" %}: {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }} +{% trans "Email" %}: {{ order.shipping_address.email }} + +{% trans "Address" %}: +{{ order.shipping_address.address }} +{{ order.shipping_address.zip }} {{ order.shipping_address.city }} +{{ order.shipping_address.get_country_display }} + +{% trans "Ordered Items" %}: +{% for product in order.products.all %} +- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %}) + {{ product.base_price }} € +{% endfor %} + +{% trans "Total" %}: {{ order.total_price }} € + +{% if notification_type == 'payment_failed' and payment_error %} +{% trans "Payment Error Details" %}: +{% trans "Error Code" %}: {{ payment_error.error_code }} +{% trans "Error Message" %}: {{ payment_error.error_message }} +{% endif %} + +{% if notification_type == 'custom_design' and order.customer_design %} +{% trans "Custom Design Details" %}: +{% trans "Design Name" %}: {{ order.customer_design.name }} +{% if order.customer_design.notes %} +{% trans "Notes" %}: {{ order.customer_design.notes }} +{% endif %} +{% if order.customer_design.design_file %} +{% trans "Design File" %}: {{ order.customer_design.design_file.url }} +{% endif %} +{% endif %} + +{% trans "View Order Details" %}: {{ order_url }} \ No newline at end of file diff --git a/shop/templates/shop/emails/low_stock_notification.html b/shop/templates/shop/emails/low_stock_notification.html new file mode 100644 index 0000000..05db628 --- /dev/null +++ b/shop/templates/shop/emails/low_stock_notification.html @@ -0,0 +1,118 @@ + + + + + + + +
+ +

{% trans "Low Stock Alert" %}

+
+ +
+ {% trans "Warning" %}: + {% trans "The following product is running low on stock and needs attention." %} +
+ +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+

{{ product.name }}

+ {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} + +
+ {% trans "Current Stock" %}: {{ product.stock }} +
+ +

+ {% trans "SKU" %}: {{ product.sku }}
+ {% trans "Base Price" %}: {{ product.base_price }} € +

+
+
+ + + + \ No newline at end of file diff --git a/shop/templates/shop/emails/low_stock_notification.txt b/shop/templates/shop/emails/low_stock_notification.txt new file mode 100644 index 0000000..8141b3a --- /dev/null +++ b/shop/templates/shop/emails/low_stock_notification.txt @@ -0,0 +1,15 @@ +{% load i18n %} + +{% trans "Low Stock Alert" %} + +{% trans "Warning" %}: {% trans "The following product is running low on stock and needs attention." %} + +{% trans "Product Details" %}: +{% trans "Name" %}: {{ product.name }} +{% trans "Type" %}: {% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %} +{% trans "SKU" %}: {{ product.sku }} +{% trans "Base Price" %}: {{ product.base_price }} € + +{% trans "Current Stock" %}: {{ product.stock }} + +{% trans "Manage this product at" %}: {{ product_url }} \ No newline at end of file diff --git a/shop/templates/shop/emails/order_confirmation.html b/shop/templates/shop/emails/order_confirmation.html new file mode 100644 index 0000000..8c97ebf --- /dev/null +++ b/shop/templates/shop/emails/order_confirmation.html @@ -0,0 +1,182 @@ + + + + + + + +
+ +

{% trans "Thank You for Your Order!" %}

+

{% trans "Your order has been confirmed and is being processed." %}

+
+ +
+

{% trans "Order Details" %}

+

{% trans "Order Number" %}: #{{ order.id }}

+

{% trans "Order Date" %}: {{ order.created_at|date:"d.m.Y" }}

+

+ {% trans "Payment Method" %}: + {% if order.payment_method == 'paypal' %} + PayPal + {% elif order.payment_method == 'credit_card' %} + {% trans "Credit Card" %} + {% else %} + {% trans "Bank Transfer" %} + {% endif %} +

+ +

{% trans "Shipping Address" %}

+

+ {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
+ {{ order.shipping_address.address }}
+ {{ order.shipping_address.zip }} {{ order.shipping_address.city }}
+ {{ order.shipping_address.get_country_display }} +

+ +

{% trans "Ordered Items" %}

+ {% for product in order.products.all %} +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+

{{ product.name }}

+ {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} +
+
+ {{ product.base_price }} € +
+
+ {% endfor %} + +
+

+ {% trans "Subtotal" %}: + {{ order.total_price }} € +

+ {% if order.total_price < 200 %} +

+ {% trans "Shipping" %}: + 5.99 € +

+

+ {% trans "Total" %}: + {{ order.total_price|add:"5.99" }} € +

+ {% else %} +

+ {% trans "Shipping" %}: + {% trans "FREE" %} +

+

+ {% trans "Total" %}: + {{ order.total_price }} € +

+ {% endif %} +
+
+ + + + + + \ No newline at end of file diff --git a/shop/templates/shop/emails/order_confirmation.txt b/shop/templates/shop/emails/order_confirmation.txt new file mode 100644 index 0000000..94db0e7 --- /dev/null +++ b/shop/templates/shop/emails/order_confirmation.txt @@ -0,0 +1,38 @@ +{% load i18n %} + +{% trans "Thank You for Your Order!" %} + +{% trans "Your order has been confirmed and is being processed." %} + +{% trans "Order Details" %}: +{% trans "Order Number" %}: #{{ order.id }} +{% trans "Order Date" %}: {{ order.created_at|date:"d.m.Y" }} +{% trans "Payment Method" %}: {% if order.payment_method == 'paypal' %}PayPal{% elif order.payment_method == 'credit_card' %}{% trans "Credit Card" %}{% else %}{% trans "Bank Transfer" %}{% endif %} + +{% trans "Shipping Address" %}: +{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }} +{{ order.shipping_address.address }} +{{ order.shipping_address.zip }} {{ order.shipping_address.city }} +{{ order.shipping_address.get_country_display }} + +{% trans "Ordered Items" %}: +{% for product in order.products.all %} +- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %}) + {{ product.base_price }} € +{% endfor %} + +{% trans "Subtotal" %}: {{ order.total_price }} € +{% if order.total_price < 200 %} +{% trans "Shipping" %}: 5.99 € +{% trans "Total" %}: {{ order.total_price|add:"5.99" }} € +{% else %} +{% trans "Shipping" %}: {% trans "FREE" %} +{% trans "Total" %}: {{ order.total_price }} € +{% endif %} + +{% trans "You can view your order details at" %}: {{ order_url }} + +{% trans "If you have any questions about your order, please contact our support team." %} +support@fursuitshop.com + +© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %} \ No newline at end of file diff --git a/shop/templates/shop/emails/order_status_update.html b/shop/templates/shop/emails/order_status_update.html new file mode 100644 index 0000000..c1771e1 --- /dev/null +++ b/shop/templates/shop/emails/order_status_update.html @@ -0,0 +1,129 @@ + + + + + + + +
+ +

{% trans "Order Status Update" %}

+

{% trans "Your order has been updated." %}

+
+ +
+

{% trans "Order" %} #{{ order.id }}

+ +

+ {% trans "New Status" %}:
+ + {{ order.get_status_display }} + +

+ + {% if update %} +

{{ update.title }}

+

{{ update.description }}

+ {% if update.image %} + {% trans 'Progress Image' %} + {% endif %} + {% endif %} + + {% if order.status == 'confirmed' %} +

{% trans "Your order has been confirmed and will be processed soon." %}

+ {% elif order.status == 'in_progress' %} +

{% trans "We are currently working on your order." %}

+ {% elif order.status == 'completed' %} +

{% trans "Your order has been completed and will be shipped soon." %}

+ {% if order.tracking_number %} +

+ {% trans "Tracking Number" %}: + {{ order.tracking_number }} +

+ {% endif %} + {% endif %} +
+ + + + + + \ No newline at end of file diff --git a/shop/templates/shop/emails/order_status_update.txt b/shop/templates/shop/emails/order_status_update.txt new file mode 100644 index 0000000..a44d06c --- /dev/null +++ b/shop/templates/shop/emails/order_status_update.txt @@ -0,0 +1,32 @@ +{% load i18n %} + +{% trans "Order Status Update" %} + +{% trans "Your order has been updated." %} + +{% trans "Order" %} #{{ order.id }} + +{% trans "New Status" %}: {{ order.get_status_display }} + +{% if update %} +{{ update.title }} +{{ update.description }} +{% endif %} + +{% if order.status == 'confirmed' %} +{% trans "Your order has been confirmed and will be processed soon." %} +{% elif order.status == 'in_progress' %} +{% trans "We are currently working on your order." %} +{% elif order.status == 'completed' %} +{% trans "Your order has been completed and will be shipped soon." %} +{% if order.tracking_number %} +{% trans "Tracking Number" %}: {{ order.tracking_number }} +{% endif %} +{% endif %} + +{% trans "You can view your order details at" %}: {{ order_url }} + +{% trans "If you have any questions about your order, please contact our support team." %} +support@fursuitshop.com + +© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %} \ No newline at end of file diff --git a/shop/templates/shop/emails/shipping_confirmation.html b/shop/templates/shop/emails/shipping_confirmation.html new file mode 100644 index 0000000..e3f9f60 --- /dev/null +++ b/shop/templates/shop/emails/shipping_confirmation.html @@ -0,0 +1,167 @@ + + + + + + + +
+ +
📦
+

{% trans "Your Order Has Been Shipped!" %}

+

{% trans "Great news! Your order has been shipped and is on its way to you." %}

+
+ +
+

{% trans "Order" %} #{{ order.id }}

+ +
+

{% trans "Tracking Information" %}

+
+ {{ order.tracking_number }} +
+

+ {% trans "Use this number to track your shipment" %} +

+
+ +

{% trans "Shipping Address" %}

+

+ {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
+ {{ order.shipping_address.address }}
+ {{ order.shipping_address.zip }} {{ order.shipping_address.city }}
+ {{ order.shipping_address.get_country_display }} +

+ +
+

{% trans "Shipped Items" %}

+ {% for product in order.products.all %} +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+

{{ product.name }}

+ {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} +
+
+ {% endfor %} +
+
+ + + + + + \ No newline at end of file diff --git a/shop/templates/shop/emails/shipping_confirmation.txt b/shop/templates/shop/emails/shipping_confirmation.txt new file mode 100644 index 0000000..7364601 --- /dev/null +++ b/shop/templates/shop/emails/shipping_confirmation.txt @@ -0,0 +1,28 @@ +{% load i18n %} + +{% trans "Your Order Has Been Shipped!" %} + +{% trans "Great news! Your order has been shipped and is on its way to you." %} + +{% trans "Order" %} #{{ order.id }} + +{% trans "Tracking Information" %}: +{% trans "Tracking Number" %}: {{ order.tracking_number }} + +{% trans "Shipping Address" %}: +{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }} +{{ order.shipping_address.address }} +{{ order.shipping_address.zip }} {{ order.shipping_address.city }} +{{ order.shipping_address.get_country_display }} + +{% trans "Ordered Items" %}: +{% for product in order.products.all %} +- {{ product.name }} ({% if product.product_type == 'fursuit' %}{% trans "Fursuit" %}{% else %}{% trans "Printed Item" %}{% endif %}) +{% endfor %} + +{% trans "You can track your shipment and view your order details at" %}: {{ order_url }} + +{% trans "If you have any questions about your shipment, please contact our support team." %} +support@fursuitshop.com + +© {% now "Y" %} Fursuit Shop. {% trans "All rights reserved." %} \ No newline at end of file diff --git a/shop/templates/shop/gallery.html b/shop/templates/shop/gallery.html new file mode 100644 index 0000000..7313726 --- /dev/null +++ b/shop/templates/shop/gallery.html @@ -0,0 +1,233 @@ +{% extends 'shop/base.html' %} +{% load static %} + +{% block content %} + +
+ Kasico Art & Design Logo +

Unsere Fursuit Galerie

+

Entdecken Sie unsere handgefertigten Kreationen und lassen Sie sich inspirieren

+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + +{% if is_paginated %} + +{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/gallery_detail.html b/shop/templates/shop/gallery_detail.html new file mode 100644 index 0000000..4b4103e --- /dev/null +++ b/shop/templates/shop/gallery_detail.html @@ -0,0 +1,228 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{{ gallery.name }} - {% trans "Gallery" %} - Fursuit Shop{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+ + +
+
+ + {% if gallery.images.first %} +
+ + {{ gallery.name }} + +
+ {% endif %} + + + {% if gallery.images.all|length > 1 %} + + {% endif %} +
+ +
+
+
+
+

{{ gallery.name }}

+ +
+ +
+ {% if gallery.fursuit_type == 'fullsuit' %} + {% trans "Fullsuit" %} + {% elif gallery.fursuit_type == 'partial' %} + {% trans "Partial Suit" %} + {% else %} + {% trans "Head Only" %} + {% endif %} + + {% if gallery.style == 'toony' %} + {% trans "Toony" %} + {% elif gallery.style == 'realistic' %} + {% trans "Realistic" %} + {% else %} + {% trans "Semi-Realistic" %} + {% endif %} +
+ +

{{ gallery.description }}

+ +
+ +
+ {% if gallery.features %} +
+
+
{% trans "Features" %}
+
    + {% for feature in gallery.features %} +
  • {{ feature }}
  • + {% endfor %} +
+
+
+ {% endif %} + + {% if gallery.materials %} +
+
+
{% trans "Materials" %}
+
    + {% for material in gallery.materials %} +
  • {{ material }}
  • + {% endfor %} +
+
+
+ {% endif %} +
+ +
+ + +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/gallery_list.html b/shop/templates/shop/gallery_list.html new file mode 100644 index 0000000..3b84aa8 --- /dev/null +++ b/shop/templates/shop/gallery_list.html @@ -0,0 +1,194 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{% trans "Fursuit Gallery" %} - Fursuit Shop{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+ +
+ +
+ + + +
+
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/home.html b/shop/templates/shop/home.html new file mode 100644 index 0000000..abbb396 --- /dev/null +++ b/shop/templates/shop/home.html @@ -0,0 +1,160 @@ +{% extends "shop/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}Willkommen bei Kasico Art & Design - Fursuit Shop{% endblock %} + +{% block content %} + +
+
+
+ Kasico Art & Design Logo +
+
+

Willkommen bei Kasico Art & Design

+

Wo Ihre Fursuit-Träume Realität werden

+ + Custom Order starten + + + Galerie ansehen + + + Kontakt + +
+
+
+ + +
+

Unsere Dienstleistungen

+
+
+
+
+ +
+

Custom Design

+

Ihr einzigartiger Charakter, zum Leben erweckt mit höchster Handwerkskunst und Liebe zum Detail.

+
    +
  • Individuelle Konzeption
  • +
  • 3D-Modellierung
  • +
  • Maßanfertigung
  • +
+
+
+
+
+
+ +
+

Reparaturen

+

Professionelle Pflege und Reparatur für Ihren bestehenden Fursuit.

+
    +
  • Fell-Erneuerung
  • +
  • Strukturreparaturen
  • +
  • Reinigung
  • +
+
+
+
+
+
+ +
+

Fotoshootings

+

Professionelle Fotografie-Sessions für Ihren Fursuit.

+
    +
  • Studio-Aufnahmen
  • +
  • Outdoor-Shootings
  • +
  • Event-Dokumentation
  • +
+
+
+
+
+ + +
+
+

Custom Orders

+
+
+
+ Kasico Art & Design +
+
+
+
+

Ihr Traum-Fursuit wartet auf Sie

+
    +
  • + + Kostenlose Designberatung +
  • +
  • + + Detaillierte 3D-Visualisierung +
  • +
  • + + Regelmäßige Fortschrittsberichte +
  • +
  • + + Höchste Materialqualität +
  • +
  • + + Maßgeschneiderte Passform +
  • +
+ + Anfrage senden + +
+
+
+
+
+ + +
+

Häufig gestellte Fragen

+
+ {% for faq in faqs %} +
+

+ +

+
+
+ {{ faq.answer }} +
+
+
+ {% endfor %} +
+
+ + +
+
+

Haben Sie Fragen?

+

Wir sind für Sie da! Kontaktieren Sie uns für individuelle Beratung und Support.

+ + Kontaktieren Sie uns + +
+
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/login.html b/shop/templates/shop/login.html new file mode 100644 index 0000000..3bf660e --- /dev/null +++ b/shop/templates/shop/login.html @@ -0,0 +1,153 @@ +{% extends "shop/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Login" %} - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Login" %}

+
+ +
+ {% csrf_token %} + + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.username }} + {% if form.username.errors %} +
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.password }} + {% if form.password.errors %} +
+ {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+ +
+

{% trans "Don't have an account?" %} + {% trans "Register here" %} +

+

{% trans "Forgot your password?" %}

+
+
+
+
+
+ + + +{% block extra_js %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/my_orders.html b/shop/templates/shop/my_orders.html new file mode 100644 index 0000000..dbeabed --- /dev/null +++ b/shop/templates/shop/my_orders.html @@ -0,0 +1,208 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{% trans "My Orders" %} - Fursuit Shop{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+

{% trans "My Orders" %}

+ + + {% trans "Continue Shopping" %} + +
+ + {% if orders %} +
+ {% for order in orders %} +
+
+
+
+ +
+
+ {% trans "Order" %} #{{ order.id }} +
+
+ + {{ order.get_status_display }} + +
+
+ {{ order.created_at|date:"d.m.Y" }} +
+
+ + +
+
{% trans "Order Items" %}
+ {% for product in order.products.all %} +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+ {{ product.name }} + {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} +
+
+ {% endfor %} +
+ + +
+
{% trans "Payment" %}
+

+ {% if order.payment_method == 'paypal' %} + PayPal + {% elif order.payment_method == 'credit_card' %} + {% trans "Credit Card" %} + {% else %} + {% trans "Bank Transfer" %} + {% endif %} +

+

+ {{ order.total_price }} € +

+
+ + +
+ +
+
+ + +
+
+ {% for update in order.progress_updates.all %} +
+
+
{{ update.title }}
+
+ {{ update.created_at|date:"d.m.Y H:i" }} +
+

{{ update.description }}

+ {% if update.image %} + {% trans 'Progress Image' %} + {% endif %} +
+ {% endfor %} +
+
+
+
+
+ {% endfor %} +
+ {% else %} +
+ +

{% trans "No orders yet" %}

+

+ {% trans "You haven't placed any orders yet." %} +

+ + + {% trans "Start Shopping" %} + +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/order_success.html b/shop/templates/shop/order_success.html new file mode 100644 index 0000000..302bab1 --- /dev/null +++ b/shop/templates/shop/order_success.html @@ -0,0 +1,129 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{% trans "Order Successful" %} - Fursuit Shop{% endblock %} + +{% block content %} +
+
+ + +

{% trans "Thank You for Your Order!" %}

+ +

+ {% trans "Your payment was successful and your order has been confirmed." %} +
+ {% trans "We'll send you an email with your order details shortly." %} +

+
+ +
+
+
+
+
{% trans "Order Details" %}
+ +
+
+

+ {% trans "Order Number" %}: + #{{ order.id }} +

+

+ {% trans "Order Date" %}: + {{ order.created_at|date:"d.m.Y" }} +

+

+ {% trans "Payment Method" %}: + {% if order.payment_method == 'paypal' %} + PayPal + {% elif order.payment_method == 'credit_card' %} + {% trans "Credit Card" %} + {% else %} + {% trans "Bank Transfer" %} + {% endif %} +

+
+
+

+ {% trans "Shipping To" %}: +

+

+ {{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}
+ {{ order.shipping_address.address }}
+ {{ order.shipping_address.zip }} {{ order.shipping_address.city }}
+ {{ order.shipping_address.get_country_display }} +

+
+
+ +
{% trans "Ordered Items" %}
+ {% for product in order.products.all %} +
+ {% if product.image %} + {{ product.name }} + {% endif %} +
+
{{ product.name }}
+ + {% if product.product_type == 'fursuit' %} + {% trans "Fursuit" %} + {% else %} + {% trans "Printed Item" %} + {% endif %} + +
+
+ {{ product.base_price }} € +
+
+ {% endfor %} + +
+ +
+

+ {% trans "Subtotal" %}: + {{ order.total_price }} € +

+ {% if order.total_price < 200 %} +

+ {% trans "Shipping" %}: + 5.99 € +

+

+ {% trans "Total" %}: + {{ order.total_price|add:"5.99" }} € +

+ {% else %} +

+ {% trans "Shipping" %}: + {% trans "FREE" %} +

+

+ {% trans "Total" %}: + {{ order.total_price }} € +

+ {% endif %} +
+
+
+ + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset.html b/shop/templates/shop/password_reset.html new file mode 100644 index 0000000..34ef2a3 --- /dev/null +++ b/shop/templates/shop/password_reset.html @@ -0,0 +1,114 @@ +{% extends 'shop/base.html' %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Reset Password" %} - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Reset Password" %}

+

{% trans "Enter your email address below, and we'll send you instructions for setting a new password." %}

+
+ +
+ {% csrf_token %} + + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.email }} + {% if form.email.help_text %} +
{{ form.email.help_text }}
+ {% endif %} +
+ +
+ +
+
+ +
+

{% trans "Remember your password?" %} + {% trans "Login here" %} +

+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset_complete.html b/shop/templates/shop/password_reset_complete.html new file mode 100644 index 0000000..1bc807e --- /dev/null +++ b/shop/templates/shop/password_reset_complete.html @@ -0,0 +1,82 @@ +{% extends 'shop/base.html' %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Password Reset Complete" %} - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Password Reset Complete" %}

+
+ +
+ +
+ +

{% trans "Your password has been set. You may go ahead and log in now." %}

+ + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset_confirm.html b/shop/templates/shop/password_reset_confirm.html new file mode 100644 index 0000000..0726a70 --- /dev/null +++ b/shop/templates/shop/password_reset_confirm.html @@ -0,0 +1,142 @@ +{% extends 'shop/base.html' %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Set New Password" %} - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Set New Password" %}

+
+ + {% if validlink %} +
+ {% csrf_token %} + + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.new_password1 }} + {% if form.new_password1.help_text %} +
{{ form.new_password1.help_text }}
+ {% endif %} +
+ +
+ + {{ form.new_password2 }} + {% if form.new_password2.help_text %} +
{{ form.new_password2.help_text }}
+ {% endif %} +
+ +
+ +
+
+ {% else %} +
+
+ +
+

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

+ + {% trans "Request New Reset Link" %} + +
+ {% endif %} +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset_done.html b/shop/templates/shop/password_reset_done.html new file mode 100644 index 0000000..ca5988a --- /dev/null +++ b/shop/templates/shop/password_reset_done.html @@ -0,0 +1,84 @@ +{% extends 'shop/base.html' %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Password Reset Email Sent" %} - Kasico Art & Design{% endblock %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Check Your Email" %}

+
+ +
+ +
+ +

{% trans "We've emailed you instructions for setting your password. You should receive them shortly." %}

+ +

{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}

+ + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset_email.html b/shop/templates/shop/password_reset_email.html new file mode 100644 index 0000000..a7911ec --- /dev/null +++ b/shop/templates/shop/password_reset_email.html @@ -0,0 +1,15 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at Kasico Art & Design.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} +{% endblock %} + +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The Kasico Art & Design Team{% endblocktrans %} + +{% endautoescape %} \ No newline at end of file diff --git a/shop/templates/shop/password_reset_subject.txt b/shop/templates/shop/password_reset_subject.txt new file mode 100644 index 0000000..742368c --- /dev/null +++ b/shop/templates/shop/password_reset_subject.txt @@ -0,0 +1,3 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Password reset on Kasico Art & Design{% endblocktrans %} +{% endautoescape %} \ No newline at end of file diff --git a/shop/templates/shop/payment_failed.html b/shop/templates/shop/payment_failed.html new file mode 100644 index 0000000..384e7ef --- /dev/null +++ b/shop/templates/shop/payment_failed.html @@ -0,0 +1,41 @@ +{% extends "shop/base.html" %} +{% load i18n %} + +{% block title %}{% trans "Payment Failed" %} - Fursuit Shop{% endblock %} + +{% block content %} +
+
+ + +

{% trans "Payment Failed" %}

+ +

+ {% trans "We're sorry, but there was a problem processing your payment." %} +
+ {% trans "Please try again or choose a different payment method." %} +

+ + {% if order.payment_errors.exists %} +
+
{% trans "Error Details" %}:
+ {% for error in order.payment_errors.all|slice:"-1:" %} +

{{ error.error_message }}

+ {% endfor %} +
+ {% endif %} + + +
+
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/product_detail.html b/shop/templates/shop/product_detail.html new file mode 100644 index 0000000..40a1c5f --- /dev/null +++ b/shop/templates/shop/product_detail.html @@ -0,0 +1,338 @@ +{% extends 'shop/base.html' %} +{% load static %} + +{% block title %}{{ product.name }} - Fursuit Shop{% endblock %} + +{% block content %} +
+ +
+
+ +
+ {{ product.name }} + + {% if product.on_sale %} +
+ Sale! +
+ {% endif %} +
+ + + {% if product.gallery.all %} +
+
+ Hauptbild +
+ {% for image in product.gallery.all %} +
+ {{ image.alt_text }} +
+ {% endfor %} +
+ {% endif %} +
+
+ + +
+
+

{{ product.name }}

+ + +
+ {% if product.on_sale %} + {{ product.regular_price }} € + {{ product.sale_price }} € + {% else %} + {{ product.price }} € + {% endif %} +
+ + +
+ {% if product.stock > 0 %} +
+ Auf Lager + {% if product.stock < 5 %} + (Nur noch {{ product.stock }} verfügbar) + {% endif %} +
+ {% else %} +
+ Auf Bestellung (4-6 Wochen Lieferzeit) +
+ {% endif %} +
+ + +
+

{{ product.short_description }}

+
+ + + {% if product.has_sizes %} +
+ + +
+ {% endif %} + + +
+ +
+ + + +
+
+ + +
+ {% if product.stock > 0 %} + + {% else %} + + {% endif %} + +
+
+ + +
+
+

+ +

+
+
+ {{ product.description|linebreaks }} +
+
+
+ +
+

+ +

+
+
+
    + {% for spec in product.specifications %} +
  • + + {{ spec.name }}: {{ spec.value }} +
  • + {% endfor %} +
+
+
+
+ +
+

+ +

+
+
+
Versandinformationen
+
    +
  • + + Sorgfältig verpackt in spezieller Schutzverpackung +
  • +
  • + + Versand innerhalb von 2-3 Werktagen +
  • +
  • + + Weltweiter Versand verfügbar +
  • +
+ +
Rückgaberecht
+

14 Tage Rückgaberecht bei ungetragenen Artikeln in Originalverpackung.

+
+
+
+
+
+
+ + +{% if related_products %} +
+

Das könnte Ihnen auch gefallen

+
+ {% for related in related_products|slice:":3" %} +
+
+ {{ related.name }} +

{{ related.name }}

+

{{ related.short_description|truncatewords:15 }}

+ +
+
+ {% endfor %} +
+
+{% endif %} + + +
+ + + +
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/product_list.html b/shop/templates/shop/product_list.html new file mode 100644 index 0000000..4cf5ed5 --- /dev/null +++ b/shop/templates/shop/product_list.html @@ -0,0 +1,203 @@ +{% extends 'shop/base.html' %} +{% load static %} + +{% block title %}Produkte - {{ block.super }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+

Unsere Produkte

+ + +
+
+ +
+ +
+ + +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + + {% if products %} +
+ {% for product in products %} +
+
+ + {{ product.get_fursuit_type_display }} + + {% if product.is_custom_order %} + + Custom Order + + {% endif %} + {% if product.image %} + {{ product.name }} + {% else %} + Placeholder + {% endif %} +
+
{{ product.name }}
+

{{ product.description|truncatewords:20 }}

+
+ {{ product.price }} € +
+ {% if product.average_rating %} + {% for i in "12345"|make_list %} + + {% endfor %} + {% else %} + {% for i in "12345"|make_list %} + + {% endfor %} + {% endif %} + ({{ product.review_count|default:"0" }}) +
+
+

+ + + {% if product.stock > 5 %} + Auf Lager + {% elif product.stock > 0 %} + Nur noch {{ product.stock }} verfügbar + {% else %} + Ausverkauft + {% endif %} + +

+
+ +
+
+ {% endfor %} +
+ + + {% if is_paginated %} + + {% endif %} + {% else %} +
+ Keine Produkte gefunden. + {% if request.GET %} + Filter zurücksetzen + {% endif %} +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/shop/templates/shop/register.html b/shop/templates/shop/register.html new file mode 100644 index 0000000..e7e4b96 --- /dev/null +++ b/shop/templates/shop/register.html @@ -0,0 +1,142 @@ +{% extends 'shop/base.html' %} +{% load i18n %} +{% load static %} + +{% block content %} +
+
+
+
+ +
+ Kasico Art & Design Logo +

{% trans "Create Account" %}

+
+ +
+ {% csrf_token %} + + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.username }} + {% if form.username.help_text %} +
{{ form.username.help_text }}
+ {% endif %} +
+ +
+ + {{ form.password1 }} + {% if form.password1.help_text %} +
{{ form.password1.help_text }}
+ {% endif %} +
+ +
+ + {{ form.password2 }} + {% if form.password2.help_text %} +
{{ form.password2.help_text }}
+ {% endif %} +
+ +
+ +
+
+ +
+

{% trans "Already have an account?" %} + {% trans "Login here" %} +

+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/shop/tests.py b/shop/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/shop/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shop/urls.py b/shop/urls.py new file mode 100644 index 0000000..44c0999 --- /dev/null +++ b/shop/urls.py @@ -0,0 +1,36 @@ +""" +URL configuration for shop project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from .views import home, contact +# from rest_framework.routers import DefaultRouter # Temporär auskommentiert +# from products.views import ProductViewSet, ReviewViewSet # Temporär auskommentiert + +# router = DefaultRouter() # Temporär auskommentiert +# router.register(r'products', ProductViewSet) # Temporär auskommentiert +# router.register(r'reviews', ReviewViewSet) # Temporär auskommentiert + +app_name = 'shop' + +urlpatterns = [ + path('', home, name='home'), + path('contact/', contact, name='contact'), + path('accounts/', include('django.contrib.auth.urls')), + # path('api/', include(router.urls)), # Temporär auskommentiert + # path('api-auth/', include('rest_framework.urls')), # Temporär auskommentiert +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/shop/views.py b/shop/views.py new file mode 100644 index 0000000..2a0f2d8 --- /dev/null +++ b/shop/views.py @@ -0,0 +1,47 @@ +from django.shortcuts import render, redirect +from django.contrib import messages +from django.utils.translation import gettext as _ +from products.models import Product, GalleryImage +from .models import ContactMessage +from django.contrib.auth.forms import UserCreationForm + +def home(request): + featured_products = Product.objects.filter(is_featured=True)[:3] + latest_galleries = GalleryImage.objects.filter(is_featured=True)[:3] + + return render(request, 'shop/home.html', { + 'featured_products': featured_products, + 'latest_galleries': latest_galleries, + }) + +def contact(request): + if request.method == 'POST': + name = request.POST.get('name') + email = request.POST.get('email') + subject = request.POST.get('subject') + message = request.POST.get('message') + + if name and email and subject and message: + ContactMessage.objects.create( + name=name, + email=email, + subject=subject, + message=message + ) + messages.success(request, 'Ihre Nachricht wurde erfolgreich gesendet! Wir werden uns in Kürze bei Ihnen melden.') + return redirect('shop:contact') + else: + messages.error(request, 'Bitte füllen Sie alle Pflichtfelder aus.') + + return render(request, 'shop/contact.html') + +def register(request): + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _('Ihr Account wurde erfolgreich erstellt! Sie können sich jetzt anmelden.')) + return redirect('login') + else: + form = UserCreationForm() + return render(request, 'shop/register.html', {'form': form}) \ No newline at end of file diff --git a/shop/wsgi.py b/shop/wsgi.py new file mode 100644 index 0000000..86e9973 --- /dev/null +++ b/shop/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for shop project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shop.settings') + +application = get_wsgi_application() diff --git a/static/css/dashboard.css b/static/css/dashboard.css new file mode 100644 index 0000000..f4b7a94 --- /dev/null +++ b/static/css/dashboard.css @@ -0,0 +1,49 @@ +.progress-bar-custom { + height: 8px; + border-radius: 4px; +} + +.status-badge { + font-size: 0.85rem; + padding: 0.35em 0.65em; +} + +.stats-card { + transition: transform 0.2s; +} + +.stats-card:hover { + transform: translateY(-5px); +} + +/* Status Badge Colors */ +.badge.bg-pending { background-color: #ffc107; } +.badge.bg-processing { background-color: #17a2b8; } +.badge.bg-shipped { background-color: #28a745; } +.badge.bg-delivered { background-color: #20c997; } +.badge.bg-cancelled { background-color: #dc3545; } +.badge.bg-quoted { background-color: #6610f2; } +.badge.bg-approved { background-color: #198754; } +.badge.bg-in_progress { background-color: #0d6efd; } +.badge.bg-ready { background-color: #20c997; } + +/* Avatar Styling */ +.avatar-placeholder { + font-size: 24px; +} + +/* Card Styling */ +.card-header { + background-color: #f8f9fa; +} + +/* Table Styling */ +.table > :not(caption) > * > * { + padding: 0.75rem; +} + +/* Modal Styling */ +.modal-body { + max-height: calc(100vh - 210px); + overflow-y: auto; +} \ No newline at end of file diff --git a/static/css/products.css b/static/css/products.css new file mode 100644 index 0000000..1eb222a --- /dev/null +++ b/static/css/products.css @@ -0,0 +1,192 @@ +.product-card { + transition: transform 0.3s ease, box-shadow 0.3s ease; + border: none; + border-radius: 15px; + overflow: hidden; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 30px rgba(139, 92, 246, 0.15); +} + +.product-card .card-img-top { + height: 250px; + object-fit: cover; +} + +.product-badge { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; +} + +.product-type-badge { + position: absolute; + top: 10px; + left: 10px; + z-index: 2; +} + +.price-tag { + font-size: 1.25rem; + font-weight: bold; + color: #8B5CF6; +} + +.stock-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; +} + +.stock-high { + background-color: #10B981; +} + +.stock-medium { + background-color: #F59E0B; +} + +.stock-low { + background-color: #EF4444; +} + +.filter-section { + background: white; + border-radius: 15px; + padding: 1.5rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; +} + +.filter-section .form-label { + font-weight: 500; +} + +.price-range-slider { + height: 5px; + position: relative; + background-color: #e1e9f6; + border-radius: 2px; +} + +.price-range-slider .ui-slider-range { + height: 5px; + background-color: #0d6efd; + border-radius: 2px; +} + +.price-range-slider .ui-slider-handle { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #0d6efd; + border: none; + top: -8px; +} + +.rating-stars { + color: #F59E0B; +} + +.rating-count { + color: #6c757d; + font-size: 0.9rem; +} + +/* Sortieroptionen Styling */ +.sort-options .btn-outline-secondary { + border-radius: 20px; + margin: 0 5px; + padding: 5px 15px; +} + +/* Kategoriefilter Styling */ +.category-filter .btn { + border-radius: 20px; + margin: 0 5px; + padding: 5px 15px; +} + +/* Featured Products Section */ +.featured-section { + background: linear-gradient(to right, #f8f9fa, #e9ecef); + padding: 30px 0; + border-radius: 15px; + margin-bottom: 30px; +} + +.featured-badge { + background: #dc3545; + color: white; + padding: 5px 10px; + border-radius: 20px; + font-size: 0.8rem; +} + +/* Buttons */ +.btn-primary { + background: linear-gradient(135deg, #8B5CF6, #EC4899); + border: none; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #EC4899, #8B5CF6); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); +} + +.btn-outline-danger { + border-color: #EC4899; + color: #EC4899; +} + +.btn-outline-danger:hover { + background-color: #EC4899; + border-color: #EC4899; + color: white; +} + +/* Pagination */ +.pagination .page-link { + color: #8B5CF6; + border-color: #E5E7EB; +} + +.pagination .page-item.active .page-link { + background-color: #8B5CF6; + border-color: #8B5CF6; + color: white; +} + +.pagination .page-link:hover { + background-color: #F3E8FF; + border-color: #8B5CF6; + color: #8B5CF6; +} + +/* Responsive Anpassungen */ +@media (max-width: 768px) { + .product-card .card-img-top { + height: 200px; + } + + .filter-section { + padding: 1rem; + } + + .sort-options { + margin-top: 15px; + } + + .category-filter { + overflow-x: auto; + white-space: nowrap; + padding-bottom: 10px; + } +} \ No newline at end of file diff --git a/static/images/custom-order.jpg b/static/images/custom-order.jpg new file mode 100644 index 0000000..35ff747 --- /dev/null +++ b/static/images/custom-order.jpg @@ -0,0 +1 @@ +https://via.placeholder.com/600x400?text=Custom+Fursuit+Creation \ No newline at end of file diff --git a/static/images/dog-user-icon.svg b/static/images/dog-user-icon.svg new file mode 100644 index 0000000..d024aed --- /dev/null +++ b/static/images/dog-user-icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/hero-fursuit.jpg b/static/images/hero-fursuit.jpg new file mode 100644 index 0000000..acb83bc --- /dev/null +++ b/static/images/hero-fursuit.jpg @@ -0,0 +1 @@ +https://via.placeholder.com/1200x400?text=Fursuit+Showcase \ No newline at end of file diff --git a/static/images/kasico-logo-simple.svg b/static/images/kasico-logo-simple.svg new file mode 100644 index 0000000..ffe427a --- /dev/null +++ b/static/images/kasico-logo-simple.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/kasico-logo.svg b/static/images/kasico-logo.svg new file mode 100644 index 0000000..ad92568 --- /dev/null +++ b/static/images/kasico-logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KASICO + + + ART & DESIGN + + \ No newline at end of file diff --git a/static/images/kasicoLogo.png b/static/images/kasicoLogo.png new file mode 100644 index 0000000..d23a159 Binary files /dev/null and b/static/images/kasicoLogo.png differ diff --git a/static/images/kofi-cup.png b/static/images/kofi-cup.png new file mode 100644 index 0000000..e1a6a94 --- /dev/null +++ b/static/images/kofi-cup.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..24df00a --- /dev/null +++ b/templates/base.html @@ -0,0 +1,231 @@ + +{% load i18n %} +{% load static %} + + + + + {% block title %}Kasico Art & Design - Fursuit Shop{% endblock %} + + + + + + + + + + + {% block extra_css %}{% endblock %} + + + + + + + + +
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + +
+
+
+
+
Über uns
+

Kasico Art & Design - Ihr Partner für hochwertige Fursuits und Custom Designs.

+
+
+
Links
+ +
+
+
Kontakt
+
    +
  • info@kasico-art.com
  • +
  • +49 (0) 123 456789
  • +
  • Berlin, Deutschland
  • +
+
+
+
+
+

© {% now "Y" %} Kasico Art & Design. Alle Rechte vorbehalten.

+
+
+
+ + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/webshop/__init__.py b/webshop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webshop/asgi.py b/webshop/asgi.py new file mode 100644 index 0000000..5d58a25 --- /dev/null +++ b/webshop/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for webshop project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings') + +application = get_asgi_application() diff --git a/webshop/settings.py b/webshop/settings.py new file mode 100644 index 0000000..53fa8d8 --- /dev/null +++ b/webshop/settings.py @@ -0,0 +1,204 @@ +""" +Django settings for webshop project. + +Generated by 'django-admin startproject' using Django 5.2.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +import os +from dotenv import load_dotenv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Lade .env Datei +load_dotenv(BASE_DIR / '.env') + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-qddfdhpsm$=%o8p74xo8q9wbsa5^818(dzl4f&yrdcyn=050dt') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.getenv('DEBUG', 'True') == 'True' + +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'shop.apps.ShopConfig', + 'products.apps.ProductsConfig', + 'paypal_integration', + 'paypal.standard.ipn', + 'payments', +] + +# Entferne den zusätzlichen staticfiles-Block +if DEBUG: + INSTALLED_APPS += [] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'webshop.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.template.context_processors.static', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'webshop.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'de' +LANGUAGES = [ + ('de', 'Deutsch'), + ('en', 'English'), +] + +TIME_ZONE = 'Europe/Berlin' +USE_I18N = True +USE_L10N = True +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] + +# Media files (Uploads) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Stellen Sie sicher, dass der media-Ordner existiert +if not os.path.exists(MEDIA_ROOT): + os.makedirs(MEDIA_ROOT) + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Stripe Einstellungen +STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '') +STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') +STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET', '') + +# E-Mail-Einstellungen (temporär Console-Backend) +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'Fursuit Shop ' + +# Admin-E-Mail-Empfänger +ADMINS = [ + ('Shop Admin', 'admin@fursuitshop.com'), +] + +# Lagerbestand-Einstellungen +LOW_STOCK_THRESHOLD = 5 # Schwellenwert für niedrigen Lagerbestand + +# Authentication Settings +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'products:product_list' +LOGOUT_REDIRECT_URL = 'shop:home' + +SITE_URL = os.getenv('SITE_URL', 'http://127.0.0.1:8000') + +# PayPal Einstellungen +PAYPAL_TEST = True +PAYPAL_RECEIVER_EMAIL = 'sb-43wjt28371773@business.example.com' +PAYPAL_CURRENCY_CODE = 'EUR' + +# PayPal URLs +PAYPAL_BN = 'Fursuit_Shop' +PAYPAL_RETURN_URL = f'{SITE_URL}/products/payment/success/' +PAYPAL_CANCEL_URL = f'{SITE_URL}/products/payment/failed/' +PAYPAL_NOTIFY_URL = f'{SITE_URL}/paypal/' + +# Sites Framework +SITE_ID = 1 + +# Payment settings +PAYMENT_HOST = SITE_URL +PAYMENT_USES_SSL = False # Set to True in production +PAYMENT_MODEL = 'products.Payment' +PAYMENT_VARIANTS = { + 'default': ('payments.dummy.DummyProvider', {}), + 'stripe': ('payments.stripe.StripeProvider', { + 'secret_key': STRIPE_SECRET_KEY, + 'public_key': STRIPE_PUBLISHABLE_KEY + }) +} + diff --git a/webshop/urls.py b/webshop/urls.py new file mode 100644 index 0000000..18d580a --- /dev/null +++ b/webshop/urls.py @@ -0,0 +1,62 @@ +""" +URL configuration for webshop project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from django.contrib.auth import views as auth_views +from shop import views as shop_views +from products.forms import CustomAuthenticationForm + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('shop.urls')), + path('products/', include('products.urls')), + path('paypal/', include('paypal.standard.ipn.urls')), + path('register/', shop_views.register, name='register'), + path('login/', auth_views.LoginView.as_view( + template_name='shop/login.html', + authentication_form=CustomAuthenticationForm + ), name='login'), + path('logout/', auth_views.LogoutView.as_view(next_page='shop:home'), name='logout'), + path('password_change/', auth_views.PasswordChangeView.as_view( + template_name='products/password_change.html', + success_url='/password_change/done/' + ), name='password_change'), + path('password_change/done/', auth_views.PasswordChangeDoneView.as_view( + template_name='products/password_change_done.html' + ), name='password_change_done'), + path('password_reset/', auth_views.PasswordResetView.as_view( + template_name='shop/password_reset.html', + email_template_name='shop/password_reset_email.html', + subject_template_name='shop/password_reset_subject.txt' + ), name='password_reset'), + path('password_reset/done/', auth_views.PasswordResetDoneView.as_view( + template_name='shop/password_reset_done.html' + ), name='password_reset_done'), + path('reset///', auth_views.PasswordResetConfirmView.as_view( + template_name='shop/password_reset_confirm.html' + ), name='password_reset_confirm'), + path('reset/done/', auth_views.PasswordResetCompleteView.as_view( + template_name='shop/password_reset_complete.html' + ), name='password_reset_complete'), + path('i18n/', include('django.conf.urls.i18n')), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/webshop/wsgi.py b/webshop/wsgi.py new file mode 100644 index 0000000..d66fefa --- /dev/null +++ b/webshop/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for webshop project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings') + +application = get_wsgi_application()