diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..cf8b83e 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..ecb1721 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..a3a8d0a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -2,21 +2,42 @@ Django settings for config project. Generated by 'django-admin startproject' using Django 5.2.7. - -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 +from django.utils.translation import gettext_lazy as _ +import django.conf.locale +import django.utils.translation BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(BASE_DIR.parent / ".env") +# Oromo and Amharic are not in Django's default LANG_INFO +EXTRA_LANG_INFO = { + 'om': { + 'bidi': False, + 'code': 'om', + 'name': 'Oromo', + 'name_local': 'Afaan Oromoo', + }, + 'am': { + 'bidi': False, + 'code': 'am', + 'name': 'Amharic', + 'name_local': 'አማርኛ', + }, +} + +# Add custom languages to LANG_INFO +for code, info in EXTRA_LANG_INFO.items(): + if code not in django.conf.locale.LANG_INFO: + django.conf.locale.LANG_INFO[code] = info + # Also update the reference in django.utils.translation if it exists + if hasattr(django.utils.translation, 'LANG_INFO') and code not in django.utils.translation.LANG_INFO: + django.utils.translation.LANG_INFO[code] = info + SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me") DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" @@ -37,18 +58,17 @@ CSRF_TRUSTED_ORIGINS = [ for host in CSRF_TRUSTED_ORIGINS ] -# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy. SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" CSRF_COOKIE_SAMESITE = "None" -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ +X_FRAME_OPTIONS = 'ALLOWALL' # Application definition INSTALLED_APPS = [ + 'modeltranslation', # Must be before admin 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -61,16 +81,13 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -X_FRAME_OPTIONS = 'ALLOWALL' - ROOT_URLCONF = 'config.urls' TEMPLATES = [ @@ -83,7 +100,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - # IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp + 'django.template.context_processors.i18n', 'core.context_processors.project_context', ], }, @@ -92,10 +109,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'config.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -110,56 +123,46 @@ DATABASES = { }, } - -# 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', - }, + {'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 = 'en' -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' +TIME_ZONE = 'Africa/Addis_Ababa' USE_I18N = True - USE_TZ = True +LANGUAGES = [ + ('en', _('English')), + ('am', _('Amharic')), + ('om', _('Afaan Oromoo')), +] -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ +MODELTRANSLATION_DEFAULT_LANGUAGE = 'en' + +LOCALE_PATHS = [ + BASE_DIR / 'locale', +] STATIC_URL = 'static/' -# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS. STATIC_ROOT = BASE_DIR / 'staticfiles' - STATICFILES_DIRS = [ BASE_DIR / 'static', BASE_DIR / 'assets', BASE_DIR / 'node_modules', ] +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email -EMAIL_BACKEND = os.getenv( - "EMAIL_BACKEND", - "django.core.mail.backends.smtp.EmailBackend" -) +EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1") EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") @@ -167,16 +170,9 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "") EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true" EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true" DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com") -CONTACT_EMAIL_TO = [ - item.strip() - for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") - if item.strip() -] +CONTACT_EMAIL_TO = [item.strip() for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") if item.strip()] -# When both TLS and SSL flags are enabled, prefer SSL explicitly if EMAIL_USE_SSL: EMAIL_USE_TLS = False -# Default primary key field type -# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/config/urls.py b/config/urls.py index bcfc074..6c5d25d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,29 +1,18 @@ -""" -URL configuration for config 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 include, path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.conf.urls.i18n import i18n_patterns urlpatterns = [ - path("admin/", admin.site.urls), - path("", include("core.urls")), + path('i18n/', include('django.conf.urls.i18n')), ] +urlpatterns += i18n_patterns( + path('admin/', admin.site.py_urls if hasattr(admin.site, 'py_urls') else admin.site.urls), + path('', include('core.urls')), +) + if settings.DEBUG: - urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..ebd4fa1 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..e088272 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/translation.cpython-311.pyc b/core/__pycache__/translation.cpython-311.pyc new file mode 100644 index 0000000..8c38538 Binary files /dev/null and b/core/__pycache__/translation.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..175a82e 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..6c24bb9 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..9156386 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,33 @@ from django.contrib import admin +from modeltranslation.admin import TranslationAdmin +from .models import Profile, Category, Vendor, Product, Order, OrderItem -# Register your models here. +@admin.register(Category) +class CategoryAdmin(TranslationAdmin): + list_display = ('name', 'slug') + prepopulated_fields = {'slug': ('name',)} + +@admin.register(Vendor) +class VendorAdmin(TranslationAdmin): + list_display = ('business_name', 'user', 'is_verified') + list_filter = ('is_verified',) + search_fields = ('business_name', 'user__username') + +@admin.register(Product) +class ProductAdmin(TranslationAdmin): + list_display = ('name', 'vendor', 'category', 'price', 'stock', 'is_available') + list_filter = ('is_available', 'category', 'vendor') + search_fields = ('name', 'description') + prepopulated_fields = {'slug': ('name',)} + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'full_name', 'total_price', 'status', 'payment_method', 'created_at') + list_filter = ('status', 'payment_method', 'created_at') + inlines = [OrderItemInline] + +admin.site.register(Profile) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d115aea --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# Generated by Django 5.2.7 on 2026-02-04 18:28 + +import django.db.models.deletion +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=100)), + ('name_om', models.CharField(max_length=100, null=True)), + ('name_am', models.CharField(max_length=100, null=True)), + ('name_en', models.CharField(max_length=100, null=True)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField(blank=True)), + ('description_om', models.TextField(blank=True, null=True)), + ('description_am', models.TextField(blank=True, null=True)), + ('description_en', models.TextField(blank=True, null=True)), + ('image', models.ImageField(blank=True, null=True, upload_to='categories/')), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + 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=255)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(max_length=20)), + ('address', models.TextField()), + ('total_price', models.DecimalField(decimal_places=2, max_digits=10)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('delivered', 'Delivered'), ('cancelled', 'Cancelled')], default='pending', max_length=20)), + ('payment_method', models.CharField(choices=[('cod', 'Cash on Delivery'), ('telebirr', 'Telebirr'), ('cbe_birr', 'CBE Birr'), ('bank_transfer', 'Bank Transfer')], default='cod', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('name_om', models.CharField(max_length=255, null=True)), + ('name_am', models.CharField(max_length=255, null=True)), + ('name_en', models.CharField(max_length=255, null=True)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField()), + ('description_om', models.TextField(null=True)), + ('description_am', models.TextField(null=True)), + ('description_en', models.TextField(null=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('stock', models.IntegerField(default=0)), + ('image', models.ImageField(upload_to='products/')), + ('is_available', models.BooleanField(default=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.CASCADE, related_name='products', to='core.category')), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('quantity', models.PositiveIntegerField(default=1)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ], + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('buyer', 'Buyer'), ('seller', 'Seller'), ('admin', 'Admin')], default='buyer', max_length=10)), + ('phone', models.CharField(blank=True, max_length=20)), + ('address', models.TextField(blank=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Vendor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('business_name', models.CharField(max_length=200)), + ('business_name_om', models.CharField(max_length=200, null=True)), + ('business_name_am', models.CharField(max_length=200, null=True)), + ('business_name_en', models.CharField(max_length=200, null=True)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField(blank=True)), + ('description_om', models.TextField(blank=True, null=True)), + ('description_am', models.TextField(blank=True, null=True)), + ('description_en', models.TextField(blank=True, null=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='vendors/')), + ('address', models.TextField()), + ('phone', models.CharField(max_length=20)), + ('is_verified', models.BooleanField(default=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='product', + name='vendor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.vendor'), + ), + ] diff --git a/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py b/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py new file mode 100644 index 0000000..b53c3d1 --- /dev/null +++ b/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 5.2.7 on 2026-02-04 18:49 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='vendor', + name='logo', + ), + migrations.RemoveField( + model_name='vendor', + name='slug', + ), + migrations.AddField( + model_name='vendor', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='order', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='order', + name='email', + field=models.EmailField(blank=True, max_length=254), + ), + migrations.AlterField( + model_name='order', + name='payment_method', + field=models.CharField(max_length=50), + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Processing', 'Processing'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled')], default='Pending', max_length=20), + ), + migrations.AlterField( + model_name='order', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='product', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='product', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='products/'), + ), + migrations.AlterField( + model_name='profile', + name='role', + field=models.CharField(choices=[('customer', 'Customer'), ('seller', 'Seller'), ('admin', 'Admin')], default='customer', max_length=20), + ), + migrations.AlterField( + model_name='vendor', + name='address', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='vendor', + name='business_name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='vendor', + name='business_name_am', + field=models.CharField(max_length=255, null=True), + ), + migrations.AlterField( + model_name='vendor', + name='business_name_en', + field=models.CharField(max_length=255, null=True), + ), + migrations.AlterField( + model_name='vendor', + name='business_name_om', + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..b5a036d Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc new file mode 100644 index 0000000..b0308f9 Binary files /dev/null and b/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..2db87a3 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,102 @@ from django.db import models +from django.contrib.auth.models import User +from django.utils.text import slugify +from django.utils import timezone -# Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True, blank=True) + image = models.ImageField(upload_to='categories/', blank=True, null=True) + description = models.TextField(blank=True) + + class Meta: + verbose_name_plural = 'Categories' + + 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 Vendor(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + business_name = models.CharField(max_length=255) + description = models.TextField(blank=True) + address = models.CharField(max_length=255) + phone = models.CharField(max_length=20) + is_verified = models.BooleanField(default=False) + created_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return self.business_name + +class Product(models.Model): + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + vendor = models.ForeignKey(Vendor, related_name='products', on_delete=models.CASCADE) + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + image = models.ImageField(upload_to='products/', blank=True, null=True) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + stock = models.IntegerField(default=0) + is_available = models.BooleanField(default=True) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + + 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 Profile(models.Model): + ROLE_CHOICES = ( + ('customer', 'Customer'), + ('seller', 'Seller'), + ('admin', 'Admin'), + ) + user = models.OneToOneField(User, on_delete=models.CASCADE) + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='customer') + phone = models.CharField(max_length=20, blank=True) + address = models.TextField(blank=True) + + def __str__(self): + return f"{self.user.username} Profile" + +class Order(models.Model): + STATUS_CHOICES = ( + ('Pending', 'Pending'), + ('Processing', 'Processing'), + ('Shipped', 'Shipped'), + ('Delivered', 'Delivered'), + ('Cancelled', 'Cancelled'), + ) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + full_name = models.CharField(max_length=255) + email = models.EmailField(blank=True) + phone = models.CharField(max_length=20) + address = models.TextField() + total_price = models.DecimalField(max_digits=10, decimal_places=2) + payment_method = models.CharField(max_length=50) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') + created_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return f"Order {self.id} - {self.full_name}" + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + price = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField(default=1) + + def __str__(self): + return f"{self.quantity} x {self.product.name}" + + @property + def total_price(self): + return self.price * self.quantity \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..9c7a16f 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,169 @@ +{% load i18n static %} - +
-| {% trans "Product" %} | +{% trans "Price" %} | +{% trans "Quantity" %} | +{% trans "Subtotal" %} | ++ |
|---|---|---|---|---|
|
+
+ {% if item.product.image %}
+
+
+
+ {{ item.product.name }}+ {{ item.product.vendor.business_name }} + |
+ {{ item.product.price }} ETB | +{{ item.quantity }} | +{{ item.subtotal }} ETB | ++ + {% trans "Remove" %} + + | +
{% trans "Your cart is empty." %}
+ {% trans "Start Shopping" %} +{{ category.description }}
+ {% endif %} +{% trans "No products available in this category yet." %}
+{% trans "Discover amazing products from local vendors across Ethiopia." %}
+ +{% trans "No categories found." %}
+ {% endfor %} +{{ product.category.name }}
+{{ product.price }} ETB
+ + {% trans "Add to Cart" %} + +{% trans "No featured products available." %}
+AppWizzy AI is collecting your requirements and applying the first changes.
-This page will refresh automatically as the plan is implemented.
-
- Runtime: Django {{ django_version }} · Python {{ python_version }}
- — UTC {{ current_time|date:"Y-m-d H:i:s" }}
-
{% trans "Your order ID is" %} #{{ order.id }}. {% trans "We have received your request and will contact you shortly for delivery." %}
+ +{% trans "Name" %}: {{ order.full_name }}
+{% trans "Phone" %}: {{ order.phone }}
+{% trans "Address" %}: {{ order.address }}
+{% trans "Total" %}: {{ order.total_price }} ETB
+{% trans "Payment" %}: {{ order.get_payment_method_display }}
+{{ product.category.name }}
+{{ product.description }}
+{% trans "Located in" %}: {{ product.vendor.address }}
+{{ product.category.name }}
+{{ product.price }} ETB
+ + {% trans "Add to Cart" %} + +{% trans "No products found matching your search." %}
+{% trans "Manage your products and orders" %}
+| {% trans "Product" %} | +{% trans "Category" %} | +{% trans "Price" %} | +{% trans "Status" %} | +{% trans "Actions" %} | +
|---|---|---|---|---|
| {{ product.name }} | +{{ product.category.name }} | +{{ product.price }} ETB | ++ {% if product.is_available %} + {% trans "Available" %} + {% else %} + {% trans "Out of Stock" %} + {% endif %} + | ++ {% trans "Edit" %} + | +
| {% trans "No products found." %} | +||||
| {% trans "Order ID" %} | +{% trans "Product" %} | +{% trans "Customer" %} | +{% trans "Total" %} | +{% trans "Status" %} | +
|---|---|---|---|---|
| #{{ item.order.id }} | +{{ item.product.name }} | +{{ item.order.full_name }} | +{{ item.total_price }} ETB | +{{ item.order.status }} | +
| {% trans "No orders found." %} | +||||
{% trans "Join our community and start selling your products to thousands of customers." %}
+ + +