Refactor frontend components and backend migrations
- Removed TradingGraph component from frontend/src/components/trading. - Updated home page to import Services component and TradingGraph from new path. - Modified PortfolioPage to return null instead of PortfolioGrid. - Added initial migrations for account, advertisement, commerce, configuration, downloader, gopay, stripe, trading212, and zasilkovna apps in the backend. - Created Services component with subcomponents for Kinematografie, Drone Service, and Website Service. - Implemented TradingGraph component with dynamic data generation and canvas rendering. - Updated DonationShop component to display donation tiers with icons and descriptions.
This commit is contained in:
@@ -1,14 +1,82 @@
|
||||
from django.contrib import admin
|
||||
from .models import Carrier, Product
|
||||
# Register your models here.
|
||||
from .models import (
|
||||
Category, Product, ProductImage, Order, OrderItem,
|
||||
Carrier, Payment, DiscountCode, Refund, Invoice
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Carrier)
|
||||
class CarrierAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "base_price", "is_active")
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "url", "parent")
|
||||
search_fields = ("name", "description")
|
||||
prepopulated_fields = {"url": ("name",)}
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "price", "currency", "stock", "is_active")
|
||||
search_fields = ("name", "description")
|
||||
list_display = ("name", "price", "stock", "is_active", "category", "created_at")
|
||||
search_fields = ("name", "description", "code")
|
||||
list_filter = ("is_active", "category", "created_at")
|
||||
prepopulated_fields = {"url": ("name",)}
|
||||
|
||||
|
||||
@admin.register(ProductImage)
|
||||
class ProductImageAdmin(admin.ModelAdmin):
|
||||
list_display = ("product", "is_main", "alt_text")
|
||||
list_filter = ("is_main",)
|
||||
search_fields = ("product__name", "alt_text")
|
||||
|
||||
|
||||
class OrderItemInline(admin.TabularInline):
|
||||
model = OrderItem
|
||||
extra = 0
|
||||
readonly_fields = ("product", "quantity")
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "user", "email", "status", "total_price", "currency", "created_at")
|
||||
list_filter = ("status", "created_at", "country")
|
||||
search_fields = ("email", "first_name", "last_name", "phone")
|
||||
readonly_fields = ("created_at", "updated_at", "total_price")
|
||||
inlines = [OrderItemInline]
|
||||
|
||||
|
||||
@admin.register(OrderItem)
|
||||
class OrderItemAdmin(admin.ModelAdmin):
|
||||
list_display = ("order", "product", "quantity")
|
||||
search_fields = ("order__id", "product__name")
|
||||
|
||||
|
||||
@admin.register(Carrier)
|
||||
class CarrierAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "shipping_method", "state", "shipping_price", "weight")
|
||||
list_filter = ("shipping_method", "state", "returning")
|
||||
search_fields = ("id",)
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "payment_method", "created_at")
|
||||
list_filter = ("payment_method", "created_at")
|
||||
|
||||
|
||||
@admin.register(DiscountCode)
|
||||
class DiscountCodeAdmin(admin.ModelAdmin):
|
||||
list_display = ("code", "percent", "amount", "active", "valid_from", "valid_to", "used_count", "usage_limit")
|
||||
list_filter = ("active", "valid_from", "valid_to")
|
||||
search_fields = ("code", "description")
|
||||
|
||||
|
||||
@admin.register(Refund)
|
||||
class RefundAdmin(admin.ModelAdmin):
|
||||
list_display = ("order", "reason_choice", "verified", "created_at")
|
||||
list_filter = ("verified", "reason_choice", "created_at")
|
||||
search_fields = ("order__id", "order__email", "reason_text")
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(admin.ModelAdmin):
|
||||
list_display = ("invoice_number", "issued_at", "due_date")
|
||||
search_fields = ("invoice_number",)
|
||||
readonly_fields = ("issued_at",)
|
||||
|
||||
162
backend/commerce/migrations/0001_initial.py
Normal file
162
backend/commerce/migrations/0001_initial.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-14 02:23
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('stripe', '0001_initial'),
|
||||
('zasilkovna', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Invoice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('invoice_number', models.CharField(max_length=50, unique=True)),
|
||||
('issued_at', models.DateTimeField(auto_now_add=True)),
|
||||
('due_date', models.DateTimeField()),
|
||||
('pdf_file', models.FileField(upload_to='invoices/')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Carrier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('shipping_method', models.CharField(choices=[('packeta', 'cz#Zásilkovna'), ('store', 'cz#Osobní odběr')], default='store', max_length=20)),
|
||||
('state', models.CharField(choices=[('ordered', 'cz#Objednávka se připravuje'), ('shipped', 'cz#Odesláno'), ('delivered', 'cz#Doručeno'), ('ready_to_pickup', 'cz#Připraveno k vyzvednutí')], default='ordered', max_length=20)),
|
||||
('weight', models.DecimalField(blank=True, decimal_places=2, help_text='Hmotnost zásilky v kg', max_digits=10, null=True)),
|
||||
('returning', models.BooleanField(default=False, help_text='Zda je tato zásilka na vrácení')),
|
||||
('shipping_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('zasilkovna', models.ManyToManyField(blank=True, related_name='carriers', to='zasilkovna.zasilkovnapacket')),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('url', models.SlugField(unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('image', models.ImageField(blank=True, upload_to='categories/')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='commerce.category')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DiscountCode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=255)),
|
||||
('percent', models.PositiveIntegerField(blank=True, help_text='Procento sleva 0-100', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
|
||||
('amount', models.DecimalField(blank=True, decimal_places=2, help_text='Fixní sleva v CZK', max_digits=10, null=True)),
|
||||
('valid_from', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('valid_to', models.DateTimeField(blank=True, null=True)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('usage_limit', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('used_count', models.PositiveIntegerField(default=0)),
|
||||
('specific_categories', models.ManyToManyField(blank=True, related_name='discount_codes', to='commerce.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('payment_method', models.CharField(choices=[('Site', 'cz#Platba v obchodě'), ('stripe', 'cz#Bankovní převod'), ('cash_on_delivery', 'cz#Dobírka')], default='Site', max_length=30)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('stripe', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='stripe.stripemodel')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(blank=True, choices=[('created', 'cz#Vytvořeno'), ('cancelled', 'cz#Zrušeno'), ('completed', 'cz#Dokončeno'), ('refunding', 'cz#Vrácení v procesu'), ('refunded', 'cz#Vráceno')], default='created', max_length=20, null=True)),
|
||||
('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('currency', models.CharField(default='CZK', max_length=10)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('address', models.CharField(max_length=255)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('postal_code', models.CharField(max_length=20)),
|
||||
('country', models.CharField(default='Czech Republic', max_length=100)),
|
||||
('note', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('carrier', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='commerce.carrier')),
|
||||
('discount', models.ManyToManyField(blank=True, related_name='orders', to='commerce.discountcode')),
|
||||
('invoice', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='commerce.invoice')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='orders', to=settings.AUTH_USER_MODEL)),
|
||||
('payment', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='commerce.payment')),
|
||||
],
|
||||
),
|
||||
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(blank=True)),
|
||||
('code', models.CharField(blank=True, max_length=100, null=True, unique=True)),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('url', models.SlugField(unique=True)),
|
||||
('stock', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('limited_to', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='commerce.category')),
|
||||
('default_carrier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_products', to='commerce.carrier')),
|
||||
('variants', models.ManyToManyField(blank=True, help_text='Symetrické varianty produktu: pokud přidáte variantu A → B, Django automaticky přidá i variantu B → A. Všechny varianty jsou rovnocenné a zobrazí se vzájemně.', related_name='variant_of', to='commerce.product')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(default=1)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='commerce.order')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='commerce.product')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='discountcode',
|
||||
name='specific_products',
|
||||
field=models.ManyToManyField(blank=True, related_name='discount_codes', to='commerce.product'),
|
||||
),
|
||||
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/')),
|
||||
('alt_text', models.CharField(blank=True, max_length=150)),
|
||||
('is_main', models.BooleanField(default=False)),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='commerce.product')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Refund',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reason_choice', models.CharField(choices=[('retuning_before_fourteen_day_period', 'cz#Vrácení před uplynutím 14-ti denní lhůty'), ('damaged_product', 'cz#Poškozený produkt'), ('wrong_item', 'cz#Špatná položka'), ('other', 'cz#Jiný důvod')], max_length=40)),
|
||||
('reason_text', models.TextField(blank=True)),
|
||||
('verified', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='refunds', to='commerce.order')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/commerce/migrations/__init__.py
Normal file
0
backend/commerce/migrations/__init__.py
Normal file
@@ -15,7 +15,7 @@ from configuration.models import SiteConfiguration
|
||||
from thirdparty.zasilkovna.models import ZasilkovnaPacket
|
||||
from thirdparty.stripe.models import StripeModel
|
||||
|
||||
from .tasks import notify_refund_accepted, notify_order_sended
|
||||
from .tasks import notify_refund_accepted, notify_Ready_to_pickup, notify_zasilkovna_sended
|
||||
|
||||
#FIXME: přidat soft delete pro všchny modely !!!!
|
||||
|
||||
@@ -253,18 +253,20 @@ class Carrier(models.Model):
|
||||
self.returning = False
|
||||
self.save()
|
||||
|
||||
notify_zasilkovna_sended.delay(order=self.orders.first(), user=self.orders.first().user)
|
||||
|
||||
elif self.shipping_method == self.SHIPPING.STORE:
|
||||
self.state = self.STATE.READY_TO_PICKUP
|
||||
self.save()
|
||||
|
||||
notify_Ready_to_pickup.delay(order=self.orders.first(), user=self.orders.first().user)
|
||||
|
||||
else:
|
||||
raise ValidationError("Tato metoda dopravy nepodporuje objednání přepravy.")
|
||||
|
||||
|
||||
notify_order_sended.delay(order=self.orders.first(), user=self.orders.first().user)
|
||||
|
||||
#... další logika pro jiné způsoby dopravy
|
||||
#TODO: přidat notifikace uživateli, jak pro zásilkovnu, tak pro vyzvednutí v obchodě!
|
||||
#... další logika pro jiné způsoby dopravy (do budoucna!)
|
||||
|
||||
|
||||
def ready_to_pickup(self):
|
||||
if self.shipping_method == self.SHIPPING.STORE:
|
||||
@@ -354,7 +356,7 @@ class DiscountCode(models.Model):
|
||||
|
||||
class OrderItem(models.Model):
|
||||
order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE)
|
||||
product = models.ForeignKey("products.Product", on_delete=models.PROTECT)
|
||||
product = models.ForeignKey("commerce.Product", on_delete=models.PROTECT)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
def get_total_price(self, discounts: list[DiscountCode] = None):
|
||||
@@ -465,7 +467,7 @@ class Refund(models.Model):
|
||||
DAMAGED_PRODUCT = "damaged_product", "cz#Poškozený produkt"
|
||||
WRONG_ITEM = "wrong_item", "cz#Špatná položka"
|
||||
OTHER = "other", "cz#Jiný důvod"
|
||||
reason_choice = models.CharField(max_length=30, choices=Reason.choices)
|
||||
reason_choice = models.CharField(max_length=40, choices=Reason.choices)
|
||||
|
||||
reason_text = models.TextField(blank=True)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from backend.thirdparty.stripe.client import StripeClient
|
||||
from thirdparty.stripe.client import StripeClient
|
||||
|
||||
from .models import Refund, Order, Invoice
|
||||
|
||||
@@ -87,7 +87,7 @@ from .models import (
|
||||
Payment,
|
||||
)
|
||||
|
||||
from thirdparty.stripe.models import StripeModel, StripePayment
|
||||
from thirdparty.stripe.models import StripeModel
|
||||
|
||||
from thirdparty.zasilkovna.serializers import ZasilkovnaPacketSerializer
|
||||
from thirdparty.zasilkovna.models import ZasilkovnaPacket
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from account.models import User
|
||||
from account.tasks import send_email_with_context
|
||||
from celery import shared_task
|
||||
|
||||
from django.apps import apps
|
||||
from django.utils import timezone
|
||||
|
||||
Order = apps.get_model('commerce', 'Order')
|
||||
|
||||
|
||||
def delete_expired_orders():
|
||||
Order = apps.get_model('commerce', 'Order')
|
||||
|
||||
expired_orders = Order.objects.filter(status=Order.STATUS_CHOICES.CANCELLED, created_at__lt=timezone.now() - timezone.timedelta(hours=24))
|
||||
count = expired_orders.count()
|
||||
expired_orders.delete()
|
||||
@@ -19,7 +18,7 @@ def delete_expired_orders():
|
||||
|
||||
# Zásilkovna
|
||||
@shared_task
|
||||
def notify_zasilkovna_sended(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_zasilkovna_sended(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
@@ -38,9 +37,10 @@ def notify_zasilkovna_sended(order:Order = None, user:User = None, **kwargs):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Shop
|
||||
@shared_task
|
||||
def notify_Ready_to_pickup(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_Ready_to_pickup(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
@@ -63,7 +63,7 @@ def notify_Ready_to_pickup(order:Order = None, user:User = None, **kwargs):
|
||||
# -- NOTIFICATIONS ORDER --
|
||||
|
||||
@shared_task
|
||||
def notify_order_successfuly_created(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_order_successfuly_created(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
@@ -84,7 +84,7 @@ def notify_order_successfuly_created(order:Order = None, user:User = None, **kwa
|
||||
|
||||
|
||||
@shared_task
|
||||
def notify_about_missing_payment(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_about_missing_payment(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
@@ -104,7 +104,7 @@ def notify_about_missing_payment(order:Order = None, user:User = None, **kwargs)
|
||||
pass
|
||||
|
||||
|
||||
def notify_refund_items_arrived(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_refund_items_arrived(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
@@ -126,7 +126,7 @@ def notify_refund_items_arrived(order:Order = None, user:User = None, **kwargs):
|
||||
|
||||
# Refund accepted, retuning money
|
||||
@shared_task
|
||||
def notify_refund_accepted(order:Order = None, user:User = None, **kwargs):
|
||||
def notify_refund_accepted(order = None, user = None, **kwargs):
|
||||
if not order or not user:
|
||||
raise ValueError("Order and User must be provided for notification.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user