From 2d088f693d4e359ddddc71ab644b8bf1c6944c95 Mon Sep 17 00:00:00 2001 From: tabea k Date: Fri, 4 Jul 2025 18:25:02 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20Django=20Shop=20ohne=20gro?= =?UTF-8?q?=C3=9Fe=20Dateien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 156 ++++ django_shop/settings.py | 21 + docs/email_system.md | 155 ++++ init_data.py | 31 + init_db.py | 43 + manage.py | 22 + paypal_integration/admin.py | 16 + paypal_integration/models.py | 19 + paypal_integration/urls.py | 10 + paypal_integration/views.py | 87 ++ products/__init__.py | 0 products/admin.py | 90 ++ products/apps.py | 6 + products/forms.py | 219 +++++ products/migrations/0001_initial.py | 29 + products/migrations/0002_cart_cartitem.py | 39 + ...product_featured_product_image_and_more.py | 60 ++ products/migrations/0004_review.py | 33 + .../migrations/0005_userprofile_wishlist.py | 38 + .../migrations/0006_faq_contactmessage.py | 54 ++ ...scription_product_fursuit_type_and_more.py | 87 ++ ...8_alter_contactmessage_options_and_more.py | 145 ++++ .../0009_remove_product_category_and_more.py | 147 ++++ products/migrations/0010_galleryimage.py | 32 + ...lter_galleryimage_fursuit_type_and_more.py | 23 + .../0012_category_product_category.py | 35 + ..._alter_product_category_delete_category.py | 23 + products/migrations/0014_payment.py | 52 ++ ...s_remove_payment_billing_phone_and_more.py | 112 +++ products/migrations/__init__.py | 0 products/models.py | 416 +++++++++ products/serializers.py | 20 + products/templates/base.html | 294 +++++++ products/templates/products/add_progress.html | 99 +++ products/templates/products/cart_detail.html | 85 ++ products/templates/products/checkout.html | 90 ++ products/templates/products/contact.html | 135 +++ .../templates/products/contact_success.html | 33 + products/templates/products/custom_order.html | 248 ++++++ .../products/custom_order_detail.html | 179 ++++ .../products/custom_order_success.html | 48 ++ products/templates/products/dashboard.html | 370 ++++++++ products/templates/products/faq.html | 77 ++ products/templates/products/gallery.html | 188 ++++ .../products/order_confirmation.html | 59 ++ .../templates/products/order_history.html | 123 +++ .../templates/products/payment_button.html | 24 + .../templates/products/payment_failed.html | 53 ++ .../templates/products/payment_process.html | 31 + .../templates/products/payment_success.html | 47 + .../templates/products/product_detail.html | 338 ++++++++ products/templates/products/product_list.html | 253 ++++++ products/templates/products/profile.html | 64 ++ products/templates/products/wishlist.html | 91 ++ products/templates/registration/register.html | 43 + products/tests.py | 3 + products/urls.py | 47 + products/views.py | 808 ++++++++++++++++++ shop/__init__.py | 0 shop/admin.py | 123 +++ shop/apps.py | 14 + shop/asgi.py | 16 + shop/emails.py | 206 +++++ shop/forms.py | 23 + shop/management/commands/test_email.py | 110 +++ shop/migrations/0001_initial.py | 138 +++ ...ucttype_alter_category_options_and_more.py | 277 ++++++ shop/migrations/0003_contactmessage.py | 29 + shop/migrations/__init__.py | 0 shop/models.py | 365 ++++++++ shop/settings.py | 143 ++++ shop/signals.py | 69 ++ shop/templates/shop/base.html | 497 +++++++++++ shop/templates/shop/cart.html | 278 ++++++ shop/templates/shop/checkout.html | 468 ++++++++++ shop/templates/shop/contact.html | 128 +++ .../shop/emails/admin_notification.html | 212 +++++ .../shop/emails/admin_notification.txt | 51 ++ .../shop/emails/low_stock_notification.html | 118 +++ .../shop/emails/low_stock_notification.txt | 15 + .../shop/emails/order_confirmation.html | 182 ++++ .../shop/emails/order_confirmation.txt | 38 + .../shop/emails/order_status_update.html | 129 +++ .../shop/emails/order_status_update.txt | 32 + .../shop/emails/shipping_confirmation.html | 167 ++++ .../shop/emails/shipping_confirmation.txt | 28 + shop/templates/shop/gallery.html | 233 +++++ shop/templates/shop/gallery_detail.html | 228 +++++ shop/templates/shop/gallery_list.html | 194 +++++ shop/templates/shop/home.html | 160 ++++ shop/templates/shop/login.html | 153 ++++ shop/templates/shop/my_orders.html | 208 +++++ shop/templates/shop/order_success.html | 129 +++ shop/templates/shop/password_reset.html | 114 +++ .../shop/password_reset_complete.html | 82 ++ .../shop/password_reset_confirm.html | 142 +++ shop/templates/shop/password_reset_done.html | 84 ++ shop/templates/shop/password_reset_email.html | 15 + .../templates/shop/password_reset_subject.txt | 3 + shop/templates/shop/payment_failed.html | 41 + shop/templates/shop/product_detail.html | 338 ++++++++ shop/templates/shop/product_list.html | 203 +++++ shop/templates/shop/register.html | 142 +++ shop/tests.py | 3 + shop/urls.py | 36 + shop/views.py | 47 + shop/wsgi.py | 16 + static/css/dashboard.css | 49 ++ static/css/products.css | 192 +++++ static/images/custom-order.jpg | 1 + static/images/dog-user-icon.svg | 25 + static/images/hero-fursuit.jpg | 1 + static/images/kasico-logo-simple.svg | 22 + static/images/kasico-logo.svg | 54 ++ static/images/kasicoLogo.png | Bin 0 -> 80949 bytes static/images/kofi-cup.png | 1 + templates/base.html | 231 +++++ webshop/__init__.py | 0 webshop/asgi.py | 16 + webshop/settings.py | 204 +++++ webshop/urls.py | 62 ++ webshop/wsgi.py | 16 + 122 files changed, 13351 insertions(+) create mode 100644 .gitignore create mode 100644 django_shop/settings.py create mode 100644 docs/email_system.md create mode 100644 init_data.py create mode 100644 init_db.py create mode 100644 manage.py create mode 100644 paypal_integration/admin.py create mode 100644 paypal_integration/models.py create mode 100644 paypal_integration/urls.py create mode 100644 paypal_integration/views.py create mode 100644 products/__init__.py create mode 100644 products/admin.py create mode 100644 products/apps.py create mode 100644 products/forms.py create mode 100644 products/migrations/0001_initial.py create mode 100644 products/migrations/0002_cart_cartitem.py create mode 100644 products/migrations/0003_product_category_product_featured_product_image_and_more.py create mode 100644 products/migrations/0004_review.py create mode 100644 products/migrations/0005_userprofile_wishlist.py create mode 100644 products/migrations/0006_faq_contactmessage.py create mode 100644 products/migrations/0007_product_extras_description_product_fursuit_type_and_more.py create mode 100644 products/migrations/0008_alter_contactmessage_options_and_more.py create mode 100644 products/migrations/0009_remove_product_category_and_more.py create mode 100644 products/migrations/0010_galleryimage.py create mode 100644 products/migrations/0011_alter_galleryimage_fursuit_type_and_more.py create mode 100644 products/migrations/0012_category_product_category.py create mode 100644 products/migrations/0013_alter_product_category_delete_category.py create mode 100644 products/migrations/0014_payment.py create mode 100644 products/migrations/0015_alter_payment_options_remove_payment_billing_phone_and_more.py create mode 100644 products/migrations/__init__.py create mode 100644 products/models.py create mode 100644 products/serializers.py create mode 100644 products/templates/base.html create mode 100644 products/templates/products/add_progress.html create mode 100644 products/templates/products/cart_detail.html create mode 100644 products/templates/products/checkout.html create mode 100644 products/templates/products/contact.html create mode 100644 products/templates/products/contact_success.html create mode 100644 products/templates/products/custom_order.html create mode 100644 products/templates/products/custom_order_detail.html create mode 100644 products/templates/products/custom_order_success.html create mode 100644 products/templates/products/dashboard.html create mode 100644 products/templates/products/faq.html create mode 100644 products/templates/products/gallery.html create mode 100644 products/templates/products/order_confirmation.html create mode 100644 products/templates/products/order_history.html create mode 100644 products/templates/products/payment_button.html create mode 100644 products/templates/products/payment_failed.html create mode 100644 products/templates/products/payment_process.html create mode 100644 products/templates/products/payment_success.html create mode 100644 products/templates/products/product_detail.html create mode 100644 products/templates/products/product_list.html create mode 100644 products/templates/products/profile.html create mode 100644 products/templates/products/wishlist.html create mode 100644 products/templates/registration/register.html create mode 100644 products/tests.py create mode 100644 products/urls.py create mode 100644 products/views.py create mode 100644 shop/__init__.py create mode 100644 shop/admin.py create mode 100644 shop/apps.py create mode 100644 shop/asgi.py create mode 100644 shop/emails.py create mode 100644 shop/forms.py create mode 100644 shop/management/commands/test_email.py create mode 100644 shop/migrations/0001_initial.py create mode 100644 shop/migrations/0002_producttype_alter_category_options_and_more.py create mode 100644 shop/migrations/0003_contactmessage.py create mode 100644 shop/migrations/__init__.py create mode 100644 shop/models.py create mode 100644 shop/settings.py create mode 100644 shop/signals.py create mode 100644 shop/templates/shop/base.html create mode 100644 shop/templates/shop/cart.html create mode 100644 shop/templates/shop/checkout.html create mode 100644 shop/templates/shop/contact.html create mode 100644 shop/templates/shop/emails/admin_notification.html create mode 100644 shop/templates/shop/emails/admin_notification.txt create mode 100644 shop/templates/shop/emails/low_stock_notification.html create mode 100644 shop/templates/shop/emails/low_stock_notification.txt create mode 100644 shop/templates/shop/emails/order_confirmation.html create mode 100644 shop/templates/shop/emails/order_confirmation.txt create mode 100644 shop/templates/shop/emails/order_status_update.html create mode 100644 shop/templates/shop/emails/order_status_update.txt create mode 100644 shop/templates/shop/emails/shipping_confirmation.html create mode 100644 shop/templates/shop/emails/shipping_confirmation.txt create mode 100644 shop/templates/shop/gallery.html create mode 100644 shop/templates/shop/gallery_detail.html create mode 100644 shop/templates/shop/gallery_list.html create mode 100644 shop/templates/shop/home.html create mode 100644 shop/templates/shop/login.html create mode 100644 shop/templates/shop/my_orders.html create mode 100644 shop/templates/shop/order_success.html create mode 100644 shop/templates/shop/password_reset.html create mode 100644 shop/templates/shop/password_reset_complete.html create mode 100644 shop/templates/shop/password_reset_confirm.html create mode 100644 shop/templates/shop/password_reset_done.html create mode 100644 shop/templates/shop/password_reset_email.html create mode 100644 shop/templates/shop/password_reset_subject.txt create mode 100644 shop/templates/shop/payment_failed.html create mode 100644 shop/templates/shop/product_detail.html create mode 100644 shop/templates/shop/product_list.html create mode 100644 shop/templates/shop/register.html create mode 100644 shop/tests.py create mode 100644 shop/urls.py create mode 100644 shop/views.py create mode 100644 shop/wsgi.py create mode 100644 static/css/dashboard.css create mode 100644 static/css/products.css create mode 100644 static/images/custom-order.jpg create mode 100644 static/images/dog-user-icon.svg create mode 100644 static/images/hero-fursuit.jpg create mode 100644 static/images/kasico-logo-simple.svg create mode 100644 static/images/kasico-logo.svg create mode 100644 static/images/kasicoLogo.png create mode 100644 static/images/kofi-cup.png create mode 100644 templates/base.html create mode 100644 webshop/__init__.py create mode 100644 webshop/asgi.py create mode 100644 webshop/settings.py create mode 100644 webshop/urls.py create mode 100644 webshop/wsgi.py 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 0000000000000000000000000000000000000000..d23a159ab755b9da6b74cf08482da2971c3ce2ff GIT binary patch literal 80949 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 z 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 \ 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()