diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..88d5360 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..1b7a2c8 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..443b50a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -62,6 +62,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -85,6 +86,7 @@ TEMPLATES = [ 'django.contrib.messages.context_processors.messages', # IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp 'core.context_processors.project_context', + 'core.context_processors.global_settings', ], }, }, @@ -134,6 +136,12 @@ AUTH_PASSWORD_VALIDATORS = [ # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = 'en-us' +LANGUAGES = [ + ('en', 'English'), + ('ar', 'Arabic'), +] + +LOCALE_PATHS = [BASE_DIR / 'locale'] TIME_ZONE = 'UTC' @@ -148,6 +156,8 @@ USE_TZ = True STATIC_URL = 'static/' # Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS. STATIC_ROOT = BASE_DIR / 'staticfiles' +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' STATICFILES_DIRS = [ BASE_DIR / 'static', @@ -179,4 +189,4 @@ if EMAIL_USE_SSL: # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index bcfc074..5d88023 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,19 +1,3 @@ -""" -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.conf import settings @@ -21,9 +5,11 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("i18n/", include("django.conf.urls.i18n")), 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) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..5a870f5 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..7392a4c 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..783a3e3 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..4ecbf23 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..f1c90d1 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..5466d1b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,36 @@ from django.contrib import admin +from .models import Category, Product, Customer, Supplier, Sale, SaleItem, Purchase -# Register your models here. +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'slug') + prepopulated_fields = {'slug': ('name_en',)} + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category') + list_filter = ('category',) + search_fields = ('name_en', 'name_ar', 'sku') + +@admin.register(Customer) +class CustomerAdmin(admin.ModelAdmin): + list_display = ('name', 'phone', 'email') + search_fields = ('name', 'phone') + +@admin.register(Supplier) +class SupplierAdmin(admin.ModelAdmin): + list_display = ('name', 'contact_person', 'phone') + +class SaleItemInline(admin.TabularInline): + model = SaleItem + extra = 1 + +@admin.register(Sale) +class SaleAdmin(admin.ModelAdmin): + list_display = ('id', 'customer', 'total_amount', 'created_at') + inlines = [SaleItemInline] + +@admin.register(Purchase) +class PurchaseAdmin(admin.ModelAdmin): + list_display = ('id', 'supplier', 'total_amount', 'created_at') + list_filter = ('supplier', 'created_at') diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..2769ebb 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,13 +1,23 @@ +from .models import SystemSetting import os -import time +from django.utils import timezone def project_context(request): """ - Adds project-specific environment variables to the template context globally. + Injects project description and social image URL from environment variables. + Also injects a deployment timestamp for cache-busting. """ return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - # Used for cache-busting static assets - "deployment_timestamp": int(time.time()), + "deployment_timestamp": int(timezone.now().timestamp()), } + +def global_settings(request): + try: + settings = SystemSetting.objects.first() + if not settings: + settings = SystemSetting.objects.create() + return {'site_settings': settings} + except: + return {} diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..afecbe5 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,91 @@ +# Generated by Django 5.2.7 on 2026-02-02 06:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=100, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')), + ('address', models.TextField(blank=True, verbose_name='Address')), + ], + ), + migrations.CreateModel( + name='Supplier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('contact_person', models.CharField(blank=True, max_length=200, verbose_name='Contact Person')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=200, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=200, verbose_name='Name (Arabic)')), + ('sku', models.CharField(max_length=50, unique=True, verbose_name='SKU')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), + ('stock_quantity', models.PositiveIntegerField(default=0, verbose_name='Stock Quantity')), + ('image', models.URLField(blank=True, null=True, verbose_name='Product Image')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')), + ], + ), + migrations.CreateModel( + name='Sale', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('discount', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.customer')), + ], + ), + migrations.CreateModel( + name='SaleItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField()), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10)), + ('line_total', models.DecimalField(decimal_places=2, max_digits=12)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.sale')), + ], + ), + migrations.CreateModel( + name='Purchase', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('supplier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier')), + ], + ), + ] diff --git a/core/migrations/0002_systemsetting.py b/core/migrations/0002_systemsetting.py new file mode 100644 index 0000000..b73dcce --- /dev/null +++ b/core/migrations/0002_systemsetting.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-02 07:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SystemSetting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('business_name', models.CharField(default='Meezan Accounting', max_length=200, verbose_name='Business Name')), + ('address', models.TextField(blank=True, verbose_name='Address')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')), + ('currency_symbol', models.CharField(default='$', max_length=10, verbose_name='Currency Symbol')), + ('tax_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Tax Rate (%)')), + ('logo_url', models.URLField(blank=True, null=True, verbose_name='Logo URL')), + ], + ), + ] 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..c78be3d Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_systemsetting.cpython-311.pyc b/core/migrations/__pycache__/0002_systemsetting.cpython-311.pyc new file mode 100644 index 0000000..1c9f1b7 Binary files /dev/null and b/core/migrations/__pycache__/0002_systemsetting.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..5568852 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,77 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ -# Create your models here. +class Category(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=100) + name_ar = models.CharField(_("Name (Arabic)"), max_length=100) + slug = models.SlugField(unique=True) + + class Meta: + verbose_name_plural = _("Categories") + + def __str__(self): + return f"{self.name_en} / {self.name_ar}" + +class Product(models.Model): + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") + name_en = models.CharField(_("Name (English)"), max_length=200) + name_ar = models.CharField(_("Name (Arabic)"), max_length=200) + sku = models.CharField(_("SKU"), max_length=50, unique=True) + description = models.TextField(_("Description"), blank=True) + price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2) + stock_quantity = models.PositiveIntegerField(_("Stock Quantity"), default=0) + image = models.URLField(_("Product Image"), blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.name_en} ({self.sku})" + +class Customer(models.Model): + name = models.CharField(_("Name"), max_length=200) + phone = models.CharField(_("Phone"), max_length=20, blank=True) + email = models.EmailField(_("Email"), blank=True) + address = models.TextField(_("Address"), blank=True) + + def __str__(self): + return self.name + +class Supplier(models.Model): + name = models.CharField(_("Name"), max_length=200) + contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True) + phone = models.CharField(_("Phone"), max_length=20, blank=True) + + def __str__(self): + return self.name + +class Sale(models.Model): + customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True) + total_amount = models.DecimalField(max_digits=12, decimal_places=2) + discount = models.DecimalField(max_digits=12, decimal_places=2, default=0) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Sale #{self.id} - {self.total_amount}" + +class SaleItem(models.Model): + sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField() + unit_price = models.DecimalField(max_digits=10, decimal_places=2) + line_total = models.DecimalField(max_digits=12, decimal_places=2) + +class Purchase(models.Model): + supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True) + total_amount = models.DecimalField(max_digits=12, decimal_places=2) + created_at = models.DateTimeField(auto_now_add=True) + +class SystemSetting(models.Model): + business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting") + address = models.TextField(_("Address"), blank=True) + phone = models.CharField(_("Phone"), max_length=20, blank=True) + email = models.EmailField(_("Email"), blank=True) + currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="$") + tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0) + logo_url = models.URLField(_("Logo URL"), blank=True, null=True) + + def __str__(self): + return self.business_name diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..25a3a05 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,148 @@ - - - +{% load static i18n %}{% get_current_language as LANGUAGE_CODE %} +
- -